diff --git a/.agents/skills/develop-typerb-native/SKILL.md b/.agents/skills/develop-typerb-native/SKILL.md new file mode 100644 index 00000000..a83a4cf4 --- /dev/null +++ b/.agents/skills/develop-typerb-native/SKILL.md @@ -0,0 +1,52 @@ +--- +name: develop-typerb-native +description: Implement and review TypeRB Native compiler, MIR, runtime, bootstrap, gate, fixture, and benchmark work. Use for changes in type-rb/type-rb-native, especially when deciding the current gate scope, preserving TypeRB semantics, or updating the pinned reference compiler. +--- + +# Develop TypeRB Native + +Work on one recorded experiment gate at a time. + +## Establish the boundary + +1. Read `README.md`, `docs/architecture.md`, and `docs/experiment-plan.md`. +2. Read the decisions relevant to the change. +3. Treat `type-rb/type-rb` at `TYPE_RB_REVISION` as the language, compiler, and + conformance source of truth. +4. State the current gate and its exit condition before expanding scope. + +## Implement + +- Write repository-owned executable compiler and runtime source in TypeRB. +- Allow external code generators, assemblers, linkers, SDKs, and system + libraries only behind explicit boundaries whose time and distribution cost + can be measured. +- Preserve source origins and exact TypeRB semantics through every lowering. +- Reject unknown, malformed, unsupported, or unverifiable input with stable, + deterministic diagnostics. Never add a semantic fallback or `Any` escape + hatch to improve a benchmark. +- Keep bootstrap snapshots, Native MIR, ABI profiles, and runtime interfaces + internal and unstable until a decision explicitly promotes them. +- Add only the feature set required by the active gate. Record a new decision + before changing language semantics, ownership boundaries, self-hosting + criteria, or backend selection policy. + +## Verify + +From the repository root, use the compiler revision in `TYPE_RB_REVISION` and +run: + +```sh +trb fmt --check . +trb check +TYPE_RB_NATIVE_ROOT="$PWD" trb test +``` + +For executable gates, run the same differential corpus through the optimized +Go reference baseline and every active native candidate. Count frontend, +serialization, lowering, code generation, assembly, linking, runtime, sidecar, +and distribution costs according to `docs/experiment-plan.md`. + +When the active gate passes, stop before starting the next gate. Report the +implemented subset, evidence for every exit condition, measurements, known +limitations, discarded paths, and decisions that need maintainer discussion. diff --git a/.github/workflows/gate-zero.yml b/.github/workflows/gate-zero.yml new file mode 100644 index 00000000..1ce8caa0 --- /dev/null +++ b/.github/workflows/gate-zero.yml @@ -0,0 +1,44 @@ +name: Gate 0 + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: Check out TypeRB Native + uses: actions/checkout@v5 + + - name: Check out the pinned TypeRB compiler + uses: actions/checkout@v5 + with: + repository: type-rb/type-rb + ref: 30b5f5206680a39a02966d1579f76de55760a349 + path: .type-rb + + - name: Set up Go for the bootstrap compiler + uses: actions/setup-go@v6 + with: + go-version-file: .type-rb/go.mod + cache-dependency-path: .type-rb/go.sum + + - name: Verify the compiler revision pin + run: test "$(cat TYPE_RB_REVISION)" = "$(git -C .type-rb rev-parse HEAD)" + + - name: Verify formatting + run: .type-rb/trb fmt --check . + + - name: Check TypeRB sources + run: .type-rb/trb check + + - name: Run Gate 0 tests and fixtures + env: + TYPE_RB_NATIVE_ROOT: ${{ github.workspace }} + run: .type-rb/trb test diff --git a/.gitignore b/.gitignore index 21a4afc1..37f008e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .DS_Store +/.trb/ +/bin/ /build/ /dist/ /tmp/ diff --git a/AGENTS.md b/AGENTS.md index 17f2ef0f..563d841e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,3 +25,9 @@ TypeRB backend or product commitment. unchecked lowering, or `Any`-shaped escape hatches. - Add only the structure required by the current experiment gate. Do not add release, package, or compatibility machinery before a real consumer needs it. +- Write repository-owned compiler and runtime implementation source in TypeRB. + External code generators, assemblers, linkers, SDKs, and system libraries are + allowed when their role and cost are explicit. +- Treat reproducible TypeRB self-hosting as a required promotion outcome. The + Go reference compiler is an early bootstrap and differential oracle, not part + of the ordinary final release path. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 563815b2..bc2ad8ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,6 +34,23 @@ Performance does not justify changing portable integer behavior, Unicode behavior, failure behavior, initialization order, source attribution, or other TypeRB guarantees. +Repository-owned compiler and runtime implementation source must be TypeRB. +External backend and platform tools are allowed when their revisions, licenses, +invocations, and distribution costs are explicit. Do not introduce a permanent +Go, Rust, Zig, or C host implementation as an intermediate shortcut. + +Use the pinned reference compiler revision recorded in `TYPE_RB_REVISION` for +gate verification. A revision update is a reviewed compatibility change, not +an incidental tool upgrade. + +Run the Gate 0 checks from the repository root: + +```sh +trb fmt --check . +trb check +TYPE_RB_NATIVE_ROOT="$PWD" trb test +``` + ## Backend changes Backend candidates share Native MIR, conformance inputs, and benchmark policy. diff --git a/README.md b/README.md index 903c41a6..bb28b031 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ TypeRB Native explores whether a TypeRB-specific native compiler and runtime can improve end-to-end build time, generated-program performance, and deployed binary size relative to an optimized release executable produced by the -reference compiler's Go backend. +reference compiler's Go backend. Its long-term objective is a self-hosted +compiler whose repository-owned implementation is written in TypeRB and whose +ordinary release/bootstrap path does not require Go or another host language. The [TypeRB repository](https://github.com/type-rb/type-rb) remains the source of truth for the language specification, reference compiler, supported @@ -19,6 +21,8 @@ semantics; it does not define a native-only TypeRB dialect. - Test a native AOT pipeline without requiring the Go toolchain to compile a TypeRB application. +- Reach reproducible self-hosting: a native TypeRB compiler builds the next + equivalent native TypeRB compiler from TypeRB source. - Design a small Native MIR, target ABI profiles, data layout, and runtime. - Compare multiple machine-code strategies behind the same MIR and semantics. - Measure complete toolchains, including code generation, linking, runtime, @@ -28,11 +32,13 @@ semantics; it does not define a native-only TypeRB dialect. ## Current status -The repository currently contains design and experiment policy only. It does -not compile TypeRB programs and does not provide a production runtime, stable -ABI, stable artifact format, or compatibility guarantee. +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. -The first implementation milestone will use a small, heap-free corpus with +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. @@ -57,15 +63,33 @@ validate -> Native MIR -> optimize -> codegen -> object -> link The bootstrap snapshot is a temporary, data-only bridge. It is not the public compiler tooling protocol, a package-extension API, or a stable serialization -of the reference compiler's internal typed IR. Native MIR remains internal to -this repository. +of the reference compiler's internal typed IR. During early gates the Go +reference compiler may produce that bridge. Later gates replace the bridge's +frontend side with a TypeRB implementation in this repository. Native MIR +remains internal here. + +The intended bootstrap sequence is: + +```text +Go reference compiler -> B0 native compiler from TypeRB source +B0 native compiler -> B1 native compiler +B1 native compiler -> B2 native compiler +B1 and B2 -> reproducibly equivalent artifacts +``` + +The Go compiler remains a differential oracle, but it is not part of the +ordinary self-hosted release/bootstrap chain. External code generators, +assemblers, linkers, SDKs, and system libraries may remain explicit toolchain +dependencies. See [Architecture](docs/architecture.md) for the ownership and pipeline boundaries. ## Backend experiments -Candidate strategies under consideration are: +QBE is the first planned executable path because it gives the lowest-cost test +of the TypeRB runtime and ABI hypothesis. Candidate roles under consideration +are: - [Cranelift](https://cranelift.dev/) as a balanced fast-codegen candidate; - [LLVM](https://llvm.org/) as a high-optimization comparison; @@ -85,11 +109,13 @@ measurement rules, and abandonment criteria. ## Non-goals -The initial experiment does not attempt to: +The initial gates do not attempt to: -- replace the Go implementation of the reference compiler; - port the compiler to Rust, Zig, or another host implementation language; -- make self-hosting a prerequisite for native-code feasibility; +- replace external code generators, assemblers, linkers, SDKs, or system + libraries merely to claim self-hosting; +- implement the full TypeRB frontend before native execution feasibility has + passed its earlier gates; - commit TypeRB to a supported native mode; - expose mutable compiler internals or backend hooks as a package API; - support the full standard library, Web, ORM, Jobs, or native package @@ -99,15 +125,16 @@ The initial experiment does not attempt to: - claim an advantage over Go without reproducible end-to-end measurements. External code generators, assemblers, and linkers may be used as experimental -components. This experiment remains responsible for preserving TypeRB semantics -and for implementing its MIR, ABI profiles, and runtime. Normative semantics -remain in the reference repository. +components. Repository-owned compiler, MIR, ABI, and runtime implementation +source is written in TypeRB. Normative semantics remain in the reference +repository. ## Documentation - [Architecture](docs/architecture.md) - [Experiment plan](docs/experiment-plan.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) - [Contributing](CONTRIBUTING.md) - [Security](SECURITY.md) diff --git a/TYPE_RB_REVISION b/TYPE_RB_REVISION new file mode 100644 index 00000000..13edaf02 --- /dev/null +++ b/TYPE_RB_REVISION @@ -0,0 +1 @@ +30b5f5206680a39a02966d1579f76de55760a349 diff --git a/docs/architecture.md b/docs/architecture.md index 66690383..daf6c718 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,11 +8,12 @@ question is whether a native pipeline can improve the practical tradeoff among build time, execution performance, and deployed binary size after all required tooling is counted. -The experiment is not a host-language rewrite. Self-hosting and native code -generation are independent concerns: the Go implementation of the reference -compiler can bootstrap a native backend, and a hypothetical TypeRB-authored -compiler could still target Go. The latter is an architectural example, not a -planned milestone. +The experiment is not a port to a different host language. Native execution +and self-hosting are separate checkpoints, but both belong to the intended +path. The Go reference compiler bootstraps early artifacts and remains a +differential oracle. The completed compiler and runtime owned by this +repository are written in TypeRB and reproduce themselves without Go in the +ordinary release/bootstrap path. ## Ownership boundary @@ -26,6 +27,7 @@ The [reference TypeRB repository](https://github.com/type-rb/type-rb) owns: This repository owns only experimental native concerns: +- an independent TypeRB-authored frontend when the self-hosting gates reach it; - bootstrap snapshot validation and lowering; - Native MIR and its verifier; - native data layout and target ABI profiles; @@ -34,9 +36,23 @@ This repository owns only experimental native concerns: - the experimental runtime; and - native correctness, portability, and performance measurements. -The normal TypeRB build, test, and release paths must not depend on this -repository. A language-level change discovered here belongs in the reference -repository's normal design and review process. +The normal reference TypeRB build, test, and release paths must not depend on +this repository. A language-level change discovered here belongs in the +reference repository's normal design and review process. Until the independent +frontend exists, the reference implementation may provide a narrow snapshot +producer on a short-lived, removable experimental surface. + +## Implementation-language boundary + +Repository-owned executable compiler and runtime source is written in TypeRB. +Go, Rust, Zig, C, or another existing implementation language is not introduced +as the permanent host for those components. Generated C, assembly, object +files, or backend IR are outputs rather than maintained implementation source. + +External tools remain allowed and must be accounted for. QBE or LLVM, an +assembler, a linker, an SDK, and system libraries do not violate self-hosting; +they are explicit dependencies of a TypeRB-authored compiler in the same way a +linker can be a dependency of another self-hosted language implementation. ## Pipeline @@ -62,6 +78,22 @@ TypeRB source Each boundary must preserve source origins so diagnostics and runtime failures can eventually refer to authored TypeRB source. +The bootstrap snapshot is intentionally transitional. After native execution +and runtime viability are established, the repository gains its own +TypeRB-authored parser, resolver, checker, and lowering. The reproducible +self-hosting sequence is: + +```text +reference Go compiler -> B0 from TypeRB compiler sources +B0 -> B1 +B1 -> B2 +compare(B1, B2) -> equivalent under the reproducibility policy +``` + +Published native releases use a previously released native compiler as their +seed. Building the bootstrap seed from Go is a recovery/development path, not +an ordinary release requirement. + ## Bootstrap snapshot The bootstrap snapshot is a deterministic, versioned, target-neutral, data-only @@ -111,8 +143,9 @@ concepts into portable TypeRB source. ## Backend adapters -Candidate adapters consume the same verified, target-neutral MIR subset. Target -lowering selects a versioned ABI profile for an operating system and +Candidate adapters consume the same verified, target-neutral MIR subset. QBE +is tried first to minimize the cost of the initial executable experiment. +Target lowering selects a versioned ABI profile for an operating system and architecture. Backend comparisons on the same target use the same profile. | Candidate | Experimental role | @@ -181,7 +214,8 @@ repository is stable. Official TypeRB packages must not depend on it. Promotion to a supported TypeRB target is a separate decision. It would require representative portable conformance, source-mapped diagnostics and failures, runtime and package boundaries, reproducible builds, primary-platform support, -and an end-to-end advantage after the complete toolchain is counted. +an end-to-end advantage after the complete toolchain is counted, and a +reproducible self-hosted compiler build whose ordinary path does not use Go. If the experiment is abandoned, the native implementation and any bootstrap bridge should remain removable. Reusable benchmark and conformance findings may diff --git a/docs/decisions/0001-experimental-native-toolchain.md b/docs/decisions/0001-experimental-native-toolchain.md index bbaf614c..fbfa208c 100644 --- a/docs/decisions/0001-experimental-native-toolchain.md +++ b/docs/decisions/0001-experimental-native-toolchain.md @@ -2,6 +2,10 @@ Status: accepted for the initial experiment +Self-hosting scope is amended by +[Decision 0002](0002-typerb-owned-self-hosting.md). This decision still defines +the repository and experimental compiler boundary. + ## Context The reference TypeRB compiler emits Go, Ruby, and TypeScript from one checked @@ -78,6 +82,10 @@ Rejected because it duplicates a working parser, resolver, checker, diagnostics, and tooling before answering the native runtime and backend feasibility question. +This rejection concerns sequencing, not the final implementation boundary. +Decision 0002 requires a TypeRB-authored frontend after earlier native +execution and runtime gates have justified that investment. + ### Publish the existing typed IR as a stable API Rejected because it would freeze reference compiler implementation details and diff --git a/docs/decisions/0002-typerb-owned-self-hosting.md b/docs/decisions/0002-typerb-owned-self-hosting.md new file mode 100644 index 00000000..61dcec78 --- /dev/null +++ b/docs/decisions/0002-typerb-owned-self-hosting.md @@ -0,0 +1,86 @@ +# 0002: TypeRB-Owned Self-Hosting + +Status: accepted + +## Context + +Native code generation alone could remove the Go toolchain from application +builds while leaving the compiler itself implemented in Go. That would test an +important product property, but it would not meet this project's intended +ownership boundary. The desired end state is a TypeRB compiler and runtime +whose maintained implementation is TypeRB and whose ordinary release path can +reproduce the compiler without Go or another host implementation language. + +Building a complete frontend before testing native execution would still spend +substantial effort before resolving the runtime, ABI, performance, and +distribution questions. Self-hosting therefore needs to be an explicit later +gate without becoming a prerequisite for the earliest executable experiment. + +## Decision + +All repository-owned compiler and runtime implementation source is written in +TypeRB. The project is not ported to Rust, Zig, C, or another host language. + +External code generators, assemblers, linkers, SDKs, and system libraries are +allowed. Their complete cost and distribution requirements are measured and +reported. Generated backend IR, C, assembly, and object files are compiler +outputs rather than maintained host implementations. + +Early gates use the Go reference compiler to compile TypeRB implementation +source and may consume a narrow, versioned, data-only bootstrap snapshot. The +Go compiler remains the semantic reference and differential oracle throughout +the experiment. + +Later gates add an independent TypeRB-authored parser, resolver, checker, and +compiler driver. Bootstrap is demonstrated in stages: + +1. The Go reference compiler builds the B0 native compiler from TypeRB source. +2. B0 builds the B1 native compiler from the same source. +3. B1 builds B2. +4. B1 and B2 are reproducibly equivalent under the recorded policy and behave + equivalently on the compiler conformance corpus. + +Ordinary releases then use a previous native TypeRB compiler as their seed. A +Go bootstrap remains available for recovery and differential testing, but is +not required by the normal build or release path. + +## Consequences + +- Native feasibility can be tested before duplicating the complete frontend. +- Gate 0 and later implementation work starts in TypeRB, so early components + can become part of the self-hosted compiler rather than being rewritten. +- The temporary snapshot boundary remains removable once the independent + frontend lowers directly to Native MIR. +- Self-hosting performance is measured explicitly; a fast Go-bootstrapped + prototype does not by itself pass product feasibility. +- Using QBE, LLVM, a linker, or platform libraries does not disqualify the + compiler from self-hosting, but every dependency remains part of toolchain + size, build time, portability, licensing, and security evaluation. +- The independent frontend must conform to the reference specification. This + repository does not gain authority to define a divergent native dialect. + +## Alternatives considered + +### Keep the compiler implemented in Go + +Rejected as the final state because application builds could become native +while compiler development and release bootstrap still depend on Go-owned +implementation source. + +### Port the compiler to another systems language + +Rejected because it replaces one host-language dependency with another and +does not advance TypeRB self-hosting. + +### Replace every external tool + +Rejected because self-hosting concerns the implementation owned by this +project. Reimplementing mature code generators, assemblers, linkers, SDKs, or +system libraries is a separate question and would obscure the native compiler +experiment's cost and risk. + +### Require self-hosting before native execution + +Rejected as an ordering constraint. It would duplicate the complete frontend +before the experiment has shown that the native runtime and backend can meet +their correctness and product goals. diff --git a/docs/experiment-plan.md b/docs/experiment-plan.md index d12d293c..4f5b79d3 100644 --- a/docs/experiment-plan.md +++ b/docs/experiment-plan.md @@ -9,6 +9,10 @@ outcomes without unacceptable regressions in the others? 2. Generated-program execution time. 3. Deployed executable size. +If it can, can the compiler and runtime be implemented in TypeRB, reproduce +themselves, and retain competitive build time and generated-code behavior once +the complete self-hosted toolchain is measured? + Secondary outcomes include compiler and runtime peak memory, startup latency, toolchain distribution size, portability, diagnostics, correctness risk, and maintenance cost. @@ -41,9 +45,9 @@ The candidates are not implemented to production completeness in parallel. They advance through small shared gates and can be removed early. 1. Use hand-authored bootstrap and MIR fixtures to validate the boundary. -2. Use QBE or a similarly small path for the cheapest runtime feasibility - check. -3. Use Cranelift as the first balanced AOT candidate. +2. Use QBE for the cheapest runtime and ABI feasibility check. +3. Consider Cranelift only if the QBE result leaves a measured development + code-generation problem worth testing. 4. Add LLVM only after the corpus is representative enough to measure an optimization ceiling. 5. Attempt a direct emitter only if profiling shows codegen or toolchain @@ -51,19 +55,32 @@ They advance through small shared gates and can be removed early. This order is a starting hypothesis, not a compatibility promise. +TinyGo may be measured once as a time-boxed calibration of the optimized Go +baseline. It is not a path to the required Go-independent compiler and is not +a gate deliverable. A C emitter is likewise deferred unless later profiling +shows that it answers a specific question more cheaply than the selected +backend. Neither is built merely to populate a comparison table. + ## Gates ### Gate 0: Boundary Scope: -- versioned, data-only fixtures; -- strict snapshot and MIR verification; -- deterministic diagnostics for malformed and unsupported input; and -- source identity retained through lowering. - -Exit condition: the experiment can validate and lower fixtures without -importing the reference compiler's internal objects. +- a documented versioned, data-only bootstrap snapshot subset; +- a TypeRB implementation of strict snapshot decoding and validation; +- a distinct Native MIR model, lowering, and verifier implemented in TypeRB; +- deterministic diagnostic codes, paths, and messages for malformed, + unsupported, and structurally invalid input; +- source identity and spans retained on every lowered function, block, and + instruction; and +- valid and invalid fixtures plus portable tests. + +Exit condition: the pinned reference TypeRB compiler can check and test the +Gate 0 implementation, and the implementation can validate and lower all Gate +0 fixtures without importing reference compiler internal objects. Malformed, +unknown, unsupported, and invariant-breaking fixtures fail deterministically; +valid input produces verified Native MIR with unchanged source origins. ### Gate 1: Heap-free execution @@ -107,7 +124,18 @@ Scope may expand to: At most one default candidate should normally reach broad runtime work. A second candidate requires a distinct, measured development or release role. -### Gate 4: Product feasibility +### Gate 4: Self-hosting compiler completeness + +Scope expands to a TypeRB-authored parser, resolver, checker, and compiler +driver sufficient to compile this repository's compiler sources. The Go +reference compiler remains a semantic oracle and recovery bootstrap but is not +linked into the native compiler. + +Exit condition: a Go-bootstrapped B0 compiler produces B1 from the TypeRB +compiler sources, and B1 produces B2 with matching observable compiler +behavior on the conformance corpus. + +### Gate 5: Self-hosted product feasibility This gate is not authorization to ship. It evaluates: @@ -118,6 +146,11 @@ This gate is not authorization to ship. It evaluates: - debugging and operational behavior; and - total ongoing maintenance cost. +It also requires B1 and B2 compiler artifacts to be reproducibly equivalent +under a documented normalization policy, and all build-time, memory, runtime, +binary-size, and toolchain-size measurements to use the self-hosted path. A +previous native release is the ordinary bootstrap seed; Go is not required. + Promotion requires a separate TypeRB design decision. ## Correctness checks diff --git a/docs/gate-0-snapshot.md b/docs/gate-0-snapshot.md new file mode 100644 index 00000000..219b9002 --- /dev/null +++ b/docs/gate-0-snapshot.md @@ -0,0 +1,71 @@ +# Gate 0 Bootstrap Snapshot + +Gate 0 accepts a deliberately small, versioned JSON snapshot. The format is a +temporary bootstrap boundary, not a public compiler API. Producer and consumer +revisions are pinned exactly while the experiment is active. + +## Envelope + +The root object has exactly these fields: + +- `format`: `"type-rb-bootstrap-snapshot"` +- `version`: `1` +- `module`: a non-empty stable module identifier +- `sources`: an array of `{ "id", "path" }` objects +- `functions`: an array of function objects + +Unknown or missing fields are errors at every object level. Limits are checked +before lowering: 1 MiB of JSON text, 128 sources, 256 functions, 256 blocks per +function, 4,096 instructions per block, and 256 Unicode code points per stable +identifier. + +## Gate 0 function subset + +A function has `id`, `name`, `result`, `entry`, `origin`, and `blocks`. Gate 0 +accepts only the `Integer` result type and exactly one block, identified by +`entry`. The envelope retains an explicit block array so Gate 1 can add control +flow without replacing the data boundary. Each block has `id`, `origin`, and +`instructions`. + +Every origin contains: + +```json +{ + "source": "main", + "startLine": 1, + "startColumn": 1, + "endLine": 1, + "endColumn": 5 +} +``` + +The source identifier must exist. Lines and columns are positive, and the end +position cannot precede the start position. + +Gate 0 instructions are: + +- `integer_literal`: `op`, `result`, `value`, and `origin` +- `integer_add_checked`: `op`, `result`, `left`, `right`, and `origin` +- `return`: `op`, `value`, and `origin` + +Value identifiers are single-assignment within a function. Operands must name +previously defined values. A block has exactly one terminator, and `return` +must be its final instruction. These constraints intentionally describe only +the boundary corpus needed before Gate 1 adds executable control flow. + +## Diagnostics + +Validation stops at the first error in deterministic document order. A +diagnostic has a stable code, JSON-style path, message, and optional authored +source coordinates: + +| Code | Meaning | +| --- | --- | +| `TRBN0001` | malformed JSON syntax | +| `TRBN0002` | schema type, missing field, or unknown field | +| `TRBN0003` | unsupported format version, type, or operation | +| `TRBN0004` | snapshot or MIR invariant violation | +| `TRBN0005` | resource limit exceeded | + +Messages remain experimental; tests and integrations may rely on the code and +path only after a later decision explicitly stabilizes them. diff --git a/fixtures/gate0/invalid/malformed.json b/fixtures/gate0/invalid/malformed.json new file mode 100644 index 00000000..c5d51fa3 --- /dev/null +++ b/fixtures/gate0/invalid/malformed.json @@ -0,0 +1 @@ +{"format":"type-rb-bootstrap-snapshot", diff --git a/fixtures/gate0/invalid/undefined-value.json b/fixtures/gate0/invalid/undefined-value.json new file mode 100644 index 00000000..df36c899 --- /dev/null +++ b/fixtures/gate0/invalid/undefined-value.json @@ -0,0 +1,22 @@ +{ + "format": "type-rb-bootstrap-snapshot", + "version": 1, + "module": "example/main", + "sources": [{ "id": "main", "path": "src/main.trb" }], + "functions": [{ + "id": "example/main#main", + "name": "main", + "result": "Integer", + "entry": "entry", + "origin": { "source": "main", "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 4 }, + "blocks": [{ + "id": "entry", + "origin": { "source": "main", "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 4 }, + "instructions": [{ + "op": "return", + "value": "missing", + "origin": { "source": "main", "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 4 } + }] + }] + }] +} diff --git a/fixtures/gate0/invalid/unknown-field.json b/fixtures/gate0/invalid/unknown-field.json new file mode 100644 index 00000000..15d6f02a --- /dev/null +++ b/fixtures/gate0/invalid/unknown-field.json @@ -0,0 +1,8 @@ +{ + "format": "type-rb-bootstrap-snapshot", + "version": 1, + "module": "example/main", + "sources": [], + "functions": [], + "unexpected": true +} diff --git a/fixtures/gate0/invalid/unknown-source.json b/fixtures/gate0/invalid/unknown-source.json new file mode 100644 index 00000000..05025643 --- /dev/null +++ b/fixtures/gate0/invalid/unknown-source.json @@ -0,0 +1,14 @@ +{ + "format": "type-rb-bootstrap-snapshot", + "version": 1, + "module": "example/main", + "sources": [{ "id": "main", "path": "src/main.trb" }], + "functions": [{ + "id": "example/main#main", + "name": "main", + "result": "Integer", + "entry": "entry", + "origin": { "source": "missing", "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 4 }, + "blocks": [] + }] +} diff --git a/fixtures/gate0/invalid/unsupported-version.json b/fixtures/gate0/invalid/unsupported-version.json new file mode 100644 index 00000000..3796282b --- /dev/null +++ b/fixtures/gate0/invalid/unsupported-version.json @@ -0,0 +1,7 @@ +{ + "format": "type-rb-bootstrap-snapshot", + "version": 2, + "module": "example/main", + "sources": [], + "functions": [] +} diff --git a/fixtures/gate0/valid/integer-add.json b/fixtures/gate0/valid/integer-add.json new file mode 100644 index 00000000..801653ed --- /dev/null +++ b/fixtures/gate0/valid/integer-add.json @@ -0,0 +1,29 @@ +{ + "format": "type-rb-bootstrap-snapshot", + "version": 1, + "module": "example/main", + "sources": [ + { "id": "main", "path": "src/main.trb" } + ], + "functions": [ + { + "id": "example/main#main", + "name": "main", + "result": "Integer", + "entry": "entry", + "origin": { "source": "main", "startLine": 1, "startColumn": 1, "endLine": 5, "endColumn": 4 }, + "blocks": [ + { + "id": "entry", + "origin": { "source": "main", "startLine": 2, "startColumn": 2, "endLine": 4, "endColumn": 15 }, + "instructions": [ + { "op": "integer_literal", "result": "v0", "value": 40, "origin": { "source": "main", "startLine": 2, "startColumn": 8, "endLine": 2, "endColumn": 9 } }, + { "op": "integer_literal", "result": "v1", "value": 2, "origin": { "source": "main", "startLine": 3, "startColumn": 8, "endLine": 3, "endColumn": 8 } }, + { "op": "integer_add_checked", "result": "v2", "left": "v0", "right": "v1", "origin": { "source": "main", "startLine": 4, "startColumn": 9, "endLine": 4, "endColumn": 13 } }, + { "op": "return", "value": "v2", "origin": { "source": "main", "startLine": 4, "startColumn": 2, "endLine": 4, "endColumn": 13 } } + ] + } + ] + } + ] +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..4583acde --- /dev/null +++ b/go.mod @@ -0,0 +1,4 @@ +// Generated from trbconfig.jsonc by trb. +module github.com/type-rb/type-rb-native/bootstrap + +go 1.27 diff --git a/src/diagnostic.trb b/src/diagnostic.trb new file mode 100644 index 00000000..f6d07312 --- /dev/null +++ b/src/diagnostic.trb @@ -0,0 +1,14 @@ +record Diagnostic + code: String + path: String + message: String + source_id: String? = nil + start_line: Integer? = nil + start_column: Integer? = nil + end_line: Integer? = nil + end_column: Integer? = nil +end + +def boundary_diagnostic(code: String, path: String, message: String): Diagnostic + return Diagnostic.new(code: code, path: path, message: message) +end diff --git a/src/gate0.trb b/src/gate0.trb new file mode 100644 index 00000000..f532820f --- /dev/null +++ b/src/gate0.trb @@ -0,0 +1,11 @@ +import { Diagnostic } from diagnostic +import { NativeModule, lower_snapshot, verify_native_mir } from native_mir +import { decode_snapshot } from snapshot +import { Result } from trb/std/result + +def validate_and_lower(source: String): Result + snapshot := try decode_snapshot(source) + native_module := lower_snapshot(snapshot) + try verify_native_mir(native_module) + return Result::Ok(native_module) +end diff --git a/src/gate0_test.trb b/src/gate0_test.trb new file mode 100644 index 00000000..193f9a22 --- /dev/null +++ b/src/gate0_test.trb @@ -0,0 +1,184 @@ +import { validate_and_lower } from gate0 +import { + NativeBlock, + NativeFunction, + NativeInstruction, + NativeModule, + NativeSource, + NativeValueType, + verify_native_mir, +} from native_mir +import { SourceOrigin } from snapshot +import { read_text } from trb/std/filesystem +import { environment } from trb/std/process +import { Result } from trb/std/result +import { describe, expect, test } from trb/std/test + +def _valid_snapshot(): String + return "{\"format\":\"type-rb-bootstrap-snapshot\",\"version\":1,\"module\":\"example/main\",\"sources\":[{\"id\":\"main\",\"path\":\"src/main.trb\"}],\"functions\":[{\"id\":\"example/main#main\",\"name\":\"main\",\"result\":\"Integer\",\"entry\":\"entry\",\"origin\":{\"source\":\"main\",\"startLine\":1,\"startColumn\":1,\"endLine\":5,\"endColumn\":4},\"blocks\":[{\"id\":\"entry\",\"origin\":{\"source\":\"main\",\"startLine\":2,\"startColumn\":2,\"endLine\":4,\"endColumn\":15},\"instructions\":[{\"op\":\"integer_literal\",\"result\":\"v0\",\"value\":40,\"origin\":{\"source\":\"main\",\"startLine\":2,\"startColumn\":8,\"endLine\":2,\"endColumn\":9}},{\"op\":\"integer_literal\",\"result\":\"v1\",\"value\":2,\"origin\":{\"source\":\"main\",\"startLine\":3,\"startColumn\":8,\"endLine\":3,\"endColumn\":8}},{\"op\":\"integer_add_checked\",\"result\":\"v2\",\"left\":\"v0\",\"right\":\"v1\",\"origin\":{\"source\":\"main\",\"startLine\":4,\"startColumn\":9,\"endLine\":4,\"endColumn\":13}},{\"op\":\"return\",\"value\":\"v2\",\"origin\":{\"source\":\"main\",\"startLine\":4,\"startColumn\":2,\"endLine\":4,\"endColumn\":13}}]}]}]}" +end + +def _expect_boundary_error(source: String, code: String, path: String) + case validate_and_lower(source) + when Result::Ok(_native_module) + expect(false).to_be_true() + when Result::Err(diagnostic) + expect(diagnostic.code).to_equal(code) + expect(diagnostic.path).to_equal(path) + end + return +end + +def _read_fixture(path: String): String + case read_text(path) + when Result::Ok(source) + return source + when Result::Err(_error) + return "" + end +end + +describe("Gate 0 boundary") do + test("strictly decodes, lowers, and preserves source origins") do + case validate_and_lower(_valid_snapshot()) + when Result::Ok(native_module) + expect(native_module.id).to_equal("example/main") + expect(native_module.sources[0].id).to_equal("main") + expect(native_module.sources[0].path).to_equal("src/main.trb") + expect(native_module.functions.size()).to_equal(1) + function := native_module.functions[0] + expect(function.origin.start_line).to_equal(1) + expect(function.blocks.size()).to_equal(1) + expect(function.blocks[0].origin.start_line).to_equal(2) + instruction := function.blocks[0].instructions[2] + case instruction + when NativeInstruction::AddIntegerChecked(result, left, right, origin) + expect(result).to_equal("v2") + expect(left).to_equal("v0") + expect(right).to_equal("v1") + expect(origin.source_id).to_equal("main") + expect(origin.start_line).to_equal(4) + else + expect(false).to_be_true() + end + when Result::Err(_diagnostic) + expect(false).to_be_true() + end + end + + test("rejects malformed JSON deterministically") do + _expect_boundary_error("{", "TRBN0001", "") + end + + test("rejects unknown fields deterministically") do + _expect_boundary_error( + "{\"format\":\"type-rb-bootstrap-snapshot\",\"version\":1,\"module\":\"example/main\",\"sources\":[],\"functions\":[],\"unexpected\":true}", + "TRBN0002", + "/unexpected", + ) + end + + test("rejects unsupported versions deterministically") do + _expect_boundary_error( + "{\"format\":\"type-rb-bootstrap-snapshot\",\"version\":2,\"module\":\"example/main\",\"sources\":[],\"functions\":[]}", + "TRBN0003", + "/version", + ) + end + + test("rejects missing required fields deterministically") do + _expect_boundary_error("{}", "TRBN0002", "/format") + end + + test("rejects unsupported operations deterministically") do + _expect_boundary_error( + _valid_snapshot().replace_all("integer_literal", "integer_multiply"), + "TRBN0003", + "/functions/0/blocks/0/instructions/0/op", + ) + end + + test("rejects undefined snapshot values") do + source := "{\"format\":\"type-rb-bootstrap-snapshot\",\"version\":1,\"module\":\"example/main\",\"sources\":[{\"id\":\"main\",\"path\":\"src/main.trb\"}],\"functions\":[{\"id\":\"example/main#main\",\"name\":\"main\",\"result\":\"Integer\",\"entry\":\"entry\",\"origin\":{\"source\":\"main\",\"startLine\":1,\"startColumn\":1,\"endLine\":1,\"endColumn\":4},\"blocks\":[{\"id\":\"entry\",\"origin\":{\"source\":\"main\",\"startLine\":1,\"startColumn\":1,\"endLine\":1,\"endColumn\":4},\"instructions\":[{\"op\":\"return\",\"value\":\"missing\",\"origin\":{\"source\":\"main\",\"startLine\":1,\"startColumn\":1,\"endLine\":1,\"endColumn\":4}}]}]}]}" + _expect_boundary_error(source, "TRBN0004", "/functions/0/blocks/0/instructions/0/value") + end + + test("verifies Native MIR independently from snapshot decoding") do + origin := SourceOrigin.new( + source_id: "main", + start_line: 1, + start_column: 1, + end_line: 1, + end_column: 4, + ) + invalid := NativeModule.new( + id: "example/main", + sources: [NativeSource.new(id: "main", path: "src/main.trb")], + functions: [ + NativeFunction.new( + id: "example/main#main", + name: "main", + result_type: NativeValueType::Integer, + entry: "entry", + origin: origin, + blocks: [ + NativeBlock.new( + id: "entry", + origin: origin, + instructions: [NativeInstruction::Return("missing", origin)], + ), + ], + ), + ], + ) + case verify_native_mir(invalid) + when Result::Ok(_valid) + expect(false).to_be_true() + when Result::Err(diagnostic) + expect(diagnostic.code).to_equal("TRBN0004") + expect(diagnostic.path).to_equal("/functions/0/blocks/0/instructions/0/value") + end + end + + test("validates the committed fixture corpus") do + root := environment("TYPE_RB_NATIVE_ROOT") + if root == nil + expect(false).to_be_true() + else + valid_source := _read_fixture(root + "/fixtures/gate0/valid/integer-add.json") + expect(valid_source.empty?()).to_be_false() + case validate_and_lower(valid_source) + when Result::Ok(_native_module) + expect(true).to_be_true() + when Result::Err(_diagnostic) + expect(false).to_be_true() + end + + _expect_boundary_error( + _read_fixture(root + "/fixtures/gate0/invalid/malformed.json"), + "TRBN0001", + "", + ) + _expect_boundary_error( + _read_fixture(root + "/fixtures/gate0/invalid/unknown-field.json"), + "TRBN0002", + "/unexpected", + ) + _expect_boundary_error( + _read_fixture(root + "/fixtures/gate0/invalid/unsupported-version.json"), + "TRBN0003", + "/version", + ) + _expect_boundary_error( + _read_fixture(root + "/fixtures/gate0/invalid/unknown-source.json"), + "TRBN0004", + "/functions/0/origin/source", + ) + _expect_boundary_error( + _read_fixture(root + "/fixtures/gate0/invalid/undefined-value.json"), + "TRBN0004", + "/functions/0/blocks/0/instructions/0/value", + ) + end + end +end diff --git a/src/native_mir.trb b/src/native_mir.trb new file mode 100644 index 00000000..cd475401 --- /dev/null +++ b/src/native_mir.trb @@ -0,0 +1,256 @@ +import { Diagnostic, boundary_diagnostic } from diagnostic +import { + BootstrapSnapshot, + SnapshotInstruction, + SourceOrigin, +} from snapshot +import { Result } from trb/std/result + +enum NativeValueType + Integer +end + +enum NativeInstruction + ConstInteger(result: String, value: Integer, origin: SourceOrigin) + AddIntegerChecked(result: String, left: String, right: String, origin: SourceOrigin) + Return(value: String, origin: SourceOrigin) +end + +record NativeBlock + id: String + origin: SourceOrigin + instructions: Array +end + +record NativeSource + id: String + path: String +end + +record NativeFunction + id: String + name: String + result_type: NativeValueType + entry: String + origin: SourceOrigin + blocks: Array +end + +record NativeModule + id: String + sources: Array + functions: Array +end + +def lower_snapshot(snapshot: BootstrapSnapshot): NativeModule + sources := snapshot.sources.map do |source| + NativeSource.new(id: source.id, path: source.path) + end + functions := snapshot.functions.map do |function| + blocks := function.blocks.map do |block| + instructions := block.instructions.map do |instruction| + case instruction + when SnapshotInstruction::IntegerLiteral(result, value, origin) + NativeInstruction::ConstInteger(result, value, origin) + when SnapshotInstruction::IntegerAddChecked(result, left, right, origin) + NativeInstruction::AddIntegerChecked(result, left, right, origin) + when SnapshotInstruction::Return(value, origin) + NativeInstruction::Return(value, origin) + end + end + NativeBlock.new(id: block.id, origin: block.origin, instructions: instructions) + end + NativeFunction.new( + id: function.id, + name: function.name, + result_type: NativeValueType::Integer, + entry: function.entry, + origin: function.origin, + blocks: blocks, + ) + end + return NativeModule.new(id: snapshot.module_id, sources: sources, functions: functions) +end + +def verify_native_mir(native_module: NativeModule): Result + if native_module.sources.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", "/sources", "Native MIR must declare at least one source") + ) + 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", "Native MIR source identifier is duplicated") + ) + end + source_ids.push(source.id) + source_index += 1 + end + if native_module.functions.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", "/functions", "Native MIR must contain at least one function") + ) + 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", "Native MIR function identifier is duplicated") + ) + end + function_ids[function.id] = true + try _verify_native_origin(function.origin, source_ids, path + "/origin") + try _verify_native_function(function, source_ids, path) + function_index += 1 + end + return Result::Ok(true) +end + +def _verify_native_function( + function: NativeFunction, + source_ids: Array, + path: String, + ): Result + if function.blocks.size() != 1 + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/blocks", "Gate 0 Native MIR requires exactly one entry block") + ) + end + mut block_ids: Hash := {} + mut values: 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", "Native MIR block identifier is duplicated") + ) + end + block_ids[block.id] = true + if block.id == function.entry + entry_found = true + end + try _verify_native_origin(block.origin, source_ids, block_path + "/origin") + if block.instructions.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", block_path + "/instructions", "Native MIR block has no terminator") + ) + end + mut instruction_index := 0 + while instruction_index < block.instructions.size() + instruction := block.instructions[instruction_index] + instruction_path := block_path + "/instructions/" + instruction_index.to_s() + try _verify_native_instruction( + instruction, + values, + source_ids, + instruction_path, + instruction_index == block.instructions.size() - 1, + ) + instruction_index += 1 + end + block_index += 1 + end + if !entry_found + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/entry", "Native MIR entry block does not exist") + ) + end + return Result::Ok(true) +end + +def _verify_native_instruction( + instruction: NativeInstruction, + mut values: Hash, + source_ids: Array, + path: String, + is_last: Boolean, + ): Result + case instruction + when NativeInstruction::ConstInteger(result, _value, origin) + try _verify_native_origin(origin, source_ids, path + "/origin") + try _native_define(values, result, path + "/result") + when NativeInstruction::AddIntegerChecked(result, left, right, origin) + try _verify_native_origin(origin, source_ids, path + "/origin") + try _native_require(values, left, path + "/left") + try _native_require(values, right, path + "/right") + try _native_define(values, result, path + "/result") + when NativeInstruction::Return(value, origin) + try _verify_native_origin(origin, source_ids, path + "/origin") + try _native_require(values, value, path + "/value") + if !is_last + return Result::Err( + boundary_diagnostic("TRBN0004", path, "Native MIR terminator is not final") + ) + end + end + if is_last + case instruction + when NativeInstruction::Return(_value, _origin) + else + return Result::Err( + boundary_diagnostic("TRBN0004", path, "Native MIR block does not end in return") + ) + end + end + return Result::Ok(true) +end + +def _verify_native_origin( + origin: SourceOrigin, + source_ids: Array, + path: String, + ): Result + if !source_ids.include?(origin.source_id) + return Result::Err( + boundary_diagnostic("TRBN0004", path, "Native MIR 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, "Native MIR origin is invalid") + ) + end + if origin.end_line == origin.start_line && origin.end_column < origin.start_column + return Result::Err( + boundary_diagnostic("TRBN0004", path, "Native MIR origin is invalid") + ) + end + return Result::Ok(true) +end + +def _native_define( + mut values: Hash, + value: String, + path: String, + ): Result + if values.key?(value) + return Result::Err( + boundary_diagnostic("TRBN0004", path, "Native MIR value is defined more than once") + ) + end + values[value] = true + return Result::Ok(true) +end + +def _native_require( + values: Hash, + value: String, + path: String, + ): Result + if !values.key?(value) + return Result::Err( + boundary_diagnostic("TRBN0004", path, "Native MIR operand is undefined") + ) + end + return Result::Ok(true) +end diff --git a/src/snapshot.trb b/src/snapshot.trb new file mode 100644 index 00000000..57bc746d --- /dev/null +++ b/src/snapshot.trb @@ -0,0 +1,545 @@ +import { Diagnostic, boundary_diagnostic } from diagnostic +import { JsonValue, parse } from trb/std/json +import { Result } from trb/std/result + +MAX_INPUT_BYTES := 1048576 +MAX_IDENTIFIER_SIZE := 256 +MAX_SOURCE_PATH_SIZE := 4096 +MAX_SOURCES := 128 +MAX_FUNCTIONS := 256 +MAX_BLOCKS := 256 +MAX_INSTRUCTIONS := 4096 + +record SourceOrigin + source_id: String + start_line: Integer + start_column: Integer + end_line: Integer + end_column: Integer +end + +record SnapshotSource + id: String + path: String +end + +enum SnapshotValueType + Integer +end + +enum SnapshotInstruction + IntegerLiteral(result: String, value: Integer, origin: SourceOrigin) + IntegerAddChecked(result: String, left: String, right: String, origin: SourceOrigin) + Return(value: String, origin: SourceOrigin) +end + +record SnapshotBlock + id: String + origin: SourceOrigin + instructions: Array +end + +record SnapshotFunction + id: String + name: String + result_type: SnapshotValueType + entry: String + origin: SourceOrigin + blocks: Array +end + +record BootstrapSnapshot + module_id: String + sources: Array + functions: Array +end + +def decode_snapshot(source: String): Result + if source.to_bytes().size() > MAX_INPUT_BYTES + return Result::Err( + boundary_diagnostic("TRBN0005", "", "snapshot exceeds the 1 MiB input limit") + ) + end + parsed := parse(source) + root := case parsed + 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", "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 != 1 + return Result::Err( + boundary_diagnostic("TRBN0003", "/version", "unsupported snapshot version") + ) + end + module_id := try _required_string(fields, "module", "", true) + source_values := try _required_array(fields, "sources", "") + if source_values.size() > MAX_SOURCES + return Result::Err( + boundary_diagnostic("TRBN0005", "/sources", "source count exceeds the Gate 0 limit") + ) + end + mut sources: Array := [] + mut source_index := 0 + while source_index < source_values.size() + sources.push(try _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() > MAX_FUNCTIONS + return Result::Err( + boundary_diagnostic("TRBN0005", "/functions", "function count exceeds the Gate 0 limit") + ) + end + mut functions: Array := [] + mut function_index := 0 + while function_index < function_values.size() + functions.push(try _decode_function(function_values[function_index], "/functions/" + function_index.to_s())) + function_index += 1 + end + snapshot := BootstrapSnapshot.new(module_id: module_id, sources: sources, functions: functions) + try verify_snapshot(snapshot) + return Result::Ok(snapshot) +end + +def verify_snapshot(snapshot: BootstrapSnapshot): Result + if snapshot.sources.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", "/sources", "snapshot must declare at least one source") + ) + end + if snapshot.functions.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", "/functions", "snapshot must contain at least one function") + ) + end + mut source_ids: Hash := {} + mut source_index := 0 + while source_index < snapshot.sources.size() + source := snapshot.sources[source_index] + if source_ids.key?(source.id) + return Result::Err( + boundary_diagnostic("TRBN0004", "/sources/" + source_index.to_s() + "/id", "duplicate source identifier") + ) + end + source_ids[source.id] = true + source_index += 1 + end + mut function_ids: Hash := {} + mut function_index := 0 + while function_index < snapshot.functions.size() + function := snapshot.functions[function_index] + function_path := "/functions/" + function_index.to_s() + if function_ids.key?(function.id) + return Result::Err( + boundary_diagnostic("TRBN0004", function_path + "/id", "duplicate function identifier") + ) + end + function_ids[function.id] = true + try _verify_origin(function.origin, source_ids, function_path + "/origin") + try _verify_snapshot_function(function, source_ids, function_path) + function_index += 1 + end + return Result::Ok(true) +end + +def _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() > MAX_SOURCE_PATH_SIZE + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/path", "source path exceeds the Gate 0 limit") + ) + end + return Result::Ok( + SnapshotSource.new( + id: try _required_string(fields, "id", path, true), + path: source_path, + ) + ) +end + +def _decode_function(value: JsonValue, path: String): Result + fields := try _strict_object(value, path, ["id", "name", "result", "entry", "origin", "blocks"]) + result_name := try _required_string(fields, "result", path, false) + if result_name != "Integer" + return Result::Err( + boundary_diagnostic("TRBN0003", path + "/result", "unsupported Gate 0 result type") + ) + end + block_values := try _required_array(fields, "blocks", path) + if block_values.size() > MAX_BLOCKS + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/blocks", "block count exceeds the Gate 0 limit") + ) + end + mut blocks: Array := [] + mut index := 0 + while index < block_values.size() + blocks.push(try _decode_block(block_values[index], path + "/blocks/" + index.to_s())) + index += 1 + end + return Result::Ok( + SnapshotFunction.new( + id: try _required_string(fields, "id", path, true), + name: try _required_string(fields, "name", path, true), + result_type: SnapshotValueType::Integer, + entry: try _required_string(fields, "entry", path, true), + origin: try _decode_origin(try _required_field(fields, "origin", path), path + "/origin"), + blocks: blocks, + ) + ) +end + +def _decode_block(value: JsonValue, path: String): Result + fields := try _strict_object(value, path, ["id", "origin", "instructions"]) + instruction_values := try _required_array(fields, "instructions", path) + if instruction_values.size() > MAX_INSTRUCTIONS + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/instructions", "instruction count exceeds the Gate 0 limit") + ) + end + mut instructions: Array := [] + mut index := 0 + while index < instruction_values.size() + instructions.push(try _decode_instruction(instruction_values[index], path + "/instructions/" + index.to_s())) + index += 1 + end + return Result::Ok( + SnapshotBlock.new( + id: try _required_string(fields, "id", path, true), + origin: try _decode_origin(try _required_field(fields, "origin", path), path + "/origin"), + instructions: instructions, + ) + ) +end + +def _decode_instruction(value: JsonValue, path: String): Result + base_fields := try _object(value, path) + op := try _required_string(base_fields, "op", path, false) + if op == "integer_literal" + fields := try _strict_object(value, path, ["op", "result", "value", "origin"]) + return Result::Ok( + SnapshotInstruction::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 == "integer_add_checked" + fields := try _strict_object(value, path, ["op", "result", "left", "right", "origin"]) + return Result::Ok( + SnapshotInstruction::IntegerAddChecked( + try _required_string(fields, "result", path, true), + 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 == "return" + fields := try _strict_object(value, path, ["op", "value", "origin"]) + return Result::Ok( + SnapshotInstruction::Return( + try _required_string(fields, "value", path, true), + try _decode_origin(try _required_field(fields, "origin", path), path + "/origin"), + ) + ) + end + return Result::Err( + boundary_diagnostic("TRBN0003", path + "/op", "unsupported Gate 0 operation") + ) +end + +def _decode_origin(value: JsonValue, path: String): Result + fields := try _strict_object( + value, + path, + ["source", "startLine", "startColumn", "endLine", "endColumn"], + ) + return Result::Ok( + SourceOrigin.new( + source_id: try _required_string(fields, "source", path, true), + start_line: try _required_integer(fields, "startLine", path), + start_column: try _required_integer(fields, "startColumn", path), + end_line: try _required_integer(fields, "endLine", path), + end_column: try _required_integer(fields, "endColumn", path), + ) + ) +end + +def _verify_snapshot_function( + function: SnapshotFunction, + source_ids: Hash, + path: String, + ): Result + if function.blocks.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/blocks", "function must contain at least one block") + ) + end + if function.blocks.size() != 1 + return Result::Err( + boundary_diagnostic("TRBN0003", path + "/blocks", "Gate 0 supports exactly one entry block") + ) + end + mut block_ids: Hash := {} + mut entry_found := false + mut values: Hash := {} + 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", "duplicate block identifier") + ) + end + block_ids[block.id] = true + if block.id == function.entry + entry_found = true + end + try _verify_origin(block.origin, source_ids, block_path + "/origin") + if block.instructions.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", block_path + "/instructions", "block must contain a terminator") + ) + end + mut instruction_index := 0 + while instruction_index < block.instructions.size() + instruction_path := block_path + "/instructions/" + instruction_index.to_s() + instruction := block.instructions[instruction_index] + try _verify_snapshot_instruction( + instruction, + values, + source_ids, + instruction_path, + instruction_index == block.instructions.size() - 1, + ) + instruction_index += 1 + end + block_index += 1 + end + if !entry_found + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/entry", "entry block does not exist") + ) + end + return Result::Ok(true) +end + +def _verify_snapshot_instruction( + instruction: SnapshotInstruction, + mut values: Hash, + source_ids: Hash, + path: String, + is_last: Boolean, + ): Result + case instruction + when SnapshotInstruction::IntegerLiteral(result, _value, origin) + try _verify_origin(origin, source_ids, path + "/origin") + try _define_value(values, result, path + "/result") + when SnapshotInstruction::IntegerAddChecked(result, left, right, origin) + try _verify_origin(origin, source_ids, path + "/origin") + try _require_value(values, left, path + "/left") + try _require_value(values, right, path + "/right") + try _define_value(values, result, path + "/result") + when SnapshotInstruction::Return(value, origin) + try _verify_origin(origin, source_ids, path + "/origin") + try _require_value(values, value, path + "/value") + if !is_last + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/op", "terminator must be the final instruction") + ) + end + end + if is_last + case instruction + when SnapshotInstruction::Return(_value, _origin) + else + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/op", "block must end with a return terminator") + ) + end + end + return Result::Ok(true) +end + +def _verify_origin( + origin: SourceOrigin, + source_ids: Hash, + path: String, + ): Result + if !source_ids.key?(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 + +def _define_value( + mut values: Hash, + value: String, + path: String, + ): Result + if values.key?(value) + return Result::Err( + boundary_diagnostic("TRBN0004", path, "value identifier is defined more than once") + ) + end + values[value] = true + return Result::Ok(true) +end + +def _require_value( + values: Hash, + value: String, + path: String, + ): Result + if !values.key?(value) + return Result::Err( + boundary_diagnostic("TRBN0004", path, "operand references an undefined value") + ) + end + return Result::Ok(true) +end + +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() > MAX_IDENTIFIER_SIZE + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/" + name, "identifier exceeds the Gate 0 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/trbconfig.jsonc b/trbconfig.jsonc new file mode 100644 index 00000000..b5a20963 --- /dev/null +++ b/trbconfig.jsonc @@ -0,0 +1,8 @@ +{ + "name": "type-rb-native", + "mode": "go", + "sourceDir": "src", + "go": { + "module": "github.com/type-rb/type-rb-native/bootstrap" + } +}