Commit Graph

629 Commits

Author SHA1 Message Date
agra
1b0c857b91 fix: struct-literal → optional coercion + #get through optional chain (issue 0160)
Two fixes for optional interactions surfaced by the #set/#get review. The
original issue 0160 mis-diagnosed (A) as an optional-chain bug; the chain works
fine for real fields. The actual bugs:

(A) A bare struct literal `.{ ... }` against an optional target `?T` was built
into the optional's {payload, has_value} layout instead of the inner T, then
re-wrapped — corrupting the value (a multi-field payload's first field clobbered
by the has_value flag, or a `?T` arg silently null) or failing LLVM
verification. lowerStructLiteral now builds the inner T, materializes it, and
wraps via coerceToType; lowerVarDecl's previously-UNCONDITIONAL optional wrap is
guarded so an already-`?T` value isn't double-wrapped. Fixed across var-decl,
arg, return, nested field, reassignment, and array-element contexts.

(B) `#get` accessors are now reachable through an optional chain (`obj?.getter`):
lowerOptionalChain dispatches the getter via a synthetic receiver, and
expr_typer types `obj?.getter` through a shared getterReturnTypeOnDeref helper
(handles `?T` and `?*T`, value and pointer optionals, and generic-instance
getters like List.len). The `#set` write side through `?.` is intentionally left
matching real-field behavior (optional-chain assignment unsupported).

Regression tests: examples/optionals/0906 (struct-literal → optional) and 0907
(accessor through chain). issues/0160 marked RESOLVED with the corrected root
cause.
2026-06-22 18:28:57 +03:00
agra
9523c29173 feat: #set property accessors (write counterpart of #get)
A method `name :: (self: *T, value: V) #set { ... }` (or `=> expr;`) is the
write counterpart of a `#get` accessor: `obj.name = rhs` dispatches to it as
`obj.name(rhs)` when no real field matches. Plumbed parallel to `#get`:

- lexer/token `#set`; `FnDecl.is_set` + `Function.is_set`; parsed in the same
  marker slot as `#get` (no return type, exactly self + one value param).
- get+set coexistence: a setter registers/mangles/dispatches under an effective
  `name$set` name (`$` is illegal in sx identifiers, so unmistakable), keeping a
  same-name `#get` under the plain `name`. Resolution is declaration-order-
  independent: a plain read query picks the non-setter, a `name$set` write query
  picks the setter (accessorEffName / accessorNameMatches / structMethodFn).
- write dispatch in lowerAssignment via tryLowerPropertyAssignment: plain assign
  synthesizes `obj.name$set(rhs)`; compound `OP=` is get-modify-set and
  evaluates the receiver EXACTLY ONCE (bound to a synthetic local); read-only
  (#get-only) and write-only (#set-only + compound) emit clear diagnostics; a
  real field of the same name still wins. Multi-assign property targets dispatch
  the setter too (tryLowerPropertyStore, via a pre-lowered-Ref binding).

Payoff: List gains a `len` #set, so `xs.len = n` works; the `.items.len = N`
write workarounds in sched.sx + ui/* + platform/* revert to `xs.len = N`.

issues/0160 records an optional-chain interaction surfaced by the review (a
pre-existing `?T` value-optional read miscompile that blocks getter-through-`?.`).
2026-06-22 17:55:18 +03:00
agra
5cc45a2b38 refactor: List is slice-backed { items: []T; cap } — directly iterable
items is now a []T slice whose .len IS the live element count (cap = allocated
capacity), so a List iterates directly: `for xs.items (e) { ... }`. A
`len :: (self) -> i64 #get => items.len` accessor keeps `xs.len` reads working;
`.len` WRITES become `.items.len`. List stays 24 bytes (`[]T`=16 + cap=8).

- list.sx: append/ensure_capacity/deinit rewritten for the slice backing. deinit
  guards the free on `cap > 0` (true ownership) and resets via explicit
  ptr=null/len=0 (a `.{}` slice assignment yields a garbage len; `.[]` is the
  empty-slice literal but can't be assigned to a generic []T — both worked around).
- Compiler coupling updated: comptime_vm makeStringList/readStringList write/read
  items as a {ptr,len} fat pointer at field 0 + cap at field 1; control_flow
  listView views an `items: []T` slice (keeps the legacy {[*]T,len} shape too).
- Migrated List `.len` writes to `.items.len` in sched.sx + ui/{render,pipeline,
  glyph_cache} + platform/{sdl3,android,uikit}.
- Snapshots: List's type-table layout changed → ~40 .ir + memory/0800 (items now
  prints as a slice) regenerated; diagnostics/1183 retargeted to a genuine
  many-pointer (xs.items is a slice now). Example memory/0840 locks for-each.
2026-06-22 11:55:19 +03:00
agra
9d3a019670 feat: #get property accessors (no-paren method-as-field)
A method declared `name :: (self: *T) -> R #get => expr;` is invoked via
no-paren field syntax (`obj.name`) instead of `obj.name()`. It is an ordinary
method (registered `Type.method`, flagged is_get); field-access lowering and
inference dispatch to it when no real field of that name exists, by synthesizing
a no-arg `obj.name()` call routed through the normal call path (so receiver
address-of and generic binding are reused).

- Lexer/token: `#get`. Parser: parsed after the return type in parseFnDecl;
  hasFnBodyAfterArrow treats it as a body marker so struct-body methods parse.
- Resolution: getAccessorFor handles a generic-struct instance and a plain
  struct. A REAL field of the same name wins (a getter never shadows stored
  data). An explicit postfix-deref receiver (`p.*.getter`) dispatches on the
  inner pointer so it takes the working auto-deref path.
- Works on plain + generic structs (incl. getters returning the type param),
  in expressions/conditions/args/loop-bounds, chained, and via a pointer
  receiver. Examples: types/0196 (basic) + types/0197 (stress).

Known narrow limitations (clean errors / workarounds, not silent): a getter
RESULT used directly as a method/getter receiver (`o.gi.dbl`) errors — bind it
to a local first; a getter named `len`/`ptr` returning non-i64 mis-infers
(the .len/.ptr builtin-field shortcut).
2026-06-22 11:55:01 +03:00
agra
b9311e7de4 fix: slicing a many-pointer yields a correct slice (issue 0159)
emitSubslice handled a struct (slice/string) base and an array base, but a
many-pointer [*]T base is an LLVM pointer kind — it fell through to the else arm
that mapped the result to LLVMGetUndef(slice_ty), so a slice of a many-pointer
(mp[lo..hi]) had a garbage .len/.ptr and iterating it segfaulted.

Add a LLVMPointerTypeKind branch: the base value IS the data pointer, so GEP by
lo and len = hi - lo (the caller supplies the bound; no length is read from the
unbounded pointer). An open-ended mp[lo..] has no resolvable upper bound (a [*]T
carries no length), so lowerSliceExpr now diagnoses it instead of emitting a
.length op that yields garbage.

A List (whose items is [*]T) is now iterable with for items[0..len] (e);
applied in Scheduler.deinit. Regressions: examples/types/0195 (valid slice +
List for-each) + examples/diagnostics/1192 (open-ended rejection).
2026-06-22 10:15:18 +03:00
agra
1e0015d6b4 fix: union struct-literal init (issue 0158)
A plain union initialized with a struct literal (b : Overlay = .{ f = 3.14 })
silently miscompiled — it fell through the generic struct-literal path
(getStructFields returns empty for a union), building a malformed structInit
whose overlapping zero-fill clobbered the named member, so it read back 0.0
(and a type-pun read segfaulted).

lowerStructLiteral now detects a plain-union target and dispatches to a new
lowerUnionLiteral, which writes each named member into a union-sized slot via
the same lvalue resolver the u.member = v assignment path uses, then loads the
union value back. Validity: the named members must share one arm — a single
direct member, or several promoted members of the same anonymous-struct variant.
Overlapping members, members from different arms, and positional union literals
are rejected with a diagnostic (no silent last-wins); an empty .{} yields an
undefined union (matching the --- form).

specs.md updated. Regressions: examples/types/0194 (valid forms) +
examples/diagnostics/1191 (overlap rejection).
2026-06-22 09:45:17 +03:00
agra
6ee4d066b3 fibers: address adversarial review of the B1 changes (6 findings)
UFCS generic overload resolution (issue 0157 follow-ups):
- P1-a: call planning (calls.zig) used the last-wins fn_ast_map winner
  while lowering reselected by receiver, so the planned result type
  could disagree with the dispatched function and misbox the result.
  Both now share selectUfcsGenericByReceiver(.., fd0).
- P1-b: selection scanned module_decls globally, flagging a
  transitively-hidden same-named overload as a false ambiguity. Now
  two-tier: directly-visible authors first (ambiguity only among
  those), global fallback for receiver-reachable namespaced methods
  (e.g. Task.cancel) that defers to fd0 on a hidden tie.
- P2-b: boolean specificity tied *$T with *Box($T). Now peels pointer
  layers so the structurally-narrower receiver wins.

Scheduler (sched.sx):
- P1-c: a second concurrent Task.wait overwrote the single waiter slot
  -> silent deadlock. Now one-awaiter-per-task loud abort.
- P2-c: sleep(negative) rewound the monotonic virtual clock. Rejected
  loudly.

(P2-a, non-generic-winner-hides-generic, did not reproduce -- the
non-generic arm already falls through.)

Regressions: examples/generics/0218 (receiver specificity +
plan/lowering agreement), examples/concurrency/1818 (negative-sleep
abort), 1819 (double-wait abort). Suite green 758/0.
2026-06-21 22:05:22 +03:00
agra
d3944570b9 lang: generic $R type-arg resolution + receiver-driven ufcs overload (issues 0156, 0157)
0156 Part 1: a single-type generic $R (parsed as comptime_pack_ref)
used as a type-arg in a pack-fn body (Box($R), size_of(Box($R))) hit a
missing arm in resolveTypeWithBindings -> .unresolved -> LLVM panic.
Fix: mirror resolveTypeArg's comptime_pack_ref arm (look up
type_bindings, else a loud diagnostic). Regression: examples/generics/0216.
(Part 2 -- deferred .. spread crashes -- reframed OPEN/non-blocking.)

0157: a user generic ufcs method whose name collides with a stdlib
re-export resolved via last-wins fn_ast_map with no receiver filtering,
so the wrong overload won, $R never bound, and .unresolved reached LLVM.
Fix: selectUfcsGenericByReceiver enumerates all module authors, keeps
the receiver-binding ones, picks the most receiver-specific (concrete >
bare $T), dedups re-exports, and flags a genuine tie as a deterministic
'ambiguous -- qualify' diagnostic. Regression: examples/generics/0217.
2026-06-21 18:43:49 +03:00
agra
b1e06f21e3 lang: fix struct-field null/undef over-store (issue 0154)
Assigning null/--- to a struct field picked up a leaked enclosing
target_type (the function's return type, set for the whole body), so
constNull/constUndef built a whole-struct-typed value. The oversized
store overran the field's slot and clobbered the saved frame pointer,
so the function returned to 0x0. Surfaced building a by-value-returned
struct whose array field precedes a pointer field (Scheduler.init()).

Fix: add null_literal/undef_literal to the needs_target switch in
lowerAssignment so the field's own type is used. Regression:
examples/types/0193-types-sret-array-before-pointer.sx.
2026-06-21 18:43:33 +03:00
agra
21d91e6718 fix: resolve module-alias-qualified type in reflection arg slot (issue 0147)
size_of(sel.Selection) and the other reflection builtins rejected a
module-alias-qualified type: in argument position it parses as a .field_access
expression (not the dotted .type_expr a declaration produces), and neither
isStaticTypeArg nor resolveTypeArg had a .field_access arm. Add both: a pure
namespace-decl scan in isStaticTypeArg, and resolution via namespaceAliasTarget
+ resolveNominalLeaf in the target module context in resolveTypeArg (mirroring
the value-position lowerFieldAccess path). No fabricated-stub fallback.

Regression: examples/0192-types-size-of-qualified-alias.sx
2026-06-21 09:33:46 +03:00
agra
7057175fb6 fix: promote mismatched comparison operands before emitting cmp (issue 0146)
A comparison with int-vs-float (or two float widths) operands emitted cmp on
the raw operands with no promotion, unlike the arithmetic arms -- producing a
mixed-type compare the LLVM verifier rejects / mis-evaluates. lowerBinaryOp now
coerces each operand to the promoted common type (from arithResultType) via
coerceToType (SIToFP / FPExt) for the ordering/equality arms when the promoted
type is a float, so LLVM gets a well-typed fcmp.

Regression: examples/0189-types-int-float-compare-promote.sx
2026-06-21 09:11:52 +03:00
agra
d4edf4b4b0 fix: method on array-index/deref receiver mutates the live place (issue 0145)
A *self method called directly on arr[i] (or a deref place) fell through to an
alloca+store-of-value, so the callee mutated a throwaway copy and the live slot
was never written. fixupMethodReceiver now takes the real address of
.index_expr/.deref_expr receivers via lowerExprAsPtr (normalized to *T),
mirroring the explicit-argument path. A comptime-pack index (xs[i] where xs is
a pack) is excluded -- a pack has no runtime storage to address -- so it keeps
flowing through the general copy path.

Regression: examples/0188-types-method-array-index-receiver.sx
2026-06-21 09:11:44 +03:00
agra
333f57026c fix: give error-set decls per-decl nominal identity (issue 0134)
A local 'error { ... }' set with the same name as an imported one collapsed
onto the import, losing its own tags, because registerErrorSetDecl deduped via
the flat findByName path while struct/enum/union use E6a per-decl identity.
Build the .error_set TypeInfo (new buildErrorSetInfo helper factored from
resolveInlineErrorSet) and intern via internNamedTypeDecl with shadowNominalId;
reserve a distinct shadow slot in scanDecls; consult per-decl type_decl_tids in
namedRefTid before findByName. The inline/anonymous findByName short-circuit is
preserved.

Regression: examples/1059-errors-same-name-error-set-own-wins.sx (moved from
issues/0134).
2026-06-21 09:11:06 +03:00
agra
ad45ae07ef fix: diagnose unknown generic #builtin instead of silently returning 0 (issue 0144)
A bodiless #builtin with a $T: Type param routes through monomorphization.
When resolveBuiltin returned null for an unrecognized name, the builtin-body
branch fell through to ensureTerminator's constInt(0) -- a silent-fallback
default the CLAUDE.md REJECTED PATTERNS forbid. Emit a loud
'error: unknown #builtin <name>' diagnostic instead.

Regression: examples/1189-diagnostics-unknown-builtin.sx
2026-06-21 09:10:38 +03:00
agra
68c1991e11 issue 0153 RESOLVED: pin generic return-type resolution to the fn's defining module
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.
2026-06-21 05:55:14 +03:00
agra
362674f04d issue 0151 RESOLVED: infer generic $T through generic-struct / pointer / UFCS-pack params
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.
2026-06-21 05:25:39 +03:00
agra
45d869da41 fibers B1.2: Io capability + context.io + blocking impl + Future/async/await/cancel
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.
2026-06-20 22:21:27 +03:00
agra
a7fe165684 fibers: rename ABI variant .pure -> .naked
"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).
2026-06-20 17:01:09 +03:00
agra
b631590574 fibers B1.0c: support params in abi(.pure) (read from registers)
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).
2026-06-20 16:36:31 +03:00
agra
4b384788e6 fibers B1.0b: abi(.pure) emits a real LLVM naked function (green)
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).
2026-06-20 16:36:12 +03:00
agra
40424df1b8 fibers B1.0a: close generic/pack is_pure gap (review)
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).
2026-06-20 14:45:29 +03:00
agra
dd363ca877 fibers B1.0a: plumb abi(.pure), emit bails (lock)
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).
2026-06-20 14:34:53 +03:00
agra
b65544a68c atomics A.3b: real swap (xchg) + fence emission + unit test (green)
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).
2026-06-20 13:51:36 +03:00
agra
fca4304f83 atomics A.3a: swap + fence ops + recognizer, emit bails (lock)
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).
2026-06-20 13:47:08 +03:00
agra
79895be401 atomics A.2b: real CAS emission (cmpxchg) + unit test (green)
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).
2026-06-20 10:57:01 +03:00
agra
dca396ed1f atomics A.2a: CAS ops + recognizer + methods, emit bails (lock)
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).
2026-06-20 10:44:31 +03:00
agra
68ed732b79 atomics A.1c: fix comptime signed fetch_min/max (was unsigned compare)
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).
2026-06-20 10:32:50 +03:00
agra
05311646aa atomics A.1b: real RMW emission (atomicrmw) + unit test (green)
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).
2026-06-20 10:19:44 +03:00
agra
718f27e27f atomics A.1a: RMW ops + recognizer + methods, emit bails (lock)
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).
2026-06-20 10:14:49 +03:00
agra
acf31839ea atomics A.0.5: full ordering surface (comptime $o: Ordering)
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).
2026-06-20 10:04:39 +03:00
agra
d95ba0a937 comptime value params: bind on generic-struct methods
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).
2026-06-20 09:57:15 +03:00
agra
d7a6857ee1 comptime value params: generalize to tagged_union (+ aggregate hook)
`$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.
2026-06-20 09:39:10 +03:00
agra
8144a88a21 atomics A.0c: harden guards (scalar-kind, ordering validity, align bail)
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).
2026-06-20 09:26:53 +03:00
agra
3c4305f78f comptime enum value params: $o: EnumType binds+resolves variant tag
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.
2026-06-20 09:19:18 +03:00
agra
64c7db5eb1 atomics A.0b: real seq_cst load/store emission (green)
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).
2026-06-20 09:08:05 +03:00
agra
22af40413d atomics A.0a: lib + IR ops + recognizer, emit bails (lock commit)
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.
2026-06-20 08:47:07 +03:00
agra
d8fb42501d comptime VM: support optional-of-word extern returns + string/any struct_init
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.
2026-06-19 22:14:57 +03:00
agra
538349611e comptime: empty-member types are valid for all kinds; keep never-defined declare rejected
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.
2026-06-19 21:41:07 +03:00
agra
ccba704378 P5.7 Step D: delete dead .define builtin arm, defineFromInfo, decodeTypeSlice
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).
2026-06-19 21:14:05 +03:00
agra
7b1d8ceb83 P5.7 Step D: re-express metatype define() as sx over register_type
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.
2026-06-19 21:09:18 +03:00
agra
8850fcce70 P5.7 Step D: re-express metatype declare() as sx over declare_type
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.
2026-06-19 20:58:34 +03:00
agra
61f5700a36 P5.7 Step E: fix issue 0141 (reject silent [*]T -> []T coercion); land regression
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.
2026-06-19 20:40:21 +03:00
agra
7b8be86834 P5.7 Step C: delete interp.zig — the comptime VM is the sole evaluator
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.
2026-06-19 20:05:57 +03:00
agra
103a156b26 P5.7 Step C2b: drop the Interpreter materialization context from emit_llvm
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.
2026-06-19 19:52:44 +03:00
agra
4d9f73f506 P5.7 Step C1: evaluate #insert on the comptime VM (sole evaluator)
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.
2026-06-19 17:25:12 +03:00
agra
64eb01918a P5.7 Step B2: remove the #compiler attribute + compiler_expr AST node
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.
2026-06-19 17:18:45 +03:00
agra
e2971f272c P5.7 Step B1: remove the compiler_call IR op + the hook Registry
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.
2026-06-19 16:54:38 +03:00
agra
5d25e23143 P5.7 Step A: VM is the sole comptime evaluator at emit-time + type-fn sites (no fallback)
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.
2026-06-19 16:44:52 +03:00
agra
48eb7bf48a P5.6 (macOS): default_pipeline drives bundling; fix issue 0125 (array-format blowup)
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.
2026-06-19 15:32:07 +03:00
agra
994d6498fc P5.6 prereq: port bitwise/shift ops into the comptime VM
`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.
2026-06-19 14:20:37 +03:00