From f909c18796103876a7899e8ad7a4e1b2ed439a6c Mon Sep 17 00:00:00 2001 From: hiroki23 <33798+hiroki23@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:59:34 +0900 Subject: [PATCH] feat: add Gate 1 QBE vertical slice --- README.md | 17 +- docs/decisions/0003-gate-1-qbe-target.md | 42 ++ docs/experiment-plan.md | 15 +- docs/gate-1-qbe.md | 96 +++ fixtures/gate1/invalid/unknown-field.json | 9 + .../gate1/invalid/unsupported-version.json | 8 + fixtures/gate1/valid/scalar-call.json | 145 ++++ src/gate1_mir.trb | 629 ++++++++++++++++ src/gate1_snapshot.trb | 566 ++++++++++++++ src/gate1_test.trb | 561 ++++++++++++++ src/gate1_toolchain.trb | 100 +++ src/json_boundary.trb | 108 +++ src/qbe.trb | 694 ++++++++++++++++++ src/snapshot.trb | 12 +- 14 files changed, 2989 insertions(+), 13 deletions(-) create mode 100644 docs/decisions/0003-gate-1-qbe-target.md create mode 100644 docs/gate-1-qbe.md create mode 100644 fixtures/gate1/invalid/unknown-field.json create mode 100644 fixtures/gate1/invalid/unsupported-version.json create mode 100644 fixtures/gate1/valid/scalar-call.json create mode 100644 src/gate1_mir.trb create mode 100644 src/gate1_snapshot.trb create mode 100644 src/gate1_test.trb create mode 100644 src/gate1_toolchain.trb create mode 100644 src/json_boundary.trb create mode 100644 src/qbe.trb diff --git a/README.md b/README.md index bb28b031..88a028ef 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,16 @@ semantics; it does not define a native-only TypeRB dialect. Gate 0 implements the experimental boundary in TypeRB: strict decoding of versioned, data-only bootstrap snapshots, lowering to Native MIR, MIR -verification, deterministic diagnostics, and source-origin preservation. It -does not yet generate or execute machine code and provides no production -runtime, stable ABI, stable artifact format, or compatibility guarantee. +verification, deterministic diagnostics, and source-origin preservation. -Gate 1 will use a small, heap-free corpus with -functions, direct calls, control flow, scalar values, exact TypeRB integer and -failure behavior, simple static-layout values, and observable output. +Gate 1 is active. Its first vertical slice decodes snapshot v2, verifies a +heap-free scalar MIR, emits QBE IL from TypeRB code, and builds a working +`darwin/arm64` executable with pinned QBE 1.3 and the system linker. The current +subset includes functions, direct calls, block parameters, branches, loops, +Boolean, portable Integer, binary64 Float, static UTF-8 output, and deterministic +arithmetic failure. It provides no production runtime, stable ABI, stable +artifact format, or compatibility guarantee. Records and tagged values are +deferred to Gate 2. ## Intended boundary @@ -133,8 +136,10 @@ repository. - [Architecture](docs/architecture.md) - [Experiment plan](docs/experiment-plan.md) +- [Gate 1 QBE vertical slice](docs/gate-1-qbe.md) - [Decision 0001: Experimental native toolchain boundary](docs/decisions/0001-experimental-native-toolchain.md) - [Decision 0002: TypeRB-owned self-hosting](docs/decisions/0002-typerb-owned-self-hosting.md) +- [Decision 0003: Gate 1 QBE and Darwin arm64 profile](docs/decisions/0003-gate-1-qbe-target.md) - [Contributing](CONTRIBUTING.md) - [Security](SECURITY.md) diff --git a/docs/decisions/0003-gate-1-qbe-target.md b/docs/decisions/0003-gate-1-qbe-target.md new file mode 100644 index 00000000..32b1e7e9 --- /dev/null +++ b/docs/decisions/0003-gate-1-qbe-target.md @@ -0,0 +1,42 @@ +# 0003: Use QBE 1.3 and a Disposable Darwin arm64 Profile for Gate 1 + +## Status + +Accepted for Gate 1 only. + +## Context + +The first executable gate needs to test TypeRB scalar semantics, runtime +behavior, and native toolchain costs before investing in a broad backend or +stable ABI. Building several complete backends would spend effort before the +shared MIR and runtime questions are understood. + +The maintainer also rejected adding a public `def main(): Integer` form merely +to simplify the bootstrap. Temporary implementation structure is acceptable, +but the experiment must not leave an unnatural language feature behind. + +## Decision + +Gate 1 uses QBE 1.3 at release commit +`c0818978acec60ebb6167fade60fb7012cbf20ca`, verified by release archive SHA-256 +`d587905d620dc5e1d2bfa7c2cc642b9b837aa89a3188c6e37b53d756cf66e320`. +The only target is QBE `arm64_apple`, behind the internal and disposable +`darwin-arm64-v0` ABI profile. + +The TypeRB-authored emitter supplies an exported C `main` wrapper around a +no-argument, `Void` Native MIR entry function. That wrapper is below the +language boundary and does not change TypeRB `main` syntax or semantics. + +Gate 1 is restricted to heap-free scalar execution. Static-layout records and +tagged values move to Gate 2. LLVM, Cranelift, direct machine-code emission, C +emission, and additional targets remain out of scope until Gate 1 measurements +identify a concrete reason to test them. + +## Consequences + +- QBE and the system assembler/linker remain explicit external dependencies. +- The initial ABI and runtime failure surface can be replaced incompatibly. +- A small hand-authored snapshot corpus must execute before a producer is added + to the reference repository. +- Passing this decision's vertical slice justifies Gate 1B integration; it does + not select QBE for production or demonstrate final self-hosted performance. diff --git a/docs/experiment-plan.md b/docs/experiment-plan.md index 4f5b79d3..3b467a68 100644 --- a/docs/experiment-plan.md +++ b/docs/experiment-plan.md @@ -91,18 +91,31 @@ Scope: - Boolean, Integer, and Float values; - exact checked Integer behavior; - static strings and observable output; and -- simple static-layout records or tagged values. +- one disposable `darwin-arm64-v0` ABI profile through QBE 1.3. + +Records and tagged values begin at Gate 2. Gate 1 does not change the TypeRB +`def main()` contract; its no-argument, `Void` MIR entry is an internal +executable convention rather than new language syntax. Every backend candidate at this gate runs the same differential corpus against the reference compiler's Go backend. A mismatch is triaged against the TypeRB specification and accepted conformance behavior rather than automatically treating either implementation as correct. +The pre-registered Gate 1 continuation criteria require complete differential +correctness and at least one representative-corpus improvement: end-to-end +build time by 20%, steady-state execution time by 10%, or stripped executable +size by 30%. The other primary outcomes should remain within 25% of the +stronger applicable optimized Go baseline. A regression greater than 2x stops +the gate for review. TinyGo is measured only if the unchanged corpus works and +the calibration costs no more than half a working day; it is not a deliverable. + ### Gate 2: Portable value model Scope may expand to: - records and payload enums; +- static-layout records and tagged values; - `Result` representation and propagation; - arrays and dynamic strings; - closures and captured environments; and diff --git a/docs/gate-1-qbe.md b/docs/gate-1-qbe.md new file mode 100644 index 00000000..2edbbef7 --- /dev/null +++ b/docs/gate-1-qbe.md @@ -0,0 +1,96 @@ +# Gate 1 QBE Vertical Slice + +Gate 1 tests whether a TypeRB-authored, heap-free scalar pipeline can produce a +correct native executable cheaply enough to justify connecting it to the +reference frontend. It is an experimental checkpoint, not a supported TypeRB +target or a measurement of the final self-hosted compiler. + +## Current pipeline + +```text +snapshot v2 JSON + -> strict TypeRB decoder + -> verified Gate 1 Native MIR + -> TypeRB QBE emitter + -> QBE 1.3 (`arm64_apple`) + -> system assembler and linker + -> Darwin arm64 executable +``` + +The repository owns the decoder, MIR, verifier, QBE lowering, scalar runtime, +and toolchain driver. QBE and the system C toolchain are external dependencies +and must be counted in build and distribution measurements. + +## Pinned backend + +- QBE release: `1.3` +- release commit: `c0818978acec60ebb6167fade60fb7012cbf20ca` +- release archive SHA-256: + `d587905d620dc5e1d2bfa7c2cc642b9b837aa89a3188c6e37b53d756cf66e320` +- QBE target: `arm64_apple` +- experimental ABI profile: `darwin-arm64-v0` + +The ABI profile is disposable and internal. It may change without an adapter. + +## Snapshot v2 + +Snapshot v2 keeps the Gate 0 envelope and adds `entryFunction`. A function has +`id`, `name`, `parameters`, `result`, `entry`, `origin`, and `blocks`. A block +has `id`, `parameters`, `origin`, `instructions`, and one separate +`terminator`. + +Supported scalar types are `Void`, `Boolean`, `Integer`, and `Float`. `Void` is +valid only as a function result. The selected entry function takes no +parameters and returns `Void`. This is an internal executable convention; it +does not add `def main(): Integer` or otherwise change the TypeRB source +contract. + +Supported instructions are: + +- `boolean_literal`, `integer_literal`, and `float_literal`; +- `integer_binary` with `add`, `subtract`, `multiply`, `divide`, or `remainder`; +- `float_binary` with `add`, `subtract`, `multiply`, or `divide`; +- `integer_compare` and `float_compare` with the six ordered comparison names; +- `boolean_not`; +- `call` with a nullable result and direct function identifier; and +- `write_static` for heap-free UTF-8 output. + +Supported terminators are `jump`, `branch`, and `return`. Jump and branch edges +carry typed block arguments. Unknown fields and operations fail explicitly, +and the decoder applies resource limits before code generation. + +## Scalar semantics + +Portable Integer values remain within `-9007199254740991` through +`9007199254740991`. Arithmetic is checked before an out-of-range result can be +observed. Divide-by-zero, remainder-by-zero, and range failures write a fixed +diagnostic to standard error and exit with status 70 in this temporary runtime. +The exact failure text remains experimental until the source-connected +differential corpus establishes the reference behavior. + +Boolean values use QBE words, Integer values use QBE longs, and Float values +use QBE doubles. Non-entry block parameters are lowered through typed stack +slots so the initial emitter does not depend on backend-specific SSA phi +construction. Static UTF-8 output is emitted as QBE data and written without a +heap allocation. + +Every MIR origin is emitted as a deterministic QBE comment containing the +source-table index and span. Runtime source traces are outside Gate 1. + +## Toolchain workspace + +`build_gate1_executable` writes `module.ssa` and `module.s` into a caller-owned +workspace and writes the executable to the requested output path. The caller +owns cleanup. Tests use gate-specific directories under `/tmp`; automation and +benchmark scripts must remove those exact directories after collecting +artifacts. + +## Gate 1B deletion condition + +The reference snapshot producer is temporary. Remove it when the native +TypeRB-authored frontend produces the same verified MIR corpus, or when the +native experiment is abandoned. It must remain process-based and data-only and +must not expose Go compiler objects as an API. + +Records, tagged values, dynamic strings, arrays, allocation, garbage +collection, a stable ABI, and additional targets begin no earlier than Gate 2. diff --git a/fixtures/gate1/invalid/unknown-field.json b/fixtures/gate1/invalid/unknown-field.json new file mode 100644 index 00000000..79fc4d8f --- /dev/null +++ b/fixtures/gate1/invalid/unknown-field.json @@ -0,0 +1,9 @@ +{ + "format": "type-rb-bootstrap-snapshot", + "version": 2, + "module": "example/main", + "entryFunction": "example/main#main", + "sources": [], + "functions": [], + "unexpected": true +} diff --git a/fixtures/gate1/invalid/unsupported-version.json b/fixtures/gate1/invalid/unsupported-version.json new file mode 100644 index 00000000..36662b13 --- /dev/null +++ b/fixtures/gate1/invalid/unsupported-version.json @@ -0,0 +1,8 @@ +{ + "format": "type-rb-bootstrap-snapshot", + "version": 3, + "module": "example/main", + "entryFunction": "example/main#main", + "sources": [], + "functions": [] +} diff --git a/fixtures/gate1/valid/scalar-call.json b/fixtures/gate1/valid/scalar-call.json new file mode 100644 index 00000000..86be1417 --- /dev/null +++ b/fixtures/gate1/valid/scalar-call.json @@ -0,0 +1,145 @@ +{ + "format": "type-rb-bootstrap-snapshot", + "version": 2, + "module": "example/main", + "entryFunction": "example/main#main", + "sources": [ + { "id": "main", "path": "src/main.trb" } + ], + "functions": [ + { + "id": "example/main#add", + "name": "add", + "parameters": [ + { + "id": "left", + "type": "Integer", + "origin": { "source": "main", "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 20 } + }, + { + "id": "right", + "type": "Integer", + "origin": { "source": "main", "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 20 } + } + ], + "result": "Integer", + "entry": "entry", + "origin": { "source": "main", "startLine": 1, "startColumn": 1, "endLine": 3, "endColumn": 4 }, + "blocks": [ + { + "id": "entry", + "parameters": [], + "origin": { "source": "main", "startLine": 2, "startColumn": 1, "endLine": 2, "endColumn": 20 }, + "instructions": [ + { + "op": "integer_binary", + "result": "sum", + "operator": "add", + "left": "left", + "right": "right", + "origin": { "source": "main", "startLine": 2, "startColumn": 9, "endLine": 2, "endColumn": 20 } + } + ], + "terminator": { + "op": "return", + "value": "sum", + "origin": { "source": "main", "startLine": 2, "startColumn": 2, "endLine": 2, "endColumn": 20 } + } + } + ] + }, + { + "id": "example/main#main", + "name": "main", + "parameters": [], + "result": "Void", + "entry": "entry", + "origin": { "source": "main", "startLine": 5, "startColumn": 1, "endLine": 10, "endColumn": 4 }, + "blocks": [ + { + "id": "entry", + "parameters": [], + "origin": { "source": "main", "startLine": 6, "startColumn": 1, "endLine": 8, "endColumn": 20 }, + "instructions": [ + { + "op": "integer_literal", + "result": "forty", + "value": 40, + "origin": { "source": "main", "startLine": 6, "startColumn": 2, "endLine": 6, "endColumn": 3 } + }, + { + "op": "integer_literal", + "result": "two", + "value": 2, + "origin": { "source": "main", "startLine": 6, "startColumn": 5, "endLine": 6, "endColumn": 5 } + }, + { + "op": "call", + "result": "answer", + "function": "example/main#add", + "arguments": ["forty", "two"], + "origin": { "source": "main", "startLine": 6, "startColumn": 7, "endLine": 6, "endColumn": 20 } + }, + { + "op": "integer_literal", + "result": "expected", + "value": 42, + "origin": { "source": "main", "startLine": 7, "startColumn": 1, "endLine": 7, "endColumn": 2 } + }, + { + "op": "integer_compare", + "result": "matches", + "operator": "equal", + "left": "answer", + "right": "expected", + "origin": { "source": "main", "startLine": 7, "startColumn": 4, "endLine": 7, "endColumn": 20 } + } + ], + "terminator": { + "op": "branch", + "condition": "matches", + "whenTrue": "success", + "trueArguments": [], + "whenFalse": "failure", + "falseArguments": [], + "origin": { "source": "main", "startLine": 7, "startColumn": 4, "endLine": 7, "endColumn": 20 } + } + }, + { + "id": "success", + "parameters": [], + "origin": { "source": "main", "startLine": 8, "startColumn": 1, "endLine": 8, "endColumn": 7 }, + "instructions": [ + { + "op": "write_static", + "value": "ok\n", + "origin": { "source": "main", "startLine": 8, "startColumn": 1, "endLine": 8, "endColumn": 7 } + } + ], + "terminator": { + "op": "return", + "value": null, + "origin": { "source": "main", "startLine": 8, "startColumn": 1, "endLine": 8, "endColumn": 7 } + } + }, + { + "id": "failure", + "parameters": [], + "origin": { "source": "main", "startLine": 9, "startColumn": 1, "endLine": 9, "endColumn": 8 }, + "instructions": [ + { + "op": "write_static", + "value": "bad\n", + "origin": { "source": "main", "startLine": 9, "startColumn": 1, "endLine": 9, "endColumn": 8 } + } + ], + "terminator": { + "op": "return", + "value": null, + "origin": { "source": "main", "startLine": 9, "startColumn": 1, "endLine": 9, "endColumn": 8 } + } + } + ] + } + ] +} diff --git a/src/gate1_mir.trb b/src/gate1_mir.trb new file mode 100644 index 00000000..192e4512 --- /dev/null +++ b/src/gate1_mir.trb @@ -0,0 +1,629 @@ +import { Diagnostic, boundary_diagnostic } from diagnostic +import { NativeSource } from native_mir +import { SourceOrigin } from snapshot +import { Result } from trb/std/result + +enum ScalarType + Void + Boolean + Integer + Float +end + +enum IntegerBinaryOperator + Add + Subtract + Multiply + Divide + Remainder +end + +enum FloatBinaryOperator + Add + Subtract + Multiply + Divide +end + +enum ComparisonOperator + Equal + NotEqual + LessThan + LessThanOrEqual + GreaterThan + GreaterThanOrEqual +end + +record Gate1Parameter + id: String + value_type: ScalarType + origin: SourceOrigin +end + +enum Gate1Instruction + BooleanLiteral(result: String, value: Boolean, origin: SourceOrigin) + IntegerLiteral(result: String, value: Integer, origin: SourceOrigin) + FloatLiteral(result: String, value: Float, origin: SourceOrigin) + IntegerBinary( + result: String, + operator: IntegerBinaryOperator, + left: String, + right: String, + origin: SourceOrigin, + ) + FloatBinary( + result: String, + operator: FloatBinaryOperator, + left: String, + right: String, + origin: SourceOrigin, + ) + IntegerCompare( + result: String, + operator: ComparisonOperator, + left: String, + right: String, + origin: SourceOrigin, + ) + FloatCompare( + result: String, + operator: ComparisonOperator, + left: String, + right: String, + origin: SourceOrigin, + ) + BooleanNot(result: String, value: String, origin: SourceOrigin) + Call(result: String?, function_id: String, arguments: Array, origin: SourceOrigin) + WriteStatic(value: String, origin: SourceOrigin) +end + +enum Gate1Terminator + Jump(target: String, arguments: Array, origin: SourceOrigin) + Branch( + condition: String, + when_true: String, + true_arguments: Array, + when_false: String, + false_arguments: Array, + origin: SourceOrigin, + ) + Return(value: String?, origin: SourceOrigin) +end + +record Gate1Block + id: String + parameters: Array + origin: SourceOrigin + instructions: Array + terminator: Gate1Terminator +end + +record Gate1Function + id: String + name: String + parameters: Array + result_type: ScalarType + entry: String + origin: SourceOrigin + blocks: Array +end + +record Gate1Module + id: String + entry_function: String + sources: Array + functions: Array +end + +def verify_gate1_mir(native_module: Gate1Module): Result + if native_module.sources.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", "/sources", "Gate 1 MIR must declare at least one source") + ) + end + if native_module.functions.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", "/functions", "Gate 1 MIR must contain at least one function") + ) + end + mut source_ids: Array := [] + mut source_index := 0 + while source_index < native_module.sources.size() + source := native_module.sources[source_index] + if source_ids.include?(source.id) + return Result::Err( + boundary_diagnostic( + "TRBN0004", + "/sources/" + source_index.to_s() + "/id", + "Gate 1 MIR source identifier is duplicated", + ) + ) + end + source_ids.push(source.id) + source_index += 1 + end + mut function_ids: Hash := {} + mut function_index := 0 + while function_index < native_module.functions.size() + function := native_module.functions[function_index] + path := "/functions/" + function_index.to_s() + if function_ids.key?(function.id) + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/id", "Gate 1 MIR function identifier is duplicated") + ) + end + function_ids[function.id] = true + try _verify_gate1_origin(function.origin, source_ids, path + "/origin") + function_index += 1 + end + entry_function := _gate1_function(native_module.functions, native_module.entry_function) + if entry_function == nil + return Result::Err( + boundary_diagnostic("TRBN0004", "/entryFunction", "Gate 1 MIR entry function does not exist") + ) + end + if !entry_function.parameters.empty?() || entry_function.result_type != ScalarType::Void + return Result::Err( + boundary_diagnostic( + "TRBN0004", + "/entryFunction", + "Gate 1 MIR entry function must take no arguments and return Void", + ) + ) + end + function_index = 0 + while function_index < native_module.functions.size() + try _verify_gate1_function( + native_module.functions[function_index], + native_module.functions, + source_ids, + "/functions/" + function_index.to_s(), + ) + function_index += 1 + end + return Result::Ok(true) +end + +def _verify_gate1_function( + function: Gate1Function, + functions: Array, + source_ids: Array, + path: String, + ): Result + if function.blocks.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/blocks", "Gate 1 MIR function has no blocks") + ) + end + mut definitions: Hash := {} + mut parameter_index := 0 + while parameter_index < function.parameters.size() + parameter := function.parameters[parameter_index] + parameter_path := path + "/parameters/" + parameter_index.to_s() + try _verify_gate1_parameter(parameter, source_ids, parameter_path) + try _gate1_define(definitions, parameter.id, parameter.value_type, parameter_path + "/id") + parameter_index += 1 + end + mut block_ids: Hash := {} + mut entry_found := false + mut block_index := 0 + while block_index < function.blocks.size() + block := function.blocks[block_index] + block_path := path + "/blocks/" + block_index.to_s() + if block_ids.key?(block.id) + return Result::Err( + boundary_diagnostic("TRBN0004", block_path + "/id", "Gate 1 MIR block identifier is duplicated") + ) + end + block_ids[block.id] = true + if block.id == function.entry + entry_found = true + if !block.parameters.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", block_path + "/parameters", "entry block must not have parameters") + ) + end + end + try _verify_gate1_origin(block.origin, source_ids, block_path + "/origin") + parameter_index = 0 + while parameter_index < block.parameters.size() + parameter := block.parameters[parameter_index] + parameter_path := block_path + "/parameters/" + parameter_index.to_s() + try _verify_gate1_parameter(parameter, source_ids, parameter_path) + try _gate1_define(definitions, parameter.id, parameter.value_type, parameter_path + "/id") + parameter_index += 1 + end + mut instruction_index := 0 + while instruction_index < block.instructions.size() + try _gate1_instruction_definition( + block.instructions[instruction_index], + functions, + definitions, + block_path + "/instructions/" + instruction_index.to_s(), + ) + instruction_index += 1 + end + block_index += 1 + end + if !entry_found + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/entry", "Gate 1 MIR entry block does not exist") + ) + end + block_index = 0 + while block_index < function.blocks.size() + try _verify_gate1_block( + function, + function.blocks[block_index], + functions, + definitions, + source_ids, + path + "/blocks/" + block_index.to_s(), + ) + block_index += 1 + end + return Result::Ok(true) +end + +def _gate1_instruction_definition( + instruction: Gate1Instruction, + functions: Array, + mut definitions: Hash, + path: String, + ): Result + case instruction + when Gate1Instruction::BooleanLiteral(result, _value, _origin) + return _gate1_define(definitions, result, ScalarType::Boolean, path + "/result") + when Gate1Instruction::IntegerLiteral(result, _value, _origin) + return _gate1_define(definitions, result, ScalarType::Integer, path + "/result") + when Gate1Instruction::FloatLiteral(result, _value, _origin) + return _gate1_define(definitions, result, ScalarType::Float, path + "/result") + when Gate1Instruction::IntegerBinary(result, _operator, _left, _right, _origin) + return _gate1_define(definitions, result, ScalarType::Integer, path + "/result") + when Gate1Instruction::FloatBinary(result, _operator, _left, _right, _origin) + return _gate1_define(definitions, result, ScalarType::Float, path + "/result") + when Gate1Instruction::IntegerCompare(result, _operator, _left, _right, _origin) + return _gate1_define(definitions, result, ScalarType::Boolean, path + "/result") + when Gate1Instruction::FloatCompare(result, _operator, _left, _right, _origin) + return _gate1_define(definitions, result, ScalarType::Boolean, path + "/result") + when Gate1Instruction::BooleanNot(result, _value, _origin) + return _gate1_define(definitions, result, ScalarType::Boolean, path + "/result") + when Gate1Instruction::Call(result, function_id, _arguments, _origin) + if result == nil + return Result::Ok(true) + end + callee := _gate1_function(functions, function_id) + if callee == nil + return _gate1_define(definitions, result, ScalarType::Void, path + "/result") + end + return _gate1_define(definitions, result, callee.result_type, path + "/result") + when Gate1Instruction::WriteStatic(_value, _origin) + return Result::Ok(true) + end +end + +def _verify_gate1_block( + function: Gate1Function, + block: Gate1Block, + functions: Array, + definitions: Hash, + source_ids: Array, + path: String, + ): Result + mut available: Hash := {} + mut index := 0 + while index < function.parameters.size() + parameter := function.parameters[index] + available[parameter.id] = parameter.value_type + index += 1 + end + index = 0 + while index < block.parameters.size() + parameter := block.parameters[index] + available[parameter.id] = parameter.value_type + index += 1 + end + index = 0 + while index < block.instructions.size() + instruction_path := path + "/instructions/" + index.to_s() + try _verify_gate1_instruction( + block.instructions[index], + functions, + definitions, + available, + source_ids, + instruction_path, + ) + _gate1_make_available(block.instructions[index], definitions, available) + index += 1 + end + try _verify_gate1_terminator(function, block.terminator, available, source_ids, path + "/terminator") + return Result::Ok(true) +end + +def _verify_gate1_instruction( + instruction: Gate1Instruction, + functions: Array, + definitions: Hash, + available: Hash, + source_ids: Array, + path: String, + ): Result + case instruction + when Gate1Instruction::BooleanLiteral(_result, _value, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + when Gate1Instruction::IntegerLiteral(_result, value, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + if value < - 9007199254740991 || value > 9007199254740991 + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/value", "Integer literal is outside the portable range") + ) + end + when Gate1Instruction::FloatLiteral(_result, value, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + if !value.finite?() + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/value", "Float literal must be finite") + ) + end + when Gate1Instruction::IntegerBinary(_result, _operator, left, right, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + try _gate1_require(available, left, ScalarType::Integer, path + "/left") + try _gate1_require(available, right, ScalarType::Integer, path + "/right") + when Gate1Instruction::FloatBinary(_result, _operator, left, right, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + try _gate1_require(available, left, ScalarType::Float, path + "/left") + try _gate1_require(available, right, ScalarType::Float, path + "/right") + when Gate1Instruction::IntegerCompare(_result, _operator, left, right, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + try _gate1_require(available, left, ScalarType::Integer, path + "/left") + try _gate1_require(available, right, ScalarType::Integer, path + "/right") + when Gate1Instruction::FloatCompare(_result, _operator, left, right, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + try _gate1_require(available, left, ScalarType::Float, path + "/left") + try _gate1_require(available, right, ScalarType::Float, path + "/right") + when Gate1Instruction::BooleanNot(_result, value, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + try _gate1_require(available, value, ScalarType::Boolean, path + "/value") + when Gate1Instruction::Call(result, function_id, arguments, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + callee := _gate1_function(functions, function_id) + if callee == nil + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/function", "call references an unknown function") + ) + end + if arguments.size() != callee.parameters.size() + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/arguments", "call argument count does not match the callee") + ) + end + mut argument_index := 0 + while argument_index < arguments.size() + try _gate1_require( + available, + arguments[argument_index], + callee.parameters[argument_index].value_type, + path + "/arguments/" + argument_index.to_s(), + ) + argument_index += 1 + end + if callee.result_type == ScalarType::Void + if result != nil + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/result", "Void call must not define a result") + ) + end + else + if result == nil + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/result", "non-Void call must define a result") + ) + end + if definitions.fetch(result) != callee.result_type + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/result", "call result type does not match the callee") + ) + end + end + when Gate1Instruction::WriteStatic(_value, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + end + return Result::Ok(true) +end + +def _gate1_make_available( + instruction: Gate1Instruction, + definitions: Hash, + mut available: Hash, + ) + case instruction + when Gate1Instruction::BooleanLiteral(result, _value, _origin) + available[result] = definitions.fetch(result) + when Gate1Instruction::IntegerLiteral(result, _value, _origin) + available[result] = definitions.fetch(result) + when Gate1Instruction::FloatLiteral(result, _value, _origin) + available[result] = definitions.fetch(result) + when Gate1Instruction::IntegerBinary(result, _operator, _left, _right, _origin) + available[result] = definitions.fetch(result) + when Gate1Instruction::FloatBinary(result, _operator, _left, _right, _origin) + available[result] = definitions.fetch(result) + when Gate1Instruction::IntegerCompare(result, _operator, _left, _right, _origin) + available[result] = definitions.fetch(result) + when Gate1Instruction::FloatCompare(result, _operator, _left, _right, _origin) + available[result] = definitions.fetch(result) + when Gate1Instruction::BooleanNot(result, _value, _origin) + available[result] = definitions.fetch(result) + when Gate1Instruction::Call(result, _function_id, _arguments, _origin) + if result != nil + available[result] = definitions.fetch(result) + end + when Gate1Instruction::WriteStatic(_value, _origin) + end + return +end + +def _verify_gate1_terminator( + function: Gate1Function, + terminator: Gate1Terminator, + available: Hash, + source_ids: Array, + path: String, + ): Result + case terminator + when Gate1Terminator::Jump(target, arguments, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + try _verify_gate1_target(function, target, arguments, available, path) + when Gate1Terminator::Branch(condition, when_true, true_arguments, when_false, false_arguments, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + try _gate1_require(available, condition, ScalarType::Boolean, path + "/condition") + try _verify_gate1_target(function, when_true, true_arguments, available, path + "/true") + try _verify_gate1_target(function, when_false, false_arguments, available, path + "/false") + when Gate1Terminator::Return(value, origin) + try _verify_gate1_origin(origin, source_ids, path + "/origin") + if function.result_type == ScalarType::Void + if value != nil + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/value", "Void function must not return a value") + ) + end + else + if value == nil + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/value", "non-Void function must return a value") + ) + end + try _gate1_require(available, value, function.result_type, path + "/value") + end + end + return Result::Ok(true) +end + +def _verify_gate1_target( + function: Gate1Function, + target: String, + arguments: Array, + available: Hash, + path: String, + ): Result + block := _gate1_block(function.blocks, target) + if block == nil + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/target", "terminator references an unknown block") + ) + end + if arguments.size() != block.parameters.size() + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/arguments", "block argument count does not match the target") + ) + end + mut index := 0 + while index < arguments.size() + try _gate1_require( + available, + arguments[index], + block.parameters[index].value_type, + path + "/arguments/" + index.to_s(), + ) + index += 1 + end + return Result::Ok(true) +end + +def _verify_gate1_parameter( + parameter: Gate1Parameter, + source_ids: Array, + path: String, + ): Result + if parameter.value_type == ScalarType::Void + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/type", "parameter type must not be Void") + ) + end + try _verify_gate1_origin(parameter.origin, source_ids, path + "/origin") + return Result::Ok(true) +end + +def _gate1_define( + mut definitions: Hash, + id: String, + value_type: ScalarType, + path: String, + ): Result + if id.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", path, "value identifier must not be empty") + ) + end + if definitions.key?(id) + return Result::Err( + boundary_diagnostic("TRBN0004", path, "value identifier is defined more than once") + ) + end + definitions[id] = value_type + return Result::Ok(true) +end + +def _gate1_require( + available: Hash, + id: String, + expected: ScalarType, + path: String, + ): Result + if !available.key?(id) + return Result::Err( + boundary_diagnostic("TRBN0004", path, "operand is not available in this block") + ) + end + if available.fetch(id) != expected + return Result::Err( + boundary_diagnostic("TRBN0004", path, "operand type does not match the operation") + ) + end + return Result::Ok(true) +end + +def _gate1_function(functions: Array, id: String): Gate1Function? + mut index := 0 + while index < functions.size() + if functions[index].id == id + return functions[index] + end + index += 1 + end + return nil +end + +def _gate1_block(blocks: Array, id: String): Gate1Block? + mut index := 0 + while index < blocks.size() + if blocks[index].id == id + return blocks[index] + end + index += 1 + end + return nil +end + +def _verify_gate1_origin( + origin: SourceOrigin, + source_ids: Array, + path: String, + ): Result + if !source_ids.include?(origin.source_id) + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/source", "origin references an unknown source") + ) + end + if origin.start_line < 1 || origin.start_column < 1 || origin.end_line < 1 || origin.end_column < 1 + return Result::Err( + boundary_diagnostic("TRBN0004", path, "origin coordinates must be positive") + ) + end + if origin.end_line < origin.start_line || (origin.end_line == origin.start_line && origin.end_column < origin.start_column) + return Result::Err( + boundary_diagnostic("TRBN0004", path, "origin end precedes its start") + ) + end + return Result::Ok(true) +end diff --git a/src/gate1_snapshot.trb b/src/gate1_snapshot.trb new file mode 100644 index 00000000..8ded490d --- /dev/null +++ b/src/gate1_snapshot.trb @@ -0,0 +1,566 @@ +import { Diagnostic, boundary_diagnostic } from diagnostic +import { + ComparisonOperator, + FloatBinaryOperator, + Gate1Block, + Gate1Function, + Gate1Instruction, + Gate1Module, + Gate1Parameter, + Gate1Terminator, + IntegerBinaryOperator, + ScalarType, + verify_gate1_mir, +} from gate1_mir +import { NativeSource } from native_mir +import { + object, + required_array, + required_field, + required_integer, + required_string, + strict_object, +} from json_boundary +import { + decode_origin, +} from snapshot +import { JsonValue, parse } from trb/std/json +import { Result } from trb/std/result + +GATE1_MAX_INPUT_BYTES := 4194304 +GATE1_MAX_SOURCES := 256 +GATE1_MAX_FUNCTIONS := 512 +GATE1_MAX_PARAMETERS := 256 +GATE1_MAX_BLOCKS := 1024 +GATE1_MAX_INSTRUCTIONS := 16384 +GATE1_MAX_ARGUMENTS := 256 +GATE1_MAX_STATIC_BYTES := 1048576 +GATE1_MAX_IDENTIFIER_SIZE := 256 +GATE1_MAX_SOURCE_PATH_SIZE := 4096 + +def decode_gate1_snapshot(source: String): Result + if source.to_bytes().size() > GATE1_MAX_INPUT_BYTES + return Result::Err( + boundary_diagnostic("TRBN0005", "", "snapshot exceeds the Gate 1 4 MiB input limit") + ) + end + root := case parse(source) + when Result::Ok(value) + value + when Result::Err(error) + return Result::Err( + Diagnostic.new( + code: "TRBN0001", + path: error.path, + message: error.message, + start_line: error.line, + start_column: error.column, + ) + ) + end + fields := try strict_object( + root, + "", + ["format", "version", "module", "entryFunction", "sources", "functions"], + ) + format := try required_string(fields, "format", "", false) + if format != "type-rb-bootstrap-snapshot" + return Result::Err( + boundary_diagnostic("TRBN0003", "/format", "unsupported snapshot format") + ) + end + version := try required_integer(fields, "version", "") + if version != 2 + return Result::Err( + boundary_diagnostic("TRBN0003", "/version", "unsupported Gate 1 snapshot version") + ) + end + source_values := try required_array(fields, "sources", "") + if source_values.size() > GATE1_MAX_SOURCES + return Result::Err( + boundary_diagnostic("TRBN0005", "/sources", "source count exceeds the Gate 1 limit") + ) + end + mut sources: Array := [] + mut source_index := 0 + while source_index < source_values.size() + sources.push(try _gate1_decode_source(source_values[source_index], "/sources/" + source_index.to_s())) + source_index += 1 + end + function_values := try required_array(fields, "functions", "") + if function_values.size() > GATE1_MAX_FUNCTIONS + return Result::Err( + boundary_diagnostic("TRBN0005", "/functions", "function count exceeds the Gate 1 limit") + ) + end + mut functions: Array := [] + mut function_index := 0 + while function_index < function_values.size() + functions.push(try _gate1_decode_function(function_values[function_index], "/functions/" + function_index.to_s())) + function_index += 1 + end + native_module := Gate1Module.new( + id: try required_string(fields, "module", "", true), + entry_function: try required_string(fields, "entryFunction", "", true), + sources: sources, + functions: functions, + ) + try verify_gate1_mir(native_module) + return Result::Ok(native_module) +end + +def _gate1_decode_source(value: JsonValue, path: String): Result + fields := try strict_object(value, path, ["id", "path"]) + source_path := try required_string(fields, "path", path, false) + if source_path.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/path", "source path must not be empty") + ) + end + if source_path.size() > GATE1_MAX_SOURCE_PATH_SIZE + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/path", "source path exceeds the Gate 1 limit") + ) + end + return Result::Ok( + NativeSource.new( + id: try required_string(fields, "id", path, true), + path: source_path, + ) + ) +end + +def _gate1_decode_function(value: JsonValue, path: String): Result + fields := try strict_object(value, path, ["id", "name", "parameters", "result", "entry", "origin", "blocks"]) + parameter_values := try required_array(fields, "parameters", path) + if parameter_values.size() > GATE1_MAX_PARAMETERS + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/parameters", "parameter count exceeds the Gate 1 limit") + ) + end + mut parameters: Array := [] + mut parameter_index := 0 + while parameter_index < parameter_values.size() + parameters.push( + try _gate1_decode_parameter(parameter_values[parameter_index], path + "/parameters/" + parameter_index.to_s()) + ) + parameter_index += 1 + end + block_values := try required_array(fields, "blocks", path) + if block_values.size() > GATE1_MAX_BLOCKS + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/blocks", "block count exceeds the Gate 1 limit") + ) + end + mut blocks: Array := [] + mut block_index := 0 + while block_index < block_values.size() + blocks.push(try _gate1_decode_block(block_values[block_index], path + "/blocks/" + block_index.to_s())) + block_index += 1 + end + return Result::Ok( + Gate1Function.new( + id: try required_string(fields, "id", path, true), + name: try required_string(fields, "name", path, true), + parameters: parameters, + result_type: try _gate1_decode_type(try required_string(fields, "result", path, false), path + "/result", true), + entry: try required_string(fields, "entry", path, true), + origin: try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + blocks: blocks, + ) + ) +end + +def _gate1_decode_parameter(value: JsonValue, path: String): Result + fields := try strict_object(value, path, ["id", "type", "origin"]) + return Result::Ok( + Gate1Parameter.new( + id: try required_string(fields, "id", path, true), + value_type: try _gate1_decode_type(try required_string(fields, "type", path, false), path + "/type", false), + origin: try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + ) + ) +end + +def _gate1_decode_type(name: String, path: String, allow_void: Boolean): Result + if name == "Boolean" + return Result::Ok(ScalarType::Boolean) + end + if name == "Integer" + return Result::Ok(ScalarType::Integer) + end + if name == "Float" + return Result::Ok(ScalarType::Float) + end + if allow_void && name == "Void" + return Result::Ok(ScalarType::Void) + end + return Result::Err( + boundary_diagnostic("TRBN0003", path, "unsupported Gate 1 scalar type") + ) +end + +def _gate1_decode_block(value: JsonValue, path: String): Result + fields := try strict_object(value, path, ["id", "parameters", "origin", "instructions", "terminator"]) + parameter_values := try required_array(fields, "parameters", path) + if parameter_values.size() > GATE1_MAX_PARAMETERS + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/parameters", "block parameter count exceeds the Gate 1 limit") + ) + end + mut parameters: Array := [] + mut parameter_index := 0 + while parameter_index < parameter_values.size() + parameters.push( + try _gate1_decode_parameter(parameter_values[parameter_index], path + "/parameters/" + parameter_index.to_s()) + ) + parameter_index += 1 + end + instruction_values := try required_array(fields, "instructions", path) + if instruction_values.size() > GATE1_MAX_INSTRUCTIONS + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/instructions", "instruction count exceeds the Gate 1 limit") + ) + end + mut instructions: Array := [] + mut instruction_index := 0 + while instruction_index < instruction_values.size() + instructions.push( + try _gate1_decode_instruction(instruction_values[instruction_index], path + "/instructions/" + instruction_index.to_s()) + ) + instruction_index += 1 + end + return Result::Ok( + Gate1Block.new( + id: try required_string(fields, "id", path, true), + parameters: parameters, + origin: try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + instructions: instructions, + terminator: try _gate1_decode_terminator(try required_field(fields, "terminator", path), path + "/terminator"), + ) + ) +end + +def _gate1_decode_instruction(value: JsonValue, path: String): Result + base_fields := try object(value, path) + op := try required_string(base_fields, "op", path, false) + if op == "boolean_literal" + fields := try strict_object(value, path, ["op", "result", "value", "origin"]) + return Result::Ok( + Gate1Instruction::BooleanLiteral( + try required_string(fields, "result", path, true), + try _gate1_required_boolean(fields, "value", path), + try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + ) + ) + end + if op == "integer_literal" + fields := try strict_object(value, path, ["op", "result", "value", "origin"]) + return Result::Ok( + Gate1Instruction::IntegerLiteral( + try required_string(fields, "result", path, true), + try required_integer(fields, "value", path), + try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + ) + ) + end + if op == "float_literal" + fields := try strict_object(value, path, ["op", "result", "value", "origin"]) + return Result::Ok( + Gate1Instruction::FloatLiteral( + try required_string(fields, "result", path, true), + try _gate1_required_float(fields, "value", path), + try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + ) + ) + end + if op == "integer_binary" + fields := try strict_object(value, path, ["op", "result", "operator", "left", "right", "origin"]) + return Result::Ok( + Gate1Instruction::IntegerBinary( + try required_string(fields, "result", path, true), + try _gate1_integer_operator(try required_string(fields, "operator", path, false), path + "/operator"), + try required_string(fields, "left", path, true), + try required_string(fields, "right", path, true), + try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + ) + ) + end + if op == "float_binary" + fields := try strict_object(value, path, ["op", "result", "operator", "left", "right", "origin"]) + return Result::Ok( + Gate1Instruction::FloatBinary( + try required_string(fields, "result", path, true), + try _gate1_float_operator(try required_string(fields, "operator", path, false), path + "/operator"), + try required_string(fields, "left", path, true), + try required_string(fields, "right", path, true), + try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + ) + ) + end + if op == "integer_compare" || op == "float_compare" + fields := try strict_object(value, path, ["op", "result", "operator", "left", "right", "origin"]) + result := try required_string(fields, "result", path, true) + operator := try _gate1_comparison_operator(try required_string(fields, "operator", path, false), path + "/operator") + left := try required_string(fields, "left", path, true) + right := try required_string(fields, "right", path, true) + origin := try decode_origin(try required_field(fields, "origin", path), path + "/origin") + if op == "integer_compare" + return Result::Ok( + Gate1Instruction::IntegerCompare(result, operator, left, right, origin) + ) + end + return Result::Ok( + Gate1Instruction::FloatCompare(result, operator, left, right, origin) + ) + end + if op == "boolean_not" + fields := try strict_object(value, path, ["op", "result", "value", "origin"]) + return Result::Ok( + Gate1Instruction::BooleanNot( + try required_string(fields, "result", path, true), + try required_string(fields, "value", path, true), + try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + ) + ) + end + if op == "call" + fields := try strict_object(value, path, ["op", "result", "function", "arguments", "origin"]) + return Result::Ok( + Gate1Instruction::Call( + try _gate1_required_nullable_string(fields, "result", path), + try required_string(fields, "function", path, true), + try _gate1_required_string_array(fields, "arguments", path), + try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + ) + ) + end + if op == "write_static" + fields := try strict_object(value, path, ["op", "value", "origin"]) + static_value := try required_string(fields, "value", path, false) + if static_value.to_bytes().size() > GATE1_MAX_STATIC_BYTES + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/value", "static output exceeds the Gate 1 limit") + ) + end + return Result::Ok( + Gate1Instruction::WriteStatic( + static_value, + try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + ) + ) + end + return Result::Err( + boundary_diagnostic("TRBN0003", path + "/op", "unsupported Gate 1 instruction") + ) +end + +def _gate1_decode_terminator(value: JsonValue, path: String): Result + base_fields := try object(value, path) + op := try required_string(base_fields, "op", path, false) + if op == "jump" + fields := try strict_object(value, path, ["op", "target", "arguments", "origin"]) + return Result::Ok( + Gate1Terminator::Jump( + try required_string(fields, "target", path, true), + try _gate1_required_string_array(fields, "arguments", path), + try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + ) + ) + end + if op == "branch" + fields := try strict_object( + value, + path, + ["op", "condition", "whenTrue", "trueArguments", "whenFalse", "falseArguments", "origin"], + ) + return Result::Ok( + Gate1Terminator::Branch( + try required_string(fields, "condition", path, true), + try required_string(fields, "whenTrue", path, true), + try _gate1_required_string_array(fields, "trueArguments", path), + try required_string(fields, "whenFalse", path, true), + try _gate1_required_string_array(fields, "falseArguments", path), + try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + ) + ) + end + if op == "return" + fields := try strict_object(value, path, ["op", "value", "origin"]) + return Result::Ok( + Gate1Terminator::Return( + try _gate1_required_nullable_string(fields, "value", path), + try decode_origin(try required_field(fields, "origin", path), path + "/origin"), + ) + ) + end + return Result::Err( + boundary_diagnostic("TRBN0003", path + "/op", "unsupported Gate 1 terminator") + ) +end + +def _gate1_integer_operator(name: String, path: String): Result + if name == "add" + return Result::Ok(IntegerBinaryOperator::Add) + end + if name == "subtract" + return Result::Ok(IntegerBinaryOperator::Subtract) + end + if name == "multiply" + return Result::Ok(IntegerBinaryOperator::Multiply) + end + if name == "divide" + return Result::Ok(IntegerBinaryOperator::Divide) + end + if name == "remainder" + return Result::Ok(IntegerBinaryOperator::Remainder) + end + return Result::Err( + boundary_diagnostic("TRBN0003", path, "unsupported Gate 1 Integer operator") + ) +end + +def _gate1_float_operator(name: String, path: String): Result + if name == "add" + return Result::Ok(FloatBinaryOperator::Add) + end + if name == "subtract" + return Result::Ok(FloatBinaryOperator::Subtract) + end + if name == "multiply" + return Result::Ok(FloatBinaryOperator::Multiply) + end + if name == "divide" + return Result::Ok(FloatBinaryOperator::Divide) + end + return Result::Err( + boundary_diagnostic("TRBN0003", path, "unsupported Gate 1 Float operator") + ) +end + +def _gate1_comparison_operator(name: String, path: String): Result + if name == "equal" + return Result::Ok(ComparisonOperator::Equal) + end + if name == "not_equal" + return Result::Ok(ComparisonOperator::NotEqual) + end + if name == "less_than" + return Result::Ok(ComparisonOperator::LessThan) + end + if name == "less_than_or_equal" + return Result::Ok(ComparisonOperator::LessThanOrEqual) + end + if name == "greater_than" + return Result::Ok(ComparisonOperator::GreaterThan) + end + if name == "greater_than_or_equal" + return Result::Ok(ComparisonOperator::GreaterThanOrEqual) + end + return Result::Err( + boundary_diagnostic("TRBN0003", path, "unsupported Gate 1 comparison operator") + ) +end + +def _gate1_required_boolean( + fields: Hash, + name: String, + path: String, + ): Result + value := try required_field(fields, name, path) + case value + when JsonValue::Boolean(result) + return Result::Ok(result) + else + return Result::Err( + boundary_diagnostic("TRBN0002", path + "/" + name, "expected a boolean") + ) + end +end + +def _gate1_required_float( + fields: Hash, + name: String, + path: String, + ): Result + value := try required_field(fields, name, path) + case value + when JsonValue::Float(result) + return Result::Ok(result) + when JsonValue::Integer(result) + return Result::Ok(result.to_f()) + else + return Result::Err( + boundary_diagnostic("TRBN0002", path + "/" + name, "expected a number") + ) + end +end + +def _gate1_required_nullable_string( + fields: Hash, + name: String, + path: String, + ): Result + value := try required_field(fields, name, path) + case value + when JsonValue::Null + return Result::Ok(nil) + when JsonValue::String(result) + if result.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/" + name, "identifier must not be empty") + ) + end + if result.size() > GATE1_MAX_IDENTIFIER_SIZE + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/" + name, "identifier exceeds the Gate 1 limit") + ) + end + return Result::Ok(result) + else + return Result::Err( + boundary_diagnostic("TRBN0002", path + "/" + name, "expected a string or null") + ) + end +end + +def _gate1_required_string_array( + fields: Hash, + name: String, + path: String, + ): Result, Diagnostic> + values := try required_array(fields, name, path) + if values.size() > GATE1_MAX_ARGUMENTS + return Result, Diagnostic>::Err( + boundary_diagnostic("TRBN0005", path + "/" + name, "argument count exceeds the Gate 1 limit") + ) + end + mut result: Array := [] + mut index := 0 + while index < values.size() + case values[index] + when JsonValue::String(item) + if item.empty?() + return Result, Diagnostic>::Err( + boundary_diagnostic("TRBN0004", path + "/" + name + "/" + index.to_s(), "identifier must not be empty") + ) + end + if item.size() > GATE1_MAX_IDENTIFIER_SIZE + return Result, Diagnostic>::Err( + boundary_diagnostic( + "TRBN0005", + path + "/" + name + "/" + index.to_s(), + "identifier exceeds the Gate 1 limit", + ) + ) + end + result.push(item) + else + return Result, Diagnostic>::Err( + boundary_diagnostic("TRBN0002", path + "/" + name + "/" + index.to_s(), "expected a string") + ) + end + index += 1 + end + return Result, Diagnostic>::Ok(result) +end diff --git a/src/gate1_test.trb b/src/gate1_test.trb new file mode 100644 index 00000000..00bc6eb9 --- /dev/null +++ b/src/gate1_test.trb @@ -0,0 +1,561 @@ +import { + ComparisonOperator, + FloatBinaryOperator, + Gate1Block, + Gate1Function, + Gate1Instruction, + Gate1Module, + Gate1Parameter, + Gate1Terminator, + IntegerBinaryOperator, + ScalarType, + verify_gate1_mir, +} from gate1_mir +import { build_gate1_executable } from gate1_toolchain +import { decode_gate1_snapshot } from gate1_snapshot +import { NativeSource } from native_mir +import { emit_qbe } from qbe +import { SourceOrigin } from snapshot +import { create_directory, read_text } from trb/std/filesystem +import { environment, run } from trb/std/process +import { Result } from trb/std/result +import { describe, expect, test } from trb/std/test + +def _gate1_origin(line: Integer): SourceOrigin + return SourceOrigin.new( + source_id: "main", + start_line: line, + start_column: 1, + end_line: line, + end_column: 8, + ) +end + +def _gate1_sum_function(): Gate1Function + return Gate1Function.new( + id: "example/main#sum_to", + name: "sum_to", + parameters: [Gate1Parameter.new(id: "limit", value_type: ScalarType::Integer, origin: _gate1_origin(1))], + result_type: ScalarType::Integer, + entry: "entry", + origin: _gate1_origin(1), + blocks: [ + Gate1Block.new( + id: "entry", + parameters: [], + origin: _gate1_origin(2), + instructions: [Gate1Instruction::IntegerLiteral("zero", 0, _gate1_origin(2))], + terminator: Gate1Terminator::Jump("loop", ["zero", "zero"], _gate1_origin(2)), + ), + Gate1Block.new( + id: "loop", + parameters: [ + Gate1Parameter.new(id: "index", value_type: ScalarType::Integer, origin: _gate1_origin(3)), + Gate1Parameter.new(id: "total", value_type: ScalarType::Integer, origin: _gate1_origin(3)), + ], + origin: _gate1_origin(3), + instructions: [ + Gate1Instruction::IntegerCompare( + "continues", + ComparisonOperator::LessThan, + "index", + "limit", + _gate1_origin(3), + ), + ], + terminator: Gate1Terminator::Branch( + "continues", + "body", + ["index", "total"], + "done", + ["total"], + _gate1_origin(3), + ), + ), + Gate1Block.new( + id: "body", + parameters: [ + Gate1Parameter.new(id: "body_index", value_type: ScalarType::Integer, origin: _gate1_origin(4)), + Gate1Parameter.new(id: "body_total", value_type: ScalarType::Integer, origin: _gate1_origin(4)), + ], + origin: _gate1_origin(4), + instructions: [ + Gate1Instruction::IntegerLiteral("one", 1, _gate1_origin(4)), + Gate1Instruction::IntegerBinary( + "next_index", + IntegerBinaryOperator::Add, + "body_index", + "one", + _gate1_origin(4), + ), + Gate1Instruction::IntegerBinary( + "next_total", + IntegerBinaryOperator::Add, + "body_total", + "next_index", + _gate1_origin(4), + ), + ], + terminator: Gate1Terminator::Jump("loop", ["next_index", "next_total"], _gate1_origin(4)), + ), + Gate1Block.new( + id: "done", + parameters: [Gate1Parameter.new(id: "result", value_type: ScalarType::Integer, origin: _gate1_origin(5))], + origin: _gate1_origin(5), + instructions: [], + terminator: Gate1Terminator::Return("result", _gate1_origin(5)), + ), + ], + ) +end + +def _gate1_float_function(): Gate1Function + return Gate1Function.new( + id: "example/main#float_check", + name: "float_check", + parameters: [], + result_type: ScalarType::Boolean, + entry: "entry", + origin: _gate1_origin(7), + blocks: [ + Gate1Block.new( + id: "entry", + parameters: [], + origin: _gate1_origin(7), + instructions: [ + Gate1Instruction::FloatLiteral("left", 1.5, _gate1_origin(7)), + Gate1Instruction::FloatLiteral("right", 2.0, _gate1_origin(7)), + Gate1Instruction::FloatLiteral("expected", 3.0, _gate1_origin(7)), + Gate1Instruction::FloatBinary( + "product", + FloatBinaryOperator::Multiply, + "left", + "right", + _gate1_origin(7), + ), + Gate1Instruction::FloatCompare( + "matches", + ComparisonOperator::Equal, + "product", + "expected", + _gate1_origin(7), + ), + ], + terminator: Gate1Terminator::Return("matches", _gate1_origin(7)), + ), + ], + ) +end + +def _gate1_entry_function(): Gate1Function + return Gate1Function.new( + id: "example/main#main", + name: "main", + parameters: [], + result_type: ScalarType::Void, + entry: "entry", + origin: _gate1_origin(9), + blocks: [ + Gate1Block.new( + id: "entry", + parameters: [], + origin: _gate1_origin(9), + instructions: [ + Gate1Instruction::IntegerLiteral("limit", 5, _gate1_origin(9)), + Gate1Instruction::Call("sum", "example/main#sum_to", ["limit"], _gate1_origin(9)), + Gate1Instruction::IntegerLiteral("expected", 15, _gate1_origin(9)), + Gate1Instruction::IntegerCompare( + "sum_matches", + ComparisonOperator::Equal, + "sum", + "expected", + _gate1_origin(9), + ), + ], + terminator: Gate1Terminator::Branch( + "sum_matches", + "check_float", + [], + "failure", + [], + _gate1_origin(9), + ), + ), + Gate1Block.new( + id: "check_float", + parameters: [], + origin: _gate1_origin(10), + instructions: [ + Gate1Instruction::Call("float_matches", "example/main#float_check", [], _gate1_origin(10)), + Gate1Instruction::BooleanNot("inverted", "float_matches", _gate1_origin(10)), + Gate1Instruction::BooleanNot("restored", "inverted", _gate1_origin(10)), + ], + terminator: Gate1Terminator::Branch( + "restored", + "success", + [], + "failure", + [], + _gate1_origin(10), + ), + ), + Gate1Block.new( + id: "success", + parameters: [], + origin: _gate1_origin(11), + instructions: [Gate1Instruction::WriteStatic("ok\n", _gate1_origin(11))], + terminator: Gate1Terminator::Return(nil, _gate1_origin(11)), + ), + Gate1Block.new( + id: "failure", + parameters: [], + origin: _gate1_origin(12), + instructions: [Gate1Instruction::WriteStatic("bad\n", _gate1_origin(12))], + terminator: Gate1Terminator::Return(nil, _gate1_origin(12)), + ), + ], + ) +end + +def _gate1_sample_module(): Gate1Module + return Gate1Module.new( + id: "example/main", + entry_function: "example/main#main", + sources: [NativeSource.new(id: "main", path: "src/main.trb")], + functions: [_gate1_sum_function(), _gate1_float_function(), _gate1_entry_function()], + ) +end + +def _gate1_arithmetic_module(overflow: Boolean): Gate1Module + mut instructions: Array := [] + if overflow + instructions = [ + Gate1Instruction::IntegerLiteral("maximum", 9007199254740991, _gate1_origin(20)), + Gate1Instruction::IntegerLiteral("two", 2, _gate1_origin(20)), + Gate1Instruction::IntegerBinary( + "overflow", + IntegerBinaryOperator::Multiply, + "maximum", + "two", + _gate1_origin(20), + ), + Gate1Instruction::WriteStatic("unreachable\n", _gate1_origin(20)), + ] + else + instructions = [ + Gate1Instruction::IntegerLiteral("ten", 10, _gate1_origin(20)), + Gate1Instruction::IntegerLiteral("three", 3, _gate1_origin(20)), + Gate1Instruction::IntegerLiteral("forty_one", 41, _gate1_origin(20)), + Gate1Instruction::IntegerLiteral("negative_seven", - 7, _gate1_origin(20)), + Gate1Instruction::IntegerLiteral("negative_five", - 5, _gate1_origin(20)), + Gate1Instruction::IntegerLiteral("expected", 77, _gate1_origin(20)), + Gate1Instruction::IntegerBinary( + "difference", + IntegerBinaryOperator::Subtract, + "ten", + "three", + _gate1_origin(20), + ), + Gate1Instruction::IntegerBinary( + "product", + IntegerBinaryOperator::Multiply, + "difference", + "three", + _gate1_origin(20), + ), + Gate1Instruction::IntegerBinary( + "quotient", + IntegerBinaryOperator::Divide, + "product", + "three", + _gate1_origin(20), + ), + Gate1Instruction::IntegerBinary( + "remainder", + IntegerBinaryOperator::Remainder, + "quotient", + "three", + _gate1_origin(20), + ), + Gate1Instruction::IntegerBinary( + "answer", + IntegerBinaryOperator::Add, + "remainder", + "forty_one", + _gate1_origin(20), + ), + Gate1Instruction::IntegerBinary( + "negative_product", + IntegerBinaryOperator::Multiply, + "negative_seven", + "negative_five", + _gate1_origin(20), + ), + Gate1Instruction::IntegerBinary( + "combined", + IntegerBinaryOperator::Add, + "answer", + "negative_product", + _gate1_origin(20), + ), + Gate1Instruction::IntegerCompare( + "matches", + ComparisonOperator::Equal, + "combined", + "expected", + _gate1_origin(20), + ), + ] + end + terminator := Gate1Terminator::Return(nil, _gate1_origin(20)) + mut blocks: Array := [] + if overflow + blocks = [ + Gate1Block.new( + id: "entry", + parameters: [], + origin: _gate1_origin(20), + instructions: instructions, + terminator: terminator, + ), + ] + else + blocks = [ + Gate1Block.new( + id: "entry", + parameters: [], + origin: _gate1_origin(20), + instructions: instructions, + terminator: Gate1Terminator::Branch( + "matches", + "success", + [], + "failure", + [], + _gate1_origin(20), + ), + ), + Gate1Block.new( + id: "success", + parameters: [], + origin: _gate1_origin(21), + instructions: [Gate1Instruction::WriteStatic("ok\n", _gate1_origin(21))], + terminator: terminator, + ), + Gate1Block.new( + id: "failure", + parameters: [], + origin: _gate1_origin(22), + instructions: [Gate1Instruction::WriteStatic("bad\n", _gate1_origin(22))], + terminator: terminator, + ), + ] + end + if !overflow + blocks = [blocks[1], blocks[0], blocks[2]] + end + entry_function := Gate1Function.new( + id: "example/arithmetic#main", + name: "main", + parameters: [], + result_type: ScalarType::Void, + entry: "entry", + origin: _gate1_origin(20), + blocks: blocks, + ) + return Gate1Module.new( + id: "example/arithmetic", + entry_function: entry_function.id, + sources: [NativeSource.new(id: "main", path: "src/main.trb")], + functions: [entry_function], + ) +end + +def _gate1_fixture_source(): String + root := environment("TYPE_RB_NATIVE_ROOT") + if root == nil + return "" + end + case read_text(root + "/fixtures/gate1/valid/scalar-call.json") + when Result::Ok(source) + return source + when Result::Err(_error) + return "" + end +end + +describe("Gate 1 scalar QBE path") do + test("verifies scalar calls and control flow") do + case verify_gate1_mir(_gate1_sample_module()) + when Result::Ok(valid) + expect(valid).to_be_true() + when Result::Err(_diagnostic) + expect(false).to_be_true() + end + end + + test("emits deterministic QBE IL with source origins") do + first := emit_qbe(_gate1_sample_module()) + second := emit_qbe(_gate1_sample_module()) + case first + when Result::Ok(source) + expect(second).to_equal(first) + expect(source.include?("export function w $main()")).to_be_true() + expect(source.include?("$trbn_iadd")).to_be_true() + expect(source.include?("trb-origin")).to_be_true() + when Result::Err(_diagnostic) + expect(false).to_be_true() + end + end + + test("strictly decodes the version 2 executable fixture") do + source := _gate1_fixture_source() + expect(source.empty?()).to_be_false() + case decode_gate1_snapshot(source) + when Result::Ok(native_module) + expect(native_module.entry_function).to_equal("example/main#main") + expect(native_module.functions.size()).to_equal(2) + when Result::Err(_diagnostic) + expect(false).to_be_true() + end + end + + test("rejects version 2 unknown fields deterministically") do + root := environment("TYPE_RB_NATIVE_ROOT") + if root == nil + expect(false).to_be_true() + else + source := case read_text(root + "/fixtures/gate1/invalid/unknown-field.json") + when Result::Ok(value) + value + when Result::Err(_error) + "" + end + case decode_gate1_snapshot(source) + when Result::Ok(_native_module) + expect(false).to_be_true() + when Result::Err(diagnostic) + expect(diagnostic.code).to_equal("TRBN0002") + expect(diagnostic.path).to_equal("/unexpected") + end + end + end + + test("builds and runs the Darwin arm64 executable when QBE is configured") do + qbe_command := environment("TYPE_RB_NATIVE_QBE") + if qbe_command == nil + expect(true).to_be_true() + else + workspace := "/tmp/type-rb-native-gate1-integration" + case create_directory(workspace) + when Result::Ok(_unit) + when Result::Err(_error) + end + case build_gate1_executable( + _gate1_sample_module(), + qbe_command, + "/usr/bin/cc", + workspace, + workspace + "/sample", + ) + when Result::Ok(report) + case run(report.output_path, []) + when Result::Ok(process_result) + expect(process_result.success).to_be_true() + expect(process_result.stdout).to_equal("ok\n") + expect(process_result.stderr).to_equal("") + when Result::Err(_error) + expect(false).to_be_true() + end + when Result::Err(_error) + expect(false).to_be_true() + end + end + end + + test("builds the version 2 data fixture when QBE is configured") do + qbe_command := environment("TYPE_RB_NATIVE_QBE") + if qbe_command == nil + expect(true).to_be_true() + else + case decode_gate1_snapshot(_gate1_fixture_source()) + when Result::Ok(native_module) + workspace := "/tmp/type-rb-native-gate1-fixture" + case create_directory(workspace) + when Result::Ok(_unit) + when Result::Err(_error) + end + case build_gate1_executable( + native_module, + qbe_command, + "/usr/bin/cc", + workspace, + workspace + "/sample", + ) + when Result::Ok(report) + case run(report.output_path, []) + when Result::Ok(process_result) + expect(process_result.success).to_be_true() + expect(process_result.stdout).to_equal("ok\n") + when Result::Err(_error) + expect(false).to_be_true() + end + when Result::Err(_error) + expect(false).to_be_true() + end + when Result::Err(_diagnostic) + expect(false).to_be_true() + end + end + end + + test("executes every checked Integer operator and rejects overflow") do + qbe_command := environment("TYPE_RB_NATIVE_QBE") + if qbe_command == nil + expect(true).to_be_true() + else + workspace := "/tmp/type-rb-native-gate1-arithmetic" + case create_directory(workspace) + when Result::Ok(_unit) + when Result::Err(_error) + end + case build_gate1_executable( + _gate1_arithmetic_module(false), + qbe_command, + "/usr/bin/cc", + workspace, + workspace + "/success", + ) + when Result::Ok(report) + case run(report.output_path, []) + when Result::Ok(process_result) + expect(process_result.success).to_be_true() + expect(process_result.stdout).to_equal("ok\n") + when Result::Err(_error) + expect(false).to_be_true() + end + when Result::Err(_error) + expect(false).to_be_true() + end + case build_gate1_executable( + _gate1_arithmetic_module(true), + qbe_command, + "/usr/bin/cc", + workspace, + workspace + "/overflow", + ) + when Result::Ok(report) + case run(report.output_path, []) + when Result::Ok(process_result) + expect(process_result.status).to_equal(70) + expect(process_result.stdout).to_equal("") + expect(process_result.stderr).to_equal("TypeRB Integer arithmetic error\n") + when Result::Err(_error) + expect(false).to_be_true() + end + when Result::Err(_error) + expect(false).to_be_true() + end + end + end +end diff --git a/src/gate1_toolchain.trb b/src/gate1_toolchain.trb new file mode 100644 index 00000000..7e8081ad --- /dev/null +++ b/src/gate1_toolchain.trb @@ -0,0 +1,100 @@ +import { Gate1Module } from gate1_mir +import { emit_qbe } from qbe +import { write_text } from trb/std/filesystem +import { run } from trb/std/process +import { Result } from trb/std/result + +record Gate1BuildError + phase: String + message: String + status: Integer? = nil + stdout: String = "" + stderr: String = "" +end + +record Gate1BuildReport + qbe_ir_path: String + assembly_path: String + output_path: String +end + +def build_gate1_executable( + native_module: Gate1Module, + qbe_command: String, + cc_command: String, + workspace: String, + output_path: String, + ): Result + qbe_source := case emit_qbe(native_module) + when Result::Ok(source) + source + when Result::Err(diagnostic) + return Result::Err( + Gate1BuildError.new( + phase: "verify", + message: diagnostic.code + " " + diagnostic.path + ": " + diagnostic.message, + ) + ) + end + qbe_ir_path := workspace + "/module.ssa" + assembly_path := workspace + "/module.s" + case write_text(qbe_ir_path, qbe_source) + when Result::Ok(_unit) + when Result::Err(error) + return Result::Err( + Gate1BuildError.new(phase: "write-qbe", message: error.operation + ": " + error.message) + ) + end + qbe_result := case run(qbe_command, ["-t", "arm64_apple", qbe_ir_path]) + when Result::Ok(process_result) + process_result + when Result::Err(error) + return Result::Err( + Gate1BuildError.new(phase: "qbe", message: error.operation + ": " + error.message) + ) + end + if !qbe_result.success + return Result::Err( + Gate1BuildError.new( + phase: "qbe", + message: "QBE rejected the generated IL", + status: qbe_result.status, + stdout: qbe_result.stdout, + stderr: qbe_result.stderr, + ) + ) + end + case write_text(assembly_path, qbe_result.stdout) + when Result::Ok(_unit) + when Result::Err(error) + return Result::Err( + Gate1BuildError.new(phase: "write-assembly", message: error.operation + ": " + error.message) + ) + end + cc_result := case run(cc_command, [assembly_path, "-Wl,-dead_strip", "-o", output_path]) + when Result::Ok(process_result) + process_result + when Result::Err(error) + return Result::Err( + Gate1BuildError.new(phase: "link", message: error.operation + ": " + error.message) + ) + end + if !cc_result.success + return Result::Err( + Gate1BuildError.new( + phase: "link", + message: "the system C toolchain rejected the QBE assembly", + status: cc_result.status, + stdout: cc_result.stdout, + stderr: cc_result.stderr, + ) + ) + end + return Result::Ok( + Gate1BuildReport.new( + qbe_ir_path: qbe_ir_path, + assembly_path: assembly_path, + output_path: output_path, + ) + ) +end diff --git a/src/json_boundary.trb b/src/json_boundary.trb new file mode 100644 index 00000000..f2c03428 --- /dev/null +++ b/src/json_boundary.trb @@ -0,0 +1,108 @@ +import { Diagnostic, boundary_diagnostic } from diagnostic +import { JsonValue } from trb/std/json +import { Result } from trb/std/result + +BOUNDARY_MAX_IDENTIFIER_SIZE := 256 + +def strict_object( + value: JsonValue, + path: String, + allowed: Array, + ): Result, Diagnostic> + fields := try object(value, path) + names := fields.keys().sort() + mut index := 0 + while index < names.size() + name := names[index] + if !allowed.include?(name) + return Result, Diagnostic>::Err( + boundary_diagnostic("TRBN0002", path + "/" + name, "unknown field") + ) + end + index += 1 + end + return Result, Diagnostic>::Ok(fields) +end + +def object(value: JsonValue, path: String): Result, Diagnostic> + case value + when JsonValue::Object(fields) + return Result, Diagnostic>::Ok(fields) + else + return Result, Diagnostic>::Err( + boundary_diagnostic("TRBN0002", path, "expected an object") + ) + end +end + +def required_field( + fields: Hash, + name: String, + path: String, + ): Result + if !fields.key?(name) + return Result::Err( + boundary_diagnostic("TRBN0002", path + "/" + name, "required field is missing") + ) + end + return Result::Ok(fields.fetch(name)) +end + +def required_string( + fields: Hash, + name: String, + path: String, + identifier: Boolean, + ): Result + value := try required_field(fields, name, path) + case value + when JsonValue::String(result) + if identifier && result.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/" + name, "identifier must not be empty") + ) + end + if identifier && result.size() > BOUNDARY_MAX_IDENTIFIER_SIZE + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/" + name, "identifier exceeds the boundary limit") + ) + end + return Result::Ok(result) + else + return Result::Err( + boundary_diagnostic("TRBN0002", path + "/" + name, "expected a string") + ) + end +end + +def required_integer( + fields: Hash, + name: String, + path: String, + ): Result + value := try required_field(fields, name, path) + case value + when JsonValue::Integer(result) + return Result::Ok(result) + else + return Result::Err( + boundary_diagnostic("TRBN0002", path + "/" + name, "expected an integer") + ) + end +end + +def required_array( + fields: Hash, + name: String, + path: String, + ): Result, Diagnostic> + value := try required_field(fields, name, path) + case value + when JsonValue::Array(result) + return Result, Diagnostic>::Ok(result) + else + return Result, Diagnostic>::Err( + boundary_diagnostic("TRBN0002", path + "/" + name, "expected an array") + ) + end +end diff --git a/src/qbe.trb b/src/qbe.trb new file mode 100644 index 00000000..594941a8 --- /dev/null +++ b/src/qbe.trb @@ -0,0 +1,694 @@ +import { Diagnostic } from diagnostic +import { + ComparisonOperator, + FloatBinaryOperator, + Gate1Block, + Gate1Function, + Gate1Instruction, + Gate1Module, + Gate1Terminator, + IntegerBinaryOperator, + ScalarType, + verify_gate1_mir, +} from gate1_mir +import { SourceOrigin } from snapshot +import { Result } from trb/std/result +import trb/std/string_builder + +record QbeStaticData + symbol: String + value: String +end + +def emit_qbe(native_module: Gate1Module): Result + try verify_gate1_mir(native_module) + static_data := _qbe_collect_static_data(native_module) + mut writer := string_builder.new() + _qbe_emit_static_data(static_data, writer) + _qbe_emit_runtime(writer) + mut function_index := 0 + while function_index < native_module.functions.size() + _qbe_emit_function(native_module, native_module.functions[function_index], function_index, static_data, writer) + function_index += 1 + end + _qbe_emit_entry(native_module, writer) + return Result::Ok(writer.to_s()) +end + +def _qbe_line(mut writer: StringBuilder, value: String) + writer.append(value) + writer.append("\n") + return +end + +def _qbe_collect_static_data(native_module: Gate1Module): Array + mut static_data: Array := [] + mut symbols: Hash := {} + mut function_index := 0 + while function_index < native_module.functions.size() + function := native_module.functions[function_index] + mut block_index := 0 + while block_index < function.blocks.size() + block := function.blocks[block_index] + mut instruction_index := 0 + while instruction_index < block.instructions.size() + case block.instructions[instruction_index] + when Gate1Instruction::WriteStatic(value, _origin) + if !symbols.key?(value) + symbol := "$trbn_data_" + static_data.size().to_s() + symbols[value] = symbol + static_data.push(QbeStaticData.new(symbol: symbol, value: value)) + end + else + end + instruction_index += 1 + end + block_index += 1 + end + function_index += 1 + end + return static_data +end + +def _qbe_emit_static_data(static_data: Array, mut writer: StringBuilder) + _qbe_line(writer, "data $trbn_arithmetic_error = { b 84 121 112 101 82 66 32 73 110 116 101 103 101 114 32 97 114 105 116 104 109 101 116 105 99 32 101 114 114 111 114 10 }") + mut index := 0 + while index < static_data.size() + item := static_data[index] + bytes := item.value.to_bytes() + if bytes.size() == 0 + _qbe_line(writer, "data " + item.symbol + " = { z 1 }") + else + mut values: Array := [] + mut byte_index := 0 + while byte_index < bytes.size() + values.push(bytes.at(byte_index).to_s()) + byte_index += 1 + end + _qbe_line(writer, "data " + item.symbol + " = { b " + values.join(" ") + " }") + end + index += 1 + end + _qbe_line(writer, "") + return +end + +def _qbe_emit_runtime(mut writer: StringBuilder) + _qbe_line(writer, "function $trbn_arithmetic_fail() {") + _qbe_line(writer, "@start") + _qbe_line(writer, "\tcall $write(w 2, l $trbn_arithmetic_error, l 32)") + _qbe_line(writer, "\tcall $exit(w 70)") + _qbe_line(writer, "\thlt") + _qbe_line(writer, "}") + _qbe_line(writer, "") + _qbe_emit_checked_add(writer) + _qbe_emit_checked_subtract(writer) + _qbe_emit_checked_multiply(writer) + _qbe_emit_checked_divide(writer) + _qbe_emit_checked_remainder(writer) + return +end + +def _qbe_emit_checked_add(mut writer: StringBuilder) + _qbe_line(writer, "function l $trbn_iadd(l %left, l %right) {") + _qbe_line(writer, "@start") + _qbe_line(writer, "\t%result =l add %left, %right") + _qbe_line(writer, "\t%too_high =w csgtl %result, 9007199254740991") + _qbe_line(writer, "\tjnz %too_high, @fail, @check_low") + _qbe_line(writer, "@check_low") + _qbe_line(writer, "\t%too_low =w csltl %result, -9007199254740991") + _qbe_line(writer, "\tjnz %too_low, @fail, @ok") + _qbe_line(writer, "@fail") + _qbe_line(writer, "\tcall $trbn_arithmetic_fail()") + _qbe_line(writer, "\thlt") + _qbe_line(writer, "@ok") + _qbe_line(writer, "\tret %result") + _qbe_line(writer, "}") + _qbe_line(writer, "") + return +end + +def _qbe_emit_checked_subtract(mut writer: StringBuilder) + _qbe_line(writer, "function l $trbn_isub(l %left, l %right) {") + _qbe_line(writer, "@start") + _qbe_line(writer, "\t%result =l sub %left, %right") + _qbe_line(writer, "\t%too_high =w csgtl %result, 9007199254740991") + _qbe_line(writer, "\tjnz %too_high, @fail, @check_low") + _qbe_line(writer, "@check_low") + _qbe_line(writer, "\t%too_low =w csltl %result, -9007199254740991") + _qbe_line(writer, "\tjnz %too_low, @fail, @ok") + _qbe_line(writer, "@fail") + _qbe_line(writer, "\tcall $trbn_arithmetic_fail()") + _qbe_line(writer, "\thlt") + _qbe_line(writer, "@ok") + _qbe_line(writer, "\tret %result") + _qbe_line(writer, "}") + _qbe_line(writer, "") + return +end + +def _qbe_emit_checked_multiply(mut writer: StringBuilder) + _qbe_line(writer, "function l $trbn_imul(l %left, l %right) {") + _qbe_line(writer, "@start") + _qbe_line(writer, "\t%left_zero =w ceql %left, 0") + _qbe_line(writer, "\tjnz %left_zero, @zero, @left_sign") + _qbe_line(writer, "@left_sign") + _qbe_line(writer, "\t%left_positive =w csgtl %left, 0") + _qbe_line(writer, "\tjnz %left_positive, @left_positive, @left_negative") + _qbe_line(writer, "@left_positive") + _qbe_line(writer, "\t%right_positive =w csgtl %right, 0") + _qbe_line(writer, "\tjnz %right_positive, @positive_positive, @positive_nonpositive") + _qbe_line(writer, "@positive_positive") + _qbe_line(writer, "\t%positive_limit =l div 9007199254740991, %right") + _qbe_line(writer, "\t%positive_overflow =w csgtl %left, %positive_limit") + _qbe_line(writer, "\tjnz %positive_overflow, @fail, @multiply") + _qbe_line(writer, "@positive_nonpositive") + _qbe_line(writer, "\t%right_zero =w ceql %right, 0") + _qbe_line(writer, "\tjnz %right_zero, @zero, @positive_negative") + _qbe_line(writer, "@positive_negative") + _qbe_line(writer, "\t%negative_limit =l div -9007199254740991, %left") + _qbe_line(writer, "\t%negative_overflow =w csltl %right, %negative_limit") + _qbe_line(writer, "\tjnz %negative_overflow, @fail, @multiply") + _qbe_line(writer, "@left_negative") + _qbe_line(writer, "\t%right_positive_two =w csgtl %right, 0") + _qbe_line(writer, "\tjnz %right_positive_two, @negative_positive, @negative_nonpositive") + _qbe_line(writer, "@negative_positive") + _qbe_line(writer, "\t%negative_limit_two =l div -9007199254740991, %right") + _qbe_line(writer, "\t%negative_overflow_two =w csltl %left, %negative_limit_two") + _qbe_line(writer, "\tjnz %negative_overflow_two, @fail, @multiply") + _qbe_line(writer, "@negative_nonpositive") + _qbe_line(writer, "\t%right_zero_two =w ceql %right, 0") + _qbe_line(writer, "\tjnz %right_zero_two, @zero, @negative_negative") + _qbe_line(writer, "@negative_negative") + _qbe_line(writer, "\t%positive_limit_two =l div 9007199254740991, %right") + _qbe_line(writer, "\t%positive_overflow_two =w csltl %left, %positive_limit_two") + _qbe_line(writer, "\tjnz %positive_overflow_two, @fail, @multiply") + _qbe_line(writer, "@multiply") + _qbe_line(writer, "\t%result =l mul %left, %right") + _qbe_line(writer, "\tret %result") + _qbe_line(writer, "@zero") + _qbe_line(writer, "\tret 0") + _qbe_line(writer, "@fail") + _qbe_line(writer, "\tcall $trbn_arithmetic_fail()") + _qbe_line(writer, "\thlt") + _qbe_line(writer, "}") + _qbe_line(writer, "") + return +end + +def _qbe_emit_checked_divide(mut writer: StringBuilder) + _qbe_line(writer, "function l $trbn_idiv(l %left, l %right) {") + _qbe_line(writer, "@start") + _qbe_line(writer, "\t%zero =w ceql %right, 0") + _qbe_line(writer, "\tjnz %zero, @fail, @ok") + _qbe_line(writer, "@fail") + _qbe_line(writer, "\tcall $trbn_arithmetic_fail()") + _qbe_line(writer, "\thlt") + _qbe_line(writer, "@ok") + _qbe_line(writer, "\t%result =l div %left, %right") + _qbe_line(writer, "\tret %result") + _qbe_line(writer, "}") + _qbe_line(writer, "") + return +end + +def _qbe_emit_checked_remainder(mut writer: StringBuilder) + _qbe_line(writer, "function l $trbn_irem(l %left, l %right) {") + _qbe_line(writer, "@start") + _qbe_line(writer, "\t%zero =w ceql %right, 0") + _qbe_line(writer, "\tjnz %zero, @fail, @ok") + _qbe_line(writer, "@fail") + _qbe_line(writer, "\tcall $trbn_arithmetic_fail()") + _qbe_line(writer, "\thlt") + _qbe_line(writer, "@ok") + _qbe_line(writer, "\t%result =l rem %left, %right") + _qbe_line(writer, "\tret %result") + _qbe_line(writer, "}") + _qbe_line(writer, "") + return +end + +def _qbe_emit_function( + native_module: Gate1Module, + function: Gate1Function, + function_index: Integer, + static_data: Array, + mut writer: StringBuilder, + ) + values := _qbe_value_names(function) + slots := _qbe_slot_names(function) + mut parameters: Array := [] + mut parameter_index := 0 + while parameter_index < function.parameters.size() + parameter := function.parameters[parameter_index] + parameters.push(_qbe_type(parameter.value_type) + " " + values.fetch(parameter.id)) + parameter_index += 1 + end + mut result_prefix := "" + if function.result_type != ScalarType::Void + result_prefix = _qbe_type(function.result_type) + " " + end + _qbe_line(writer, "function " + result_prefix + "$trbn_f" + function_index.to_s() + "(" + parameters.join(", ") + ") {") + entry_block_index := _qbe_block_index(function.blocks, function.entry) + _qbe_emit_block(native_module, function, entry_block_index, values, slots, static_data, writer) + mut block_index := 0 + while block_index < function.blocks.size() + if block_index != entry_block_index + _qbe_emit_block(native_module, function, block_index, values, slots, static_data, writer) + end + block_index += 1 + end + _qbe_line(writer, "}") + _qbe_line(writer, "") + return +end + +def _qbe_emit_block( + native_module: Gate1Module, + function: Gate1Function, + block_index: Integer, + values: Hash, + slots: Hash, + static_data: Array, + mut writer: StringBuilder, + ) + block := function.blocks[block_index] + _qbe_line(writer, "@b" + block_index.to_s()) + if block.id == function.entry + _qbe_emit_slots(function, slots, writer) + else + _qbe_emit_block_parameter_loads(block, values, slots, writer) + end + mut instruction_index := 0 + while instruction_index < block.instructions.size() + _qbe_emit_instruction( + native_module, + block.instructions[instruction_index], + block_index, + instruction_index, + values, + static_data, + writer, + ) + instruction_index += 1 + end + _qbe_emit_terminator(native_module, function, block.terminator, block_index, values, slots, writer) + return +end + +def _qbe_value_names(function: Gate1Function): Hash + mut values: Hash := {} + mut next_index := 0 + mut parameter_index := 0 + while parameter_index < function.parameters.size() + values[function.parameters[parameter_index].id] = "%v" + next_index.to_s() + next_index += 1 + parameter_index += 1 + end + mut block_index := 0 + while block_index < function.blocks.size() + block := function.blocks[block_index] + parameter_index = 0 + while parameter_index < block.parameters.size() + values[block.parameters[parameter_index].id] = "%v" + next_index.to_s() + next_index += 1 + parameter_index += 1 + end + mut instruction_index := 0 + while instruction_index < block.instructions.size() + result := _qbe_instruction_result(block.instructions[instruction_index]) + if result != nil + values[result] = "%v" + next_index.to_s() + next_index += 1 + end + instruction_index += 1 + end + block_index += 1 + end + return values +end + +def _qbe_instruction_result(instruction: Gate1Instruction): String? + case instruction + when Gate1Instruction::BooleanLiteral(result, _value, _origin) + return result + when Gate1Instruction::IntegerLiteral(result, _value, _origin) + return result + when Gate1Instruction::FloatLiteral(result, _value, _origin) + return result + when Gate1Instruction::IntegerBinary(result, _operator, _left, _right, _origin) + return result + when Gate1Instruction::FloatBinary(result, _operator, _left, _right, _origin) + return result + when Gate1Instruction::IntegerCompare(result, _operator, _left, _right, _origin) + return result + when Gate1Instruction::FloatCompare(result, _operator, _left, _right, _origin) + return result + when Gate1Instruction::BooleanNot(result, _value, _origin) + return result + when Gate1Instruction::Call(result, _function_id, _arguments, _origin) + return result + when Gate1Instruction::WriteStatic(_value, _origin) + return nil + end +end + +def _qbe_slot_names(function: Gate1Function): Hash + mut slots: Hash := {} + mut next_index := 0 + mut block_index := 0 + while block_index < function.blocks.size() + block := function.blocks[block_index] + if block.id != function.entry + mut parameter_index := 0 + while parameter_index < block.parameters.size() + slots[block.parameters[parameter_index].id] = "%slot" + next_index.to_s() + next_index += 1 + parameter_index += 1 + end + end + block_index += 1 + end + return slots +end + +def _qbe_emit_slots(function: Gate1Function, slots: Hash, mut writer: StringBuilder) + mut block_index := 0 + while block_index < function.blocks.size() + block := function.blocks[block_index] + if block.id != function.entry + mut parameter_index := 0 + while parameter_index < block.parameters.size() + parameter := block.parameters[parameter_index] + mut size := "8" + mut allocation := "alloc8" + if parameter.value_type == ScalarType::Boolean + size = "4" + allocation = "alloc4" + end + _qbe_line(writer, "\t" + slots.fetch(parameter.id) + " =l " + allocation + " " + size) + parameter_index += 1 + end + end + block_index += 1 + end + return +end + +def _qbe_emit_block_parameter_loads( + block: Gate1Block, + values: Hash, + slots: Hash, + mut writer: StringBuilder, + ) + mut index := 0 + while index < block.parameters.size() + parameter := block.parameters[index] + _qbe_line(writer, + "\t" + values.fetch(parameter.id) + " =" + _qbe_type(parameter.value_type) + " " + + _qbe_load(parameter.value_type) + " " + slots.fetch(parameter.id) + ) + index += 1 + end + return +end + +def _qbe_emit_instruction( + native_module: Gate1Module, + instruction: Gate1Instruction, + block_index: Integer, + instruction_index: Integer, + values: Hash, + static_data: Array, + mut writer: StringBuilder, + ) + case instruction + when Gate1Instruction::BooleanLiteral(result, value, origin) + _qbe_emit_origin(native_module, origin, writer) + mut literal := "0" + if value + literal = "1" + end + _qbe_line(writer, "\t" + values.fetch(result) + " =w copy " + literal) + when Gate1Instruction::IntegerLiteral(result, value, origin) + _qbe_emit_origin(native_module, origin, writer) + _qbe_line(writer, "\t" + values.fetch(result) + " =l copy " + value.to_s()) + when Gate1Instruction::FloatLiteral(result, value, origin) + _qbe_emit_origin(native_module, origin, writer) + _qbe_line(writer, "\t" + values.fetch(result) + " =d copy d_" + value.to_s()) + when Gate1Instruction::IntegerBinary(result, operator, left, right, origin) + _qbe_emit_origin(native_module, origin, writer) + _qbe_line(writer, + "\t" + values.fetch(result) + " =l call " + _qbe_integer_operator(operator) + + "(l " + values.fetch(left) + ", l " + values.fetch(right) + ")" + ) + when Gate1Instruction::FloatBinary(result, operator, left, right, origin) + _qbe_emit_origin(native_module, origin, writer) + _qbe_line(writer, + "\t" + values.fetch(result) + " =d " + _qbe_float_operator(operator) + " " + + values.fetch(left) + ", " + values.fetch(right) + ) + when Gate1Instruction::IntegerCompare(result, operator, left, right, origin) + _qbe_emit_origin(native_module, origin, writer) + _qbe_line(writer, + "\t" + values.fetch(result) + " =w " + _qbe_comparison(operator, "l") + " " + + values.fetch(left) + ", " + values.fetch(right) + ) + when Gate1Instruction::FloatCompare(result, operator, left, right, origin) + _qbe_emit_origin(native_module, origin, writer) + _qbe_line(writer, + "\t" + values.fetch(result) + " =w " + _qbe_comparison(operator, "d") + " " + + values.fetch(left) + ", " + values.fetch(right) + ) + when Gate1Instruction::BooleanNot(result, value, origin) + _qbe_emit_origin(native_module, origin, writer) + _qbe_line(writer, "\t" + values.fetch(result) + " =w ceqw " + values.fetch(value) + ", 0") + when Gate1Instruction::Call(result, function_id, arguments, origin) + _qbe_emit_origin(native_module, origin, writer) + callee_index := _qbe_function_index(native_module.functions, function_id) + callee := native_module.functions[callee_index] + mut call_arguments: Array := [] + mut argument_index := 0 + while argument_index < arguments.size() + call_arguments.push( + _qbe_type(callee.parameters[argument_index].value_type) + " " + values.fetch(arguments[argument_index]) + ) + argument_index += 1 + end + mut prefix := "\t" + if result != nil + prefix = "\t" + values.fetch(result) + " =" + _qbe_type(callee.result_type) + " " + end + _qbe_line(writer, prefix + "call $trbn_f" + callee_index.to_s() + "(" + call_arguments.join(", ") + ")") + when Gate1Instruction::WriteStatic(value, origin) + _qbe_emit_origin(native_module, origin, writer) + symbol := _qbe_static_symbol(static_data, value) + _qbe_line(writer, + "\t%io_" + block_index.to_s() + "_" + instruction_index.to_s() + + " =l call $write(w 1, l " + symbol + ", l " + value.to_bytes().size().to_s() + ")" + ) + end + return +end + +def _qbe_emit_terminator( + native_module: Gate1Module, + function: Gate1Function, + terminator: Gate1Terminator, + block_index: Integer, + values: Hash, + slots: Hash, + mut writer: StringBuilder, + ) + case terminator + when Gate1Terminator::Jump(target, arguments, origin) + _qbe_emit_origin(native_module, origin, writer) + _qbe_emit_edge(function, target, arguments, values, slots, writer) + _qbe_line(writer, "\tjmp " + _qbe_block_label(function.blocks, target)) + when Gate1Terminator::Branch(condition, when_true, true_arguments, when_false, false_arguments, origin) + _qbe_emit_origin(native_module, origin, writer) + true_edge := "@edge_" + block_index.to_s() + "_true" + false_edge := "@edge_" + block_index.to_s() + "_false" + _qbe_line(writer, "\tjnz " + values.fetch(condition) + ", " + true_edge + ", " + false_edge) + _qbe_line(writer, true_edge) + _qbe_emit_edge(function, when_true, true_arguments, values, slots, writer) + _qbe_line(writer, "\tjmp " + _qbe_block_label(function.blocks, when_true)) + _qbe_line(writer, false_edge) + _qbe_emit_edge(function, when_false, false_arguments, values, slots, writer) + _qbe_line(writer, "\tjmp " + _qbe_block_label(function.blocks, when_false)) + when Gate1Terminator::Return(value, origin) + _qbe_emit_origin(native_module, origin, writer) + if value == nil + _qbe_line(writer, "\tret") + else + _qbe_line(writer, "\tret " + values.fetch(value)) + end + end + return +end + +def _qbe_emit_edge( + function: Gate1Function, + target: String, + arguments: Array, + values: Hash, + slots: Hash, + mut writer: StringBuilder, + ) + block := _qbe_find_block(function.blocks, target) + mut index := 0 + while index < arguments.size() + parameter := block.parameters[index] + _qbe_line(writer, "\t" + _qbe_store(parameter.value_type) + " " + values.fetch(arguments[index]) + ", " + slots.fetch(parameter.id)) + index += 1 + end + return +end + +def _qbe_emit_entry(native_module: Gate1Module, mut writer: StringBuilder) + entry_index := _qbe_function_index(native_module.functions, native_module.entry_function) + _qbe_line(writer, "export function w $main() {") + _qbe_line(writer, "@start") + _qbe_line(writer, "\tcall $trbn_f" + entry_index.to_s() + "()") + _qbe_line(writer, "\tret 0") + _qbe_line(writer, "}") + return +end + +def _qbe_emit_origin(native_module: Gate1Module, origin: SourceOrigin, mut writer: StringBuilder) + mut source_index := 0 + while native_module.sources[source_index].id != origin.source_id + source_index += 1 + end + _qbe_line(writer, + "\t# trb-origin source=" + source_index.to_s() + " start=" + origin.start_line.to_s() + ":" + + origin.start_column.to_s() + " end=" + origin.end_line.to_s() + ":" + origin.end_column.to_s() + ) + return +end + +def _qbe_integer_operator(operator: IntegerBinaryOperator): String + case operator + when IntegerBinaryOperator::Add + return "$trbn_iadd" + when IntegerBinaryOperator::Subtract + return "$trbn_isub" + when IntegerBinaryOperator::Multiply + return "$trbn_imul" + when IntegerBinaryOperator::Divide + return "$trbn_idiv" + when IntegerBinaryOperator::Remainder + return "$trbn_irem" + end +end + +def _qbe_float_operator(operator: FloatBinaryOperator): String + case operator + when FloatBinaryOperator::Add + return "add" + when FloatBinaryOperator::Subtract + return "sub" + when FloatBinaryOperator::Multiply + return "mul" + when FloatBinaryOperator::Divide + return "div" + end +end + +def _qbe_comparison(operator: ComparisonOperator, suffix: String): String + case operator + when ComparisonOperator::Equal + return "ceq" + suffix + when ComparisonOperator::NotEqual + return "cne" + suffix + when ComparisonOperator::LessThan + if suffix == "l" + return "csltl" + end + return "cltd" + when ComparisonOperator::LessThanOrEqual + if suffix == "l" + return "cslel" + end + return "cled" + when ComparisonOperator::GreaterThan + if suffix == "l" + return "csgtl" + end + return "cgtd" + when ComparisonOperator::GreaterThanOrEqual + if suffix == "l" + return "csgel" + end + return "cged" + end +end + +def _qbe_type(value_type: ScalarType): String + case value_type + when ScalarType::Boolean + return "w" + when ScalarType::Integer + return "l" + when ScalarType::Float + return "d" + when ScalarType::Void + return "" + end +end + +def _qbe_load(value_type: ScalarType): String + if value_type == ScalarType::Boolean + return "loadw" + end + if value_type == ScalarType::Float + return "loadd" + end + return "loadl" +end + +def _qbe_store(value_type: ScalarType): String + if value_type == ScalarType::Boolean + return "storew" + end + if value_type == ScalarType::Float + return "stored" + end + return "storel" +end + +def _qbe_function_index(functions: Array, id: String): Integer + mut index := 0 + while functions[index].id != id + index += 1 + end + return index +end + +def _qbe_block_label(blocks: Array, id: String): String + return "@b" + _qbe_block_index(blocks, id).to_s() +end + +def _qbe_block_index(blocks: Array, id: String): Integer + mut index := 0 + while blocks[index].id != id + index += 1 + end + return index +end + +def _qbe_find_block(blocks: Array, id: String): Gate1Block + mut index := 0 + while blocks[index].id != id + index += 1 + end + return blocks[index] +end + +def _qbe_static_symbol(static_data: Array, value: String): String + mut index := 0 + while static_data[index].value != value + index += 1 + end + return static_data[index].symbol +end diff --git a/src/snapshot.trb b/src/snapshot.trb index 57bc746d..b35488b0 100644 --- a/src/snapshot.trb +++ b/src/snapshot.trb @@ -210,7 +210,7 @@ def _decode_function(value: JsonValue, path: String): Result::Ok( SnapshotBlock.new( id: try _required_string(fields, "id", path, true), - origin: try _decode_origin(try _required_field(fields, "origin", path), path + "/origin"), + origin: try decode_origin(try _required_field(fields, "origin", path), path + "/origin"), instructions: instructions, ) ) @@ -248,7 +248,7 @@ def _decode_instruction(value: JsonValue, path: String): Result::Ok( SnapshotInstruction::Return( try _required_string(fields, "value", path, true), - try _decode_origin(try _required_field(fields, "origin", path), path + "/origin"), + try decode_origin(try _required_field(fields, "origin", path), path + "/origin"), ) ) end @@ -277,7 +277,7 @@ def _decode_instruction(value: JsonValue, path: String): Result +def decode_origin(value: JsonValue, path: String): Result fields := try _strict_object( value, path,