Replace the explored byte-layout-override engine (offset-ordered LLVM structs /
weld plans / byte-blobs — all unnecessary) with a much simpler design: a welded
`struct abi(.zig) extern compiler { … }` is a bodied header declaring its fields
in the bound compiler type's MEMORY order. The compiler reflects the real Zig
type (field names via @typeInfo, offsets via @offsetOf, size via @sizeOf —
nothing hand-maintained) and validates the header matches, with loud diagnostics.
On pass it is an ordinary struct whose natural layout already equals the Zig
layout — no reorder, no padding, no index/remap tables, no special LLVM path — so
@ptrCast'ing it to the compiler's own type and dereferencing is byte-identical.
When types.zig shifts, the header stops matching and the developer gets a specific
message to fix it.
- compiler_lib.zig: weldStruct reflects field names and bakes bound_types fields
in ascending-offset (memory) order; deleted computeWeldPlan/WeldPlan/WeldElement.
- nominal.zig validateWeldedStruct: precise diagnostics — field-not-found,
wrong-field-order (+ expected memory order), type-layout (size) mismatch,
total-size mismatch.
- Examples: 0627 (StructInfo in memory order, byte-identical, usable),
1186 (source-order StructInfo -> wrong-field-order diagnostic); 1183 refreshed.
- Design doc + checkpoint updated.
20 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)
Phase 1 done; Phase 2 welded structs are working via a much simpler design than
the original byte-layout-override "GEP engine" (that plan — computeWeldPlan,
offset-ordered LLVM structs, byte-blobs — was explored and DROPPED). The locked
design: a welded Name :: struct abi(.zig) extern compiler { … } is a bodied
header declaring fields in the compiler type's MEMORY order; the compiler reflects
the bound Zig type (@typeInfo names + @offsetOf offsets + @sizeOf, nothing
maintained by hand) and VALIDATES the header matches, with loud diagnostics. On
pass it's an ordinary byte-identical struct — so @ptrCast to the compiler's own
type + deref just works; no index tables, no reorder, no special emit.
Next: Phase 2 continues — re-express type_info/define (struct) as sx over
welded register_struct/find_type (host-call bridge, Phase 2.5/2.6); see
## Next step. Read order: this file → src/ir/compiler_lib.zig (registry +
reflection) → src/ir/lower/nominal.zig validateWeldedStruct. Build/verify:
zig build && zig build test.
⚠ 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
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 — Phase 2: welded compiler FUNCTIONS over the real types
Welded structs are byte-identical mirrors now, so the API surface can 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
- 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.