`BuildOptions :: struct #compiler { ...35 methods... }` becomes
`BuildOptions :: struct { }` (an opaque null-sentinel handle) plus 35 free
`ufcs (self: BuildOptions, …) abi(.compiler)` decls in build.sx, each serviced
by a new `comptime_vm.callBuildOptionFn` arm (off `callCompilerFn`). No legacy
`compiler_lib` handler: the names are registered in `bound_fns` with a single
bailing stub only so `weldedCompilerFn` accepts them.
- String lifetime: setters dupe the arg into the persistent `Vm.gpa` (the
Compilation allocator, threaded into both `tryEval` and `runBuildCallback` —
not the per-eval VM arena) and write/append to the threaded `BuildConfig`.
Getters read the field/slice or compute the target predicate from the triple.
- Dispatch routing (Option B): a `#run`/const-init entry that directly calls a
compiler-domain/welded fn (`emit_llvm.entryNeedsVm`) runs on 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).
- Mark the 5 `platform/bundle.sx` getter-calling helpers `abi(.compiler)` (they
are comptime-only bundler code; otherwise their now-welded getter calls trip
the runtime-call gate).
- 37 `.ir` snapshots regenerated (std transitively imports build.sx → string-
pool/type-table indices shift); verified `.ir`-only, zero behavior-stream diffs.
BuildOptions `compiler_call` strict bails gone (1609/1614/1615 strict-clean);
1616 now bails on a separate, pre-existing unported bitwise/shift VM gap (`shr`),
to port first in P5.6. 703/0 both gates.
Also sweep the outdated "flat memory" terminology to "comptime/byte-addressable"
across comptime_vm + the plan/checkpoint/CLAUDE docs: the comptime VM is
arena-backed, byte-addressable memory where `Addr` is a real host pointer, not a
flat contiguous address space (flag names `-Dcomptime-flat`/`SX_COMPTIME_FLAT` kept).
130 KiB
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#compilerstruct attribute with ONE welded mechanism. Branch:reify(offmaster). 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 onreifythrough40d075c) and is kept only to scope the Phase 0 strip. ReadPLAN-COMPILER-VM.mdfirst.Why the pivot: the comptime evaluator (
src/ir/interp.zig) represents values as taggedValueunions, NOT native bytes — so a comptime@ptrCast(*StructInfo)reads theValueunion'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-unionenum_initall run NATIVELY on the VM (.call_builtinexec arm →callBuiltinVm;defineFromInfodecodes aTypeInfofrom comptime memory,buildTypeInforeflects one INTO comptime memory — faithful ports of legacydefineEnum/Struct/Tuple/reflectTypeInfo). The ENTIRE metatype range0614–0624+0632runs HANDLED with ZERO fallback (incl. thedefine(declare, type_info(T))round-trips0619/0622/0623); VM output byte-matches legacy.enum_init/define/type_infobail loudly on abacking_typetagged 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 inPLAN-COMPILER-VM.md; user decision: UNIFY (the VM runs the post-link bundler too,interp.zigfully deleted). DONE this arc (all green): 4A.1 box_any/unbox_any +.anyas 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.callHostExterndlsym + host_ffi, any extern, args/returns pass untouched since Addr IS a host pointer (e7a8708, example 0636toupper); 4D.2 slice/string args → NUL-termchar*+ float-arg/return guards (6a7f690, example 0637strlen([:0]u8)). 699/0 BOTH gates.DIRECTION CORRECTION (2026-06-18, user):
#compiler/compiler_callis DELETED, not bridged on the VM.BuildOptionsis RE-EXPRESSED asabi(.zig) extern compilerfunctions (the compiler-API surface the VM already dispatches viacallCompilerFn); the#compilerattribute, thecompiler_callIR op, and theValue-based hookRegistry(compiler_hooks.zig) all go away. So there is NO transitionalcompiler_call→hooks shim on the VM (I started one — threading the legacy interp intotryEvalfor the hook registry — and reverted it; tree clean atb05c74f).0602/0603stay on legacy fallback until the BuildOptions migration lands. Migration shape (end-state, shares theBuildConfig-on-the-VM prerequisite with the bundler 4E): (1) eachBuildOptionssetter/getter becomes acompilerfn incompiler_lib.bound_fns+Vm.callCompilerFn, reading comptime args + a*BuildConfigthreaded into theVm(the sameBuildConfigmain.zigforwards); (2)library/modules/build.sxdeclares themabi(.zig) extern compilerinstead ofstruct #compiler; (3) delete thecompiler_callop +compiler_hooks.zigHookFn/Registry+ the#compilerparse/lower path. SeePLAN-COMPILER-VM.mdPhase 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.outDONE (2026-06-19, newest Log entry): removed theoutbuiltin — it's a plain sx fn calling libcwrite, so the VM handles it via host-FFI (no buffer, no special arm; no double-print because there's nooutop to bail-then-fallback on).trace_resolvePORTED (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 propercomptime type construction failed:diagnostic; VM-gap strict bails are now ONLY the 4compiler_call) · 4C#insert. BuildOptions migration — design settled + foundation underway (2026-06-18, see the two newest Log entries):#compiler/compiler_callis replaced byabi(.compiler)(a compiler-domain ABI — runs in the comptime evaluator, never in the binary). S1+S2 DONE:abi(.compiler)introduced, the oldabi(.zig) extern compiler+#library "compiler"fully removed, all compiler-API examples migrated. S3 DONE: emit_llvm skips BODIEDabi(.compiler)(compiler-domain) functions; a comptime-only call from a dead body emitsundef(regression0638; 701/0 both gates). The earlier "runtime-reachability gating" blocker is MOOT — a compiler-domain callback isn't LLVM-emitted, so itsbuild_options()calls never reach theemitCallgate. S4 SKIPPED (optional ergonomics): anabi(.compiler)function is type-compatible with a plain() -> Rparam (the ABI marks the function, not its type), so callbacks/registrars just declare themselvesabi(.compiler)(S3) — no param-propagation needed. S5a DONE:build_options+set_post_link_callback→abi(.compiler),BuildConfigthreaded into the VM;bundle_main+ the platform registrars markedabi(.compiler); strictcompiler_callbails 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).BuildConfigbecomes plain sx data; the compiler shrinks to a fewabi(.compiler)primitives (emit_object/link/queries, explicit args,-> !not bool) + anon_buildslot (stdlibdefault_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_sourcesthreaded in; VM bail → hard build error (comptime_vm.last_bail_reasonsurfaced bymain.printInterpBailDiag). Smoke test1661-platform-post-link-vm-list(AOT) — a post-link callback that GROWS aList(0141: works on VM, bails on legacy withstruct_get); build succeeds (exit 0) only via the VM.flushInterpOutputdeleted (VM writesoutdirect 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)areabi(.compiler)primitives (new stdlib homelibrary/modules/compiler.sx), serviced bycomptime_vm.callCompilerFnreadingBuildConfigfieldsmain.zigforwards (c_object_paths/link_libraries). New reusable VM helpermakeStringListbuilds aList(string)in comptime memory (target-aware via the result type's offsets);invoke/callCompilerFnnow thread the call's result type (ins.ty). Legacy handlers bail loudly (VM-only by nature — post-link). Smoke test1662-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() -> stringALSO landed (a QUERY — the Zig driver emits eagerly, the primitive returnsBuildConfig.object_path; NO vtable). So all three QUERY primitives are done. 703/0 both gates. P5.2b (linkACTION) DONE (2026-06-19, newest Log entry):link(objects, output, libraries, frameworks, flags, target)dispatches through a host-installedcompiler_hooks.BuildHooksvtable (main.zigLinkHooksCtx→target.link); USER DECISION: the build callback is NOT fallible —linkis plain VOID, a failure BAILS (hard build error), no-> !/failable-tuple needed. New VM readersreadStringList/readStringArg. Smoke test1663-platform-build-pipeline-link(AOT): a post-link callback re-links the build's objects to a temp output via sxlink— the relinked binary RUNS; negative-probe verified (bad path → bail → build exit 1). P5.3 (on_buildregistrar) + P5.4 CORE DONE (2026-06-19, newest Log entries): the whole build is sx-driven viadefault_pipeline(force-lowered + auto-invoked; NO Zig auto-emit/auto-link);on_build(cb)is the sole callback mechanism;set_post_link_callbackdeleted. 703/0 both gates. NEXT — the FULL MIGRATION (no legacy left), spec'd as Phase 5 steps P5.5–P5.8 inPLAN-COMPILER-VM.md: P5.5 DONE (2026-06-19, newest Log entry): the 35BuildOptions#compilermethods → VM-nativeabi(.compiler)arms (comptime_vm.callBuildOptionFn, NO legacy handler); setter strings duped into the persistentVm.gpa;#run/const-init compiler-domain entries routed to the VM (entryNeedsVm, no fallback) so gate-OFF stays green; 5 bundle.sx helpers markedabi(.compiler). BuildOptionscompiler_callbails GONE (1609/1614/1615 strict-clean; 1616 now bails onshr— a SEPARATE unported bitwise/shift VM gap, do FIRST in P5.6). 37.irregenerated (string-pool churn, behavior-identical). 703/0 BOTH gates. · P5.6 ALL bundling + code signing for EVERY target (macOS/iOS-device/iOS-sim/Android) in the sxdefault_pipeline· P5.7 DELETE#compiler/compiler_call/compiler_hooks/interp.zig+ theregToValuebridge + VM→legacy fallback (drop gate-OFF; VM is the SOLE evaluator) · P5.8 build~/projects/m3te+~/projects/distributionend-to-end as the acceptance test + add.app/.apksmoke tests. FINAL atomic step (4F): (outalready done — VM-native via libcwrite) handleinterp_print_frames+ flip strict-to-default (remove the fallback) + deleteinterp.zig/Value+ re-expressdefine/make_enum. SeePLAN-COMPILER-VM.md→ Phase 4 for the full plan + top risks (bundler test coverage). Earlier landed: dedicatedTypebuiltin 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 aTypeTablequery both the legacy handler and the VM call (no drift). Examples 0628–0630.- WRITE side (P3.3, legacy-only at lowering time):
declare_type+pointer_to+ ONE kind-branchingregister_type(subsumesdefine's per-kind dispatch; codes matchtype_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); bareEnumType.variantqualified construction. Examples 0631–0635, 0187.- Lowering-time VM (P3.4): hardened the VM against malformed lowering-time IR (
refTy, bailingaggType, bounds-checked branch targets — bails, never panics); wiredtryEvalintorunComptimeTypeFuncbehind the flag with legacy fallback; materialized a zeroed lowering-timeContext(the global isn't built yet at lowering). All measured green.THE WALL (next step): a
Typevalue is an 8-byte tid, but.any(the boxed-any) is a 16-byte{tag,value}— and they share one TypeId (.any). So aTypein an aggregate (Member.ty/EnumVariant.payload) is sized 16B while the value is 8B → every lowering-time type-fn bails atconst_type/ the Member-array build. Can't makekindOf(.any)a word: at EMIT time.anyreally is a 16B box (variadic any, 0603), so that would silently corrupt it. The correct fix is a dedicatedTypebuiltin TypeId (8B), distinct from.any— measured at ~123.anyreferences 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.anyas 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_typereturns a non-optionalTypeIdusingunresolved(0), NOT?Type; reader names use thetype_*family (avoid colliding with stdfield_name/type_name); the write side is a single kind-branchingregister_type; the write side stays LEGACY-only until the VM runs at lowering time (needs theTypeTypeId). End-state guarantee: ONE evaluator —interp.zigdeleted; 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-flatOR envSX_COMPTIME_FLAT=1. Coverage trace:SX_COMPTIME_FLAT_TRACE=1(now also prints lowering-timetype-fnHANDLED/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-goldensto regenerate ONLY the named example(s) — a full-Dupdate-goldensre-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; atypes.zigchange re-reflects on the next compiler build. - On pass it's an ORDINARY struct whose natural layout already equals the Zig
layout →
@ptrCastto 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:weldStructnow REFLECTS field names (@typeInfo) and bakesbound_typesfields in ascending-OFFSET (memory) order — no hand-listed names. DeletedcomputeWeldPlan/WeldPlan/WeldElement.validateStructLayoutchecks the sx header against the memory-ordered registry.nominal.zigvalidateWeldedStruct: renders the precise diagnostics (+weldedFieldOrderStr).- Examples:
0627(StructInfo in memory order, byte-identical, usable);1186(source-order StructInfo → wrong-field-order diagnostic).1183message refreshed. zig build+zig build testgreen (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
StructInfotofields@0,name@16,nominal_id@20,is_protocol@24, size 32 — vs sx-naturalname@0,fields@8, … So the override is genuinely required (Field's two-u32 natural layout was the easy case). compiler_lib.zig: registeredStructInfo(weldStruct, the secondbound_typesentry). AddedWeldElement/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 testgreen.
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 iscompiler_weldedAND the ENCLOSING function is notis_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 setcomptime_failed(the driver halts before object/JIT emission). The enclosingis_comptimeguard is what keeps the legitimate#runuse (example 0626) green.- Corpus:
examples/1185-diagnostics-weld-fn-runtime-call.sx(runtimeintern(…)→ clean error, exit 1, no link failure). zig build+zig build testgreen (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, andFnHandler(*Interpreter, []Value -> Value).internmutates viainterp.mint orelse @constCast(&module.types)(the same mutable-table access the metatype mint path uses);text_ofreads the const pool. Importsinterp.zig(the compiler_hooks↔interp cycle pattern).- IR
Functiongainedcompiler_welded: bool.declareFunction(src/ir/lower/decl.zig) sets it viaweldedCompilerFn, which also VALIDATES: the bound lib must becompilerand 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, acompiler_weldedfunction routes tocompiler_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).findFnlookup 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 testgreen (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 firstLayoutMismatch(field count / name / size / total) or null. Pluslib_name = "compiler"andSxField. Unit-tested (faithfulFieldpasses; each drift flagged as the right variant).registerStructDecl(src/ir/lower/nominal.zig): forsd.abi == .zig,validateWeldedStructchecks the bound lib iscompiler, 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 spuriouslibcompiler.soload warning).- Corpus:
examples/0625-comptime-weld-struct-field.sx(faithfulFieldwelds, validates, usable as data →name=7 ty=3);examples/1183-diagnostics-weld- struct-field-count.sx(one-fieldField→ 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 withStructInfoin 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 testgreen (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— thecompilerlibrary's binding registry, the curated safety boundary.BoundType { sx_name, size, alignment, fields: []FieldLayout{name, offset, size} };weldStructbakes the layout from a real Zig struct via@sizeOf/@alignOf/@offsetOfat compiler-build time (a sx-field-count mismatch is a@compileError, never a silent truncation).bound_typesexportsField(welded totypes.TypeInfo.StructInfo.Field— twou32s);findType(sx_name) ?*const BoundTypeis 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 realStructInfo.Field@sizeOf/@alignOf/@offsetOf(8 bytes, two u32s at 0/4); an unexported name returns null. Break-verified (a wrong size → suite red, namedir.compiler_lib.test...). zig build+zig build testgreen (454 unit tests).
Earlier — second sub-step (struct-decl parse)
abi(.zig) extern <lib> PARSES on a STRUCT decl (parse-only, no semantics).
ast.StructDeclgainedabi: ABI+extern_lib: ?[]const u8binding fields.parseStructDecl(src/parser.zig): afterstruct(and the#compilercheck), parse an optionalabi(...)then optionalextern <lib>— same slot order as fn decls — and thread them onto the node. Ordinary structs are unperturbed (parseOptionalAbi/parseOptionalExternExportno-op when absent).- Parser unit tests (
src/parser.test.zig):Field :: struct abi(.zig) extern compiler { name: StringId; ty: Type; }parses withabi == .zig,extern_lib == "compiler", field list intact; a plain struct leavesabi == .default/extern_lib == null. Break-verified (a wrong-sentinel assert turns the suite red, confirming the test runs). zig build+zig build testgreen.
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 beforeextern/export. Unified replacement forcallconv(...), which is removed.ABI = { default, c, zig, pure }:.c(C ABI),.zig(Zig-layout weld → thecompilerlibrary),.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):CallingConvention→ABI { default, c, zig, pure }; thecall_convfield →abi: ABIonFnDecl/Lambda/FunctionTypeExpr. - Lexer/token (
src/token.zig,src/lexer.zig):kw_callconv→kw_abi, keyword string"callconv"→"abi". - Parser (
src/parser.zig):parseOptionalCallConv→parseOptionalAbi(parsesabi(.c|.zig|.pure)); wired in the fn-decl postfix slot (beforeextern/export), the function-type-expr slot, and the lambda slot;isFunctionDef/hasFnBodyAfterArrowrecognisekw_abi. - AST→IR map (
src/ir/type_resolver.zig,src/ir/lower/decl.zig,sema.zig,closure.zig): the AST.abi == .creads 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 textcallconv(.c)→abi(.c). - sx migration: 52
.sxfilescallconv(→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.zig—abi(.zig) extern <lib>on a fn decl (assertsabi == .zig,extern_export == .extern_,extern_lib == "compiler"); bareexternleavesabi == .default; standaloneabi(.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 onreifyHEAD (40d075c); they are the strip list for Phase 0, not the forward direction. The#library/abi/externsyntax 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 compilerSTRUCTS: layout-validated against the registry (faithful → ok; drift → build-gating diagnostic).Fieldwelds + usable.abi(.zig) extern compilerFUNCTIONS: dispatched under the comptime interp to their registered Zig handler (intern/text_ofround-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
Fieldcase + 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.zigreflection+validation,nominal.zigvalidateWeldedStruct, thecompiler_weldeddispatch, the weld examples/diagnostics 0625/0627/1183/1184/1185/1186), keeping the#library/abi/externsyntax. 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_typeover the host-call bridge (compiler_lib.zigbound_fns, likeintern/text_of).register_structtakes a weldedStructInfoand mints a realTypeId(guarded: dup field names, kind well-formedness — the checksdefinedoes today). Because the weldedStructInfois byte-identical, the handler can read it as the real Zig*StructInfo(cast + deref) rather than marshalling aValuefield-by-field — the payoff of the byte-weld.find_type(StringId) -> ?Typereads the table. Prove: build a struct programmatically + round-trip a source one. - Re-express
type_info/define(struct) as sx overregister_struct/find_type; migrateexamples/0622; delete the bespoke struct interp arms (defineStruct/ thereflectTypeInfostruct 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
Listgrowth; orthogonal, seecurrent/CHECKPOINT-METATYPE.md.)
Log
- P5.5 — the 35
BuildOptionsaccessors migrated offstruct #compileronto VM-nativeabi(.compiler)(2026-06-19).BuildOptions :: struct #compiler { ...35 methods... }→BuildOptions :: struct { }(an opaque null-sentinel handle) + 35 freeufcs (self: BuildOptions, …) abi(.compiler)decls inlibrary/modules/build.sx, each serviced by a newcomptime_vm.callBuildOptionFnarm (dispatched fromcallCompilerFn). NO legacycompiler_libhandler (per the full-migration direction): the 35 names are registered incompiler_lib.bound_fnsonly soweldedCompilerFnaccepts them, with a single bailing stubhandleBuildOptionsAccessor(never reached). String lifetime: setters dupe the arg string into the PERSISTENTVm.gpa(the Compilation allocator threaded into bothtryEvalandrunBuildCallback— NOT the per-eval VM arena, whose bytes die atVm.deinit), so a#run-set path survives to post-link. Setters write/append the duped string to the threadedBuildConfig(output_path/bundle_path/…, thelink_flags/frameworks/asset_dirsArrayLists); string getters return the field (or""); bool getters compute from the triple (predIsMacOS/predIsIOS/…, mirroring the legacy hooks); count/indexed getters read theBuildConfigslices. 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-flatgate — so gate-OFF stays green without a legacy BuildOptions handler (P5.7 retires the legacy interp entirely). The 5platform/bundle.sxhelpers that call getters (build_info_plist/embed_framework/android_bundle_main/build_android_manifest/compile_jni_main_sources) are markedabi(.compiler)too (they're comptime-only bundler code; without it their now-welded getter calls trip the runtime-call gate). Snapshots: 37.irchurned (std transitively imports build.sx → string-pool/ type-table indices shift) — regen scoped via-Dname; verified ONLY.irchanged (zero behavior-stream diffs). 703/0 BOTH gates. Strict sweep: the BuildOptionscompiler_callbails are GONE (1609/1614/1615 strict-clean); 1616 now bails onshr(a pre-existing, separate VM gap — bitwise/shift opsshl/shr/bit_and/bit_or/bit_xor/bit_notare unported incomptime_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 whereAddris a REAL host pointer, NOT a flat contiguous address space; "flat memory"/"flat-memory" → "comptime memory" / "byte-addressable" acrosscomptime_vm.zig+ the plan/checkpoint/CLAUDE docs (flag names-Dcomptime-flat/SX_COMPTIME_FLATkept).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) intocomptime_vmso the real bundler path runs on the VM (the 1616shrgap). Then moveplatform/bundle.sx's per-target logic to read the migratedabi(.compiler)getters +fs/processhost-FFI, callbundle()fromdefault_pipelineafterlinkwhenbundle_path()is set, and remove the--bundle/post_link_moduleZig 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 hostBuildHooksvtable;main.BuildHooksCtx); new query primitivesbuild_output/build_target/build_frameworks/build_flags(read the mergedBuildConfig);library/modules/build.sximportscompiler.sx+ definesdefault_pipeline(emit → gather c_objs → link); the compiler force-lowersdefault_pipeline(well-known name,decl.isDefaultBuildPipeline, force-lowered after Pass 2) and auto-invokes it post-codegen when noon_buildoverride (mainfinal fallbackinvokeByName "default_pipeline"); the BUILD path auto-importsmodules/build.sx(prepends a synthetic import node incompileWithTimer) so prelude-less programs (asm tests) still getdefault_pipeline; removed the build cache short-circuits (a future cache can live indefault_pipeline). (2) on_build-only (65ac370) — migrated all 9set_post_link_callbackcallers toon_build(cb)(callback gainsopt: BuildOptions); DELETEDset_post_link_callback. Override semantics changed: anon_buildcallback REPLACES the build (must emit+link orreturn 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 todefault_pipeline; deleted 1661/1663 (primitives now exercised by EVERY AOT build).sx run(JIT) is UNTOUCHED (emits in-process, never invokesdefault_pipeline). Benign.irchurn 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-nativeabi(.compiler)arms — NO legacycompiler_libhandlers, NO dual-path. SeePLAN-COMPILER-VM.md→ Phase 5 steps P5.5–P5.8 for the full spec. In brief:- P5.5 — migrate all 36
BuildOptions :: struct #compilermethods → freeufcs … abi(.compiler)decls +comptime_vm.callCompilerFnarms (NO legacy handler). Setters dupe strings into a PERSISTENT allocator (threademit_llvm.allocvia e.g.BuildConfig.string_alloc). Kills the 4 strictcompiler_callbails. - 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 sxdefault_pipeline(via the migrated getters +fs/processhost-FFI). Remove the--bundle/post_link_moduleZig shim. Compiler keeps ONLY the linker primitive (Option B). - P5.7 — DELETE
#compiler+compiler_callop +compiler_hooks(Registry/HookFn) +interp.zig(Interpreter/Value/reflectTypeInfo/callExtern) +regToValue/valueToReg+ the VM→legacy fallback; make-Dcomptime-flatpermanent. A VM bail is ALWAYS a build diagnostic now. Re-expressdefine/make_enumas sx. Land the 0141 repro; reconcile 1654. - P5.8 — build
~/projects/m3te+~/projects/distributionend-to-end as the acceptance test thatdefault_pipelinecovers all targets; add.app+.apkbundle smoke tests (no corpus coverage today).
- P5.5 — migrate all 36
- P5.3 (
on_buildregistrar) — the build-callback registration mechanism; callback takesBuildOptions(2026-06-19). Per the user's design:on_build(cb)is the build-callback registrar (a FREE fn), generalizingset_post_link_callback— the callback is(opt: BuildOptions) -> bool abi(.compiler)and the compiler invokes it post-codegen WITH the opaqueBuildOptionshandle. Key simplification: the handle is a single null-sentinel word, so passing it sidesteps the feared fat-BuildConfigmarshaling. Changes: VMcallCompilerFnon_buildarm + legacyhandleOnBuild(both setpost_link_callback_fn+ a newBuildConfig.post_link_takes_optionsflag);comptime_vmrunEntry→runEntryArgs(extra)(implicit ctx + explicit args) + a publicrunBuildCallback(..., pass_options);core.invokeByFuncId/invokeByNamenow takepass_options(was an always-empty args slice);main.zigpassesgetPostLinkTakesOptions();build.sxon_builddecl. Smoke test1664-platform-on-build-callback(AOT). Benign 37-.irchurn (type table +1 for theon_buildfn type; behavior identical — verified only.irstreams 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):
- Migrate to
on_buildONLY — convert everyset_post_link_callback(cb)caller (platform/bundle.sxbundle_main, examples 1611/1614/1615/1616, 0602/0603) to#run on_build(cb)withcb: (opt: BuildOptions) -> bool; DELETEset_post_link_callback(build.sx + compiler_lib + VM arm). - Bundle/Android config → sx data in the default script. The
#compileraccessors 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 sxBuildConfig/default script (sx-owned data), not compiler hooks. default_pipeline+ override model.library/modules/build.sxships#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 overwritespost_link_callback_fn).default_pipelinecallsemit_object/c_object_paths/link_libraries/link+ the sx bundler.- REMOVE the Zig driver's auto-emit/auto-link (
main.compileWithTimer) — COUPLED with (3): oncedefault_pipelinedrives 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). - Delete
#compiler/compiler_call/compiler_hooks+ the S5abuild_optionsonce config is sx data → kills the 4 strictcompiler_callbails (1609/1614/1615/1616) → strict sweep green →interp.zigdeletable.
- Migrate to
- P5.2b (
linkACTION) — the sxlinkprimitive 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)(inlibrary/modules/compiler.sx). USER DECISION this step: drop fallibility from the build callback — solinkis 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.zigcan't depend on the driver (core/main/target), solinkdispatches through a newcompiler_hooks.BuildHooks { ctx, link_fn }thatmain.ziginstalls intoBuildConfig.build_hooksbefore the post-link callback. The driver side ismain.LinkHooksCtx(holds allocator/io/base_config/has_jni_main; itslinkadapter unions the explicitflagswith the CLI ones and callstarget.link(objects[0], objects[1..], …)— the linker treats first-vs-rest as equal inputs). New VM readers (inverse ofmakeStringList):readStringList(aList(string)arg →[][]const u8, element bytes are views into stable comptime arena) +readStringArg(astringarg). Registeredlinkonbound_fns(legacy stub bails — VM-only). Smoke testexamples/1663-platform-build-pipeline-link(AOT): a post-link callback re-links the build's own objects (viac_object_paths+emit_object) into a temp output through the sxlinkprimitive — 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 →ldfails →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'starget.link; the test links to a SEPARATE temp output) — removing the auto-link + havingon_builddrive everything is P5.3/P5.4. 704/0 both gates. - P5.2 (metadata queries) —
c_object_paths/link_librariescompiler primitives + the VMList(string)builder (2026-06-19). Phase 5 step 2 (the read-only slice): twoabi(.compiler)primitives that the sx build driver will pass tolink—c_object_paths() -> List(string)(the#import ccompanion.os) andlink_libraries() -> List(string)(the#librarynames). They live in a NEW stdlib filelibrary/modules/compiler.sx(the Phase 5 home the sxdefault_buildgrows into) and are serviced bycomptime_vm.callCompilerFnreading two newBuildConfigfields (c_object_paths/link_libraries) thatmain.zigforwards before the post-link callback (alongsidebinary_path/target_triple/…). Reusable new piece:Vm.makeStringList(table, list_ty, items)builds aList(string)in comptime memory — backing array ofstringfat 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/callCompilerFnnow thread the call instruction'sins.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 aList(string)isn't faithfully buildable in the legacyValuemodel (0141). Registered onbound_fnssoweldedCompilerFnrecognizes them. Smoke testexamples/1662-platform-build-pipeline-queries(AOT + a 1-line C#source→ exactly one C object): a post-link callback assertsc_object_paths().len == 1,items[0].len > 0, and iterateslink_libraries()(liveness touch) — build exit 0 only if the VM-built list is well-formed. Negative-probe verified a real guard (forcinglen != 2→ "post-link callback returned false", build exit 1).emit_object() -> stringALSO 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 newBuildConfig.object_pathfieldmain.zigforwards (no driver vtable needed). 1662's callback now also assertsemit_object().len > 0. So ALL THREE query primitives (emit_object/c_object_paths/link_libraries) are done; onlylink(the genuine ACTION) remains. No unit test formakeStringList— constructing aList(string)TypeIdin 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 oncore/main/target). 703/0 both gates + strict JIT run clean (nocompiler_callbail). - 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 invocationmain.zigfires aftertarget.link— now routes the callback through the comptime VM (comptime_vm.tryEval) instead of the legacyInterpreter. REQUIRED because the sx build driver allocates/growsLists, 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 incomptime_vm.last_bail_reason(now surfaced bymain.printInterpBailDiag, which previously only read the legacy interp'slast_bail_*statics).BuildConfig(&emitter.build_config) +import_sourcesare threaded into the VM call. Deleted the now-deadflushInterpOutput(the VM writesoutdirectly via host-FFI — no buffer to flush). Non-emptyargsrejected loudly (error.ComptimeVmArgsUnsupported) — theon_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 buildexit 1,OutOfBounds (op=struct_get)) and SUCCEEDS after the change (exit 0). Formalized asexamples/1661-platform-post-link-vm-list({ "aot": true }): the callback grows aListto 3 + returnslen == 3; the build links cleanly (exit 0) and the binary printsruntime 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#runtime but run JIT) — soinvokeByFuncIdwas previously untested by the corpus; 1661 is the first coverage. The 4 strictcompiler_callbails (1609/1614/1615/1616) are UNAFFECTED — they bail at#run configure()on still-#compileraccessors (set_bundle_pathetc.), 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 adefine()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'sdefinemessages with the legacy's exact text —comptime define():(wascomptime define:), and the duplicate variant/field cases now NAME the offender via a newfailFmthelper ('...' duplicate variant name 'value'). (2) The strict type-fn path (lower/comptime.zig) now emitsd.addFmt(.err, span, "comptime type construction failed: {s}", .{vm_reason})— the SAME diagnostic as the legacy fallback, so 1179/1180 produce their exact expected.stderrunder strict with NO legacy interp involved. Left the const-init/#runstrict 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 4compiler_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. outis now a PLAIN SX FUNCTION (libcwrite), NOT a builtin — VM handles it via host-FFI;trace_resolveported; 0522 fixed (2026-06-19). Per user: removed theout#builtinentirely.library/modules/std/core.sxnow defineslibc_write :: (fd, [*]u8, usize) -> isize extern libc "write"+out :: (str: string) { libc_write(1, str.ptr, xx str.len); }. DeletedBuiltinId.out(inst.zig), theresolveBuiltin"out" mapping (call.zig), the sema builtins-list entry (sema.zig), and BOTH.outarms (interp.zigbuffered-append,ops.zigLLVMwritelowering). At comptimeoutruns through the evaluator's host-FFI (the VM's dlsymwritepath / the interp's extern call) — so the VM HANDLESoutwith NO special arm. Benign prelude.irchurn ([*]u8interned earlier +@out→@write+ theoutfn body) → regen'd 54.irsnapshots (verified: only string-table renumber + the intended decl/fn-body change; zero stdout/exit changes). This UNMASKED two latent VM gaps theout-bail was hiding (the VM now runs pastout): (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 asource_mapnow threaded into the VM (newtryEvalparam,&import_sourcesfrom emit_llvm), build the{file,line,col,func,line_text}Framestruct in comptime memory (makeStringValue/writeField/fieldOffset). (2) 0522 (bare-pack[]Any) — was a CRASH (reflectArgTypeId@intCastof a garbage word) → hardened to a loud bail (typeIdxOfchecked cast; the VM must never panic). ROOT CAUSE: after the 0143 fix$argsmaterializes as[]type_value(8-byte), but the example declareddescribe(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 ARETypes, 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 sxdefault_builddriver usesoutfor diagnostics — now VM-native; no compileroutbuiltin to special-case.) THENinterp_print_framesported to the VM too (1034): unlikeoutit needs the live evaluator call-chain, so it's a VM arm (mirrors legacyprintInterpFrames) — walkscall_stack(skips the last frame), writesat <name>lines straight to fd 1 (consistent without's directwrite). 1034 matches; 701/0. STRICT DELETION-GATE NOW DOWN TO 7 (all known categories):compiler_call(4 — 1609/1614/1615/1616, the still-#compilerBuildOptions accessors → Phase 5 sx-build-pipeline) · VM-diagnostic negatives (2 — 1179/1180, thedefinebail 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
BuildOptionsaccessor to anabi(.compiler)fn that delegates to acompiler_hookshook just re-encodes sx-level logic (setters/getters,is_macostriple-matching, list appends) as compiler hooks — they need NOTHING from the compiler but theBuildConfigstate. So instead:BuildConfigbecomes 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 fewabi(.compiler)PRIMITIVES taking EXPLICIT args (emit_object() -> !string,link(objects, output, libs, fws, flags, target) -> !, metadata queries) + anon_build : (BuildConfig) -> ! abi(.compiler)slot (stdlib defaultdefault_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 shellscc/ld) is a later refinement. NO bool — failures are the error channel (-> !); VERIFIED on the current build: void#run,-> !/-> !Efailable#run, and araiseat#runfails the build with a return trace (+ suggests#run … catch (e){…}).on_buildGENERALIZES today'spost_link_callback_fn(assignable typed global w/ default, vs a setter). Full design + step plan inPLAN-COMPILER-VM.md→ Phase 5. S5a (below) is a green intermediate that the sx-pipeline replaces wholesale (don't extend it; P5.4 deletesbuild_options/set_post_link_callback+ all#compiler). NEXT — P5.1 (= 4E): route the post-codegen /on_buildinvocation through the VM (core.invokeByFuncId→ VM), REQUIRED because the sx driver allocatesLists and the legacy interp can't (0141, VERIFIED: comptimeListgrowth works on the VM, fails on legacy withstruct_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_callbackmigrated off#compilerontoabi(.compiler);BuildConfigthreaded into the VM (2026-06-18). The corpus-covered slice of the BuildOptions migration. (1)comptime_vm.zig—Vm.build_config: ?*BuildConfig, threaded via a newtryEvalparam (&self.build_configfrom emit_llvm's#run/const-init sites;nullat lowering-time type-fn). (2) TwocallCompilerFnarms:build_options(returns the null-sentinel handle) +set_post_link_callback(reads the cbfunc_ref, storespost_link_callback_fnon the threadedBuildConfig). (3)compiler_lib.zig— matching legacyhandleBuildOptions/handleSetPostLinkCallback(gate-OFF dual path). (4)build.sx—build_options :: () -> BuildOptions abi(.compiler);andset_post_link_callbackEXTRACTED from thestruct #compileras a freeufcs (…) abi(.compiler)(soopts.set_post_link_callback(cb)still resolves via UFCS); the other ~38 BuildOptions methods stay#compilerfor now. (5) Registrars/callbacks that call these are now compiler-domain:platform/bundle.sxbundle_main :: () -> bool abi(.compiler), and the six platform examples'configure/configure_buildregistrars markedabi(.compiler); 0602/0603 reworked the same way. KEY learning: every example transitively importsbuild.sxvia the prelude, so theset_post_link_callbackmethod→free-function change is BENIGN.irchurn (declaration renumber + global@str/@tag.strsuffix shift) in all 37 examples that have.irsnapshots — 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-VMcompiler_callbail 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 emitundef(2026-06-18). A BODIEDabi(.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) IRFunctiongainedis_compiler_domain: bool(inst.zig). (2)decl.zig— newfnIsBodilessCompilersplits the API surface (bodiless → declare-only,compiler_welded, no implicit ctx — the S1 behavior) from a bodiedabi(.compiler)function (lowers its body for VM eval; flaggedis_compiler_domain+is_comptime; gets normal implicit-ctx). The four S1 guards now gate onfnIsBodilessCompilernotfd.abi == .compiler. (3)emit_llvm.zig— Pass 2 skipsis_compiler_domainbodies; Pass 1 declares them EXTERNAL-linkage (an internal empty decl fails LLVM verification). (4) KEYops.zigemitCall— a call to a comptime-only callee (compiler_weldedORis_compiler_domain) from a dead comptime body now emitsundefinstead of a realcall; the runtime-call gate covers both. Without the undef, an AOTsx buildleft an undefined_double/_internsymbol — 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(doublefolds a#runconst → 84, absent from the binary vianm, JIT + AOT both run). 701/0 both gates + all unit tests. NEXT: S4 — anabi(.compiler)function-TYPE param (cb: () -> bool abi(.compiler)) flags the bound function compiler-domain (so a plainbundle_main :: () -> bool { … }becomes compiler-domain when passed toset_post_link_callback). Then S5 (BuildOptions migration + delete#compiler/compiler_call/compiler_hooks). - S1+S2 DONE —
abi(.compiler)replacesabi(.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.zigABI variant entirely (ast.ABIis now{ default, c, compiler, pure }) and madeabi(.compiler)the sole spelling for a compiler-domain / compiler-API function — the ABI alone marks it, noextern <lib>, no fake#library "compiler". Changes: (1)ast.zig—.zig→.compiler(doc rewritten). (2)parser.zig—parseOptionalAbiaccepts.compiler(drops.zig); a bodilessabi(.compiler)decl (ends in;, noextern) is now accepted — synthesizes the empty-block placeholder like anexternimport (the Zig/VM handler is the impl). (3)decl.zig—weldedCompilerFnkeys offfd.abi == .compiler+ export-list membership (noextern_lib == "compiler"check); a bodilessabi(.compiler)decl lowers extern-like (is_extern_decl, and the two body-lowering pathslowerFunction/lazyLowerFunctionskip it) so it is declared-not-defined;funcWantsImplicitCtxreturns false forabi == .compiler(an implicit__sx_ctxprepend 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 everycompiler :: #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 bylibc :: #library "c"etc.) — only the compiler-API's USE of them is gone.compiler_lib.lib_name- the
main.zigdlopen-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 BODIEDabi(.compiler)functions (Pass 2, likeis_extern); thread anabi(.compiler)flag onto the IRFunctionand refine the three "today everyabi(.compiler)fn is bodiless" guards indecl.zig(marked withS3 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).
- the
- DESIGN PIVOT (2026-06-18, user) —
abi(.compiler)is the compiler-domain ABI; DROP the fake#library "compiler". Supersedes both theabi(.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 so —abi(.compiler)— not because it's "extern" to an imaginary library. One annotation covers BOTH roles:- Compiler-API surface (
intern,text_of,find_type,declare_type,register_type,build_options,set_post_link_callback, …): bodilessabi(.compiler)decls (the Zig/VM handler IS the impl). Replaces… abi(.zig) extern compiler;+ thecompiler :: #library "compiler";line — both GO AWAY. - User compiler-domain functions (post-link callbacks like
platform.bundle.bundle_main): BODIEDabi(.compiler)functions. emit_llvm does NOT lower them (skip in Pass 2, likeis_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.zigemitCall) only fired because comptime-only callback bodies (bundle_main, 0602'sconfigure) were being LLVM-emitted. A bodiedabi(.compiler)function is never emitted → itsbuild_options()/binary_path()calls never reachemitCallas runtime code → no enforcement, no undefined-symbol risk. 1185 stays correct:mainis an ordinary runtime fn (notabi(.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 theis_comptime__runwrapper; 0602/0603 only tripped via an intermediateconfigure(), a test-shape artifact.) Staged plan (each its own step, both gates green):
- S1 — introduce
abi(.compiler)as a newABIvariant that marks a functioncompiler_welded(export-list checked) WITHOUT requiringextern compiler/#library. Add it ALONGSIDE the existing.zig extern compilerpath so migration is incremental; prove with one example (0626 →abi(.compiler)). (.zigis a misnomer — "we don't really have a zig abi"; it becomes.compiler, ultimately replacing.zigonce all callers move.) - S2 — migrate the rest of the compiler-API decls (0628–0633, 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 compilerparse path +#library "compiler". - S3 — emit_llvm skips bodied
abi(.compiler)functions (Pass 2continue, likeis_extern); thread theabi(.compiler)flag onto the IRFunction. 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/… becomeabi(.compiler)(+ VMcallCompilerFnarms / legacycompiler_libhandlers;BuildConfigthreaded into the VM — the bundler 4E shares this); callbacks declared/typedabi(.compiler); delete#compiler/compiler_call/compiler_hooksRegistry. Then 4E bundler on the VM. Reusable facts from the reverted attempt: onlybuild.sxuses#compiler; VM dual-path bail-to-fallback means the VM needs only corpus-covered fns; UFCS on a free fn needs theufcsmarker (composes with the ABI annotation); the binding mechanism currently lives indecl.zigweldedCompilerFn(keys offextern_lib == "compiler"— S1 makes it key offabi == .compiler). Mechanism files:ast.zig(ABIenum),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(emitCallgate).
- Compiler-API surface (
- Phase 4 — BuildOptions→
abi(.zig) extern compilermigration ATTEMPTED, then REVERTED; BLOCKER found: the comptime-only welded-call enforcement (2026-06-18). Scoped an incremental slice (migrate only the corpus-coveredbuild_options()+set_post_link_callback, leaving the 38 bundler accessors on#compiler→ VM bails → legacy fallback). Built it end-to-end: threadedBuildConfiginto theVm(tryEvalgained a?*BuildConfigparam, passed&self.build_configfrom emit_llvm's#run/const-init sites); addedcallCompilerFnarms + legacycompiler_libbound-handlers for both; rewrotebuild.sx(build_options→abi(.zig) extern compiler; extractedset_post_link_callbackout of thestruct #compileras a freeufcs (...) abi(.zig) extern compilerfn soopts.set_post_link_callback(cb)still resolves via UFCS; addedcompiler :: #library "compiler";). All COMPILED and the welded dispatch fired. BLOCKED at LLVM emission, NOT a bug — a design limitation the migration surfaces: acompiler_weldedcall inside a NON-is_comptimefunction is a hard build-gating error (ops.zigemitCall, 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'sbundle_main :: () -> bool, and 0602'sconfigure) that run ONLY at comptime (post-link interp/VM) yet are still LLVM-emitted as real() -> boolbodies. The OLD#compiler/compiler_callpath emitted those as deadundef(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 frommain) 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: inemit_llvm, mark functions reachable from runtime roots (main/ exported runtime fns); a welded call in an UNREACHABLE function emitsundef(dead, likecompiler_calldid) 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 callbacksis_comptime— can't statically identify whichfunc_refs become post-link callbacks; (C) blanket softening toundef— would silently swallow genuine runtime misuse (1185). Other migration facts confirmed this attempt (reuse next session): onlybuild.sxuses#compiler(theissues/*.mdhits 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 theufcsmarker, which composes withabi(.zig) extern compiler;build.sxmust declarecompiler :: #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_framescorrectly DEFERRED (2026-06-18). Also portedtype_is_unsigned(aBuiltinIdviacallBuiltinVm): resolves the queriedTypeIdthe same way astype_name(a.type_valueword, or an Any box{tag@0,value@8}whose tag IS the boxed value's type) then returnstable.isUnsignedInt(tid). Extracted the shared resolution into areflectArgTypeIdhelper (VM-nativeValue.reflectTypeIdmirror) sotype_name+type_is_unsignedcan't drift. MATCH-verified by a new VM unit test (type_is_unsigned(u32) - type_is_unsigned(i64) == 1). Strict sweep: 0600type_is_unsigned→out(now its only remaining bail); notype_is_unsignedbails 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.zigexec switch): (1)error_tag_name_get— a runtime tag-id word → its name string viatable.getTagName+makeStringValue(uses the table, not the module, so it's unit-testable;self.table == &module.types); (2)global_addr— name-matches__sx_default_contextand returns the already-testedmaterializeDefaultContextAddr (an aggregate value IS its address, so a downstreamloadsees the materialised Context), bailing for any other global exactly like legacy. MATCH verification:error_tag_name_getlocked in by a new VM unit test (tag id →"Bad", viaregToValue);global_addrproven by the strict sweep (0600's first bail moved past it) and reusesmaterializeDefaultContext, 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 asout(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 FINALout/strict-default step, NOT now. Strict-sweep burndown: 1035error_tag_name_get→out; 0600global_addr→type_is_unsigned(a NEW pure-op bail surfaced — still a known pure op, next to port); 1034 stays atinterp_print_frames(deferred, as it should). Also fixed the stalecomptime_vm.zigheader 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. Onreify. - Phase 4 burndown — issue 0143 FIXED (pack-as-
[]Typestride) + regression test (2026-06-18). Root cause was a stale consequence of the.type_valuemigration:buildPackSliceValue(lower/pack.zig) materialized a bare$<pack>[]Typeslice as[]Any(16-byte elements) whileconst_typenow yields an 8-byte.type_valueand[]Typeresolves 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 stopgaptype_name.unresolvedguard (its whole reason is gone; dropping it keeps any future stride bug VISIBLE as wrong output rather than a silent fallback). SiblingmaterialisePackSlicechecked — it genuinely boxes values into[]Any(correct, not the same bug). Regression testexamples/0525-packs-pack-as-type-slice-arg. 700/0 both gates. 0114 (and 0521/0522/0524) now bail ONLY atout(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:
outis end-state-only (2026-06-18). Ported two PURE comptime ops (379ed05):switch_br(i64-discriminant multi-way branch — enum/error tag or.type_valueindex) andtype_name(Type value / Any box →table.typeName, with an.unresolved-bail guard). Correct in isolation; 0520–0524 run GREEN under strict. Two blockers found:- issue 0143 (FILED, OPEN) — pack-as-
[]Typestride mismatch. A..$argspack forwarded as a[]TypeARGUMENT 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. Blocksexamples/0114from running HANDLED. Per CLAUDE.md: filed, NOT worked around (thetype_name.unresolvedguard just makes the VM decline rather than emit garbage). Repro + fix-prompt inissues/0143-…md. out(comptime print) is an END-STATE op — it cannot land while the fallback exists. Under the legacy fallback, an eval that prints viaoutthen BAILS double-prints (the VM wrote to fd 1, then legacy re-runs the whole eval — no rewind). 0114 demonstrated it. So a direct-writeoutis 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 addouttogether — at which point everyout-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.
- issue 0143 (FILED, OPEN) — pack-as-
- Phase 4 — STRICT no-fallback mode (the interp-retirement enumeration gate) + full gap list (2026-06-18).
Added
-Dcomptime-flat-strict/ envSX_COMPTIME_FLAT_STRICT(impliescomptime_flat): at all THREE comptime sites (type-fn inlower/comptime.zig, const-init +#runinemit_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 andinterp.zigcan be deleted (4F). Default behaviour unchanged — 699/0 both default gates. (Fixed a wiring bug: the type-fn site's localcomptime_flatdidn'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 viaSX_COMPTIME_FLAT_STRICT=1over 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[]Typeslice materialized when a pack ($args) is passed ACROSS A CALL reads itsstringelement 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 compilermigration (delete#compiler/compiler_call; threadBuildConfiginto 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_valueword / Any-box tag →table.typeName).global_addr(1): 0600 — only&__sx_default_contextis 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). ExtractedmarshalExternArg: a scalar/pointer WORD passes verbatim (acstringarg already works as a pointer word via 4D.1); astring/slice{ptr,len}fat pointer is copied into a NUL-terminated arena buffer and itschar*passed (mirrors legacymarshalExternArg— what the bundler'spopen(cmd: [:0]u8, …)needs). Added FLOAT guards on args AND returns: floats arekindOf == .wordbut 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 example0637-comptime-extern-slice-arg(#run strlen("hello, world")with a[:0]u8param → 12) runs HANDLED on the VM, byte-matching legacy. 699/0 BOTH gates. Onreify. 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.invokewithcallHostExtern: resolve the symbol viahost_ffi.lookupSymbol(dlsym RTLD_DEFAULT) and dispatch through thehost_ffitrampolines, exactly like the legacyinterp.callExtern. Marshalling is now trivial becauseAddris a real host pointer (4D.0): every WORD-kind arg passes asusizeverbatim — a scalar's bits OR a pointer, no translation — and a pointer return is a validAddr. PickscallPtrRet(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 example0636-comptime-extern-libc(#run toupper(97)/tolower(90)fold to 65/122) runs HANDLED on the VM, output byte-matching legacy. (absdoesn't dlsym-resolve on macOS — a compiler builtin — and the VM fails identically to legacy, confirming parity.) 698/0 BOTH gates (one new example). Onreify. Next (4D.2): string/aggregate extern args (string→NUL-term cstring) + float args/returns, thencompiler_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 growableArrayList(u8)flat buffer (which reallocs/MOVES on growth) with astd.heap.ArenaAllocator: eachallocBytesis a separate arena allocation that never moves and is freed wholesale ondeinit(no per-object free, no cap, no fixed buffer).Addris 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/bytesderef 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; theFrame.bad_refguard still catches the dominant malformed-IR vector) and the test-onlymark/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 inVm.invoke— marshal args (scalars by value, pointers as the host pointer they already are), call viahost_ffitrampolines, return scalars/pointers; a new#runlibc example as the corpus guard. - Phase 4A.1 (VM plan) —
box_any/unbox_anyon the VM +.anyas a 16-byte aggregate (2026-06-18). Ported the Any-boxing conversion pair:box_anyallocates the 16-byte{ type_tag@0, value@8 }box (tag = source TypeId index, matching the legacy comptime interp), writing a word source's scalar viawriteField(source_type)(so f32 round-trips) or an aggregate source's comptime ADDR (the runtime pointer-in-value-slot shape);unbox_anyreads the value slot back (word →readField, aggregate → the stored ADDR). Required making.anya first-class comptime aggregate (it waskindOf → .unsupported):kindOf(.any) = .aggregate(16B, by-address) +fieldOffsetspecial-cases.anyto the{@0, @8}layout (shared with string/slice) — without the latter, astruct_geton an Any panicked (union field 'struct' while 'any' is active), caught + fixed (no crash; "never crash" upheld). Updated two unit tests that usedunbox_anyas the "unported op" example → nowcompiler_call; added a box→unbox round-trip test. 697/0 BOTH gates + all unit tests. Onreify. The 6 box_any examples (0114/0520–0524/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 atswitch_br(comptime Any-tag type-switch),type_name, andout/print (4A.2+/later steps). Next (4A.2): comptimeout/print (VM output buffer + flush). - Phase 3 P3.4 step 8 (VM plan) — VM-native
type_infoREFLECTION → the whole metatype surface is HANDLED (2026-06-18). Portedtype_info($T)into the VM (callBuiltinVm.type_infoarm → newbuildTypeInfo), the inverse of step 7'sdefine: reflect a type INTO aTypeInfoVALUE built in FLAT MEMORY (the VM-native mirror of legacyreflectTypeInfo). Decodes the source type into a tag + members (tagged-union/struct field & enum variant →{ name, ty }, a payloadless variant →void; tuple → bare positionalTypes), then lays out the nested value bottom-up using layouts derived from theTypeInfoRESULT type (ins.ty, now threaded intocallBuiltinVm): 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 amakeStringValuehelper extracted fromtext_of. Samebacking_typeguard as step 7. Result: the ENTIRE metatype surface runs HANDLED on the VM with ZERO fallback —0614–0624+0632(0616field_typefolds at lower time, no comptime eval); thedefine(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. Onreify. 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 + deleteinterp.zig. - Phase 3 P3.4 step 7 (VM plan) — VM-native metatype CONSTRUCTION:
declare/define+ tagged-unionenum_init(2026-06-18). Ported the metatype type-CONSTRUCTION builtins into the VM so the construction examples run HANDLED end-to-end (nocall_builtinfallback). Three pieces: (1) tagged-unionenum_initwith 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 LLVMbackend/llvm/types.ziglayout) and copies the payload attag_size. (2) A.call_builtinexec arm → newcallBuiltinVm, the VM-native mirror of the legacyexecBuiltinInner:declare(name)mints an empty forward nominal slot (shareddeclareNominalhelper, also used bydeclare_type);define(handle, info)reads theTypeInfotagged-union VALUE from FLAT MEMORY (tag@0, active payloadEnumInfo/StructInfo/TupleInfostruct attag_size, its single slice field) and mints viadefineFromInfo, a faithful port of legacydefineEnum/defineStruct/defineTuple(all-void enum → real.@"enum"per issue 0142, dup-name rejection,updatePreservingKeyvsreplaceKeyedInfo). (3) Refactored the[]{name,ty}decode out ofregisterTypeVminto a shareddecodeMemberSlice(+decodeTypeSlicefor bare-Typetuple elements), keyed to the module-levelNamedMember. 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/defineassume a tag-headed layout, which is WRONG for abacking_typetagged union (laid out as the backing struct) — both now bail loudly onbacking_type != nullrather than silent-clobber. Result: examples0614/0620/0621/0624/0632run fully HANDLED on the VM (define is the whole eval);0622/0623run define HANDLED then fall back cleanly at the still-unportedtype_inforeflection. VM output byte-matches legacy for all 7. 697/0 BOTH gates + all unit tests (added: tagged-unionenum_initpayload layout). Onreify. Next: porttype_info(REFLECT a type → build aTypeInfovalue in comptime memory, the inverse — reuses the tagged-unionenum_initwrite) so0619/0622/0623go 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)materializeDefaultContextbuilds 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 inlineAllocatorvalue{ctx=null, alloc_fn@+ptr, dealloc_fn@+2*ptr}at the head ofContext, socontext.allocator.alloc_bytesdispatchescall_indirect→ thunk → native VMmalloc; (3)aggTypenow DEREFS a pointerbase_type(the List write path emitsstruct_gepwithbase_type = *Struct—fieldOffsetpanicked on the pointer; now derefs to the pointee, no panic); (4)subslicehandles a[*]Tmany-pointer /*Tbase (a List'sitemsfield — the base IS the data pointer). Verified end-to-end (manual probe): a compiler-API type-fn that builds its[]Memberin aList(Member)(.append×3, thenregister_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_gepwith a pointerbase_type). 697/0 BOTH gates + all unit tests, EXIT=0. Onreify. Remaining for the original 0141 repro (uses metatypedefine/make_enum→call_builtin→ legacy fallback → legacy fails): re-express the metatype over the compiler-API so the whole type-fn runs on the VM (nocall_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 callscontext.allocator.alloc_bytesat lowering time bails in legacy withcomptime 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-expressingdefine/make_enumover the compiler-API needs to BUILD[]Memberslices 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 (needsEnumVariant/StructFieldto be usable ASMember— 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_typeintoVm.callCompilerFn, mirroring the legacycompiler_libhandlers (mint via@constCast(table)— the same mutable access the read-sideinternuses; the lowering-time mint target IS&module.types).register_typereads the[]Memberslice from FLAT MEMORY: threadedref_typesthroughinvoke→callCompilerFnso the slice's element type (Member = {name: string, ty: Type}) gives the field offsets + stride; decodes each{name, ty}and branches onkind(1 struct · 2 enum · 3 tagged_union · 4 tuple) exactly as legacy (dup-name / payload-on-enum rejections, idempotent re-fill vianominalIdentOf). Key unblock: the synthesized comptime type-fn wrapper (createComptimeFunction/…WithPrelude) was built with return type.any→regToValuebailed at the VM↔legacy boundary; changed to.type_value(the legacy path reads viaasTypeIdregardless, so no legacy change). Result: the compiler-API write type-fns now run HANDLED end-to-end on the VM at LOWERING time —0631(register-graph: 2 HANDLED, A↔B cycle via forward handles +pointer_to) and0635(multi-edge import: 2 HANDLED), parity-correct. They run on the ZEROED lowering-time context (fixed.[…]member arrays, no allocation). The metatypemake_enum/defineexamples (0632) still fall back CLEANLY throughcall_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. Onreify. Next: (optional, deferred) a REAL lowering-time Context (CAllocator thunk func-refs) for List-growing type-fns; and re-express the metatypedefine/make_enumover the compiler-API to delete the bespoke interp arms (the end-state: ONE evaluator). - Phase 3 P3.4 step 4 (VM plan) — model
.type_valuenatively in the comptime VM (2026-06-18). The VM now HANDLES Type values instead of bailing:kindOf(.type_value)→.word; a newconst_typeexec arm → the wordTypeId.index();regToValuemaps a.type_valueword back to a.type_tagValue at the legacy boundary (valueToRegalready mapped.type_tag→ index). Surfaced + fixed a VM PANIC (forbidden):struct_initassumed 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 thedefine/make_enumcall_builtin→ legacy mints — no mutation before the bail, parity holds). VM unit test added (const_type → word → regToValue →.type_tag). Onreify. Next (the payoff): port the WRITE side (declare_type / register_type / pointer_to) intoVm.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
Typebuiltin TypeId: RESOLVER FLIPPED +.anymigration (2026-06-18). Flippedtype_resolver:64("Type"→.type_value),module.zigconstType(result type →.type_value), andemitConstType(a bare i64 carryingtid.index(), NOT a 16-byte Any box). Then migrated every.anyreference 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_valueat all 4 consumers —reflectArgTypeId(LLVM),reflectTypeId+ the.type_tag-as-struct-field comptime path (interp), andresolveTypeCategoryTags("type")(generic.zig); (b) reflection-builtin RETURN types.any→.type_value(type_of/declare/define); the runtimetype_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 ais_rawbacktick exemption —`stringis a value, never the reserved type); (d)reflectionArgIsTypeaccepts.type_valueOR.any(a reflection arg can be a bare Type OR a boxed Any — the over-narrow==.type_valuewas the catastrophic-regression cause, caught + fixed); (e) the comptimeswitch_braccepts a.type_tagdiscriminant (type-category match); (f) a bare function name in aTypeslot now lowers toconst_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 genuineAnyparams; (g) the field-not-found diagnostic +formatTypeNamerender.type_valueas "Type". Fixed 3 unit tests asserting the old.anyType behavior. 697/0 BOTH gates + all 494 unit tests (EXIT=0). Gate ON stays green because the VM'skindOf(.type_value)→.unsupported→ bails CLEANLY to legacy (no silent-wrong) — the VM doesn't modelTypevalues YET (next step), but parity holds. Regenerated 24 snapshots (22.irconst_type-shape; 2.stderrAny→Type — diff reviewed, only the intended changes). Onreify. Next: model.type_valuenatively in the VM (kindOf→ word,const_type→ word =TypeId.index(),regToValueword →.type_tag) for COVERAGE, then port the WRITE side intocallCompilerFn+ a real lowering-time Context → the first HANDLED lowering-time type-fn. - Phase 3 P3.4 step 2 (VM plan) — dedicated
Typebuiltin TypeId: FOUNDATION landed (dead/additive) (2026-06-18). AddedTypeId.type_value(slot 19) + a matchingTypeInfo.type_valuevariant + the builtins init entry — an 8-byte type handle distinct from the 16-byte boxed.any(THE WALL). Alltypes.ziglayout handlers wired:sizeOf/typeSizeBytes→ 8,typeAlignBytes→ 8,typeName→ "Type",hashTypeInfo/typeInfoEqlno-payload arms. Only ONE exhaustive switch needed a new arm (backend/llvm/types.zigtoLLVMTypeInfo→cached_i64); every otherswitch(TypeInfo)site has anelse(audited when the resolver flips).first_user19 → 100 (per the user): slots 20–99 are RESERVED builtin headroom (infos padded with theunresolvedtripwire), so future builtins don't renumber user TypeIds / churnsx irsnapshots. Cost: ~80 default entries in each binary's per-type reflection arrays (user opted in). Still dead:type_resolver.zig:64STILL returns.anyfor "Type" — nothing produces.type_valueyet, so NO behavior change. Regenerated 22 IR snapshots (pure TypeId renumber to 100-base;git diff --name-onlyconfirmed ONLY.irfiles + the 2 source files changed — no stdout/stderr/exit). 697/0 both gates (OFF and-Dcomptime-flat). Next: fliptype_resolver:64→.type_value, then migrate the.anyrefs that mean "a Type value" (const_type result / reflection returns / metatypeTypeparams /.type_tagchecks) — 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).
materializeDefaultContextnow falls back to a ZEROEDContext(found by name) when the__sx_default_contextglobal 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 nullalloc_fn(zeroed) →call_indirecton 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 — metatypemake_enumnow bails atconst_type(theType-literal op, unported);register_typetype-fns bail at the welded write call (declare_type/register_type aren't incallCompilerFn). 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) theconst_typeop → a word =TypeId.index(); (b) the Type-return bridge (regToValuefor aType/.anyword →.type_tag); (c) the VM-native write side (declare_type/register_type/pointer_to incallCompilerFn) + 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) throughcomptime_vm.tryEvalbehind-Dcomptime-flat/SX_COMPTIME_FLATwith 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 withno __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, notTypemodeling — the VM needs a way to materialize/skip the default context at lowering time (most type-fns get an implicit ctx for potentialList-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 modelTypevalues + 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.noneoperand — the 0737 crash). Closed the remaining panic vectors so the VM BAILS (→ legacy fallback) instead of aborting: (1) a checkedVm.refTy(ref_types, r)replaces every rawref_types[ref.index()]inexec(the type-side companion toFrame.get'sbad_refvalue-side guard); (2)aggTypeis now a bailing method (Error!TypeId) usingrefTy; (3) the block-dispatch loop bounds-checks the branch target before indexingfunc.blocks.items.global_getwas already guarded. No behavior change — gate OFF and ON both 697/0; unit test added (acmp_ltwith aRef.noneoperand bails, not panics). Next: wiretryEvalintorunComptimeTypeFuncbehind 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 modelsTypevalues + 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), andregister_type(handle, kind, members: []Member) -> Typewhich branches onkindIN THE COMPILER (subsuming define's per-kind dispatch). Take/return realTypevalues (matching meta.sx declare/define). Timing (per user): mint LAZILY at lowering time, single pass (the existingrunComptimeTypeFunc), so the write side is legacy-only (compiler_libhandlers) — the VM isn't wired at lowering time, no VM mirror needed; readers stay dual-path. A non-generic-> Typebuilder is now flaggedis_comptime(decl.zig) so its dead body permits the welded calls. Graph: forward handles +pointer_toexpress mutually-recursive A↔B (*A,*B, B-by-value);register_typeis idempotent (re-fill a nominal slot reached via two import edges —nominalIdent).kindcodes matchtype_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 metatypedefineEnum); (b) bareEnumType.variantpayloadless qualified construction wasn't supported (failed for hand-written enums too) — added inlowerFieldAccess(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'stype_info(T)needs (added tocompiler_lib.bound_fnsANDVm.callCompilerFn, each backed by aTypeTablequery 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) andtype_field_value(t, idx) -> i64(memberValue— an enum variant's explicit value or ordinal; mirrors thefield_value_intbuiltin; loud-bail for non-enum / out-of-range). Example0630-comptime-compiler-type-kindreflectsColor/WindowFlags(flags) /Point. The READ side is now COMPLETE —find_type+type_kind+type_field_count+type_field_name/type_field_type/type_nominal_name+type_field_valuecover everythingreflectTypeInforeads. VM unit test added. Parity 691/691 (gate ON and OFF). Revised forward direction (per the user): the WRITE side is ONEregister_type(info)fn that branches on the kind IN THE COMPILER (subsumingdefine's per-kind dispatch), not a per-kindregister_struct. - Phase 3 P3.2 (VM plan) — field-level reflection readers:
type_nominal_name+type_field_name+type_field_type(2026-06-18). Three morecompiler-library readers on the sameTypeId-handle shape (added tocompiler_lib.bound_fnsANDVm.callCompilerFn), each backed by a newTypeTablequery BOTH paths call (no drift):nominalName(a named type's own name handle; loud-bail for unnamed types likei64/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 —callCompilerFnreads arg 1 = idx; addedVm.argHandle/argTypeId(range-checked u32/TypeId arg reads) and refactoredfind_type/type_field_countonto them. Namedtype_*to avoid clashing with the std metatype builtins (field_name/type_nameexist in core.sx);nominalName(the TypeTable method) is distinct from the existingtypeName(id) []const u8display-string renderer. Example0629-comptime-compiler-field-reflectreflectsPair { 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 morecompiler-library fns, bound the same way as theintern/text_ofseed (added tocompiler_lib.bound_fnsfor the legacy handler + the welded-decl export check, AND toVm.callCompilerFnfor the native comptime path — NO marshaling). A type handle is a plainu32TypeId(likeStringId), so both keep the seed's clean scalar shape:find_type(name: StringId) -> TypeId(TypeTable.findByName,unresolved/0 if absent) andtype_field_count(t: TypeId) -> i64(a NEWTypeTable.memberCountquery — 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 example0628-comptime-compiler-find-typechainsintern → 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'sfind_type → ?Typesketch): return a NON-optionalTypeIdwith theunresolved(0) sentinel for not-found, NOT?Type— aTypevalue resolves to.any(which the comptime VM doesn't represent) and an optional can't cross the legacy↔VM eval boundary;unresolvedis the project-blessed unmistakable "no type" marker. Forward (P3.2): more readers on the same handle shape (type_name/field_name/field_type/kind), thenregister_struct(first mutating fn). - VM robustness —
Framebounds-check; lowering-time#insertwiring explored + reverted (2026-06-18). Explored wiring the VM at the LOWERING-time comptime site (evalComptimeString, the#insertstring fold). 12/13#insertexamples ran on the VM with parity, but0737(an#insertof an unresolvedsecret()) CRASHED the VM (SIGABRT): lowering-time IR can be malformed (aret Ref.nonefrom the unresolved name) andFrame.getpanicked 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 (everyref_types[...]/aggTypeaccess, not justFrame) 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/setnow bounds-check and flip abad_refflag; therunloop bails (badRef) instead of panicking. Unit test added (malformedret Ref.none→ bail, not crash). Parity 688/688 both ways. - Phase 3 SEED (VM plan) — compiler-call path:
intern/text_ofnative on the VM (2026-06-18).invokenow dispatches a weldedcompiler-library fn (gated oncompiler_welded) toVm.callCompilerFn, serviced NATIVELY on comptime memory (no legacyInterpreter):intern(string)->StringIdreads the comptime string bytes andinternStrings into the const-cast table (pool-only — doesn't touch type layout, so cached sizes stay valid);text_of(StringId)->stringmaterializes the pooled text back into comptime memory. Unlocked0626; 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-flatbuild flag (the "swap behind a build flag" step) (2026-06-18). Added the-Dcomptime-flatbuild option (build.zig → abuild_optsoptions module onmod;emit_llvm.initreadsbuild_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-flatruns the FULL corpus on the VM (688/0). Verified the flag toggles the binary: flag-builtsxreports VM HANDLED with no env var; default-built does not. Default OFF —zig build testunchanged (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
#runside-effect path + trace-clear-on-fallback (2026-06-18). Wired the SECOND comptime call site (runComptimeSideEffects, top-level#run <expr>;) throughtryEvalwith legacy fallback, mirroring the const-init fold.tryEvalnow 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. onprint) had the legacy re-run DOUBLE-push them (sx_trace_pushis a side effect on the shared buffer). Both wiring sites nowsx_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 (unlocked1030). Ported the failable/error-channel cluster (1037escape,1038handled):kindOf(error_set)→word,regToValuebridges TUPLES (the failable(value…,tag)shapecheckComptimeFailablereads),trace_framepacks(func_id<<32|span.start)from a newcall_stack(pushed by invoke/runEntry), andsx_trace_push/sx_trace_clearserviced 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:readFieldwas ZERO-extending signed sub-64-bit loads, so a storedi32 -1reloaded as0xFFFFFFFF(+4.29e9) and< 0was false — silently hidingraise error.Bad; now SIGN-extendsi8/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"),
runEntrynow materializes the REAL default context instead of a zeroed one. The implicit-ctx param is an opaque*void, somaterializeDefaultContextfinds the__sx_default_contextglobal and lays its initializer ({ {null, alloc_fn, dealloc_fn}, null }, the CAllocator thunk func-refs) into comptime memory via a new recursivelayoutConst. Withfunc_ref(function value encodedFuncId.index()+1, reserving word 0 for the null fn-ptr) andcall_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_bytes→libc_malloc→ native comptime malloc. Unlocked0606(string global). Also:global_getlazily evaluates a comptime global'scomptime_func(memoized) — unlockedCT_CHAIN; field access (fieldOffset/struct_get) handles string/slice{ptr@0,len@8}fat pointers (needed byalloc_string);regToValuemaps function-typed words →.func_ref(kept1128's rejection byte-identical). Nativemallocis 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):.unsupportedaggregates (3× —1037/1038), extern/builtinintern+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/calloc→allocBytes(16-aligned, 256-MiB cap → bail),free→ no-op,memcpy/memmove/memseton 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/writeFieldwere truncating the f64 bits (writing zeros for1.0); now they@floatCaston f32 load/store (mirrors legacystoreAtRawPtr). 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: thekindOf.unsupportedaggregates (3×),global_get(2×), the rest. - Phase 1.final step 5 (VM plan) — implicit-context materialization; coverage 0→16 (2026-06-17).
tryEvalnow MATERIALIZES the implicit ctx instead of skipping it: ahas_implicit_ctxcomptime entry (sole param*Context) gets a zeroedContextof 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 unportedcall_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 anullnon-pointer optional (thenull_addrsentinel) into an aggregate slot OOB-bailed —writeFieldnow ZEROES the destination for anull_addraggregate 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.unsupportedaggregates (3×),global_get(2×), func_ref / call_indirect / trace_frame / is_comptime. - Phase 1.final steps 1–4 (VM plan) — host wiring landed; coverage measured (2026-06-17).
(1) Hardening:
Machine.readWord/writeWord/bytesnow returnerror.OutOfBounds(null / out-of-range / oversized / overflow-safe) instead ofassert-panicking;OutOfBoundsadded toVm.Error;trythreaded through every helper + exec arm + the bridge. New unit tests (accessor OOB returns; null-deref →tryEvalnull, not a crash). (2) Implicit context:tryEvalreturns null forhas_implicit_ctxfuncs (legacy fallback) — conservative; full ctx materialization deferred to step 5. (3) Wiring: const-init fold inemit_llvm.zigemitGlobalsis(if comptime_flat) tryEval else null) orelse interp.call(...), gated by envSX_COMPTIME_FLAT(read once intoLLVMEmitter.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 arehas_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).
- env
- Phase 1.final start (VM plan) — wiring entry point
tryEval(2026-06-17).comptime_vm.tryEval(gpa, module, func_id) ?Valueruns a comptime function entirely on the VM, returns a legacyValue(deep-copied togpa) ornullto fall back. Unit-tested (pure 6*7 → 42; unbox_any → null). NOT yet routed into the host: needs (1) panic→error hardening ofMachineaccessors so arbitrary funcs bail instead of crashing, (2) implicit-ctx handling, (3) wiring atemit_llvmconst-init behindSX_COMPTIME_FLAT, (4) corpus parity run. SeePLAN-COMPILER-VM.mdPhase 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).Vmgainedmodule(callee resolution) +depth/max_depthguard.callmarshals arg Refs → Reg and recursively runs the callee; aggregates pass as Addrs over shared comptime memory.Frameno longer reclaims the machine on exit (else a returned aggregate Addr dangles) — allocations live toVm.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 onerror.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 toVm.deinit). 688 corpus green. SeePLAN-COMPILER-VM.mddecision 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). Portedconst_string(text+NUL + fat pointer in comptime memory),length/data_ptr,array_to_slice,subslice, index-through-slice (elemAddrloads.ptr), andstr_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).
kindOfwidened (tuple/array → aggregate). Portedtuple_init/tuple_get(tupleFieldOffset),index_get/index_gep(elemAddr= base + idx*elem_size over array/pointer/many_pointer; slice/string bases bail),lengthon 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).
Vmgained optionaltable: *const TypeTable(target-aware layout). Portedalloca/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 andstruct_gep= base+offset.kindOfbails 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
Vmtocomptime_vm.zig: walks the same IRInstover comptime frames (registerReg= scalar bits orAddr), mirroring the legacy interp's scalar semantics (i64 wrapping/signed, f64). Ported constants, arithmetic, comparison, logical, conversions, terminators (br/cond_br/ret/ret_void) andblock_param; every other op bails loudly (error.Unsupported+ op name indetail). Unit-tested on hand-built tiny IR (Fbbuilder): 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 withmark/reset, scalarreadWord/writeWord1/2/4/8 LE,bytesviews, addr 0 reserved asnull_addr) +Frame(Ref-indexed register file, stack reclamation on deinit).Reg= raw u64 (immediate scalar ORAddr). 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_ofbridge kept (2026-06-17). Removed the struct-weld registry fromcompiler_lib.zig(weldStruct/bound_types/BoundType/FieldLayout/findType/SxField/LayoutMismatch/validateStructLayout),validateWeldedStruct/weldedFieldOrderStr- the
sd.abi == .zigcall fromnominal.zig, the struct-weld unit tests, and examples0625/0627/1183/1186. KEPT (decision) theintern/text_offunction host-call bridge — a clean scalar dispatch, not weld/serialize/marshal, the Phase-3 compiler-call seed — soweldedCompilerFn, thecompiler_weldeddispatch, theemitCallcomptime-only gate, the#library/abi/externsyntax, and examples0626/1184/1185remain.zig build testgreen (688 corpus, 0 failed). Next: Phase 1 (byte-addressable value model) perPLAN-COMPILER-VM.md.
- the
- 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 todesign/comptime-compiler-api.md(superseded). Reverted the session's uncommittedregister_struct/find_typemarshaling experiment back toreifyHEAD (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 (
emitCallgate, guarded by enclosing-is_comptimeso#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_libfunction registry (intern/text_of) +findFn; IRFunctioncompiler_weldedflag set/validated indeclareFunction(weldedCompilerFn);interp.call()dispatches welded calls to the Zig handler. Examples 0626 (round- trip) + 1184 (unexported-fn diagnostic);findFnunit-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) +validateWeldedStructwired intoregisterStructDecl: astruct abi(.zig) extern compileris 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: thecompilerlib's welded-type registry;Fieldwelded toStructInfo.Fieldwith layout baked from the real Zig type (@offsetOf/@sizeOf/@alignOf);findTypelookup 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.StructDeclgainedabi+extern_lib;parseStructDeclparsesabi(.zig) extern <lib>afterstruct. Parser unit tests (weldedField+ plain struct), break-verified. Build + suite green. Parse-only sub-step (fns + structs) of Phase 1.1 complete. - Phase 1.1 first sub-step +
callconv→abiunification. Parsedabi(.zig) extern <lib>on fn decls; unifiedcallconvintoabi(.c|.zig|.pure)(removed thecallconvkeyword), migrated 52 sx files + compiler diagnostics + docs + snapshots. Build + suite green. The original design'sextern(.zig)single qualifier was split intoabi(.zig)(ABI/layout, before extern) +extern <lib>(linkage + source) — recorded in the design doc's syntax-decision note.