Port library/modules/std/sched.sx to run on aarch64-linux alongside
aarch64-macOS, validated byte-identical on both via Apple `container`.
Per-OS bits are comptime-branched:
- MAP_AP (mmap MAP_ANON flag): linux 0x22 / macOS 0x1002.
- fd-readiness backend: epoll on linux, kqueue on darwin (epoll import
scoped to the linux branch). block_on_fd, the run-loop Mode-2 drain,
and cancel_io_waiter_for each branch; the epoll paths EPOLL_CTL_DEL on
fire and on early-wake (EPOLLONESHOT only disables a registration;
kqueue EV_ONESHOT auto-removes it).
- first-entry trampoline: a per-OS hand-written global-asm symbol becomes
a naked sx fn fib_tramp (mov x0,x19; br x20) + register-indirect
dispatch (spawn presets regs[1] == x20 == &fib_dispatch), dropping the
per-OS .global symbol entirely.
Fixes issue 0193 Bug A: the trampoline redesign bus-errored on the
go/wait/sleep capstone (1817) until `export "fib_dispatch"` was restored.
Without the export, fib_dispatch reverts to sx's internal ABI (x0 =
implicit context, first arg self shifted to x1) while the trampoline
hands self over in x0 (C-ABI); on first entry the body runs (x1 happens
to alias self) but the closure then loads regs[1] == &fib_dispatch as its
first capture and re-invokes fib_dispatch forever -> stack overflow ->
bus error. The export pins fib_dispatch to the C-ABI (self in x0),
matching the trampoline. Root cause found via lldb on an AOT build;
confirmed against the compiler source.
Bug B (a top-level asm block wrapped in inline-if is dropped during the
comptime-conditional flatten) is carved out to issue 0194 (OPEN) -- no
live trigger remains, since the naked-fn trampoline sidesteps it.
1811/1814/1816/1817 run byte-identical on the aarch64-macOS host and in
an aarch64-linux container; full suite green (817/0). Documents the fiber
runtime in readme.md.
Port of std/sched.sx (the M:1 fiber runtime) to aarch64-linux. The epoll
bindings + std.event.Loop epoll backend are already committed and runtime-
validated (cc137002); this records the SCHEDULER port, which is WIP:
- WORKS, validated in an Apple `container` Linux VM: 1811 (round-robin) and 1816
(block_on_fd over the epoll fd path) run identically to macOS kqueue.
- Bug A: a register-indirect trampoline (naked fn + `br x20`, to avoid a per-OS
hand-written global-asm symbol) bus-errors on the 1817 go/wait/sleep capstone
on both platforms, though 1811/1816 work — unresolved.
- Bug B: wrapping the original global `asm` trampoline in an `inline if`/`case`
drops it (nm: fib_tramp U) in sched.sx's context, though every minimal repro
emits fine — a flatten/lowering interaction in src/imports.zig.
The WIP sched.sx port is preserved both in `git stash` and as
issues/0193-linux-fiber-port.patch. Two resolution paths (either suffices)
documented in the issue. sched.sx itself is left at HEAD (macOS green).
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.
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.
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).
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.
File four issue write-ups discovered alongside the 0179 work:
- 0185: binary-op operand auto-unwrap silently miscompiles a NULL ?T
- 0186: closure VALUE call does not coerce arg to ?T parameter
- 0187: lambda with inferred return type + block body with early returns
mis-infers its return type
- 0188: closure-VALUE calls skip argument validation (arity + tuple spread)
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.
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.
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).
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.
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).
(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).
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.
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.
The type-erased value type is spelled Any (capital), per specs.md and
type_resolver.zig. Lowercase 'any' is an undefined name that resolves to
an empty-struct stub, which is why ?any appeared to silently discard the
value. ?Any round-trips correctly (present/absent/unwrap all work), so
there is no Any-TypeId canonicalization bug. Reword the 0165 cross-ref
accordingly.
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).
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).
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).
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).
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).
[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).
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).
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).
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).
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.
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).
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.
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-`?.`).
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).
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).
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).
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.
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.
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.
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
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.
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
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
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
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).
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
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
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.
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.