`$s: <TaggedUnion>` now binds a constant variant-literal argument as a
compile-time-known value and resolves it in the inlined body — the
payload-bearing generalization of the enum value param (3c4305f). A bare
variant (`.point`) or a payload variant (`.circle(5.0)`) both bind:
* the variant TAG goes into `comptime_value_bindings` (i64), so
`comptimeIntNamed`/`if s == .circle` keep working and the param is
readable in a TYPE position (`[s]i64`);
* the full materialized `enum_init(tag, payload)` value goes into a new
`comptime_value_ref_bindings` (param -> Ref) AND is scoped, so a
payload read off the bound value (`s.rect`) resolves. A new
lowering-time accessor `comptimeValueRefNamed(param)` reads it.
`bindEnumValueParams` is generalized to `bindComptimeValueParams`, which
switches on the constraint kind: `.@"enum"` -> tag-only bind,
`.tagged_union` -> tag + value bind. Other value kinds (struct/array
aggregates) are left with an explicit `else` (no silent default) and a
comment marking where the aggregate-const arm goes when a repro lands; a
non-constant arg / unknown variant is a loud, well-spanned diagnostic.
Locked by examples/0640-comptime-tagged-union-value-param.sx (bare +
payload variants, tag comparison, tag-as-dimension, payload read).
0627 (enum) stays green.
Adversarial review of A.0 found two silent-wrong defects reachable via the public
atomic_load/atomic_store intrinsics (raw LLVM verifier errors, not clean sx
diagnostics) + a latent alignment fallback. All fixed:
- scalar-kind allowlist (call.zig): the size-only T guard admitted same-sized
aggregates ([8]u8, 8-byte structs) -> invalid 'load atomic [8 x i8]'. Now an
allowlist switch (integer/float/bool/pointer/enum/vector) rejects loudly.
- per-op ordering validity (call.zig): load cannot release/acq_rel, store cannot
acquire/acq_rel -> loud diagnostic instead of invalid LLVM.
- val_ty align fallback (ops.zig): the 'else .i64' (align 8) default would
over-align a sub-8 store -> now bails loudly on a missing val_ty.
Locked by examples 1130 (non-scalar) + 1131 (bad ordering). Suite green (713/0).
A comptime value param whose constraint is a plain enum ($o: Ord) now
binds its enum-literal argument to the variant tag during inlined
comptime-call lowering. The tag is recorded in comptime_value_bindings
(readable downstream via comptimeIntNamed / direct map lookup, and as an
array-dim style const-int leaf) AND the param is bound into scope as an
enum_init value so body comparisons like 'if o == .a' lower as ordinary
enum comparisons. Distinct ordering args monomorphize the inlined body
per value.
A non-constant argument or an unknown variant emits a loud diagnostic
and binds nothing — never a silent default.
Locked by examples/0627-comptime-enum-value-param.sx.
Replace the A.0a emit bail with real LLVM atomic codegen:
- emitAtomicLoad: LLVMBuildLoad2 + LLVMSetOrdering + LLVMSetAlignment
- emitAtomicStore: LLVMBuildStore + LLVMSetOrdering + LLVMSetAlignment (value
coerced to the pointee type, mirroring emitStore)
- llvmOrdering: explicit sx AtomicOrdering -> LLVMAtomicOrdering map (LLVM's enum
is non-contiguous; never an identity cast)
examples/1700 now prints 7/42/43; IR is 'load atomic i64, ptr .. seq_cst, align 8'
+ 'store atomic ..'. Unit test 'emit: atomic load/store (seq_cst, aligned)' locks
the emission shape (load atomic/store atomic/seq_cst/align 8) without a fragile
full-module .ir snapshot. Suite green (710 examples + units).
Stream A (atomics) foundation. Net-new atomic load/store codegen path, wired
end-to-end except LLVM emission, which deliberately bails loudly so the example
locks to a clean diagnostic (A.0b turns it green — cadence: no commit both adds a
test and makes it pass).
- library/modules/std/atomic.sx: Ordering enum, Atomic($T) transparent wrapper
(init/load/store, seq_cst-only for now), atomic_load/atomic_store #builtin
intrinsics. Opt-in import, NOT in the universal std facade (Ordering in the
prelude grows every program's type table + churns 37 .ir snapshots).
- IR: atomic_load/atomic_store ops + AtomicOrdering (all 5) + structs (inst.zig);
print arms; comptime_vm arms reuse load/store (single-thread correct);
recognizer tryLowerAtomicIntrinsic (const-ordering + scalar-size guards, both
loud); emit dispatch -> emitAtomicLoad/Store bail via comptime_failed.
- examples/1700-atomics-load-store.sx locked to the bail diagnostic.
Full ordering surface (a.load(.acquire)) blocked on comptime-constant ordering
propagation (comptime enum value params) — A.0.5, migrated not legacy.
Fold the adversarial-review corrections into the program plan + design-of-record:
- atomics is 100% net-new (no scaffolding; lower.zig 'ordering' is comparison-only)
- context is already an implicit *Context param (not TLS) — B1.1 rescoped
- abi(.pure) exists but is inert (no naked emission) — B1.0 rescoped
- B1.3 switch-stress harness is the first deliverable + mandatory stack guards
- Stream C gated on a named TSan/ASan + run-N stress harness, not a footnote
Mirror the macOS .app smoke test (1665) for Android. New `.build` `apk`
directive (ApkCheck = { out, bundle_id, expect }) cross-compiles via
`sx build --target android --apk ... --bundle-id ... -o lib*.so`, then
asserts the produced APK's zip entries (AndroidManifest.xml, classes.dex,
lib/arm64-v8a/) via `unzip -l`. Build+inspect only — aarch64-linux-android
can't execute on the host, so no exit/stdout/stderr snapshot; the apk
branch is self-contained and never falls through to stream comparison.
Gated on the Android SDK ($ANDROID_HOME / $ANDROID_SDK_ROOT /
~/Library/Android/sdk) AND a real JDK (`javac -version` exit 0 — the
macOS /usr/bin/javac stub fails the gate). Missing either → skip cleanly,
so a bare-host `zig build test` stays green. Cleanup rm -rf's the apk,
staged .so, .stage dir, .unaligned/.aligned intermediates, and the
apksigner .idsig sidecar.
Verified: default `zig build test` skips 1666 (709 examples ran, 0 failed;
476/476 unit). With JAVA_HOME set to Android Studio's jbr, 1666 RUNS and
PASSES (apk built + all three entries found).
Two host-FFI gaps surfaced by the sx Android bundler running on the VM
(default_pipeline calls env() -> getenv() -> ?cstring, and from_cstring builds
a string literal):
- callHostExtern: an extern returning an OPTIONAL whose child is a single
register word (e.g. getenv() -> ?cstring) now wraps the bare C payload word
into the {payload@0, has@sizeof(child)} optional aggregate (present iff
non-null), mirroring emit_llvm's char*->?cstring handling. Previously bailed
'non-word return'. The non-word bail now names the symbol + return type.
- struct_init: the builtin two-word aggregates string ({ptr,len}) and any
({tag,value}) can now be struct_init'd (e.g. string.{ ptr=, len= } in
from_cstring). Previously bailed 'struct_init at a builtin result type'.
These let the full Android .apk bundling pipeline (javac/d8/aapt2/zipalign/
apksigner) run on the comptime VM. 709/0 corpus + 476/476 unit.
Update CHECKPOINT-COMPILER-API: Resume banner + Log entries for Step D
(metatype declare/define re-expressed as sx over the compiler-API) and the
empty-member-types-valid change. 709/0 corpus + 476/476 unit.
A comptime-constructed type with NO members is now VALID for every kind
(empty struct, empty tuple, empty enum, empty tagged_union) — only a bare
`declare("X")` placeholder that is never completed by a matching `define`
stays rejected (it would panic codegen).
- comptime_vm.zig registerTypeVm: drop the blanket "a type with no members
is never valid" rejection. The per-kind loops are vacuous for an empty
member list and the dup-name checks stay correct.
- types.zig TaggedUnionInfo: add `defined: bool = true`. Every real
construction (normal unions, error sets, register_type completion) is
"defined" by default; only the two declare-PLACEHOLDER sites set it false:
comptime_vm.declareNominal and lower/comptime.preregisterForwardTypes.
- lower/comptime.checkComptimeTypeResult: reject on `!defined` (never-defined
placeholder) instead of `fields.len == 0`, so an explicitly-defined empty
union passes through while a never-completed declare is still gated.
- types.zig typeSizeBytes(tagged_union): floor the payload area at 8 bytes
when no field carries a payload, mirroring the LLVM lowering — fixes a
verifySizes panic on an empty/all-void tagged_union (IR sized to tag-only,
LLVM laid out tag + [8 x i8]).
Tests:
- examples/1179: repurposed from "empty enum rejected" (now valid) to the
never-defined `declare` case (the remaining rejection); preserves its
issue-0140 regression role.
- examples/1180 (duplicate variant): still rejected, unchanged output.
- examples/0641 (new): construct empty struct/tuple/enum/tagged_union via
define/declare; instantiate the constructible ones; exit 0.
Now that define() is sx over register_type, remove the bespoke metatype define
surface from the comptime VM: the .define callBuiltinVm arm, the defineFromInfo
helper (kind-branching minting), and decodeTypeSlice (its only caller). Remove the
BuiltinId.define enum member. The .declare/.define interceptions in lowering and
their BuiltinIds are now gone; only type_info/field_type remain as metatype
builtins. register_type/decodeMemberSlice stay (shared by the sx define and the
compiler-API graph builder).
define(handle, info) is now an ordinary sx fn in modules/std/meta.sx: it matches
the TypeInfo union and calls the abi(.compiler) register_type primitive with the
matching kind code, decoding the variant/field/element list into []Member. An
all-void enum variant set registers as kind 2 (actual enum); any payload variant
as kind 3 (tagged_union).
To support matching the TypeInfo VALUE in the comptime VM, added tagged-union
value support: kindOf now treats tagged_union as a by-address aggregate, enum_tag
reads the tag word at offset 0, and a new enum_payload arm reads the active
payload at tag_size (both bail loudly on backing_type unions, whose layout
differs). register_type's duplicate-name diagnostics now include the offending
name. Dropped the define interception in tryLowerReflectionCall; the .enum(...)
arg infers TypeInfo from the sx fn's param type via the ordinary call path.
Regenerated 1179/1180 diagnostic snapshots (same span/line; the message now
names register_type instead of define()). define/type_info builtins still exist
pending dead-code removal.
declare(name) is now an ordinary sx fn in modules/std/meta.sx that calls the
abi(.compiler) declare_type primitive — both mint/find the same forward nominal
slot. Removed the bespoke .declare arm from callBuiltinVm and the BuiltinId.declare
member; dropped the declare interception in tryLowerReflectionCall (the call now
routes to the sx fn). preregisterForwardTypes still scans for the literal
declare("Name") spelling so *Name self-references forward-register before the
body lowers (0618). define/type_info/field_type remain builtins.
The 0141 repro relied on a silent-wrong coercion: passing List.items (a
[*]T many-pointer, no length) to a []T parameter passed the bare 8-byte
pointer into a 16-byte {ptr,len} slot — garbage .len, at comptime a segfault
in the VM slice decoder (decodeMemberSlice), at runtime an LLVM verify failure.
Fix (root cause): classify [*]T -> []T as many_to_slice_reject in
conversions.zig and emit a build-gating diagnostic in coerce.zig telling the
user to slice with a length (ptr[0..len]). Guard runComptimeTypeFunc to skip
VM eval once diagnostics.hasErrors() — a type-fn body that failed coercion
holds malformed comptime data (a real host Addr) that would fault the VM's
Ref-level guards.
Land the corrected feature as examples/0640 (List-grown comptime enum via
vs.items[0..vs.len] -> green=7) and the rejection as
examples/1183-diagnostics-many-pointer-to-slice-rejected. Mark issue 0141
RESOLVED.
708/0 corpus + 476/476 unit.
The legacy tagged-Value Interpreter is gone. Relocate the Value result-DTO
+ decodeVariantElements into a new comptime_value.zig (the VM<->host
materialization boundary); repoint comptime_vm/emit_llvm/ir-barrel Value to
it and BuildConfig to compiler_hooks; delete the dead valueToReg bridge;
slim compiler_lib.zig to just the name registry (BoundFn{sx_name} + bound_fns
+ findFn — weldedCompilerFn only validates names); simplify printInterpBailDiag
to comptime_vm.last_bail_reason; drop the unused interp_mod import in lower.zig.
rm src/ir/interp.zig + interp.test.zig.
Value is relocated (not eliminated): it survives only as the slim result DTO
at the VM->valueToLLVMConst boundary; the execution-time marshaling the VM
pivot targeted is gone. Drop dead Value.asString/reflectTypeId.
706/0 corpus + 476/476 unit.
valueToLLVMConst / serializeAggregateValue took a *const Interpreter only to
resolve .heap_ptr data fields via interp.heapSlice — but the VM's regToValue
never produces .heap_ptr (it's interp-internal). Drop the param, remove the
dead interp_inst in emitGlobals, and drop the .heap_ptr fat-pointer data arm
(falls through to the loud bail). Remove the now-unused Interpreter alias.
500/500 unit + 706/0 corpus.
The emitCall inline comptime-call fold (zero-arg comptime callee -> constant)
was the last backend use of the legacy Interpreter. Route it through
comptime_vm.tryEval; a bail falls through to the normal call path. Drop the
interp_mod/Interpreter imports from ops.zig.
500/500 unit + 706/0 corpus.
evalComptimeString (the #insert lowering-time site) was the last user of
the legacy Interpreter.call. Route it through comptime_vm.tryEval instead:
the VM is hardened to bail (never panic) on malformed lowering-time IR
(0737's ret Ref.none), and regToValue dupes the result string into the
lowering allocator so it outlives the VM arena. Drop the now-unused
interp_mod / build_opts imports from comptime.zig.
500/500 unit + 706/0 corpus.
The #compiler struct attribute + #compiler-suffixed bodyless methods were
fully superseded by abi(.compiler) (P5.5) — no sx code uses them.
Remove the hash_compiler token (token/lexer/lsp), the is_compiler_struct /
struct_default_compiler parser machinery + the two compiler_expr body-
synthesis branches, the compiler_expr AST variant, and every
.builtin_expr/.compiler_expr switch arm + == .compiler_expr check across
sema/resolver/semantic_diagnostics/generic/decl/call/calls (kept .builtin_expr).
abi(.compiler) is untouched. Delete the obsolete calls.test.zig dispatch test.
500/500 unit + 706/0 corpus.
The compiler_call op + #compiler hook mechanism was fully superseded by
abi(.compiler) VM-native dispatch (P5.5) — no sx code emits it anymore.
Remove: the compiler_call op variant + CompilerCall struct (inst.zig); the
Builder.compilerCall emitter (module.zig); the two dead producer blocks in
lower/call.zig (compiler_expr-bodied free fns + methods); every consumer
switch arm (emit_llvm, ops.emitCompilerCall, print, interp dispatch); the
interp.hooks field + init/deinit. Strip compiler_hooks.zig down to the still-
live BuildConfig / BuildHooks / AssetDir (delete HookError/HookFn/Registry/
registerDefaults + all hookXxx, and the now-unused interp/Value imports).
Test refs that used compiler_call as a sample unported op now use vec_splat.
501/501 unit + 706/0 corpus.
Remove the comptime_flat/need_vm gate and the vm_result-orelse-legacy
fallback from emit_llvm.zig (runComptimeSideEffects + emitGlobals const-init)
and comptime.zig (runComptimeTypeFunc). The comptime VM now always runs;
a bail is always a build-gating diagnostic, never a fallback. Delete the
now-moot entryNeedsVm. runComptimeSideEffects drops the Interpreter entirely
(VM writes #run output direct to fd 1); emitGlobals keeps a fresh interp_inst
only as the valueToLLVMConst materialization context (the regToValue bridge,
removed with interp.zig in a later step).
#insert (evalComptimeString) still routes through the legacy interp — deferred
until interp.zig deletion.
Reconcile 1654: the comptime asm-global #run now reports the VM's clean dlsym
bail instead of the legacy CannotEvalComptime wrapper (exit still 1).
501/501 unit + 706/0 corpus.
The corpus had ZERO bundler coverage (the stream's named top risk). Add a `.build`
`bundle` directive to the corpus runner: after a successful `aot` build it asserts
each `expect` entry exists under the produced `.app` (repo-relative), then `rm -rf`s
it. macOS-host only — the `.app` + codesign are Apple-specific, so the example is
skipped on other hosts.
`examples/1665-platform-macos-bundle-smoke.sx` sets `bundle_path`/`bundle_id` via a
`#run` config; `default_pipeline` auto-bundles (build.sx imports the bundler, no
explicit `on_build` needed). The directive asserts `Contents/MacOS`,
`Contents/Info.plist`, `Contents/_CodeSignature`. Verified: passes on BOTH gates
(the bundler runs on the legacy interp AND the VM), the `.app` is cleaned up, and a
bad `expect` entry correctly fails (the check is not vacuous). Unit test +
CLAUDE.md `.build`-directive docs updated. 706/0 both gates.
build.sx now `#import`s the sx bundler and `default_pipeline` delegates to its
`bundle_main` when a bundle was requested (emit + link, then wrap the binary into
the `.app`/`.apk`); otherwise it just emit+links via the shared `emit_and_link`
core. The Zig `--bundle`/`post_link_module` dispatch shim is removed — the CLI
bundle flags only feed `BuildConfig`, and `default_pipeline` branches on
`bundle_path()`. Validated end-to-end on macOS: `sx build --bundle App.app
--bundle-id … foo.sx` on a plain program AND auto-bundle from `set_bundle_path`
both produce a valid signed `.app` (correct `Contents/MacOS/` layout, Info.plist,
passes `codesign`, binary runs). Also fixed a pre-existing host-build bug:
target_triple was left empty for host builds → `is_macos()` false → wrong flat
layout; main.zig now exposes the host triple when `--target` is absent.
bundle_main no longer re-calls `build_options()` (the handle is already its `opts`
param).
Fix issue 0125 (root cause): the type-match dispatcher unboxed each interned array
tag to the concrete array type — a whole-array load — and passed it to
`array_to_string` by value, which LLVM scalarized into one SelectionDAG node per
element (~12s / segfault at [65536]u8). The bundler's `format("…{}…")` instantiates
`any_to_string`, so importing it into the prelude surfaced 0125 for any large-array
program. Fix (route 1): `any_to_string`'s `case array:` arm calls `slice_to_string`,
and `lowerRuntimeDispatchCall` detects an ARRAY tag bound to a SLICE param and builds
a `{ptr,len}` slice VIEW of the payload pointer (`unbox_any → [*]elem` is an
int-to-ptr with NO load, paired with the array length) instead of loading the array.
Output is byte-identical (`[a, b, c]`). Pinned as
examples/0056-basic-large-array-format-no-blowup.sx; 0055 drops 12s → 0.2s.
37 `.ir` snapshots regenerated (build.sx now pulls in the bundler's types + the
array-format lowering changed); verified `.ir`-only, zero behavior-stream diffs.
705/0 both gates.
`comptime_vm` exec now handles `bit_and`/`bit_or`/`bit_xor`/`bit_not`/`shl`/`shr`
(a new `bitwise` helper next to `arith`), mirroring the legacy interp's i64 model
exactly: the shift amount clamps to `@min(rhs, 63)` and `shr` is an arithmetic
right shift (sign-extending).
These were unported and bailed; the `shr` gap surfaced via the iOS-device bundler
once P5.5 let it run further (1616). With the port, 1616's strict VM run reaches
the real bundler logic and stops only at the genuinely-unavailable iOS runtime on
macOS (`_UIApplicationMain` / no linked binary under `sx run`), as expected.
New corpus test `examples/0639-comptime-bitwise-shift.sx` folds AND/OR/XOR/NOT/
shl/shr/arith-shr as `::` consts — identical on both evaluators. 704/0 both gates.
`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).
Records the user's decision: drop gate-OFF entirely (VM is the sole comptime
evaluator; delete interp.zig); migrate BuildOptions directly to VM-native
abi(.compiler) arms with NO legacy handlers; ALL bundling + code signing for
every target (macOS/iOS-device/iOS-sim/Android) lives in the sx default_pipeline;
validate against ~/projects/m3te + ~/projects/distribution. Phase 5 steps
P5.5-P5.8 in PLAN-COMPILER-VM.md.
on_build is now the sole post-build callback mechanism. Migrated the 9 callers
(0602/0603/1611/1614/1615/1616 + the platform bundle_main) from
opts.set_post_link_callback(cb) to on_build(cb), giving each callback the
(opt: BuildOptions) param. Deleted set_post_link_callback from build.sx,
compiler_lib (bound_fns + handleSetPostLinkCallback), and the VM arm.
Reworked the P5 smoke tests for the new semantics: an on_build override REPLACES
the build (must emit+link or delegate), unlike the old post-link callback which
ran after the auto-link. 1662 (queries) + 1664 (override+List-grow) now delegate
to default_pipeline for the real build; deleted 1661/1663 (the primitives are now
exercised by every AOT build). bundle_main invoked with pass_options=true.
Benign 37-.ir churn (build.sx shrank). 703/0 both gates.
The compiler's post-IR role shrinks to: codegen -> invoke the build callback.
There is NO Zig auto-emit / auto-link anymore; emit + link are sx-called actions.
- emit_object() is now an ACTION (verify + emit via a host BuildHooks vtable),
returning the object path. New query primitives build_output/build_target/
build_frameworks/build_flags (data reads from the merged BuildConfig).
- library/modules/build.sx imports compiler.sx and defines default_pipeline:
emit_object -> gather c_object_paths -> link(objs, output, libs, fws, flags,
target). The std<->build import cycle is handled by the resolver.
- The compiler FORCE-LOWERS default_pipeline (well-known name) and AUTO-INVOKES
it post-codegen when no on_build/set_post_link_callback override was
registered (driver's final fallback: invokeByName default_pipeline).
- Prelude-less programs (e.g. asm tests) don't import build.sx, so the BUILD
path auto-imports modules/build.sx (idempotent if already transitive) so
default_pipeline is always available. JIT sx run is untouched (emits in-process).
- Removed the build cache short-circuits (incompatible with the always-run sx
driver; a future cache can live in default_pipeline).
Benign 37-.ir churn (build.sx grew); zero behavior changes (verified diff is
.ir-only). 705/0 both gates.
Per user design: on_build(build) is the build-callback registrar (a free fn),
generalizing set_post_link_callback — the callback is (opt: BuildOptions) ->
bool and the compiler invokes it post-codegen WITH the BuildOptions handle.
- VM: callCompilerFn 'on_build' arm + legacy handleOnBuild, both set
post_link_callback_fn + a new BuildConfig.post_link_takes_options flag.
- comptime_vm: runEntry refactored to runEntryArgs(extra) (implicit ctx +
explicit args); new public runBuildCallback(..., pass_options) passes the
opaque BuildOptions handle (one word) after the ctx. The fat-config
marshaling fear is moot — the handle is a single null-sentinel word.
- core.invokeByFuncId/invokeByName take pass_options (was an unused args
slice); main.zig passes comp.getPostLinkTakesOptions().
- build.sx: on_build decl (set_post_link_callback kept for now).
Smoke test examples/1664-platform-on-build-callback (AOT): #run on_build(build)
with build :: (opt: BuildOptions) -> bool; the callback is invoked with the
handle arg (runEntryArgs param-count match) and runs the primitives.
Benign .ir churn (37 snapshots: type table +1 for the on_build fn type +
global renumber; behavior identical). 705/0 both gates.
Per user direction: the low-level abi(.compiler) primitive surface is the
comptime 'compiler' library, so name the file compiler.sx (a peer of build.sx)
instead of the interim std/build.sx — which also frees the 'build' name for the
default build IMPLEMENTATION (default_build + on_build slot), which will live in
modules/build.sx alongside the BuildOptions DSL.
Updated the two example imports + the plan's Phase 5 file-split note. 704/0
both gates.
The one genuine action primitive: link(objects, output, libraries, frameworks,
flags, target) in library/modules/std/build.sx. Per the user decision to drop
fallibility from the build callback, link is plain VOID — a link failure bails
on the VM (hard build error), no -> ! / failable-tuple needed.
comptime_vm.zig can't depend on the driver (core/main/target), so link
dispatches through a new compiler_hooks.BuildHooks { ctx, link } vtable that
main.zig installs into BuildConfig.build_hooks before the post-link callback.
The driver side is main.LinkHooksCtx (unions explicit + CLI link flags, calls
target.link). New VM readers readStringList / readStringArg (inverse of
makeStringList) decode the List(string)/string args from flat memory.
Smoke test examples/1663-platform-build-pipeline-link (AOT): a post-link
callback re-links the build's own objects (c_object_paths + emit_object) into a
temp output via sx link — the relinked binary is a functional executable that
runs. Negative-probe verified (bad path -> ld fails -> ComptimeVmBail -> build
exit 1). The Zig driver still auto-links; removing that is P5.4.
704/0 both gates.
The compiler emits the sx object eagerly (the Zig driver, before the post-link
callback), so emit_object is a QUERY (not an action): it returns the path from
a new BuildConfig.object_path field main.zig forwards — no driver vtable. This
completes the build-pipeline QUERY primitives (emit_object / c_object_paths /
link_libraries); only link (the genuine action) remains for the vtable step.
Extended examples/1662 to also assert emit_object().len > 0. 703/0 both gates.
Two abi(.compiler) build-pipeline primitives the sx driver will pass to link:
- c_object_paths() -> List(string) (#import c companion objects)
- link_libraries() -> List(string) (#library names)
They live in a new stdlib home library/modules/std/build.sx and are serviced
by comptime_vm.callCompilerFn reading two new BuildConfig fields that main.zig
forwards before the post-link callback. New reusable VM helper makeStringList
builds a List(string) in flat memory from the call's result type offsets
(target-aware); invoke/callCompilerFn now thread ins.ty for that. Legacy
handlers bail loudly (VM-only by nature — post-link; List(string) isn't
faithfully buildable in the legacy Value model, 0141).
Smoke test examples/1662-platform-build-pipeline-queries (AOT + a 1-line C
#source → one object): a post-link callback verifies the VM-built list is
well-formed; build exit 0 only if so (negative-probe confirmed a real guard).
emit_object + link (the actions) deferred to P5.2b — they replace the Zig
driver's auto-emit/auto-link and need a host-installed callback vtable.
703/0 both gates.
core.invokeByFuncId routes the post-link callback through comptime_vm.tryEval
instead of the legacy Interpreter. REQUIRED because the sx build driver
allocates/grows Lists, which the legacy interp can't do at comptime (issue
0141: struct_get: base has no fields); the VM can. No fallback (a
side-effecting post-link callback can't double-execute): a VM bail is a hard
build error (comptime_vm.last_bail_reason, surfaced by printInterpBailDiag).
BuildConfig + import_sources threaded in; non-empty args rejected loudly.
flushInterpOutput deleted (VM out writes direct via host-FFI).
Smoke test examples/1661-platform-post-link-vm-list (AOT): a post-link
callback grows a List to 3 + returns len==3, so the build succeeds (exit 0)
only via the VM. First corpus coverage of the post-link path.
702/0 both gates.
Lands the full VM/compiler-API arc on branch reify (701/0 both gates):
- abi(.compiler) ABI replaces abi(.zig) extern compiler + the fake
#library "compiler"; bodiless decl = compiler-API surface, bodied =
user compiler-domain fn (lowered for VM eval, emit-skipped).
- out is a plain sx fn (libc write) — the out builtin deleted; the VM
handles it via host-FFI. trace_resolve + interp_print_frames ported.
- 4B VM-native diagnostics: 1179/1180 render proper comptime type
construction failed: under strict.
- S5a: build_options/set_post_link_callback on abi(.compiler) with
BuildConfig threaded into the VM (green intermediate).
- 0522 fixed (describe(args: []Type)); regression 0638.
Strict deletion-gate down to 4 compiler_call bails (1609/1614/1615/1616)
+ 1654 (legitimate unresolvable-symbol diagnostic).
buildPackSliceValue (lower/pack.zig) materialized a bare `$<pack>` []Type slice
as []Any (16-byte elements) — a stale mapping from before the dedicated Type
builtin (.type_value, 8 bytes) replaced Type -> .any. It stored 8-byte const_type
words into 16-byte slots, so a []Type reader (8-byte stride) read [t0, pad, t1, ...]
instead of [t0, t1, ...]. The legacy interp's tagged-Value model hid it; the
byte-accurate comptime VM exposed it (a `..$args` pack forwarded as a []Type
argument across a call read its elements shifted/garbled).
Fix: build the pack-slice array + slice as .type_value (8 bytes). Removed the
stopgap type_name .unresolved guard (379ed05) now that the root cause is fixed.
Regression test examples/0525-packs-pack-as-type-slice-arg.sx (outer(42,"hi",true)
-> inner($args: []Type) -> "i64 string bool"). 700/0 both gates; issue 0143 RESOLVED.
0143: a ..$args pack forwarded as a []Type argument across a call is backed by a
[N x Any] (16B) array but viewed as []type_value (8B) -> half-stride reads. A
lowering bug the legacy Value model masks; the byte-accurate VM exposes it. Blocks
examples/0114 on the VM. Filed per CLAUDE.md (not worked around; the type_name
.unresolved guard only makes the VM decline rather than emit garbage).
Checkpoint also records the sequencing insight: comptime `out` (print) can only
land once the fallback is removed (a print-then-bail double-prints under the legacy
re-run), so side-effecting ops + fallback-removal are the FINAL step; pure ops +
migrations land first.
Port two pure (side-effect-free) comptime ops toward the no-fallback gate:
- switch_br: multi-way branch on an i64 discriminant (enum/error tag, or a
.type_value whose word is its TypeId index) — mirrors the legacy
asInt-orelse-asTypeId switch.
- type_name(x): a Type value (.type_value word) or Any box ({tag,value}; tag ==
type_value means the boxed Type's id is in the value slot) -> table.typeName.
Guards an .unresolved TypeId (bail, not "<unresolved>") to surface a bad
slice/pack read instead of emitting garbage (see issue 0143).
These are correct in isolation (0520-0524 run green under strict mode) but flip
nothing yet because their examples also print via `out`, which can only land at
the end state: under the legacy fallback a print-then-bail double-prints (no
re-run rewind), so `out` is deferred until the fallback is removed. 699/0 both
default gates.
Add -Dcomptime-flat-strict / env SX_COMPTIME_FLAT_STRICT (implies comptime_flat):
at all three comptime sites (type-fn in lower/comptime.zig, const-init + #run in
emit_llvm.zig) a VM bail becomes a build-gating error naming the reason INSTEAD of
falling back to legacy. 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, interp.zig can be deleted.
Default behaviour unchanged (699/0 both default gates). Fixed a wiring bug: the
type-fn site's local comptime_flat didn't include the strict flag (every type-fn
falsely reported <unknown>); strict now implies flat there too.
Swept the gap list (19 strict bails): switch_br (5, + unmasks a []Type-across-call
silent-wrong in 0114), compiler_call (6, = the BuildOptions->abi(.zig) extern
compiler migration), out (2), type_name (1), global_addr (1), interp_print_frames
(1), 2 negative-test diagnostics (1179/1180), 1 dlsym (1654). Recorded as the
deletion checklist in CHECKPOINT-COMPILER-API.md.
Direction correction (user): the VM does NOT get a transitional compiler_call →
hook-registry shim. BuildOptions is re-expressed as abi(.zig) extern compiler
functions (the compiler-API the VM already dispatches via callCompilerFn), and the
#compiler attribute + compiler_call op + Value-based hook Registry are deleted.
Also record that 4D (host FFI) is DONE via the arena/absolute-pointer allocator
(the earlier pin/tag hazard is moot — the arena never moves an allocation), and
that 0602/0603 stay on legacy fallback until the BuildOptions migration lands.
No code change (reverted the speculative compiler_call bridge).
Update the resume banner: box_any/unbox_any (4A.1), arena allocator with absolute
host-pointer Addr (4D.0), general host-FFI escape (4D.1), slice/string args + float
guards (4D.2) all landed and green (699/0 both gates). Next is 4D.3 (#compiler
hooks / compiler_call) with the hook-ABI + build_config findings recorded for a fast
restart.