inferGenericReturnType resolved a generic call's return-type AST ($R, !E) in
the CALL-SITE module context. For a re-exported fn the error-set name (LE /
IoErr, re-exported as LE :: lib.LE) resolved through the call-site alias to a
TypeId NOT tagged .error_set, so the planned result was a tuple whose last
field wasn't an error set — errorChannelOf saw a plain tuple and the value-
failable's ! channel was lost (try/or rejected it / built a malformed i1 PHI).
monomorphizeFunction already pins the source to the fn's defining module
before resolving the return type; inferGenericReturnType did not, so the
planned call-result type disagreed with the instance's real signature. Fix:
pin the source to fd.body.source_file around the return-type resolution
(binding-build stays in the call-site context — its args are typed there).
Regression test examples/1058-errors-reexport-value-failable-channel.sx
(+ companion lib.sx). Suite green 732/0.
LLVM rejects a sub-byte atomic memory access (must be byte-sized), so
Atomic(bool) — bool lowers to i1 — failed verification on load/store. The
atomic emitters in src/backend/llvm/ops.zig now perform a sub-byte access in
its byte storage type (i8) and trunc/zext the value at the boundary (new
atomicByteType helper: i8 for .bool, null otherwise). rmw/cmpxchg are left
as-is on purpose — a bool rmw/CAS is rejected at the sx level (integer-only),
so a sub-byte element never reaches those emitters.
Regression test examples/1705-atomics-bool-byte-promoted.sx. Suite green 729/0.
Unblocks Future.canceled: Atomic(bool) in the B1.2 async layer.
The generic-inference engine could not bind a $T from a generic-struct
argument head. Four gaps, all on the inference + UFCS dispatch path:
- extractTypeParam / matchTypeParam(Static) gained a parameterized_type_expr
arm: recover the arg instance's recorded per-param bindings
(struct_instance_bindings + the template's ordered type_params via
struct_instance_author) and recurse positionally, so $T binds from
Box($T) <=> Box(i64) like it does from []$T <=> []i64. This also fixes
the pointer case — *Box($T) recurses into its Box($T) pointee.
- The pointer_type_expr arm now falls through to match the pointee against a
non-pointer arg (auto-address-of: a *Box($T) param accepts a by-value
Box($T), e.g. the UFCS receiver b.m()).
- ExprTyper.inferType gained a .lambda arm building the closure type from the
lambda's annotations, so the UFCS binder (which types args from the raw AST
before they are lowered) can bind a Closure(..) -> $R from the worker's
declared return type.
- A pack UFCS target (worker: Closure(..) -> $R, ..$args) now routes through
the same lowerPackFnCall the direct call uses, with the receiver spliced in
as args[0] (lowerPackFnCall reads only call_node.args, never the callee).
Regression tests: examples/0214 (direct + UFCS closure-return pack) and
examples/0215 (by-value / pointer / multi-param / nested / UFCS-auto-ref
generic-struct-head inference). Suite green 728/0.
Threads an `Io` capability onto `Context` exactly like `Allocator`: a
`protocol #inline` whose process-wide default is a stateless `CBlockingIo`
(the mirror of `CAllocator`), installed in `__sx_default_context`.
Library (library/modules/std):
- core.sx: `Io` protocol (spawn_raw / suspend_raw / ready / poll / now_ms /
arm_timer) + `SpawnOpts` / `PinTarget` / `ParkToken`; `Context` gains an
`io: Io` field LAST (allocator stays index 0, data stays index 1).
- io.sx (new): `CBlockingIo` + `impl Io` (blocking M:1 semantics — now_ms is
a real monotonic clock, the rest are no-ops/0; suspend never called);
`Future($R)` { value; state: FutureState; err: IoErr; park; task; canceled:
Atomic(bool) } with `Value :: R`; the async ergonomic layer
`async` / `async_void` / `await` (value-carrying `(R, !IoErr)`) / `cancel`.
Built with the verified `= ---` + field-assign + `Closure(..$args) -> $R` +
`..$args` idiom (NON-void $R only — Future(void) is deferred per issue 0150).
- std.sx: re-export the Io surface + the io.sx tail.
Compiler (src/ir):
- protocol.zig `emitDefaultContextGlobal` + comptime_vm.zig
`materializeDefaultContext`: both materializers of `__sx_default_context`
now build the inline CBlockingIo->Io vtable (7 words) at the new field.
- stmt.zig `lowerPush`: `push Context.{...}` now INHERITS omitted fields from
the ambient context (seed the slot from current_ctx_ref, overwrite only the
literal's named fields) — correct capability-bag semantics, so the partial
`push Context.{ allocator = X }` sites don't zero a null `io` vtable.
- protocols.zig + lower.zig + error_analysis.zig: record protocol-impl method
names so the "declared `!` but never errors" lint skips a conforming impl
whose `!` is dictated by the protocol contract (e.g. Io.suspend_raw).
37 `.ir` snapshots regenerated: layout-only (the Context type now carries the
Io field, shifting type-table numbering); no stdout/stderr/exit changes.
The blocking Io + now_ms + Future/async work when `async` is called with the
receiver passed explicitly; the user-facing UFCS form `context.io.async(...)`
is blocked on a separate UFCS generic-inference bug (filed next).
Suite: 726 ran, 0 failed.
Adds the blocking-Io async/await example with a seeded empty .exit marker.
The example fails today (Io protocol + context.io + async/await not yet
implemented); the next commit lands the Io interface + blocking impl +
both __sx_default_context materializers + push-inherit fix to turn it green.
Worker is a lambda with annotated params (the verified B1.2 idiom);
named-fn workers are deferred pending a :: callable-param feature.
The .pure->.naked rename (a7fe165) git-mv'd examples 1800-1803 to their
naked names but the perl content edit (abi(.pure) -> abi(.naked) in the
bodies/comments) was never re-staged, so HEAD carried the renamed files
with stale abi(.pure) bodies — which the compiler now rejects ("unknown
ABI"). The working tree had the correct .naked bodies uncommitted; this
commits them so HEAD parses + builds clean.
A fiber needs its own root Context (the spawner's snapshot), not the
ambient one. Probed whether that needs compiler support: it does not.
context is an implicit slot-0 *Context param (call-carried, rides the
callee's own stack) and push Context allocates on the caller frame —
never TLS, never re-read from the __sx_default_context global mid-stack.
So the spawn convention is pure library sx:
snap := context; // snapshot the spawner's context
f := Fiber.{ root = snap }; // store it
push f.root { entry(args) } // trampoline installs it as the fiber root
examples/1804-concurrency-context-snapshot.sx locks it: a trampoline
running under ambient ctx 99 installs a stored snapshot (42); the body
reads 42, and the push scope restores 99 on exit. No fiber runtime yet
(B1.3) — this proves the plumbing it builds on.
The design doc's "lower context as swappable indirection, never raw
TLS" guarded a non-problem — context was already param-carried.
Suite green (726/0).
"pure" universally means side-effect-free (GCC __attribute__((pure)),
FP purity, D's pure) — the opposite of a register-clobbering context
switch. The concept is "naked": no compiler-generated prologue/epilogue,
body is raw asm that emits its own ret. That is the established term
everywhere (LLVM's naked function attribute — which we literally emit —
plus Zig callconv(.naked), Rust #[naked], GCC/Clang __attribute__
((naked))). Rename the keyword + everything keyed off it so concept,
surface, field, and the emitted LLVM attribute all agree.
- ast.zig: ABI enum variant pure -> naked (+ doc).
- parser: accept abi(.naked); error text updated.
- IR Function.is_pure -> is_naked; type_resolver/decl/generic/pack/
emit_llvm references updated; diagnostics say abi(.naked).
- examples 1800-1803 renamed *-pure-* -> *-naked-* (source + expected/
snapshots; .ir/.exit/.stdout/.stderr are byte-identical — the emitted
IR is unchanged, only the keyword spelling differs).
- docs (PLAN-FIBERS, CHECKPOINT-FIBERS, PLAN-POST-METATYPE, the design
roadmap, the compiler-API checkpoint/design) updated; the naming
rationale now records why .naked over .pure.
No semantic change — pure cosmetics. Suite green (725/0).
Adversarial review of B1.0b found a param-bearing abi(.pure) function
emitted invalid LLVM ("cannot use argument of naked function" — loud
verifier error, not silent) because the param-alloca loop spilled the
args to stack slots, which a naked function cannot have.
Fixed forward — this ENABLES the B1.3 context-switch use case rather
than rejecting it: gate the param-alloca loop on fd.abi != .pure in
decl.zig (both body-lowering paths) and generic.zig. A naked function's
args stay in their ABI registers and are read directly by the asm body
(e.g. swap_context reads from/to from x0/x1); the LLVM args are
declared-but-unused, which the verifier allows.
examples/1803-concurrency-pure-asm-param.sx: naked add(a, b) reads x0/x1
(add x0, x0, x1; ret) -> 40 + 2 = 42. aarch64-pinned.
Pack abi(.pure) (variadic + naked — nonsensical, can't read a runtime
pack from registers) left unsupported: pack.zig's param loop is
intertwined with comptime-param/#insert handling, so that case still
hits the loud verifier error. Documented in the checkpoint.
Also updates PLAN-FIBERS / CHECKPOINT-FIBERS for B1.0 completion.
B1.0 complete. Suite green (725/0).
Flip the B1.0a emit bail to real emission. The emit_llvm declaration
pass now adds LLVM's naked + noinline + nounwind attributes for an
is_pure function and skips frame-pointer=all (incompatible with a
frameless function); Pass 2 emits the body normally, and the naked
attribute makes the backend emit it verbatim (the inline asm + its own
ret) with no prologue/epilogue.
IR shape verified:
; Function Attrs: naked noinline nounwind
define internal i64 @answer() #0 {
entry:
call void asm sideeffect "...ret...", ""()
unreachable
}
The caller invokes it as an ordinary () -> i64 call (.pure is
call_conv == .default).
- examples/1800-concurrency-pure-asm.sx: now green, aarch64-pinned
(.build macos) -> exit 42 + .ir snapshot.
- examples/1801-concurrency-pure-generic.sx (renamed from -bail): the
generic .pure now emits a correct naked answer__i64 (exit 42),
proving generic.zig produces a naked body, not a framed one.
- examples/1802-concurrency-pure-asm-x86.sx: x86_64 cross sibling
(.build x86_64-linux, ir-only here); .ir locks naked + movl $42,%eax.
- unit test in emit_llvm.test.zig asserts the naked attribute is present
and frame-pointer absent on an abi(.pure) function.
Suite green (724/0).
Adversarial review of dd363ca found is_pure was set only at the two
declareFunction decl sites. Generic monomorphization (generic.zig) and
pack expansion (pack.zig) create the IR Function via a different path
and left is_pure false, so a generic abi(.pure) instance bypassed the
emit bail and silently shipped a framed body — it returned 42 but
leaked the prologue's stack adjustment (the exact SP-in != SP-out
corruption the lock exists to prevent).
Both paths now set is_pure and route .pure bodies through the asm-only
+ unreachable cap, mirroring the decl path. Locked by
examples/1801-concurrency-pure-generic-bail.sx (generic .pure reaches
the loud bail).
The review's other CRITICAL (a .pure lambda) is a false positive:
isLambda's return-type scan (parser.zig:3652) breaks on the abi
keyword, so a .pure lambda is unparseable and parseLambda's abi
handling is never reached. Latent isLambda/parseLambda inconsistency,
not a B1 concern.
Suite green (723/0).
First implementation step of Stream B1 (fibers). Make the inert abi(.pure)
ABI carry an is_pure flag through lowering, with LLVM emission deliberately
bailing loudly until B1.0b — the lock half of the lock->green cadence.
- IR Function.is_pure, set from fd.abi == .pure at both declareFunction
decl sites.
- funcWantsImplicitCtx skips .pure (no synthetic __sx_ctx, mirroring the
.c skip): a pure fn reads args from ABI registers, an implicit ctx would
occupy a register slot the asm doesn't expect.
- both body-lowering paths bypass lowerValueBody for .pure: lower the asm
body as statements + cap with unreachable. A pure body has no sx return
(the asm rets itself), so the implicit-return diagnostic must not fire.
- emit_llvm Pass 2 bails loudly when func.is_pure (build-gating nonzero
exit) rather than emit a framed body, whose epilogue would corrupt a
context switch's deliberate SP-in != SP-out.
examples/1800-concurrency-pure-asm.sx: one host example (no .build pin --
the bail fires before instruction selection, so it is host-independent),
locked to the bail snapshot. B1.0b flips emit to LLVM's naked attribute +
asm-only body and pins the example per-arch.
The sx-facing name is "pure" throughout (field, diagnostic); LLVM's naked
attribute is only the B1.0b lowering mechanism. Suite green (722/0).
Final whole-stream adversarial review came back CLEAN (no CRITICAL/MEDIUM/LOW).
Close the one informational gap it noted: extend examples/1703 with a #run
comptime swap so swap's comptime VM arm is locked (742, matches runtime) — every
op now has comptime↔runtime corpus coverage.
Docs: PLAN-ATOMICS.md status banner (COMPLETE); PLAN-POST-METATYPE.md Stream A
marked done (unblocks B2-channels + C-parallel); readme.md gains a user-facing
Atomics section. Suite green (721/0).
emitAtomicRmw xchg arm (swap) and emitAtomicFence (LLVMBuildFence) now real.
examples/1703 (swap old=7/now=42, 'atomicrmw xchg') + 1704 (fence release/acquire/
seq_cst) green. Unit test 'emit: atomic swap (xchg) + fence'. Stream A
(atomics) is feature-complete: load/store, RMW (add/sub/and/or/xor/min/max),
compare_exchange[_weak], swap, fence. Suite green (721/0).
swap (atomicrmw xchg) and a standalone fence wired end-to-end except LLVM
emission (both bail loudly; A.3b makes them real).
- RmwKind += xchg; atomic_swap intrinsic + swap method reuse the atomic_rmw op.
- new atomic_fence op (+ AtomicFence) — ordering-only, void; fence($o)/atomic_fence
intrinsic; recognizer rejects .relaxed (LLVM has no monotonic fence).
- comptime_vm: xchg = store operand/return old; fence = no-op (single-thread).
- examples 1703 (swap) + 1704 (fence) locked to bails; 1187 (relaxed-fence reject).
- 1186 converted to a direct-intrinsic call → stable user-file diagnostic span
(the lib-forward-site span shifted when atomic.sx grew — fragile-snapshot fix).
Also fixes a latent A.2 comptime-CAS bug found while here: the success/null
has_value write was 'writeWord(addr, SIZE=0, val=1)' — a 0-byte no-op, correct
ONLY because allocBytes zero-inits (REJECTED-PATTERNS 'coincidentally correct').
Now writes the flag explicitly (size=1, val=0). Suite green (721/0).
emitAtomicCmpxchg: LLVMBuildAtomicCmpXchg (success/failure orderings,
singleThread=0) returns a {T, i1} pair; LLVMSetWeak for the weak variant. The
sx ?T result (null = SUCCESS) is built as { extractvalue 0 (actual value),
xor(extractvalue 1 (success), true) } -- has_value = NOT success. Integer-only
(recognizer guard), so never a pointer/niche optional.
examples/1702 green: successful CAS returns null (value updated), failing CAS
returns the actual value (unchanged), weak retry loop increments a counter
(100 -> 105). LLVM IR shows `cmpxchg ... acq_rel acquire` and `cmpxchg weak`.
Unit test `emit: atomic cmpxchg (strong + weak)` locks `cmpxchg` + the weak
marker. Suite green (718/0).
compare_exchange/_weak wired end-to-end except LLVM emission (bails loudly;
A.2b makes it real). New IR op atomic_cmpxchg + AtomicCmpxchg{ptr, cmp, new,
val_ty, success_ordering, failure_ordering, weak}; result type = ?T (null =
SUCCESS, failure carries the actual value for retry). print arm; emit dispatch
-> emitAtomicCmpxchg (BAILS). comptime_vm arm does real single-thread CAS (read
actual / compare / store-on-equal / build ?T: success->none, failure->some;
weak == strong at comptime). Recognizer extended (atomic_cmpxchg/_weak, 6 args)
-- CAS restricted to INTEGER T (loud reject); BOTH orderings resolved via
atomicOrderingFromNode; dual-ordering validation (failure may not be
release/acq_rel nor stronger than success, via atomicOrderingRank). Methods
compare_exchange/_weak on Atomic($T) with comptime $success/$failure: Ordering.
examples/1702 locked to the bail; examples/1186 locks a rejected ordering pair.
Suite green (718/0).
Adversarial review CRITICAL: the comptime VM's atomic_rmw min/max arm called
@max/@min directly on Reg (=u64) values for SIGNED types, doing an UNSIGNED
compare — so comptime fetch_min/max on negatives diverged from the runtime LLVM
atomicrmw min/max (signed). Fix: reinterpret as i64 in the signed branch before
comparing, bitcast back (mirrors the unsigned branch + the emit-side signedness).
Closes the coverage gap that hid it: extend examples/1701 with signed min/max on
a negative at BOTH comptime (#run) and runtime — they now agree (3 / -5). Suite
green (716/0).
emitAtomicRmw: LLVMBuildAtomicRMW (binop from RmwKind; signed Min/Max vs
unsigned UMin/UMax from val_ty; singleThread=0; LLVM supplies ABI alignment).
examples/1701 green (add/sub/and/or/xor/min/max return old values, results
verified). Unit test 'emit: atomic rmw (add + signed/unsigned min)' locks
'atomicrmw add' + signed 'min' vs unsigned 'umin'. Suite green (716/0).
fetch_add/sub/and/or/xor/min/max wired end-to-end except LLVM emission (bails
loudly; A.1b makes it real). New IR op atomic_rmw + RmwKind (no nand) +
AtomicRmw{ptr, operand, val_ty, ordering, kind}. print arm; comptime_vm arm
implements real single-thread RMW (load/compute/store/return-old, signed|unsigned
min/max from val_ty). Recognizer extended (rmwKindFromName) — RMW restricted to
integer T (float fadd / pointer RMW out of scope, rejected loudly); all orderings
valid for RMW. Methods fetch_* on Atomic($T) with comptime $o: Ordering.
examples/1701 locked to the bail. Suite green (716/0).
Migrate Atomic methods from seq_cst-only to the explicit ordering surface now
that comptime value params work on generic-struct methods (workers 3c4305f /
d7a6857 / d95ba0a):
- atomic.sx: load/store take a comptime $o: Ordering (explicit, Rust-style; no
default, matching design 4.6). a.load(.acquire) -> 'load atomic .. acquire'.
- call.zig: atomicOrderingFromNode resolves a comptime-bound ordering identifier
via comptimeIntNamed (+ atomicOrderingFromTag); documents the sx-Ordering <->
IR-AtomicOrdering declaration-order invariant. The per-op validity guard fires
through the method path (a.load(.release) is a compile error).
- 1700 migrated to explicit orderings (output unchanged 7/42/43).
Suite green (715/0).
A free function's $o comptime value param binds via lowerComptimeCall →
bindComptimeValueParams. The generic-struct-instance method path
(b.pick(.b)) took a different dispatch route: genericInstanceMethod →
ensureGenericInstanceMethodLowered emitted a plain call to the
monomorphized FuncId, never checking hasComptimeParams — so the method's
$o was never bound and lowered to 'unresolved o'.
Fix: when the selected generic-instance method declares comptime params,
route through the new lowerComptimeGenericInstanceMethod, which composes
the two mechanisms — installs the struct instance's type_bindings (so T /
*Box(T) resolve), pre-binds the receiver self as a normal pointer-param
alloca (so self.field reads work in the inlined body), then routes the
remaining ($) params through lowerComptimeCallArgsSkip(skip_params=1).
That reuses bindComptimeValueParams, so comptimeIntNamed /
comptimeValueRefNamed resolve the value param inside the method body,
identically to the free-function path.
lowerComptimeCall is refactored into lowerComptimeCallArgs(Skip) cores
parameterized over the effective arg-node slice + a leading skip count;
the original free-call entry point is unchanged behaviorally.
Loud-diagnostic behavior preserved: a non-constant / unknown-variant arg
still emits the value-param diagnostic, never a silent default. Int value
params ($n: i64) remain unbound — a pre-existing limitation shared with
free functions, orthogonal to this fix.
Locks examples/0642 (enum + tagged-union comptime value params on a
generic-struct method, incl. self.field read and comptimeIntNamed via a
type-position [o]i64).
`$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.
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).
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.
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.
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.
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).
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.
Extract marshalExternArg: a scalar/pointer word passes verbatim (a cstring arg
already works as a pointer word via 4D.1); a string/slice {ptr,len} fat pointer is
copied into a NUL-terminated arena buffer and its char* passed -- mirrors the legacy
marshalExternArg, and is what the bundler's popen(cmd: [:0]u8, ...) needs.
Add float guards on args AND returns: floats are kindOf == .word but the host_ffi
trampolines have no float variant, so bail loudly rather than miscall through an
integer register (the legacy interp doesn't support float FFI either -> parity).
New example 0637-comptime-extern-slice-arg (#run strlen("hello, world") with a
[:0]u8 param -> 12) runs HANDLED on the VM, byte-matching legacy. 699/0 both gates.
The FFI escape now covers scalar/pointer/cstring/slice args + scalar/pointer returns.
Replace the "extern not ported -> bail" stub in Vm.invoke with callHostExtern:
resolve the symbol via host_ffi.lookupSymbol (dlsym RTLD_DEFAULT) and dispatch
through the host_ffi trampolines, like the legacy interp.callExtern.
Marshalling is trivial now that Addr is a real host pointer (4D.0): every WORD-kind
arg passes as usize verbatim (a scalar's bits OR a pointer, no translation), and a
pointer return is a valid Addr. Picks callPtrRet (void*-ABI) for pointer-ish
returns, callIntRet (i64-ABI) otherwise; honors variadic. Non-word
(aggregate/string/float) args+returns bail loudly (4D.2 adds them). One general
mechanism for all externs, not per-builtin special cases.
New example 0636-comptime-extern-libc (#run toupper(97)/tolower(90) -> 65/122) runs
HANDLED on the VM, output byte-matching legacy. 698/0 both gates.
type_resolver "Type" -> .type_value; const_type result + emitConstType now a
bare 8-byte i64 handle (not a 16-byte Any box). Migrated every .any ref meaning
"a Type value", leaving real boxed-Any refs:
- "Any holds a Type" meta-marker tag .any -> .type_value at all 4 consumers
(reflectArgTypeId, reflectTypeId, the comptime type_tag-as-struct path,
resolveTypeCategoryTags "type").
- reflection-builtin return types (type_of/declare/define) -> .type_value;
runtime type_of(any) reads the tag as a .type_value (no re-box).
- expr_typer: a bare type-name expr is .type_value (backtick is_raw exempt).
- reflectionArgIsType accepts .type_value OR .any (a reflection arg can be a
bare Type or a boxed Any).
- comptime switch_br accepts a .type_tag discriminant (type-category match).
- a bare function name in a Type slot -> const_type(its function type), not a
func-ref (fixes a JIT crash); old string-box kept only for genuine Any params.
- field-not-found diagnostic + formatTypeName render .type_value as "Type".
Fixed 3 unit tests asserting the old .any behavior. 697/0 both gates (gate ON
bails cleanly to legacy since the VM doesn't model Type values yet) + 494 unit
tests. 24 snapshots regenerated (22 .ir const_type shape; 2 .stderr Any->Type).
Add TypeId.type_value (slot 19) + matching TypeInfo.type_value variant: an
8-byte type handle, distinct from the 16-byte boxed .any. All types.zig layout
handlers wired (size/align 8, display "Type", hash/eql); toLLVMTypeInfo -> i64.
Reserve builtin headroom: first_user 19 -> 100 (slots 20-99 padded with the
unresolved tripwire) so future builtins don't renumber user TypeIds / churn
sx ir snapshots. 22 IR snapshots regenerated (pure renumber to 100-base).
type_resolver still returns .any for "Type" — nothing produces .type_value
yet, so no behavior change. 697/0 both gates.