diff --git a/Makefile b/Makefile index 5bd121f1..89c8bcfa 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,8 @@ spec.core: PHONY SAVI # Update deps for the specs for the core package. spec.core.deps: PHONY SAVI echo && $(SAVI) deps update --cd spec/core $(extra_args) + # TODO: Remove these temporary library patches: + sh -c 'cd spec/core/deps/github:savi-lang/Time/v0.20220513.0 && git apply ../../../../../../tmp-lib-patch-Time.patch' # Run the specs for the core package in lldb for debugging. spec.core.lldb: PHONY SAVI @@ -94,6 +96,8 @@ example.compile: PHONY SAVI # Update deps for the specs for the given example directory. example.deps: PHONY SAVI echo && $(SAVI) deps update --cd "$(dir)" $(extra_args) + # TODO: Remove these temporary library patches: + sh -c "cd $(dir)/deps/github:savi-lang/Time/v0.20220513.0 && git apply ../../../../../../../tmp-lib-patch-Time.patch" # Compile the vscode extension. vscode: PHONY SAVI diff --git a/core/Bytes.Format.savi b/core/Bytes.Format.savi index 5adf72c3..1e412061 100644 --- a/core/Bytes.Format.savi +++ b/core/Bytes.Format.savi @@ -18,22 +18,21 @@ :let _value Bytes'box :new box _new(@_value) - :fun into_string(out String'iso) String'iso + :fun into_string(out String'ref) None out.push_byte('b') out.push_byte('"') @_value.each -> (byte | case ( - | byte >= 0x7f | out = byte.format.hex.with_prefix("\\x").into_string(--out) + | byte >= 0x7f | byte.format.hex.with_prefix("\\x").into_string(out) | byte == '"' | out.push_byte('\\').push_byte('"') | byte >= 0x20 | out.push_byte(byte) | byte == '\n' | out.push_byte('\\').push_byte('n') | byte == '\r' | out.push_byte('\\').push_byte('r') | byte == '\t' | out.push_byte('\\').push_byte('t') - | out = byte.format.hex.with_prefix("\\x").into_string(--out) + | byte.format.hex.with_prefix("\\x").into_string(out) ) ) out.push_byte('"') - --out :fun into_string_space USize // Use a conservative estimate, assuming all bytes will be escaped hex. @@ -60,45 +59,42 @@ // It takes 68 bytes to show each row of the hex dump. @_row_count * 68 - :fun into_string(out String'iso) String'iso + :fun into_string(out String'ref) None @_row_count.times -> (row_index | row_address = row_index * 16 // Emit the address shown on the left. - out = row_address.u32.format.hex.bare.into_string(--out) + row_address.u32.format.hex.bare.into_string(out) out.push_byte(':') // Emit the hex view shown in the center. USize[8].times -> (pair_index | out.push_byte(' ') - out = @_emit_hex_pair(--out, row_address + pair_index * 2) + @_emit_hex_pair(out, row_address + pair_index * 2) ) out.push_byte(' ') // Emit the ASCII view shown on the right. USize[16].times -> (byte_index | - out = @_emit_ascii(--out, row_address + byte_index) + @_emit_ascii(out, row_address + byte_index) ) // Emit a final newline for this row. out.push_byte('\n') ) - --out - :fun _emit_hex_pair(out String'iso, pair_address USize) - out = @_emit_hex(--out, pair_address) - out = @_emit_hex(--out, pair_address + 1) - --out + :fun _emit_hex_pair(out String'ref, pair_address USize) None + @_emit_hex(out, pair_address) + @_emit_hex(out, pair_address + 1) - :fun _emit_hex(out String'iso, byte_address USize) + :fun _emit_hex(out String'ref, byte_address USize) None try ( - out = @_value[byte_address]!.format.hex.bare.into_string(--out) + @_value[byte_address]!.format.hex.bare.into_string(out) | out.push_byte(' ').push_byte(' ') ) - --out - :fun _emit_ascii(out String'iso, byte_address USize) + :fun _emit_ascii(out String'ref, byte_address USize) None try ( byte = @_value[byte_address]! if (byte > 0x20 && byte < 0x7f) ( @@ -109,4 +105,3 @@ | out.push_byte(' ') ) - --out diff --git a/core/FloatingPoint.Format.savi b/core/FloatingPoint.Format.savi index 71a965c1..060b144e 100644 --- a/core/FloatingPoint.Format.savi +++ b/core/FloatingPoint.Format.savi @@ -64,27 +64,6 @@ @is_zero = False @is_nan = False - :: Emit the represented value into the string, in the event that it is one - :: of the "special cases" (i.e. that it is not a finite non-zero value). - :: - :: If the value `is_finite_non_zero`, it will be yielded back to the caller, - :: who has a responsibility to emit as desired into the string and return it. - :fun _into_string_unless_finite_non_zero(out String'iso) String'iso - :yields String'iso for String'iso - case ( - | @is_finite_non_zero | - out = yield --out - | @is_zero | - if @is_negative out.push_byte('-') - out << "0.0" - | @is_nan | - out << "NaN" - | - if @is_negative out.push_byte('-') - out << "Infinity" - ) - --out - :: Return the maxmium number of bytes that may be needed to emit the :: stored value into a string buffer, in the event that it is one :: of the "special cases" (i.e. that it is not a finite non-zero value). @@ -185,11 +164,11 @@ FloatingPoint.Format.WithoutExponent(T)._new(@_value).into_string_space ) - :fun into_string(out String'iso) String'iso + :fun into_string(out String'ref) None if (@_value.power_of_10 > 2 || @_value.scientific_exponent < -3) ( - FloatingPoint.Format.Scientific(T)._new(@_value).into_string(--out) + FloatingPoint.Format.Scientific(T)._new(@_value).into_string(out) | - FloatingPoint.Format.WithoutExponent(T)._new(@_value).into_string(--out) + FloatingPoint.Format.WithoutExponent(T)._new(@_value).into_string(out) ) :: Format the given floating-point with an exponent (i.e. scientific notation). @@ -222,10 +201,10 @@ + 1 + @_value.scientific_exponent.format.decimal.into_string_space - :fun into_string(out String'iso) String'iso + :fun into_string(out String'ref) None // First, deal with any special cases (zero or non-finite numbers). // We print these in a special (hard-coded) way and return early. - try (out << @_value._special_case_as_string!, return --out) + try (out << @_value._special_case_as_string!, return) significand = @_value.significand digit_count = @_value.digit_count @@ -246,11 +225,9 @@ // Print the base-10 exponent suffix. if exponent.is_nonzero ( out.push_byte('e') - out = exponent.into_string(--out) + exponent.into_string(out) ) - --out - :: Format the given floating-point with no exponent (no scientific notation). :struct val FloatingPoint.Format.WithoutExponent(T FloatingPoint(T)'val) :is IntoString @@ -282,10 +259,10 @@ byte_count - :fun into_string(out String'iso) String'iso + :fun into_string(out String'ref) None // First, deal with any special cases (zero or non-finite numbers). // We print these in a special (hard-coded) way and return early. - try (out << @_value._special_case_as_string!, return --out) + try (out << @_value._special_case_as_string!, return) significand = @_value.significand digit_count = @_value.digit_count @@ -320,5 +297,3 @@ out.push_byte('.') out.push_byte('0') ) - - --out diff --git a/core/FloatingPoint.savi b/core/FloatingPoint.savi index 117e8d27..edf4a63f 100644 --- a/core/FloatingPoint.savi +++ b/core/FloatingPoint.savi @@ -161,7 +161,7 @@ FloatingPoint.Format(F64)._new(_FormattableF64.from_f64(@as_val.f64)) :is IntoString - :fun into_string(out String'iso): @format.shortest.into_string(--out) + :fun into_string(out String'ref): @format.shortest.into_string(out) :fun into_string_space: @format.shortest.into_string_space :: This trait isn't meant to be used externally. It's just a base implementation diff --git a/core/Inspect.savi b/core/Inspect.savi index d0c006cb..c3930125 100644 --- a/core/Inspect.savi +++ b/core/Inspect.savi @@ -4,26 +4,27 @@ :fun "[]!"(index USize) Any'box // TODO: use `box` instead of `Any'box` :trait box _InspectCustom - :fun inspect_into(output String'iso) String'iso + :fun inspect_into(output String'ref) None // TODO: Move this out of savi maybe? Does that make sense? // TODO: Make this into a trait with "implement for"/typeclass style polymorphism :module Inspect :fun "[]"(input Any'box) String'val // TODO: use `box` instead of `Any'box` - @into(String.new_iso, input) + output = String.new + @into(output, input) + output.take_buffer :fun out(input Any'box) // TODO: use `box` instead of `Any'box` _FFI.puts(@[input].cstring) - :fun into(output String'iso, input Any'box) String'iso // TODO: use `box` instead of `Any'box` // TODO: use something like Crystal IO instead of String? + :fun into(output String'ref, input Any'box) None // TODO: use `box` instead of `Any'box` case input <: ( - | _InspectCustom | input.inspect_into(--output) - | Bytes'box | input.format.literal.into_string(--output) + | _InspectCustom | input.inspect_into(output) + | Bytes'box | input.format.literal.into_string(output) | String'box | output.push_byte('"') output << input.clone // TODO: show some characters as escaped. output.push_byte('"') - --output | _InspectEach | output.push_byte('[') index USize = 0 @@ -31,18 +32,16 @@ if (index > 0) (output.push_byte(','), output.push_byte(' ')) try ( element = input[index]! - output = @into(--output, element) + @into(output, element) ) index += 1 ) output.push_byte(']') - --output | IntoString | // If there's nothing more specific, then our last option is to print // the same representation that `into_string` gives for that value. - input.into_string(--output) + input.into_string(output) | // Otherwise, fall back to just printing the name of the type. output << reflection_of_runtime_type_name input - --output ) diff --git a/core/Integer.Format.savi b/core/Integer.Format.savi index c38b48d7..3da1e032 100644 --- a/core/Integer.Format.savi +++ b/core/Integer.Format.savi @@ -31,11 +31,11 @@ ) @_bcd = U64.BCD.new(value.u64) - :fun into_string(out String'iso) String'iso + :fun into_string(out String'ref) None if @_is_negative ( out.push_byte('-') ) - @_bcd.into_string(--out) + @_bcd.into_string(out) :fun into_string_space USize @_bcd.into_string_space + if @_is_negative (1 | 0) @@ -80,7 +80,7 @@ // TODO: different strategy when `_has_leading_zeros` is False. @_prefix.size + if (T.bit_width == 1) (1 | T.bit_width.usize / 4) - :fun into_string(out String'iso) String'iso + :fun into_string(out String'ref) None zeros = @_has_leading_zeros out << @_prefix case T.bit_width == ( @@ -121,7 +121,6 @@ digit = @_digit(4), if (zeros || digit != '0') (zeros = True, out.push_byte(digit)) digit = @_digit(0), out.push_byte(digit) ) - --out :fun _digit(shr) u4 = @_value.bit_shr(shr).u8.bit_and(0xf) @@ -158,7 +157,7 @@ // TODO: different strategy when `_has_leading_zeros` is False. @_prefix.size + T.bit_width.usize - :fun into_string(out String'iso) String'iso + :fun into_string(out String'ref) None show_zeros = @_has_leading_zeros out << @_prefix @@ -179,8 +178,6 @@ // If we haven't seen any zeros or ones yet, show at least one zero if !show_zeros out.push_byte('0') - --out - :: Format the given integer as a Unicode codepoint. :struct val Integer.Format.Unicode(T Integer(T)'val) :is IntoString @@ -189,9 +186,8 @@ :new val _new(@_value) - :fun into_string(out String'iso) String'iso + :fun into_string(out String'ref) None out.push_utf8(@_value.u32) - --out :fun into_string_space USize // This is only a rough guess @@ -207,7 +203,7 @@ :let _value U32 :new val _new(value T): @_value = value.u32 - :fun into_string(out String'iso) String'iso + :fun into_string(out String'ref) None case ( | @_value < 127 && @_value >= 32 | case @_value == ( @@ -225,19 +221,18 @@ | '\n' | out.push_byte('\\'), out.push_byte('n') | out.push_byte('\\'), out.push_byte('x') - out = @_value.u8.format.hex.bare.into_string(--out) + @_value.u8.format.hex.bare.into_string(out) ) | @_value <= 0xff | out.push_byte('\\'), out.push_byte('x') - out = @_value.u8.format.hex.bare.into_string(--out) + @_value.u8.format.hex.bare.into_string(out) | @_value <= 0xffff | out.push_byte('\\'), out.push_byte('u') - out = @_value.u16.format.hex.bare.into_string(--out) + @_value.u16.format.hex.bare.into_string(out) | out.push_byte('\\'), out.push_byte('U') - out = @_value.format.hex.bare.into_string(--out) + @_value.format.hex.bare.into_string(out) ) - --out :fun into_string_space USize case ( diff --git a/core/Integer.savi b/core/Integer.savi index bbd3e1c4..2e6307bf 100644 --- a/core/Integer.savi +++ b/core/Integer.savi @@ -293,7 +293,7 @@ :fun format: Integer.Format(T)._new(@as_val) :is IntoString - :fun into_string(out String'iso): @format.decimal.into_string(--out) + :fun into_string(out String'ref): @format.decimal.into_string(out) :fun into_string_space: @format.decimal.into_string_space :: This trait isn't meant to be used externally. It's just a base implementation @@ -423,5 +423,5 @@ :fun member_name String :is IntoString - :fun into_string(out String'iso): @member_name.into_string(--out) + :fun into_string(out String'ref): @member_name.into_string(out) :fun into_string_space: @member_name.into_string_space diff --git a/core/IntoString.savi b/core/IntoString.savi index 1ab5f013..98715716 100644 --- a/core/IntoString.savi +++ b/core/IntoString.savi @@ -3,9 +3,11 @@ :: These methods are used by string interpolation syntax, so ensuring that :: a type implements this trait will make it directly usable in interpolation. :trait box IntoString - :: Emit a representation of this value into the given String, preserving - :: its isolation and returning the String with the new content appended. - :fun box into_string(out String'iso) String'iso + :: Emit a representation of this value into the given `String`. + :: + :: This method is expected by convention to append some bytes into the + :: `String` but not to modify any earlier portion of the `String`. + :fun box into_string(out String'ref) None :: Return a conservative estimate for how much many bytes are required to hold :: the string representation of this value when emitted with `into_string`. diff --git a/core/None.savi b/core/None.savi index 182fac30..e78fc09a 100644 --- a/core/None.savi +++ b/core/None.savi @@ -2,8 +2,8 @@ :is IntoString // TODO: These shouldn't need to be `:fun box` - `:fun non` should be okay. :: When emitting into a string, emit nothing (i.e. an empty string). - :fun box into_string(out String'iso): --out + :fun box into_string(out String'ref): None :fun box into_string_space USize: 0 :: When inspecting, print explicitly using the name `None`. - :fun box inspect_into(out String'iso): out << "None", --out + :fun box inspect_into(out String'ref) None: out << "None" diff --git a/core/String.savi b/core/String.savi index 9219543f..89c9ed7c 100644 --- a/core/String.savi +++ b/core/String.savi @@ -58,14 +58,13 @@ :fun into_string_space: @space - :fun into_string(out String'iso) String'iso + :fun into_string(out String'ref) None if (@_size > 0) ( new_size = out._size + @_size out.reserve(new_size) out._clone_from(@_ptr._unsafe_val, @_size, out._size) out._size = new_size ) - --out :fun "=="(other String'box) (@_size == other._size) && (@_ptr._compare(other._ptr, @_size) == 0) diff --git a/core/U64.BCD.savi b/core/U64.BCD.savi index 47d8b7c6..e62e062d 100644 --- a/core/U64.BCD.savi +++ b/core/U64.BCD.savi @@ -1,5 +1,5 @@ :: Binary Coded Decimal (BCD) representation of an U64 integer. -:: +:: :: Each digit is represented by a nibble (4 bits), requiring :: a total of 80 bits for up to 20 digits of the U64. :: Digits are stored in reverse order, so the the decimal `123` @@ -20,7 +20,7 @@ :let _low U64 :new (u64 U64) - high U64 = 0 + high U64 = 0 low U64 = 0xF ndigits U64 = 0 while True ( @@ -69,7 +69,7 @@ ) :fun box each_digit - :yields U8 for None + :yields U8 for None high = @_high low = @_low while (low.bit_and(0xF) != 0xF) ( @@ -81,10 +81,7 @@ :fun box into_string_space USize @ndigits - :fun box into_string(out String'iso) String'iso + :fun box into_string(out String'ref) None @each_digit -> (digit | out.push_byte('0'.u8 + digit) ) - - --out - diff --git a/src/savi/compiler/macros.cr b/src/savi/compiler/macros.cr index 621f3902..87142470 100644 --- a/src/savi/compiler/macros.cr +++ b/src/savi/compiler/macros.cr @@ -305,28 +305,43 @@ class Savi::Compiler::Macros < Savi::AST::CopyOnMutateVisitor ).from(node) } - # Call each part's `into_string` method, passing the result of the previous - # part as the argument. The argument for the first call is `String.new_iso` - # with the space expression as its argument to allocate the requested space. - # The hope is that if the correct amount of space was requested, we won't - # need to re-allocate the string as its size grows (if its `space >= size`). - string_expr = AST::Call.new( + # Allocate the new string to hold the specified amount of space, and assign + # it to a local variable with a hygienic name. + new_string_expr = AST::Call.new( AST::Identifier.new("String").from(node), - AST::Identifier.new("new_iso").from(node), + AST::Identifier.new("new").from(node), AST::Group.new("(", [space_expr] of AST::Node).from(node), ).from(node) + + # Assign the string value to a new local variable (with a hygienic name). + # We'll use this name to pass it to the `into_string` method of each part, + # and then once at the end to refer to the final value of the built string. + local_name = next_local_name + local = AST::Identifier.new(local_name).from(node) + local_assign = AST::Relate.new( + local, + AST::Operator.new("=").from(new_string_expr), + new_string_expr, + ).from(node) + sequence.terms << local_assign + + # Call each part's `into_string` method. parts.each { |part| - string_expr = AST::Call.new( + sequence.terms << AST::Call.new( part, AST::Identifier.new("into_string").from(part), - AST::Group.new("(", [string_expr] of AST::Node).from(part), + AST::Group.new("(", [local] of AST::Node).from(part), ).from(part) } - # Add the string expression to the sequence of terms. - # This is the final expression in the sequence, so it will - # be the result of the sequence when we return the sequence. - sequence.terms << string_expr + # Take the underlying buffer from the local variable holding the string, + # to obtain an isolated string. This is is the final expression in the + # sequence, so it will be the result of the sequence when it executes. + sequence.terms << AST::Call.new( + local, + AST::Identifier.new("take_buffer").from(local), + ).from(local) + sequence rescue exc : Exception diff --git a/tmp-lib-patch-Time.patch b/tmp-lib-patch-Time.patch new file mode 100644 index 00000000..c6291ccc --- /dev/null +++ b/tmp-lib-patch-Time.patch @@ -0,0 +1,91 @@ +diff --git a/src/Time.Duration.savi b/src/Time.Duration.savi +index 7da2d5b..e427278 100644 +--- a/src/Time.Duration.savi ++++ b/src/Time.Duration.savi +@@ -166,7 +166,7 @@ + ) + + :: Print the duration for human inspection (the format is subject to change). +- :fun inspect_into(output String'iso) ++ :fun inspect_into(output String'ref) None + weeks_result = @total_weeks_with_remainder + days_result = weeks_result.tail.total_days_with_remainder + hours_result = days_result.tail.total_hours_with_remainder +@@ -183,42 +183,41 @@ + output << "Time.Duration(" + + if (weeks > 0) ( +- output = Inspect.into(--output, weeks), output << " weeks" ++ Inspect.into(output, weeks), output << " weeks" + printed_anything = True + ) + + if (days > 0) ( + if printed_anything (output << ", ") +- output = Inspect.into(--output, days), output << " days" ++ Inspect.into(output, days), output << " days" + printed_anything = True + ) + + if (hours > 0) ( + if printed_anything (output << ", ") +- output = Inspect.into(--output, hours), output << " hours" ++ Inspect.into(output, hours), output << " hours" + printed_anything = True + ) + + if (minutes > 0) ( + if printed_anything (output << ", ") +- output = Inspect.into(--output, minutes), output << " minutes" ++ Inspect.into(output, minutes), output << " minutes" + printed_anything = True + ) + + if (seconds > 0) ( + if printed_anything (output << ", ") +- output = Inspect.into(--output, seconds), output << " seconds" ++ Inspect.into(output, seconds), output << " seconds" + printed_anything = True + ) + + if (nanoseconds > 0) ( + if printed_anything (output << ", ") +- output = Inspect.into(--output, nanoseconds), output << " nanoseconds" ++ Inspect.into(output, nanoseconds), output << " nanoseconds" + printed_anything = True + ) + + output << ")" +- --output + + :: Return True if the given duration is exactly equivalent to this one. + :fun "=="(other Time.Duration'box) +diff --git a/src/Time.savi b/src/Time.savi +index 2084a65..06840cc 100644 +--- a/src/Time.savi ++++ b/src/Time.savi +@@ -178,16 +178,15 @@ + (@total_seconds % @_seconds_per_minute).u8 + + :: Print the time data for human inspection (the format is subject to change). +- :fun inspect_into(output String'iso) ++ :fun inspect_into(output String'ref) None + // TODO: Properly pad numbers with zeros for a constant string width. +- output = Inspect.into(--output, @year), output.push_byte('-') +- output = Inspect.into(--output, @month), output.push_byte('-') +- output = Inspect.into(--output, @day), output.push_byte(' ') +- output = Inspect.into(--output, @hour), output.push_byte(':') +- output = Inspect.into(--output, @minute), output.push_byte(':') +- output = Inspect.into(--output, @second), output.push_byte('\'') +- output = Inspect.into(--output, @nanosecond) +- --output ++ Inspect.into(output, @year), output.push_byte('-') ++ Inspect.into(output, @month), output.push_byte('-') ++ Inspect.into(output, @day), output.push_byte(' ') ++ Inspect.into(output, @hour), output.push_byte(':') ++ Inspect.into(output, @minute), output.push_byte(':') ++ Inspect.into(output, @second), output.push_byte('\'') ++ Inspect.into(output, @nanosecond) + + :: Returns a bit-packed representation of the year, month, and day, + :: for internal reuse by other functions that need this information.