Files
sx/current/CHECKPOINT-COMPILER-API.md

136 KiB
Raw Blame History

CHECKPOINT-COMPILER-API — comptime compiler library (#library "compiler" + abi(.zig) extern)

Companion to the design-of-record ../design/comptime-compiler-api.md (the plan

  • phased build order live there). This stream supersedes the metatype declare/define/type_info #builtins and the #compiler struct attribute with ONE welded mechanism. Branch: reify (off master). Update after every step.

⏯ Resume (fresh session)

⚠ DIRECTION CHANGED (2026-06-17). The active plan is now PLAN-COMPILER-VM.md, NOT the weld. The byte-weld + serialization/marshaling approach is the wrong direction and is being stripped. New foundation: a bytecode VM over byte-addressable memory so comptime values are native bytes; then the compiler-API rides on it with direct memory access (no weld, no validation, no marshaling). Everything below this banner describes the now-superseded weld state (committed on reify through 40d075c) and is kept only to scope the Phase 0 strip. Read PLAN-COMPILER-VM.md first.

Why the pivot: the comptime evaluator (src/ir/interp.zig) represents values as tagged Value unions, NOT native bytes — so a comptime @ptrCast(*StructInfo) reads the Value union's memory, not a struct. The weld tried to bridge that with hand-marshaling — exactly what the design set out to kill. Comptime memory makes comptime values real bytes, so the bridge disappears. (JIT-native comptime was rejected: it breaks cross-compilation — host vs target layout — and loses the sandbox. A comptime VM keeps both while getting native bytes + speed.)

Next action (2026-06-18) — the WHOLE metatype surface is VM-native (steps 7+8, committed through d0ebc55; step 8 uncommitted). declare/define/type_info + tagged-union enum_init all run NATIVELY on the VM (.call_builtin exec arm → callBuiltinVm; defineFromInfo decodes a TypeInfo from comptime memory, buildTypeInfo reflects one INTO comptime memory — faithful ports of legacy defineEnum/Struct/Tuple/reflectTypeInfo). The ENTIRE metatype range 06140624 + 0632 runs HANDLED with ZERO fallback (incl. the define(declare, type_info(T)) round-trips 0619/0622/0623); VM output byte-matches legacy. enum_init/define/type_info bail loudly on a backing_type tagged union rather than silent-clobber. 697/0 BOTH gates + all unit tests. THE NEXT STEP — Phase 4D.3 (compiler_call / #compiler hooks on the VM). Phase 4 (legacy-interp retirement) is PLANNED in PLAN-COMPILER-VM.md; user decision: UNIFY (the VM runs the post-link bundler too, interp.zig fully deleted). DONE this arc (all green): 4A.1 box_any/unbox_any + .any as a 16-byte aggregate (1526d19); 4D.0 comptime memory → an ARENA, Addr = real host pointer, no buffer/cap/move (625ba0f); 4D.1 general host-FFI escape — Vm.callHostExtern dlsym + host_ffi, any extern, args/returns pass untouched since Addr IS a host pointer (e7a8708, example 0636 toupper); 4D.2 slice/string args → NUL-term char* + float-arg/return guards (6a7f690, example 0637 strlen([:0]u8)). 699/0 BOTH gates.

DIRECTION CORRECTION (2026-06-18, user): #compiler / compiler_call is DELETED, not bridged on the VM. BuildOptions is RE-EXPRESSED as abi(.zig) extern compiler functions (the compiler-API surface the VM already dispatches via callCompilerFn); the #compiler attribute, the compiler_call IR op, and the Value-based hook Registry (compiler_hooks.zig) all go away. So there is NO transitional compiler_call→hooks shim on the VM (I started one — threading the legacy interp into tryEval for the hook registry — and reverted it; tree clean at b05c74f). 0602/0603 stay on legacy fallback until the BuildOptions migration lands. Migration shape (end-state, shares the BuildConfig-on-the-VM prerequisite with the bundler 4E): (1) each BuildOptions setter/getter becomes a compiler fn in compiler_lib.bound_fns + Vm.callCompilerFn, reading comptime args + a *BuildConfig threaded into the Vm (the same BuildConfig main.zig forwards); (2) library/modules/build.sx declares them abi(.zig) extern compiler instead of struct #compiler; (3) delete the compiler_call op + compiler_hooks.zig HookFn/Registry + the #compiler parse/lower path. See PLAN-COMPILER-VM.md Phase 4.

Corpus-driven remainder (independent of the BuildOptions migration): ALL PURE ops are DONE: switch_br, type_name, error_tag_name_get, global_addr, type_is_unsigned. out DONE (2026-06-19, newest Log entry): removed the out builtin — it's a plain sx fn calling libc write, so the VM handles it via host-FFI (no buffer, no special arm; no double-print because there's no out op to bail-then-fallback on). trace_resolve PORTED (1035). 0613/1035/0522/1038 run VM-HANDLED. Remaining side-effect op: interp_print_frames (1034 — writes the comptime frame chain; could likewise become a plain sx fn over the trace runtime). · 4B VM diagnostics (1179/1180) — DONE (strict renders the proper comptime type construction failed: diagnostic; VM-gap strict bails are now ONLY the 4 compiler_call) · 4C #insert. BuildOptions migration — design settled + foundation underway (2026-06-18, see the two newest Log entries): #compiler/compiler_call is replaced by abi(.compiler) (a compiler-domain ABI — runs in the comptime evaluator, never in the binary). S1+S2 DONE: abi(.compiler) introduced, the old abi(.zig) extern compiler + #library "compiler" fully removed, all compiler-API examples migrated. S3 DONE: emit_llvm skips BODIED abi(.compiler) (compiler-domain) functions; a comptime-only call from a dead body emits undef (regression 0638; 701/0 both gates). The earlier "runtime-reachability gating" blocker is MOOT — a compiler-domain callback isn't LLVM-emitted, so its build_options() calls never reach the emitCall gate. S4 SKIPPED (optional ergonomics): an abi(.compiler) function is type-compatible with a plain () -> R param (the ABI marks the function, not its type), so callbacks/registrars just declare themselves abi(.compiler) (S3) — no param-propagation needed. S5a DONE: build_options + set_post_link_callbackabi(.compiler), BuildConfig threaded into the VM; bundle_main + the platform registrars marked abi(.compiler); strict compiler_call bails 6→2 (0602/0603/1604/1611 HANDLED). S5a is a GREEN INTERMEDIATE — do NOT extend it. DESIGN PIVOT (2026-06-18, user): the 37-hook BuildOptions port is DEAD — DRIVE THE BUILD PIPELINE FROM SX (newest Log entry + PLAN-COMPILER-VM.md → Phase 5). BuildConfig becomes plain sx data; the compiler shrinks to a few abi(.compiler) primitives (emit_object/link/queries, explicit args, -> ! not bool) + an on_build slot (stdlib default_build, user override #run on_build = build;). P5.1 (= 4E) DONE (2026-06-19, newest Log entry): core.invokeByFuncId (the post-link build-driver invocation) now runs the callback on the VM with NO fallback (a side-effecting callback can't double-execute); BuildConfig + import_sources threaded in; VM bail → hard build error (comptime_vm.last_bail_reason surfaced by main.printInterpBailDiag). Smoke test 1661-platform-post-link-vm-list (AOT) — a post-link callback that GROWS a List (0141: works on VM, bails on legacy with struct_get); build succeeds (exit 0) only via the VM. flushInterpOutput deleted (VM writes out direct via host-FFI). 702/0 both gates. P5.2 metadata queries DONE (2026-06-19, newest Log entry): c_object_paths() -> List(string) + link_libraries() -> List(string) are abi(.compiler) primitives (new stdlib home library/modules/compiler.sx), serviced by comptime_vm.callCompilerFn reading BuildConfig fields main.zig forwards (c_object_paths/link_libraries). New reusable VM helper makeStringList builds a List(string) in comptime memory (target-aware via the result type's offsets); invoke/callCompilerFn now thread the call's result type (ins.ty). Legacy handlers bail loudly (VM-only by nature — post-link). Smoke test 1662-platform-build-pipeline-queries (AOT, C companion → 1 object): a post-link callback checks the VM-built list is well-formed; build exit 0 ONLY if so (negative-probe verified: wrong count → "post-link callback returned false", exit 1). emit_object() -> string ALSO landed (a QUERY — the Zig driver emits eagerly, the primitive returns BuildConfig.object_path; NO vtable). So all three QUERY primitives are done. 703/0 both gates. P5.2b (link ACTION) DONE (2026-06-19, newest Log entry): link(objects, output, libraries, frameworks, flags, target) dispatches through a host-installed compiler_hooks.BuildHooks vtable (main.zig LinkHooksCtxtarget.link); USER DECISION: the build callback is NOT falliblelink is plain VOID, a failure BAILS (hard build error), no -> !/failable-tuple needed. New VM readers readStringList/ readStringArg. Smoke test 1663-platform-build-pipeline-link (AOT): a post-link callback re-links the build's objects to a temp output via sx link — the relinked binary RUNS; negative-probe verified (bad path → bail → build exit 1). P5.3 (on_build registrar) + P5.4 CORE DONE (2026-06-19, newest Log entries): the whole build is sx-driven via default_pipeline (force-lowered + auto-invoked; NO Zig auto-emit/auto-link); on_build(cb) is the sole callback mechanism; set_post_link_callback deleted. 703/0 both gates. NEXT — the FULL MIGRATION (no legacy left), spec'd as Phase 5 steps P5.5P5.8 in PLAN-COMPILER-VM.md: P5.5 DONE (2026-06-19, newest Log entry): the 35 BuildOptions #compiler methods → VM-native abi(.compiler) arms (comptime_vm.callBuildOptionFn, NO legacy handler); setter strings duped into the persistent Vm.gpa; #run/const-init compiler-domain entries routed to the VM (entryNeedsVm, no fallback) so gate-OFF stays green; 5 bundle.sx helpers marked abi(.compiler). BuildOptions compiler_call bails GONE (1609/1614/1615 strict-clean). 37 .ir regenerated (string-pool churn, behavior-identical). · P5.6 prereq DONE (994d649): ported bit_and/bit_or/bit_xor/bit_not/shl/shr into the VM (the 1616 shr gap); test 0639. 704/0 BOTH gates. · P5.6 REMAINING (the big body, NEXT): move platform/bundle.sx's per-target logic into / called by default_pipeline (call bundle() after link when bundle_path() is set), reading the migrated abi(.compiler) getters + fs/process host-FFI; remove the --bundle/post_link_module Zig shim (compiler keeps ONLY the linker primitive). ALL bundling + code signing for EVERY target (macOS/iOS-device/iOS-sim/Android) in the sx default_pipeline · P5.7 DELETE #compiler/compiler_call/compiler_hooks/interp.zig + the regToValue bridge + VM→legacy fallback (drop gate-OFF; VM is the SOLE evaluator) · P5.8 build ~/projects/m3te + ~/projects/distribution end-to-end as the acceptance test + add .app/.apk smoke tests. FINAL atomic step (4F): (out already done — VM-native via libc write) handle interp_print_frames + flip strict-to-default (remove the fallback) + delete interp.zig/Value + re-express define/make_enum. See PLAN-COMPILER-VM.md → Phase 4 for the full plan + top risks (bundler test coverage). Earlier landed: dedicated Type builtin TypeId (6844fb9/94f60c5/554871b); WRITE side declare_type/register_type/pointer_to VM-native (66005af); real lowering-time Context (eb68d9e); metatype construction declare/define/enum_init (d0ebc55).

Done so far in Phase 3:

  • READ side (7 readers, dual-path): find_type/type_kind/type_field_count/ type_nominal_name/type_field_name/type_field_type/type_field_value, each backed by a TypeTable query both the legacy handler and the VM call (no drift). Examples 06280630.
  • WRITE side (P3.3, legacy-only at lowering time): declare_type + pointer_to + ONE kind-branching register_type (subsumes define's per-kind dispatch; codes match type_kind: 1 struct · 2 actual .@"enum" · 3 tagged_union · 4 tuple). Idempotent re-fill (two-edge import). Plus two fixes (issue 0142): all-void enum → real .@"enum" (was a verifySizes panic); bare EnumType.variant qualified construction. Examples 06310635, 0187.
  • Lowering-time VM (P3.4): hardened the VM against malformed lowering-time IR (refTy, bailing aggType, bounds-checked branch targets — bails, never panics); wired tryEval into runComptimeTypeFunc behind the flag with legacy fallback; materialized a zeroed lowering-time Context (the global isn't built yet at lowering). All measured green.

THE WALL (next step): a Type value is an 8-byte tid, but .any (the boxed-any) is a 16-byte {tag,value} — and they share one TypeId (.any). So a Type in an aggregate (Member.ty/EnumVariant.payload) is sized 16B while the value is 8B → every lowering-time type-fn bails at const_type / the Member-array build. Can't make kindOf(.any) a word: at EMIT time .any really is a 16B box (variadic any, 0603), so that would silently corrupt it. The correct fix is a dedicated Type builtin TypeId (8B), distinct from .any — measured at ~123 .any references across ~25 files (pack.zig has 30), a ~100-touch-point cross-cutting change → its own focused session (USER CHOSE to pause rather than rush it). Rejected alternatives: a scoped "lowering-mode treats .any as a word" flag (silent-wrong on a real Any box in a reflection type-fn); scalar-only Type-fns (safe but no real corpus type-fn is scalar-only — they all build a Member/variant aggregate).

Decisions recorded: find_type returns a non-optional TypeId using unresolved(0), NOT ?Type; reader names use the type_* family (avoid colliding with std field_name/type_name); the write side is a single kind-branching register_type; the write side stays LEGACY-only until the VM runs at lowering time (needs the Type TypeId). End-state guarantee: ONE evaluator — interp.zig deleted; dual-path + fallback are transitional (see PLAN end state). Build/verify: zig build && zig build test (697, gate OFF). Run the corpus ON the VM: zig build test -Dcomptime-flat OR env SX_COMPTIME_FLAT=1. Coverage trace: SX_COMPTIME_FLAT_TRACE=1 (now also prints lowering-time type-fn HANDLED/fallback lines).

(superseded) prior weld resume

Phase 1 done; Phase 2 welded structs were working via reflection + memory-order validation (the computeWeldPlan/byte-blob "GEP engine" was explored + DROPPED even earlier). A welded Name :: struct abi(.zig) extern compiler { … } declared fields in the compiler type's MEMORY order; the compiler reflected the bound Zig type and VALIDATED the header. This whole mechanism is now being stripped — see the banner.

⚠ Snapshot workflow: use -Dname=examples/NNNN-foo.sx[,…] -Dupdate-goldens to regenerate ONLY the named example(s) — a full -Dupdate-goldens re-runs all ~690 and a flaky/host-divergent example (AOT/cross-arch) can clobber good snapshots. See CLAUDE.md → Snapshot integrity.

Last completed step

Phase 2 — welded structs by reflection + memory-order validation (byte-identical, no GEP engine). A welded struct abi(.zig) extern compiler { … } now works end-to-end as a byte-identical mirror of the bound Zig type.

Design (locked, supersedes the byte-layout-override plan):

  • The sx header declares fields in the compiler type's MEMORY order. The compiler REFLECTS the bound Zig type — field names from @typeInfo, offsets from @offsetOf, size from @sizeOf — and validates the header matches. Nothing is maintained by hand; a types.zig change re-reflects on the next compiler build.
  • On pass it's an ORDINARY struct whose natural layout already equals the Zig layout → @ptrCast to the compiler type + deref is byte-identical. No byte-blob, no index/remap tables, no reorder, no special LLVM path.
  • Loud, precise diagnostics on any drift: field not found (+ memory order), wrong field order at position N (+ expected memory order), type layout mismatch (field size), layout mismatch (total size / count).

What changed from the dropped plan:

  • compiler_lib.zig: weldStruct now REFLECTS field names (@typeInfo) and bakes bound_types fields in ascending-OFFSET (memory) order — no hand-listed names. Deleted computeWeldPlan/WeldPlan/WeldElement. validateStructLayout checks the sx header against the memory-ordered registry.
  • nominal.zig validateWeldedStruct: renders the precise diagnostics (+ weldedFieldOrderStr).
  • Examples: 0627 (StructInfo in memory order, byte-identical, usable); 1186 (source-order StructInfo → wrong-field-order diagnostic). 1183 message refreshed.
  • zig build + zig build test green (692 corpus, unit tests pass).

Earlier — Phase 2.1 (weld-plan layout math, now removed)

The weld-plan offset math + StructInfo registered. Was the core of the byte-layout-override engine; superseded by the reflection+validation design above.

Decision (locked 2026-06-17): full byte-layout weld — a welded sx struct is laid out byte-identically to the bound Zig type (Zig's @offsetOf, reordering + padding included), so it passes to a Zig handler as raw memory with zero marshalling. (The alternative — handlers reading interp Value aggregates logically, no layout override — was rejected; welded types must also be usable as runtime data, and the design wants the literal byte weld.)

  • Measured: Zig reorders StructInfo to fields@0, name@16, nominal_id@20, is_protocol@24, size 32 — vs sx-natural name@0, fields@8, … So the override is genuinely required (Field's two-u32 natural layout was the easy case).
  • compiler_lib.zig: registered StructInfo (weldStruct, the second bound_types entry). Added WeldElement / WeldPlan + computeWeldPlan(alloc, fields, total) — pure: orders fields by ascending byte offset, inserts padding elements for gaps + the alignment tail, and builds the sx-field → LLVM-element remap. This is what the LLVM type builder + struct-GEP sites will consume.
  • Unit-tested (compiler_lib.test.zig): Field → identity plan (2 elems, no pad); StructInfo → 5 elems [fields@0, name@16, nominal_id@20, is_protocol@24, pad@25..32], remap [1,0,3,2].
  • zig build + zig build test green.

Earlier — Phase 1 polish (comptime-only enforcement)

A RUNTIME call to a fn abi(.zig) extern compiler is a clean build-gating error instead of an undefined-symbol link failure.

  • emitCall (src/backend/llvm/ops.zig): when the callee is compiler_welded AND the ENCLOSING function is not is_comptime (i.e. genuine runtime code, not a #run/:: initializer wrapper whose LLVM body is dead), print a clear "comptime-only … cannot be called at runtime" error and set comptime_failed (the driver halts before object/JIT emission). The enclosing is_comptime guard is what keeps the legitimate #run use (example 0626) green.
  • Corpus: examples/1185-diagnostics-weld-fn-runtime-call.sx (runtime intern(…) → clean error, exit 1, no link failure).
  • zig build + zig build test green (458 unit + 690 corpus).

Earlier — fifth sub-step (host-call bridge)

A fn abi(.zig) extern compiler dispatches, under the comptime interpreter, to its registered Zig handler instead of dlsym.

  • compiler_lib.zig: function registry — BoundFn { sx_name, handler }, bound_fns = intern(string)->StringId + text_of(StringId)->string (the string-pool round-trip), findFn, and FnHandler (*Interpreter, []Value -> Value). intern mutates via interp.mint orelse @constCast(&module.types) (the same mutable-table access the metatype mint path uses); text_of reads the const pool. Imports interp.zig (the compiler_hooks↔interp cycle pattern).
  • IR Function gained compiler_welded: bool. declareFunction (src/ir/lower/decl.zig) sets it via weldedCompilerFn, which also VALIDATES: the bound lib must be compiler and the name must be on the function-export list — else a build-gating .err (no silent fall-through to dlsym).
  • interp.call(): before the dlsym/extern path, a compiler_welded function routes to compiler_lib.findFn(name).handler(self, args) (clean bail off the export list).
  • Corpus: examples/0626-comptime-weld-fn-intern-text-of.sx (#run text_of(intern("hello, compiler")) folds to a string constant → prints it); examples/1184-diagnostics-weld-fn-unexported.sx (unexported welded-fn name → build error). findFn lookup unit-tested.
  • Runtime-call rejection is NOT yet clean — welded fns are comptime-only; a RUNTIME call would emit a reference to a non-existent extern symbol → a loud LINK error (not silent, but not a tidy diagnostic). The examples call welded fns only inside #run. A dedicated "comptime-only symbol" emit diagnostic is the immediate follow-up.
  • zig build + zig build test green (458 unit tests + 689 corpus).

Earlier — fourth sub-step (welded-struct layout validation)

A struct abi(.zig) extern compiler { … } is validated against the binding registry as a header checked against the implementation.

  • compiler_lib.zig: validateStructLayout(bt, sx_fields, total) — pure, returns the first LayoutMismatch (field count / name / size / total) or null. Plus lib_name = "compiler" and SxField. Unit-tested (faithful Field passes; each drift flagged as the right variant).
  • registerStructDecl (src/ir/lower/nominal.zig): for sd.abi == .zig, validateWeldedStruct checks the bound lib is compiler, the name is on the export list (findType), and the sx layout (field names + typeSizeBytes + total) matches the welded type — emitting a build-gating .err (good span into the struct body) on any failure. No silent reinterpretation.
  • #library "compiler" is the comptime-only internal surface, NOT a dylib — src/main.zig's dlopen walker skips it (was emitting a spurious libcompiler.so load warning).
  • Corpus: examples/0625-comptime-weld-struct-field.sx (faithful Field welds, validates, usable as data → name=7 ty=3); examples/1183-diagnostics-weld- struct-field-count.sx (one-field Field → build-gating field-count diagnostic).
  • Offset-override / GEP emission for non-natural Zig layouts is NOT here — it isn't exercised by Field (two u32s = natural layout coincides with the weld). It arrives with StructInfo in Phase 2 (slices/reordering), where the bound offsets actually differ from the sx-natural ones. The validation already checks per-field size + total, so a layout drift is caught even before the override engine exists.
  • zig build + zig build test green (456 unit tests + 687 corpus).

Earlier — third sub-step (binding registry)

The binding registry (welded-type lookup, layout baked from the real Zig type).

  • New src/ir/compiler_lib.zig — the compiler library's binding registry, the curated safety boundary. BoundType { sx_name, size, alignment, fields: []FieldLayout{name, offset, size} }; weldStruct bakes the layout from a real Zig struct via @sizeOf/@alignOf/@offsetOf at compiler-build time (a sx-field-count mismatch is a @compileError, never a silent truncation). bound_types exports Field (welded to types.TypeInfo.StructInfo.Field — two u32s); findType(sx_name) ?*const BoundType is the lookup the welded-decl resolution path will consult (returns null off the export list — clean boundary, no silent default).
  • Registered in the barrel (src/ir/ir.zig): compiler_lib + compiler_lib_tests.
  • Tests (src/ir/compiler_lib.test.zig): findType("Field") equals the real StructInfo.Field @sizeOf/@alignOf/@offsetOf (8 bytes, two u32s at 0/4); an unexported name returns null. Break-verified (a wrong size → suite red, named ir.compiler_lib.test...).
  • zig build + zig build test green (454 unit tests).

Earlier — second sub-step (struct-decl parse)

abi(.zig) extern <lib> PARSES on a STRUCT decl (parse-only, no semantics).

  • ast.StructDecl gained abi: ABI + extern_lib: ?[]const u8 binding fields.
  • parseStructDecl (src/parser.zig): after struct (and the #compiler check), parse an optional abi(...) then optional extern <lib> — same slot order as fn decls — and thread them onto the node. Ordinary structs are unperturbed (parseOptionalAbi/parseOptionalExternExport no-op when absent).
  • Parser unit tests (src/parser.test.zig): Field :: struct abi(.zig) extern compiler { name: StringId; ty: Type; } parses with abi == .zig, extern_lib == "compiler", field list intact; a plain struct leaves abi == .default / extern_lib == null. Break-verified (a wrong-sentinel assert turns the suite red, confirming the test runs).
  • zig build + zig build test green.

Earlier — first sub-step (fn decls) + the syntax pivot

abi(.zig) extern <lib> PARSES on a fn decl (parse-only). Plus the syntax pivot it required.

Syntax decision (locked 2026-06-17, supersedes the doc's original extern(.zig) <lib> single-qualifier form): the ABI/layout selector and the linkage keyword are two orthogonal annotations.

  • abi(.x) — ABI / calling-convention annotation in the slot before extern/export. Unified replacement for callconv(...), which is removed. ABI = { default, c, zig, pure }: .c (C ABI), .zig (Zig-layout weld → the compiler library), .pure (naked asm), .default (unannotated). Can appear standalone (no extern) on any fn / fn-type / lambda.
  • extern <lib> — linkage keyword + binding source (named library).

So a welded binding is text_of :: (id: StringId) -> string abi(.zig) extern compiler;.

What landed:

  • AST (src/ast.zig): CallingConventionABI { default, c, zig, pure }; the call_conv field → abi: ABI on FnDecl / Lambda / FunctionTypeExpr.
  • Lexer/token (src/token.zig, src/lexer.zig): kw_callconvkw_abi, keyword string "callconv""abi".
  • Parser (src/parser.zig): parseOptionalCallConvparseOptionalAbi (parses abi(.c|.zig|.pure)); wired in the fn-decl postfix slot (before extern/export), the function-type-expr slot, and the lambda slot; isFunctionDef/hasFnBodyAfterArrow recognise kw_abi.
  • AST→IR map (src/ir/type_resolver.zig, src/ir/lower/decl.zig, sema.zig, closure.zig): the AST .abi == .c reads kept their C-ABI meaning; the function-type resolver maps .zig/.pure → IR .default (no fn-pointer-type CC for those decl-level ABIs; neither occurs in a function-TYPE position yet).
  • CC-mismatch diagnostic (src/ir/lower/expr.zig, src/sema.zig): the user-facing text callconv(.c)abi(.c).
  • sx migration: 52 .sx files callconv(abi( (all were function-type callback annotations — none in the fn-decl postfix slot, so no reordering).
  • Docs: readme.md, specs.md, the design doc, snapshots (0114 / 1104 / 1200) regenerated for the rename.
  • Tests: parser unit tests in src/parser.test.zigabi(.zig) extern <lib> on a fn decl (asserts abi == .zig, extern_export == .extern_, extern_lib == "compiler"); bare extern leaves abi == .default; standalone abi(.c) / abi(.pure). lexer/sema tests updated.

zig build + zig build test green (450/450 unit + 685 corpus).

Current state

Pivoted — see the banner + PLAN-COMPILER-VM.md. The items below are the weld machinery as it stands on reify HEAD (40d075c); they are the strip list for Phase 0, not the forward direction. The #library/abi/extern syntax stays; the weld semantics (layout reflection/validation, marshaling dispatch) go.

  • compiler :: #library "compiler"; parses + is recognised as the comptime-only internal surface (never dlopen'd).
  • abi(.zig) extern compiler STRUCTS: layout-validated against the registry (faithful → ok; drift → build-gating diagnostic). Field welds + usable.
  • abi(.zig) extern compiler FUNCTIONS: dispatched under the comptime interp to their registered Zig handler (intern/text_of round-trip works); unexported names rejected at declaration. Comptime-only.
  • A RUNTIME call to a welded fn is a clean build-gating error (comptime-only enforcement at emitCall); the legitimate #run/:: use stays green.
  • The whole Phase 1 foundation (parse → registry → struct-layout validation → function host-call bridge → comptime-only enforcement) is in place for the two-u32 Field case + the two string readers.
  • Deferred: offset-override / LLVM byte-offset GEP for non-natural layouts (needed by StructInfo's slice field, Phase 2).

Next step — execute PLAN-COMPILER-VM.md

The weld is being stripped. The next step is Phase 0 of PLAN-COMPILER-VM.md — remove the weld / serialize / marshal machinery (compiler_lib.zig reflection+validation, nominal.zig validateWeldedStruct, the compiler_welded dispatch, the weld examples/diagnostics 0625/0627/1183/1184/1185/1186), keeping the #library/abi/extern syntax. Then Phase 1 (byte-addressable value model). The weld-era "next step" below is obsolete — kept only as a record of what the weld surface was about to do.

(obsolete) weld-era next step

Welded structs were byte-identical mirrors, so the API surface was set to grow:

  • Bind register_struct / find_type over the host-call bridge (compiler_lib.zig bound_fns, like intern/text_of). register_struct takes a welded StructInfo and mints a real TypeId (guarded: dup field names, kind well-formedness — the checks define does today). Because the welded StructInfo is byte-identical, the handler can read it as the real Zig *StructInfo (cast + deref) rather than marshalling a Value field-by-field — the payoff of the byte-weld. find_type(StringId) -> ?Type reads the table. Prove: build a struct programmatically + round-trip a source one.
  • Re-express type_info/define (struct) as sx over register_struct/ find_type; migrate examples/0622; delete the bespoke struct interp arms (defineStruct / the reflectTypeInfo struct path).

Then Phase 3+: widen the welded types to EnumInfo/TaggedUnionInfo/TupleInfo (optional fields → sentinels) — each just needs an sx header in the compiler type's memory order + the matching register_* fn. Finally migrate BuildOptions to abi(.zig) extern compiler (re-home the #compiler registry) and delete #compiler.

Note: a welded struct with an ?T / union(enum) field (e.g. EnumInfo's backing_type: ?TypeId, explicit_values: ?[]const i64) is the next layout wrinkle — the sx header must mirror Zig's optional/union representation. Handle when reached (sentinels or accessor fns; see the design doc Risks).

Known issues

  • None for this stream. (Metatype's deferred enhancement is issue 0141 — comptime List growth; orthogonal, see current/CHECKPOINT-METATYPE.md.)

Log

  • P5.8 (partial) — real-project validation: m3te + distribution build with the new pipeline (2026-06-19). Acceptance test for the sx-driven build pipeline. m3te (~/projects/m3te, an SDL3 match-3 game): migrated its build.sx off the deleted API — configure_build :: ()() abi(.compiler), opts.set_post_link_callback( bundle_main)on_build(bundle_main) (the ONLY source change needed). Then sx build main.sx produces a valid SIGNED macOS .app (correct Contents/{MacOS,Resources} layout, Resources/assets/ bundled, links Homebrew SDL3, passes codesign); sx build --target ios-sim main.sx produces the flat iOS .app (DTPlatformName= iPhoneSimulator; system-framework "not embedded" warnings are expected/benign). So BOTH the macOS and iOS-sim bundle paths validate end-to-end on real project code. distribution (~/projects/distribution, a server/CLI+sqlite project, NO bundling): make build clean (smoke + dist binaries via default_pipeline's emit+link with C objects + vendored sqlite); make test 24/25 (the 1 fail, publish_happy.sx, is a PRE-EXISTING stale #foreign parse error — that keyword is gone from the parser; unrelated to the pipeline/this session). m3te's build.sx edit is in its working tree, uncommitted (the user's repo) — reported, not committed.

    REMAINING P5.8: iOS-device + Android paths still unvalidated on this host (no device identity round-trip / Android SDK); add .app/.apk bundle SMOKE TESTS to the corpus (needs corpus-runner support — a .build directive for a bundle dir + structure assertion + cleanup; deferred as its own focused step). m3te is now a repeatable real-project acceptance test in the meantime.

  • P5.6 (macOS path) — default_pipeline drives bundling; fix issue 0125 (2026-06-19). build.sx now #imports platform/bundle.sx and default_pipeline delegates to bundle_main when bundle_path() is set (emit+link via the shared emit_and_link core, then wrap the .app/.apk); else just emit+link. Removed the Zig --bundle/post_link_module dispatch shim (main.zig) — CLI bundle flags only feed BuildConfig, default_pipeline branches on bundle_path(). USER DECISION: import the bundler directly (it's abi(.compiler), emit-skipped, never in the binary; the build↔bundle import cycle resolves like std↔build) — NOT a registration slot. Validated end-to-end on macOS (the stream's top-risk gap — bundler had ZERO coverage): sx build --bundle App.app --bundle-id … plain.sx AND auto-bundle from set_bundle_path both produce a valid SIGNED .app (correct Contents/MacOS/ layout, Info.plist, passes codesign, binary runs). Fixed a pre-existing host-build bug: target_triple was empty for host builds → is_macos() false → wrong flat layout; main.zig now exposes the host triple (LLVMGetDefaultTargetTriple) when --target is absent. bundle_main dropped its redundant build_options() re-fetch (uses its opts param). Fix issue 0125 (surfaced because the bundler's format("…{}…") instantiates any_to_string, which over-materialized array types): the type-match dispatcher (lowerRuntimeDispatchCall) unboxed each interned array tag to the concrete array type (whole-array load) → LLVM scalarized to one DAG node/element (~12s / segfault at [65536]u8). Fix (route 1): case array: arm calls slice_to_string, dispatcher builds a {ptr,len} slice VIEW of the payload pointer (unbox_any → [*]elem = int-to-ptr, NO load) for an ARRAY tag bound to a SLICE param. Output byte-identical ([a, b, c]); 0055 drops 12s→0.2s. Pinned examples/0056-basic-large-array-format-no-blowup.sx; issue 0125 marked RESOLVED. 37 .ir regenerated (bundler types + array-format lowering); .ir-only. 705/0 both gates (48eb7bf).

    REMAINING P5.6 / next: iOS-device / iOS-sim / Android paths in bundle_main exist + now run on the VM (the shr port let them through) but are NOT validated on this host (no iOS/Android SDK + no corpus bundle harness) — that's P5.8 (build m3te/distribution end-to-end + add .app/.apk corpus smoke tests). Then P5.7 (delete #compiler/compiler_call/compiler_hooks/interp.zig + the legacy fallback; VM becomes the sole evaluator).

  • P5.6 prerequisite — bitwise/shift ops ported into the VM (2026-06-19). comptime_vm exec now handles bit_and/bit_or/bit_xor/bit_not/shl/shr (new bitwise helper next to arith), mirroring the legacy interp's i64 model EXACTLY: shift amount clamps to @min(rhs, 63), shr is an ARITHMETIC right shift (sign-extending). These were unported and bailed — the shr gap surfaced via the iOS-device bundler once P5.5 let it run further (1616). With the port, 1616's strict VM run reaches the real bundler logic (no more shr bail; it now stops only at the genuinely-unavailable iOS runtime on macOS — _UIApplicationMain / no linked binary under sx run, expected). New focused corpus test examples/0639-comptime-bitwise-shift.sx (:: consts fold AND/OR/XOR/NOT/shl/shr/arith-shr; identical on both evaluators). 704/0 BOTH gates.
  • P5.5 — the 35 BuildOptions accessors migrated off struct #compiler onto VM-native abi(.compiler) (2026-06-19). BuildOptions :: struct #compiler { ...35 methods... }BuildOptions :: struct { } (an opaque null-sentinel handle) + 35 free ufcs (self: BuildOptions, …) abi(.compiler) decls in library/modules/build.sx, each serviced by a new comptime_vm.callBuildOptionFn arm (dispatched from callCompilerFn). NO legacy compiler_lib handler (per the full-migration direction): the 35 names are registered in compiler_lib.bound_fns only so weldedCompilerFn accepts them, with a single bailing stub handleBuildOptionsAccessor (never reached). String lifetime: setters dupe the arg string into the PERSISTENT Vm.gpa (the Compilation allocator threaded into both tryEval and runBuildCallback — NOT the per-eval VM arena, whose bytes die at Vm.deinit), so a #run-set path survives to post-link. Setters write/append the duped string to the threaded BuildConfig (output_path/bundle_path/…, the link_flags/ frameworks/asset_dirs ArrayLists); string getters return the field (or ""); bool getters compute from the triple (predIsMacOS/predIsIOS/…, mirroring the legacy hooks); count/indexed getters read the BuildConfig slices. Dispatch routing (Option B, chosen at start): a #run / const-init entry that directly calls a compiler-domain / compiler-welded fn (emit_llvm.entryNeedsVm) is routed through the VM with NO legacy fallback — regardless of the -Dcomptime-flat gate — so gate-OFF stays green without a legacy BuildOptions handler (P5.7 retires the legacy interp entirely). The 5 platform/bundle.sx helpers that call getters (build_info_plist/embed_framework/android_bundle_main/build_android_manifest/compile_jni_main_sources) are marked abi(.compiler) too (they're comptime-only bundler code; without it their now-welded getter calls trip the runtime-call gate). Snapshots: 37 .ir churned (std transitively imports build.sx → string-pool/ type-table indices shift) — regen scoped via -Dname; verified ONLY .ir changed (zero behavior-stream diffs). 703/0 BOTH gates. Strict sweep: the BuildOptions compiler_call bails are GONE (1609/1614/1615 strict-clean); 1616 now bails on shr (a pre-existing, separate VM gap — bitwise/shift ops shl/shr/bit_and/bit_or/ bit_xor/bit_not are unported in comptime_vm, surfaced now that the iOS-device bundler runs further; 1616 is unpinned + can't JIT-run on macOS anyway). Also (per user): swept the outdated "flat memory" terminology — the comptime VM is byte-addressable, ARENA-backed memory where Addr is a REAL host pointer, NOT a flat contiguous address space; "flat memory"/"flat-memory" → "comptime memory" / "byte-addressable" across comptime_vm.zig + the plan/checkpoint/CLAUDE docs (flag names -Dcomptime-flat/SX_COMPTIME_FLAT kept).

    NEXT — P5.6 (ALL bundling + code signing in default_pipeline). First likely sub-task: port the bitwise/shift ops (shl/shr/bit_and/bit_or/bit_xor/bit_not) into comptime_vm so the real bundler path runs on the VM (the 1616 shr gap). Then move platform/bundle.sx's per-target logic to read the migrated abi(.compiler) getters + fs/process host-FFI, call bundle() from default_pipeline after link when bundle_path() is set, and remove the --bundle/post_link_module Zig shim.

  • P5.4 CORE — the whole build is sx-driven via default_pipeline; no Zig auto-emit/auto-link (2026-06-19). The compiler's post-IR role is now: codegen → invoke the build callback. There is NO auto-emit / auto-link. Commits (all green): (1) core (d178454) — emit_object() is an ACTION (verify+emit via a host BuildHooks vtable; main.BuildHooksCtx); new query primitives build_output/build_target/ build_frameworks/build_flags (read the merged BuildConfig); library/modules/build.sx imports compiler.sx + defines default_pipeline (emit → gather c_objs → link); the compiler force-lowers default_pipeline (well-known name, decl.isDefaultBuildPipeline, force-lowered after Pass 2) and auto-invokes it post-codegen when no on_build override (main final fallback invokeByName "default_pipeline"); the BUILD path auto-imports modules/build.sx (prepends a synthetic import node in compileWithTimer) so prelude-less programs (asm tests) still get default_pipeline; removed the build cache short-circuits (a future cache can live in default_pipeline). (2) on_build-only (65ac370) — migrated all 9 set_post_link_callback callers to on_build(cb) (callback gains opt: BuildOptions); DELETED set_post_link_callback. Override semantics changed: an on_build callback REPLACES the build (must emit+link or return default_pipeline(opt) — delegation verified), unlike the old post-link callback that ran AFTER the auto-link. Reworked tests: 1662 (queries) + 1664 (override+List-grow) DELEGATE to default_pipeline; deleted 1661/1663 (primitives now exercised by EVERY AOT build). sx run (JIT) is UNTOUCHED (emits in-process, never invokes default_pipeline). Benign .ir churn each step; 703/0 both gates.

    REMAINING P5.4 (the BuildOptions-surface migration — large, mechanical, dual-path, string-lifetime-sensitive; NOT YET DONE): FINAL DIRECTION (user 2026-06-19): FULL MIGRATION — NO LEGACY. Drop gate-OFF entirely; the VM is the SOLE evaluator; delete interp.zig. Migrate DIRECTLY to VM-native abi(.compiler) arms — NO legacy compiler_lib handlers, NO dual-path. See PLAN-COMPILER-VM.md → Phase 5 steps P5.5P5.8 for the full spec. In brief:

    • P5.5 — migrate all 36 BuildOptions :: struct #compiler methods → free ufcs … abi(.compiler) decls + comptime_vm.callCompilerFn arms (NO legacy handler). Setters dupe strings into a PERSISTENT allocator (thread emit_llvm.alloc via e.g. BuildConfig.string_alloc). Kills the 4 strict compiler_call bails.
    • P5.6 — ALL bundling + code signing for EVERY target (macOS .app, iOS device/sim, Android .apk: Info.plist/codesign/provisioning/entitlements/framework-embed/AndroidManifest/javac/d8/aapt2/zipalign/ apksigner) runs in the sx default_pipeline (via the migrated getters + fs/process host-FFI). Remove the --bundle/post_link_module Zig shim. Compiler keeps ONLY the linker primitive (Option B).
    • P5.7 — DELETE #compiler + compiler_call op + compiler_hooks (Registry/HookFn) + interp.zig (Interpreter/Value/reflectTypeInfo/callExtern) + regToValue/valueToReg + the VM→legacy fallback; make -Dcomptime-flat permanent. A VM bail is ALWAYS a build diagnostic now. Re-express define/make_enum as sx. Land the 0141 repro; reconcile 1654.
    • P5.8 — build ~/projects/m3te + ~/projects/distribution end-to-end as the acceptance test that default_pipeline covers all targets; add .app + .apk bundle smoke tests (no corpus coverage today).
  • P5.3 (on_build registrar) — the build-callback registration mechanism; callback takes BuildOptions (2026-06-19). Per the user's design: on_build(cb) is the build-callback registrar (a FREE fn), generalizing set_post_link_callback — the callback is (opt: BuildOptions) -> bool abi(.compiler) and the compiler invokes it post-codegen WITH the opaque BuildOptions handle. Key simplification: the handle is a single null-sentinel word, so passing it sidesteps the feared fat-BuildConfig marshaling. Changes: VM callCompilerFn on_build arm + legacy handleOnBuild (both set post_link_callback_fn + a new BuildConfig.post_link_takes_options flag); comptime_vm runEntryrunEntryArgs(extra) (implicit ctx + explicit args) + a public runBuildCallback(..., pass_options); core.invokeByFuncId/invokeByName now take pass_options (was an always-empty args slice); main.zig passes getPostLinkTakesOptions(); build.sx on_build decl. Smoke test 1664-platform-on-build-callback (AOT). Benign 37-.ir churn (type table +1 for the on_build fn type; behavior identical — verified only .ir streams changed). 705/0 both gates.

    CONSOLIDATED REMAINING PLAN (P5.4 — from the user's 2026-06-19 direction; large + coupled + re-churns snapshots; the bundler has NO corpus coverage = the stream's top risk):

    1. Migrate to on_build ONLY — convert every set_post_link_callback(cb) caller (platform/bundle.sx bundle_main, examples 1611/1614/1615/1616, 0602/0603) to #run on_build(cb) with cb: (opt: BuildOptions) -> bool; DELETE set_post_link_callback (build.sx + compiler_lib + VM arm).
    2. Bundle/Android config → sx data in the default script. The #compiler accessors the user flagged — set_bundle_path/bundle_path/bundle_id/codesign_identity/provisioning_profile, set_manifest_path/keystore_path, jni_main_count/jni_main_runtime_path_at/jni_main_java_source_at — move into the sx BuildConfig/default script (sx-owned data), not compiler hooks.
    3. default_pipeline + override model. library/modules/build.sx ships #run on_build(default_pipeline) (the stdlib default); a user's #run on_build(custom) in main.sx OVERRIDES it (LAST-WINS — already the behavior, since registration just overwrites post_link_callback_fn). default_pipeline calls emit_object/c_object_paths/link_libraries/link + the sx bundler.
    4. REMOVE the Zig driver's auto-emit/auto-link (main.compileWithTimer) — COUPLED with (3): once default_pipeline drives emit+link, the driver must stop doing them or it double-links. Riskiest piece (whole build/bundle path; no corpus guard → needs dedicated bundle smoke tests).
    5. Delete #compiler/compiler_call/compiler_hooks + the S5a build_options once config is sx data → kills the 4 strict compiler_call bails (1609/1614/1615/1616) → strict sweep green → interp.zig deletable.
  • P5.2b (link ACTION) — the sx link primitive links on the VM via a host-installed vtable; build callback de-failable'd (2026-06-19). Phase 5's one genuine ACTION primitive: link(objects, output, libraries, frameworks, flags, target) (in library/modules/compiler.sx). USER DECISION this step: drop fallibility from the build callback — so link is a plain VOID primitive (no -> !), and a link failure BAILS on the VM → hard build error (sidesteps the failable-tuple-return construction entirely). The vtable: comptime_vm.zig can't depend on the driver (core/main/target), so link dispatches through a new compiler_hooks.BuildHooks { ctx, link_fn } that main.zig installs into BuildConfig.build_hooks before the post-link callback. The driver side is main.LinkHooksCtx (holds allocator/io/base_config/has_jni_main; its link adapter unions the explicit flags with the CLI ones and calls target.link(objects[0], objects[1..], …) — the linker treats first-vs-rest as equal inputs). New VM readers (inverse of makeStringList): readStringList (a List(string) arg → [][]const u8, element bytes are views into stable comptime arena) + readStringArg (a string arg). Registered link on bound_fns (legacy stub bails — VM-only). Smoke test examples/1663-platform-build-pipeline-link (AOT): a post-link callback re-links the build's own objects (via c_object_paths + emit_object) into a temp output through the sx link primitive — and the relinked binary is a FUNCTIONAL executable that runs (verified manually). Build exit 0 only if the VM-driven link succeeds; negative-probe verified (bad output path → ld fails → ComptimeVmBail: comptime link: linking failed, build exit 1 — the P5.1 VM-reason diagnostic path). The driver still auto-links too (P5.2b does NOT remove the Zig driver's target.link; the test links to a SEPARATE temp output) — removing the auto-link + having on_build drive everything is P5.3/P5.4. 704/0 both gates.
  • P5.2 (metadata queries) — c_object_paths / link_libraries compiler primitives + the VM List(string) builder (2026-06-19). Phase 5 step 2 (the read-only slice): two abi(.compiler) primitives that the sx build driver will pass to linkc_object_paths() -> List(string) (the #import c companion .os) and link_libraries() -> List(string) (the #library names). They live in a NEW stdlib file library/modules/compiler.sx (the Phase 5 home the sx default_build grows into) and are serviced by comptime_vm.callCompilerFn reading two new BuildConfig fields (c_object_paths/link_libraries) that main.zig forwards before the post-link callback (alongside binary_path/target_triple/…). Reusable new piece: Vm.makeStringList(table, list_ty, items) builds a List(string) in comptime memory — backing array of string fat pointers + the {items,len,cap} struct, all laid out from the RESULT type's field offsets/types (target-aware, no hardcoded layout). To get the result type, invoke/callCompilerFn now thread the call instruction's ins.ty (the only call-result-type need so far). Legacy (compiler_lib) handlers for these bail loudly (handleBuildPipelineQuery) — they're VM-only by nature (the post-link callback always runs on the VM since P5.1), and a List(string) isn't faithfully buildable in the legacy Value model (0141). Registered on bound_fns so weldedCompilerFn recognizes them. Smoke test examples/1662-platform-build-pipeline-queries (AOT + a 1-line C #source → exactly one C object): a post-link callback asserts c_object_paths().len == 1, items[0].len > 0, and iterates link_libraries() (liveness touch) — build exit 0 only if the VM-built list is well-formed. Negative-probe verified a real guard (forcing len != 2 → "post-link callback returned false", build exit 1). emit_object() -> string ALSO landed (same step): a QUERY, not an action — the compiler emits the object eagerly (the Zig driver, before the callback), so the primitive just returns the path from a new BuildConfig.object_path field main.zig forwards (no driver vtable needed). 1662's callback now also asserts emit_object().len > 0. So ALL THREE query primitives (emit_object/c_object_paths/link_libraries) are done; only link (the genuine ACTION) remains. No unit test for makeStringList — constructing a List(string) TypeId in the test harness needs generic instantiation; the corpus test exercises the real stdlib type end-to-end with a non-empty list + a negative guard instead. emit_object + link (the ACTIONS) deferred to P5.2b — they must replace the Zig driver's auto-emit/auto-link (not duplicate it), so they need the driver-restructuring + a host-installed callback vtable (the VM can't depend on core/main/target). 703/0 both gates + strict JIT run clean (no compiler_call bail).
  • P5.1 (= 4E) — the post-link build driver runs on the VM (NO fallback); smoke test 1661 (2026-06-19). Phase 5 step 1: core.invokeByFuncId — the post-codegen / post-link callback invocation main.zig fires after target.link — now routes the callback through the comptime VM (comptime_vm.tryEval) instead of the legacy Interpreter. REQUIRED because the sx build driver allocates/grows Lists, which the legacy interp can't do at comptime (issue 0141: struct_get: base has no fields); the VM can. NO fallback (user directive): a side-effecting post-link callback can't safely re-run on a second evaluator (double execution), so a VM bail is a HARD build error — error.ComptimeVmBail, with the reason in comptime_vm.last_bail_reason (now surfaced by main.printInterpBailDiag, which previously only read the legacy interp's last_bail_* statics). BuildConfig (&emitter.build_config) + import_sources are threaded into the VM call. Deleted the now-dead flushInterpOutput (the VM writes out directly via host-FFI — no buffer to flush). Non-empty args rejected loudly (error.ComptimeVmArgsUnsupported) — the on_build(config) arg-passing entry arrives in P5.3. Verification: a probe with a List-growing post-link callback FAILS on the pre-change legacy path (sx build exit 1, OutOfBounds (op=struct_get)) and SUCCEEDS after the change (exit 0). Formalized as examples/1661-platform-post-link-vm-list ({ "aot": true }): the callback grows a List to 3 + returns len == 3; the build links cleanly (exit 0) and the binary prints runtime main. AOT snapshots the binary's streams (build stdout discarded), so the VM-success is pinned via exit 0 + runtime main — a legacy regression would flip the build to exit 1 and mismatch. No corpus example fires post-link (none had AOT sidecars; the platform examples register a callback at #run time but run JIT) — so invokeByFuncId was previously untested by the corpus; 1661 is the first coverage. The 4 strict compiler_call bails (1609/1614/1615/1616) are UNAFFECTED — they bail at #run configure() on still-#compiler accessors (set_bundle_path etc.), killed by P5.4, not here. 702/0 both gates.
  • 4B (VM-native diagnostics) — the metatype negative tests (1179/1180) render proper diagnostics under strict; strict gap-bails now ONLY compiler_call (2026-06-19). The legacy and VM both BAIL on a define() validation failure with an identical detail string; only the host's STRICT rendering differed (generic "bailed on the VM (strict)" vs the proper "comptime type construction failed: " + span the non-strict legacy path emits). Fixed: (1) aligned the VM's define messages with the legacy's exact text — comptime define(): (was comptime define:), and the duplicate variant/field cases now NAME the offender via a new failFmt helper ('...' duplicate variant name 'value'). (2) The strict type-fn path (lower/comptime.zig) now emits d.addFmt(.err, span, "comptime type construction failed: {s}", .{vm_reason}) — the SAME diagnostic as the legacy fallback, so 1179/1180 produce their exact expected .stderr under strict with NO legacy interp involved. Left the const-init/#run strict paths on the "bailed on the VM" wrapper ON PURPOSE — they still carry genuine VM-gap bails (compiler_call), so the burndown sweep must keep distinguishing those. 701/0 both gates. STRICT GAP-BAILS NOW: only the 4 compiler_call (1609/1614/1615/1616 → Phase 5 sx-build-pipeline) + 1654 (a legitimate unresolvable-symbol diagnostic — an asm global called at comptime; the legacy can't resolve it either; reconciles to VM wording at the 4F flip). So: BuildOptions/Phase 5 is the ONLY thing between the VM and a green strict sweep.
  • out is now a PLAIN SX FUNCTION (libc write), NOT a builtin — VM handles it via host-FFI; trace_resolve ported; 0522 fixed (2026-06-19). Per user: removed the out #builtin entirely. library/modules/std/core.sx now defines libc_write :: (fd, [*]u8, usize) -> isize extern libc "write" + out :: (str: string) { libc_write(1, str.ptr, xx str.len); }. Deleted BuiltinId.out (inst.zig), the resolveBuiltin "out" mapping (call.zig), the sema builtins-list entry (sema.zig), and BOTH .out arms (interp.zig buffered-append, ops.zig LLVM write lowering). At comptime out runs through the evaluator's host-FFI (the VM's dlsym write path / the interp's extern call) — so the VM HANDLES out with NO special arm. Benign prelude .ir churn ([*]u8 interned earlier + @out@write + the out fn body) → regen'd 54 .ir snapshots (verified: only string-table renumber + the intended decl/fn-body change; zero stdout/exit changes). This UNMASKED two latent VM gaps the out-bail was hiding (the VM now runs past out): (1) trace_resolve (1035) — PORTED to the VM (comptime_vm.zig): unpack the (func_id<<32|offset) comptime frame, resolve func name + file:line:col + source line via a source_map now threaded into the VM (new tryEval param, &import_sources from emit_llvm), build the {file,line,col,func,line_text} Frame struct in comptime memory (makeStringValue/writeField/fieldOffset). (2) 0522 (bare-pack []Any) — was a CRASH (reflectArgTypeId @intCast of a garbage word) → hardened to a loud bail (typeIdxOf checked cast; the VM must never panic). ROOT CAUSE: after the 0143 fix $args materializes as []type_value (8-byte), but the example declared describe(args: []Any) (16-byte) → every element past the first read at the wrong stride; the legacy's loose Value model tolerated it, the byte-accurate VM didn't. The bare-pack elements ARE Types, so the fix is the honest type — describe(args: []Type) (output identical). Result: out/trace_resolve/the 0522 pack-reflection all run VM-HANDLED under strict (0613/1035/0522/1038 no longer bail). 701/0 BOTH gates + full suite. (Build-pipeline relevance: the sx default_build driver uses out for diagnostics — now VM-native; no compiler out builtin to special-case.) THEN interp_print_frames ported to the VM too (1034): unlike out it needs the live evaluator call-chain, so it's a VM arm (mirrors legacy printInterpFrames) — walks call_stack (skips the last frame), writes at <name> lines straight to fd 1 (consistent with out's direct write). 1034 matches; 701/0. STRICT DELETION-GATE NOW DOWN TO 7 (all known categories): compiler_call (4 — 1609/1614/1615/1616, the still-#compiler BuildOptions accessors → Phase 5 sx-build-pipeline) · VM-diagnostic negatives (2 — 1179/1180, the define bail IS the expected outcome → 4B: surface as a proper build diagnostic) · target-specific dlsym (1 — 1654, an asm global called at comptime; legacy can't resolve it either → a clean diagnostic, not a bug). EVERY pure + side-effect op bail is cleared.
  • DESIGN PIVOT (2026-06-18, user) — DRIVE THE BUILD PIPELINE FROM SX; the 37-hook BuildOptions port is dead. Trigger: porting each BuildOptions accessor to an abi(.compiler) fn that delegates to a compiler_hooks hook just re-encodes sx-level logic (setters/getters, is_macos triple-matching, list appends) as compiler hooks — they need NOTHING from the compiler but the BuildConfig state. So instead: BuildConfig becomes plain sx data (ordinary struct, sx-owned, no #compiler/hooks/shared-state/weld), and the build pipeline is an sx program — the logical end of "bundling lives in sx". The compiler shrinks to a few abi(.compiler) PRIMITIVES taking EXPLICIT args (emit_object() -> !string, link(objects, output, libs, fws, flags, target) -> !, metadata queries) + an on_build : (BuildConfig) -> ! abi(.compiler) slot (stdlib default default_build; user overrides via #run on_build = build;). Chosen boundary: Option B (compiler keeps the Zig linker; sx owns config+orchestration+bundle); Option A (sx shells cc/ld) is a later refinement. NO bool — failures are the error channel (-> !); VERIFIED on the current build: void #run, -> !/-> !E failable #run, and a raise at #run fails the build with a return trace (+ suggests #run … catch (e){…}). on_build GENERALIZES today's post_link_callback_fn (assignable typed global w/ default, vs a setter). Full design + step plan in PLAN-COMPILER-VM.md → Phase 5. S5a (below) is a green intermediate that the sx-pipeline replaces wholesale (don't extend it; P5.4 deletes build_options/set_post_link_callback + all #compiler). NEXT — P5.1 (= 4E): route the post-codegen / on_build invocation through the VM (core.invokeByFuncId → VM), REQUIRED because the sx driver allocates Lists and the legacy interp can't (0141, VERIFIED: comptime List growth works on the VM, fails on legacy with struct_get: base has no fields). Add dedicated bundle smoke tests (no corpus coverage today). Both gates 701/0.
  • S5a DONE — build_options + set_post_link_callback migrated off #compiler onto abi(.compiler); BuildConfig threaded into the VM (2026-06-18). The corpus-covered slice of the BuildOptions migration. (1) comptime_vm.zigVm.build_config: ?*BuildConfig, threaded via a new tryEval param (&self.build_config from emit_llvm's #run/const-init sites; null at lowering-time type-fn). (2) Two callCompilerFn arms: build_options (returns the null-sentinel handle) + set_post_link_callback (reads the cb func_ref, stores post_link_callback_fn on the threaded BuildConfig). (3) compiler_lib.zig — matching legacy handleBuildOptions/handleSetPostLinkCallback (gate-OFF dual path). (4) build.sxbuild_options :: () -> BuildOptions abi(.compiler); and set_post_link_callback EXTRACTED from the struct #compiler as a free ufcs (…) abi(.compiler) (so opts.set_post_link_callback(cb) still resolves via UFCS); the other ~38 BuildOptions methods stay #compiler for now. (5) Registrars/callbacks that call these are now compiler-domain: platform/bundle.sx bundle_main :: () -> bool abi(.compiler), and the six platform examples' configure/configure_build registrars marked abi(.compiler); 0602/0603 reworked the same way. KEY learning: every example transitively imports build.sx via the prelude, so the set_post_link_callback method→free-function change is BENIGN .ir churn (declaration renumber + global @str/@tag.str suffix shift) in all 37 examples that have .ir snapshots — verified line-by-line that NO instruction/control-flow/payload changed (only auto-numbered global-name suffixes), then regen'd those 37 snapshots scoped with -Dname. Strict-VM compiler_call bail set dropped 6→2: 0602/0603/1604/1611 now fully VM-HANDLED; 1609/1615 still bail on the other (still-#compiler) BuildOptions methods they use → S5b (migrate the remaining ~38 setters/getters). 701/0 BOTH gates + all unit tests.
  • S3 DONE — emit_llvm skips BODIED abi(.compiler) (compiler-domain) functions; comptime-only calls emit undef (2026-06-18). A BODIED abi(.compiler) function is a user compiler-domain function (post-link callback / compiler helper): the comptime evaluator runs its sx body, but it NEVER runs in the binary, so the backend skips it. Changes: (1) IR Function gained is_compiler_domain: bool (inst.zig). (2) decl.zig — new fnIsBodilessCompiler splits the API surface (bodiless → declare-only, compiler_welded, no implicit ctx — the S1 behavior) from a bodied abi(.compiler) function (lowers its body for VM eval; flagged is_compiler_domain + is_comptime; gets normal implicit-ctx). The four S1 guards now gate on fnIsBodilessCompiler not fd.abi == .compiler. (3) emit_llvm.zig — Pass 2 skips is_compiler_domain bodies; Pass 1 declares them EXTERNAL-linkage (an internal empty decl fails LLVM verification). (4) KEY ops.zig emitCall — a call to a comptime-only callee (compiler_welded OR is_compiler_domain) from a dead comptime body now emits undef instead of a real call; the runtime-call gate covers both. Without the undef, an AOT sx build left an undefined _double/_intern symbol — this ALSO fixed a pre-existing, untested AOT breakage of the bodiless compiler-API examples (the corpus runs them JIT). Diagnostic reworded "compiler-library" → "compiler-domain" (1185 snapshot regen'd). Regression: examples/0638-comptime-domain-fn-not-emitted (double folds a #run const → 84, absent from the binary via nm, JIT + AOT both run). 701/0 both gates + all unit tests. NEXT: S4 — an abi(.compiler) function-TYPE param (cb: () -> bool abi(.compiler)) flags the bound function compiler-domain (so a plain bundle_main :: () -> bool { … } becomes compiler-domain when passed to set_post_link_callback). Then S5 (BuildOptions migration + delete #compiler/compiler_call/compiler_hooks).
  • S1+S2 DONE — abi(.compiler) replaces abi(.zig) extern compiler + #library "compiler" (clean cutover, no legacy path) (2026-06-18). Per the design pivot below, and the user's "no legacy paths": REMOVED the .zig ABI variant entirely (ast.ABI is now { default, c, compiler, pure }) and made abi(.compiler) the sole spelling for a compiler-domain / compiler-API function — the ABI alone marks it, no extern <lib>, no fake #library "compiler". Changes: (1) ast.zig.zig.compiler (doc rewritten). (2) parser.zigparseOptionalAbi accepts .compiler (drops .zig); a bodiless abi(.compiler) decl (ends in ;, no extern) is now accepted — synthesizes the empty-block placeholder like an extern import (the Zig/VM handler is the impl). (3) decl.zigweldedCompilerFn keys off fd.abi == .compiler + export-list membership (no extern_lib == "compiler" check); a bodiless abi(.compiler) decl lowers extern-like (is_extern_decl, and the two body-lowering paths lowerFunction/lazyLowerFunction skip it) so it is declared-not-defined; funcWantsImplicitCtx returns false for abi == .compiler (an implicit __sx_ctx prepend would shift args and break the handler arity — this was the live bug surfaced + fixed). (4) type_resolver.zig — the function-TYPE CC switch handles .compiler (sx-default CC). (5) Migrated ALL 8 compiler-API examples (0626/0628/0629/0630/0631/0633 + the 1184/1185 negatives) … abi(.zig) extern compiler;… abi(.compiler); and deleted every compiler :: #library "compiler"; line; regen'd the 1184 stderr snapshot (new "not a function exported by the compiler" wording + shifted line). (6) Updated the two parser unit tests. All 8 examples run HANDLED on the strict VM with byte-correct output; 1184 (unexported name) + 1185 (runtime call) still error cleanly; gate-OFF legacy still works. 700/0 BOTH gates + all unit tests. NOTE: the general #library/extern <lib> PARSE paths stay (used by libc :: #library "c" etc.) — only the compiler-API's USE of them is gone. compiler_lib.lib_name
    • the main.zig dlopen-skip for a "compiler" lib are now dead defensive code (harmless; a #library "compiler" is just meaningless now). The struct-abi(...) parse slot is vestigial (weld stripped) — parse-only test kept. NEXT: S3 — emit_llvm skips BODIED abi(.compiler) functions (Pass 2, like is_extern); thread an abi(.compiler) flag onto the IR Function and refine the three "today every abi(.compiler) fn is bodiless" guards in decl.zig (marked with S3 NOTE) to allow a bodied callback's body to lower for VM eval while NOT emitting it. Then S4 (callback-param propagation) + S5 (BuildOptions migration).
  • DESIGN PIVOT (2026-06-18, user) — abi(.compiler) is the compiler-domain ABI; DROP the fake #library "compiler". Supersedes both the abi(.zig) extern compiler + #library "compiler" binding mechanism AND the previous "runtime-reachability gating" idea for the BuildOptions blocker (entry below). The unifying concept: a function is compiler-domain (runs in the comptime evaluator, NEVER in the shipped binary) because its ABI says soabi(.compiler) — not because it's "extern" to an imaginary library. One annotation covers BOTH roles:
    1. Compiler-API surface (intern, text_of, find_type, declare_type, register_type, build_options, set_post_link_callback, …): bodiless abi(.compiler) decls (the Zig/VM handler IS the impl). Replaces … abi(.zig) extern compiler; + the compiler :: #library "compiler"; line — both GO AWAY.
    2. User compiler-domain functions (post-link callbacks like platform.bundle.bundle_main): BODIED abi(.compiler) functions. emit_llvm does NOT lower them (skip in Pass 2, like is_extern); the comptime VM/interp evaluates them. A callback PARAM type carries it too — set_post_link_callback(self, cb: () -> bool abi(.compiler)) — so the bound function is flagged compiler-domain. Why this dissolves the BuildOptions blocker: the welded-call enforcement (ops.zig emitCall) only fired because comptime-only callback bodies (bundle_main, 0602's configure) were being LLVM-emitted. A bodied abi(.compiler) function is never emitted → its build_options()/binary_path() calls never reach emitCall as runtime code → no enforcement, no undefined-symbol risk. 1185 stays correct: main is an ordinary runtime fn (not abi(.compiler)) calling a compiler-domain fn → still a clean build-gating error. (The registrar half is independently fine via the idiomatic #run { … } block — the welded calls sit in the is_comptime __run wrapper; 0602/0603 only tripped via an intermediate configure(), a test-shape artifact.) Staged plan (each its own step, both gates green):
    • S1 — introduce abi(.compiler) as a new ABI variant that marks a function compiler_welded (export-list checked) WITHOUT requiring extern compiler/#library. Add it ALONGSIDE the existing .zig extern compiler path so migration is incremental; prove with one example (0626 → abi(.compiler)). (.zig is a misnomer — "we don't really have a zig abi"; it becomes .compiler, ultimately replacing .zig once all callers move.)
    • S2 — migrate the rest of the compiler-API decls (06280633, 1184/1185) to abi(.compiler); drop the #library "compiler" lines; regen snapshots (the 1184 unexported-name + 1185 runtime-call diagnostics must stay red with refreshed wording). Then retire the .zig extern compiler parse path + #library "compiler".
    • S3 — emit_llvm skips bodied abi(.compiler) functions (Pass 2 continue, like is_extern); thread the abi(.compiler) flag onto the IR Function. Prove a bodied compiler-domain function isn't emitted.
    • S4 — callback-param propagation: an abi(.compiler) function-type PARAM flags the bound function compiler-domain.
    • S5 — BuildOptions migration (now unblocked): build_options/set_post_link_callback/… become abi(.compiler) (+ VM callCompilerFn arms / legacy compiler_lib handlers; BuildConfig threaded into the VM — the bundler 4E shares this); callbacks declared/typed abi(.compiler); delete #compiler/compiler_call/ compiler_hooks Registry. Then 4E bundler on the VM. Reusable facts from the reverted attempt: only build.sx uses #compiler; VM dual-path bail-to-fallback means the VM needs only corpus-covered fns; UFCS on a free fn needs the ufcs marker (composes with the ABI annotation); the binding mechanism currently lives in decl.zig weldedCompilerFn (keys off extern_lib == "compiler" — S1 makes it key off abi == .compiler). Mechanism files: ast.zig (ABI enum), parser.zig (parseOptionalAbi + the extern-compiler postfix), decl.zig (weldedCompilerFn), compiler_lib.zig (export list), comptime_vm.zig (callCompilerFn), emit_llvm.zig (Pass-2 skip), ops.zig (emitCall gate).
  • Phase 4 — BuildOptions→abi(.zig) extern compiler migration ATTEMPTED, then REVERTED; BLOCKER found: the comptime-only welded-call enforcement (2026-06-18). Scoped an incremental slice (migrate only the corpus-covered build_options() + set_post_link_callback, leaving the 38 bundler accessors on #compiler → VM bails → legacy fallback). Built it end-to-end: threaded BuildConfig into the Vm (tryEval gained a ?*BuildConfig param, passed &self.build_config from emit_llvm's #run/const-init sites); added callCompilerFn arms + legacy compiler_lib bound-handlers for both; rewrote build.sx (build_optionsabi(.zig) extern compiler; extracted set_post_link_callback out of the struct #compiler as a free ufcs (...) abi(.zig) extern compiler fn so opts.set_post_link_callback(cb) still resolves via UFCS; added compiler :: #library "compiler";). All COMPILED and the welded dispatch fired. BLOCKED at LLVM emission, NOT a bug — a design limitation the migration surfaces: a compiler_welded call inside a NON-is_comptime function is a hard build-gating error (ops.zig emitCall, the Phase-1 enforcement guarding genuine runtime misuse — example 1185). But the post-link callback idiom calls comptime-only-API functions (build_options(), binary_path(), bundle_path(), …) inside callback bodies (platform/bundle.sx's bundle_main :: () -> bool, and 0602's configure) that run ONLY at comptime (post-link interp/VM) yet are still LLVM-emitted as real () -> bool bodies. The OLD #compiler/compiler_call path emitted those as dead undef (emitCompilerCall), so no error; the welded enforcement instead halts the build, and it CANNOT distinguish a dead comptime-reachable body from genuine runtime use (1185, reachable from main) without runtime-reachability analysis. Reverted the whole attempt (kept only the green pure-ops work); both gates back to 700/0. THE DECISION the next session must make FIRST (before any BuildOptions migration): how to emit a welded call in a comptime-only-but-LLVM-emitted function. Recommended path A — runtime-reachability gating: in emit_llvm, mark functions reachable from runtime roots (main / exported runtime fns); a welded call in an UNREACHABLE function emits undef (dead, like compiler_call did) instead of erroring, while a reachable one still errors (1185 stays red). This is also the right foundation for eventually NOT emitting comptime-only bodies at all. Rejected: (B) marking callbacks is_comptime — can't statically identify which func_refs become post-link callbacks; (C) blanket softening to undef — would silently swallow genuine runtime misuse (1185). Other migration facts confirmed this attempt (reuse next session): only build.sx uses #compiler (the issues/*.md hits are doc text); the VM dual-path bail-to-fallback means the VM needs only the corpus-covered fns, the 38 bundler accessors can ride legacy; UFCS on a free fn requires the ufcs marker, which composes with abi(.zig) extern compiler; build.sx must declare compiler :: #library "compiler";. Do the reachability fix as its OWN step (verify 1185 still errors + a comptime-only-body welded call now emits clean), THEN redo the BuildOptions slice on top.
  • Phase 4 burndown — three PURE comptime ops ported (error_tag_name_get + global_addr + type_is_unsigned); interp_print_frames correctly DEFERRED (2026-06-18). Also ported type_is_unsigned (a BuiltinId via callBuiltinVm): resolves the queried TypeId the same way as type_name (a .type_value word, or an Any box {tag@0,value@8} whose tag IS the boxed value's type) then returns table.isUnsignedInt(tid). Extracted the shared resolution into a reflectArgTypeId helper (VM-native Value.reflectTypeId mirror) so type_name + type_is_unsigned can't drift. MATCH-verified by a new VM unit test (type_is_unsigned(u32) - type_is_unsigned(i64) == 1). Strict sweep: 0600 type_is_unsignedout (now its only remaining bail); no type_is_unsigned bails remain in the corpus. With this, all PURE comptime ops are ported — the remaining strict bails are side-effect (out/interp_print_frames), compiler_call (the BuildOptions migration), VM diagnostics (1179/1180), and #insert/bundler. Ported two side-effect-free ops onto the VM (comptime_vm.zig exec switch): (1) error_tag_name_get — a runtime tag-id word → its name string via table.getTagName + makeStringValue (uses the table, not the module, so it's unit-testable; self.table == &module.types); (2) global_addr — name-matches __sx_default_context and returns the already-tested materializeDefaultContext Addr (an aggregate value IS its address, so a downstream load sees the materialised Context), bailing for any other global exactly like legacy. MATCH verification: error_tag_name_get locked in by a new VM unit test (tag id → "Bad", via regToValue); global_addr proven by the strict sweep (0600's first bail moved past it) and reuses materializeDefaultContext, already exercised by every implicit-ctx comptime call on the VM. KEY CORRECTION to the handover's "three PURE ops" plan: interp_print_frames (1034) is NOT pure — it WRITES the comptime call-frame chain to the build output, a side effect in the SAME bucket as out (the VM has no output buffer; output is direct-write, so a print-then-bail double-prints under the legacy fallback). It must land atomically in the FINAL out/strict-default step, NOT now. Strict-sweep burndown: 1035 error_tag_name_getout; 0600 global_addrtype_is_unsigned (a NEW pure-op bail surfaced — still a known pure op, next to port); 1034 stays at interp_print_frames (deferred, as it should). Also fixed the stale comptime_vm.zig header comment (it still said "bump/stack allocator"; the memory model is an ARENA of stable host allocations since 4D.0). 700/0 BOTH gates + all unit tests. On reify.
  • Phase 4 burndown — issue 0143 FIXED (pack-as-[]Type stride) + regression test (2026-06-18). Root cause was a stale consequence of the .type_value migration: buildPackSliceValue (lower/pack.zig) materialized a bare $<pack> []Type slice as []Any (16-byte elements) while const_type now yields an 8-byte .type_value and []Type resolves to []type_value — so 8-byte words sat in 16-byte slots and an 8-byte-stride reader got [t0, pad, t1, …]. Fixed by building the array+slice as .type_value (8 bytes). Removed the stopgap type_name .unresolved guard (its whole reason is gone; dropping it keeps any future stride bug VISIBLE as wrong output rather than a silent fallback). Sibling materialisePackSlice checked — it genuinely boxes values into []Any (correct, not the same bug). Regression test examples/0525-packs-pack-as-type-slice-arg. 700/0 both gates. 0114 (and 0521/0522/0524) now bail ONLY at out (the deferred end-state op) — the type bug is gone. issue 0143 RESOLVED.
  • Phase 4 burndown — switch_br + type_name ported; issue 0143 filed; KEY sequencing insight: out is end-state-only (2026-06-18). Ported two PURE comptime ops (379ed05): switch_br (i64-discriminant multi-way branch — enum/error tag or .type_value index) and type_name (Type value / Any box → table.typeName, with an .unresolved-bail guard). Correct in isolation; 05200524 run GREEN under strict. Two blockers found:
    1. issue 0143 (FILED, OPEN) — pack-as-[]Type stride mismatch. A ..$args pack forwarded as a []Type ARGUMENT across a call is backed by a [N x Any] (16B) array but viewed as []type_value (8B) → half-stride reads ([i64 <unresolved> string] vs legacy [i64 string bool]). A LOWERING bug the legacy's Value model masks; the byte-accurate VM exposes it. Blocks examples/0114 from running HANDLED. Per CLAUDE.md: filed, NOT worked around (the type_name .unresolved guard just makes the VM decline rather than emit garbage). Repro + fix-prompt in issues/0143-…md.
    2. out (comptime print) is an END-STATE op — it cannot land while the fallback exists. Under the legacy fallback, an eval that prints via out then BAILS double-prints (the VM wrote to fd 1, then legacy re-runs the whole eval — no rewind). 0114 demonstrated it. So a direct-write out is only safe once the fallback is GONE (strict-by-default). Revised ordering: land the PURE ops (switch_br/type_name/type_is_unsigned/error_tag_name_get/global_addr/interp_print_frames) + the BuildOptions migration + #insert + bundler FIRST; then in the FINAL step flip strict-to-default (removing the fallback) AND add out together — at which point every out-using example flips atomically with deletion. (Most of the gap-list examples print, so they stay on fallback until that final flip — that's expected, not a regression.) 699/0 both default gates.
  • Phase 4 — STRICT no-fallback mode (the interp-retirement enumeration gate) + full gap list (2026-06-18). Added -Dcomptime-flat-strict / env SX_COMPTIME_FLAT_STRICT (implies comptime_flat): at all THREE comptime sites (type-fn in lower/comptime.zig, const-init + #run in emit_llvm.zig) a VM bail becomes a build-gating error naming the reason INSTEAD of falling back to legacy. This forces every comptime eval onto the VM so the complete gap set is enumerable in one sweep; when the corpus is green under strict mode AND every example MATCHES legacy, the VM handles everything and interp.zig can be deleted (4F). Default behaviour unchanged — 699/0 both default gates. (Fixed a wiring bug: the type-fn site's local comptime_flat didn't include the strict flag, so every type-fn falsely reported <unknown>; now strict implies flat there too.) THE DELETION CHECKLIST (19 strict bails, swept via SX_COMPTIME_FLAT_STRICT=1 over examples+issues; 0103/0800 "WRONG" were false positives — raw heap-pointer addresses the corpus normalizes):
    • switch_br (5): 0114, 0521, 0522, 0524, 1035 — port the type-category multi-way branch (trivial jump). CAUTION: porting it (+type_name) UNMASKS a silent-wrong in 0114 — a []Type slice materialized when a pack ($args) is passed ACROSS A CALL reads its string element as <unresolved>. Must fix that VM pack-Type-materialization bug, not just add the op.
    • compiler_call (6): 0602, 0603, 1604, 1609, 1611, 1615 — the BuildOptions → abi(.zig) extern compiler migration (delete #compiler/compiler_call; thread BuildConfig into the VM). Big.
    • out (2): 0613, 1038 — comptime print. Direct write to fd 1, BUT only safe when the WHOLE eval is VM-handled (a print-then-bail double-prints under the legacy re-run — 0613). Flip atomically.
    • type_name (1): 0520 — reflection reader (.type_value word / Any-box tag → table.typeName).
    • global_addr (1): 0600 — only &__sx_default_context is materialised (mirror legacy).
    • interp_print_frames (1): 1034 — return-trace frame printing.
    • VM-native diagnostics (4B) (2): 1179, 1180 — NEGATIVE tests; the VM bail (define: enum has no variants / duplicate variant name) IS the expected outcome → must surface as the proper build-gating diagnostic, not the generic strict error.
    • dlsym not found (1): 1654 — a target-specific extern (asm global) called at comptime; likely a legitimately-unresolvable case → confirm it stays a clean diagnostic. Sweep command: SX_COMPTIME_FLAT_STRICT=1 ./zig-out/bin/sx run <ex> per example, diff vs legacy; a strict bail prints ... bailed on the VM (strict, no fallback): <reason>.
  • Phase 4D.2 (VM plan) — extern SLICE/string args (→ NUL-terminated char*) + float guards (2026-06-18). Extracted marshalExternArg: a scalar/pointer WORD passes verbatim (a cstring arg already works as a pointer word via 4D.1); a string/slice {ptr,len} fat pointer is copied into a NUL-terminated arena buffer and its char* passed (mirrors legacy marshalExternArg — what the bundler's popen(cmd: [:0]u8, …) needs). Added FLOAT guards on args AND returns: floats are kindOf == .word but the host_ffi trampolines have no float variant, so they bail loudly rather than miscall through an integer register (the legacy interp doesn't support float FFI either, so parity holds — no corpus float-FFI example exists). New example 0637-comptime-extern-slice-arg (#run strlen("hello, world") with a [:0]u8 param → 12) runs HANDLED on the VM, byte-matching legacy. 699/0 BOTH gates. On reify. The FFI escape is now complete for scalar/pointer/cstring/ slice args + scalar/pointer returns — enough for the bundler's libc surface. Next (4D.3): compiler_call (#compiler hooks — 0602/0603), the last legacy-only role besides #insert/bundler.
  • Phase 4D.1 (VM plan) — general host-FFI escape: the VM calls any extern libc fn via dlsym + host_ffi (2026-06-18). Replaced the "extern not ported → bail" stub in Vm.invoke with callHostExtern: resolve the symbol via host_ffi.lookupSymbol (dlsym RTLD_DEFAULT) and dispatch through the host_ffi trampolines, exactly like the legacy interp.callExtern. Marshalling is now trivial because Addr is a real host pointer (4D.0): every WORD-kind arg passes as usize verbatim — a scalar's bits OR a pointer, no translation — and a pointer return is a valid Addr. Picks callPtrRet (void*-ABI) for pointer-ish returns, callIntRet (i64-ABI) otherwise; honors variadic (is_variadic and args > fixed). Non-word (aggregate/string/float) args+returns bail loudly (no silent miscall — 4D.2 adds NUL-term cstring marshalling + float). NOT per-builtin: ONE general mechanism for all externs. New example 0636-comptime-extern-libc (#run toupper(97)/ tolower(90) fold to 65/122) runs HANDLED on the VM, output byte-matching legacy. (abs doesn't dlsym-resolve on macOS — a compiler builtin — and the VM fails identically to legacy, confirming parity.) 698/0 BOTH gates (one new example). On reify. Next (4D.2): string/aggregate extern args (string→NUL-term cstring) + float args/returns, then compiler_call (#compiler hooks, 4D.3).
  • Phase 4D.0 (VM plan) — comptime VM memory = an ARENA of stable host allocations; Addr = real host pointer (2026-06-18). Replaced the growable ArrayList(u8) flat buffer (which reallocs/MOVES on growth) with a std.heap.ArenaAllocator: each allocBytes is a separate arena allocation that never moves and is freed wholesale on deinit (no per-object free, no cap, no fixed buffer). Addr is now the allocation's absolute host pointer (@intFromPtr), not an offset — so a comptime pointer and an FFI-returned host pointer are the SAME kind of value, and the FFI bridge (4D.1) can pass them to/from libc with ZERO translation and no per-call pinning (the original moving-buffer hazard is gone by construction). Machine.readWord/writeWord/bytes deref the absolute pointer directly, keeping the null-check bail (the malformed-IR / null-deref safety contract). Dropped the offset-based upper-bounds check (can't bound an absolute pointer; the Frame.bad_ref guard still catches the dominant malformed-IR vector) and the test-only mark/reset (the arena has no cheap reset-to-mark; the VM never used them outside tests). Decision rationale (user): use a GPA-like allocator, no artificial buffer limits. 697/0 BOTH gates + all unit tests (rewrote the two Machine tests: null-deref bail + arena-stability-across-grows). Pure refactor, no comptime behavior change. Next (4D.1): extern-call dispatch in Vm.invoke — marshal args (scalars by value, pointers as the host pointer they already are), call via host_ffi trampolines, return scalars/pointers; a new #run libc example as the corpus guard.
  • Phase 4A.1 (VM plan) — box_any/unbox_any on the VM + .any as a 16-byte aggregate (2026-06-18). Ported the Any-boxing conversion pair: box_any allocates the 16-byte { type_tag@0, value@8 } box (tag = source TypeId index, matching the legacy comptime interp), writing a word source's scalar via writeField(source_type) (so f32 round-trips) or an aggregate source's comptime ADDR (the runtime pointer-in-value-slot shape); unbox_any reads the value slot back (word → readField, aggregate → the stored ADDR). Required making .any a first-class comptime aggregate (it was kindOf → .unsupported): kindOf(.any) = .aggregate (16B, by-address) + fieldOffset special-cases .any to the {@0, @8} layout (shared with string/slice) — without the latter, a struct_get on an Any panicked (union field 'struct' while 'any' is active), caught + fixed (no crash; "never crash" upheld). Updated two unit tests that used unbox_any as the "unported op" example → now compiler_call; added a box→unbox round-trip test. 697/0 BOTH gates + all unit tests. On reify. The 6 box_any examples (0114/05200524/1035) no longer bail at box_any and produce VM output byte-matching legacy, but are not YET fully HANDLED — they now fall back further at switch_br (comptime Any-tag type-switch), type_name, and out/print (4A.2+/later steps). Next (4A.2): comptime out/print (VM output buffer + flush).
  • Phase 3 P3.4 step 8 (VM plan) — VM-native type_info REFLECTION → the whole metatype surface is HANDLED (2026-06-18). Ported type_info($T) into the VM (callBuiltinVm .type_info arm → new buildTypeInfo), the inverse of step 7's define: reflect a type INTO a TypeInfo VALUE built in FLAT MEMORY (the VM-native mirror of legacy reflectTypeInfo). Decodes the source type into a tag + members (tagged-union/struct field & enum variant → { name, ty }, a payloadless variant → void; tuple → bare positional Types), then lays out the nested value bottom-up using layouts derived from the TypeInfo RESULT type (ins.ty, now threaded into callBuiltinVm): element array → {ptr,len} slice → info struct (EnumInfo/StructInfo/TupleInfo) → TypeInfo { tag, payload } tagged union (reusing step 7's tagged-union write). Variant/field names materialize via a makeStringValue helper extracted from text_of. Same backing_type guard as step 7. Result: the ENTIRE metatype surface runs HANDLED on the VM with ZERO fallback06140624 + 0632 (0616 field_type folds at lower time, no comptime eval); the define(declare, type_info(T)) round-trips (0619/0622/0623) mint byte-identical copies on the VM. VM output byte-matches legacy for all. 697/0 BOTH gates + all unit tests. On reify. Remaining VM fallbacks in the comptime corpus are now genuinely-non-metatype emit-time side effects: print/out (0613), global_addr (0600), compiler_call #compiler hooks (0602/0603), and the inline-asm global (1654). Next: port those (or confirm each is a legitimately-non-comptime case) to drive the fallback list to empty, then — with user go-ahead — flip the VM to default + delete interp.zig.
  • Phase 3 P3.4 step 7 (VM plan) — VM-native metatype CONSTRUCTION: declare/define + tagged-union enum_init (2026-06-18). Ported the metatype type-CONSTRUCTION builtins into the VM so the construction examples run HANDLED end-to-end (no call_builtin fallback). Three pieces: (1) tagged-union enum_init with payload — the arm previously bailed; now allocates the value (zeroed), writes the tag at offset 0 ({ header(tag)@0, [N x i8] payload@tag_size }, the LLVM backend/llvm/types.zig layout) and copies the payload at tag_size. (2) A .call_builtin exec arm → new callBuiltinVm, the VM-native mirror of the legacy execBuiltinInner: declare(name) mints an empty forward nominal slot (shared declareNominal helper, also used by declare_type); define(handle, info) reads the TypeInfo tagged-union VALUE from FLAT MEMORY (tag@0, active payload EnumInfo/StructInfo/TupleInfo struct at tag_size, its single slice field) and mints via defineFromInfo, a faithful port of legacy defineEnum/defineStruct/defineTuple (all-void enum → real .@"enum" per issue 0142, dup-name rejection, updatePreservingKey vs replaceKeyedInfo). (3) Refactored the []{name,ty} decode out of registerTypeVm into a shared decodeMemberSlice (+ decodeTypeSlice for bare-Type tuple elements), keyed to the module-level NamedMember. Unmodeled builtins (type_info/type_name/…) return null → bail with the builtin name → legacy fallback (dual-path parity). Correctness guard (caught via review): enum_init/define assume a tag-headed layout, which is WRONG for a backing_type tagged union (laid out as the backing struct) — both now bail loudly on backing_type != null rather than silent-clobber. Result: examples 0614/0620/0621/0624/0632 run fully HANDLED on the VM (define is the whole eval); 0622/0623 run define HANDLED then fall back cleanly at the still-unported type_info reflection. VM output byte-matches legacy for all 7. 697/0 BOTH gates + all unit tests (added: tagged-union enum_init payload layout). On reify. Next: port type_info (REFLECT a type → build a TypeInfo value in comptime memory, the inverse — reuses the tagged-union enum_init write) so 0619/0622/0623 go fully HANDLED; then the rest of the comptime corpus (drive the SX_COMPTIME_FLAT_TRACE fallback list toward the genuinely-non-comptime cases) before the VM-default flip + legacy deletion.
  • Phase 3 P3.4 step 6 (VM plan) — REAL lowering-time Context: allocating + List-building type-fns now run HANDLED on the VM (2026-06-18). The VM can now evaluate a comptime type-fn that ALLOCATES at lowering time (the 0141 family) — the legacy interp cannot. Four changes: (1) runComptimeTypeFunc (lower/comptime.zig) FORCES the CAllocator→Allocator thunks to exist (getOrCreateThunks, idempotent, guarded by Allocator/ CAllocator registered) BEFORE eval — a type-fn const runs at scanDecls (Pass 1), before Pass 1c builds the default-context global + thunks, so the comptime allocator was otherwise null; (2) materializeDefaultContext builds a REAL context at lowering time when the global is absent — finds the two thunks by name (findFuncByName) and lays their func-refs into the inline Allocator value {ctx=null, alloc_fn@+ptr, dealloc_fn@+2*ptr} at the head of Context, so context.allocator.alloc_bytes dispatches call_indirect → thunk → native VM malloc; (3) aggType now DEREFS a pointer base_type (the List write path emits struct_gep with base_type = *StructfieldOffset panicked on the pointer; now derefs to the pointee, no panic); (4) subslice handles a [*]T many-pointer / *T base (a List's items field — the base IS the data pointer). Verified end-to-end (manual probe): a compiler-API type-fn that builds its []Member in a List(Member) (.append ×3, then register_type(handle, kind, vs.items[0..vs.len])) runs HANDLED on the VM and mints correctly (green=7) — the exact 0141 List-growth pattern, on the VM. Can't be a corpus test yet (gate-OFF/legacy still can't allocate at lowering time — the dual-path bind), so locked in via VM unit tests instead (many-pointer subslice; struct_gep with a pointer base_type). 697/0 BOTH gates + all unit tests, EXIT=0. On reify. Remaining for the original 0141 repro (uses metatype define/ make_enumcall_builtin → legacy fallback → legacy fails): re-express the metatype over the compiler-API so the whole type-fn runs on the VM (no call_builtin). THEN the repro works on the VM — and the dual-path bind resolves only at the VM-default-flip + legacy-deletion end-state.
  • Phase 3 P3.4 — investigation: the "real lowering-time Context" is BLOCKED by issue 0141 (2026-06-18). Probed whether the VM needs a REAL lowering-time Context (CAllocator thunk func-refs) for allocating type-fns. Finding: lowering-time comptime ALLOCATION fails in the LEGACY interp too — a type-fn that calls context.allocator.alloc_bytes at lowering time bails in legacy with comptime call_indirect: callee is not a func_ref Value (raw fn-pointers from extern calls aren't dispatchable in interp), and the VM bails at parity (call_indirect through a null function pointer). This is exactly issue 0141's root cause (its analysis already notes "the null allocator is the same story for the CAllocator thunks") — an OPEN deferred issue. So: (1) the VM is CORRECT (parity — both bail; no regression); (2) the real-context work is PREMATURE — its only consumer (allocating lowering-time type-fns) can't pass gate-OFF, so no corpus test can validate it, and even a more-capable VM can't ship a divergence during the dual-path phase. Consequence for the metatype re-expression: re-expressing define/make_enum over the compiler-API needs to BUILD []Member slices dynamically (allocation) — which is blocked by 0141 at lowering time. The viable paths are: (a) avoid allocation by passing the caller's existing slice through (needs EnumVariant/StructField to be usable AS Member — they're layout-identical {string, Type}, but distinct nominal types — a metatype-API decision), or (b) wait for 0141. No code change this step (the VM already bails correctly). Recorded so the next session doesn't re-derive it. 697/0 both gates unchanged.
  • Phase 3 P3.4 step 5 (VM plan) — WRITE side ported to the VM → FIRST HANDLED lowering-time type-fns (2026-06-18). Ported declare_type / pointer_to / register_type into Vm.callCompilerFn, mirroring the legacy compiler_lib handlers (mint via @constCast(table) — the same mutable access the read-side intern uses; the lowering-time mint target IS &module.types). register_type reads the []Member slice from FLAT MEMORY: threaded ref_types through invokecallCompilerFn so the slice's element type (Member = {name: string, ty: Type}) gives the field offsets + stride; decodes each {name, ty} and branches on kind (1 struct · 2 enum · 3 tagged_union · 4 tuple) exactly as legacy (dup-name / payload-on-enum rejections, idempotent re-fill via nominalIdentOf). Key unblock: the synthesized comptime type-fn wrapper (createComptimeFunction/…WithPrelude) was built with return type .anyregToValue bailed at the VM↔legacy boundary; changed to .type_value (the legacy path reads via asTypeId regardless, so no legacy change). Result: the compiler-API write type-fns now run HANDLED end-to-end on the VM at LOWERING time0631 (register-graph: 2 HANDLED, A↔B cycle via forward handles + pointer_to) and 0635 (multi-edge import: 2 HANDLED), parity-correct. They run on the ZEROED lowering-time context (fixed .[…] member arrays, no allocation). The metatype make_enum/define examples (0632) still fall back CLEANLY through call_builtin(define) (the separate metatype path — re-expressing it onto the compiler-API is the other half of P3.4). 697/0 BOTH gates + EXIT=0. On reify. Next: (optional, deferred) a REAL lowering-time Context (CAllocator thunk func-refs) for List-growing type-fns; and re-express the metatype define/make_enum over the compiler-API to delete the bespoke interp arms (the end-state: ONE evaluator).
  • Phase 3 P3.4 step 4 (VM plan) — model .type_value natively in the comptime VM (2026-06-18). The VM now HANDLES Type values instead of bailing: kindOf(.type_value).word; a new const_type exec arm → the word TypeId.index(); regToValue maps a .type_value word back to a .type_tag Value at the legacy boundary (valueToReg already mapped .type_tag → index). Surfaced + fixed a VM PANIC (forbidden): struct_init assumed a .@"struct" result type and union-access-panicked on an ARRAY literal (EnumVariant.[ … ], reached now that Type args no longer bail early) — it's the generic aggregate-literal op, so it now dispatches on the result kind (struct / array / tuple) and BAILS loudly on anything else, never panics. 697/0 both gates (the make_enum type-fns now run further on the VM, then bail cleanly at the define/make_enum call_builtin → legacy mints — no mutation before the bail, parity holds). VM unit test added (const_type → word → regToValue → .type_tag). On reify. Next (the payoff): port the WRITE side (declare_type / register_type / pointer_to) into Vm.callCompilerFn + give the lowering-time path a REAL Context (CAllocator thunk func-refs, not zeroed) → the first HANDLED lowering-time type-fn end-to-end on the VM.
  • Phase 3 P3.4 step 3 (VM plan) — dedicated Type builtin TypeId: RESOLVER FLIPPED + .any migration (2026-06-18). Flipped type_resolver:64 ("Type".type_value), module.zig constType (result type → .type_value), and emitConstType (a bare i64 carrying tid.index(), NOT a 16-byte Any box). Then migrated every .any reference that means "a Type value", classified per CLAUDE.md (leave the real boxed-Any refs): (a) the "Any holds a Type" meta-marker tag moved .any.type_value at all 4 consumers — reflectArgTypeId (LLVM), reflectTypeId + the .type_tag-as-struct-field comptime path (interp), and resolveTypeCategoryTags("type") (generic.zig); (b) reflection-builtin RETURN types .any.type_value (type_of/declare/ define); the runtime type_of(any) now reads the tag AS a .type_value (no re-box); (c) expr_typer infers a bare type-name expr as .type_value (with a is_raw backtick exemption — `string is a value, never the reserved type); (d) reflectionArgIsType accepts .type_value OR .any (a reflection arg can be a bare Type OR a boxed Any — the over-narrow ==.type_value was the catastrophic-regression cause, caught + fixed); (e) the comptime switch_br accepts a .type_tag discriminant (type-category match); (f) a bare function name in a Type slot now lowers to const_type(its real function type) instead of a func-ref (fixed a JIT crash — was a func-ref word read as a TypeId), keeping the old string-box path only for genuine Any params; (g) the field-not-found diagnostic + formatTypeName render .type_value as "Type". Fixed 3 unit tests asserting the old .any Type behavior. 697/0 BOTH gates + all 494 unit tests (EXIT=0). Gate ON stays green because the VM's kindOf(.type_value).unsupported → bails CLEANLY to legacy (no silent-wrong) — the VM doesn't model Type values YET (next step), but parity holds. Regenerated 24 snapshots (22 .ir const_type-shape; 2 .stderr Any→Type — diff reviewed, only the intended changes). On reify. Next: model .type_value natively in the VM (kindOf → word, const_type → word = TypeId.index(), regToValue word → .type_tag) for COVERAGE, then port the WRITE side into callCompilerFn + a real lowering-time Context → the first HANDLED lowering-time type-fn.
  • Phase 3 P3.4 step 2 (VM plan) — dedicated Type builtin TypeId: FOUNDATION landed (dead/additive) (2026-06-18). Added TypeId.type_value (slot 19) + a matching TypeInfo.type_value variant + the builtins init entry — an 8-byte type handle distinct from the 16-byte boxed .any (THE WALL). All types.zig layout handlers wired: sizeOf/typeSizeBytes → 8, typeAlignBytes → 8, typeName → "Type", hashTypeInfo/typeInfoEql no-payload arms. Only ONE exhaustive switch needed a new arm (backend/llvm/types.zig toLLVMTypeInfocached_i64); every other switch(TypeInfo) site has an else (audited when the resolver flips). first_user 19 → 100 (per the user): slots 2099 are RESERVED builtin headroom (infos padded with the unresolved tripwire), so future builtins don't renumber user TypeIds / churn sx ir snapshots. Cost: ~80 default entries in each binary's per-type reflection arrays (user opted in). Still dead: type_resolver.zig:64 STILL returns .any for "Type" — nothing produces .type_value yet, so NO behavior change. Regenerated 22 IR snapshots (pure TypeId renumber to 100-base; git diff --name-only confirmed ONLY .ir files + the 2 source files changed — no stdout/stderr/exit). 697/0 both gates (OFF and -Dcomptime-flat). Next: flip type_resolver:64.type_value, then migrate the .any refs that mean "a Type value" (const_type result / reflection returns / metatype Type params / .type_tag checks) — leave the real boxed-Any refs — file-by-file with a build after each.
  • Phase 3 P3.4 step 1 (VM plan) — lowering-time default context; first blocker cleared (2026-06-18). materializeDefaultContext now falls back to a ZEROED Context (found by name) when the __sx_default_context global is absent — i.e. at LOWERING time, where the global isn't emitted yet. A type-fn that never touches the allocator now runs past context setup; one that allocates reads a null alloc_fn (zeroed) → call_indirect on the null func-ref bails → legacy fallback (a REAL lowering-time context with the CAllocator thunk func-refs, so allocating type-fns also run on the VM, is a follow-up). Measurement: the bail moved deeper — metatype make_enum now bails at const_type (the Type-literal op, unported); register_type type-fns bail at the welded write call (declare_type/register_type aren't in callCompilerFn). No table mutation happens before either bail (the write fns bail before minting), so parity holds: both gates 697/0, no crashes. Next blockers (the "model Type" chunk): (a) the const_type op → a word = TypeId.index(); (b) the Type-return bridge (regToValue for a Type/.any word → .type_tag); (c) the VM-native write side (declare_type/register_type/pointer_to in callCompilerFn) + a real lowering-time context. Only once those land does a type-fn actually run end-to-end on the VM (a HANDLED case).
  • Phase 3 P3.4 (VM plan) — wire the VM at the LOWERING-time site + measure (2026-06-18). Routed runComptimeTypeFunc (the type-fn fold — the THIRD comptime call site) through comptime_vm.tryEval behind -Dcomptime-flat/SX_COMPTIME_FLAT with legacy fallback, mirroring the two emit-time folds. Extracted the shared post-check (checkComptimeTypeResult — the declared-but-never-defined zero-field guard) so both paths use it. Measurement (SX_COMPTIME_FLAT_TRACE): every metatype/compiler-API type-fn currently bails CLEANLY with no __sx_default_context global to materialize the implicit context — at lowering time the default-context global doesn't exist yet (it's built at emit time), so the VM bails at context materialization, BEFORE running the body (no partial mint, no crash → legacy mints). The hardening holds: no crashes across the corpus on the VM lowering-time path. Both gates 697/0. So the FIRST lowering-time blocker is the implicit context, not Type modeling — the VM needs a way to materialize/skip the default context at lowering time (most type-fns get an implicit ctx for potential List-growth alloc; many don't use it). Next: materialize a lowering-time default context for the VM (or pass a null ctx + bail only if the allocator is actually used), THEN model Type values + the VM-native write side. This is near-pure fallback today — permanent scaffolding that lights up as those land.
  • Phase 3 P3.4-prep (VM plan) — harden the VM against malformed lowering-time IR (2026-06-18). Prerequisite for wiring the VM at the LOWERING-time comptime site (runComptimeTypeFunc), where IR can be malformed (an unresolved name lowers to a dangling / Ref.none operand — the 0737 crash). Closed the remaining panic vectors so the VM BAILS (→ legacy fallback) instead of aborting: (1) a checked Vm.refTy(ref_types, r) replaces every raw ref_types[ref.index()] in exec (the type-side companion to Frame.get's bad_ref value-side guard); (2) aggType is now a bailing method (Error!TypeId) using refTy; (3) the block-dispatch loop bounds-checks the branch target before indexing func.blocks.items. global_get was already guarded. No behavior change — gate OFF and ON both 697/0; unit test added (a cmp_lt with a Ref.none operand bails, not panics). Next: wire tryEval into runComptimeTypeFunc behind the flag with legacy fallback and measure (most minting type-fns will still bail at the welded-write call / Type-result conversion until the VM models Type values + the VM-native write side land — those are the steps that actually move lowering-time comptime onto the VM, toward deleting legacy).
  • Phase 3 P3.3 (VM plan) — WRITE side: declare_type + pointer_to + ONE kind-branching register_type (2026-06-18). The mutating compiler-API: declare_type(name) -> Type (forward handle), pointer_to(t) -> Type (build *T), and register_type(handle, kind, members: []Member) -> Type which branches on kind IN THE COMPILER (subsuming define's per-kind dispatch). Take/return real Type values (matching meta.sx declare/define). Timing (per user): mint LAZILY at lowering time, single pass (the existing runComptimeTypeFunc), so the write side is legacy-only (compiler_lib handlers) — the VM isn't wired at lowering time, no VM mirror needed; readers stay dual-path. A non-generic -> Type builder is now flagged is_comptime (decl.zig) so its dead body permits the welded calls. Graph: forward handles + pointer_to express mutually-recursive A↔B (*A, *B, B-by-value); register_type is idempotent (re-fill a nominal slot reached via two import edges — nominalIdent). kind codes match type_kind (1 struct · 2 actual .@"enum" · 3 tagged_union · 4 tuple). Fixed two bugs (issue 0142): (a) a fully payloadless minted enum was an all-void tagged_union → verifySizes panic; now a real .@"enum" (register_type kind 2 AND metatype defineEnum); (b) bare EnumType.variant payloadless qualified construction wasn't supported (failed for hand-written enums too) — added in lowerFieldAccess (isPayloadlessVariant). Examples 0631 (graph + actual enum + reflection), 0632 (make_enum all-void), 0633/0634/0635 (namespaced / bare / multi-edge import of a minted type), 0187 (qualified variant construction). Parity 697/697 (gate ON and OFF); unit tests added. Next (P3.4): re-express declare/define/type_info as sx over the compiler-API + delete the bespoke interp arms (needs the VM hardened for lowering-time IR, or the metatype migrated onto the legacy compiler-API calls).
  • Phase 3 P3.2b (VM plan) — kind + enum-value readers: type_kind + type_field_value; READ side complete (2026-06-18). The last two read-only readers the metatype's type_info(T) needs (added to compiler_lib.bound_fns AND Vm.callCompilerFn, each backed by a TypeTable query both call): type_kind(t) -> i64 (kindCode — a stable, compiler-owned discriminant: 0 other · 1 struct · 2 enum · 3 tagged_union · 4 tuple · 5 union · 6 array · 7 vector · 8 error_set; TOTAL, never bails) and type_field_value(t, idx) -> i64 (memberValue — an enum variant's explicit value or ordinal; mirrors the field_value_int builtin; loud-bail for non-enum / out-of-range). Example 0630-comptime-compiler-type-kind reflects Color / WindowFlags (flags) / Point. The READ side is now COMPLETEfind_type + type_kind + type_field_count + type_field_name/type_field_type/type_nominal_name + type_field_value cover everything reflectTypeInfo reads. VM unit test added. Parity 691/691 (gate ON and OFF). Revised forward direction (per the user): the WRITE side is ONE register_type(info) fn that branches on the kind IN THE COMPILER (subsuming define's per-kind dispatch), not a per-kind register_struct.
  • Phase 3 P3.2 (VM plan) — field-level reflection readers: type_nominal_name + type_field_name + type_field_type (2026-06-18). Three more compiler-library readers on the same TypeId-handle shape (added to compiler_lib.bound_fns AND Vm.callCompilerFn), each backed by a new TypeTable query BOTH paths call (no drift): nominalName (a named type's own name handle; loud-bail for unnamed types like i64/pointers), memberName (struct/union/tagged-union field, enum variant, named-tuple element), memberType (struct/tuple/array/vector member type). All loud-bail on out-of-range idx / no-member (no silent default). First MULTI-ARG compiler fns — callCompilerFn reads arg 1 = idx; added Vm.argHandle/argTypeId (range-checked u32/TypeId arg reads) and refactored find_type/type_field_count onto them. Named type_* to avoid clashing with the std metatype builtins (field_name/type_name exist in core.sx); nominalName (the TypeTable method) is distinct from the existing typeName(id) []const u8 display-string renderer. Example 0629-comptime-compiler-field-reflect reflects Pair { lo: Point; hi: Point } — each field name + the nominal name of a field's type, all #run-folded, all VM-HANDLED natively. VM unit test added (type_field_name → "hi"; type_nominal_name(type_field_type(Pair,0)) → "Point"). Parity 690/690 (gate ON and OFF).
  • Phase 3 P3.1 (VM plan) — first read-only reflection readers: find_type + type_field_count (2026-06-18). Two more compiler-library fns, bound the same way as the intern/text_of seed (added to compiler_lib.bound_fns for the legacy handler + the welded-decl export check, AND to Vm.callCompilerFn for the native comptime path — NO marshaling). A type handle is a plain u32 TypeId (like StringId), so both keep the seed's clean scalar shape: find_type(name: StringId) -> TypeId (TypeTable.findByName, unresolved/0 if absent) and type_field_count(t: TypeId) -> i64 (a NEW TypeTable.memberCount query — struct/union/ tagged-union fields, enum variants, array/vector length — called by BOTH paths so they can't drift; bails loudly, never a silent 0). New example 0628-comptime-compiler-find-type chains intern → find_type → type_field_count (and a not-found lookup → 0), both folded at #run, both VM-HANDLED natively (trace confirms no fallback). VM unit test added (find_type + type_field_count, struct found → 3 fields, missing → unresolved). Parity 689/689 (gate ON and OFF). Decision (resolves the plan's find_type → ?Type sketch): return a NON-optional TypeId with the unresolved (0) sentinel for not-found, NOT ?Type — a Type value resolves to .any (which the comptime VM doesn't represent) and an optional can't cross the legacy↔VM eval boundary; unresolved is the project-blessed unmistakable "no type" marker. Forward (P3.2): more readers on the same handle shape (type_name/field_name/field_type/kind), then register_struct (first mutating fn).
  • VM robustness — Frame bounds-check; lowering-time #insert wiring explored + reverted (2026-06-18). Explored wiring the VM at the LOWERING-time comptime site (evalComptimeString, the #insert string fold). 12/13 #insert examples ran on the VM with parity, but 0737 (an #insert of an unresolved secret()) CRASHED the VM (SIGABRT): lowering-time IR can be malformed (a ret Ref.none from the unresolved name) and Frame.get panicked on the out-of-range index. Decision: reverted the lowering-time wiring — unlike the emit-time folds (fully lowered IR), lowering-time IR can be erroneous, and hardening the VM against ALL malformed IR (every ref_types[...] / aggType access, not just Frame) is out of scope here. The emit-time sites already give full corpus coverage. KEPT the defensive fix regardless (CLAUDE.md "never crash"): Frame.get/set now bounds-check and flip a bad_ref flag; the run loop bails (badRef) instead of panicking. Unit test added (malformed ret Ref.none → bail, not crash). Parity 688/688 both ways.
  • Phase 3 SEED (VM plan) — compiler-call path: intern/text_of native on the VM (2026-06-18). invoke now dispatches a welded compiler-library fn (gated on compiler_welded) to Vm.callCompilerFn, serviced NATIVELY on comptime memory (no legacy Interpreter): intern(string)->StringId reads the comptime string bytes and internStrings into the const-cast table (pool-only — doesn't touch type layout, so cached sizes stay valid); text_of(StringId)->string materializes the pooled text back into comptime memory. Unlocked 0626; the ONLY remaining const-init fallback is now the inline-asm global (1654). Parity 688/688 (gate ON and OFF); unit test added. This is the mechanism Phase 3 grows — the next compiler fns (find_type, register_struct, reflection readers) bind the same way (comptime pointer in, handle/pointer out, no marshaling).
  • Phase 1.final step 9 (VM plan) — -Dcomptime-flat build flag (the "swap behind a build flag" step) (2026-06-18). Added the -Dcomptime-flat build option (build.zig → a build_opts options module on mod; emit_llvm.init reads build_opts.comptime_flat or SX_COMPTIME_FLAT env). This is the plan's "reach parity → swap behind a build flag → delete the old path" mechanism. zig build test -Dcomptime-flat runs the FULL corpus on the VM (688/0). Verified the flag toggles the binary: flag-built sx reports VM HANDLED with no env var; default-built does not. Default OFF — zig build test unchanged (688/0). Env var still works for ad-hoc runs. Next (forward): Phase 2 (bytecode) / Phase 3 (compiler-API on comptime memory); eventual default-flip + legacy deletion.
  • Phase 1.final step 8 (VM plan) — wire the #run side-effect path + trace-clear-on-fallback (2026-06-18). Wired the SECOND comptime call site (runComptimeSideEffects, top-level #run <expr>;) through tryEval with legacy fallback, mirroring the const-init fold. tryEval now handles void/noreturn entries (→ .void_val) so a void side-effect doesn't bail at the result conversion. Fixed a trace-corruption the new site exposed (1035): a side-effect that pushes return-trace frames and then bails (e.g. on print) had the legacy re-run DOUBLE-push them (sx_trace_push is a side effect on the shared buffer). Both wiring sites now sx_trace_clear() right before the legacy fallback, discarding the VM's partial pushes. Parity 688/688 (gate ON and OFF). Most side-effects still bail (print/global_addr/call_builtin) → legacy, but the path is now uniform. All comptime evaluation routes through the VM-with-fallback.
  • Phase 1.final step 7 (VM plan) — is_comptime + failable/error cluster + signed-load fix; coverage 31→36 (2026-06-18). is_comptime → 1 (unlocked 1030). Ported the failable/error-channel cluster (1037 escape, 1038 handled): kindOf(error_set)→word, regToValue bridges TUPLES (the failable (value…,tag) shape checkComptimeFailable reads), trace_frame packs (func_id<<32|span.start) from a new call_stack (pushed by invoke/runEntry), and sx_trace_push/sx_trace_clear serviced NATIVELY (the VM calls the real sx_trace.c functions linked into the compiler, so the return-trace buffer is populated identically to legacy). raise/catch/or now run on the VM. Surfaced + fixed a real GENERAL bug: readField was ZERO-extending signed sub-64-bit loads, so a stored i32 -1 reloaded as 0xFFFFFFFF (+4.29e9) and < 0 was false — silently hiding raise error.Bad; now SIGN-extends i8/i16/i32/isize (gate-ON parity confirms it's a strict fix; unit test added). VM HANDLES 36 corpus const-inits (was 31); parity 688/688 (gate ON and OFF). Only 2 fallbacks remain, both principled: intern (0626, welded compiler-API fn — Phase 3) + inline-asm global (1654). Forward work: Phase 2 (bytecode), Phase 3 (compiler-API on comptime memory).
  • Phase 1.final step 6 (VM plan) — real default context + call_indirect + func_ref + global_get; coverage 27→31 (2026-06-17). Per the user's direction ("the VM can set up a default context"), runEntry now materializes the REAL default context instead of a zeroed one. The implicit-ctx param is an opaque *void, so materializeDefaultContext finds the __sx_default_context global and lays its initializer ({ {null, alloc_fn, dealloc_fn}, null }, the CAllocator thunk func-refs) into comptime memory via a new recursive layoutConst. With func_ref (function value encoded FuncId.index()+1, reserving word 0 for the null fn-ptr) and call_indirect (decode word → FuncId → dispatch; 0 → bail) ported, the whole allocator protocol runs on the VM: context.allocator.alloc_bytes → call_indirect → thunk → CAllocator.alloc_byteslibc_malloc → native comptime malloc. Unlocked 0606 (string global). Also: global_get lazily evaluates a comptime global's comptime_func (memoized) — unlocked CT_CHAIN; field access (fieldOffset/struct_get) handles string/slice {ptr@0,len@8} fat pointers (needed by alloc_string); regToValue maps function-typed words → .func_ref (kept 1128's rejection byte-identical). Native malloc is still required (the thunk bottoms out at it; a host pointer can't be used with comptime load/store). VM HANDLES 31 corpus const-inits (was 27); parity 688/688 (gate ON and OFF). Unit tests: global_get, func_ref+call_indirect. Remaining fallbacks (7): .unsupported aggregates (3×1037/1038), extern/builtin intern+asm (2×), trace_frame, is_comptime.
  • Phase 1.final step 5 cont. (VM plan) — libc memory builtins + f32 fix; coverage 16→27 (2026-06-17). Identified the dominant fallback (call to extern/builtin) as 11× malloc (0604) + 1× intern. Modeled a curated set of libc MEMORY builtins natively on comptime memory (Vm.callMemBuiltin): malloc/callocallocBytes (16-aligned, 256-MiB cap → bail), free → no-op, memcpy/memmove/memset on comptime bytes — sandboxed (no host heap/dlsym), target-aware; the computed result is byte-identical to legacy (which calls real libc). This surfaced a real latent f32 bug: float registers hold f64 bits, but f32 MEMORY is the 4-byte single — readField/writeField were truncating the f64 bits (writing zeros for 1.0); now they @floatCast on f32 load/store (mirrors legacy storeAtRawPtr). Result: VM HANDLES 27 corpus const-inits (was 16); parity 688/688 (gate ON and OFF). Unit tests added (f32 round-trip; malloc → usable comptime memory). Next: the kindOf .unsupported aggregates (3×), global_get (2×), the rest.
  • Phase 1.final step 5 (VM plan) — implicit-context materialization; coverage 0→16 (2026-06-17). tryEval now MATERIALIZES the implicit ctx instead of skipping it: a has_implicit_ctx comptime entry (sole param *Context) gets a zeroed Context of the right size/align in comptime memory, its address passed as arg 0. Const bodies that ignore the ctx run; a body that uses the allocator hits unported call_indirect → bails → legacy. No func-ref materialization needed (handled bodies don't read ctx contents; parity is the guard). Fixed a real bug surfaced by the coverage pass: storing a null non-pointer optional (the null_addr sentinel) into an aggregate slot OOB-bailed — writeField now ZEROES the destination for a null_addr aggregate source (= none/empty); unit-test regression added. Result: VM HANDLES 16 corpus const-inits (was 0); parity 688/688 both gate ON and OFF. Next: port the ops the trace names — call_builtin/compiler_call/ extern (13×, via the bridge), kindOf .unsupported aggregates (3×), global_get (2×), func_ref / call_indirect / trace_frame / is_comptime.
  • Phase 1.final steps 14 (VM plan) — host wiring landed; coverage measured (2026-06-17). (1) Hardening: Machine.readWord/writeWord/bytes now return error.OutOfBounds (null / out-of-range / oversized / overflow-safe) instead of assert-panicking; OutOfBounds added to Vm.Error; try threaded through every helper + exec arm + the bridge. New unit tests (accessor OOB returns; null-deref → tryEval null, not a crash). (2) Implicit context: tryEval returns null for has_implicit_ctx funcs (legacy fallback) — conservative; full ctx materialization deferred to step 5. (3) Wiring: const-init fold in emit_llvm.zig emitGlobals is (if comptime_flat) tryEval else null) orelse interp.call(...), gated by env SX_COMPTIME_FLAT (read once into LLVMEmitter.comptime_flat). Default OFF. (4) Parity + coverage: gate ON → full corpus byte-identical (688, 0 failed) + manual 0605/0606/0607 byte-identical. Finding: 0 of 37 measured corpus const-inits are VM-handled — ALL are has_implicit_ctx-gated. Added a coverage-trace facility (comptime_vm.last_bail_reason
    • env SX_COMPTIME_FLAT_TRACE). Next: step 5 = implicit-context materialization (the unblocker), then port the deferred ops. 688 corpus green (gate OFF).
  • Phase 1.final start (VM plan) — wiring entry point tryEval (2026-06-17). comptime_vm.tryEval(gpa, module, func_id) ?Value runs a comptime function entirely on the VM, returns a legacy Value (deep-copied to gpa) or null to fall back. Unit-tested (pure 6*7 → 42; unbox_any → null). NOT yet routed into the host: needs (1) panic→error hardening of Machine accessors so arbitrary funcs bail instead of crashing, (2) implicit-ctx handling, (3) wiring at emit_llvm const-init behind SX_COMPTIME_FLAT, (4) corpus parity run. See PLAN-COMPILER-VM.md Phase 1.final. 688 corpus green.
  • Phase 1 sub-step 1.5b (VM plan) — Reg↔Value boundary bridge (2026-06-17). Builtin/compiler_call/extern handlers are coupled to the legacy Interpreter, so the wiring will use WHOLE-FUNCTION fallback (VM runs pure functions; bail → legacy re-runs the whole eval). Built the boundary bridge that enables it: valueToReg (Value arg → Reg, aggregates into comptime memory) + regToValue (VM result → Value, deep-copied). Covers scalars/strings/structs; other shapes bail. Transitional. Round-trip unit-tested. 688 corpus green. Next: the wiring (flag + route a comptime entry through the VM with legacy fallback).
  • Phase 1 sub-step 1.5 (VM plan) — direct call + stack-lifetime change (2026-06-17). Vm gained module (callee resolution) + depth/max_depth guard. call marshals arg Refs → Reg and recursively runs the callee; aggregates pass as Addrs over shared comptime memory. Frame no longer reclaims the machine on exit (else a returned aggregate Addr dangles) — allocations live to Vm.deinit. Extern/builtin callees bail (1.5b). Unit-tested: direct call (142), recursion sum(0..n) (15/55). 688 corpus green. Next: 1.5b (call_builtin/compiler_call/extern), then hybrid wiring.
  • Phase 1 sub-step 4d (VM plan) — deref/addr_of; pivot decision (2026-06-17). Ported addr_of (pass-through) + deref (readField through pointer), unit-tested (deref *i64 → 77, addr_of struct + field → 80). DECIDED to stop porting rarer ops (tagged-union payload/any/closures) blind — their byte semantics are ambiguous without real call sites — and pivot to CALLS (sub-step 1.5: call, then builtin/compiler) + HYBRID WIRING (-Dcomptime-flat → VM with legacy fallback on error.Unsupported), so the VM runs the real corpus and surfaces exactly what's needed. Key design point for calls: aggregate-return lifetime → drop per-frame stack reclaim (let a comptime eval's allocations live to Vm.deinit). 688 corpus green. See PLAN-COMPILER-VM.md decision block.
  • Phase 1 sub-step 4c (VM plan) — optionals + payloadless enums (2026-06-17). kindOf: enum → word; ?T → word (pointer-child, null==0) or {T@0,i1@sizeof(T)} aggregate. Ported optional_wrap/unwrap/has_value/coalesce (optChildIsPtr/optHas; const_null reads as none) + payloadless enum_init/enum_tag. Unit-tested (?i64 → 91, ?*i64 null==0 → 99, enum tag → 11). 688 corpus green. Next: 4d (tagged unions, any, closures).
  • Phase 1 sub-step 4b (VM plan) — slices + strings on comptime memory (2026-06-17). {ptr@0(pointer_size), len@8(i64)} fat pointers (kindOf: string/slice → aggregate). Ported const_string (text+NUL + fat pointer in comptime memory), length/data_ptr, array_to_slice, subslice, index-through-slice (elemAddr loads .ptr), and str_eq/str_ne (memcmp). Unit-tested (str length+eq/ne, array→slice index sum=23, subslice sum=43). 688 corpus green. Next: 4c (optionals/enums/any/closures).
  • Phase 1 sub-step 4a (VM plan) — tuples + arrays on comptime memory (2026-06-17). kindOf widened (tuple/array → aggregate). Ported tuple_init/tuple_get (tupleFieldOffset), index_get/index_gep (elemAddr = base + idx*elem_size over array/pointer/many_pointer; slice/string bases bail), length on array values. Unit-tested (mixed tuple, [3]i64 index sum=42, length=3). 688 corpus green. Next: sub-step 4b (slices/strings, then optionals/enums/any/closures).
  • Phase 1 sub-step 3 (VM plan) — memory + structs on comptime memory (2026-06-17). Vm gained optional table: *const TypeTable (target-aware layout). Ported alloca/load/store + struct_init/struct_get/struct_gep, laying structs out at the table's natural offsets. Value model: scalar/pointer → register word; struct → lives in comptime memory, its value IS its address (read→addr, write→memcpy), so nested structs compose and struct_gep = base+offset. kindOf bails loudly on not-yet-ported types. Addr-based values survive allocator realloc. Unit-tested (struct round-trip, alloca+gep+store+load, nested struct). 688 corpus green. Next: sub-step 4 (arrays/slices/strings/optionals/enums/tuples/any/closures, then calls).
  • Phase 1 sub-step 2 (VM plan) — comptime executor: scalars + control flow (2026-06-17). Added Vm to comptime_vm.zig: walks the same IR Inst over comptime frames (register Reg = scalar bits or Addr), mirroring the legacy interp's scalar semantics (i64 wrapping/signed, f64). Ported constants, arithmetic, comparison, logical, conversions, terminators (br/cond_br/ret/ret_void) and block_param; every other op bails loudly (error.Unsupported + op name in detail). Unit-tested on hand-built tiny IR (Fb builder): int add, f64 arithmetic, cond_br selection, a block-param loop, div-by-zero + unsupported-op bails. Corpus untouched (688 green). Next: sub-step 3 (memory + aggregates on comptime memory, where target-aware layout enters).
  • Phase 1 sub-step 1 (VM plan) — comptime machine substrate (2026-06-17). New src/ir/comptime_vm.zig: Machine (linear byte memory + bump/stack allocator with mark/reset, scalar readWord/writeWord 1/2/4/8 LE, bytes views, addr 0 reserved as null_addr) + Frame (Ref-indexed register file, stack reclamation on deinit). Reg = raw u64 (immediate scalar OR Addr). Unit-tested (comptime_vm.test.zig), registered in the barrel; standalone — the legacy interpreter stays live, corpus untouched (688 green). Next: sub-step 2 (executor + scalar/branch ops over the same IR). Also removed the "~500 lines / split step" rule from CLAUDE.md per request.
  • Phase 0 (VM plan) — struct-weld stripped; intern/text_of bridge kept (2026-06-17). Removed the struct-weld registry from compiler_lib.zig (weldStruct/bound_types/BoundType/FieldLayout/findType/SxField/ LayoutMismatch/validateStructLayout), validateWeldedStruct/weldedFieldOrderStr
    • the sd.abi == .zig call from nominal.zig, the struct-weld unit tests, and examples 0625/0627/1183/1186. KEPT (decision) the intern/text_of function host-call bridge — a clean scalar dispatch, not weld/serialize/marshal, the Phase-3 compiler-call seed — so weldedCompilerFn, the compiler_welded dispatch, the emitCall comptime-only gate, the #library/abi/extern syntax, and examples 0626/1184/1185 remain. zig build test green (688 corpus, 0 failed). Next: Phase 1 (byte-addressable value model) per PLAN-COMPILER-VM.md.
  • DIRECTION CHANGE — pivot off the byte-weld to a byte-addressable bytecode VM (2026-06-17). Decided the weld + serialization/marshaling bridge is the wrong direction (it hand-marshals onto a comptime value model that isn't bytes — exactly what the design set out to kill). New foundation: a bytecode VM over comptime memory so comptime values are native bytes; the compiler-API then rides on it via direct memory (no weld/validation/marshaling). JIT-native comptime was weighed and rejected (breaks cross-compilation, loses the sandbox). Wrote current/PLAN-COMPILER-VM.md (Phase 0 strip → Phase 1 byte-addressable value model → Phase 2 bytecode → Phase 3 compiler-API on comptime memory). Banner added to design/comptime-compiler-api.md (superseded). Reverted the session's uncommitted register_struct/find_type marshaling experiment back to reify HEAD (40d075c). No code stripped yet — Phase 0 is the next action.
  • Phase 2 — welded structs by reflection + memory-order validation. Dropped the byte-layout-override engine (computeWeldPlan / offset-ordered LLVM struct / byte-blob — all explored, all unnecessary). Instead: the sx header declares fields in the compiler type's memory order; the compiler reflects the bound Zig type (@typeInfo/@offsetOf/@sizeOf) and validates the header matches with loud diagnostics (field-not-found, wrong-order+expected-order, size mismatch). On pass it's an ordinary byte-identical struct — cast + deref just works. Examples 0627 (usable) / 1186 (wrong-order diagnostic). Suite green (692).
  • Phase 2.1 — weld-plan layout math (REMOVED). The byte-layout-override math; superseded by the reflection+validation design and deleted.
  • Phase 1 polish — comptime-only enforcement. A runtime call to a welded fn is a clean build-gating error (emitCall gate, guarded by enclosing-is_comptime so #run/:: uses stay green), not a link failure. Example 1185. Build + suite green (458 unit, 690 corpus).
  • Phase 1.1 fifth sub-step — host-call bridge (welded functions). compiler_lib function registry (intern/text_of) + findFn; IR Function compiler_welded flag set/validated in declareFunction (weldedCompilerFn); interp.call() dispatches welded calls to the Zig handler. Examples 0626 (round- trip) + 1184 (unexported-fn diagnostic); findFn unit-tested. Runtime-call clean rejection deferred (loud link error today). Build + suite green (458 unit, 689 corpus).
  • Phase 1.1 fourth sub-step — welded-struct layout validation. validateStructLayout (pure, unit-tested) + validateWeldedStruct wired into registerStructDecl: a struct abi(.zig) extern compiler is validated against the registry (lib == compiler, name exported, layout matches) with build-gating diagnostics. #library "compiler" no longer dlopen'd. Examples 0625 (faithful Field) + 1183 (field-count mismatch diagnostic). Offset-override/GEP deferred to Phase 2 (not exercised by Field's natural layout). Build + suite green (456 unit, 687 corpus).
  • Phase 1.1 third sub-step — binding registry. New src/ir/compiler_lib.zig: the compiler lib's welded-type registry; Field welded to StructInfo.Field with layout baked from the real Zig type (@offsetOf/@sizeOf/@alignOf); findType lookup proven by unit test (+ null off the export list). Standalone island — not yet consumed by lowering. Build + suite green (454 unit tests). Break-verified.
  • Phase 1.1 second sub-step — struct-decl binding parses. ast.StructDecl gained abi + extern_lib; parseStructDecl parses abi(.zig) extern <lib> after struct. Parser unit tests (welded Field + plain struct), break-verified. Build + suite green. Parse-only sub-step (fns + structs) of Phase 1.1 complete.
  • Phase 1.1 first sub-step + callconvabi unification. Parsed abi(.zig) extern <lib> on fn decls; unified callconv into abi(.c|.zig|.pure) (removed the callconv keyword), migrated 52 sx files + compiler diagnostics + docs + snapshots. Build + suite green. The original design's extern(.zig) single qualifier was split into abi(.zig) (ABI/layout, before extern) + extern <lib> (linkage + source) — recorded in the design doc's syntax-decision note.