diff --git a/compiler/test/input/memoryBase/asserts.gr b/compiler/test/input/memoryBase/asserts.gr index ccab0119b9..6d818aee8a 100644 --- a/compiler/test/input/memoryBase/asserts.gr +++ b/compiler/test/input/memoryBase/asserts.gr @@ -12,7 +12,7 @@ let doTest = () => { use WasmI32.{ (==), (>), (<) } assert typeMetadata() == 0x110008n assert heapStart() > 0x110008n - assert heapStart() < 0x110308n + assert heapStart() < 0x110808n } doTest() diff --git a/compiler/test/runner.re b/compiler/test/runner.re index 9c1ce28bd9..087ebce55e 100644 --- a/compiler/test/runner.re +++ b/compiler/test/runner.re @@ -519,11 +519,12 @@ let makeStdlibRunner = (test, ~code=0, name) => { }); }; -let makeRuntimeRunner = (test, ~code=0, name) => { +let makeRuntimeRunner = (test, ~code=0, ~elide_type_info=false, name) => { test(name, ({expect}) => { Config.preserve_all_configs(() => { // Run stdlib suites in release mode Config.profile := Some(Release); + Config.elide_type_info := elide_type_info; let infile = runtimefile(name); let outfile = wasmfile(name); ignore @@ compile_file(~link=true, infile, outfile); diff --git a/compiler/test/runtime/toString.test.gr b/compiler/test/runtime/toString.test.gr new file mode 100644 index 0000000000..297cd05a5b --- /dev/null +++ b/compiler/test/runtime/toString.test.gr @@ -0,0 +1,412 @@ +module ToStringTest + +from "array" include Array +from "string" include String +from "list" include List +from "runtime/toString" include ToString +use ToString.{ toString } + +// Short Values +module ShortValues { + assert toString('a') == "a" // Char + assert toString('\b') == "\b" // Char escape + assert toString('\f') == "\f" // Char escape + assert toString('\n') == "\n" // Char escape + assert toString('\r') == "\r" // Char escape + assert toString('\t') == "\t" // Char escape + assert toString('\v') == "\v" // Char escape + assert toString('\'') == "'" // Char escape + assert toString('"') == "\"" // Char no escape + assert toString('ñ') == "ñ" // Char emoji (2 bytes) + assert toString('☃') == "☃" // Char emoji (3 bytes) + assert toString('🌾') == "🌾" // Char emoji (4 bytes) + assert toString(-32s) == "-32" // Int8 + assert toString(0s) == "0" // Int8 + assert toString(32s) == "32" // Int8 + assert toString(-32S) == "-32" // Int16 + assert toString(0S) == "0" // Int16 + assert toString(32S) == "32" // Int16 + assert toString(0us) == "0" // UInt8 + assert toString(32us) == "32" // UInt8 + assert toString(0uS) == "0" // UInt16 + assert toString(32uS) == "32" // UInt16 +} +// Constants +module Constants { + assert toString(void) == "void" + assert toString(true) == "true" + assert toString(false) == "false" +} +// Number +module Number { + assert toString(-32) == "-32" // Simple Number + assert toString(0) == "0" // Simple Number + assert toString(32) == "32" // Simple Number + assert toString(-9_000_000_000_000_000_000) == "-9000000000000000000" // Int64 + assert toString(9_000_000_000_000_000_000) == "9000000000000000000" // Int64 + assert toString(-32.5) == "-32.5" // Float64 + assert toString(-32.0) == "-32.0" // Float64 + assert toString(-0.0) == "0.0" // Float64 + assert toString(0.0) == "0.0" // Float64 + assert toString(32.0) == "32.0" // Float64 + assert toString(32.5) == "32.5" // Float64 + assert toString(-NaN) == "NaN" // Float64 + assert toString(NaN) == "NaN" // Float64 + assert toString(-Infinity) == "-Infinity" // Float64 + assert toString(Infinity) == "Infinity" // Float64 + assert toString(1/2) == "1/2" // Rational + assert toString(-1/2) == "-1/2" // Rational + assert toString(20/80) == "1/4" // Rational + assert toString(4/4) == "1" // Rational + assert toString(-9_000_000_000_000_000_000_000) == "-9000000000000000000000" // BigInt + assert toString(9_000_000_000_000_000_000_000) == "9000000000000000000000" // BigInt +} +// Heap values +module HeapValues { + // Tuple + assert toString((1, 2, 3)) == "(1, 2, 3)" + assert toString( + ( + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + ) + ) + == "(\n 1,\n 2,\n 3,\n 4,\n 5,\n 6,\n 7,\n 8,\n 9,\n 10,\n 11,\n 12,\n 13,\n 14,\n 15,\n 16,\n 17,\n 18,\n 19,\n 20,\n 21,\n 22,\n 23,\n)" // Multiline + // Array + assert toString([>]) == "[>]" + assert toString([> 1, 2, 3]) == "[>1, 2, 3]" + assert toString( + [> + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + ] + ) + == "[>\n 1,\n 2,\n 3,\n 4,\n 5,\n 6,\n 7,\n 8,\n 9,\n 10,\n 11,\n 12,\n 13,\n 14,\n 15,\n 16,\n 17,\n 18,\n 19,\n 20,\n 21,\n 22,\n 23,\n]" // Multiline + // Record + record SingleRecord { + value: Number, + } + assert toString({ value: 1, }) == "{ value: 1, }" + record MultipleRecord { + value: Number, + value2: Number, + } + assert toString({ value: 1, value2: 2 }) == "{ value: 1, value2: 2 }" + record MultiRecord { + value: Number, + value2: Number, + value3: Number, + value4: Number, + value5: Number, + value6: Number, + value7: Number, + value8: Number, + } + assert toString( + { + value: 1, + value2: 2, + value3: 3, + value4: 4, + value5: 5, + value6: 6, + value7: 7, + value8: 8, + } + ) + == "{ \n value: 1,\n value2: 2,\n value3: 3,\n value4: 4,\n value5: 5,\n value6: 6,\n value7: 7,\n value8: 8, \n}" + record NestedRecord1 { + inner: Number, + } + record NestedRecord2 { + top: Number, + nested: NestedRecord1, + } + assert toString({ top: 1, nested: { inner: 2, } }) + == "{ top: 1, nested: { inner: 2, } }" + // ADT + enum TestEnum { + SimpleEnum, + TupleEnum(Number), + TupleEnum2(Number, Number), + RecordEnum{ value: Number, }, + RecordEnum2{ value: Number, value2: Number }, + } + assert toString(SimpleEnum) == "SimpleEnum" // No Data + assert toString(TupleEnum(1)) == "TupleEnum(1)" // Single Data + assert toString(TupleEnum2(1, 2)) == "TupleEnum2(1, 2)" // Multiple Data + assert toString(RecordEnum{ value: 1 }) == "RecordEnum{ value: 1, }" // Record Data + assert toString(RecordEnum2{ value: 1, value2: 2 }) + == "RecordEnum2{ value: 1, value2: 2 }" // Record Multiple Data + provide enum PrintableADT { + PrintableFoo, + PrintableBar, + PrintableBaz(String), + PrintableQux(Number, String, Bool), + PrintableQuux, + PrintableFlip(String), + } + assert toString(PrintableFoo) == "PrintableFoo" + assert toString(PrintableBar) == "PrintableBar" + assert toString(PrintableBaz("baz")) == "PrintableBaz(\"baz\")" + assert toString(PrintableQux(5, "qux", false)) + == "PrintableQux(5, \"qux\", false)" + assert toString(PrintableQuux) == "PrintableQuux" + assert toString(PrintableFlip("flip")) == "PrintableFlip(\"flip\")" + // Exception - subcase of ADT + exception TestException + assert toString(TestException) == "TestException" + exception TupleException(Number, Number) + assert toString(TupleException(1, 2)) == "TupleException(1, 2)" + exception RecordException{ value: Number, } + assert toString(RecordException{ value: 1 }) == "RecordException{ value: 1, }" + // Lambda + assert toString(() => void) == "" + assert toString((a, b, c) => void) == "" + // String + assert toString("a") == "a" // Short + assert toString("test") == "test" // Long + assert toString("test\t") == "test\t" // Special + assert toString("test\"") == "test\"" // Double Quote + assert toString("test\'") == "test\'" // Single Quote + // Bytes + assert toString(b"") == "" + assert toString(b"test") == "" + assert toString( + b"123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789" + ) + == "" + // Int64 + assert toString(-9_000_000_000_000_000_000L) == "-9000000000000000000" + assert toString(-32L) == "-32" + assert toString(0L) == "0" + assert toString(32L) == "32" + assert toString(9_000_000_000_000_000_000L) == "9000000000000000000" + // Float64 + assert toString(-32.5d) == "-32.5" + assert toString(-32.0d) == "-32.0" + assert toString(-0.0d) == "0.0" + assert toString(0.0d) == "0.0" + assert toString(32.0d) == "32.0" + assert toString(32.5d) == "32.5" + assert toString(-NaNd) == "NaN" + assert toString(NaNd) == "NaN" + assert toString(-Infinityd) == "-Infinity" + assert toString(Infinityd) == "Infinity" + // Rational + assert toString(1/2r) == "1/2" + assert toString(-1/2r) == "-1/2" + assert toString(20/80r) == "1/4" + assert toString(4/4r) == "1/1" + // BigInt + assert toString(-32t) == "-32" + assert toString(0t) == "0" + assert toString(32t) == "32" + assert toString(-9_000_000_000_000_000_000t) == "-9000000000000000000" + assert toString(9_000_000_000_000_000_000t) == "9000000000000000000" + // Int32 + assert toString(-32l) == "-32" + assert toString(0l) == "0" + assert toString(32l) == "32" + // Float32 + assert toString(-32.5f) == "-32.5" + assert toString(-32.0f) == "-32.0" + assert toString(-0.0f) == "0.0" + assert toString(0.0f) == "0.0" + assert toString(32.0f) == "32.0" + assert toString(32.5f) == "32.5" + assert toString(-NaNf) == "NaN" + assert toString(NaNf) == "NaN" + assert toString(-Infinityf) == "-Infinity" + assert toString(Infinityf) == "Infinity" + // Uint32 + assert toString(0ul) == "0" + assert toString(32ul) == "32" + // Uint64 + assert toString(0uL) == "0" + assert toString(32uL) == "32" +} +// Complex Heap Values -- nested, recursive, mixed, nested escaping char, string, '\'', '\t', etc... +assert toString(box('a')) == "box('a')" // Nested Regular Char +assert toString(box('\n')) == "box('\\n')" // Nested Special Char +assert toString(box("test")) == "box(\"test\")" // Box Nested Regular String +assert toString(box("test'\b\f\n\r\t\v\\\"")) + == "box(\"test'\\b\\f\\n\\r\\t\\v\\\\\\\"\")" // Box Nested Special String +record RecordNestedStringChar { + charValue: Char, + stringValue: String, +} +assert toString({ charValue: '\'', stringValue: "test'\b\f\n\r\t\v\\\"" }) + == "{ charValue: '\\'', stringValue: \"test'\\b\\f\\n\\r\\t\\v\\\\\\\"\" }" // Record Nested Special String and Char +assert toString(box(('a', "test", 32))) == "box(('a', \"test\", 32))" // Nested Mixed Tuple +assert toString(box(box(32))) == "box(box(32))" // Nested Box +assert toString(box(box(box([1, 2, 3])))) == "box(box(box([1, 2, 3])))" // Deeply Nested Box +assert toString(box(box(box(box(box(box(42))))))) + == "box(box(box(box(box(box(42))))))" // Very Deeply Nested Box +assert toString( + box( + box( + box( + box( + box( + box(box(box(box(box(box(box(box(box(box(box(box(box(42))))))))))))) + ) + ) + ) + ) + ) +) + == "box(\n box(\n box(\n box(\n box(box(box(box(box(box(box(box(box(box(box(box(box(box(42)))))))))))))),\n ),\n ),\n ),\n)" // Very Very Deeply Nested Box +// Cycles +record rec Cycle1 { + mut a: Option, +} +let cycle1 = { a: None, } +cycle1.a = Some(cycle1) +assert toString(cycle1) == "<1> { a: Some(>), }" +record rec Cycle2 { + val: Number, + mut a: Option, +} +let cycle2A = { val: 1, a: None } +let cycle2B = { val: 2, a: None } +let cycle1AOpt = Some(cycle2A) +let cycle2BOpt = Some(cycle2B) +cycle2A.a = cycle2BOpt +cycle2B.a = cycle1AOpt +assert toString([cycle2A, cycle2B]) + == "[\n <1> { val: 1, a: Some({ val: 2, a: Some(>) }) },\n <2> { val: 2, a: Some(<1> { val: 1, a: Some(>) }) },\n]" +record rec Cycle3 { + val: Number, + mut next: Option, +} + +let cycle3A = { val: 1, next: None } +let cycle3AOpt = Some(cycle3A) +let cycle3B = Some({ val: 2, next: cycle3AOpt }) +let cycle3C = Some({ val: 3, next: cycle3B }) +let cycle3D = Some({ val: 4, next: cycle3C }) +let cycle3E = Some({ val: 5, next: cycle3D }) +cycle3A.next = cycle3E +assert toString([cycle3AOpt, cycle3B, cycle3C, cycle3D, cycle3E]) + == "[\n <1> Some(\n { \n val: 1,\n next: Some(\n { \n val: 5,\n next: Some(\n { \n val: 4,\n next: Some(\n { val: 3, next: Some({ val: 2, next: > }) },\n ), \n },\n ), \n },\n ), \n },\n ),\n <2> Some(\n { \n val: 2,\n next: <1> Some(\n { \n val: 1,\n next: Some(\n { \n val: 5,\n next: Some(\n { val: 4, next: Some({ val: 3, next: > }) },\n ), \n },\n ), \n },\n ), \n },\n ),\n <3> Some(\n { \n val: 3,\n next: <2> Some(\n { \n val: 2,\n next: <1> Some(\n { \n val: 1,\n next: Some(\n { val: 5, next: Some({ val: 4, next: > }) },\n ), \n },\n ), \n },\n ), \n },\n ),\n <4> Some(\n { \n val: 4,\n next: <3> Some(\n { \n val: 3,\n next: <2> Some(\n { \n val: 2,\n next: <1> Some(\n { val: 1, next: Some({ val: 5, next: > }) },\n ), \n },\n ), \n },\n ), \n },\n ),\n <5> Some(\n { \n val: 5,\n next: <4> Some(\n { \n val: 4,\n next: <3> Some(\n { \n val: 3,\n next: <2> Some(\n { val: 2, next: <1> Some({ val: 1, next: > }) },\n ), \n },\n ), \n },\n ), \n },\n ),\n]" +enum rec Cycle4 { + Rec4(Box>), +} +let cycle4A = box(None) +let cycle4B = Rec4(cycle4A) +cycle4A := Some(cycle4B) +assert toString(cycle4A) == "<1> box(Some(Rec4(>)))" +assert toString(unbox(cycle4A)) == "<1> Some(Rec4(box(>)))" +enum rec Cycle5 { + Rec5(Array>), +} +let cycle5A = [> None] +let cycle5B = Rec5(cycle5A) +cycle5A[0] = Some(cycle5B) +assert toString(cycle5A) == "<1> [>Some(Rec5(>))]" +// Max Depth +record rec Nested { + next: Option, +} +let rec constructNested = (depth, acc: Nested) => { + if (depth <= 0) acc else constructNested(depth - 1, { next: Some(acc), }) +} +let nested = constructNested(33, { next: None, }) +assert toString(nested) + == "{ \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n { \n next: Some(\n ,\n ), \n },\n ), \n },\n ), \n },\n ), \n },\n ), \n },\n ), \n },\n ), \n },\n ), \n },\n ), \n },\n ), \n },\n ), \n },\n ), \n },\n ), \n },\n ), \n },\n ), \n },\n ), \n}" + +// Builtins +module Builtins { + // List + assert toString([1, 2, 3]) == "[1, 2, 3]" + assert toString( + [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + ] + ) + == "[\n 1,\n 2,\n 3,\n 4,\n 5,\n 6,\n 7,\n 8,\n 9,\n 10,\n 11,\n 12,\n 13,\n 14,\n 15,\n 16,\n 17,\n 18,\n 19,\n 20,\n 21,\n 22,\n 23,\n]" // Multiline + // Range + assert toString({ rangeStart: 1, rangeEnd: 2 }) + == "{ rangeStart: 1, rangeEnd: 2 }" + // Option + assert toString(None) == "None" + assert toString(Some(1)) == "Some(1)" + // Result + assert toString(Ok(1)) == "Ok(1)" + assert toString(Err(2)) == "Err(2)" + // Box + assert toString(box(1)) == "box(1)" +} + +// Large Iterables +module LargeIterables { + // Large list + assert String.length(toString(List.init(100_000, i => i + 1))) == 888898 + // Large array + assert String.length(toString(Array.init(100_000, i => i + 1))) == 888899 +} diff --git a/compiler/test/runtime/toStringNoTypeInfo.test.gr b/compiler/test/runtime/toStringNoTypeInfo.test.gr new file mode 100644 index 0000000000..f8bb8c8f96 --- /dev/null +++ b/compiler/test/runtime/toStringNoTypeInfo.test.gr @@ -0,0 +1,35 @@ +module ToStringNoTypeInfoTest + +from "runtime/toString" include ToString +use ToString.{ toString } + +record Record_ { + value: Number, +} +assert toString({ value: 1, }) == "{ : 1, }" +enum Enum { + Empty, + Tuple(Number), + Record{ value: Number, }, +} +assert toString(Empty) == "" +assert toString(Tuple(1)) == "(1)" +// NOTE: Records are printed as tuples when type info is elided +assert toString(Record{ value: 1 }) == "(1)" + +// Builtins +module Builtins { + // List + assert toString([1, 2, 3]) == "[1, 2, 3]" + // Range + assert toString({ rangeStart: 1, rangeEnd: 2 }) + == "{ rangeStart: 1, rangeEnd: 2 }" + // Option + assert toString(None) == "None" + assert toString(Some(1)) == "Some(1)" + // Result + assert toString(Ok(1)) == "Ok(1)" + assert toString(Err(2)) == "Err(2)" + // Box + assert toString(box(1)) == "box(1)" +} diff --git a/compiler/test/suites/arrays.re b/compiler/test/suites/arrays.re index bb4c8387ba..9be9d20d12 100644 --- a/compiler/test/suites/arrays.re +++ b/compiler/test/suites/arrays.re @@ -18,8 +18,8 @@ describe("arrays", ({test, testSkip}) => { let assertParse = makeParseRunner(test); let assertWarning = makeWarningRunner(test); - assertRun("array1", "print([> 1, 2, 3])", "[> 1, 2, 3]\n"); - assertRun("array2", "print([>])", "[> ]\n"); + assertRun("array1", "print([> 1, 2, 3])", "[>1, 2, 3]\n"); + assertRun("array2", "print([>])", "[>]\n"); assertSnapshot("array3", "[>\n1, 2, 3]"); assertCompileError("array_error", "[> 1, false, 2]", "has type Bool but"); assertSnapshot("array_access", "let x = [> 1, 2, 3]; x[0]"); @@ -71,22 +71,22 @@ describe("arrays", ({test, testSkip}) => { assertRun( "array_set", "let x = [> 1, 2, 3]; x[0] = 4; print(x)", - "[> 4, 2, 3]\n", + "[>4, 2, 3]\n", ); assertRun( "array_set2", "let x = [> 1, 2, 3]; x[-2] = 4; print(x)", - "[> 1, 4, 3]\n", + "[>1, 4, 3]\n", ); assertRun( "array_set3", "let x = [> 1, 2, 3]; x[0] += 1; print(x)", - "[> 2, 2, 3]\n", + "[>2, 2, 3]\n", ); assertRun( "array_set4", "let x = [> 1, 2, 3]; let mut c = 0; let getC = () => {c += 1; c}; x[getC()] += 1; print(x)", - "[> 1, 3, 3]\n", + "[>1, 3, 3]\n", ); assertCompileError( "array_set_err", diff --git a/compiler/test/suites/basic_functionality.re b/compiler/test/suites/basic_functionality.re index 4675ea523c..1e22f83df5 100644 --- a/compiler/test/suites/basic_functionality.re +++ b/compiler/test/suites/basic_functionality.re @@ -485,6 +485,6 @@ describe("basic functionality", ({test, testSkip}) => { ~config_fn=smallestFileConfig, "smallest_grain_program", "", - 329, + 410, ); }); diff --git a/compiler/test/suites/cycles.re b/compiler/test/suites/cycles.re index 3b41432b62..305f31d9b7 100644 --- a/compiler/test/suites/cycles.re +++ b/compiler/test/suites/cycles.re @@ -17,7 +17,7 @@ describe("cyclic references", ({test, testSkip}) => { x.a = Some(x) print(x) |}, - "<1> {\n a: Some(>)\n}\n", + "<1> { a: Some(>), }\n", ); assertRun( "cycles2", @@ -36,7 +36,7 @@ describe("cyclic references", ({test, testSkip}) => { print([x, y]) |}, - "[<1> {\n val: 1,\n a: Some({\n val: 2,\n a: Some(>)\n })\n}, <2> {\n val: 2,\n a: Some(<1> {\n val: 1,\n a: Some(>)\n })\n}]\n", + "[\n <1> { val: 1, a: Some({ val: 2, a: Some(>) }) },\n <2> { val: 2, a: Some(<1> { val: 1, a: Some(>) }) },\n]\n", ); assertRun( "cycles3", @@ -55,83 +55,7 @@ describe("cyclic references", ({test, testSkip}) => { a.next = e print([aOpt, b, c, d, e]) |}, - {|[Some(<1> { - val: 1, - next: Some({ - val: 5, - next: Some({ - val: 4, - next: Some({ - val: 3, - next: Some({ - val: 2, - next: Some(>) - }) - }) - }) - }) -}), Some(<2> { - val: 2, - next: Some(<1> { - val: 1, - next: Some({ - val: 5, - next: Some({ - val: 4, - next: Some({ - val: 3, - next: Some(>) - }) - }) - }) - }) -}), Some(<3> { - val: 3, - next: Some(<2> { - val: 2, - next: Some(<1> { - val: 1, - next: Some({ - val: 5, - next: Some({ - val: 4, - next: Some(>) - }) - }) - }) - }) -}), Some(<4> { - val: 4, - next: Some(<3> { - val: 3, - next: Some(<2> { - val: 2, - next: Some(<1> { - val: 1, - next: Some({ - val: 5, - next: Some(>) - }) - }) - }) - }) -}), Some(<5> { - val: 5, - next: Some(<4> { - val: 4, - next: Some(<3> { - val: 3, - next: Some(<2> { - val: 2, - next: Some(<1> { - val: 1, - next: Some(>) - }) - }) - }) - }) -})] -|}, + "[\n <1> Some(\n { \n val: 1,\n next: Some(\n { \n val: 5,\n next: Some(\n { \n val: 4,\n next: Some(\n { val: 3, next: Some({ val: 2, next: > }) },\n ), \n },\n ), \n },\n ), \n },\n ),\n <2> Some(\n { \n val: 2,\n next: <1> Some(\n { \n val: 1,\n next: Some(\n { \n val: 5,\n next: Some(\n { val: 4, next: Some({ val: 3, next: > }) },\n ), \n },\n ), \n },\n ), \n },\n ),\n <3> Some(\n { \n val: 3,\n next: <2> Some(\n { \n val: 2,\n next: <1> Some(\n { \n val: 1,\n next: Some(\n { val: 5, next: Some({ val: 4, next: > }) },\n ), \n },\n ), \n },\n ), \n },\n ),\n <4> Some(\n { \n val: 4,\n next: <3> Some(\n { \n val: 3,\n next: <2> Some(\n { \n val: 2,\n next: <1> Some(\n { val: 1, next: Some({ val: 5, next: > }) },\n ), \n },\n ), \n },\n ), \n },\n ),\n <5> Some(\n { \n val: 5,\n next: <4> Some(\n { \n val: 4,\n next: <3> Some(\n { \n val: 3,\n next: <2> Some(\n { val: 2, next: <1> Some({ val: 1, next: > }) },\n ), \n },\n ), \n },\n ), \n },\n ),\n]\n", ); assertRun( "cycles4", @@ -146,7 +70,7 @@ describe("cyclic references", ({test, testSkip}) => { print(a) print(unbox(a)) |}, - "<1> box(Some(Rec(>)))\nSome(Rec(<1> box(Some(Rec(>)))))\n", + "<1> box(Some(Rec(>)))\n<1> Some(Rec(box(>)))\n", ); assertRun( "cycles5", @@ -160,6 +84,6 @@ describe("cyclic references", ({test, testSkip}) => { a[0] = Some(b) print(a) |}, - "<1> [> Some(Rec(>))]\n", + "<1> [>Some(Rec(>))]\n", ); }); diff --git a/compiler/test/suites/enums.re b/compiler/test/suites/enums.re index ea28edb915..86dbfd9923 100644 --- a/compiler/test/suites/enums.re +++ b/compiler/test/suites/enums.re @@ -59,7 +59,7 @@ describe("enums", ({test, testSkip}) => { print(r == Rec{ x: 2, y: 2 }) print(Tup(1, 2, 3)) |}, - "Rec{\n x: 1,\n y: 2\n}\nRec{\n x: 11,\n y: 12\n}\ntrue\nfalse\nTup(1, 2, 3)\n", + "Rec{ x: 1, y: 2 }\nRec{ x: 11, y: 12 }\ntrue\nfalse\nTup(1, 2, 3)\n", ); assertCompileError( "enum_inline_record_2", @@ -100,7 +100,7 @@ describe("enums", ({test, testSkip}) => { let b = Rec{ x } print(b) |}, - "Rec{\n x: 1\n}\n", + "Rec{ x: 1, }\n", ); assertRun( "deeply_nested_enum", diff --git a/compiler/test/suites/exceptions.re b/compiler/test/suites/exceptions.re index b6d6c3c0c4..28f1b7f2ae 100644 --- a/compiler/test/suites/exceptions.re +++ b/compiler/test/suites/exceptions.re @@ -55,7 +55,7 @@ describe("exceptions", ({test, testSkip}) => { assertRun( "record_exception_1", {|exception Foo { msg: String, bar: Number }; print(Foo{msg: "Oops", bar: 1})|}, - "Foo{\n msg: \"Oops\",\n bar: 1\n}\n", + "Foo{ msg: \"Oops\", bar: 1 }\n", ); assertRunError( "record_exception_2", diff --git a/compiler/test/suites/includes.re b/compiler/test/suites/includes.re index be5050b20e..3b05adff4c 100644 --- a/compiler/test/suites/includes.re +++ b/compiler/test/suites/includes.re @@ -200,12 +200,12 @@ describe("includes", ({test, testSkip}) => { assertRun( "reprovide_type2", "from \"reprovideContents\" include ReprovideContents; use ReprovideContents.{ type OtherT as TT, val }; print(val); print({ x: 2 })", - "{\n x: 1\n}\n{\n x: 2\n}\n", + "{ x: 1, }\n{ x: 2, }\n", ); assertRun( "reprovide_type3", "from \"reprovideContents\" include ReprovideContents; use ReprovideContents.{ type OtherT as Other }; print({ x: 1 }: Other)", - "{\n x: 1\n}\n", + "{ x: 1, }\n", ); /* Duplicate imports */ test("dedupe_includes", ({expect}) => { diff --git a/compiler/test/suites/modules.re b/compiler/test/suites/modules.re index 17fc8a322a..f93e9e677e 100644 --- a/compiler/test/suites/modules.re +++ b/compiler/test/suites/modules.re @@ -126,7 +126,7 @@ describe("modules", ({test, testSkip}) => { assertFileRun( "nested_and_reprovided_modules", "nestedModules", - "hello from foo\nhello from bar\n[2, 3, 4]\n9\n[> 2, 3, 4]\nfalse\nfoo\n", + "hello from foo\nhello from bar\n[2, 3, 4]\n9\n[>2, 3, 4]\nfalse\nfoo\n", ); assertSnapshot( "reprovided_module", diff --git a/compiler/test/suites/print.re b/compiler/test/suites/print.re index 00754f2c11..21b2a9f621 100644 --- a/compiler/test/suites/print.re +++ b/compiler/test/suites/print.re @@ -11,13 +11,13 @@ describe("print", ({test, testSkip}) => { ~config_fn=() => {Grain_utils.Config.elide_type_info := true}, "elided_type_info_1", "enum Foo { Foo }; print(Foo)", - "\n", + "\n", ); assertRun( ~config_fn=() => {Grain_utils.Config.elide_type_info := true}, "elided_type_info_2", "record Foo { foo: String }; print({ foo: \"foo\" })", - "\n", + "{ : \"foo\", }\n", ); assertRun( "print_int64_large", @@ -32,12 +32,12 @@ describe("print", ({test, testSkip}) => { assertRun( "print_nested_records", "record Foo { foo: Number }; record Bar { bar: Foo }; print({ bar: { foo: 1 } })", - "{\n bar: {\n foo: 1\n }\n}\n", + "{ bar: { foo: 1, }, }\n", ); assertRun( "print_nested_records_multiple", "record Foo { foo: Number }; record Bar { bar: Foo }; print({ bar: { foo: 1 } }); print({ bar: { foo: 1 } }); print({ bar: { foo: 1 } })", - "{\n bar: {\n foo: 1\n }\n}\n{\n bar: {\n foo: 1\n }\n}\n{\n bar: {\n foo: 1\n }\n}\n", + "{ bar: { foo: 1, }, }\n{ bar: { foo: 1, }, }\n{ bar: { foo: 1, }, }\n", ); assertRun( "print_issue892_1", diff --git a/compiler/test/suites/records.re b/compiler/test/suites/records.re index c11947ddd7..18d3bdfa9b 100644 --- a/compiler/test/suites/records.re +++ b/compiler/test/suites/records.re @@ -18,17 +18,17 @@ describe("records", ({test, testSkip}) => { assertRun( "record_1", "record Rec {foo: Number}; print({foo: 4})", - "{\n foo: 4\n}\n", + "{ foo: 4, }\n", ); assertRun( "record_2", "provide record Rec {foo: Number}; print({foo: 4})", - "{\n foo: 4\n}\n", + "{ foo: 4, }\n", ); assertRun( "record_multiple", "provide record Rec {foo: Number, bar: String, baz: Bool}; print({foo: 4, bar: \"boo\", baz: true})", - "{\n foo: 4,\n bar: \"boo\",\n baz: true\n}\n", + "{ foo: 4, bar: \"boo\", baz: true }\n", ); assertSnapshot( "record_pun", @@ -188,13 +188,13 @@ describe("records", ({test, testSkip}) => { provide enum Bar { Baz(Foo) } print(Baz({ bar: 1 })) |}, - "Baz({\n bar: 1\n})\n", + "Baz({ bar: 1, })\n", ); // record spread assertRun( "record_spread_1", "record Rec {foo: Number, bar: Number, mut baz: Number}; let a = {foo: 1, bar: 2, baz: 3}; let b = {...a, bar: 3}; b.baz = 5; print(b); print(a)", - "{\n foo: 1,\n bar: 3,\n baz: 5\n}\n{\n foo: 1,\n bar: 2,\n baz: 3\n}\n", + "{ foo: 1, bar: 3, baz: 5 }\n{ foo: 1, bar: 2, baz: 3 }\n", ); assertSnapshot( "record_spread_2", diff --git a/compiler/test/suites/runtime.re b/compiler/test/suites/runtime.re index 35cc3ec8ce..ccb784ac91 100644 --- a/compiler/test/suites/runtime.re +++ b/compiler/test/suites/runtime.re @@ -13,4 +13,7 @@ describe("runtime", ({test, testSkip}) => { assertRuntime("unsafe/wasmi32.test"); assertRuntime("unsafe/wasmi64.test"); assertRuntime("unsafe/wasmref.test"); + + assertRuntime("toString.test"); + assertRuntime(~elide_type_info=true, "toStringNoTypeInfo.test"); }); diff --git a/compiler/test/suites/strings.re b/compiler/test/suites/strings.re index 133c25124c..ceba896ce0 100644 --- a/compiler/test/suites/strings.re +++ b/compiler/test/suites/strings.re @@ -390,7 +390,7 @@ bar", 1))|}, ); assertRun( "bytes_literal_long", - {|print(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg")|}, + {|print(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh")|}, "\n", ); assertCompileError( @@ -413,6 +413,6 @@ bar", 1))|}, assertRun( "range_printing", {|print({ rangeStart: 1, rangeEnd: 2 })|}, - "{\n rangeStart: 1,\n rangeEnd: 2\n}\n", + "{ rangeStart: 1, rangeEnd: 2 }\n", ); }); diff --git a/compiler/test/suites/types.re b/compiler/test/suites/types.re index 0776ebfc68..4c7663ec3a 100644 --- a/compiler/test/suites/types.re +++ b/compiler/test/suites/types.re @@ -450,7 +450,7 @@ describe("recursive types", ({test, testSkip}) => { type T = T print({ x: 1 }: T) |}, - "{\n x: 1\n}\n", + "{ x: 1, }\n", ); }); diff --git a/compiler/test/suites/wasi_args.re b/compiler/test/suites/wasi_args.re index b53756ec0b..94e0b68c8e 100644 --- a/compiler/test/suites/wasi_args.re +++ b/compiler/test/suites/wasi_args.re @@ -26,36 +26,36 @@ describe("wasi args and env", ({test, testSkip}) => { ~extra_args=[|"--", "a", "b"|], "print_args1", print_wasi_info, - "[> \"a\", \"b\"]\n[> ]\n", + "[>\"a\", \"b\"]\n[>]\n", ); assertRun( ~extra_args=[|"a", "b"|], "print_args2", print_wasi_info, - "[> ]\n[> ]\n", + "[>]\n[>]\n", ); assertRun( ~extra_args=[|"--env=FOO=bar", "a", "b"|], "print_args3", print_wasi_info, - "[> ]\n[> \"FOO=bar\"]\n", + "[>]\n[>\"FOO=bar\"]\n", ); assertRun( ~extra_args=[|"--env", "FOO=bar", "BAR=baz", "BAZ"|], "print_args4", print_wasi_info, - "[> ]\n[> \"FOO=bar\", \"BAR=baz\", \"BAZ=\"]\n", + "[>]\n[>\"FOO=bar\", \"BAR=baz\", \"BAZ=\"]\n", ); assertRun( ~extra_args=[|"--env", "FOO=bar", "--", "a", "b"|], "print_args5", print_wasi_info, - "[> \"a\", \"b\"]\n[> \"FOO=bar\"]\n", + "[>\"a\", \"b\"]\n[>\"FOO=bar\"]\n", ); assertRun( ~extra_args=[|"--", "a", "b", "--env", "FOO=bar"|], "print_args6", print_wasi_info, - "[> \"a\", \"b\", \"--env\", \"FOO=bar\"]\n[> ]\n", + "[>\"a\", \"b\", \"--env\", \"FOO=bar\"]\n[>]\n", ); }); diff --git a/stdlib/pervasives.md b/stdlib/pervasives.md index 6c6fff5b20..47619b6dc8 100644 --- a/stdlib/pervasives.md +++ b/stdlib/pervasives.md @@ -838,20 +838,19 @@ No other changes yet. toString: (value: a) => String ``` -Converts the given operand to a string. -Provides a better representation of data types if those types are provided from the module. +Converts any Grain value to its string representation. Parameters: -| param | type | description | -| ------- | ---- | ----------- | -| `value` | `a` | The operand | +| param | type | description | +| ------- | ---- | -------------------------- | +| `value` | `a` | The Grain value to convert | Returns: -| type | description | -| -------- | ------------------------ | -| `String` | The operand, as a string | +| type | description | +| -------- | -------------------------------------------- | +| `String` | The string representation of the Grain value | ### Pervasives.**print** diff --git a/stdlib/runtime/doc.gr b/stdlib/runtime/doc.gr new file mode 100644 index 0000000000..40a10463f1 --- /dev/null +++ b/stdlib/runtime/doc.gr @@ -0,0 +1,792 @@ +/** + The Doc module implements a document IR and engine for pretty-printing code. + Concatenation of Doc.t nodes is O(1) and printing a document is O(n) to the + size of the document. + + The most important aspect of the engine are groups and how breaks interact + with them. By default, the engine will print a group by either breaking none + of the break hints in that group if the entire group would fit on that line + (known as Flat mode) or all of the break hints in that group if the group + would not fit if printed in Flat mode (known as Breaking mode). This covers + 95% of formatting use cases, and users should tend to reach for default + groups before considering one of the alternatives. For the remaining 5% of + use cases, groups can also be created in FitGroups mode or FitAll mode. In + FitGroups mode, the engine will attempt to print as many subgroups in Flat + mode as possible on each line, breaking only when necessary. In FitAll mode, + the engine will attempt to print as many subgroups in Breaking mode as + possible on each line. + + Hardlines should be avoided. Instead, emit break hints and allow the engine + to decide when breaks should be made. If hardlines must be used, consider + using the group's ~print_width parameter to manually specify how wide the + engine should consider the group. By default, a group is only considered as + wide as the content leading to the first hardline. + + That's most of what you need to know to effectively use this module! Further + details on each node are provided below for maintainers or curious consumers. + + IR nodes: + • Empty + Has no effect on the output of the printing engine. + • GroupBreaker + Causes the enclosing group to be printed in Breaking mode. + • String + Prints the string as-is. The `string` function is Utf8-aware. + • Blank + Prints the specified number of spaces. + • BreakHint + Tells the engine that a break can be made here. If the engine decides not + to print a break, it prints the supplied document instead. + • Hardline + Forces the engine to print a newline character. Width calculations for + the current line are truncated at the Hardline. If the `phantom` field is + set to `true`, instead the Hardline is calculated as a zero-width non- + breaking character (the newline is emitted in the output, but + calculations assume it's just not there). + • IfBroken + If the engine has broken the current group, prints the `breaking` + document and prints the `flat` document otherwise. Note that for FitAll + and FitGroups groups, the `flat` document would be printed if the + IfBroken node appears before the point at which the group is broken, as + the engine makes that decision while printing the group (unlike default + groups, where the engine makes this decision before printing the group). + • Indent + Introduces indentation equal to the number of spaces specified when the + enclosing group is broken. When newline characters are emitted, they are + followed by space characters equal to the amount of indentation that has + been applied by all groups, unless this would lead to trailing + whitespace. Note that if the enclosing group has not been broken, the + indentation will not apply. For example, in this document, + group(~kind=FitGroups, indent(2, + group(indent(2, string("foo") ++ break ++ string("bar"))) + )) + if the break hint is broken by the engine, `bar`'s indentation level will + only be two spaces, as the outer group could never be broken by + the engine. + • Group + ~kind=Auto + The engine checks if the group would fit on the current line if printed + in Flat mode. If so, it prints the group in Flat mode and Breaking mode + otherwise. + ~kind=FitGroups + The engine begins printing the group. When it encounters a break hint, + it checks if the following node would fit on the current line. If that + node is a Group, its Flat mode width is used for the check. If the node + would not fit, a break is emitted. + ~kind=FitAll + The engine begins printing the group. When it encounters a break hint, + it checks if the following node would fit on the current line. If that + node is a Group, its Breaking mode width is used for the check. If the + node would not fit, a break is emitted. + • Concat + Prints the first document followed by the second document. Keeps track of + the combined width to allow the engine to make constant-time decisions + about line breaks. +*/ +@noPervasives +module Doc + +from "runtime/unsafe/wasmref" include WasmRef +use WasmRef.{ module WasmArrayRef } +from "runtime/unsafe/wasmi32" include WasmI32 +from "runtime/dataStructures" include DataStructures +use DataStructures.{ + allocateString, + getStringSize, + getStringArrayRef, + getCompoundValueArrayRef, + tagSimpleNumber, + untagSimpleNumber, +} +from "runtime/utf8" include Utf8 +use Utf8.{ isLeadingByte } + +from "runtime/miniBuffer" include MiniBuffer +from "runtime/vector" include Vector + +// Offsets + +primitive (||) = "@or" +primitive (!) = "@not" +primitive (is) = "@is" +primitive magic = "@magic" +primitive throw = "@throw" +primitive box = "@box" +primitive unbox = "@unbox" + +exception Impossible(String) + +@unsafe +let (+) = (a: Number, b: Number) => { + use WasmI32.{ (+) } + tagSimpleNumber(untagSimpleNumber(a) + untagSimpleNumber(b)) +} + +@unsafe +let (>) = (a: Number, b: Number) => { + use WasmI32.{ (>) } + untagSimpleNumber(a) > untagSimpleNumber(b) +} + +@unsafe +let stringLength = (string: String) => { + use WasmI32.{ (+), ltU as (<) } + let strRef = WasmRef.fromGrain(string) + let strData = getStringArrayRef(strRef) + let strSize = getStringSize(strRef) + + let mut len = 0n + for (let mut i = 0n; i < strSize; i += 1n) { + let byte = WasmArrayRef.getI8U(strData, i) + if (isLeadingByte(byte)) len += 1n + } + tagSimpleNumber(len) +} + +@unsafe +let buildSpaceString = count => { + let size = untagSimpleNumber(count) + let strRef = allocateString(size) + let strData = getStringArrayRef(strRef) + // 32n is ASCII code for space + WasmArrayRef.fillI8(strData, 0n, 32n, size) + WasmRef.toGrain(strRef): String +} + +/** + * End-of-line styles for the printing engine. + */ +provide enum EOL { + /** Carriage return + line feed */ + CRLF, + /** Line feed */ + LF, +} + +/** + * The document IR used for printing. + */ +abstract enum rec LayoutNode { + Empty, + GroupBreaker, + String{ value: String, width: Width, rawWidth: Number }, + Blank{ count: Number, }, + BreakHint{ doc: LayoutNode, flatWidth: Width }, + Hardline{ phantom: Bool, }, + IfBroken{ + flat: LayoutNode, + breaking: LayoutNode, + flatWidth: Width, + breakingWidth: Width, + }, + Indent{ + count: Number, + doc: LayoutNode, + hasGroupBreaker: Bool, + flatWidth: Width, + breakingWidth: Width, + }, + Group{ + groupType: GroupType, + doc: LayoutNode, + flatWidth: Width, + breakingWidth: Width, + }, + // TODO: Consider adding a repeat or ConcatList node to avoid allocating as many doc nodes for large chains of concatenations + Concat{ + left: LayoutNode, + right: LayoutNode, + hasGroupBreaker: Bool, + flatWidth: Width, + breakingWidth: Width, + }, +} +/** + * The type of group to create. + */ +and provide enum GroupType { + /** + * The engine checks if the group would fit on the current line if printed + * in Flat mode. If so, it prints the group in Flat mode and Breaking mode + * otherwise. + */ + Auto, + /** + * The engine begins printing the group. When it encounters a break hint, + * it checks if the following node would fit on the current line. If that + * node is a Group, its Flat mode width is used for the check. If the node + * would not fit, a break is emitted. + */ + FitGroups, + /** + * The engine begins printing the group. When it encounters a break hint, + * it checks if the following node would fit on the current line. If that + * node is a Group, its Breaking mode width is used for the check. If the + * node would not fit, a break is emitted. + */ + FitAll, +} +/** + * Represents a width that may or may not account for line breaks. + */ +and abstract enum Width { + WithBreak(Number), + WithoutBreak(Number), +} + +let breakingWidth = doc => { + match (doc) { + Empty | GroupBreaker => WithoutBreak(0), + String{ width, _ } => width, + Indent{ breakingWidth, _ } | + Group{ breakingWidth, _ } | + Concat{ breakingWidth, _ } | + IfBroken{ breakingWidth, _ } => breakingWidth, + Blank{ count } => WithoutBreak(count), + BreakHint{ _ } | Hardline{ phantom: false } => WithBreak(0), + Hardline{ phantom: true } => WithoutBreak(0), + } +} + +let flatWidth = doc => { + match (doc) { + Empty | GroupBreaker => WithoutBreak(0), + String{ width, _ } => width, + Indent{ flatWidth, _ } | + Group{ flatWidth, _ } | + Concat{ flatWidth, _ } | + IfBroken{ flatWidth, _ } | + BreakHint{ flatWidth, _ } => flatWidth, + Blank{ count } => WithoutBreak(count), + Hardline{ phantom: false } => WithBreak(0), + Hardline{ phantom: true } => WithoutBreak(0), + } +} + +let addWidth = (left, right) => { + match (left) { + WithBreak(l) => left, + WithoutBreak(l) => match (right) { + WithBreak(r) => WithBreak(l + r), + WithoutBreak(r) => WithoutBreak(l + r), + }, + } +} + +let hasGroupBreaker = doc => { + match (doc) { + Concat{ hasGroupBreaker, _ } | Indent{ hasGroupBreaker, _ } => + hasGroupBreaker, + GroupBreaker => true, + _ => false, + } +} + +@unsafe +let widthValue = (width: Width) => { + let ref = WasmRef.fromGrain(width) + let data = getCompoundValueArrayRef(ref) + WasmRef.toGrain(WasmArrayRef.getAny(data, 0n)): Number +} + +/** + * Utilities for constructing the document IR. + */ +provide module Builder { + /** + * An empty node that has no effect on the output of the printing engine. + */ + provide let empty = Empty + /** + * A node that causes the enclosing group to be printed in Breaking mode. + */ + provide let groupBreaker = GroupBreaker + /** + * A node that prints the string as-is. The `string` + * function is Utf8-aware. + * + * @param str: The string to print + * + * @returns A LayoutNode that prints the string + */ + provide let string = str => { + let strLen = stringLength(str) + String{ value: str, width: WithoutBreak(strLen), rawWidth: strLen } + } + /** + * A node that prints a constant string as-is. + * + * NOTE: This should only be used for ASCII strings, as the width is + * calculated based on the number of bytes, which is not accurate for + * non-ASCII strings. + * + * @param str: The ascii string to print + * + * @returns A LayoutNode that prints the ascii string + */ + @unsafe + provide let asciiString = str => { + let strLen = tagSimpleNumber(getStringSize(WasmRef.fromGrain(str))) + String{ value: str, width: WithoutBreak(strLen), rawWidth: strLen } + } + + /** + * A node that prints the specified number of spaces. + * + * @param c: The number of spaces to print + * + * @returns A LayoutNode that prints the spaces + */ + provide let blank = c => Blank{ count: c } + /** + * A node that forces the engine to print a newline character. + * Width calculations for the current line are truncated at the Hardline. + */ + provide let hardline = Hardline{ phantom: false } + /** + * A node that forces the engine to print a newline character. + * Width calculations for the current line are truncated at the Hardline. + * The Hardline is calculated as a zero-width non-breaking character (the + * newline is emitted in the output, but calculations assume it's just not + * there). + */ + provide let phantomHardline = Hardline{ phantom: true } + /** + * Constructs a node where if the engine has broken the current group, + * prints the `breaking` document and prints the `flat` document otherwise. + * Note that for FitAll and FitGroups groups, the `flat` document would be + * printed if the IfBroken node appears before the point at which the group + * is broken, as the engine makes that decision while printing the group + * (unlike default groups, where the engine makes this decision before + * printing the group). + * + * @param breaking: The document to print if the group is broken + * @param flat: The document to print if the group is not broken + * + * @returns A LayoutNode that conditionally prints one of the documents + */ + provide let ifBroken = (breaking, flat) => + IfBroken{ + flat, + breaking, + flatWidth: flatWidth(flat), + breakingWidth: breakingWidth(breaking), + } + /** + * Constructs a node that introduces indentation equal to the number of spaces + * specified when the enclosing group is broken. When newline characters are emitted, + * they are followed by space characters equal to the amount of indentation that has + * been applied by all groups, unless this would lead to trailing whitespace. + * Note that if the enclosing group has not been broken, the + * indentation will not apply. For example, in this document, + * ```grain + * group(~kind=FitGroups, indent(2, + * group(indent(2, string("foo") ++ break ++ string("bar"))) + * )) + * ``` + * if the break hint is broken by the engine, `bar`'s indentation level will + * only be two spaces, as the outer group could never be broken by + * the engine. + * + * @param count: The number of spaces to indent by (default: 2) + * @param doc: The document to indent + * + * @returns A LayoutNode that indents the document + */ + provide let indent = (count=2, doc) => + Indent{ + count, + doc, + hasGroupBreaker: hasGroupBreaker(doc), + flatWidth: flatWidth(doc), + breakingWidth: breakingWidth(doc), + } + /** + * Constructs a node that creates a group around the supplied document, + * allowing the printing mode to be controlled. + * + * @param printWidth: An optional width to consider the group as when printing + * @param kind: The kind of group to create (default: Auto) + * @param doc: The document to group + * + * @returns A LayoutNode that groups the document + */ + provide let group = (printWidth=None, kind=Auto, doc) => { + match (printWidth) { + Some(width) => { + let width = WithBreak(width) + Group{ groupType: kind, doc, flatWidth: width, breakingWidth: width } + }, + None => { + Group{ + groupType: kind, + doc, + flatWidth: flatWidth(doc), + breakingWidth: breakingWidth(doc), + } + }, + } + } + /** + * A node that prints the first document followed by the second document. + * While keeping track of the combined width to allow the engine to make + * constant-time decisions about line breaks. + * + * @param left: The first document + * @param right: The second document + * + * @returns A LayoutNode that concatenates the documents + */ + provide let (++) = (left, right) => { + let hasGroupBreaker = hasGroupBreaker(left) || hasGroupBreaker(right) + let breakingWidth = addWidth(breakingWidth(left), breakingWidth(right)) + + Concat{ + left, + right, + hasGroupBreaker, + flatWidth: if (hasGroupBreaker) { + breakingWidth + } else { + addWidth(flatWidth(left), flatWidth(right)) + }, + breakingWidth, + } + } + + let rec concatMapHelp = (sep, trail, func, acc, lst) => { + match (lst) { + [ultimate] => + // one element list + acc ++ func(final=true, ultimate) ++ trail(ultimate), + [elem, ...[next, ..._] as rest] => + concatMapHelp( + sep, + trail, + func, + acc ++ func(final=false, elem) ++ sep(elem, next), + rest + ), + [] => throw Impossible("empty list in concatMap"), + } + } + + /** + * Maps over a list, applying the given function to each element + * and concatenating the results. + * + * @param sep: A function that produces a separator to be placed between elements + * @param lead: A function that produces a leading document before the first element + * @param trail: A function that produces a trailing document after the last element + * @param func: A function that produces a document for each element + * @param lst: The list of elements to map over + * + * @returns A LayoutNode that represents the concatenated mapped documents + */ + provide let concatMap = (sep, lead, trail, func, lst) => { + match (lst) { + [] => Empty, + [first, ..._] => concatMapHelp(sep, trail, func, lead(first), lst), + } + } + + /** + * Maps over an array, applying the given function to each element + * and concatenating the results. + * + * @param sep: A function that produces a separator to be placed between elements + * @param lead: A function that produces a leading document before the first element + * @param trail: A function that produces a trailing document after the last element + * @param func: A function that produces a document for each element + * @param arr: The array of elements to map over + * + * @returns A LayoutNode that represents the concatenated mapped documents + */ + @unsafe + provide let concatArrayMap = (sep, lead, trail, func, arr: Array) => { + use WasmI32.{ (+), (<) } + let arrRef = WasmRef.fromGrain(arr) + let arrData = DataStructures.getCompoundValueArrayRef(arrRef) + let arrLen = WasmArrayRef.length(arrData) + if (WasmI32.eqz(arrLen)) return Empty + let first = WasmRef.toGrain(WasmArrayRef.getAny(arrData, 0n)): a + let mut acc = lead(first) + let mut prev = first + for (let mut i = 1n; i < arrLen; i += 1n) { + let curr = WasmRef.toGrain(WasmArrayRef.getAny(arrData, i)): a + acc = acc ++ func(final=false, prev) ++ sep(prev, curr) + prev = curr + } + return acc ++ func(final=true, prev) ++ trail(prev) + } + + // Constant Strings + let closingBracket = asciiString("]") + let doubleQuote = asciiString("\"") + let singleQuote = asciiString("'") + + /** + * A node that represents a space that may be broken + * if necessary. + */ + provide let breakableSpace = BreakHint{ + doc: Blank{ count: 1 }, + flatWidth: WithoutBreak(1), + } + /** A node that represents a break. */ + provide let _break = BreakHint{ doc: Empty, flatWidth: WithoutBreak(0) } + /** A node that represents a space. */ + provide let space = blank(1) + /** A node that represents a comma. */ + provide let comma = asciiString(",") + /** A node that represents a comma followed by a breakable space. */ + provide let commaBreakableSpace = () => comma ++ breakableSpace + /** + * Constructs a node wrapping the given document in parentheses. + * + * @param wrap: An optional wrapping function to apply (default: group) + * @param doc: The document to wrap + * + * @returns A LayoutNode that wraps the document in parentheses + */ + provide let parens = (wrap=doc => group(doc), doc) => + wrap(asciiString("(") ++ doc ++ asciiString(")")) + /** + * Constructs a node wrapping the given document in braces. + * + * @param wrap: An optional wrapping function to apply (default: group) + * @param doc: The document to wrap + * + * @returns A LayoutNode that wraps the document in braces + */ + provide let braces = (wrap=doc => group(doc), doc) => + wrap(asciiString("{") ++ doc ++ asciiString("}")) + /** + * Constructs a node wrapping the given document in array brackets. + * + * @param wrap: An optional wrapping function to apply (default: group) + * @param doc: The document to wrap + * + * @returns A LayoutNode that wraps the document in array brackets + */ + provide let arrayBrackets = (wrap=doc => group(doc), doc) => + wrap(asciiString("[>") ++ doc ++ closingBracket) + /** + * Constructs a node wrapping the given document in list brackets. + * + * @param wrap: An optional wrapping function to apply (default: group) + * @param doc: The document to wrap + * + * @returns A LayoutNode that wraps the document in list brackets + */ + provide let listBrackets = (wrap=doc => group(doc), doc) => + wrap(asciiString("[") ++ doc ++ closingBracket) + /** + * Constructs a node wrapping the given document in angle brackets. + * + * @param wrap: An optional wrapping function to apply (default: group) + * @param doc: The document to wrap + * + * @returns A LayoutNode that wraps the document in angle brackets + */ + provide let angleBrackets = (wrap=doc => group(doc), doc) => + wrap(asciiString("<") ++ doc ++ asciiString(">")) + /** + * Constructs a node wrapping the given document in double quotes. + * + * @param doc: The document to wrap + * + * @returns A LayoutNode that wraps the document in double quotes + */ + provide let doubleQuotes = doc => doubleQuote ++ doc ++ doubleQuote + /** + * Constructs a node wrapping the given document in single quotes. + * + * @param doc: The document to wrap + * + * @returns A LayoutNode that wraps the document in single quotes + */ + provide let singleQuotes = doc => singleQuote ++ doc ++ singleQuote + /** + * A node that represents a trailing comma to be added if the + * current group is broken. + */ + provide let trailingComma = () => ifBroken(comma, empty) +} + +/** The printing engine for LayoutNode documents. */ +provide module Engine { + enum EngineMode { + Flat, + Breaking, + FitFlat, + FitBreaking, + } + + record EngineGroup { + mode: EngineMode, + mut globalIndent: Number, + mut localIndent: Number, + mut broken: Bool, + } + /* + * NOTE: + * This is an extremely unsafe optimization instead of using `None` or a closure for the fit continuation, + * we cast a void value to a function and check if that value is the default to determine if we need to + * call the continuation, this saves us a lot of performance and some size, but if defaultFitContinuation + * were to ever be called in it's default state it would cause a crash. + */ + @unsafe + let defaultFitContinuation = magic(void): LayoutNode => WasmI32 + + @unsafe + let flushWriteQueue = (write, writeQueue) => { + let str = unbox(writeQueue) + let strRef = WasmRef.fromGrain(str) + let strSize = getStringSize(strRef) + if (!WasmI32.eqz(strSize)) { + write(str) + writeQueue := "" + } + } + + @unsafe + let breakHelp = (write, writeQueue, eol, group) => { + group.broken = true + group.globalIndent += group.localIndent + group.localIndent = 0 + write(eol) + writeQueue := buildSpaceString(group.globalIndent) + group.globalIndent + } + + /** + * Prints the given document using the provided write function. + * + * @param write: A function that takes a string and writes it to an output + * @param eol: The end-of-line style to use + * @param lineWidth: The maximum line width to use + * @param doc: The document to print + * + * @returns Void + */ + @unsafe + provide let print = (write, eol, lineWidth, doc) => { + let eol = match (eol) { + CRLF => "\r\n", + LF => "\n", + } + // The current column we're writing to + let mut column = 0 + // Queue for indentation to prevent lines with just spaces + let writeQueue = box("") + // Continuation for Fit mode calculations that depend on the size of the next node + let mut fitContinuation = defaultFitContinuation + // The queue of printing instructions + let queue = Vector.make() + + // Helpers + let fit = (write, eol, group, nextWidth, width, doc) => { + let nextWidth = widthValue(nextWidth) + let hintWidth = widthValue(width) + if (column + hintWidth + nextWidth > lineWidth) { + column = breakHelp(write, writeQueue, eol, group) + } else { + Vector.push(queue, (group, doc)) + } + 1n + } + + let mut current = ( + { mode: Flat, globalIndent: 0, localIndent: 0, broken: false }, + doc, + ) + while (true) { + let (group, doc) = current + if (!(fitContinuation is defaultFitContinuation)) { + fitContinuation(doc) + fitContinuation = defaultFitContinuation + continue + } + match (doc) { + Empty | GroupBreaker => void, + String{ value, width, rawWidth } => { + flushWriteQueue(write, writeQueue) + write(value) + column += rawWidth + }, + Blank{ count } => { + flushWriteQueue(write, writeQueue) + write(buildSpaceString(count)) + column += count + }, + BreakHint{ doc, flatWidth: width } => { + match (group.mode) { + Flat => { + current = (group, doc) + continue + }, + Breaking => column = breakHelp(write, writeQueue, eol, group), + FitFlat => { + fitContinuation = nextDoc => + fit(write, eol, group, flatWidth(nextDoc), width, doc) + }, + FitBreaking => { + fitContinuation = nextDoc => + fit(write, eol, group, breakingWidth(nextDoc), width, doc) + }, + } + }, + Hardline{ _ } => column = breakHelp(write, writeQueue, eol, group), + IfBroken{ flat, breaking, _ } => { + current = (group, if (group.broken) breaking else flat) + continue + }, + Indent{ count, doc, _ } => { + current = ({ ...group, localIndent: group.localIndent + count }, doc) + continue + }, + Group{ doc, groupType, flatWidth, _ } => { + let broken = hasGroupBreaker(doc) + let mode = match (groupType) { + _ when broken => Breaking, + Auto when column + widthValue(flatWidth) > lineWidth => Breaking, + Auto => Flat, + FitGroups => FitFlat, + FitAll => FitBreaking, + } + current = ( + { mode, globalIndent: group.globalIndent, localIndent: 0, broken }, + doc, + ) + continue + }, + Concat{ left, right, _ } => { + Vector.push(queue, (group, right)) + current = (group, left) + continue + }, + } + if (WasmI32.eqz(Vector.length(queue))) break + current = Vector.pop(queue) + } + } + + /** + * Converts the given document to a string using the specified + * end-of-line style and line width. + * + * @param eol: The end-of-line style to use + * @param lineWidth: The maximum line width to use + * @param doc: The document to convert to a string + * + * @returns The string representation of the document + */ + @unsafe + provide let toString = (eol, lineWidth, doc) => { + let b = MiniBuffer.make(2048n) + let write = str => MiniBuffer.addString(str, b) + print(write, eol, lineWidth, doc) + MiniBuffer.toString(b) + } +} diff --git a/stdlib/runtime/doc.md b/stdlib/runtime/doc.md new file mode 100644 index 0000000000..daa84e386d --- /dev/null +++ b/stdlib/runtime/doc.md @@ -0,0 +1,704 @@ +--- +title: Doc +--- + + The Doc module implements a document IR and engine for pretty-printing code. + Concatenation of Doc.t nodes is O(1) and printing a document is O(n) to the + size of the document. + + The most important aspect of the engine are groups and how breaks interact + with them. By default, the engine will print a group by either breaking none + of the break hints in that group if the entire group would fit on that line + (known as Flat mode) or all of the break hints in that group if the group + would not fit if printed in Flat mode (known as Breaking mode). This covers + 95% of formatting use cases, and users should tend to reach for default + groups before considering one of the alternatives. For the remaining 5% of + use cases, groups can also be created in FitGroups mode or FitAll mode. In + FitGroups mode, the engine will attempt to print as many subgroups in Flat + mode as possible on each line, breaking only when necessary. In FitAll mode, + the engine will attempt to print as many subgroups in Breaking mode as + possible on each line. + + Hardlines should be avoided. Instead, emit break hints and allow the engine + to decide when breaks should be made. If hardlines must be used, consider + using the group's ~print_width parameter to manually specify how wide the + engine should consider the group. By default, a group is only considered as + wide as the content leading to the first hardline. + + That's most of what you need to know to effectively use this module! Further + details on each node are provided below for maintainers or curious consumers. + + IR nodes: + • Empty + Has no effect on the output of the printing engine. + • GroupBreaker + Causes the enclosing group to be printed in Breaking mode. + • String + Prints the string as-is. The `string` function is Utf8-aware. + • Blank + Prints the specified number of spaces. + • BreakHint + Tells the engine that a break can be made here. If the engine decides not + to print a break, it prints the supplied document instead. + • Hardline + Forces the engine to print a newline character. Width calculations for + the current line are truncated at the Hardline. If the `phantom` field is + set to `true`, instead the Hardline is calculated as a zero-width non- + breaking character (the newline is emitted in the output, but + calculations assume it's just not there). + • IfBroken + If the engine has broken the current group, prints the `breaking` + document and prints the `flat` document otherwise. Note that for FitAll + and FitGroups groups, the `flat` document would be printed if the + IfBroken node appears before the point at which the group is broken, as + the engine makes that decision while printing the group (unlike default + groups, where the engine makes this decision before printing the group). + • Indent + Introduces indentation equal to the number of spaces specified when the + enclosing group is broken. When newline characters are emitted, they are + followed by space characters equal to the amount of indentation that has + been applied by all groups, unless this would lead to trailing + whitespace. Note that if the enclosing group has not been broken, the + indentation will not apply. For example, in this document, + group(~kind=FitGroups, indent(2, + group(indent(2, string("foo") ++ break ++ string("bar"))) + )) + if the break hint is broken by the engine, `bar`'s indentation level will + only be two spaces, as the outer group could never be broken by + the engine. + • Group + ~kind=Auto + The engine checks if the group would fit on the current line if printed + in Flat mode. If so, it prints the group in Flat mode and Breaking mode + otherwise. + ~kind=FitGroups + The engine begins printing the group. When it encounters a break hint, + it checks if the following node would fit on the current line. If that + node is a Group, its Flat mode width is used for the check. If the node + would not fit, a break is emitted. + ~kind=FitAll + The engine begins printing the group. When it encounters a break hint, + it checks if the following node would fit on the current line. If that + node is a Group, its Breaking mode width is used for the check. If the + node would not fit, a break is emitted. + • Concat + Prints the first document followed by the second document. Keeps track of + the combined width to allow the engine to make constant-time decisions + about line breaks. + +## Types + +Type declarations included in the Doc module. + +### Doc.**EOL** + +```grain +enum EOL { + CRLF, + LF, +} +``` + +End-of-line styles for the printing engine. + +Variants: + +```grain +CRLF +``` + +Carriage return + line feed + +```grain +LF +``` + +Line feed + +### Doc.**LayoutNode** + +```grain +type LayoutNode +``` + +The document IR used for printing. + +### Doc.**GroupType** + +```grain +enum GroupType { + Auto, + FitGroups, + FitAll, +} +``` + +The type of group to create. + +Variants: + +```grain +Auto +``` + +The engine checks if the group would fit on the current line if printed +in Flat mode. If so, it prints the group in Flat mode and Breaking mode +otherwise. + +```grain +FitGroups +``` + +The engine begins printing the group. When it encounters a break hint, +it checks if the following node would fit on the current line. If that +node is a Group, its Flat mode width is used for the check. If the node +would not fit, a break is emitted. + +```grain +FitAll +``` + +The engine begins printing the group. When it encounters a break hint, +it checks if the following node would fit on the current line. If that +node is a Group, its Breaking mode width is used for the check. If the +node would not fit, a break is emitted. + +### Doc.**Width** + +```grain +type Width +``` + +Represents a width that may or may not account for line breaks. + +## Doc.Builder + +Utilities for constructing the document IR. + +### Values + +Functions and constants included in the Doc.Builder module. + +#### Doc.Builder.**empty** + +```grain +empty: LayoutNode +``` + +An empty node that has no effect on the output of the printing engine. + +#### Doc.Builder.**groupBreaker** + +```grain +groupBreaker: LayoutNode +``` + +A node that causes the enclosing group to be printed in Breaking mode. + +#### Doc.Builder.**string** + +```grain +string: (str: String) => LayoutNode +``` + +A node that prints the string as-is. The `string` +function is Utf8-aware. + +Parameters: + +| param | type | description | +| ----- | -------- | ------------------- | +| `str` | `String` | The string to print | + +Returns: + +| type | description | +| ------------ | ----------------------------------- | +| `LayoutNode` | A LayoutNode that prints the string | + +#### Doc.Builder.**asciiString** + +```grain +asciiString: (str: String) => LayoutNode +``` + +A node that prints a constant string as-is. + +NOTE: This should only be used for ASCII strings, as the width is +calculated based on the number of bytes, which is not accurate for +non-ASCII strings. + +Parameters: + +| param | type | description | +| ----- | -------- | ------------------------- | +| `str` | `String` | The ascii string to print | + +Returns: + +| type | description | +| ------------ | ----------------------------------------- | +| `LayoutNode` | A LayoutNode that prints the ascii string | + +#### Doc.Builder.**blank** + +```grain +blank: (c: Number) => LayoutNode +``` + +A node that prints the specified number of spaces. + +Parameters: + +| param | type | description | +| ----- | -------- | ----------------------------- | +| `c` | `Number` | The number of spaces to print | + +Returns: + +| type | description | +| ------------ | ----------------------------------- | +| `LayoutNode` | A LayoutNode that prints the spaces | + +#### Doc.Builder.**hardline** + +```grain +hardline: LayoutNode +``` + +A node that forces the engine to print a newline character. +Width calculations for the current line are truncated at the Hardline. + +#### Doc.Builder.**phantomHardline** + +```grain +phantomHardline: LayoutNode +``` + +A node that forces the engine to print a newline character. +Width calculations for the current line are truncated at the Hardline. +The Hardline is calculated as a zero-width non-breaking character (the +newline is emitted in the output, but calculations assume it's just not +there). + +#### Doc.Builder.**ifBroken** + +```grain +ifBroken: (breaking: LayoutNode, flat: LayoutNode) => LayoutNode +``` + +Constructs a node where if the engine has broken the current group, +prints the `breaking` document and prints the `flat` document otherwise. +Note that for FitAll and FitGroups groups, the `flat` document would be +printed if the IfBroken node appears before the point at which the group +is broken, as the engine makes that decision while printing the group +(unlike default groups, where the engine makes this decision before +printing the group). + +Parameters: + +| param | type | description | +| ---------- | ------------ | ------------------------------------------------ | +| `breaking` | `LayoutNode` | The document to print if the group is broken | +| `flat` | `LayoutNode` | The document to print if the group is not broken | + +Returns: + +| type | description | +| ------------ | ----------------------------------------------------------- | +| `LayoutNode` | A LayoutNode that conditionally prints one of the documents | + +#### Doc.Builder.**indent** + +```grain +indent: (?count: Number, doc: LayoutNode) => LayoutNode +``` + +Constructs a node that introduces indentation equal to the number of spaces +specified when the enclosing group is broken. When newline characters are emitted, +they are followed by space characters equal to the amount of indentation that has +been applied by all groups, unless this would lead to trailing whitespace. +Note that if the enclosing group has not been broken, the +indentation will not apply. For example, in this document, +```grain +group(~kind=FitGroups, indent(2, + group(indent(2, string("foo") ++ break ++ string("bar"))) +)) +``` +if the break hint is broken by the engine, `bar`'s indentation level will +only be two spaces, as the outer group could never be broken by +the engine. + +Parameters: + +| param | type | description | +| -------- | ------------ | ---------------------------------------------- | +| `?count` | `Number` | The number of spaces to indent by (default: 2) | +| `doc` | `LayoutNode` | The document to indent | + +Returns: + +| type | description | +| ------------ | -------------------------------------- | +| `LayoutNode` | A LayoutNode that indents the document | + +#### Doc.Builder.**group** + +```grain +group: + (?printWidth: Option, ?kind: GroupType, doc: LayoutNode) => + LayoutNode +``` + +Constructs a node that creates a group around the supplied document, +allowing the printing mode to be controlled. + +Parameters: + +| param | type | description | +| ------------- | ---------------- | -------------------------------------------------------- | +| `?printWidth` | `Option` | An optional width to consider the group as when printing | +| `?kind` | `GroupType` | The kind of group to create (default: Auto) | +| `doc` | `LayoutNode` | The document to group | + +Returns: + +| type | description | +| ------------ | ------------------------------------- | +| `LayoutNode` | A LayoutNode that groups the document | + +#### Doc.Builder.**(++)** + +```grain +(++): (left: LayoutNode, right: LayoutNode) => LayoutNode +``` + +A node that prints the first document followed by the second document. +While keeping track of the combined width to allow the engine to make +constant-time decisions about line breaks. + +Parameters: + +| param | type | description | +| ------- | ------------ | ------------------- | +| `left` | `LayoutNode` | The first document | +| `right` | `LayoutNode` | The second document | + +Returns: + +| type | description | +| ------------ | -------------------------------------------- | +| `LayoutNode` | A LayoutNode that concatenates the documents | + +#### Doc.Builder.**concatMap** + +```grain +concatMap: + (sep: ((a, a) => LayoutNode), lead: (a => LayoutNode), + trail: (a => LayoutNode), func: ((final: Bool, a) => LayoutNode), + lst: List) => LayoutNode +``` + +Maps over a list, applying the given function to each element +and concatenating the results. + +Parameters: + +| param | type | description | +| ------- | -------------------------------- | -------------------------------------------------------------------- | +| `sep` | `(a, a) => LayoutNode` | A function that produces a separator to be placed between elements | +| `lead` | `a => LayoutNode` | A function that produces a leading document before the first element | +| `trail` | `a => LayoutNode` | A function that produces a trailing document after the last element | +| `func` | `(final: Bool, a) => LayoutNode` | A function that produces a document for each element | +| `lst` | `List` | The list of elements to map over | + +Returns: + +| type | description | +| ------------ | -------------------------------------------------------------- | +| `LayoutNode` | A LayoutNode that represents the concatenated mapped documents | + +#### Doc.Builder.**concatArrayMap** + +```grain +concatArrayMap: + (sep: ((a, a) => LayoutNode), lead: (a => LayoutNode), + trail: (a => LayoutNode), func: ((final: Bool, a) => LayoutNode), + arr: Array) => LayoutNode +``` + +Maps over an array, applying the given function to each element +and concatenating the results. + +Parameters: + +| param | type | description | +| ------- | -------------------------------- | -------------------------------------------------------------------- | +| `sep` | `(a, a) => LayoutNode` | A function that produces a separator to be placed between elements | +| `lead` | `a => LayoutNode` | A function that produces a leading document before the first element | +| `trail` | `a => LayoutNode` | A function that produces a trailing document after the last element | +| `func` | `(final: Bool, a) => LayoutNode` | A function that produces a document for each element | +| `arr` | `Array` | The array of elements to map over | + +Returns: + +| type | description | +| ------------ | -------------------------------------------------------------- | +| `LayoutNode` | A LayoutNode that represents the concatenated mapped documents | + +#### Doc.Builder.**breakableSpace** + +```grain +breakableSpace: LayoutNode +``` + +A node that represents a space that may be broken +if necessary. + +#### Doc.Builder.**_break** + +```grain +_break: LayoutNode +``` + +A node that represents a break. + +#### Doc.Builder.**space** + +```grain +space: LayoutNode +``` + +A node that represents a space. + +#### Doc.Builder.**comma** + +```grain +comma: LayoutNode +``` + +A node that represents a comma. + +#### Doc.Builder.**commaBreakableSpace** + +```grain +commaBreakableSpace: () => LayoutNode +``` + +A node that represents a comma followed by a breakable space. + +#### Doc.Builder.**parens** + +```grain +parens: + (?wrap: ((doc: LayoutNode) => LayoutNode), doc: LayoutNode) => LayoutNode +``` + +Constructs a node wrapping the given document in parentheses. + +Parameters: + +| param | type | description | +| ------- | --------------------------------- | ------------------------------------------------------- | +| `?wrap` | `(doc: LayoutNode) => LayoutNode` | An optional wrapping function to apply (default: group) | +| `doc` | `LayoutNode` | The document to wrap | + +Returns: + +| type | description | +| ------------ | --------------------------------------------------- | +| `LayoutNode` | A LayoutNode that wraps the document in parentheses | + +#### Doc.Builder.**braces** + +```grain +braces: + (?wrap: ((doc: LayoutNode) => LayoutNode), doc: LayoutNode) => LayoutNode +``` + +Constructs a node wrapping the given document in braces. + +Parameters: + +| param | type | description | +| ------- | --------------------------------- | ------------------------------------------------------- | +| `?wrap` | `(doc: LayoutNode) => LayoutNode` | An optional wrapping function to apply (default: group) | +| `doc` | `LayoutNode` | The document to wrap | + +Returns: + +| type | description | +| ------------ | ---------------------------------------------- | +| `LayoutNode` | A LayoutNode that wraps the document in braces | + +#### Doc.Builder.**arrayBrackets** + +```grain +arrayBrackets: + (?wrap: ((doc: LayoutNode) => LayoutNode), doc: LayoutNode) => LayoutNode +``` + +Constructs a node wrapping the given document in array brackets. + +Parameters: + +| param | type | description | +| ------- | --------------------------------- | ------------------------------------------------------- | +| `?wrap` | `(doc: LayoutNode) => LayoutNode` | An optional wrapping function to apply (default: group) | +| `doc` | `LayoutNode` | The document to wrap | + +Returns: + +| type | description | +| ------------ | ------------------------------------------------------ | +| `LayoutNode` | A LayoutNode that wraps the document in array brackets | + +#### Doc.Builder.**listBrackets** + +```grain +listBrackets: + (?wrap: ((doc: LayoutNode) => LayoutNode), doc: LayoutNode) => LayoutNode +``` + +Constructs a node wrapping the given document in list brackets. + +Parameters: + +| param | type | description | +| ------- | --------------------------------- | ------------------------------------------------------- | +| `?wrap` | `(doc: LayoutNode) => LayoutNode` | An optional wrapping function to apply (default: group) | +| `doc` | `LayoutNode` | The document to wrap | + +Returns: + +| type | description | +| ------------ | ----------------------------------------------------- | +| `LayoutNode` | A LayoutNode that wraps the document in list brackets | + +#### Doc.Builder.**angleBrackets** + +```grain +angleBrackets: + (?wrap: ((doc: LayoutNode) => LayoutNode), doc: LayoutNode) => LayoutNode +``` + +Constructs a node wrapping the given document in angle brackets. + +Parameters: + +| param | type | description | +| ------- | --------------------------------- | ------------------------------------------------------- | +| `?wrap` | `(doc: LayoutNode) => LayoutNode` | An optional wrapping function to apply (default: group) | +| `doc` | `LayoutNode` | The document to wrap | + +Returns: + +| type | description | +| ------------ | ------------------------------------------------------ | +| `LayoutNode` | A LayoutNode that wraps the document in angle brackets | + +#### Doc.Builder.**doubleQuotes** + +```grain +doubleQuotes: (doc: LayoutNode) => LayoutNode +``` + +Constructs a node wrapping the given document in double quotes. + +Parameters: + +| param | type | description | +| ----- | ------------ | -------------------- | +| `doc` | `LayoutNode` | The document to wrap | + +Returns: + +| type | description | +| ------------ | ----------------------------------------------------- | +| `LayoutNode` | A LayoutNode that wraps the document in double quotes | + +#### Doc.Builder.**singleQuotes** + +```grain +singleQuotes: (doc: LayoutNode) => LayoutNode +``` + +Constructs a node wrapping the given document in single quotes. + +Parameters: + +| param | type | description | +| ----- | ------------ | -------------------- | +| `doc` | `LayoutNode` | The document to wrap | + +Returns: + +| type | description | +| ------------ | ----------------------------------------------------- | +| `LayoutNode` | A LayoutNode that wraps the document in single quotes | + +#### Doc.Builder.**trailingComma** + +```grain +trailingComma: () => LayoutNode +``` + +A node that represents a trailing comma to be added if the +current group is broken. + +## Doc.Engine + +The printing engine for LayoutNode documents. + +### Values + +Functions and constants included in the Doc.Engine module. + +#### Doc.Engine.**print** + +```grain +print: + (write: (String => a), eol: EOL, lineWidth: Number, doc: LayoutNode) => + Void +``` + +Prints the given document using the provided write function. + +Parameters: + +| param | type | description | +| ----------- | ------------- | --------------------------------------------------------- | +| `write` | `String => a` | A function that takes a string and writes it to an output | +| `eol` | `EOL` | The end-of-line style to use | +| `lineWidth` | `Number` | The maximum line width to use | +| `doc` | `LayoutNode` | The document to print | + +Returns: + +| type | description | +| ------ | ----------- | +| `Void` | Void | + +#### Doc.Engine.**toString** + +```grain +toString: (eol: EOL, lineWidth: Number, doc: LayoutNode) => String +``` + +Converts the given document to a string using the specified +end-of-line style and line width. + +Parameters: + +| param | type | description | +| ----------- | ------------ | ----------------------------------- | +| `eol` | `EOL` | The end-of-line style to use | +| `lineWidth` | `Number` | The maximum line width to use | +| `doc` | `LayoutNode` | The document to convert to a string | + +Returns: + +| type | description | +| -------- | ----------------------------------------- | +| `String` | The string representation of the document | + diff --git a/stdlib/runtime/miniBuffer.gr b/stdlib/runtime/miniBuffer.gr new file mode 100644 index 0000000000..6538d65e41 --- /dev/null +++ b/stdlib/runtime/miniBuffer.gr @@ -0,0 +1,91 @@ +/** A low level micro buffer implementation. */ +@noPervasives +module MiniBuffer + +from "runtime/unsafe/wasmref" include WasmRef +use WasmRef.{ module WasmArrayRef } +from "runtime/unsafe/wasmi32" include WasmI32 +from "runtime/dataStructures" include DataStructures +use DataStructures.{ + allocateBytes, + getBytesSize, + getBytesArrayRef, + allocateString, + getStringSize, + getStringArrayRef, + tagSimpleNumber, + untagSimpleNumber, +} + +/** A tiny string buffer. */ +abstract record MiniBuffer { + mut data: Bytes, + mut pos: Number, +} + +/** + * Constructs a new MiniBuffer with the given initial size. + * + * @param size: The initial size of the buffer in bytes. + * @returns A new MiniBuffer instance with the specified initial size. + */ +@unsafe +provide let make = (size: WasmI32) => + { data: WasmRef.toGrain(allocateBytes(size)): Bytes, pos: 0 } + +@unsafe +let autogrow = (buffer: MiniBuffer, addedSize: WasmI32) => { + use WasmI32.{ (+), (*), (==), (>=), (<) } + let bufferRef = WasmRef.fromGrain(buffer.data) + let bufferData = getBytesArrayRef(bufferRef) + let mut bufferSize = getBytesSize(bufferRef) + let bufferPosition = untagSimpleNumber(buffer.pos) + let desiredSize = bufferPosition + addedSize + if (bufferSize >= desiredSize) return + if (bufferSize == 0n) bufferSize = 1n + while (bufferSize < desiredSize) bufferSize *= 2n + let newBufferRef = allocateBytes(bufferSize) + let newBufferData = getBytesArrayRef(newBufferRef) + WasmArrayRef.copyI8(newBufferData, 0n, bufferData, 0n, bufferPosition) + buffer.data = WasmRef.toGrain(newBufferRef): Bytes + return +} + +/** + * Appends the given string to the MiniBuffer, automatically growing the buffer if necessary. + * + * @param str: The string to append to the buffer. + * @param buffer: The MiniBuffer to which the string will be appended. + */ +@unsafe +provide let addString = (str: String, buffer: MiniBuffer) => { + use WasmI32.{ (+) } + let strRef = WasmRef.fromGrain(str) + let strData = getStringArrayRef(strRef) + let strSize = getStringSize(strRef) + autogrow(buffer, strSize) + let bufferRef = WasmRef.fromGrain(buffer.data) + let bufferData = getBytesArrayRef(bufferRef) + let bufferPosition = untagSimpleNumber(buffer.pos) + // Write String + WasmArrayRef.copyI8(bufferData, bufferPosition, strData, 0n, strSize) + buffer.pos = tagSimpleNumber(bufferPosition + strSize) +} + +/** + * Converts the contents of the MiniBuffer to a String. + * + * @param buffer: The MiniBuffer whose contents will be converted to a String. + * + * @returns The content of the MiniBuffer as a String. + */ +@unsafe +provide let toString = (buffer: MiniBuffer) => { + let srcRef = WasmRef.fromGrain(buffer.data) + let srcData = getBytesArrayRef(srcRef) + let size = untagSimpleNumber(buffer.pos) + let dstRef = allocateString(size) + let dstData = getStringArrayRef(dstRef) + WasmArrayRef.copyI8(dstData, 0n, srcData, 0n, size) + WasmRef.toGrain(dstRef): String +} diff --git a/stdlib/runtime/miniBuffer.md b/stdlib/runtime/miniBuffer.md new file mode 100644 index 0000000000..36ee45b739 --- /dev/null +++ b/stdlib/runtime/miniBuffer.md @@ -0,0 +1,77 @@ +--- +title: MiniBuffer +--- + +A low level micro buffer implementation. + +## Types + +Type declarations included in the MiniBuffer module. + +### MiniBuffer.**MiniBuffer** + +```grain +type MiniBuffer +``` + +A tiny string buffer. + +## Values + +Functions and constants included in the MiniBuffer module. + +### MiniBuffer.**make** + +```grain +make: (size: WasmI32) => MiniBuffer +``` + +Constructs a new MiniBuffer with the given initial size. + +Parameters: + +| param | type | description | +| ------ | --------- | ---------------------------------------- | +| `size` | `WasmI32` | The initial size of the buffer in bytes. | + +Returns: + +| type | description | +| ------------ | ---------------------------------------------------------- | +| `MiniBuffer` | A new MiniBuffer instance with the specified initial size. | + +### MiniBuffer.**addString** + +```grain +addString: (str: String, buffer: MiniBuffer) => Void +``` + +Appends the given string to the MiniBuffer, automatically growing the buffer if necessary. + +Parameters: + +| param | type | description | +| -------- | ------------ | ---------------------------------------------------- | +| `str` | `String` | The string to append to the buffer. | +| `buffer` | `MiniBuffer` | The MiniBuffer to which the string will be appended. | + +### MiniBuffer.**toString** + +```grain +toString: (buffer: MiniBuffer) => String +``` + +Converts the contents of the MiniBuffer to a String. + +Parameters: + +| param | type | description | +| -------- | ------------ | ------------------------------------------------------------ | +| `buffer` | `MiniBuffer` | The MiniBuffer whose contents will be converted to a String. | + +Returns: + +| type | description | +| -------- | ------------------------------------------ | +| `String` | The content of the MiniBuffer as a String. | + diff --git a/stdlib/runtime/string.gr b/stdlib/runtime/string.gr index 94d12d52ad..b63497bde9 100644 --- a/stdlib/runtime/string.gr +++ b/stdlib/runtime/string.gr @@ -2,294 +2,18 @@ module String from "runtime/unsafe/wasmi32" include WasmI32 -use WasmI32.{ - (+), - (-), - (*), - (/), - remS as (%), - (<<), - (>>), - (&), - (>>>), - (|), - (==), - (!=), - (>=), - (>), - (<=), - (<), -} -from "runtime/unsafe/wasmi64" include WasmI64 -from "runtime/unsafe/wasmf32" include WasmF32 -from "runtime/unsafe/wasmf64" include WasmF64 from "runtime/unsafe/wasmref" include WasmRef use WasmRef.{ module WasmArrayRef } -from "runtime/bigint" include Bigint as BI from "runtime/unsafe/memory" include Memory from "runtime/malloc" include Malloc -from "runtime/unsafe/tags" include Tags -from "runtime/numberUtils" include NumberUtils -from "runtime/utf8" include Utf8 -use Utf8.{ usvEncodeLength, writeUtf8CodePoint } from "runtime/dataStructures" include DataStructures -use DataStructures.{ - allocateString, - allocateArray, - tagSimpleNumber, - untagSimpleNumber, - getCompoundValueArrayRef, - getStringArrayRef, - loadCycleMarker, - storeCycleMarker, - loadAdtVariant, -} +use DataStructures.{ allocateString, getStringArrayRef } +from "runtime/toString" include ToString +use ToString.{ toString } foreign wasm fd_write: (WasmI32, WasmI32, WasmI32, WasmI32) => WasmI32 from "wasi_snapshot_preview1" -primitive (!) = "@not" -primitive (&&) = "@and" -primitive (||) = "@or" -primitive (is) = "@is" -primitive builtinId = "@builtin.id" - -@unsafe -primitive typeMetadata = "@heap.type_metadata" - -@unsafe -let findTypeMetadata = typeHash => { - let typeMetadata = typeMetadata() - if (typeMetadata == -1n) return -1n - let numBuckets = WasmI32.load(typeMetadata, 0n) - if (WasmI32.eqz(numBuckets)) return -1n - let hashHash = typeHash % numBuckets - // First 8 bytes of metadata are for table size - let bucketPtr = typeMetadata + 8n + (hashHash << 3n) // 8 bytes/bucket - let bucketDataOffset = WasmI32.load(bucketPtr, 0n) - let bucketSize = WasmI32.load(bucketPtr, 4n) - let beginDataPtr = typeMetadata + bucketDataOffset - let endDataPtr = beginDataPtr + (bucketSize << 3n) - for (let mut ptr = beginDataPtr; ptr < endDataPtr; ptr += 8n) { - if (WasmI32.load(ptr, 0n) == typeHash) { - return typeMetadata + WasmI32.load(ptr, 4n) - } - } - return -1n -} - -@unsafe -let _LIST_ID = builtinId("List") -@unsafe -let _OPTION_ID = builtinId("Option") -@unsafe -let _RESULT_ID = builtinId("Result") -@unsafe -let _RANGE_ID = builtinId("Range") - -let _SOME = "Some" -let _NONE = "None" -let _OK = "Ok" -let _ERR = "Err" - -let _RANGE_FIELDS = [> "rangeStart", "rangeEnd"] - -@unsafe -let _VISITED_BIT = 0x1n - -// Resizable array -record Vec { - mut size: Number, - mut capacity: Number, - mut data: WasmArrayRef.WasmArrayRef, -} - -@unsafe -let makeVec = () => { - { - size: 0, - capacity: 4, - data: WasmArrayRef.makeAny(4n, WasmRef.fromGrain(void)), - } -} - -@unsafe -let vecPush = (vec, val) => { - if (vec.size is vec.capacity) { - let newCapacity = untagSimpleNumber(vec.capacity) * 2n - let newArray = WasmArrayRef.makeAny(newCapacity, WasmRef.fromGrain(void)) - WasmArrayRef.copyAny( - newArray, - 0n, - vec.data, - 0n, - untagSimpleNumber(vec.capacity) - ) - vec.data = newArray - vec.capacity = tagSimpleNumber(newCapacity) - } - let size = untagSimpleNumber(vec.size) - WasmArrayRef.setAny(vec.data, size, val) - vec.size = tagSimpleNumber(size + 1n) -} - -@unsafe -let vecLen = vec => { - untagSimpleNumber(vec.size) -} - -@unsafe -let vecFindIndex = (vec, val) => { - let len = vecLen(vec) - for (let mut i = 0n; i < len; i += 1n) { - if (WasmArrayRef.getAny(vec.data, i) is val) { - return i - } - } - return -1n -} - -@unsafe -let isListVariant = variant => { - let typeId = DataStructures.loadVariantTypeId(variant) - typeId is _LIST_ID -} - -@unsafe -let isRangeRecord = record_ => { - let typeId = DataStructures.loadRecordTypeId(record_) - typeId is _RANGE_ID -} - -@unsafe -let getBuiltinVariantName = variant => { - let typeId = DataStructures.loadVariantTypeId(variant) - let variantId = loadAdtVariant(variant) - - match (typeId) { - id when id is _OPTION_ID => { - if (variantId is 0) { - Some(WasmRef.fromGrain(_SOME)) - } else { - Some(WasmRef.fromGrain(_NONE)) - } - }, - id when id is _RESULT_ID => { - if (variantId is 0) { - Some(WasmRef.fromGrain(_OK)) - } else { - Some(WasmRef.fromGrain(_ERR)) - } - }, - _ => None, - } -} - -@unsafe -let getFieldArray = (fields, arity) => { - let fieldArray = getCompoundValueArrayRef( - allocateArray(arity, WasmRef.fromGrain(void)) - ) - - let mut fieldOffset = 0n - for (let mut i = 0n; i < arity; i += 1n) { - let fieldLength = WasmI32.load(fields + fieldOffset, 4n) - let fieldName = allocateString(fieldLength) - Memory.copyLinearMemoryToRefArray( - getStringArrayRef(fieldName), - fields + fieldOffset + 8n, - fieldLength - ) - WasmArrayRef.setAny(fieldArray, i, fieldName) - - fieldOffset += WasmI32.load(fields + fieldOffset, 0n) - } - - fieldArray -} - -@unsafe -let getVariantMetadata = variant => { - let typeHash = untagSimpleNumber(DataStructures.loadVariantTypeHash(variant)) - let variantId = untagSimpleNumber(loadAdtVariant(variant)) - - let mut block = findTypeMetadata(typeHash) - - if (block == -1n) return -1n - - let sectionLength = WasmI32.load(block, 0n) - block += 4n - - let end = block + sectionLength - while (block < end) { - if (WasmI32.load(block, 8n) == variantId) { - return block - } - block += WasmI32.load(block, 0n) - } - - return -1n -} - -@unsafe -let getRecordFieldNames = record_ => { - let typeHash = untagSimpleNumber(DataStructures.loadRecordTypeHash(record_)) - if (isRangeRecord(record_)) { - return Some(getCompoundValueArrayRef(WasmRef.fromGrain(_RANGE_FIELDS))) - } else { - let arity = WasmArrayRef.length(getCompoundValueArrayRef(record_)) - - let mut fields = findTypeMetadata(typeHash) - - if (fields == -1n) return None - - fields += 4n - return Some(getFieldArray(fields, arity)) - } -} - -@unsafe -let rec totalBytes = (acc, list) => { - match (list) { - [hd, ...tl] => - totalBytes(acc + DataStructures.getStringSize(WasmRef.fromGrain(hd)), tl), - [] => acc, - } -} - -@unsafe -let rec writeStrings = (buf, offset, list) => { - match (list) { - [hd, ...tl] => { - let hd = getStringArrayRef(WasmRef.fromGrain(hd)) - let hdSize = WasmArrayRef.length(hd) - WasmArrayRef.copyI8(buf, offset, hd, 0n, hdSize) - writeStrings(buf, offset + hdSize, tl) - }, - [] => void, - } -} - -@unsafe -let join = list => { - let len = totalBytes(0n, list) - let str = allocateString(len) - writeStrings(getStringArrayRef(str), 0n, list) - WasmRef.toGrain(str): String -} - -@unsafe -let reverse = list => { - @unsafe - let rec iter = (list, acc) => { - match (list) { - [] => acc, - [first, ...rest] => iter(rest, [first, ...acc]), - } - } - iter(list, []) -} - /** * Concatenate two strings. * @@ -303,6 +27,7 @@ let reverse = list => { */ @unsafe provide let concat = (str1: String, str2: String) => { + use WasmI32.{ (+) } let ref1 = WasmRef.fromGrain(str1) let ref2 = WasmRef.fromGrain(str2) @@ -329,530 +54,7 @@ provide let concat = (str1: String, str2: String) => { WasmRef.toGrain(newString): String } -@unsafe -let escape = (ref, isString) => { - let _SEQ_B = 0x08n - let _SEQ_F = 0x0Cn - let _SEQ_N = 0x0An - let _SEQ_R = 0x0Dn - let _SEQ_T = 0x09n - let _SEQ_V = 0x0Bn - let _SEQ_SLASH = 0x5Cn - let _SEQ_DQUOTE = 0x22n - let _SEQ_SQUOTE = 0x27n - - let _SEQ_QUOTE = if (isString) _SEQ_DQUOTE else _SEQ_SQUOTE - - let data = getStringArrayRef(ref) - let size = WasmArrayRef.length(data) - - let mut newSize = 2n // extra space for quote characters - for (let mut i = 0n; i < size; i += 1n) { - let byte = WasmArrayRef.getI8U(data, i) - if ( - byte >= _SEQ_B && byte <= _SEQ_R - || /* b, f, n, r, t, v */ - byte == _SEQ_SLASH - || byte == _SEQ_QUOTE - ) { - newSize += 2n - } else { - newSize += 1n - } - } - - let escapedString = allocateString(newSize) - let escapedStringArray = getStringArrayRef(escapedString) - - // one extra byte for leading quote character - let mut j = 1n - - for (let mut i = 0n; i < size; i += 1n) { - let byte = WasmArrayRef.getI8U(data, i) - if ( - byte >= _SEQ_B && byte <= _SEQ_R - || /* b, f, n, r, t, v */ - byte == _SEQ_SLASH - || byte == _SEQ_QUOTE - ) { - WasmArrayRef.setI8(escapedStringArray, j, _SEQ_SLASH) - j += 1n - let seq = match (byte) { - b when b == _SEQ_B => 0x62n, - f when f == _SEQ_F => 0x66n, - n when n == _SEQ_N => 0x6en, - r when r == _SEQ_R => 0x72n, - t when t == _SEQ_T => 0x74n, - v when v == _SEQ_V => 0x76n, - _ => byte, - } - WasmArrayRef.setI8(escapedStringArray, j, seq) - j += 1n - } else { - WasmArrayRef.setI8(escapedStringArray, j, byte) - j += 1n - } - } - - WasmArrayRef.setI8(escapedStringArray, 0n, _SEQ_QUOTE) - WasmArrayRef.setI8(escapedStringArray, j, _SEQ_QUOTE) - - WasmRef.toGrain(escapedString): String -} - -@unsafe -let escapeString = (s: String) => { - escape(WasmRef.fromGrain(s), true) -} - -@unsafe -let escapeChar = (s: String) => { - escape(WasmRef.fromGrain(s), false) -} - -@unsafe -let reportCycle = (ref, cycles) => { - let mut cycleNum = vecFindIndex(cycles, ref) - if (cycleNum == -1n) { - cycleNum = vecLen(cycles) - vecPush(cycles, ref) - } - let numStr = NumberUtils.itoa32(cycleNum + 1n, 10n) - join([">"]) -} - -@unsafe -let cyclePrefix = (ref, cycles) => { - let cycleNum = vecFindIndex(cycles, ref) - if (cycleNum != -1n) { - join(["<", NumberUtils.itoa32(cycleNum + 1n, 10n), "> "]) - } else { - "" - } -} - -@unsafe -let rec heapValueToString = (ref, extraIndents, toplevel, cycles) => { - let tag = DataStructures.loadValueTag(ref) - match (tag) { - t when t == Tags._GRAIN_STRING_HEAP_TAG => { - if (toplevel) { - WasmRef.toGrain(ref): String - } else { - escapeString(WasmRef.toGrain(ref)) - } - }, - t when t == Tags._GRAIN_BYTES_HEAP_TAG => { - let data = DataStructures.getBytesArrayRef(ref) - let mut numBytes = DataStructures.getBytesSize(ref) - let mut needsEllipsis = false - if (numBytes > 32n) { - numBytes = 32n - needsEllipsis = true - } - let headBytes = 8n // - } else { - 1n // > - } - let strLen = headBytes + hexBytes + tailBytes - let str = allocateString(strLen) - let strArray = getStringArrayRef(str) - - // >> 8n) - WasmArrayRef.setI8(strArray, j + 2n, 0x20n) - } - - if (needsEllipsis) { - // ...> - WasmArrayRef.setI8(strArray, hexBytes + strOffset, 0x2en) - WasmArrayRef.setI8(strArray, hexBytes + strOffset + 1n, 0x2en) - WasmArrayRef.setI8(strArray, hexBytes + strOffset + 2n, 0x2en) - WasmArrayRef.setI8(strArray, hexBytes + strOffset + 3n, 0x3en) - } else { - // > - WasmArrayRef.setI8(strArray, hexBytes + strOffset, 0x3en) - } - - WasmRef.toGrain(str): String - }, - t when t == Tags._GRAIN_ADT_HEAP_TAG => { - // [ , , , , , elts ... ] - let builtinVariantName = getBuiltinVariantName(ref) - match (builtinVariantName) { - Some(builtinVariantName) => { - // Assumes that all builtin variants do not have inline record - // constructors; if this changes this should be changed as well - tupleVariantToString( - ref, - WasmRef.toGrain(builtinVariantName), - extraIndents, - cycles - ) - }, - None => { - if (isListVariant(ref)) { - listToString(ref, extraIndents, cycles) - } else { - let variantPtr = getVariantMetadata(ref) - if (variantPtr == -1n) { - "" - } else { - let length = WasmI32.load(variantPtr, 12n) - let variantName = allocateString(length) - Memory.copyLinearMemoryToRefArray( - getStringArrayRef(variantName), - variantPtr + 16n, - length - ) - let variantName = WasmRef.toGrain(variantName): String - let distToRecordFields = WasmI32.load(variantPtr, 4n) - let isRecordVariant = distToRecordFields != 0n - if (isRecordVariant) { - let fields = variantPtr + distToRecordFields - let recordArity = WasmArrayRef.length( - getCompoundValueArrayRef(ref) - ) - let recordVariantFields = getFieldArray(fields, recordArity) - let recordString = recordToString( - ref, - recordArity, - recordVariantFields, - extraIndents, - cycles - ) - let strings = [variantName, recordString] - - join(strings) - } else { - tupleVariantToString(ref, variantName, extraIndents, cycles) - } - } - } - }, - } - }, - t when t == Tags._GRAIN_RECORD_HEAP_TAG => { - let recordArity = WasmArrayRef.length(getCompoundValueArrayRef(ref)) - let fields = getRecordFieldNames(ref) - match (fields) { - Some(fields) => { - if (loadCycleMarker(ref) != 0n) { - reportCycle(ref, cycles) - } else { - storeCycleMarker(ref, _VISITED_BIT) - let result = recordToString( - ref, - recordArity, - fields, - extraIndents, - cycles - ) - storeCycleMarker(ref, 0n) - join([cyclePrefix(ref, cycles), result]) - } - }, - None => "", - } - }, - t when t == Tags._GRAIN_ARRAY_HEAP_TAG => { - let arity = WasmArrayRef.length(getCompoundValueArrayRef(ref)) - if (loadCycleMarker(ref) != 0n) { - reportCycle(ref, cycles) - } else { - storeCycleMarker(ref, _VISITED_BIT) - let rbrack = "]" - let mut strings = [rbrack] - let comspace = ", " - let data = getCompoundValueArrayRef(ref) - for (let mut i = arity - 1n; i >= 0n; i -= 1n) { - let item = toStringHelp( - WasmArrayRef.getAny(data, i), - extraIndents, - false, - cycles - ) - strings = [item, ...strings] - if (i > 0n) { - strings = [comspace, ...strings] - } - } - storeCycleMarker(ref, 0n) - let lbrack = "[> " - strings = [lbrack, ...strings] - - join([cyclePrefix(ref, cycles), ...strings]) - } - }, - t when t == Tags._GRAIN_BOXED_NUM_HEAP_TAG => { - let numberTag = DataStructures.getNumberTag(ref) - match (numberTag) { - t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => { - NumberUtils.itoa64(DataStructures.getInt64Value(ref), 10n) - }, - t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => { - BI.bigIntToString10(ref) - }, - t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => { - let numerator = BI.bigIntToString10( - DataStructures.getRationalNumerator(ref) - ) - let denominator = BI.bigIntToString10( - DataStructures.getRationalDenominator(ref) - ) - let slash = "/" - let strings = [numerator, slash, denominator] - join(strings) - }, - t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => { - NumberUtils.dtoa(DataStructures.getFloat64Value(ref)) - }, - _ => { - "" - }, - } - }, - t when t == Tags._GRAIN_INT32_HEAP_TAG => { - NumberUtils.itoa32(DataStructures.getInt32Value(ref), 10n) - }, - t when t == Tags._GRAIN_FLOAT32_HEAP_TAG => { - NumberUtils.dtoa(WasmF64.promoteF32(DataStructures.getFloat32Value(ref))) - }, - t when t == Tags._GRAIN_UINT32_HEAP_TAG => { - NumberUtils.utoa32(DataStructures.getUint32Value(ref), 10n) - }, - t when t == Tags._GRAIN_UINT64_HEAP_TAG => { - NumberUtils.utoa64(DataStructures.getUint64Value(ref), 10n) - }, - t when t == Tags._GRAIN_TUPLE_HEAP_TAG => { - let tupleLength = WasmArrayRef.length(getCompoundValueArrayRef(ref)) - if (loadCycleMarker(ref) != 0n) { - reportCycle(ref, cycles) - } else { - storeCycleMarker(ref, _VISITED_BIT) - let comspace = ", " - let rparen = ")" - let mut lparen = "(" - let mut strings = [rparen] - let data = getCompoundValueArrayRef(ref) - for (let mut i = tupleLength - 1n; i >= 0n; i -= 1n) { - let item = toStringHelp( - WasmArrayRef.getAny(data, i), - extraIndents, - false, - cycles - ) - strings = [item, ...strings] - if (i > 0n) { - strings = [comspace, ...strings] - } - } - storeCycleMarker(ref, 0n) - - strings = [lparen, ...strings] - if (tupleLength <= 1n) { - // Special case: unary tuple, which is not valid Grain syntax; however, boxed values - // are stored as a unary tuple, so we keep this in case one gets printed - strings = ["box", ...strings] - } - join([cyclePrefix(ref, cycles), ...strings]) - } - }, - t when t == Tags._GRAIN_LAMBDA_HEAP_TAG => { - "" - }, - _ => { - let strings = [ - "", - ] - join(strings) - }, - } -} -and toStringHelp = (grainValue, extraIndents, toplevel, cycles) => { - if (WasmRef.isRefI31(grainValue)) { - if (grainValue is WasmRef.fromGrain(true)) { - return "true" - } else if (grainValue is WasmRef.fromGrain(false)) { - return "false" - } else if (grainValue is WasmRef.fromGrain(void)) { - return "void" - } - - let grainValue = WasmRef.i31GetS(grainValue) - if ((grainValue & 1n) != 0n) { - // Simple (unboxed) numbers - return NumberUtils.itoa32(grainValue >> 1n, 10n) - } else { - let tag = grainValue & 7n - if (tag == Tags._GRAIN_SHORTVAL_TAG_TYPE) { - let shortVal = grainValue >> 8n - let shortValTag = (grainValue & 0xF8n) >> 3n - if (shortValTag == Tags._GRAIN_CHAR_SHORTVAL_TAG) { - let byteCount = usvEncodeLength(shortVal) - let string = allocateString(byteCount) - writeUtf8CodePoint(getStringArrayRef(string), 0n, shortVal) - let string = WasmRef.toGrain(string): String - if (toplevel) { - return string - } else { - return escapeChar(string) - } - } else if ( - shortValTag == Tags._GRAIN_INT8_SHORTVAL_TAG - || shortValTag == Tags._GRAIN_INT16_SHORTVAL_TAG - ) { - return NumberUtils.itoa32(shortVal, 10n) - } else if ( - shortValTag == Tags._GRAIN_UINT8_SHORTVAL_TAG - || shortValTag == Tags._GRAIN_UINT16_SHORTVAL_TAG - ) { - return NumberUtils.utoa32(shortVal, 10n) - } else { - return "" - } - } else { - return "" - } - } - } else { - return heapValueToString(grainValue, extraIndents, toplevel, cycles) - } -} -and listToString = (ref, extraIndents, cycles) => { - let mut cur = ref - let mut isFirst = true - - let lbrack = "[" - let commaspace = ", " - let mut strings = [lbrack] - - while (true) { - let variantId = untagSimpleNumber(loadAdtVariant(cur)) - if (variantId == 1n) { - break - } else { - if (!isFirst) { - strings = [commaspace, ...strings] - } - isFirst = false - let data = getCompoundValueArrayRef(cur) - let item = toStringHelp( - WasmArrayRef.getAny(data, 0n), - extraIndents, - false, - cycles - ) - strings = [item, ...strings] - cur = WasmArrayRef.getAny(data, 1n) - } - } - let rbrack = "]" - strings = [rbrack, ...strings] - let reversed = reverse(strings) - join(reversed) -} -and tupleVariantToString = (ref, variantName, extraIndents, cycles) => { - let data = getCompoundValueArrayRef(ref) - let variantArity = WasmArrayRef.length(data) - if (variantArity == 0n) { - variantName - } else { - let comspace = ", " - let rparen = ")" - let mut strings = [rparen] - for (let mut i = variantArity - 1n; i >= 0n; i -= 1n) { - let tmp = toStringHelp( - WasmArrayRef.getAny(data, i), - extraIndents, - false, - cycles - ) - strings = [tmp, ...strings] - if (i > 0n) { - strings = [comspace, ...strings] - } - } - let lparen = "(" - strings = [variantName, lparen, ...strings] - - join(strings) - } -} -and recordToString = (ref, recordArity, fields, extraIndents, cycles) => { - let prevPadAmount = extraIndents * 2n - let prevSpacePadding = if (prevPadAmount == 0n) { - "" - } else { - let v = allocateString(prevPadAmount) - WasmArrayRef.fillI8(getStringArrayRef(v), 0n, 0x20n, prevPadAmount) // create indentation for closing brace - WasmRef.toGrain(v): String - } - let padAmount = (extraIndents + 1n) * 2n - let spacePadding = allocateString(padAmount) - WasmArrayRef.fillI8(getStringArrayRef(spacePadding), 0n, 0x20n, padAmount) // create indentation - let spacePadding = WasmRef.toGrain(spacePadding): String - let newline = "\n" - let rbrace = "}" - let mut strings = [newline, prevSpacePadding, rbrace] - let colspace = ": " - let comlf = ",\n" - let recordValues = getCompoundValueArrayRef(ref) - for (let mut i = recordArity - 1n; i >= 0n; i -= 1n) { - let fieldName = WasmRef.toGrain(WasmArrayRef.getAny(fields, i)): String - let fieldValue = toStringHelp( - WasmArrayRef.getAny(recordValues, i), - extraIndents + 1n, - false, - cycles - ) - strings = [spacePadding, fieldName, colspace, fieldValue, ...strings] - if (i > 0n) { - strings = [comlf, ...strings] - } - } - let lbrace = "{\n" - strings = [lbrace, ...strings] - - join(strings) -} - -/** - * Converts the given operand to a string. - * Provides a better representation of data types if those types are provided from the module. - * - * @param value: The operand - * @returns The operand, as a string - * - * @since v0.1.0 - */ -@unsafe -provide let toString = value => { - let ref = WasmRef.fromGrain(value) - let cycles = makeVec() - toStringHelp(ref, 0n, true, cycles) -} +provide { toString } /** * Prints the given operand to the console. Works for any type. Internally, calls `toString` @@ -867,6 +69,7 @@ provide let toString = value => { */ @unsafe provide let print = (value, suffix="\n") => { + use WasmI32.{ (+) } // First convert the value to string, if it isn't one already. let valueRef = getStringArrayRef(WasmRef.fromGrain(toString(value))) let suffixRef = getStringArrayRef(WasmRef.fromGrain(suffix)) diff --git a/stdlib/runtime/string.md b/stdlib/runtime/string.md index 8d7a5f147d..31085572e6 100644 --- a/stdlib/runtime/string.md +++ b/stdlib/runtime/string.md @@ -49,20 +49,19 @@ No other changes yet. toString: (value: a) => String ``` -Converts the given operand to a string. -Provides a better representation of data types if those types are provided from the module. +Converts any Grain value to its string representation. Parameters: -| param | type | description | -| ------- | ---- | ----------- | -| `value` | `a` | The operand | +| param | type | description | +| ------- | ---- | -------------------------- | +| `value` | `a` | The Grain value to convert | Returns: -| type | description | -| -------- | ------------------------ | -| `String` | The operand, as a string | +| type | description | +| -------- | -------------------------------------------- | +| `String` | The string representation of the Grain value | ### String.**print** diff --git a/stdlib/runtime/toString.gr b/stdlib/runtime/toString.gr new file mode 100644 index 0000000000..243e085b0d --- /dev/null +++ b/stdlib/runtime/toString.gr @@ -0,0 +1,956 @@ +/** + * Utilities for converting Grain values to their string representations. + * + * @example ToString.toString(true) == "true" + * @example ToString.toString(123) == "123" + */ +@runtimeMode +module ToString + +from "runtime/unsafe/typeMetaData" include TypeMetaData +from "runtime/unsafe/wasmref" include WasmRef +use WasmRef.{ module WasmArrayRef } +from "runtime/unsafe/wasmi32" include WasmI32 +from "runtime/unsafe/wasmf64" include WasmF64 +from "runtime/unsafe/conv" include Conv +from "runtime/unsafe/tags" include Tags +from "runtime/bigint" include Bigint +from "runtime/numberUtils" include NumberUtils +from "runtime/dataStructures" include DataStructures +use DataStructures.{ + allocateString, + getStringSize, + getStringArrayRef, + getBytesSize, + getBytesArrayRef, + allocateArray, + getCompoundValueArrayRef, + // Tagging + tagSimpleNumber, + untagSimpleNumber, + // Short Values + untagChar, + untagInt8, + untagInt16, + untagUint8, + untagUint16, +} +from "runtime/utf8" include Utf8 +use Utf8.{ usvEncodeLength, writeUtf8CodePoint } +from "runtime/vector" include Vector +from "runtime/doc" include Doc + +primitive (!) = "@not" +primitive (&&) = "@and" +primitive (is) = "@is" +primitive magic = "@magic" + +module Helpers { + /** + * Checks if the given grain value is a heap value. + * + * @param val: The grain value to check + * + * @returns `true` if the grain value is a heap value, `false` otherwise + */ + @unsafe + provide let isHeapValue = val => { + let ref = WasmRef.fromGrain(val) + WasmRef.isGrainHeapValue(ref) + } + + /** + * Checks if the given grain value is a stack value. + * + * @param val: The grain value to check + * + * @returns `true` if the grain value is a stack value, `false` otherwise + */ + @unsafe + provide let isStackValue = val => { + let ref = WasmRef.fromGrain(val) + WasmRef.isRefI31(ref) + } + + /** + * Checks if the given grain value is a simple number. + * + * NOTE: + * This throws a wasm trap if the value is not a (ref i31), + * isStackValue should be used to check for (ref i31) values + * before calling this function. + * + * @param val: The grain value to check + * + * @returns `true` if the grain value is a simple number, `false` otherwise + */ + @unsafe + provide let isSimpleNumberValue = val => { + use WasmI32.{ (==), (&) } + let ref = WasmRef.fromGrain(val) + (WasmRef.i31GetU(ref) & Tags._GRAIN_NUMBER_TAG_MASK) + == Tags._GRAIN_NUMBER_TAG_TYPE + } + + /** + * Checks if the given grain value is a constant value. + * + * NOTE: + * This throws a wasm trap if the value is not a (ref i31), + * isStackValue should be used to check for (ref i31) values + * before calling this function. + * + * @param val: The grain value to check + * + * @returns `true` if the grain value is a constant value, `false` otherwise + */ + @unsafe + provide let isConstantValue = val => { + use WasmI32.{ (==), (&) } + let ref = WasmRef.fromGrain(val) + (WasmRef.i31GetU(ref) & Tags._GRAIN_GENERIC_TAG_MASK) + == Tags._GRAIN_CONST_TAG_TYPE + } + + /** + * Checks if the given grain value is a short value. + * + * NOTE: + * This throws a wasm trap if the value is not a (ref i31), + * isStackValue should be used to check for (ref i31) values + * before calling this function. + * + * @param val: The grain value to check + * + * @returns `true` if the grain value is a short value, `false` otherwise + */ + @unsafe + provide let isShortValue = val => { + use WasmI32.{ (==), (&) } + let ref = WasmRef.fromGrain(val) + (WasmRef.i31GetU(ref) & Tags._GRAIN_GENERIC_TAG_MASK) + == Tags._GRAIN_SHORTVAL_TAG_TYPE + } + + /** + * Checks if the given grain value is a char short value. + * + * @param ref: The grain value to check + * + * @returns `true` if the grain value is a char short value, `false` otherwise + */ + @unsafe + provide let isChar = ref => { + use WasmI32.{ (==), (&) } + WasmRef.isRefI31(ref) + && (WasmRef.i31GetU(ref) & Tags._GRAIN_GENERIC_SHORTVAL_TAG_MASK) + == Tags._GRAIN_CHAR_SHORTVAL_TAG + } + + /** + * Checks if the given grain value is a int8 short value. + * + * @param ref: The grain value to check + * + * @returns `true` if the grain value is a int8 short value, `false` otherwise + */ + @unsafe + provide let isInt8 = ref => { + use WasmI32.{ (==), (&) } + WasmRef.isRefI31(ref) + && (WasmRef.i31GetU(ref) & Tags._GRAIN_GENERIC_SHORTVAL_TAG_MASK) + == Tags._GRAIN_INT8_TAG_MASK + } + + /** + * Checks if the given grain value is a int16 short value. + * + * @param ref: The grain value to check + * + * @returns `true` if the grain value is a int16 short value, `false` otherwise + */ + @unsafe + provide let isInt16 = ref => { + use WasmI32.{ (==), (&) } + WasmRef.isRefI31(ref) + && (WasmRef.i31GetU(ref) & Tags._GRAIN_GENERIC_SHORTVAL_TAG_MASK) + == Tags._GRAIN_INT16_TAG_MASK + } + + /** + * Checks if the given grain value is a uint8 short value. + * + * @param ref: The grain value to check + * + * @returns `true` if the grain value is a uint8 short value, `false` otherwise + */ + @unsafe + provide let isUInt8 = ref => { + use WasmI32.{ (==), (&) } + WasmRef.isRefI31(ref) + && (WasmRef.i31GetU(ref) & Tags._GRAIN_GENERIC_SHORTVAL_TAG_MASK) + == Tags._GRAIN_UINT8_TAG_MASK + } + + /** + * Checks if the given grain value is a uint16 short value. + * + * @param ref: The grain value to check + * + * @returns `true` if the grain value is a uint16 short value, `false` otherwise + */ + @unsafe + provide let isUInt16 = ref => { + use WasmI32.{ (==), (&) } + WasmRef.isRefI31(ref) + && (WasmRef.i31GetU(ref) & Tags._GRAIN_GENERIC_SHORTVAL_TAG_MASK) + == Tags._GRAIN_UINT16_TAG_MASK + } +} + +@unsafe +let _VISITED_BIT = 0x1n + +/** + * This is the maximum depth we will traverse objects when stringifying. + * + * This exists for multiple reasons the most critical being to prevent stack overflows which happen a little bit + * after a `MAX_DEPTH` of 256. The reason for choosing 32 specifically is that for most data structures + * after a print depth of 32 the output starts to become unwieldy and less useful. + */ +let _MAX_DEPTH = 32 + +// Computes the escaped length of a usv +@unsafe +let computeEscapedUSVLength = (usv: WasmI32, isString: Bool) => { + match (usv) { + 0x08n | 0x0Cn | 0x0An | 0x0Dn | 0x09n | 0x0Bn | 0x5Cn => 2n, + 0x27n when !isString => 2n, + 0x22n when isString => 2n, + _ => usvEncodeLength(usv), + } +} + +// Writes an escaped usv to the given pointer +@unsafe +let writeEscapedUSV = ( + usv: WasmI32, + strData: WasmArrayRef.WasmArrayRef, + index: WasmI32, + isString: Bool, +) => { + use WasmI32.{ (+), (==) } + let mut index = index + if (computeEscapedUSVLength(usv, isString) == 2n) { + index += writeUtf8CodePoint(strData, index, 0x5Cn) // \ + } + index += writeUtf8CodePoint(strData, index, match (usv) { + 0x08n => 0x62n, // \b, + 0x0Cn => 0x66n, // \f + 0x0An => 0x6En, // \n + 0x0Dn => 0x72n, // \r + 0x09n => 0x74n, // \t + 0x0Bn => 0x76n, // \v + 0x5Cn => 0x5Cn, // \\ + 0x27n when !isString => 0x27n, // \' + 0x22n when isString => 0x22n, // \" + _ => usv, + }) + index +} + +/** + * A container for the stringification logic. + * + * This can be exposed if we ever want to allow using the individual data type + * stringification functions externally. + */ +module Stringify { + // Settings + @unsafe + let defaultRadix = 10n // The default radix for numbers + /** + * State used during stringification to track cycles and context. + */ + abstract record StringifyState { + /** Maximum depth to prevent infinite recursion in cyclic structures */ + maxDepth: Number, + /** Indicates if we are currently stringifying the top-level value. */ + topLevel: Bool, + /** A map of heap values to their cycle numbers. */ + cycleMap: Vector.Vector, + } + /** + * Creates an initial empty stringification state. + * + * @returns An empty stringification state + */ + provide let makeState = maxDepth => + { maxDepth, topLevel: true, cycleMap: Vector.make() } + + /** + * Generates the next stringification state for nested values, decrementing the max depth. + */ + provide let nextState = state => { + use WasmI32.{ (-) } + { + ...state, + maxDepth: tagSimpleNumber(untagSimpleNumber(state.maxDepth) - 1n), + topLevel: false, + } + } + + // Helpers for printing cycles + @unsafe + let reportCycle = (ref, cycleVector) => { + use Doc.Builder.{ (++) } + use WasmI32.{ (==), (+) } + let mut cycleIndex = Vector.findIndex(cycleVector, ref) + if (cycleIndex == -1n) { + cycleIndex = Vector.length(cycleVector) + Vector.push(cycleVector, ref) + } + let numStr = NumberUtils.itoa32(cycleIndex + 1n, defaultRadix) + Doc.Builder.angleBrackets( + Doc.Builder.asciiString("cycle to ") + ++ Doc.Builder.angleBrackets(Doc.Builder.asciiString(numStr)) + ) + } + @unsafe + let cyclePrefix = (ref, cycleVector, content) => { + use Doc.Builder.{ (++) } + use WasmI32.{ (+), (!=) } + let cycleIndex = Vector.findIndex(cycleVector, ref) + if (cycleIndex != -1n) { + Doc.Builder.group( + Doc.Builder.angleBrackets( + Doc.Builder.asciiString( + NumberUtils.itoa32(cycleIndex + 1n, defaultRadix) + ) + ) + ++ Doc.Builder.space + ++ content + ) + } else { + content + } + } + + // Heap Values + /** + * Converts a String value to its string representation. + * + * @param val: The String value to convert + * + * @returns A document layout representing the stringified String value + */ + @unsafe + provide let string = (val: String, state) => { + if (state.topLevel) { + Doc.Builder.string(val) + } else { + use WasmI32.{ (+), (<) } + let strRef = WasmRef.fromGrain(val) + let strSize = getStringSize(strRef) + let strData = getStringArrayRef(strRef) + // Compute String Length with Escaping + let mut escapedLength = 0n + for (let mut i = 0n; i < strSize;) { + let usv = Utf8.getCodePoint(strData, i) + i += Utf8.usvEncodeLength(usv) + escapedLength += computeEscapedUSVLength(usv, true) + } + // Write String with Escaping + let escapedStrRef = allocateString(escapedLength) + let escapedStrData = getStringArrayRef(escapedStrRef) + let mut escapedIndex = 0n + for (let mut i = 0n; i < strSize;) { + let usv = Utf8.getCodePoint(strData, i) + i += Utf8.usvEncodeLength(usv) + escapedIndex = writeEscapedUSV(usv, escapedStrData, escapedIndex, true) + } + let content = Doc.Builder.string(WasmRef.toGrain(escapedStrRef): String) + Doc.Builder.doubleQuotes(content) + } + } + /** + * Converts a Bytes value to its string representation. + * + * @param val: The Bytes value to convert + * + * @returns A document layout representing the stringified Bytes value + */ + @unsafe + provide let bytes = (val: Bytes) => { + use Doc.Builder.{ (++) } + use WasmI32.{ (+), (-), (*), (==), (<), (>), (!=) } + let ref = WasmRef.fromGrain(val) + let size = getBytesSize(ref) + let content = if (WasmI32.eqz(size)) { + Doc.Builder.empty + } else { + let data = getBytesArrayRef(ref) + let size = if (size > 33n) 32n else size // Limit to 32 bytes for display + let strLen = size * 3n - 1n // 2 hex digits + 1 space per byte - trailing space + let strRef = allocateString(strLen) + let strData = getStringArrayRef(strRef) + WasmArrayRef.fillI8(strData, 0n, 0x20n, strLen) // Space before each byte except the first + for (let mut i = 0n; i < size; i += 1n) { + use WasmI32.{ (>>>) } + let n = WasmArrayRef.getI8U(data, i) * 2n + let hex = NumberUtils.get_HEX_DIGITS(n) + let strIndex = i * 3n + WasmArrayRef.setI8(strData, strIndex, hex) + WasmArrayRef.setI8(strData, strIndex + 1n, hex >>> 8n) + } + let content = Doc.Builder.string(WasmRef.toGrain(strRef): String) + if (size == 32n) { + content ++ Doc.Builder.asciiString("...") + } else { + content + } + } + Doc.Builder.angleBrackets(Doc.Builder.asciiString("bytes: ") ++ content) + } + + /** + * Converts an Int8 byte value to its string representation. + * + * @param val: The Int8 value to convert + * + * @returns A document layout representing the stringified Int8 value + */ + @unsafe + provide let int8 = (val: Int8) => + Doc.Builder.asciiString(NumberUtils.itoa32(untagInt8(val), defaultRadix)) + /** + * Converts an Int16 value to its string representation. + * + * @param val: The Int16 value to convert + * + * @returns A document layout representing the stringified Int16 value + */ + @unsafe + provide let int16 = (val: Int16) => + Doc.Builder.asciiString(NumberUtils.itoa32(untagInt16(val), defaultRadix)) + + /** + * Converts an Int32 value to its string representation. + * + * @param val: The Int32 value to convert + * + * @returns A document layout representing the stringified Int32 value + */ + @unsafe + provide let int32 = (val: Int32) => { + let num = Conv.fromInt32(val) + Doc.Builder.asciiString(NumberUtils.itoa32(num, defaultRadix)) + } + + /** + * Converts an Int64 value to its string representation. + * + * @param val: The Int64 value to convert + * + * @returns A document layout representing the stringified Int64 value + */ + @unsafe + provide let int64 = (val: Int64) => { + let num = Conv.fromInt64(val) + Doc.Builder.asciiString(NumberUtils.itoa64(num, defaultRadix)) + } + + /** + * Converts a Uint8 byte value to its string representation. + * + * @param val: The Uint8 value to convert + * + * @returns A document layout representing the stringified Uint8 value + */ + @unsafe + provide let uint8 = (val: Uint8) => + Doc.Builder.asciiString(NumberUtils.itoa32(untagUint8(val), defaultRadix)) + /** + * Converts a Uint16 value to its string representation. + * + * @param val: The Uint16 value to convert + * + * @returns A document layout representing the stringified Uint16 value + */ + @unsafe + provide let uint16 = (val: Uint16) => + Doc.Builder.asciiString(NumberUtils.itoa32(untagUint16(val), defaultRadix)) + + /** + * Converts a Uint32 value to its string representation. + * + * @param val: The Uint32 value to convert + * + * @returns A document layout representing the stringified Uint32 value + */ + @unsafe + provide let uint32 = (val: Uint32) => { + let num = Conv.fromUint32(val) + Doc.Builder.asciiString(NumberUtils.utoa32(num, defaultRadix)) + } + /** + * Converts a Uint64 value to its string representation. + * + * @param val: The Uint64 value to convert + * + * @returns A document layout representing the stringified Uint64 value + */ + @unsafe + provide let uint64 = (val: Uint64) => { + let num = Conv.fromUint64(val) + Doc.Builder.asciiString(NumberUtils.utoa64(num, defaultRadix)) + } + + /** + * Converts a Float32 value to its string representation. + * + * @param val: The Float32 value to convert + * + * @returns A document layout representing the stringified Float32 value + */ + @unsafe + provide let float32 = (val: Float32) => { + let num = Conv.fromFloat32(val) + let num = WasmF64.promoteF32(num) + Doc.Builder.asciiString(NumberUtils.dtoa(num)) + } + + /** + * Converts a Float64 value to its string representation. + * + * @param val: The Float64 value to convert + * + * @returns A document layout representing the stringified Float64 value + */ + @unsafe + provide let float64 = (val: Float64) => { + let num = Conv.fromFloat64(val) + Doc.Builder.asciiString(NumberUtils.dtoa(num)) + } + + /** + * Converts a Rational value to its string representation. + * + * @param val: The Rational value to convert + * + * @returns A document layout representing the stringified Rational value + */ + @unsafe + provide let rational = (val: Rational) => { + use Doc.Builder.{ (++) } + let ref = WasmRef.fromGrain(val) + let numerator = DataStructures.getRationalNumerator(ref) + let numerator = Bigint.bigIntToString(numerator, defaultRadix) + let denominator = DataStructures.getRationalDenominator(ref) + let denominator = Bigint.bigIntToString(denominator, defaultRadix) + Doc.Builder.asciiString(numerator) + ++ Doc.Builder.asciiString("/") + ++ Doc.Builder.asciiString(denominator) + } + + /** + * Converts a BigInt value to its string representation. + * + * @param val: The BigInt value to convert + * + * @returns A document layout representing the stringified BigInt value + */ + @unsafe + provide let bigInt = (val: BigInt) => { + let ref = WasmRef.fromGrain(val) + let str = Bigint.bigIntToString(ref, defaultRadix) + Doc.Builder.asciiString(str) + } + + /** + * Converts a Char value to its string representation. + * + * @param val: The Char value to convert + * + * @returns A document layout representing the stringified Char value + */ + @unsafe + provide let char = (val: Char, state) => { + let usv = untagChar(val) + if (state.topLevel) { + let strLength = usvEncodeLength(usv) + let strRef = allocateString(strLength) + let strData = getStringArrayRef(strRef) + writeUtf8CodePoint(strData, 0n, usv) + Doc.Builder.string(WasmRef.toGrain(strRef): String) + } else { + let strLength = computeEscapedUSVLength(usv, false) + let strRef = allocateString(strLength) + let strData = getStringArrayRef(strRef) + writeEscapedUSV(usv, strData, 0n, false) + Doc.Builder.singleQuotes( + Doc.Builder.string(WasmRef.toGrain(strRef): String) + ) + } + } + + /** + * Converts a Number value to its string representation. + * + * @param val: The Number value to convert + * + * @returns A document layout representing the stringified Number value + */ + @unsafe + provide let number = (val: Number) => { + if (Helpers.isStackValue(val) && Helpers.isSimpleNumberValue(val)) { + Doc.Builder.asciiString( + NumberUtils.itoa32(untagSimpleNumber(val), defaultRadix) + ) + } else { + let ref = WasmRef.fromGrain(val) + let tag = DataStructures.getNumberTag(ref) + use WasmI32.{ (==) } + if (tag == Tags._GRAIN_INT64_BOXED_NUM_TAG) { + int64(magic(val): Int64) + } else if (tag == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG) { + float64(magic(val): Float64) + } else if (tag == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG) { + rational(magic(val): Rational) + } else if (tag == Tags._GRAIN_BIGINT_BOXED_NUM_TAG) { + bigInt(magic(val): BigInt) + } else { + Doc.Builder.asciiString("") + } + } + } + + // Polymorphic conversions + let shortValueToString = (ref: WasmRef, state) => { + use WasmI32.{ (==), (&) } + // Get The Short Tag + if (Helpers.isChar(ref)) { + char(WasmRef.toGrain(ref): Char, state) + } else if (Helpers.isInt8(ref)) { + int8(WasmRef.toGrain(ref): Int8) + } else if (Helpers.isInt16(ref)) { + int16(WasmRef.toGrain(ref): Int16) + } else if (Helpers.isUInt8(ref)) { + uint8(WasmRef.toGrain(ref): Uint8) + } else if (Helpers.isUInt16(ref)) { + uint16(WasmRef.toGrain(ref): Uint16) + } else { + Doc.Builder.asciiString("") + } + } + + // Constant Values + @unsafe + let constantValueToString = (ref: WasmRef) => { + if (ref is WasmRef.fromGrain(true)) { + Doc.Builder.asciiString("true") + } else if (ref is WasmRef.fromGrain(false)) { + Doc.Builder.asciiString("false") + } else if (ref is WasmRef.fromGrain(void)) { + Doc.Builder.asciiString("void") + } else { + Doc.Builder.asciiString("") + } + } + @unsafe + let rec heapValueToString = (ref: WasmRef, state) => { + use WasmI32.{ (==), (<=) } + // Get The Heap Tag + let tag = DataStructures.loadValueTag(ref) + if (untagSimpleNumber(state.maxDepth) <= 0n) { + Doc.Builder.asciiString("") + } else if (tag == Tags._GRAIN_TUPLE_HEAP_TAG) { + tuple(ref, state) + } else if (tag == Tags._GRAIN_ARRAY_HEAP_TAG) { + array(WasmRef.toGrain(ref): Array, state) + } else if (tag == Tags._GRAIN_RECORD_HEAP_TAG) { + _record(ref, state) + } else if (tag == Tags._GRAIN_ADT_HEAP_TAG) { + adt(ref, state) + } else if (tag == Tags._GRAIN_LAMBDA_HEAP_TAG) { + Doc.Builder.asciiString("") + } else if (tag == Tags._GRAIN_STRING_HEAP_TAG) { + string(WasmRef.toGrain(ref): String, state) + } else if (tag == Tags._GRAIN_BYTES_HEAP_TAG) { + bytes(WasmRef.toGrain(ref): Bytes) + } else if (tag == Tags._GRAIN_BOXED_NUM_HEAP_TAG) { + number(WasmRef.toGrain(ref): Number) + } else if (tag == Tags._GRAIN_INT32_HEAP_TAG) { + int32(WasmRef.toGrain(ref): Int32) + } else if (tag == Tags._GRAIN_FLOAT32_HEAP_TAG) { + float32(WasmRef.toGrain(ref): Float32) + } else if (tag == Tags._GRAIN_UINT32_HEAP_TAG) { + uint32(WasmRef.toGrain(ref): Uint32) + } else if (tag == Tags._GRAIN_UINT64_HEAP_TAG) { + uint64(WasmRef.toGrain(ref): Uint64) + } else { + Doc.Builder.asciiString("") + } + } + and iterableItemHelp = (val: a, final: Bool, state) => { + use Doc.Builder.{ (++) } + let val = value(val, state) + if (final) { + Doc.Builder.group(val) ++ Doc.Builder.trailingComma() + } else { + Doc.Builder.group(val ++ Doc.Builder.comma) + } + } + and tupleHelp = (items: Array, isVariant, state) => { + use Doc.Builder.{ (++) } + let state = nextState(state) + let content = Doc.Builder.parens( + Doc.Builder.indent( + Doc.Builder.concatArrayMap( + lead=next => Doc.Builder._break, + sep=(prev, next) => Doc.Builder.breakableSpace, + trail=prev => Doc.Builder.empty, + func=(final, item) => + iterableItemHelp(WasmRef.toGrain(item), final, state), + items + ) + ) + ++ Doc.Builder._break + ) + match (items) { + [> _] when !isVariant => Doc.Builder.asciiString("box") ++ content, + _ => content, + } + } + /** + * Converts a tuple value to its string representation. + * + * @param ref: The tuple value to convert + * @param state: The current stringification state + * + * @returns A document layout representing the stringified tuple value + */ + and tuple = (ref: WasmRef, state) => { + use WasmI32.{ (!=) } + if (DataStructures.loadCycleMarker(ref) != 0n) { + reportCycle(ref, state.cycleMap) + } else { + DataStructures.storeCycleMarker(ref, _VISITED_BIT) + let data = TypeMetaData.getTupleData(ref) + let content = tupleHelp(data, false, state) + DataStructures.storeCycleMarker(ref, 0n) + cyclePrefix(ref, state.cycleMap, content) + } + } + /** + * Converts an array value to its string representation. + * + * @param val: The array value to convert + * @param state: The current stringification state + * + * @returns A document layout representing the stringified array value + */ + and array = (val: Array, state) => { + use WasmI32.{ (!=) } + let ref = WasmRef.fromGrain(val) + if (DataStructures.loadCycleMarker(ref) != 0n) { + reportCycle(ref, state.cycleMap) + } else { + use Doc.Builder.{ (++) } + let state = nextState(state) + DataStructures.storeCycleMarker(ref, _VISITED_BIT) + let content = Doc.Builder.arrayBrackets( + Doc.Builder.indent( + Doc.Builder.concatArrayMap( + lead=next => Doc.Builder._break, + sep=(prev, next) => Doc.Builder.breakableSpace, + trail=prev => Doc.Builder.empty, + func=(final, item) => + iterableItemHelp(WasmRef.toGrain(item), final, state), + magic(val): Array + ) + ) + ++ Doc.Builder._break + ) + DataStructures.storeCycleMarker(ref, 0n) + cyclePrefix(ref, state.cycleMap, content) + } + } + /** + * Converts a list value to its string representation. + * + * @param val: The list value to convert + * @param state: The current stringification state + * + * @returns A document layout representing the stringified list value + */ + and list = (val: List, state) => { + use Doc.Builder.{ (++) } + let state = nextState(state) + Doc.Builder.listBrackets( + Doc.Builder.indent( + Doc.Builder.concatMap( + lead=next => Doc.Builder._break, + sep=(prev, next) => Doc.Builder.breakableSpace, + trail=prev => Doc.Builder.empty, + func=(final, item) => + iterableItemHelp(WasmRef.toGrain(item), final, state), + val + ) + ) + ++ Doc.Builder._break + ) + } + /** + * Converts an ADT value to its string representation. + * + * @param ref: The ADT value to convert + * @param state: The current stringification state + * + * @returns A document layout representing the stringified ADT value + */ + and adt = (ref: WasmRef, state) => { + use WasmI32.{ (-), (!=), (>=) } + if (DataStructures.loadCycleMarker(ref) != 0n) { + reportCycle(ref, state.cycleMap) + } else { + use Doc.Builder.{ (++) } + DataStructures.storeCycleMarker(ref, _VISITED_BIT) + let content = if (TypeMetaData.isListVariant(ref)) { + list(WasmRef.toGrain(ref), state) + } else { + let (name, fields) = TypeMetaData.getVariantMetaData(ref) + let fields = getCompoundValueArrayRef(WasmRef.fromGrain(fields)) + let data = getCompoundValueArrayRef(ref) + let arity = WasmArrayRef.length(data) + let valuesRef = allocateArray(arity, WasmRef.fromGrain(void)) + let valuesData = getCompoundValueArrayRef(valuesRef) + let namePortion = Doc.Builder.string(name) + // Get Variant Data + if (WasmI32.eqz(arity)) { + namePortion + } else if (WasmI32.eqz(WasmArrayRef.length(fields))) { + WasmArrayRef.copyAny(valuesData, 0n, data, 0n, arity) + namePortion + ++ tupleHelp( + WasmRef.toGrain(valuesRef): Array, + true, + state + ) + } else { + for (let mut i = arity - 1n; i >= 0n; i -= 1n) { + WasmArrayRef.setAny( + valuesData, + i, + WasmRef.fromGrain( + (WasmArrayRef.getAny(fields, i), WasmArrayRef.getAny(data, i)) + ) + ) + } + namePortion + ++ _recordHelp( + WasmRef.toGrain(valuesRef): Array<(String, WasmRef)>, + state + ) + } + } + DataStructures.storeCycleMarker(ref, 0n) + cyclePrefix(ref, state.cycleMap, content) + } + } + and _recordHelp = (entries: Array<(String, WasmRef)>, state) => { + use Doc.Builder.{ (++) } + let state = nextState(state) + Doc.Builder.braces( + Doc.Builder.indent( + Doc.Builder.concatArrayMap( + lead=next => Doc.Builder.space ++ Doc.Builder._break, + sep=(prev, next) => Doc.Builder.breakableSpace, + trail=last => Doc.Builder.space, + func=(final, (name, ref)) => { + let val = value(WasmRef.toGrain(ref), state) + let entry = Doc.Builder.group( + Doc.Builder.string(name) + ++ Doc.Builder.asciiString(":") + ++ Doc.Builder.space + ++ val + ) + if (final) { + entry ++ match (entries) { + [> _] => Doc.Builder.comma, + _ => Doc.Builder.trailingComma(), + } + } else { + Doc.Builder.group(entry ++ Doc.Builder.comma) + } + }, + entries + ) + ) + ++ Doc.Builder._break + ) + } + /** + * Converts a record value to its string representation. + * + * @param ref: The record value to convert + * @param state: The current stringification state + * + * @returns A document layout representing the stringified record value + */ + and _record = (ref: WasmRef, state) => { + use WasmI32.{ (!=) } + if (DataStructures.loadCycleMarker(ref) != 0n) { + reportCycle(ref, state.cycleMap) + } else { + DataStructures.storeCycleMarker(ref, _VISITED_BIT) + let content = _recordHelp(TypeMetaData.getRecordData(ref), state) + DataStructures.storeCycleMarker(ref, 0n) + cyclePrefix(ref, state.cycleMap, content) + } + } + /** + * Converts a Grain value to its string representation. + * + * @param value: The grain value to convert + * @param state: The current stringification state + * + * @returns A document layout representing the stringified value + */ + and value = (value: a, state) => { + let ref = WasmRef.fromGrain(value) + if (Helpers.isStackValue(ref)) { + if (Helpers.isSimpleNumberValue(value)) { + number(magic(value): Number) + } else if (Helpers.isShortValue(ref)) { + shortValueToString(ref, state) + } else if (Helpers.isConstantValue(ref)) { + constantValueToString(ref) + } else { + Doc.Builder.asciiString("") + } + } else if (Helpers.isHeapValue(ref)) { + heapValueToString(ref, state) + } else { + Doc.Builder.asciiString("") + } + } + + provide { value, array, list, tuple, _record, adt } +} + +/** + * Converts any Grain value to its string representation. + * + * @param value: The Grain value to convert + * + * @returns The string representation of the Grain value + * + * @since v0.1.0 + */ +provide let toString = (value: a) => { + // Note: This is a bit of hack to get around grain's lack of explicit forall types + let value = magic(value): b + let ast = Stringify.value(value, Stringify.makeState(_MAX_DEPTH)) + Doc.Engine.toString(LF, 80, ast) +} diff --git a/stdlib/runtime/toString.md b/stdlib/runtime/toString.md new file mode 100644 index 0000000000..0a01d58de3 --- /dev/null +++ b/stdlib/runtime/toString.md @@ -0,0 +1,43 @@ +--- +title: ToString +--- + +Utilities for converting Grain values to their string representations. + +```grain +ToString.toString(true) == "true" +``` + +```grain +ToString.toString(123) == "123" +``` + +## Values + +Functions and constants included in the ToString module. + +### ToString.**toString** + +
+Added in 0.1.0 +No other changes yet. +
+ +```grain +toString: (value: a) => String +``` + +Converts any Grain value to its string representation. + +Parameters: + +| param | type | description | +| ------- | ---- | -------------------------- | +| `value` | `a` | The Grain value to convert | + +Returns: + +| type | description | +| -------- | -------------------------------------------- | +| `String` | The string representation of the Grain value | + diff --git a/stdlib/runtime/unsafe/typeMetaData.gr b/stdlib/runtime/unsafe/typeMetaData.gr new file mode 100644 index 0000000000..dab2756ca9 --- /dev/null +++ b/stdlib/runtime/unsafe/typeMetaData.gr @@ -0,0 +1,277 @@ +/** + * Unsafe utilities for extracting runtime type information from grain values. + * + * Note: + * This module is unsafe and should be used with caution. + * The grain team offers no guarantees on breaking changes or + * end user support. + */ +@noPervasives +module TypeMetaData + +from "runtime/unsafe/wasmref" include WasmRef +use WasmRef.{ module WasmArrayRef } +from "runtime/unsafe/wasmi32" include WasmI32 +from "runtime/unsafe/memory" include Memory +from "runtime/unsafe/tags" include Tags +from "runtime/dataStructures" include DataStructures +use DataStructures.{ + allocateString, + getStringArrayRef, + allocateArray, + getCompoundValueArrayRef, + untagSimpleNumber, +} + +exception Impossible(String) + +primitive (&&) = "@and" +primitive builtinId = "@builtin.id" +primitive throw = "@throw" + +@unsafe +let _LIST_ID = untagSimpleNumber(builtinId("List")) +@unsafe +let _OPTION_ID = untagSimpleNumber(builtinId("Option")) +@unsafe +let _RESULT_ID = untagSimpleNumber(builtinId("Result")) +@unsafe +let _RANGE_ID = untagSimpleNumber(builtinId("Range")) + +/** + * Provides the tuples contents. + * + * @param ref: A reference to the tuple value to extract data from + * + * @returns An array of references representing the tuple's elements + */ +@unsafe +provide let getTupleData = (ref: WasmRef) => { + let data = getCompoundValueArrayRef(ref) + let arity = WasmArrayRef.length(data) + let arrRef = allocateArray(arity, WasmRef.fromGrain(void)) + let arrData = getCompoundValueArrayRef(arrRef) + WasmArrayRef.copyAny(arrData, 0n, data, 0n, arity) + WasmRef.toGrain(arrRef): Array +} + +primitive typeMetadata = "@heap.type_metadata" +/** + * Finds the type metadata block for a given type hash. + * + * @param typeHash: The type hash to find metadata for + * + * @returns Pointer to the type metadata block, or -1n if not found + */ +@unsafe +let findTypeMetadata = typeHash => { + use WasmI32.{ (+), remS as (%), (==), (<), (<<), (&) } + // Get the pointer to the type metadata table + let mut typeMetadata = typeMetadata() + if (typeMetadata == 0n) return -1n // No metadata registered + let numBuckets = WasmI32.load(typeMetadata, 0n) + if (WasmI32.eqz(numBuckets)) return -1n // No buckets in metadata table + // Locate the block + let bucketHash = (typeHash % numBuckets) << 3n // 8 bytes per entry + let bucketPtr = typeMetadata + 8n + bucketHash // First 8 bytes of metadata are for table size + // Locate the bucket + let bucketOffset = WasmI32.load(bucketPtr, 0n) + let bucketSize = WasmI32.load(bucketPtr, 4n) + // Scan the bucket entries for matching type hash + let startPtr = typeMetadata + bucketOffset + let endPtr = startPtr + (bucketSize << 3n) // 8 bytes per entry + for (let mut blockPtr = startPtr; blockPtr < endPtr; blockPtr += 8n) { + let blockHash = WasmI32.load(blockPtr, 0n) + if (blockHash == typeHash) { + return typeMetadata + WasmI32.load(blockPtr, 4n) // Metadata pointer + } + } + return -1n +} + +@unsafe +let getRecordFields = ( + metaDataPtr: WasmI32, + metaDataEndPtr: WasmI32, + arity: WasmI32, +) => { + use WasmI32.{ (+), (<), (>) } + // + // +0 32-bit size of record field name block + // +4 32-bit length of record field name + // +8 n-bit string + // + let mut metaDataPtr = metaDataPtr + let arrRef = allocateArray(arity, WasmRef.fromGrain(void)) + let arrData = getCompoundValueArrayRef(arrRef) + for (let mut i = 0n; i < arity; i += 1n) { + if (metaDataPtr > metaDataEndPtr) throw Impossible("Invalid metadata") + let blockSize = WasmI32.load(metaDataPtr, 0n) + let length = WasmI32.load(metaDataPtr, 4n) + let strRef = allocateString(length) + let strData = getStringArrayRef(strRef) + Memory.copyLinearMemoryToRefArray(strData, metaDataPtr + 8n, length) + WasmArrayRef.setAny(arrData, i, strRef) + metaDataPtr += blockSize + } + WasmRef.toGrain(arrRef): Array +} + +/** + * Provides the metadata for a record type. + * + * @param ref: The reference to the record value to extract metadata from + * + * @returns The names of the fields in the record, or an empty array if none found + */ +@unsafe +provide let getRecordMetaData = ref => { + use WasmI32.{ (+), (==) } + let typeHash = untagSimpleNumber(DataStructures.loadRecordTypeHash(ref)) + // Lookup Metadata + let metaDataPtr = findTypeMetadata(typeHash) + if (metaDataPtr == -1n) { + let typeTag = untagSimpleNumber(DataStructures.loadRecordTypeId(ref)) + // Possible built-in record type + if (typeTag == _RANGE_ID) { + [> "rangeStart", "rangeEnd"] + } else { + [>] + } + } else { + // +0 32-bit size of complete type info block + // + // +0 32-bit size of record field name block + // +4 32-bit length of record field name + // +8 n-bit string + // + let data = getCompoundValueArrayRef(ref) + let arity = WasmArrayRef.length(data) + let metaDataEndPtr = metaDataPtr + WasmI32.load(metaDataPtr, 0n) + getRecordFields(metaDataPtr + 4n, metaDataEndPtr, arity) + } +} + +/** + * Provides the metadata for a variant type. + * + * @param ref: The reference to the variant value to extract metadata from + * + * @returns The name and fields of the variant + */ +@unsafe +provide let getVariantMetaData = (ref: WasmRef) => { + use WasmI32.{ (+), (==), (<) } + let typeHash = untagSimpleNumber(DataStructures.loadVariantTypeHash(ref)) + let variantTag = untagSimpleNumber(DataStructures.loadAdtVariant(ref)) + let metaDataPtr = findTypeMetadata(typeHash) + if (metaDataPtr == -1n) { + let typeTag = untagSimpleNumber(DataStructures.loadVariantTypeId(ref)) + let name = if (typeTag == _OPTION_ID) { + if (variantTag == 0n) { + "Some" + } else { + "None" + } + } else if (typeTag == _RESULT_ID) { + if (variantTag == 0n) { + "Ok" + } else { + "Err" + } + } else { + "" + } + return (name, [>]) + } else { + // +0 32-bit size of complete type info block + // + // +0 32-bit size of constructor name block + // +4 32-bit offset to field data (for inline record constructors) or 0 (for tuple-like constructors) + // +4 32-bit constructor id + // +8 32-bit length of constructor name + // +12 n-bit string + // (only for inline record constructors) + // +0 32-bit size of record field name block + // +4 32-bit length of record field name + // +8 n-bit string + // + // + let metaDataSize = WasmI32.load(metaDataPtr, 0n) + let metaDataEndPtr = metaDataPtr + metaDataSize + let mut ptr = metaDataPtr + 4n // Skip block size field + // Locate the variant metadata + while (ptr < metaDataEndPtr) { + // Check variant tag + let blockSize = WasmI32.load(ptr, 0n) + let constructorId = WasmI32.load(ptr, 8n) + if (constructorId == variantTag) { + // Read variant name + let strLen = WasmI32.load(ptr, 12n) + let strRef = allocateString(strLen) + let strData = getStringArrayRef(strRef) + let strDataPtr = ptr + 16n // +16 to skip size(4) + offset(4) + id(4) + + Memory.copyLinearMemoryToRefArray(strData, strDataPtr, strLen) + let str = WasmRef.toGrain(strRef): String + // Read Variant Fields + let fieldOffset = WasmI32.load(ptr, 4n) + if (fieldOffset == 0n) return (str, [>]) // Tuple variant + let arity = WasmArrayRef.length(getCompoundValueArrayRef(ref)) + let fields = getRecordFields(ptr + fieldOffset, metaDataEndPtr, arity) + return (str, fields) + } + ptr += blockSize + } + throw Impossible("Invalid metadata") + } +} + +/** + * Provides the records field data along with field names if available. + * + * If the names are not available, the field names will be returned as "". + * + * @param ref: The reference to the record value to extract field data from + * + * @returns An associated array of field names and their corresponding values + */ +@unsafe +provide let getRecordData = (ref: WasmRef) => { + use WasmI32.{ (+), (-), (>=) } + let data = getCompoundValueArrayRef(ref) + let arity = WasmArrayRef.length(data) + // Get field names + let namesRef = WasmRef.fromGrain(getRecordMetaData(ref)) + let namesData = getCompoundValueArrayRef(namesRef) + let noMetaData = WasmI32.eqz(WasmArrayRef.length(namesData)) + // Get field data + let fieldRef = allocateArray(arity, WasmRef.fromGrain(void)) + let fieldData = getCompoundValueArrayRef(fieldRef) + for (let mut i = arity - 1n; i >= 0n; i -= 1n) { + // Determine field name + let name = if (noMetaData) { + "" + } else { + WasmRef.toGrain(WasmArrayRef.getAny(namesData, i)): String + } + // Determine field value + let value = WasmArrayRef.getAny(data, i) + // Pack data + WasmArrayRef.setAny(fieldData, i, WasmRef.fromGrain((name, value))) + } + WasmRef.toGrain(fieldRef): Array<(String, WasmRef)> +} + +/** + * Checks if the given ADT value is a List variant. + * + * @param ref: The ADT value to check. + * + * @returns `true` if the ADT value is a List variant, `false` otherwise. + */ +@unsafe +provide let isListVariant = (ref: WasmRef) => { + use WasmI32.{ (==) } + let typeId = untagSimpleNumber(DataStructures.loadVariantTypeId(ref)) + typeId == _LIST_ID +} diff --git a/stdlib/runtime/unsafe/typeMetaData.md b/stdlib/runtime/unsafe/typeMetaData.md new file mode 100644 index 0000000000..400edae929 --- /dev/null +++ b/stdlib/runtime/unsafe/typeMetaData.md @@ -0,0 +1,117 @@ +--- +title: TypeMetaData +--- + +Unsafe utilities for extracting runtime type information from grain values. + +Note: + This module is unsafe and should be used with caution. + The grain team offers no guarantees on breaking changes or + end user support. + +## Values + +Functions and constants included in the TypeMetaData module. + +### TypeMetaData.**getTupleData** + +```grain +getTupleData: (ref: WasmRef) => Array +``` + +Provides the tuples contents. + +Parameters: + +| param | type | description | +| ----- | --------- | --------------------------------------------------- | +| `ref` | `WasmRef` | A reference to the tuple value to extract data from | + +Returns: + +| type | description | +| ---------------- | -------------------------------------------------------- | +| `Array` | An array of references representing the tuple's elements | + +### TypeMetaData.**getRecordMetaData** + +```grain +getRecordMetaData: (ref: WasmRef) => Array +``` + +Provides the metadata for a record type. + +Parameters: + +| param | type | description | +| ----- | --------- | ---------------------------------------------------------- | +| `ref` | `WasmRef` | The reference to the record value to extract metadata from | + +Returns: + +| type | description | +| --------------- | ---------------------------------------------------------------------- | +| `Array` | The names of the fields in the record, or an empty array if none found | + +### TypeMetaData.**getVariantMetaData** + +```grain +getVariantMetaData: (ref: WasmRef) => (String, Array) +``` + +Provides the metadata for a variant type. + +Parameters: + +| param | type | description | +| ----- | --------- | ----------------------------------------------------------- | +| `ref` | `WasmRef` | The reference to the variant value to extract metadata from | + +Returns: + +| type | description | +| ------------------------- | ---------------------------------- | +| `(String, Array)` | The name and fields of the variant | + +### TypeMetaData.**getRecordData** + +```grain +getRecordData: (ref: WasmRef) => Array<(String, WasmRef)> +``` + +Provides the records field data along with field names if available. + +If the names are not available, the field names will be returned as "". + +Parameters: + +| param | type | description | +| ----- | --------- | ------------------------------------------------------------ | +| `ref` | `WasmRef` | The reference to the record value to extract field data from | + +Returns: + +| type | description | +| -------------------------- | ----------------------------------------------------------------- | +| `Array<(String, WasmRef)>` | An associated array of field names and their corresponding values | + +### TypeMetaData.**isListVariant** + +```grain +isListVariant: (ref: WasmRef) => Bool +``` + +Checks if the given ADT value is a List variant. + +Parameters: + +| param | type | description | +| ----- | --------- | ----------------------- | +| `ref` | `WasmRef` | The ADT value to check. | + +Returns: + +| type | description | +| ------ | ------------------------------------------------------------- | +| `Bool` | `true` if the ADT value is a List variant, `false` otherwise. | + diff --git a/stdlib/runtime/vector.gr b/stdlib/runtime/vector.gr new file mode 100644 index 0000000000..a67f37fa78 --- /dev/null +++ b/stdlib/runtime/vector.gr @@ -0,0 +1,98 @@ +/** A low level resizeable array implementation. */ +@noPervasives +module Vector + +from "runtime/unsafe/wasmi32" include WasmI32 +from "runtime/unsafe/wasmref" include WasmRef +use WasmRef.{ module WasmArrayRef } +from "runtime/dataStructures" include DataStructures +use DataStructures.{ + allocateArray, + getCompoundValueArrayRef, + untagSimpleNumber, + tagSimpleNumber, +} + +primitive (is) = "@is" + +/** A resizeable array. */ +abstract record Vector
{ + mut size: Number, + mut data: WasmArrayRef.WasmArrayRef, +} + +/** Constructs a new Vector. */ +@unsafe +provide let make = () => { + let data = WasmArrayRef.makeAny(8n, WasmRef.fromGrain(void)) + { size: 0, data } +} + +/** + * Gets the length of the Vector. + * + * @param vector: The Vector to inspect + * + * @returns The length of the Vector + */ +@unsafe +provide let length = (vector: Vector) => untagSimpleNumber(vector.size) + +/** + * Appends a value to the end of the Vector, resizing if necessary. + * + * @param vector: The Vector to append to + * @param value: The value to append to the Vector + */ +@unsafe +provide let push = (vector: Vector, value: a) => { + use WasmI32.{ (+), (<<), (==) } + let size = untagSimpleNumber(vector.size) + let capacity = WasmArrayRef.length(vector.data) + if (size == capacity) { + let capacity = capacity << 2n + let data = WasmArrayRef.makeAny(capacity, WasmRef.fromGrain(void)) + WasmArrayRef.copyAny(data, 0n, vector.data, 0n, size) + vector.data = data + } + WasmArrayRef.setAny(vector.data, size, WasmRef.fromGrain(value)) + vector.size = tagSimpleNumber(size + 1n) +} + +/** + * Removes the last element from the vector. + * + * This function does not do a bounds check, it is the callers responsibility + * to ensure the vector is not empty before calling this function. + * + * @param vector: The Vector to pop from + * + * @returns The value that was removed from the vector + */ +@unsafe +provide let pop = (vector: Vector) => { + use WasmI32.{ (-) } + let size = length(vector) + vector.size = tagSimpleNumber(size - 1n) + WasmRef.toGrain(WasmArrayRef.getAny(vector.data, size - 1n)): a +} + +/** + * Finds the index of a reference in the vector, returning -1 if not found. + * + * @param vector: The Vector to search + * @param value: The value to search for + * + * @returns The index of the vector if found, otherwise `-1` + */ +@unsafe +provide let findIndex = (vector: Vector, value: a) => { + use WasmI32.{ (+), (<) } + let size = length(vector) + for (let mut i = 0n; i < size; i += 1n) { + if (WasmArrayRef.getAny(vector.data, i) is value) { + return i + } + } + return -1n +} diff --git a/stdlib/runtime/vector.md b/stdlib/runtime/vector.md new file mode 100644 index 0000000000..84fd5622ae --- /dev/null +++ b/stdlib/runtime/vector.md @@ -0,0 +1,109 @@ +--- +title: Vector +--- + +A low level resizeable array implementation. + +## Types + +Type declarations included in the Vector module. + +### Vector.**Vector** + +```grain +type Vector +``` + +A resizeable array. + +## Values + +Functions and constants included in the Vector module. + +### Vector.**make** + +```grain +make: () => Vector +``` + +Constructs a new Vector. + +### Vector.**length** + +```grain +length: (vector: Vector) => WasmI32 +``` + +Gets the length of the Vector. + +Parameters: + +| param | type | description | +| -------- | ----------- | --------------------- | +| `vector` | `Vector` | The Vector to inspect | + +Returns: + +| type | description | +| --------- | ------------------------ | +| `WasmI32` | The length of the Vector | + +### Vector.**push** + +```grain +push: (vector: Vector, value: a) => Void +``` + +Appends a value to the end of the Vector, resizing if necessary. + +Parameters: + +| param | type | description | +| -------- | ----------- | --------------------------------- | +| `vector` | `Vector` | The Vector to append to | +| `value` | `a` | The value to append to the Vector | + +### Vector.**pop** + +```grain +pop: (vector: Vector) => a +``` + +Removes the last element from the vector. + +This function does not do a bounds check, it is the callers responsibility +to ensure the vector is not empty before calling this function. + +Parameters: + +| param | type | description | +| -------- | ----------- | ---------------------- | +| `vector` | `Vector` | The Vector to pop from | + +Returns: + +| type | description | +| ---- | ------------------------------------------ | +| `a` | The value that was removed from the vector | + +### Vector.**findIndex** + +```grain +findIndex: (vector: Vector, value: WasmRef) => WasmI32 +``` + +Finds the index of a reference in the vector, returning -1 if not found. + +Parameters: + +| param | type | description | +| -------- | ----------------- | ----------------------- | +| `vector` | `Vector` | The Vector to search | +| `value` | `WasmRef` | The value to search for | + +Returns: + +| type | description | +| --------- | ------------------------------------------------ | +| `WasmI32` | The index of the vector if found, otherwise `-1` | +