Skip to content

Commit

Permalink
Add types to make debug formatting easier
Browse files Browse the repository at this point in the history
This adds the types ArrayFormatter, ObjectFormatter and TupleFormatter,
all defined in the std.fmt module. These types make it easier to format
common objects, such as tuples, enums and regular objects. The API is
largely inspired by how Rust does things (e.g. using its DebugStruct
type).

The use of "finish" exists (similar to Rust) even though I wanted to use
a Drop implementation instead. The need for "finish" exists because in a
call chain (x.tuple(...).field(...)), the last method returns a
reference to the formatter type (e.g. TupleFormatter), but the owned
value it points to is dropped prior to returning that reference,
resulting in a drop error. Using "finish" we can work around that by not
having it return a reference.

Changelog: added
  • Loading branch information
yorickpeterse committed Aug 9, 2023
1 parent b0bbc43 commit 3b8534c
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 196 deletions.
11 changes: 3 additions & 8 deletions std/src/std/array.inko
Original file line number Diff line number Diff line change
Expand Up @@ -734,15 +734,10 @@ impl Hash for Array if T: Hash {

impl Format for Array if T: Format {
fn pub fmt(formatter: mut Formatter) {
formatter.write('[')
let fmt = formatter.array

iter.each_with_index fn (index, value) {
if index > 0 { formatter.write(', ') }

value.fmt(formatter)
}

formatter.write(']')
iter.each fn (value) { fmt.value(value) }
fmt.finish
}
}

Expand Down
11 changes: 3 additions & 8 deletions std/src/std/byte_array.inko
Original file line number Diff line number Diff line change
Expand Up @@ -543,14 +543,9 @@ impl Contains[Int] for ByteArray {

impl Format for ByteArray {
fn pub fmt(formatter: mut Formatter) {
formatter.write('[')
let fmt = formatter.array

iter.each_with_index fn (index, byte) {
if index > 0 { formatter.write(', ') }

formatter.write(byte.to_string)
}

formatter.write(']')
iter.each fn (byte) { fmt.value(byte) }
fmt.finish
}
}
10 changes: 6 additions & 4 deletions std/src/std/cmp.inko
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ impl Equal[Ordering] for Ordering {

impl Format for Ordering {
fn pub fmt(formatter: mut Formatter) {
match self {
case Less -> formatter.write('Less')
case Equal -> formatter.write('Equal')
case Greater -> formatter.write('Greater')
let name = match self {
case Less -> 'Less'
case Equal -> 'Equal'
case Greater -> 'Greater'
}

formatter.tuple(name).finish
}
}

Expand Down
162 changes: 156 additions & 6 deletions std/src/std/fmt.inko
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import std.string.(IntoString, StringBuffer)
# The value to use for objects if the nesting is too great.
let PLACEHOLDER = '...'

# The maximum number of objects to recurse into when formatting an object,
# before returning a placeholder.
let MAX_DEPTH = 16

# Formats a value using the default formatter.
#
# Examples
Expand All @@ -23,14 +19,111 @@ fn pub fmt[T: Format](value: ref T) -> String {
formatter.into_string
}

# A type for making it easy to format tuple-like values, such as tuples and
# enums.
class pub TupleFormatter {
let @formatter: mut Formatter
let @named: Bool
let @fields: Int

# Adds a new formatted field to the output.
fn pub mut field(value: ref Format) -> mut TupleFormatter {
match @fields {
case 0 -> @formatter.write('(')
case _ -> @formatter.write(', ')
}

@formatter.descend fn { value.fmt(@formatter) }
@fields += 1
self
}

# Finishes formatting the tuple.
#
# This method is used instead of a `Drop` implementation, otherwise a call
# chain (e.g. `x.tuple('').field(10)`) results in a drop error, as the final
# reference returned by `field` would outlive the `TupleFormatter`.
fn pub mut finish {
match @fields {
case 0 if @named -> {}
case 0 -> @formatter.write('()')
case _ -> @formatter.write(')')
}
}
}

# A type for making it easy to format array-like values.
class pub ArrayFormatter {
let @formatter: mut Formatter
let @fields: Int

# Adds a new formatted value to the output.
fn pub mut value(value: ref Format) -> mut ArrayFormatter {
if @fields > 0 { @formatter.write(', ') }

@formatter.descend fn { value.fmt(@formatter) }
@fields += 1
self
}

# Finishes formatting the tuple.
#
# This method is used instead of a `Drop` implementation, otherwise a call
# chain (e.g. `x.array.value(10)`) results in a drop error, as the final
# reference returned by `field` would outlive the `ArrayFormatter`.
fn pub mut finish {
@formatter.write(']')
}
}

# A type for making it easy to format regular objects.
class pub ObjectFormatter {
let @formatter: mut Formatter
let @named: Bool
let @fields: Int

# Adds a new formatted field to the output.
fn pub mut field(name: String, value: ref Format) -> mut ObjectFormatter {
let start = match @fields {
case 0 if @named -> ' {'
case 0 -> '{'
case _ -> ','
}

@formatter.write(start)
@formatter.write(' @')
@formatter.write(name)
@formatter.write(' = ')
@formatter.descend fn { value.fmt(@formatter) }
@fields += 1
self
}

# Finishes formatting the object.
#
# This method is used instead of a `Drop` implementation, otherwise a call
# chain (e.g. `x.object('A').field('foo', 10)`) results in a drop error, as
# the final reference returned by `field` would outlive the `ObjectFormatter`.
fn pub mut finish {
match @fields {
case 0 if @named -> {}
case 0 -> @formatter.write('{}')
case _ -> @formatter.write(' }')
}
}
}

# The default formatter to use when formatting an object.
class pub Formatter {
let @buffer: StringBuffer
let @nesting: Int

# The maximum object depth before object formatting stops.
let pub @maximum_depth: Int

# Returns a new `Formatter` with its default settings.
fn pub static new -> Formatter {
Formatter { @buffer = StringBuffer.new, @nesting = 0 }
Formatter { @buffer = StringBuffer.new, @nesting = 0, @maximum_depth = 10 }
}

# Writes the given `String` into the underlying buffer.
Expand All @@ -44,7 +137,7 @@ class pub Formatter {
# If nesting _is_ too great, a placeholder value is added to the buffer, and
# the supplied block is not executed.
fn pub mut descend(block: fn) {
if @nesting >= MAX_DEPTH {
if @nesting >= @maximum_depth {
write(PLACEHOLDER)
return
}
Expand All @@ -53,6 +146,63 @@ class pub Formatter {
block.call
@nesting -= 1
}

# Returns a `TupleFormatter` to make formatting tuple-like values easy.
#
# The `name` argument can be used as the type name of the value. When
# formatting actual tuples, this can be set to an empty `String` to omit
# adding a name.
#
# # Examples
#
# import std.fmt.Formatter
#
# let fmt = Formatter.new
#
# fmt.tuple('').field(10).field(20).finish
# fmt.into_string # => '(10, 20)'
fn pub mut tuple(name: String) -> TupleFormatter {
let named = name.size > 0

if named { write(name) }

TupleFormatter { @formatter = self, @named = named, @fields = 0 }
}

# Returns a `ArrayFormatter` to make formatting array-like values easy.
#
# # Examples
#
# import std.fmt.Formatter
#
# let fmt = Formatter.new
#
# fmt.array.value(10).value(20).finish
# fmt.into_string # => '[10, 20]'
fn pub mut array -> ArrayFormatter {
write('[')
ArrayFormatter { @formatter = self, @fields = 0 }
}

# Returns a `ObjectFormatter` to make formatting regular objects easy.
#
# The `name` argument can be used as the type name of the value.
#
# # Examples
#
# import std.fmt.Formatter
#
# let fmt = Formatter.new
#
# fmt.object('Person').field('name', 'Alice').field('age', 42).finish
# fmt.into_string # => 'Person { @name = "Alice", @age = 42 }'
fn pub mut object(name: String) -> ObjectFormatter {
let named = name.size > 0

if named { write(name) }

ObjectFormatter { @formatter = self, @named = named, @fields = 0 }
}
}

impl IntoString for Formatter {
Expand Down
19 changes: 7 additions & 12 deletions std/src/std/fs.inko
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ impl Equal[FileType] for FileType {

impl Format for FileType {
fn pub fmt(formatter: mut Formatter) {
let write = match self {
let name = match self {
case File -> 'File'
case Directory -> 'Directory'
case SymbolicLink -> 'SymbolicLink'
case Other -> 'Other'
}

formatter.write(write)
formatter.tuple(name).finish
}
}

Expand All @@ -63,15 +63,10 @@ impl Equal[DirectoryEntry] for DirectoryEntry {

impl Format for DirectoryEntry {
fn pub fmt(formatter: mut Formatter) {
formatter.write('DirectoryEntry { ')

formatter.descend fn {
formatter.write('@path = ')
@path.fmt(formatter)
formatter.write(', @type = ')
@type.fmt(formatter)
}

formatter.write(' }')
formatter
.object('DirectoryEntry')
.field('path', @path)
.field('type', @type)
.finish
}
}
8 changes: 3 additions & 5 deletions std/src/std/io.inko
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ impl ToString for Error {

impl Format for Error {
fn pub fmt(formatter: mut Formatter) {
let string = match self {
let name = match self {
case AddressInUse -> 'AddressInUse'
case AddressUnavailable -> 'AddressUnavailable'
case AlreadyConnected -> 'AlreadyConnected'
Expand Down Expand Up @@ -246,14 +246,12 @@ impl Format for Error {
case TimedOut -> 'TimedOut'
case WouldBlock -> 'WouldBlock'
case Other(code) -> {
formatter.write('Other(')
code.fmt(formatter)
formatter.write(')')
formatter.tuple('Other').field(code).finish
return
}
}

formatter.write(string)
formatter.tuple(name).finish
}
}

Expand Down
45 changes: 13 additions & 32 deletions std/src/std/json.inko
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,12 @@ impl Equal[Error] for Error {

impl FormatTrait for Error {
fn pub fmt(formatter: mut Formatter) {
formatter.write(to_string)
formatter
.object('Error')
.field('message', @message)
.field('line', @line)
.field('offset', @offset)
.finish
}
}

Expand Down Expand Up @@ -252,37 +257,13 @@ impl ToString for Json {
impl FormatTrait for Json {
fn pub fmt(formatter: mut Formatter) {
match self {
case Int(val) -> {
formatter.write('Int(')
val.fmt(formatter)
formatter.write(')')
}
case Float(val) -> {
formatter.write('Float(')
val.fmt(formatter)
formatter.write(')')
}
case String(val) -> {
formatter.write('String(')
val.fmt(formatter)
formatter.write(')')
}
case Array(val) -> {
formatter.write('Array(')
val.fmt(formatter)
formatter.write(')')
}
case Object(val) -> {
formatter.write('Object(')
val.fmt(formatter)
formatter.write(')')
}
case Bool(val) -> {
formatter.write('Bool(')
val.fmt(formatter)
formatter.write(')')
}
case Null -> formatter.write('Null')
case Int(val) -> formatter.tuple('Int').field(val).finish
case Float(val) -> formatter.tuple('Float').field(val).finish
case String(val) -> formatter.tuple('String').field(val).finish
case Array(val) -> formatter.tuple('Array').field(val).finish
case Object(val) -> formatter.tuple('Object').field(val).finish
case Bool(val) -> formatter.tuple('Bool').field(val).finish
case Null -> formatter.tuple('Null').finish
}
}
}
Expand Down
8 changes: 2 additions & 6 deletions std/src/std/option.inko
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,8 @@ impl Clone[Option[T]] for Option if T: Clone[T] {
impl Format for Option if T: Format {
fn pub fmt(formatter: mut Formatter) {
match self {
case Some(v) -> {
formatter.write('Some(')
v.fmt(formatter)
formatter.write(')')
}
case None -> formatter.write('None')
case Some(v) -> formatter.tuple('Some').field(v).finish
case None -> formatter.tuple('None').finish
}
}
}
Loading

0 comments on commit 3b8534c

Please sign in to comment.