Commit Graph

665 Commits

Author SHA1 Message Date
agra
8bacb2b01c feat: true cancellation for the fiber Io layer (PLAN-IO-UNIFY Phase 3)
A cancelled async worker now abandons its body at its next suspend instead
of running to completion.

- Cancel-flag back-ref (D4): SpawnOpts.cancel_flag (core.sx) + Fiber.cancel_flag
  (sched.sx), set from opts.cancel_flag in Scheduler.spawn_raw; async passes
  xx @f.canceled (the Future.canceled Atomic(bool) erased to *void).
- Delivery: Scheduler.suspend_raw consults fiber_canceled(self.current) PRE-park
  (raise without parking — no deadlock if cancel landed before the worker ran)
  and POST-resume (cancel landed while parked), raising error.Canceled.
  cancel(f) flips the sticky flag, marks .canceled, and wakes the worker.
- async worker is failable Closure() -> ($R, !); the completion closure
  f.value = worker() catch {…} marks .canceled/.failed and wakes the awaiter,
  so post-suspend side effects never run. New failable io.sleep(ms) is the
  cancellation point.
- Compiler: a -> ! fn whose only error source is try-ing a protocol method
  (io.suspend_raw) was wrongly flagged 'declared ! but never errors';
  collectErrorSites now marks a try of a non-identifier callee as a dynamic
  (opaque) error source, suppressing the warning.
- Two UAFs found by adversarial review and fixed: (1) cancel-before-park
  orphaned io.sleep's armed timer — suspend_raw's pre-park raise now evicts the
  current fiber's timer/waiter first; (2) cancel(f) could wake a reaped worker —
  now only wakes when was_pending.

Migrated 1805/1806/1824 to failable workers. Lock: example 1825 (seq: 1 -99,
post-suspend line never runs); byte-identical on aarch64-macOS + aarch64-linux.
.ir churn is the SpawnOpts layout change (type-table string renumbering).
2026-06-28 09:19:01 +03:00
agra
45bd561a0d fix: resolve closure/fn-pointer struct-field call types (issue 0201)
Calling a closure or function-pointer value stored in a struct data field
(`box.run(args)`) typed the call as 'unresolved': value returns marshalled
as garbage, failable fields could not be try/catch-ed. Lowering already
dispatched these (call_closure / call_indirect); only CallResolver.plan
lacked a field-access arm. Add a closure/fn-pointer field arm to plan
(before the instance-method check, mirroring lowering's precedence — a
closure-typed field shadows a same-named method) and extend the lowering
closure-field arm to also handle bare .function fields via call_indirect.

Lock: examples/closures/0315-closures-struct-field-call.sx.
2026-06-28 09:18:30 +03:00
agra
69a6ecfb57 fix: capture failable closures called via error-handling exprs
collectCaptures did not descend into catch/try/onfail/raise/multi_assign/
push/comptime/insert/spread/asm nodes, so a free variable referenced only
inside them (e.g. a failable worker called as `worker() catch {…}` in a
nested lambda) was never captured into the env struct — inside the lambda
it resolved against an empty scope and typed as 'unresolved'. Add the
missing traversal arms. The push_stmt arm also closes the noted
'free-var analysis does not descend into a nested push Context {…}' gap.

Unblocks the PLAN-IO-UNIFY Phase 3 async completion closure shape.
Lock: examples/closures/0314-closures-capture-failable-call.sx.
2026-06-28 09:18:15 +03:00
agra
b322dcfe61 fix: type-safe stores + Any unbox/eq; finish multi-return deferrals
Type-checking gaps (segfault/corruption → compile errors):

- 0197: reject a store into an annotated slot whose value has no modeled
  coercion AND a different byte width (a 16-byte string into a 4-byte i32
  overran the slot and segfaulted). New checkAssignable / noneReinterpretIsUnsafe
  (coerce.zig, width via the LLVM-accurate typeSizeBytes) wired into every store
  site: var/const-decl, single + multi assignment (identifier/field/index/
  element/deref), named-return defaults. Same-width reinterpretations (*T→[*]T,
  i64→isize, fn-ref) and explicit xx/cast stay allowed; cascades suppressed via
  externalErrorsExist. Examples 1205, 1206.
- 0198: an implicit `Any → T` unbox is now a compile error (it blindly
  reinterpreted the boxed payload — silent garbage for a wrong scalar, a segfault
  for an aggregate). xx and compiler-generated match/pack unboxes are unaffected.
  Example 1207.
- 0199: `Any == <concrete>` (one operand Any) aborted the LLVM verifier — the
  comparison arm now fires when either operand is Any, boxing the concrete side
  first. Example 0654.

Multi-return deferrals (PLAN-MULTIRET #6 + named-order + D3 + generic):

- Reorder named return elements by name instead of requiring slot order; error on
  unknown/duplicate/missing (value-only AND full-failable-tuple forms). Examples
  0210, 0214.
- Reject a bare-paren (A, B) multi-return signature in generic-arg position
  (return-position-only). Example 0215.
- Multi-return closure types / lambda literals work via the reused tuple
  machinery (destructure, single-bind+field, lambda arg). Example 0216.
- Generic multi-return: positional works (0217); 0200: the named-slot
  implicit-return form now works for generic free fns + struct methods —
  monomorphizeFunction now calls bindNamedReturnSlots. Example 0218.

readme.md documents the annotated-store coercion rule; CHECKPOINT-MULTIRET.md
updated. Full corpus green (850/0).
2026-06-27 17:28:27 +03:00
agra
76689a1ea6 feat: multiple return values — bare-paren signatures, named returns, must-set, defaults
A function may return multiple values via a bare-paren return signature:
`-> (A, B)` / `-> (x: A, y: B)` / `-> (A, B, !)` (error always the last slot),
and `-> ()` is `void`. This is DISTINCT from a `Tuple(…)` value — return-position
only (a dedicated `ReturnTypeExpr` AST node resolving to a reused `.tuple`
TypeId); a parameter / field / variable annotation `x: (A, B)` is rejected. A
single-value `-> (T, !)` stays a plain failable (= `-> T !`).

Returns use the bare comma form `return a, b` / `return x = a, y = b` (no `.( … )`
literal). Consume by destructuring (`a, b := f()`) or single-bind + field access
(`c := f(); c.sum`); a failable bound value holds only the value slots (the error
stays on the `!` channel).

Named return slots are in-scope assignable locals; with no explicit `return` the
implicit return is synthesized from them. Path-sensitive definite-assignment
enforces the must-set rule, and a slot may carry a default that exempts it.
Validation rejects arity mismatches, out-of-slot-order named elements, a
slot/parameter name collision, a comma list from a single-value function, and a
multi-return signature used as a value type.

Examples 0202-0213; readme + specs updated. issues/0197 files a pre-existing
annotated-assignment type-check gap (`x: i32 = "hi"` segfaults) surfaced by the
adversarial review.
2026-06-27 12:31:23 +03:00
agra
6a97628749 feat: comptime tuple-element L-values + named-tuple-literal binding (GAP 2)
Completes comptime-cursor tuple indexing (started by the read path in
fee86adf) and unblocks the `race` runtime synthesis. Five enablers:

1. Named-tuple-literal type inference preserves element NAMES. A
   `.(a = x, b = y)` passed DIRECTLY as a `$T` argument inferred to a
   tuple with `.names = null`, so `field_name(T, i)` reflected "" and a
   `make_enum` over those labels collided on the empty name. The typer
   now mirrors `lowerTupleLiteral`'s name capture.

2. `inferExprType` resolves a comptime-constant tuple index to the i-th
   field's CONCRETE type (the inference sibling of the fee86adf read
   path), so `tup[i].field` / methods / comparisons on it resolve.

3. Tuple-element L-VALUES by comptime index — `tup[i] = v`,
   `tup[i].f = v`, `@tup[i]` — lower to a typed `structGep` of field i
   across all four paths (`lowerAssignment`, the multi-assign store,
   `lowerExprAsPtr`, and address-of-index). Previously each emitted an
   `index_gep` with a `ptrTo(.unresolved)` element type (a tuple has no
   uniform element) that panicked at LLVM emit. An out-of-range comptime
   index now diagnoses loudly on every path instead of falling through to
   that panic.

4. A user generic `($X..) -> Type` call is recognized as type-shaped
   (`isTypeReturningCallNode`), so it can bind a `$E: Type` parameter —
   e.g. `make_variant(RaceResult(T), i, …)`. The static
   `isTypeShapedAstNode` only knew the type-returning builtins
   (field_type/pointee/type_of).

Locked by examples/comptime/0652 (read, fee86adf) and 0653 (store +
address-of + element-pointer field store).
2026-06-26 18:06:55 +03:00
agra
fee86adf2c feat: comptime-cursor indexing of a named-tuple VALUE (GAP 1)
`tup[i]` where `i` folds to a compile-time integer (an `inline for`
cursor or a literal) now reads the i-th tuple field with its CONCRETE
type instead of failing with "cannot index a value of type '(…)'".

A tuple's elements are heterogeneous, so there is no runtime
element-indexing op — a comptime index lowers exactly like the `.N`
field-access path (a `structGet` of the i-th field). A genuinely
runtime index into a tuple value still falls through to the existing
"cannot index a value of type" error (no single element type). A
comptime index out of range gets a dedicated loud diagnostic.

This is the read side `race` needs: pull the i-th `*Task(T_i)` handle
out of a named-tuple param keeping its real type so field/method access
on it resolves.

Locked by examples/comptime/0652-comptime-tuple-cursor-index.sx.
2026-06-26 16:17:16 +03:00
agra
84c2ae4f22 fix: return inside inline if no longer drops trailing statements
A `return` inside an `inline if` / comptime `case` branch — itself inside an
`inline for`, under a runtime `if` — made the compiler wrongly reject the
function as "body produces no value" and DROP its trailing statements (e.g. a
trailing `return -1`).

Root cause: the inline-if branch lowering sets the global `block_terminated` flag
when its taken arm returns (control_flow.zig / stmt.zig `lowerInlineBranch`),
unlike a bare `return` STATEMENT which deliberately never sets it (precisely to
avoid leaking past an `if cond { return }` merge — see the comment at
stmt.zig:37-42). The enclosing runtime-`if`'s merge never reset the flag, so it
leaked past the merge and `lowerBlock` skipped the following statements as dead.

Fix: after the runtime-`if` switches to its merge block, set
`block_terminated = then_diverged and has_else and else_diverged` — the `if`
leaves the block terminated ONLY when both arms diverged with an `else` covering
the cond-false edge (otherwise the merge is reachable and the flag must be
false). Adds `else_diverged` tracking alongside the existing `then_diverged`.

Adversarially reviewed (SHIP): the reachability predicate is correct across all
arm/else/divergence cases; the both-arms-diverge case still sets true (preserving
prior behavior); value-ternary and inline-for-unroll paths are unaffected.
Regression: examples/comptime/0651-comptime-inline-if-return.sx (nested inline-if
and comptime `case`, both with per-arm returns, asserting the trailing return is
emitted). Suite green (822/0).
2026-06-26 15:32:32 +03:00
agra
eb18bbc6fd feat: comptime type-call composition (field_type/pointee/field_name in value position)
A comptime-type-call's `Type` result (`field_type(T, i)`, `pointee(P)`) could only
be used in a type-arg slot — not as a `Type`-typed struct-field value, a generic
`$P: Type` argument, or a nested type-call arg — when the index was an `inline for`
loop variable. It routed through value / generic-fn lowering ("cannot infer generic
type parameter" / "unknown #builtin field_type") instead of the type-call fold. This
is what blocked the variable-arity `race` result synthesis: a `($T) -> Type` builder
looping `field_type(pointee(field_type(T, i)), 0)` to mint a tagged-union.

Three coordinated changes route these through the SAME type-call fold (which folds
the index, including a loop var), so type-arg and value positions never disagree:

- `isTypeShapedAstNode` (type_bridge.zig): a `.call` to a type-returning builtin
  (`field_type`/`pointee`/`type_of`, via new `isTypeReturningBuiltinName`) is
  type-shaped, so generic-arg inference (buildTypeBindings Strategy 1) resolves it
  via `resolveTypeArg` rather than failing value inference.
- `tryLowerReflectionCall` (call.zig): value-position `field_type`/`pointee` fold
  to `constType(resolveTypeCallWithBindings(c))` — the value twin of the existing
  `type_of` fold (every failure path already diagnoses before `.unresolved`).
- `field_name` (call.zig): folds to a const STRING via `memberName` when the type
  resolves and the index is a compile-time constant (matching the runtime
  `field_name_get` array exactly — same `memberName`, same "" for nameless
  members); a dynamic index still emits the `field_name_get` instruction.

Adversarially reviewed (SHIP): no over-broadening (only type-demanding slots consult
isTypeShapedAstNode; only `$T: Type` slots are affected), no silent defaults (every
fold failure is preceded by a diagnostic; "" is the runtime-matching value for a
nameless member). Locked by examples/comptime/0649-comptime-typecall-composition.sx
(reflect a named tuple of `*Box(..)` handles → mint a tagged-union with the tuple's
labels, projecting `*Box(A)` -> `A`). Suite green (821/0). Unblocks PLAN-RACE step 2.
2026-06-26 13:40:52 +03:00
agra
2a6ef39829 feat: fold field_count/size_of/align_of as comptime constants
The int-returning type-query builtins now fold to a compile-time constant in
const-required positions (`inline for` bound, array dimension), like a plain
`K :: 3` const — previously they evaluated only as runtime values, so
`[field_count(S)]T` and `inline for 0..field_count(S)` were rejected as "not a
compile-time integer". This is what lets a `($T) -> Type` builder loop
`inline for 0..field_count(T)` to assemble a member list from a type's fields
(the `race` result synthesis).

The shared comptime-int folder `evalConstIntExpr` (program_index.zig) gained a
`.call => ctx.evalConstCallInt(node)` arm. The body-lowering ctx (`Lowering`)
implements it — resolve the type arg via `resolveTypeArg`, return
`memberCount orelse 0` / `typeSizeBytes` / `typeAlignBytes`, matching the runtime
value path in lower/call.zig exactly. `SourceConstCtx` delegates to its wrapped
Lowering; the stateless ctxs (`ModuleConstCtx`, `StatelessInner`, test `DimCtx`)
stub null (they cannot resolve a type-expr arg). A non-type-query call / wrong
arg count / unresolved type arg folds to null (not a comptime integer).

Adversarially reviewed (SHIP): the fold matches the value path across every type
kind, ctx coverage is complete, recursion is AST-depth bounded, no speculative
spurious diagnostics, `orelse 0` is field_count's definitional value for
non-aggregates (not a silent default). Locked by
examples/comptime/0648-comptime-typequery-const-fold.sx. Suite green (820/0).
2026-06-26 13:13:16 +03:00
agra
f1d298764f feat: pointee($P: Type) -> Type comptime reflection builtin
Project a pointer type to its target: `pointee(*X)` -> `X`. The one reflection
primitive missing for the `race` result synthesis (`*Task(A)` -> `A` via
`field_type(pointee(*Task(A)), 0)`) — reflection could read aggregate fields but
was blind to a pointer's target type.

Mirrors the `field_type` builtin: declared `#builtin` in std/core.sx, resolved as
a lower-time type-call fold in resolveTypeCallWithBindings (src/ir/lower/generic.zig)
so it composes in any type-arg slot. `.pointer` -> pointee, `.many_pointer` ->
element; a non-pointer arg is a loud diagnostic + `.unresolved` sentinel (no silent
fallback). Adversarially reviewed (SHIP). Locked by
examples/comptime/0647-comptime-pointee-reflection.sx. Suite green (819/0).

PLAN-RACE step 1 of 6.
2026-06-26 12:47:02 +03:00
agra
8ac6c573e8 fix: comptime field reflection on tuples/arrays/vectors (issue 0195)
`field_count` / `field_name` were broken on every non-struct/enum aggregate:
`field_count(Tuple(i64, bool))` silently returned 0 (a missing `.tuple` arm in
the count switches), and `field_name(tuple/array/vector, i)` SEGFAULTED — the
LLVM backend built a zero-length `[0 x string]` name array for those kinds while
sizing the runtime GEP at the (often non-zero) member count, so the indexed load
ran past the array.

Root cause was three+ parallel switches that each had to know how to count an
aggregate's members, and disagreed: `field_count` lowering and `memberCount` had
struct/union/tagged_union/enum/array/vector but no `.tuple`; the backend's
`field_name_get` build + GEP sizing had neither `.tuple` nor `.array`/`.vector`.

Fix:
- add the `.tuple` arm to `field_count` lowering (src/ir/lower/call.zig) and
  `TypeTable.memberCount` (src/ir/types.zig; this also backs the COMPILER-API
  `type_field_count` VM reader).
- unify the LLVM backend onto the single source of truth: both
  `getOrBuildFieldNameArray` (reflection.zig) and `emitFieldNameGet`'s GEP sizing
  (ops.zig) now derive from `memberCount` / `memberName`, so the name-array
  length and the GEP array type can never diverge again — for any kind. A member
  with no name (positional-tuple / array / vector element) reflects as "" (one
  slot per member, always in-bounds); named-tuple elements recover their labels.

The array/vector clone was surfaced by adversarial review of the tuple-only fix.

Regression: examples/comptime/0646-comptime-field-reflect-tuple-array.sx exercises
field_count/field_name/field_type over struct, enum, positional + named tuple,
array, and vector. Full suite green (818/0). Unblocks the `race` synthesis, which
must reflect a named tuple's labels + element types.
2026-06-26 12:28:09 +03:00
agra
493469fd74 fix: lambda inferred return type from a block body's early returns (issue 0187)
A `:=`-bound closure with no explicit `-> T` and a BLOCK body inferred its
return type via inferExprType(lam.body), which yields the last statement's
type. A block whose value comes only from early `return`s ends in a return
statement (void/noreturn), so the closure was built with a void return while
the body returned i64 — the call site then fed `i64 undef` and LLVM
verification failed. (A block whose tail referenced a block-local hit the
sibling failure: inferExprType returned .unresolved → an LLVM panic.)

Infer the return type exactly as a named fn does (resolveReturnType in
lower.zig): an arrow body `(params) => expr` uses the expression type; a block
body `(params) { stmts }` takes the first explicit `return <val>` type via
findReturnValueType, else void (the block tail is a discarded statement unless
an explicit `-> R` makes it the value). Regression test:
examples/closures/0313-closure-inferred-return-early.sx.
2026-06-26 09:28:29 +03:00
agra
501399b1a9 fix: resolve qualified-import-member const as a compile-time constant (issue 0192)
A namespaced import's const (`m :: #import "lib.sx"; … m.CAP`) only ever
resolved as a runtime value — the const folders in program_index.zig had no
namespace-member arm, so a qualified const was rejected as an array dimension /
Vector lane / generic value-param and could not seed another const, while the
flat-import form worked everywhere.

Add a `lookupQualifiedConst` (+ float / float-typed twins) ctx hook: resolve
the alias via `namespaceAliasVerdictFrom` to its target module, then fold the
member from that module's per-source const cache (`foldQualifiedConstInt` in
lower/comptime.zig), pinned to the target source so nested const RHSs fold
there. Wire it into evalConstIntExpr / evalConstFloatExpr / isFloatValuedExpr —
both the expression-position field_access arm (`[m.CAP]T`) and the
type-argument dotted-name arm (`Vector(m.LANES, …)`, generic value-params).

Implemented on the source-aware ctxs (Lowering / SourceConstCtx); the
namespace-blind ModuleConstCtx / StatelessInner return null, so a qualified-const
dim reached only via the stateless type-alias path stays a clean unresolved-dim
diagnostic, never a fabricated length. Resolves correctly for array dims,
arithmetic, integral-float dims, Vector lanes, generic value-params, inline-for
bounds, and struct fields.

Regression: examples/modules/0842-modules-qualified-import-const-comptime.sx.
2026-06-26 07:51:27 +03:00
agra
df1327e316 fix: initialize the error-channel slot on every failable implicit success return (issue 0190)
A failable function that returned by IMPLICIT success (no explicit
`return`) left its error-tag slot uninitialized, so a caller's `catch` /
`or` (or `main`) read a garbage tag and reported a phantom unhandled
error — and for value-carrying failables the success value was dropped.
The "no error" sentinel was only written on the explicit-`return;` path.

Unified all function-body-return lowering so the failable-success slot
is always written:
  - void `-> !` fall-through: `ensureTerminator` (control_flow.zig) now
    emits `ret constInt(0)` for a pure-failable end-of-body.
  - value-failable trailing-expression success: `lowerValueBody`
    (stmt.zig) routes through `lowerFailableSuccessReturn`.
  - generic + pack-fn instances: `monomorphizeFunction` (generic.zig) and
    `monomorphizePackFn` (pack.zig) now DELEGATE their body-return to
    `lowerValueBody` instead of hand-rolling a `coerce`+`ret` that drifted
    (covers generic/pack value-failables).

Also fixes the missing-value diagnostic guard added here: it now counts
`.err`-level diagnostics (new `DiagnosticList.errorCount`) rather than the
total list length, so a warning/note emitted while lowering the body
(e.g. an ObjC selector arity warning) can no longer suppress a genuine
"body produces no value" error — which previously shipped an
uninitialized return at exit 0.

Regressions: examples/errors/1061 (void fall-through), 1062 (value-failable
trailing expr), 1063 (generic value-failable trailing expr).
2026-06-25 22:39:49 +03:00
agra
45e69ac1bb fix: reject non-type expression in type position instead of fabricating {} (issue 0189)
Two type-resolution paths silently resolved a non-type AST node in type
position to a zero-field `{}` struct that reached codegen with no
diagnostic:
  - a dotted `type_expr` / field-access (`g.a`, `g` a runtime value) whose
    prefix is not a namespace alias
  - an `error_type_expr` (`!Name`) whose `Name` is not a declared error set

Now both reject loudly:
  - `resolveTypeWithBindings` (lower.zig): "expected a type, found a value
    '<name>' in type position" + `.unresolved`
  - `checkTypeNodeForUnknown` (semantic_diagnostics.zig): validates a named
    `!E` against the declared error-set names — "unknown error set
    '<name>'" / "expected an error set after '!', found type '<name>'".

A bare `!` (void channel) and a declared `!E` in return position stay
valid; namespace-qualified types (`pkg.Type`) are unaffected.

Regression: examples/diagnostics/1195-diagnostics-non-type-in-type-position.
2026-06-25 20:35:02 +03:00
agra
989e18b760 feat: tuple syntax cutover — Tuple(...) type + .(...) value
Replace the bare-paren tuple grammar with explicit, position-unambiguous
forms, mirroring how structs work:

  type     `(A, B)`        -> `Tuple(A, B)`          (named keeps `:`)
  value    `(a, b)`        -> `.(a, b)`              (named uses `=`)
  typed    (new)           -> `Tuple(A, B).(a, b)`   (like `Point.{...}`)
  failable `-> (T, !)`     -> `-> T !`
           `-> (T1, T2, !)`-> `-> Tuple(T1, T2) !`   (channel outside Tuple)

Bare `(...)` is now grouping only, everywhere; a comma in bare parens is a
hard error with a migration hint. Grouping, function types `(A, B) -> R`,
param lists, lambdas, and match bindings are unaffected.

`Tuple(...)` is strictly a TYPE in every position (including `size_of` /
`type_info` args); a tuple VALUE comes only from `.(...)` (anonymous) or
`Tuple(...).(...)` (explicitly typed). A bare `Tuple(1, 2)` is a tuple
type with non-type elements -> rejected.

The ~110 tuple-bearing corpus files were migrated with a one-shot
AST-aware migrator (the `sx migrate` tool from the prior commit, removed
here). New examples: 0130 (new syntax), 0131 (typed construction), 1060
(named-tuple failable return). 1116 golden updated for the new hint text.
2026-06-25 17:53:57 +03:00
agra
468461becc fix: gate implicit optional unwrap on flow narrowing (issue 0179)
Optional (?T) operands were implicitly unwrapped without proof of
presence, silently miscompiling a NULL ?T to garbage. Unwraps in
binary ops and other expression positions are now gated on flow
narrowing: a ?T value is only auto-unwrapped where control flow has
established it is non-null (the narrowed_refs set). Outside a narrowed
region, an implicit unwrap is rejected rather than producing garbage.

Touches the lowering pipeline (lower.zig + lower/{call,closure,coerce,
comptime,control_flow,expr,ffi,generic,pack,stmt}.zig). Adds optionals
examples 0919-0923 and closures example 0312 covering flow narrowing,
binop narrowing, no-implicit-unwrap rejection, and no closure leak of
narrowed state. Updates specs.md and readme.md.
2026-06-25 13:57:48 +03:00
agra
6c89a0aa3e fix: body-local #run of an unbridged shape fails loudly instead of silent garbage (issue 0182)
The body-local #run fold in emitCall was effectively dead (gated on
args.len==0, but the __ct comptime wrapper always carries the implicit
*Context arg), so every body-local #run fell through to a RUNTIME call:
bridgeable shapes lucked into the right value; an unbridgeable shape
(e.g. [2][]i64) ran over --- storage -> garbage, exit 0, no diagnostic.

Fold any is_comptime callee (gated !enclosing.is_comptime so nested
metatype calls in a comptime wrapper's dead body aren't folded). On a
tryEval bail, distinguish a BRIDGE bail (result can't regToValue-
materialize -> error: comptime init of 'X' failed: <reason> +
comptime_failed, build fails, symmetric with the global #run path) from
an EXECUTION bail (VM can't run the body, e.g. NaN/extern -> runtime
fallthrough, preserving types/0150), via comptime_vm.last_bail_was_bridge
(reset at tryEval entry, set only at regToValue). The const name is
threaded onto the wrapper (comptime_display_name) so the diagnostic reads
the source name, not __ct_N.

Regressions: diagnostics/1204 (negative), comptime/0645 (positive).
Verified by 3 adversarial reviews, suite 801/0.
2026-06-23 19:29:11 +03:00
agra
95c9c0df4c fix: diagnose indexing a non-indexable type instead of panicking (issue 0183)
lowerIndexExpr fell through to an index_get with an .unresolved element
type for any non-indexable object (*T, *[]T, struct, scalar), reaching
codegen -> 'unresolved type reached LLVM emission' panic. Add a guard
after all indexable arms: if getElementType(obj_ty) is .unresolved and
obj_ty is itself resolved (genuinely non-indexable, not a prior-error
placeholder), emit a located 'cannot index a value of type <T>'
diagnostic + placeholder (hasErrors aborts before codegen). A single
pointer hints by pointee: ptr-to-scalar -> many-pointer/dereference;
ptr-to-array/slice -> dereference first. No false-positives (generics,
aliases, late-resolved, every indexable shape verified).

Regression: examples/diagnostics/1203-diagnostics-index-non-indexable.sx.
Verified by 3 adversarial reviews, suite 799/0. Filed adjacent pre-existing
panic 0184 (untyped positional .{ } literal with no target type).
2026-06-23 17:29:12 +03:00
agra
097d23d909 fix: presence-preserving optional->optional coercion (issue 0180)
The generic-?? wrong-fallback was not in lowerNullCoalesce: coercing
?A -> ?B (differing payload, e.g. the ?i32->?i64 call-arg coercion when
instantiating unwrap_or(99, ?i32)) routed through .optional_wrap, which
unconditionally unwrapped the source and re-wrapped as ALWAYS-PRESENT, so
a null became present-zero everywhere (args, returns, field init,
var-decl, ??). Add a CoercionPlan.optional_to_optional (conversions.zig)
+ a presence-preserving arm in coerceMode (coerce.zig): has_value ->
present: unwrap+coerce-child+wrap-present; absent: constNull(dst); merge
via a dst_ty block param. lowerVarDecl gains a !src_is_optional guard so
an annotated x : ?B = <?A> routes through the same arm (also makes
aggregate-payload var-decl ?[3]i64->?[]i64 / ?Concrete->?Protocol work).

Alias-optional struct-literal default already works (grouping + 0166);
a 1-tuple default ?(i32,) ?? 5 now emits a clean diagnostic instead of an
LLVM PHI abort (no implicit scalar->1-tuple coercion per spec).

Regressions: optionals/0916 (generic ??), 0917 (alias struct default),
0918 (var-decl optional->optional), diagnostics/1202 (1-tuple default) +
a conversions.test.zig unit test. Verified by 3 adversarial reviews,
suite 798/0.
2026-06-23 16:16:47 +03:00
agra
4ca466fa96 fix: optional-chain index opt?.xs[i] over array/ptr-array field (issue 0181)
opt?.xs[i] typed and lowered the index over the optional CONTAINER
(?[N]T); getElementType returned .unresolved, so index_get reached LLVM
with an unresolved element type and panicked. Mirroring the 0101
!-unwrap fix: add lowerOptionalChainIndex (optional_has_value -> some:
unwrap + index (index_gep+load for ?*[N]T, else index_get) +
optional_wrap; none: const_null; merge -> ?ElemType, element-optional
flattened). The typer + dispatch guard compute the element via
ptrToArrayElem(child) orelse getElementType(child), so value-arrays,
slices, many-pointers, AND pointer-to-array (?*[N]T) children resolve.
Null receivers short-circuit (no null deref).

Regression: examples/optionals/0915-optional-chain-array-field-index.sx.
Verified by 3 adversarial reviews, suite 794/0. Filed broader pre-existing
gap 0183 (indexing a non-indexable type panics instead of diagnosing).
2026-06-23 12:29:29 +03:00
agra
fa7c07faf8 fix: comptime reg->value bridge for array-in-aggregate + clean abort on comptime-init failure (issue 0167)
(C) regToValue (comptime_vm.zig) gained no array arm, so a #run returning
an aggregate containing an array bailed 'reg->value: aggregate shape not
bridged yet'. Add an .array arm: read N elements at stride
typeSizeBytes(elem) from the array address, bridge each recursively via
regToValue -> an .aggregate Value (serializeAggregateValue already emits
arrays). Composes with struct fields, nested arrays, array-of-structs,
and the ?Arr optional payload; unbridgeable elements bail loudly.

(E) A global failing #run proceeded into LLVM emission and panicked
'unresolved type reached LLVM emission' when the unresolved const was
used. Add 'if (self.comptime_failed) return;' in emit() after Pass 0 so
it aborts cleanly (exit 1, the comptime diagnostic) across run/ir/build.

Regression: examples/comptime/0644-comptime-run-array-aggregate.sx.
Verified by 3 adversarial reviews, suite 793/0. Filed separate bugs found
during review: 0181 (optional-chain ?. to array field + index panics),
0182 (body-local #run unbridged silently miscompiles).
2026-06-23 11:34:22 +03:00
agra
555ccdc024 feat: parenthesized type grouping — (T) groups, (T,) is a 1-tuple (issue 0177)
In type position, parentheses now mirror value position: (T) (a single
unnamed element, no trailing comma) is a GROUPING that resolves to the
inner type; (T,) is a 1-tuple; (A, B) a 2-tuple; named (x: T) and spread
(..Ts) stay tuples; (...) -> R stays a function type. This lets a
closure/optional/function type be parenthesized for readability without
silently becoming a 1-tuple:
  [1](Closure(i64,i64) -> i64)   // array of closures (issue 0177) -> 7
  ?(?i64)                        // genuine nested optional (issue 0165 intent)

Parser: src/parser.zig returns the inner node for a single unnamed
non-spread no-trailing-comma parenthesized type. formatTypeName (both
generic.zig diagnostics + types.zig reflection) now render a 1-tuple as
(T,) so the spelling is unambiguous and diagnostics are self-consistent.
The 0165 coerce/stmt note reworded accordingly.

specs.md §Type Syntax updated; basic/0036 wrap return -> (i64,); obsolete
diagnostic 1195 removed (?(?i64) now compiles); regression
examples/types/0201-types-parenthesized-type-grouping.sx added; 0414 .ir
golden regenerated for the (T,) rendering. Resolves 0177; updates
0165/0170. Verified by 3 adversarial reviews; suite 792/0.
2026-06-23 10:43:47 +03:00
agra
c41f51aed3 fix: validate protocol impl method signatures vs the protocol declaration (issue 0178)
The issue-0176 conformance gate was name-only, so an impl P for T with a
mismatched return/param type (or arity) built a wrong-ABI thunk that
silently miscompiled (exit 0, wrong value). firstUnimplementedMethod now
validates arity (after self), each param type, and the return type
against the protocol declaration, substituting protocol Self->concrete
via resolveProtoTypeSubSelf (recurses through pointer/many-pointer/
optional/slice/array so []Self<->[]T match; conservative .unresolved for
Self-in-generic-arg). Comparison is by structural formatTypeName
(alias/module/spelling independent); typesClearlyDiffer skips when either
side has an unresolved leaf at any depth, biasing against false-positives.

Regressions: diagnostics/1201 (negative), protocols/0420 (positive,
[]Self param). Verified by 3+3 adversarial reviews (a mid-fix []Self
false-positive was found and closed); suite 792/0.
2026-06-23 08:48:31 +03:00
agra
58f97fff10 fix: diagnose ?? with a non-optional lhs instead of codegen panic (issue 0172)
lowerNullCoalesce fed resolveOptionalInner's .unresolved (returned for a
non-optional lhs) into the merge-block params / optionalUnwrap / RHS
target type, reaching codegen and panicking 'unresolved type reached
LLVM emission'. Guard: when inferExprType(nc.lhs) is a resolved
non-optional type, emit a located diagnostic and bail; an .unresolved
lhs (prior error) is excluded to avoid double-report. ?? is optional-only
per specs.md (error unions use or/catch), so rejecting a failable lhs is
correct; comptime panic closed too.

Regression: examples/diagnostics/1200-diagnostics-null-coalesce-non-optional.sx.
Verified by 3 adversarial reviews, suite 790/0. Filed adjacent bug 0180
(?? lowering defects for generic/alias/tuple optional lhs).
2026-06-23 03:31:58 +03:00
agra
e5b682e622 fix: reject implicit ?T -> bool coercion instead of silent false (issue 0169)
The Optional->Concrete unwrap classify rule treated ?i64 -> bool as
unwrap+narrow (both builtin), silently yielding false for every optional
(present or null). specs.md defines no implicit optional->bool
conversion. Reject it: conversions.zig adds an optional_to_bool_reject
plan (dst == bool, child != bool); coerce.zig emits a located diagnostic
suggesting '!= null'. Covers arg/field-init/return via the shared
coerceMode. The if-opt presence test (issue 0164) is a separate path,
untouched.

Regression: examples/diagnostics/1199-diagnostics-optional-to-bool.sx +
conversions.test.zig unit test. Verified by 3 adversarial reviews, suite
789/0. Filed adjacent issue 0179 (whole implicit ?T->concrete unwrap
family silently miscompiles a null optional; design-touching).
2026-06-23 02:47:51 +03:00
agra
3c738695dc fix: diagnose non-conforming protocol erasure instead of unreachable-thunk SIGABRT (issue 0176)
Erasing a type to a protocol when it conforms only via a free function
(not an explicit impl P for T) built a vtable of unreachable thunks ->
SIGABRT on first dispatch, with no diagnostic. Per specs.md erasure is
impl-driven, not structural, so the erasure was never valid.

Add a conformance gate (firstUnimplementedMethod in buildProtocolValue,
src/ir/lower/protocol.zig): emit a located diagnostic when a protocol
method has no reachable impl, or when an impl method introduces its own
type params (signature mismatch — it bails lazyLowerFunction and would
reach the unreachable thunk). A std.debug.panic tripwire guards the
diagnostics==null path so a non-conforming erasure can never silently
ship as undef. Gate<->thunk equivalence verified bidirectional.

Regressions: protocols/0419 (positive struct-field dispatch),
diagnostics/1197 (no-impl) + 1198 (generic-method signature mismatch).
Updated memory/0808 (it erased a non-conforming type that never
dispatched). Verified by 3+1 adversarial reviews, suite 788/0. Filed
adjacent bug 0178 (protocol impl method type-mismatch silent miscompile).
2026-06-23 02:13:30 +03:00
agra
3605165398 fix: dispatch unwrapped optional-closure call g!() through call_closure (issue 0170)
Calling through an unwrapped optional closure (g!()) crashed with LLVM
'Called function must be a pointer!': the indirect-call catch-all else
arm emitted call_indirect on the whole {fn,env} closure struct with a
hardcoded .i64 return. The else arm now inspects inferExprType(callee):
a .closure callee dispatches through call_closure (threads env + ctx via
the [ctx, env, user_args] ABI, returns closure.ret); a plain fn pointer
uses call_indirect with the callee's real function.ret instead of i64.

The filed repro's ?(() -> void) spelling is a tuple-optional (now
diagnosed by the 0165 fix); the real ?Closure(...) layout was already
correct. Verified load-bearing (HEAD crashes) by 3 adversarial reviews,
suite 785/0. Regression: examples/closures/0311-closures-optional-closure.sx.
Filed adjacent bug 0177 (array-element closure direct call crashes).
2026-06-23 01:02:13 +03:00
agra
28bb101a4a fix: literal element typing — typed-array null element, tuple coercion, positional var element (0173-0175)
0173: resolveArrayLiteralType gained no arm for [N]T/[]T heads, so a
([2]?i64).[...] head lost its ?i64 element type and a bare null reached
LLVM as const_null(.unresolved). Route structural heads through
resolveTypeWithBindings; validate an undefined element name in the head
via UnknownTypeChecker (semantic_diagnostics.zig) instead of a silent
empty-struct stub (no-silent-fallback).

0174: positional .{...} against a TUPLE target now coerces each element
to TupleInfo.fields[i] (was neither struct nor array, so uncoerced).

0175: a positional struct literal with a bare-variable element was
misclassified as a named shorthand (parser puns .{x} -> x=x), zeroing
the fields. has_names now consults the struct definition to reclassify a
punned non-field name as positional; positional coercion uses the
lowered value's real getRefType.

Regressions: optionals/0914, types/0199, types/0200, diagnostics/1196.
Verified by 4 adversarial reviews; suite 784/0. Filed adjacent bug 0176
(protocol-typed struct field method call aborts).
2026-06-23 00:25:28 +03:00
agra
5a436eddb1 fix: coerce array/vector literal elements to element type (issue 0168)
[N]?T arrays were corrupted: a positional literal .{ null, 7 } stored
bare T/null elements into {T,i1} optional slots because array elements
were never coerced (getStructFields is empty for an array, so the
i<struct_fields.len field-coercion gate never fired). A present element
then read back as absent and direct indexing segfaulted.

lowerStructLiteral's positional branch now computes array_elem_ty for
array/vector targets and coerces each element to it; lowerArrayLiteral
generalizes its slice-only coercion to coerce every element via
coerceToType (layout-aware: scalar->{T,i1}, pointer-sentinel->one-word,
array->slice, concrete->protocol). Verified by 3 adversarial reviews,
suite 780/0.

Regression: examples/optionals/0913-optionals-array-of-optionals.sx.
Filed adjacent pre-existing bugs: 0173 (typed .[null,..] element), 0174
(tuple positional-element coercion), 0175 (positional struct literal
variable element zeroed).
2026-06-22 22:50:20 +03:00
agra
2ea25e84ec fix: thread optional child type into ?? struct-literal default (issue 0166)
The RHS of a null-coalesce was lowered with no target type, so a bare
struct literal default (x ?? .{ ... }) produced a struct_init with
.ty == .unresolved that panicked in emitStructInit. lowerNullCoalesce
now saves self.target_type, sets it to the optional's resolved child
before lowering nc.rhs, and restores it (leak-free). Verified across
struct/slice/enum/tuple/protocol/nested-optional/generic child types by
3 adversarial reviews.

Regression: examples/optionals/0912-null-coalesce-struct-literal.sx.
Filed adjacent pre-existing bug 0172 (?? on a non-optional lhs panics).
2026-06-22 22:17:01 +03:00
agra
0bc8005b99 fix: diagnose ?(?T) tuple-payload mismatch instead of malformed IR (issue 0165)
In type position (T) is a 1-tuple (specs.md:843), so ?(?i64) is
optional(tuple(?i64)); assigning a bare ?i64 had coerceToType classify
.none and pass the value through, then optionalWrap built a corrupt
insertvalue that aborted the LLVM verifier. After coercing toward an
optional's child, verify the coerced type equals the child type
(stmt.zig decl-init + coerce.zig .optional_wrap); on mismatch emit a
located diagnostic (tuple-specific note only when the child is a tuple).
formatTypeName now renders tuples as (x: i64, y: i64).

Regressions: optionals/0911 (nested optional via alias, round-trip),
diagnostics/1195 (the mismatch diagnostic). Updated diagnostics/1101 +
protocols/0414 goldens for the improved tuple type-name rendering.
Verified by 3 adversarial reviews. Filed adjacent bug 0171 (?any child
not canonicalized).
2026-06-22 21:54:12 +03:00
agra
3e8d003e3d fix: bindingless if/while/and/or over optional reads has_value (issue 0164)
lowerIfExpr emitted optional_has_value only for the binding form; a bare
'if opt' passed the raw {T,i1} aggregate to condBr, where emitCondBr's
catch-all struct arm silently folded it to 'i1 true' (structs always
truthy) — a silent miscompile that took the present-branch for null
optionals. while / and / or shared the same defect.

Reduce bindingless optional conditions to optional_has_value in
lowerIfExpr/lowerWhile and via a new lowerBoolCondition helper for and/or
operands. Replace the silent-true emitCondBr arm with a lowering-time
diagnostic (checkConditionType/isValidConditionType) rejecting conditions
whose type isn't bool/integer/pointer/optional; the backend @panic is now
an unreachable tripwire.

Regressions: examples/optionals/0908..0910 + diagnostics/1194 (negative).
Verified by 3+3 adversarial reviews.

Filed adjacent bugs found during review: 0168 (array-of-optionals element
load), 0169 (optional->bool coercion), 0170 (closure-optional layout).
2026-06-22 21:04:05 +03:00
agra
7c21f84151 fix: comptime VM reg→value bridge for optional results (issue 0162)
Add an .optional arm to regToValue in comptime_vm.zig: read the
has_value flag at offset sizeof(child), bridge the payload recursively
into a { payload, i1=true } aggregate when set, yield .null_val (zero
{T,i1}) when clear or the bare null sentinel. Matching serialize arm in
serializeAggregateValue (emit_llvm.zig). Pointer/?Closure/?Protocol-child
optionals and array-payload aggregates bail loudly, not silently.

Regression: examples/comptime/0643-comptime-run-optional-aggregate.sx
(present ?T, present ?i64, null ?i64). Verified by 3 adversarial reviews.
2026-06-22 19:42:41 +03:00
agra
ff9e448f8c fix: optional-chain getter/field correctness from 0160 adversarial review
Five adversarial reviews of the issue-0160 fix surfaced three more bugs in the
touched optional-chain / optional-coercion code; all fixed here:

1. A COLD generic-instance getter through `?.` (`?*Vec(i64)` `.getter`, never
   called directly first) panicked with "unresolved type reached LLVM emission":
   a cold instance method is absent from resolveFuncByName, so the getter's
   return type resolved to .unresolved → a ?unresolved merge type. lowerOptionalChain
   and getterReturnTypeOnDeref now warm the monomorph (ensureGenericInstanceMethodLowered)
   before querying its return type. (The 0907 test passed only by luck — List(i64)
   is warmed by stdlib use; 0907 now also exercises a cold user generic.)

2. A real-field read through a `?*T` chain (`op?.field`, op: ?*T) reinterpreted
   the pointer bits as the field (silent garbage) — the some-branch real-field
   path didn't load through the pointer. It now derefs `?*T` before the field
   access. (Pre-existing — the else-branch predates 0160 — but it's the same
   function and a silent miscompile, so fixed here.)

3. `?[]T = array` skipped the array→slice promotion (corrupt .len/.ptr): the
   lowerVarDecl optional arm wrapped the raw array. It now coerces the value to
   the optional's child type (array→slice) before wrapping.

Regression examples 0906/0907 extended to cover all three. Distinct PRE-EXISTING
bugs the reviews surfaced in untouched subsystems are filed as issues 0161
(struct-literal vs scalar), 0162 (#run returning an optional aggregate), 0163
(untagged-union payload-binding match).
2026-06-22 18:55:41 +03:00
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