From 7a039286a75760d1ef36330b1ec34980fc81cbe2 Mon Sep 17 00:00:00 2001 From: hiroki23 <33798+hiroki23@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:43:33 +0900 Subject: [PATCH] feat: establish Gate 2 aggregate layouts --- README.md | 36 +- docs/architecture.md | 24 +- .../0004-sustained-native-development.md | 65 +++ docs/experiment-plan.md | 111 +++-- docs/gate-2-aggregates.md | 59 +++ src/gate2_layout.trb | 441 ++++++++++++++++++ src/gate2_layout_test.trb | 233 +++++++++ 7 files changed, 896 insertions(+), 73 deletions(-) create mode 100644 docs/decisions/0004-sustained-native-development.md create mode 100644 docs/gate-2-aggregates.md create mode 100644 src/gate2_layout.trb create mode 100644 src/gate2_layout_test.trb diff --git a/README.md b/README.md index b75a1314..6dcd92d5 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,15 @@ > TypeRB backend, runtime, or release target. Everything in this repository may > change incompatibly or be removed without notice. -TypeRB Native explores whether a TypeRB-specific native compiler and runtime -can improve end-to-end build time, generated-program performance, and deployed +TypeRB Native develops a TypeRB-specific native compiler and runtime intended +to 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. 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 repository remains experimental while that implementation is incomplete; +the gates are engineering checkpoints that keep correctness and whole-toolchain +performance visible as the implementation grows. The [TypeRB repository](https://github.com/type-rb/type-rb) remains the source of truth for the language specification, reference compiler, supported @@ -27,8 +30,9 @@ semantics; it does not define a native-only TypeRB dialect. - Compare multiple machine-code strategies behind the same MIR and semantics. - Measure complete toolchains, including code generation, linking, runtime, sidecars, and distribution size. -- Keep the experiment removable if it does not improve the practical tradeoff - offered by the Go backend. +- Preserve a credible path to a native implementation that is at least as + practical as the Go backend, and use measured regressions to direct + optimization work rather than treating early gates as disposable demos. ## Current status @@ -46,11 +50,13 @@ binary64 Float, static UTF-8 output, and deterministic arithmetic failure. On the recorded Apple M2 Pro run, native warm build time improved by 30.5% to 36.3%, stripped executable size improved by 96.85%, and the worst runtime result was a 16.0% regression, within the pre-registered 25% bound. See the -[Gate 1 result](results/2026-08-28-gate1-qbe-darwin-arm64/README.md). Development -stops before Gate 2 for maintainer review. The result does not select QBE for -production or measure the final self-hosted compiler. The current path provides -no production runtime, stable ABI, stable artifact format, or compatibility -guarantee. Records and tagged values remain deferred to Gate 2. +[Gate 1 result](results/2026-08-28-gate1-qbe-darwin-arm64/README.md). Gate 2 is +active. Its first checkpoint adds heap-free, static-layout records and tagged +values, including the representation needed for payload enums and `Result`, +before dynamic strings, arrays, closures, or a memory manager are introduced. +The Gate 1 result does not select QBE for production or measure the final +self-hosted compiler. The current path provides no production runtime, stable +ABI, stable artifact format, or compatibility guarantee. ## Intended boundary @@ -114,8 +120,8 @@ gate. More than one implementation may remain only when distinct development, release, or target use cases show a durable benefit that justifies the maintenance cost. -See the [experiment plan](docs/experiment-plan.md) for correctness gates, -measurement rules, and abandonment criteria. +See the [development and validation plan](docs/experiment-plan.md) for +correctness gates, measurement rules, and backend selection criteria. ## Non-goals @@ -124,8 +130,8 @@ The initial gates do not attempt to: - port the compiler to Rust, Zig, or another host implementation language; - 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; +- implement the full TypeRB frontend before the shared native value model and + runtime boundaries are concrete enough to support it; - 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 @@ -142,12 +148,14 @@ repository. ## Documentation - [Architecture](docs/architecture.md) -- [Experiment plan](docs/experiment-plan.md) +- [Development and validation plan](docs/experiment-plan.md) - [Gate 1 QBE vertical slice](docs/gate-1-qbe.md) - [Gate 1 QBE Darwin arm64 result](results/2026-08-28-gate1-qbe-darwin-arm64/README.md) +- [Gate 2 heap-free aggregate value model](docs/gate-2-aggregates.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) +- [Decision 0004: Sustained native implementation and staged Gate 2](docs/decisions/0004-sustained-native-development.md) - [Contributing](CONTRIBUTING.md) - [Security](SECURITY.md) diff --git a/docs/architecture.md b/docs/architecture.md index daf6c718..5a9c3792 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,11 +2,12 @@ ## Purpose -TypeRB Native investigates a TypeRB-specific native compiler and runtime while -keeping the supported language and reference compiler independent. The initial -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. +TypeRB Native develops a TypeRB-specific native compiler and runtime while +keeping the supported language and reference compiler independent. Its +engineering objective is a self-hosted implementation that removes Go from the +ordinary bootstrap and application-build path while matching or improving the +practical tradeoff among build time, execution performance, and deployed binary +size after all required tooling is counted. 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 @@ -184,8 +185,9 @@ full-language target would require accepted solutions for: The initial runtime remains deliberately smaller: static data, scalar values, simple aggregate layout, observable output, and deterministic process failure. -Heap management and concurrency begin only after the first backend comparison -passes its gates. +Gate 2 completes the heap-free aggregate layer before heap ownership and memory +management are added. This separation keeps record and tagged-value semantics +independent of the later allocation strategy. Runtime semantics are shared across backend candidates. Target-specific ABI profiles and small shims may differ, but the runtime must not be independently @@ -217,6 +219,8 @@ runtime and package boundaries, reproducible builds, primary-platform support, 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 -be retained without preserving a dormant compatibility surface. +The bootstrap bridge remains removable because the independent frontend will +eventually replace it, not because removal is the default project outcome. +Gates expose correctness, performance, and maintenance problems early enough to +improve the shared MIR, runtime, backend, or build pipeline before those choices +become public contracts. diff --git a/docs/decisions/0004-sustained-native-development.md b/docs/decisions/0004-sustained-native-development.md new file mode 100644 index 00000000..f766a4e9 --- /dev/null +++ b/docs/decisions/0004-sustained-native-development.md @@ -0,0 +1,65 @@ +# 0004: Sustained Native Development and a Staged Gate 2 + +## Status + +Accepted. + +## Context + +The initial plan described the repository primarily as a sequence of bounded +experiments whose native path could be abandoned after an early performance +miss. Gate 1 instead established a useful executable baseline: the QBE path was +correct on its scalar corpus, substantially reduced build time and executable +size, and kept runtime within the registered bound. + +The project's intent is now stronger. It should make a sustained engineering +effort toward a better native implementation, ultimately self-hosted in TypeRB, +rather than treating each incomplete stage as a reason to stop. Measurements +remain essential, but their main purpose is to locate work and guide backend or +runtime choices. + +Gate 2 previously grouped static aggregates with dynamic strings, arrays, +closures, allocation, and failure behavior. Those features answer different +architectural questions. Implementing them together would obscure whether a +problem belongs to value layout or memory ownership. + +## Decision + +TypeRB Native is a sustained implementation effort with experimental stability. +Gates are correctness, architecture, and whole-toolchain performance +checkpoints. Missing a target keeps the gate open for diagnosis and improvement +unless the evidence reveals a fundamental conflict with TypeRB semantics, +safety, or sustainable self-hosting. + +Gate 2 is restricted to heap-free aggregate values: + +- nominal, immutable records; +- payloadless and payload-bearing enum variants; +- the static tagged representation needed for `Result`; +- aggregate construction, projection, dispatch, direct calls, block parameters, + and returns; and +- monomorphized layouts composed only from Gate 1 scalars and other Gate 2 + aggregates. + +Dynamic strings, collections, closures, escaping values, allocation, and memory +management move to Gate 3. This is a staging decision only; those capabilities +remain required for the eventual native compiler and runtime. + +The bootstrap snapshot, Native MIR, `darwin-arm64-v0` ABI profile, and QBE +adapter remain internal and disposable. Their instability lets the +implementation improve without creating a native-only TypeRB dialect or a +public compiler API. + +## Consequences + +- Gate 2 can establish deterministic layout and tagged-value semantics without + prematurely selecting ownership or garbage collection. +- Records, payload enums, and `Result` use the existing TypeRB language contract; + this decision adds no syntax or user-visible semantic variation. +- Performance misses produce optimization work or a backend decision rather + than automatic project abandonment. +- The Go reference compiler remains an early bootstrap producer and semantic + oracle until the later TypeRB-authored frontend and reproducible self-hosting + gates replace it in the ordinary path. +- Experimental adapters can still be removed when they no longer have a + distinct measured role. diff --git a/docs/experiment-plan.md b/docs/experiment-plan.md index 3b467a68..acc4c291 100644 --- a/docs/experiment-plan.md +++ b/docs/experiment-plan.md @@ -1,17 +1,18 @@ -# Experiment Plan +# Development and Validation Plan -## Research question +## Engineering objective -Can a TypeRB-specific native AOT pipeline improve at least one of these primary -outcomes without unacceptable regressions in the others? +Build a TypeRB-specific native AOT pipeline that ultimately matches or improves +the optimized Go backend across these primary outcomes: 1. End-to-end application build time. 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? +The compiler and runtime are implemented in TypeRB, reproduce themselves, and +must retain competitive build time and generated-code behavior once the +complete self-hosted toolchain is measured. Early gates establish this outcome +incrementally; they are not a sequence of throwaway demonstrations. Secondary outcomes include compiler and runtime peak memory, startup latency, toolchain distribution size, portability, diagnostics, correctness risk, and @@ -30,19 +31,22 @@ intentionally unstripped, cold, or otherwise disadvantaged Go configuration. - Unsupported behavior fails explicitly. - Measurements include serialization, lowering, optimization, code generation, assembly, linking, runtime, and required external components. -- Thresholds and removal rules are recorded before reviewing a result. +- Quality and performance targets are recorded before reviewing a result. - Microbenchmarks diagnose a phase; representative programs determine viability. Before a gate begins, its issue must record metric-specific non-inferiority -bounds, a minimum meaningful primary-metric improvement, catastrophic-regression -limits, and a time or engineering-effort budget. These values cannot be revised -after results are reviewed merely to keep a candidate alive. +bounds, a minimum meaningful primary-metric improvement where the gate is +expected to provide one, and catastrophic-regression limits. A miss identifies +required engineering work or an architectural decision; it does not by itself +end the native implementation. Targets cannot be weakened after results are +reviewed merely to label a gate complete. ## Candidate sequence -The candidates are not implemented to production completeness in parallel. -They advance through small shared gates and can be removed early. +Backend candidates are not implemented to production completeness in parallel. +They advance through small shared gates, and only implementations with a clear +role continue to accumulate maintenance cost. 1. Use hand-authored bootstrap and MIR fixtures to validate the boundary. 2. Use QBE for the cheapest runtime and ABI feasibility check. @@ -110,24 +114,42 @@ 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 +### Gate 2: Heap-free aggregate 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 -- deterministic observable behavior, defined allocation failure, and - reproducible artifacts. +Scope: -Only candidates that pass Gate 1 correctness and comparability continue. +- nominal records with immutable, statically laid-out fields; +- payloadless and payload-bearing enum variants represented as tagged values; +- aggregate construction, field and payload projection, direct calls, returns, + block parameters, and exhaustive variant dispatch; +- monomorphized static layouts needed for records and `Result` whose + fields and payloads are themselves heap-free Gate 2 values; +- deterministic snapshots, MIR, QBE output, executables, diagnostics, and + layout computation; and +- the existing disposable `darwin-arm64-v0` profile and QBE 1.3 path. + +Dynamic strings, arrays, hashes, closures, captured environments, escaping +values, heap allocation, and a memory manager remain outside Gate 2. A static +string literal may still be used only for the existing observable-output +operation; it is not yet a first-class aggregate field or payload. + +Exit condition: the pinned reference compiler and native path produce identical +observable results for the registered source corpus covering records, nested +records, payload enums, exhaustive `case`, explicit `Result` handling, and +`try` propagation. Invalid snapshot and MIR inputs fail deterministically, +layout boundary tests pass, and repeated builds reproduce the same snapshot, +MIR, QBE IL, and executable. On the registered aggregate workloads, stripped +native executable size remains at least 30% below the stronger applicable Go +baseline, while warm end-to-end build time and runtime each remain within 25% +and no primary metric regresses by more than 2x. A target miss keeps Gate 2 open +for diagnosis and improvement. ### Gate 3: Runtime viability Scope may expand to: +- arrays, hashes, and dynamic strings; +- closures and captured environments; - memory-management strategy and cycles; - classes, interfaces, unions, and nullable values; - source-mapped failures and unwind behavior; @@ -236,15 +258,16 @@ Every published result should include: Store results under a date- and experiment-specific directory only after the first executable benchmark exists. Do not commit placeholder result files. -## Selection policy +## Backend selection policy -A candidate remains only when it: +A backend implementation remains active only when it: - passes the current correctness and reproducibility gates; - satisfies the pre-registered non-inferiority and catastrophic-regression limits; - achieves the pre-registered minimum improvement in at least one primary - outcome before product feasibility; + outcome before product feasibility, or has a concrete diagnostic role in + reaching that outcome; - has a credible path for the next required target and runtime feature; - does not impose disproportionate distribution, security, or maintenance costs. @@ -258,25 +281,15 @@ A secondary improvement may justify a bounded diagnostic experiment, but it does not pass product feasibility when all three primary outcomes miss their registered gates. -## Abandonment policy - -Archive or remove the native path if time-boxed milestones show that: - -- no candidate improves the practical tradeoff over an optimized release - executable produced by the reference compiler's Go backend; -- a candidate exceeds its registered time or engineering-effort budget without - passing the current gate; -- gains disappear after linking, runtime, sidecars, and distribution are - counted; -- correctness requires a competing dialect or weaker semantics; -- the boundary repeatedly duplicates or leaks the reference frontend; -- source mapping, runtime safety, or package interoperability requires - backend-specific language APIs; -- a second primary target requires divergent language semantics, a separate - frontend or runtime, or disproportionate target-specific maintenance; or -- maintenance and security costs outweigh the demonstrated benefit. - -If abandoned, retain generally useful benchmark methodology, conformance tests, -and architectural findings. Remove experimental bootstrap surfaces that have no -remaining consumer rather than preserving compatibility for a failed -experiment. +## Reassessment policy + +A missed checkpoint triggers diagnosis of the MIR, runtime, backend, or build +pipeline and a recorded plan to close the gap. Backend adapters may be replaced +or removed when another implementation serves their role better. The native +implementation itself is reconsidered only when evidence exposes a fundamental +conflict with portable TypeRB semantics, safe implementation, or sustainable +self-hosting—not merely because an early implementation needs optimization. + +Temporary bootstrap surfaces still have no compatibility guarantee. Remove +them when the independent frontend replaces them, and retain generally useful +benchmark methodology, conformance tests, and architectural findings. diff --git a/docs/gate-2-aggregates.md b/docs/gate-2-aggregates.md new file mode 100644 index 00000000..4dd2bc69 --- /dev/null +++ b/docs/gate-2-aggregates.md @@ -0,0 +1,59 @@ +# Gate 2 Heap-Free Aggregate Value Model + +Gate 2 extends the verified QBE path from scalar SSA values to heap-free, +static-layout values. It implements existing TypeRB record, payload-enum, and +`Result` semantics without introducing a native-only language feature. + +## Checkpoint boundary + +The supported value graph is finite and known at compile time: + +- `Boolean`, portable `Integer`, and binary64 `Float`; +- nominal records whose fields are supported Gate 2 values; and +- nominal tagged values whose variant payload fields are supported Gate 2 + values. + +Recursive-by-value declarations, dynamic strings, arrays, hashes, closures, +escaping references, and heap allocation are rejected explicitly. Static UTF-8 +output remains available as an observable operation but is not a first-class +field or payload value. + +The source corpus must cover record construction and projection, nested +records, payloadless and payload-bearing variants, exhaustive variant dispatch, +explicit `Result` matching, `try` propagation, and aggregate values crossing +direct-call, return, and control-flow-block boundaries. + +## Representation direction + +Native MIR keeps nominal aggregate types and semantic construction/projection +operations. The `darwin-arm64-v0` lowering computes deterministic size, +alignment, field offsets, tag values, and payload offsets. QBE-specific memory +operations remain below that boundary. + +The first implementation uses caller-owned stack storage for non-escaping +aggregate values. Aggregate parameters are borrowed for the duration of a +direct call, and aggregate results use caller-provided result storage. The MIR +semantics remain value semantics even though the disposable target ABI passes +addresses internally. A later allocation strategy can therefore add escaping +storage without changing TypeRB record or enum behavior. + +## Exit evidence + +Gate 2 completes when: + +1. strict snapshot and MIR validation reject malformed types, layouts, + constructors, projections, tags, and control-flow edges deterministically; +2. layout tests cover alignment, padding, nesting, maximum configured size, and + distinct nominal types with identical shapes; +3. the registered TypeRB source corpus matches the optimized Go backend in + stdout, stderr, and exit status; +4. repeated builds reproduce snapshot, MIR, QBE IL, and executable artifacts; +5. stripped executable size improves by at least 30%, warm end-to-end build time + and runtime remain within 25%, and no primary metric regresses by more than + 2x against the stronger applicable optimized Go baseline; and +6. the result report counts frontend, snapshot, decode, emit, QBE, assembly, + linking, runtime, peak memory, and distribution costs under the shared + measurement policy. + +Missing an engineering target leaves Gate 2 open for diagnosis and improvement. +Gate 3 does not start until this checkpoint is reported. diff --git a/src/gate2_layout.trb b/src/gate2_layout.trb new file mode 100644 index 00000000..68e32253 --- /dev/null +++ b/src/gate2_layout.trb @@ -0,0 +1,441 @@ +import { Diagnostic, boundary_diagnostic } from diagnostic +import { ScalarType } from gate1_mir +import { Result } from trb/std/result + +GATE2_MAX_TYPE_DEFINITIONS := 1024 +GATE2_MAX_FIELDS := 256 +GATE2_MAX_VARIANTS := 1024 +GATE2_MAX_LAYOUT_BYTES := 1048576 + +enum Gate2ValueType + Scalar(value_type: ScalarType) + Aggregate(type_id: String) +end + +record Gate2Field + name: String + value_type: Gate2ValueType +end + +record Gate2Variant + name: String + fields: Array +end + +enum Gate2TypeDefinition + Record(type_id: String, fields: Array) + Tagged(type_id: String, variants: Array) +end + +enum Gate2LayoutKind + Record + Tagged +end + +record Gate2FieldLayout + name: String + value_type: Gate2ValueType + offset: Integer + size: Integer + alignment: Integer +end + +record Gate2VariantLayout + name: String + tag: Integer + fields: Array + payload_size: Integer + payload_alignment: Integer +end + +record Gate2Layout + type_id: String + kind: Gate2LayoutKind + size: Integer + alignment: Integer + payload_offset: Integer + fields: Array + variants: Array +end + +record Gate2SizeAlignment + size: Integer + alignment: Integer +end + +def gate2_value_types_equal(left: Gate2ValueType, right: Gate2ValueType): Boolean + case left + when Gate2ValueType::Scalar(left_scalar) + case right + when Gate2ValueType::Scalar(right_scalar) + return left_scalar == right_scalar + when Gate2ValueType::Aggregate(_right_id) + return false + end + when Gate2ValueType::Aggregate(left_id) + case right + when Gate2ValueType::Scalar(_right_scalar) + return false + when Gate2ValueType::Aggregate(right_id) + return left_id == right_id + end + end +end + +def verify_gate2_layouts(definitions: Array): Result + if definitions.size() > GATE2_MAX_TYPE_DEFINITIONS + return Result::Err( + boundary_diagnostic("TRBN0005", "/types", "type definition count exceeds the Gate 2 limit") + ) + end + mut type_ids: Hash := {} + mut index := 0 + while index < definitions.size() + type_id := _gate2_definition_id(definitions[index]) + if type_id.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", "/types/" + index.to_s() + "/id", "type identifier must not be empty") + ) + end + if type_ids.key?(type_id) + return Result::Err( + boundary_diagnostic("TRBN0004", "/types/" + index.to_s() + "/id", "type identifier is duplicated") + ) + end + type_ids[type_id] = true + index += 1 + end + index = 0 + while index < definitions.size() + try compute_gate2_layout(definitions, _gate2_definition_id(definitions[index])) + index += 1 + end + return Result::Ok(true) +end + +def compute_gate2_layout( + definitions: Array, + type_id: String, + ): Result + return _gate2_compute_layout(definitions, type_id, [], "/types/" + type_id) +end + +def _gate2_compute_layout( + definitions: Array, + type_id: String, + visiting: Array, + path: String, + ): Result + if visiting.include?(type_id) + return Result::Err( + boundary_diagnostic("TRBN0004", path, "recursive by-value aggregate layout is unsupported") + ) + end + definition := _gate2_definition(definitions, type_id) + if definition == nil + return Result::Err( + boundary_diagnostic("TRBN0004", path, "aggregate type does not exist") + ) + end + mut next_visiting: Array := [] + mut visiting_index := 0 + while visiting_index < visiting.size() + next_visiting.push(visiting[visiting_index]) + visiting_index += 1 + end + next_visiting.push(type_id) + case definition + when Gate2TypeDefinition::Record(_id, fields) + return _gate2_record_layout(definitions, type_id, fields, next_visiting, path) + when Gate2TypeDefinition::Tagged(_id, variants) + return _gate2_tagged_layout(definitions, type_id, variants, next_visiting, path) + end +end + +def _gate2_record_layout( + definitions: Array, + type_id: String, + fields: Array, + visiting: Array, + path: String, + ): Result + if fields.size() > GATE2_MAX_FIELDS + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/fields", "field count exceeds the Gate 2 limit") + ) + end + field_layouts := try _gate2_fields_layout(definitions, fields, visiting, path + "/fields", 0) + mut alignment := 1 + mut end_offset := 0 + mut index := 0 + while index < field_layouts.size() + field := field_layouts[index] + alignment = _gate2_max(alignment, field.alignment) + end_offset = field.offset + field.size + index += 1 + end + mut size := _gate2_align_up(end_offset, alignment) + if size == 0 + size = 1 + end + try _gate2_require_layout_size(size, path) + return Result::Ok( + Gate2Layout.new( + type_id: type_id, + kind: Gate2LayoutKind::Record, + size: size, + alignment: alignment, + payload_offset: 0, + fields: field_layouts, + variants: [], + ) + ) +end + +def _gate2_tagged_layout( + definitions: Array, + type_id: String, + variants: Array, + visiting: Array, + path: String, + ): Result + if variants.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", path + "/variants", "tagged type must declare at least one variant") + ) + end + if variants.size() > GATE2_MAX_VARIANTS + return Result::Err( + boundary_diagnostic("TRBN0005", path + "/variants", "variant count exceeds the Gate 2 limit") + ) + end + mut names: Hash := {} + mut payload_alignment := 1 + mut index := 0 + while index < variants.size() + variant := variants[index] + variant_path := path + "/variants/" + index.to_s() + try _gate2_require_name(names, variant.name, variant_path + "/name", "variant") + if variant.fields.size() > GATE2_MAX_FIELDS + return Result::Err( + boundary_diagnostic("TRBN0005", variant_path + "/fields", "field count exceeds the Gate 2 limit") + ) + end + probe := try _gate2_fields_layout(definitions, variant.fields, visiting, variant_path + "/fields", 0) + mut field_index := 0 + while field_index < probe.size() + payload_alignment = _gate2_max(payload_alignment, probe[field_index].alignment) + field_index += 1 + end + index += 1 + end + payload_offset := _gate2_align_up(4, payload_alignment) + mut variant_layouts: Array := [] + mut payload_size := 0 + index = 0 + while index < variants.size() + variant := variants[index] + variant_path := path + "/variants/" + index.to_s() + fields := try _gate2_fields_layout( + definitions, + variant.fields, + visiting, + variant_path + "/fields", + payload_offset, + ) + mut variant_end := payload_offset + mut variant_alignment := 1 + mut field_index := 0 + while field_index < fields.size() + field := fields[field_index] + variant_end = field.offset + field.size + variant_alignment = _gate2_max(variant_alignment, field.alignment) + field_index += 1 + end + current_payload_size := variant_end - payload_offset + payload_size = _gate2_max(payload_size, current_payload_size) + variant_layouts.push( + Gate2VariantLayout.new( + name: variant.name, + tag: index, + fields: fields, + payload_size: current_payload_size, + payload_alignment: variant_alignment, + ) + ) + index += 1 + end + alignment := _gate2_max(4, payload_alignment) + size := _gate2_align_up(payload_offset + payload_size, alignment) + try _gate2_require_layout_size(size, path) + return Result::Ok( + Gate2Layout.new( + type_id: type_id, + kind: Gate2LayoutKind::Tagged, + size: size, + alignment: alignment, + payload_offset: payload_offset, + fields: [], + variants: variant_layouts, + ) + ) +end + +def _gate2_fields_layout( + definitions: Array, + fields: Array, + visiting: Array, + path: String, + start_offset: Integer, + ): Result, Diagnostic> + mut names: Hash := {} + mut layouts: Array := [] + mut offset := start_offset + mut index := 0 + while index < fields.size() + field := fields[index] + field_path := path + "/" + index.to_s() + try _gate2_require_name(names, field.name, field_path + "/name", "field") + size_alignment := try _gate2_size_alignment(definitions, field.value_type, visiting, field_path + "/type") + offset = _gate2_align_up(offset, size_alignment.alignment) + if offset + size_alignment.size > GATE2_MAX_LAYOUT_BYTES + return Result, Diagnostic>::Err( + boundary_diagnostic("TRBN0005", field_path, "aggregate layout exceeds the Gate 2 byte limit") + ) + end + layouts.push( + Gate2FieldLayout.new( + name: field.name, + value_type: field.value_type, + offset: offset, + size: size_alignment.size, + alignment: size_alignment.alignment, + ) + ) + offset += size_alignment.size + index += 1 + end + return Result, Diagnostic>::Ok(layouts) +end + +def _gate2_size_alignment( + definitions: Array, + value_type: Gate2ValueType, + visiting: Array, + path: String, + ): Result + case value_type + when Gate2ValueType::Scalar(scalar) + if scalar == ScalarType::Boolean + return Result::Ok(Gate2SizeAlignment.new(size: 1, alignment: 1)) + end + if scalar == ScalarType::Integer || scalar == ScalarType::Float + return Result::Ok(Gate2SizeAlignment.new(size: 8, alignment: 8)) + end + return Result::Err( + boundary_diagnostic("TRBN0004", path, "Void is not a storable Gate 2 value") + ) + when Gate2ValueType::Aggregate(type_id) + layout := try _gate2_compute_layout(definitions, type_id, visiting, path) + return Result::Ok( + Gate2SizeAlignment.new(size: layout.size, alignment: layout.alignment) + ) + end +end + +def _gate2_require_name( + mut names: Hash, + name: String, + path: String, + kind: String, + ): Result + if name.empty?() + return Result::Err( + boundary_diagnostic("TRBN0004", path, kind + " name must not be empty") + ) + end + if names.key?(name) + return Result::Err( + boundary_diagnostic("TRBN0004", path, kind + " name is duplicated") + ) + end + names[name] = true + return Result::Ok(true) +end + +def _gate2_require_layout_size(size: Integer, path: String): Result + if size > GATE2_MAX_LAYOUT_BYTES + return Result::Err( + boundary_diagnostic("TRBN0005", path, "aggregate layout exceeds the Gate 2 byte limit") + ) + end + return Result::Ok(true) +end + +def _gate2_definition_id(definition: Gate2TypeDefinition): String + case definition + when Gate2TypeDefinition::Record(type_id, _fields) + return type_id + when Gate2TypeDefinition::Tagged(type_id, _variants) + return type_id + end +end + +def _gate2_definition( + definitions: Array, + type_id: String, + ): Gate2TypeDefinition? + mut index := 0 + while index < definitions.size() + definition := definitions[index] + if _gate2_definition_id(definition) == type_id + return definition + end + index += 1 + end + return nil +end + +def gate2_field_layout(layout: Gate2Layout, name: String): Gate2FieldLayout? + mut index := 0 + while index < layout.fields.size() + if layout.fields[index].name == name + return layout.fields[index] + end + index += 1 + end + return nil +end + +def gate2_variant_layout(layout: Gate2Layout, name: String): Gate2VariantLayout? + mut index := 0 + while index < layout.variants.size() + if layout.variants[index].name == name + return layout.variants[index] + end + index += 1 + end + return nil +end + +def gate2_variant_field_layout(variant: Gate2VariantLayout, name: String): Gate2FieldLayout? + mut index := 0 + while index < variant.fields.size() + if variant.fields[index].name == name + return variant.fields[index] + end + index += 1 + end + return nil +end + +def _gate2_align_up(value: Integer, alignment: Integer): Integer + return ((value + alignment - 1) / alignment) * alignment +end + +def _gate2_max(left: Integer, right: Integer): Integer + if left > right + return left + end + return right +end diff --git a/src/gate2_layout_test.trb b/src/gate2_layout_test.trb new file mode 100644 index 00000000..21246454 --- /dev/null +++ b/src/gate2_layout_test.trb @@ -0,0 +1,233 @@ +import { + Gate2Field, + Gate2LayoutKind, + Gate2TypeDefinition, + Gate2ValueType, + Gate2Variant, + compute_gate2_layout, + gate2_field_layout, + gate2_value_types_equal, + gate2_variant_field_layout, + gate2_variant_layout, + verify_gate2_layouts, +} from gate2_layout +import { ScalarType } from gate1_mir +import { Result } from trb/std/result +import { describe, expect, test } from trb/std/test + +def _gate2_scalar(value_type: ScalarType): Gate2ValueType + return Gate2ValueType::Scalar(value_type) +end + +def _gate2_field(name: String, value_type: Gate2ValueType): Gate2Field + return Gate2Field.new(name: name, value_type: value_type) +end + +def _gate2_layout_definitions(): Array + return [ + Gate2TypeDefinition::Record( + "example#Point", + [ + _gate2_field("visible", _gate2_scalar(ScalarType::Boolean)), + _gate2_field("x", _gate2_scalar(ScalarType::Integer)), + _gate2_field("y", _gate2_scalar(ScalarType::Float)), + ], + ), + Gate2TypeDefinition::Record( + "example#Envelope", + [ + _gate2_field("point", Gate2ValueType::Aggregate("example#Point")), + _gate2_field("selected", _gate2_scalar(ScalarType::Boolean)), + ], + ), + Gate2TypeDefinition::Tagged( + "example#Outcome", + [ + Gate2Variant.new(name: "None", fields: []), + Gate2Variant.new( + name: "Count", + fields: [_gate2_field("value", _gate2_scalar(ScalarType::Integer))], + ), + Gate2Variant.new( + name: "Located", + fields: [_gate2_field("point", Gate2ValueType::Aggregate("example#Point"))], + ), + ], + ), + ] +end + +describe("Gate 2 static aggregate layout") do + test("lays out scalar and nested record fields deterministically") do + definitions := _gate2_layout_definitions() + case compute_gate2_layout(definitions, "example#Point") + when Result::Ok(layout) + expect(layout.kind).to_equal(Gate2LayoutKind::Record) + expect(layout.size).to_equal(24) + expect(layout.alignment).to_equal(8) + expect(gate2_field_layout(layout, "visible").offset).to_equal(0) + expect(gate2_field_layout(layout, "x").offset).to_equal(8) + expect(gate2_field_layout(layout, "y").offset).to_equal(16) + when Result::Err(_diagnostic) + expect(false).to_be_true() + end + case compute_gate2_layout(definitions, "example#Envelope") + when Result::Ok(layout) + expect(layout.size).to_equal(32) + expect(layout.alignment).to_equal(8) + expect(gate2_field_layout(layout, "point").offset).to_equal(0) + expect(gate2_field_layout(layout, "selected").offset).to_equal(24) + when Result::Err(_diagnostic) + expect(false).to_be_true() + end + end + + test("lays out tags and maximum payload storage") do + case compute_gate2_layout(_gate2_layout_definitions(), "example#Outcome") + when Result::Ok(layout) + expect(layout.kind).to_equal(Gate2LayoutKind::Tagged) + expect(layout.payload_offset).to_equal(8) + expect(layout.size).to_equal(32) + expect(layout.alignment).to_equal(8) + none := gate2_variant_layout(layout, "None") + count := gate2_variant_layout(layout, "Count") + located := gate2_variant_layout(layout, "Located") + expect(none.tag).to_equal(0) + expect(none.payload_size).to_equal(0) + expect(count.tag).to_equal(1) + if count != nil + expect(gate2_variant_field_layout(count, "value").offset).to_equal(8) + end + expect(located.tag).to_equal(2) + if located != nil + expect(gate2_variant_field_layout(located, "point").size).to_equal(24) + end + when Result::Err(_diagnostic) + expect(false).to_be_true() + end + end + + test("retains nominal identity for identical shapes") do + left := Gate2ValueType::Aggregate("example#Left") + right := Gate2ValueType::Aggregate("example#Right") + expect(gate2_value_types_equal(left, left)).to_be_true() + expect(gate2_value_types_equal(left, right)).to_be_false() + expect( + compute_gate2_layout(_gate2_layout_definitions(), "example#Outcome") + ).to_equal(compute_gate2_layout(_gate2_layout_definitions(), "example#Outcome")) + end + + test("rejects recursive by-value layouts") do + definitions := [ + Gate2TypeDefinition::Record( + "example#Recursive", + [_gate2_field("next", Gate2ValueType::Aggregate("example#Recursive"))], + ), + ] + case verify_gate2_layouts(definitions) + when Result::Ok(_valid) + expect(false).to_be_true() + when Result::Err(diagnostic) + expect(diagnostic.code).to_equal("TRBN0004") + expect(diagnostic.message).to_equal("recursive by-value aggregate layout is unsupported") + end + end + + test("rejects duplicate fields and variants") do + duplicate_fields := [ + Gate2TypeDefinition::Record( + "example#Duplicate", + [ + _gate2_field("value", _gate2_scalar(ScalarType::Integer)), + _gate2_field("value", _gate2_scalar(ScalarType::Float)), + ], + ), + ] + case verify_gate2_layouts(duplicate_fields) + when Result::Ok(_valid) + expect(false).to_be_true() + when Result::Err(diagnostic) + expect(diagnostic.message).to_equal("field name is duplicated") + end + duplicate_variants := [ + Gate2TypeDefinition::Tagged( + "example#DuplicateTag", + [ + Gate2Variant.new(name: "Same", fields: []), + Gate2Variant.new(name: "Same", fields: []), + ], + ), + ] + case verify_gate2_layouts(duplicate_variants) + when Result::Ok(_valid) + expect(false).to_be_true() + when Result::Err(diagnostic) + expect(diagnostic.message).to_equal("variant name is duplicated") + end + end + + test("rejects unknown aggregate field types") do + definitions := [ + Gate2TypeDefinition::Record( + "example#Broken", + [_gate2_field("missing", Gate2ValueType::Aggregate("example#Missing"))], + ), + ] + case verify_gate2_layouts(definitions) + when Result::Ok(_valid) + expect(false).to_be_true() + when Result::Err(diagnostic) + expect(diagnostic.message).to_equal("aggregate type does not exist") + end + end + + test("rejects duplicate type identities") do + definitions := [ + Gate2TypeDefinition::Record("example#Same", []), + Gate2TypeDefinition::Tagged( + "example#Same", + [Gate2Variant.new(name: "Only", fields: [])], + ), + ] + case verify_gate2_layouts(definitions) + when Result::Ok(_valid) + expect(false).to_be_true() + when Result::Err(diagnostic) + expect(diagnostic.message).to_equal("type identifier is duplicated") + end + end + + test("enforces the configured aggregate byte limit") do + mut chunk_fields: Array := [] + mut index := 0 + while index < 256 + chunk_fields.push(_gate2_field("word" + index.to_s(), _gate2_scalar(ScalarType::Integer))) + index += 1 + end + mut page_fields: Array := [] + index = 0 + while index < 256 + page_fields.push(_gate2_field("chunk" + index.to_s(), Gate2ValueType::Aggregate("example#Chunk"))) + index += 1 + end + definitions := [ + Gate2TypeDefinition::Record("example#Chunk", chunk_fields), + Gate2TypeDefinition::Record("example#Page", page_fields), + Gate2TypeDefinition::Record( + "example#TooLarge", + [ + _gate2_field("first", Gate2ValueType::Aggregate("example#Page")), + _gate2_field("second", Gate2ValueType::Aggregate("example#Page")), + _gate2_field("third", Gate2ValueType::Aggregate("example#Page")), + ], + ), + ] + case compute_gate2_layout(definitions, "example#TooLarge") + when Result::Ok(_layout) + expect(false).to_be_true() + when Result::Err(diagnostic) + expect(diagnostic.code).to_equal("TRBN0005") + expect(diagnostic.message).to_equal("aggregate layout exceeds the Gate 2 byte limit") + end + end +end