Commit Graph

219 Commits

Author SHA1 Message Date
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
2637ae98a5 docs: file issues 0164-0167 (optional/comptime bugs found during 0162 review)
0164 if <optional> no-binding folds has_value to true (silent miscompile)
0165 parenthesized nested optional ?(?T) malformed double-wrap (crash)
0166 ?? .{ } struct-literal default unresolved type (crash)
0167 comptime regToValue array-in-aggregate gap + unclean recovery
2026-06-22 19:43:55 +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
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
8367ad18b1 fibers: M:1 scheduler core + suspending fiber-task async (B1.5a, B1.4a)
library/modules/std/sched.sx: a generic Fiber + Scheduler over the
proven naked swap_context on guarded mmap stacks --
init/spawn/yield_now/suspend_self/wake/run (B1.5a), then Task($R) +
go/wait/cancel, a truly-suspending nullary-thunk async layer (B1.4a).
go(work) runs a thunk as a real fiber; wait() parks the caller until it
completes. Self-contained in sched.sx (io.sx importing it would
duplicate the _fib_tramp global asm).

Hardened per adversarial review: wake guarded on .suspended (FIFO
corruption), suspend_self/yield_now guard a null current, loud
mmap/mprotect/OOM/deadlock bails, cancel skips not-yet-run work.
Closure-env + heap-Task leaks documented (bounded, default-GPA-invisible).

Examples: 1811 (round-robin), 1812 (suspend/wake + spurious-wake guard),
1813 (async interleave + await-suspend + cancel). Also files issue 0155
(scalar-pointer index panics codegen -- non-blocking, found in review).
2026-06-21 18:44:03 +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
6d1409bc1f test: remove resolved/fixed issue writeups
Delete the issues/*.md whose writeup carries a RESOLVED or FIXED banner;
only the open issues (0030, 0148) remain.
2026-06-21 14:41:18 +03:00
agra
eb93c63c45 docs(issue 0148): record root cause, working mechanism, and blast-radius findings
Attempted the canonicalize-path fix (realpath + cwd-relativization for display);
it fixes the e2e absolute-entry duplication but ripples path-identity changes
into ~8 import-subsystem unit tests + 2 cosmetic snapshots. Reverted as too broad
for a drive-by rework; documented a minimal repro and a recommended deliberate
approach (lexical normalize, single chokepoint, batch test updates). Issue stays
OPEN.
2026-06-21 09:49:55 +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
c21b683b08 docs(issues): mark 17 already-fixed issues RESOLVED with verified banners
Each banner was re-verified against the current binary (repro now behaves
correctly) and cites the actual fix location in current src/** plus the covering
regression example. Closes the stale-but-fixed backlog: 0019, 0042-0056, 0131.
No compiler change.
2026-06-21 09:25:52 +03:00
agra
4fc5411cd9 fix: allow void (zero-sized) struct/tuple fields instead of crashing (issue 0150)
A struct/tuple/?T with a void field crashed the compiler: the field lowered to
LLVM's unsized 'void' type, which traps getTypeSizeInBits. Lower a void field to
a SIZED zero-byte [0 x i8] (fieldLLVMType) so the enclosing aggregate stays sized
with identical element indices, and skip inserting a value for a void field in
emitStructInit (the i64 placeholder would type-mismatch the [0 x i8] slot and
corrupt the aggregate constant -> runtime bus error). Future(void) now works.

Regression: examples/0190-types-void-struct-field-zero-sized.sx
2026-06-21 09:21:18 +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
6ed29621ad fix: diagnose missing 'main' instead of segfaulting on 'sx run' (issue 0137)
A program with no 'main' reached the JIT entry-point call with a garbage
address (ORC reports lookup success but leaves main_addr degenerate), then
called it -> SIGSEGV. Add a pre-JIT entry-point check in main.zig that emits
'error: no main function found' and exits non-zero before codegen, plus a
defensive main_addr==0 guard in target.zig runJITFromObject as a backstop.

Regression: examples/1188-diagnostics-run-no-main.sx
2026-06-21 09:10:30 +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
a7499d5f51 fibers B1.2: 0152 fixed → Atomic(bool) works; blocked on 0153 (re-export value-failable loses ! channel)
With 0151 + 0152 fixed, the async surface is callable and Atomic(bool) works.
Building the async examples isolated the TRUE remaining blocker (the earlier
'secondary or PHI' symptom, confirmed NOT an Atomic cascade): a re-exported
generic value-failable ($R, !E) fn loses its ! error channel at the call site
— the result types as a plain tuple, so await(...) or { ... } / try ...await()
fail / build a malformed i1 PHI. await/IoErr are re-exported via std.sx, so the
async surface hits it.

Narrowed to the generic + re-export co-requirement (non-generic re-export OK;
direct generic import OK). Filed issues/0153 with a minimal co-located 2-file
repro + a single-file stdlib-await repro + investigation prompt (root cause:
the monomorphized return-type's error-set, reached via the re-export alias,
resolves to a non-.error_set TypeId, so errorChannelOf misses the channel).
Per the STOP rule, paused B1.2's async examples pending the 0153 fix.
2026-06-21 05:45:27 +03:00
agra
e5586f61b8 issue 0152 RESOLVED: byte-promote sub-byte (Atomic(bool)) atomic load/store
LLVM rejects a sub-byte atomic memory access (must be byte-sized), so
Atomic(bool) — bool lowers to i1 — failed verification on load/store. The
atomic emitters in src/backend/llvm/ops.zig now perform a sub-byte access in
its byte storage type (i8) and trunc/zext the value at the boundary (new
atomicByteType helper: i8 for .bool, null otherwise). rmw/cmpxchg are left
as-is on purpose — a bool rmw/CAS is rejected at the sx level (integer-only),
so a sub-byte element never reaches those emitters.

Regression test examples/1705-atomics-bool-byte-promoted.sx. Suite green 729/0.
Unblocks Future.canceled: Atomic(bool) in the B1.2 async layer.
2026-06-21 05:42:48 +03:00
agra
ea1faf7b69 fibers B1.2: 0151 fixed → async surface callable; blocked on 0152 (Atomic(bool) i1 atomic)
issue 0151 (generic $T inference through generic-struct / pointer / UFCS-pack
params) is fixed and committed, so io.sx's async/await/cancel are now callable
in every form. Building the async examples then tripped a SEPARATE codegen bug:
Atomic(bool) emits a sub-byte (i1) atomic load/store that fails LLVM
verification (must be byte-sized). Future.canceled: Atomic(bool) hits it.

Filed issues/0152 with a standalone repro + investigation prompt (codegen fix
in src/backend/llvm/ops.zig — promote sub-byte atomics to i8 storage). Per the
STOP rule, paused B1.2's async examples (1805/1806) pending the 0152 fix.
Checkpoint updated: 0151 RESOLVED, async surface BLOCKED on 0152.
2026-06-21 05:27:41 +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
0ab26c8a40 fibers B1.2: record review findings — async surface blocked on 0151 (widened)
Adversarial review of 45d869d: the Io infrastructure (both materializers,
push-inherit, 37 .ir regens, !-lint) is correct + landed; but await/cancel
(*Future($R)) are uncallable in EVERY form because sx can't infer a generic
$T from a pointer-wrapped arg. Widened issue 0151 to that root (repro:
unbox(b: *Box($T)) -> $T). Checkpoint: B1.2 partially landed; next = fix 0151
generic inference -> make await/cancel callable -> add 1805/1806 -> B1.3.
2026-06-21 00:43:09 +03:00
agra
3eeb965925 issue 0151: UFCS dot-call leaves $R inferred from a closure return type via a pack unresolved
A generic free fn whose `$R` is inferred from a worker `Closure(..$args) -> $R`
(+ trailing `..$args`) and which returns a type built from `$R` (`-> Wrap($R)`)
monomorphizes correctly when called directly (`f(recv, worker, ..)`) but leaves
`$R` UNRESOLVED when called via UFCS dot syntax (`recv.f(worker, ..)`) — the
unresolved type reaches LLVM emission and trips the `.unresolved` tripwire
(SIGTRAP). Distinct from RESOLVED issue 0119 (UFCS `$T` from receiver/slice).

Blocks the B1.2 user-facing async idiom `context.io.async((a,b) -> R => ..., x, y)`
(a UFCS call inferring $R from the worker closure's return type). The Io/async
library + compiler plumbing are in place and correct (landed in the prior
commit); only the UFCS call form hits this inference gap. Repro depends on no
project symbols beyond modules/std.sx; unpinned (no expected/ marker) so it
does not run in the corpus.
2026-06-20 22:21:38 +03:00
agra
7bf65565bd fibers B1.2: UNBLOCKED — remove invalid issue 0151, correct the async idiom
The B1.2 "blockers" were not real:
- Issue 0151 was INVALID: its repro used the non-idiomatic `($A) -> $R`
  bare-fn-ptr form. The canonical higher-order pack idiom
  `Closure(..$args) -> $R` + `..$args` (see examples/0543-packs-canonical-map)
  infers $R fine and runs today with no compiler change. Removed 0151.
- The correct async idiom is verified working live (42 42 for homo + hetero
  args): async :: (io, worker: Closure(..$args) -> $R, ..$args) -> Future($R)
  with a lambda worker (annotated params) + a `result = ---; result.v = ...`
  build form. No compiler change needed.

Issue 0150 (void struct field -> SIGTRAP exit 133) IS a real bug but is only
reached via Future(void) (void-returning worker / timeout) — deferred to B1.4;
B1.2 supports non-void workers.

Updates the PLAN/CHECKPOINT B1.2 status to UNBLOCKED with the corrected idiom
and the resume plan. No compiler/library code changed in this commit.
2026-06-20 20:00:36 +03:00
agra
e78320637f fibers B1.2: BLOCKED on compiler bugs 0150 + 0151 (Io design proven)
Stream B1 B1.2 (Io capability + context.io + Future + cancel) is blocked on
two newly-discovered, independent compiler bugs, both with standalone repros:

- 0150: a `void` struct field crashes the compiler with an unsized-type
  SIGTRAP in LLVM getTypeSizeInBits. Blocks `Future(void)` -> `timeout`.
- 0151: a type-var inferred from a fn-pointer parameter's RETURN type is not
  bound as a usable type in the function body (`unknown type 'R'`). Blocks the
  central `async(io, worker: ($A)->$R, arg)` free-fn's `Future(R)`.

The B1.2 design itself is validated end-to-end (the Io protocol threaded on
Context like Allocator, the stateless blocking CBlockingIo default, both
__sx_default_context materializers, and `context.io.now_ms()` all work live).
Only the async/await/timeout ergonomic layer hits the two bugs. Per the
IMPASSABLE STOP rule, all B1.2 working changes were reverted (master green,
726/0) and the work paused pending fixes; WIP is saved at .sx-tmp/b12-wip/.

Checkpoint + plan updated to mark B1.2 BLOCKED with full resume notes.
2026-06-20 18:54:04 +03:00
agra
3fad2d5a21 issue 0144: unrecognized $T-param #builtin silently returns 0
A bodiless #builtin with a $T: Type parameter that no recognizer matches folds
to 0 (exit 0) instead of erroring — while a non-type-param #builtin link-errors
loudly. Discovered during the atomics stream (Atomic methods ran to 0 before
recognition existed). The reflection/type-arg lowering path defaults instead of
rejecting (REJECTED-PATTERNS silent-fallback class). Repro + investigation prompt
in the issue. Open (unpinned — not added to the suite, since the repro currently
exits 0 by the bug).
2026-06-20 14:09:41 +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
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
f807436f04 fix issue 0143: pack-as-[]Type built as []Any — build it as []type_value
buildPackSliceValue (lower/pack.zig) materialized a bare `$<pack>` []Type slice
as []Any (16-byte elements) — a stale mapping from before the dedicated Type
builtin (.type_value, 8 bytes) replaced Type -> .any. It stored 8-byte const_type
words into 16-byte slots, so a []Type reader (8-byte stride) read [t0, pad, t1, ...]
instead of [t0, t1, ...]. The legacy interp's tagged-Value model hid it; the
byte-accurate comptime VM exposed it (a `..$args` pack forwarded as a []Type
argument across a call read its elements shifted/garbled).

Fix: build the pack-slice array + slice as .type_value (8 bytes). Removed the
stopgap type_name .unresolved guard (379ed05) now that the root cause is fixed.

Regression test examples/0525-packs-pack-as-type-slice-arg.sx (outer(42,"hi",true)
-> inner($args: []Type) -> "i64 string bool"). 700/0 both gates; issue 0143 RESOLVED.
2026-06-18 19:42:42 +03:00
agra
a446550013 issues: file 0143 (pack-as-[]Type stride mismatch) + record out is end-state-only
0143: a ..$args pack forwarded as a []Type argument across a call is backed by a
[N x Any] (16B) array but viewed as []type_value (8B) -> half-stride reads. A
lowering bug the legacy Value model masks; the byte-accurate VM exposes it. Blocks
examples/0114 on the VM. Filed per CLAUDE.md (not worked around; the type_name
.unresolved guard only makes the VM decline rather than emit garbage).

Checkpoint also records the sequencing insight: comptime `out` (print) can only
land once the fallback is removed (a print-then-bail double-prints under the legacy
re-run), so side-effecting ops + fallback-removal are the FINAL step; pure ops +
migrations land first.
2026-06-18 19:28:04 +03:00
agra
eb68d9ed94 comptime VM: real lowering-time Context — allocating + List-building type-fns run on the VM (issue 0141)
The VM can now evaluate a comptime type-fn that allocates at lowering time (the
0141 family) — the legacy interp cannot. Four changes:

- runComptimeTypeFunc (lower/comptime.zig): force the CAllocator->Allocator thunks
  to exist (getOrCreateThunks, idempotent, guarded) BEFORE eval. A type-fn const
  runs at scanDecls (Pass 1), before Pass 1c builds the default-context global +
  thunks, so the comptime allocator was otherwise null.
- materializeDefaultContext: build a REAL context at lowering time when the global
  is absent — find the two thunks by name and lay their func-refs into the inline
  Allocator value at the head of Context, so context.allocator.alloc_bytes
  dispatches call_indirect -> thunk -> native VM malloc.
- aggType: deref a pointer base_type (the List write path emits struct_gep with
  base_type = *Struct; fieldOffset panicked on the pointer — now derefs, no panic).
- subslice: handle a [*]T many-pointer / *T base (a List's items field — the base
  IS the data pointer).

Verified end-to-end (manual probe): a compiler-API type-fn building its []Member in
a List(Member) runs HANDLED on the VM and mints (green=7) — the 0141 List-growth
pattern. Can't be a corpus test yet (gate-OFF/legacy can't allocate at lowering
time — the dual-path bind), so locked in via VM unit tests (many-pointer subslice;
struct_gep with a pointer base_type). 697/0 both gates + all unit tests.
2026-06-18 15:04:55 +03:00
agra
3c0e0852a8 issue 0141: re-refine root cause — IR is correct; it's a legacy slot_ptr chain + null comptime allocator (VM has neither failure mode) 2026-06-18 14:41:33 +03:00
agra
9e3aabcf76 comptime VM: Phase 3 — register_type write side + payloadless-enum fixes
The mutating compiler-API, minting types LAZILY at lowering time (single pass,
the existing runComptimeTypeFunc path — so the write side is legacy-only; the
VM isn't wired at lowering time, and the read-side readers stay dual-path):

  declare_type(name) -> Type            forward nominal handle (≈ declare)
  pointer_to(t) -> Type                 build *T references
  register_type(handle, kind, members)  ONE kind-branching fill (≈ unified define)

register_type branches on kind IN THE COMPILER (subsuming define's per-kind
dispatch); codes match type_kind: 1 struct, 2 actual .@"enum", 3 tagged_union,
4 tuple. Members are {name: string, ty: Type}. A non-generic `-> Type` builder is
now flagged is_comptime (decl.zig) so its dead body permits the welded calls.

Graph support: forward declare_type handles + pointer_to express a mutually-
recursive A<->B graph (*A, *B, B-by-value) before bodies are filled. register_type
is idempotent — re-filling a nominal slot (a minting module reached via two import
edges) re-mints identically rather than erroring (nominalIdent reads identity from
any nominal kind).

Fixes (issue 0142):
- A fully payloadless comptime-minted enum was minted as an all-void tagged_union,
  whose IR size disagrees with its LLVM size -> verifySizes panic. Now mints a real
  .@"enum" (register_type kind 2 AND the metatype defineEnum).
- Bare `EnumType.variant` qualified construction of a payloadless variant wasn't
  supported (failed for hand-written enums too — the type name lowered to a Type
  value). Added in lowerFieldAccess via isPayloadlessVariant; payload-carrying
  variants keep their call form.

Examples: 0631 (graph + actual enum + reflection), 0632 (make_enum all-void),
0633/0634/0635 (namespaced / bare / multi-edge import of a minted type), 0187
(qualified variant construction). Unit tests added.

Parity 697/697 (gate OFF and -Dcomptime-flat).
2026-06-18 10:47:36 +03:00
agra
e2b2e22fa7 issue(0141): Direction 2 (defer eval) ruled out by experiment; Direction 1 is the path
Wired a minimal deferral (eval at a new Pass 1c' after the CAllocator
thunks exist) — the List repro STILL bailed with struct_get, and it
destabilized examples/0620. So deferring past the thunks isn't the cause
of the wrong IR; the field-access lowering only emits struct_gep at
body-lowering/emit time. No single pass slot satisfies both 'body lowers
correctly' and 'layout ready before use'. Pivot to Direction 1 (robust
*Struct field-access lowering). Experiment reverted; tree clean.
2026-06-17 08:34:50 +03:00
agra
a448f50f7f issue(0141): refine root cause — wrong IR (struct_get vs struct_gep) at scanDecls
Instrumentation shows List.append lowers list.len/list.cap to struct_gep
(correct) at #run/emit time but struct_get (wrong, value access on a *T
receiver) at scanDecls/metatype time — same source, different IR. The
function IS lowered both ways, just to wrong IR at scanDecls due to
incomplete generic-instantiation context. So an interp-side lazy-lower
hook can't fix it (IR is wrong before the interp runs); the fix is either
robust field-access lowering or deferring the comptime type-construction
eval to a complete-world pass (like #run). Supersedes the two-layer framing.
2026-06-17 08:21:55 +03:00
agra
0f88525884 issue(0141): comptime List growth in type construction (two-layer)
File the last METATYPE deferred enhancement: List(T).append at comptime
bails ('struct_get: base has no fields') in a type-construction ::.
Standalone repro + two-layer root cause (null comptime allocator at
scanDecls; *T slot_ptr struct_get) + investigation prompt. Non-blocking:
array-literal locals already build variant lists (examples/0620/0624).
Checkpoint + Known issues reference 0141.
2026-06-17 08:07:11 +03:00
agra
4da6add334 test(0140): pin comptime type-construction bail diagnostic (examples/1179)
Move the issue 0140 repro into the feature suite as a regression test.
Asserts the build-gating diagnostic 'comptime type construction failed:
comptime define(): enum has no variants' at the construction site, exit
1 — locking out the prior 'unresolved type reached LLVM emission' panic.
2026-06-17 04:36:09 +03:00
agra
37ec3da8cb fix(0140): surface comptime type-construction bail as a diagnostic
evalComptimeType did `interp.call(...) catch return null`, dropping the
interp's last_bail_detail; callers poisoned to .unresolved with no
diagnostic, so the sentinel reached LLVM emission and panicked
("unresolved type reached LLVM emission"), or hid behind a downstream
cascade.

Clear last_bail_detail before the call; on the catch emit a build-gating
.err at the construction expr's span ("comptime type construction
failed: {detail}", mirroring the #run surfacing in emit_llvm.zig), then
return null to keep the .unresolved poison — now gated by a real message
so no unresolved type reaches emission unannounced.

Empty-variant define now prints 'comptime define(): enum has no
variants' and exits 1 (no panic); make_enum-style computed-slice
failures show their root reason at the construction site.
2026-06-17 04:31:38 +03:00
agra
3a062780f7 issue(0140): comptime type-construction bail panics instead of diagnosing
A failing declare/define (e.g. empty variant list) bails correctly in
the interp, but evalComptimeType swallows last_bail_detail via
`catch return null`; the decl poisons to .unresolved with no diagnostic
and reaches LLVM emission -> panic ("unresolved type reached LLVM
emission"), or hides behind a misleading downstream cascade.

Pre-existing (plain define path), surfaced while starting the make_enum
step. Blocks make_enum's computed (pointer-backed) []EnumVariant slice
decode. Repro + investigation prompt filed; CHECKPOINT-METATYPE marked
BLOCKED. Session paused pending fix per CLAUDE.md IMPASSABLE rule.
2026-06-16 22:59:49 +03:00