Three adjacent cells of the shared count surface still diverged from the
rest; all now route through the same leaf+fold+narrow+diagnose path.
1. Aliased integer constraint bypassed the value-param range gate — only
builtin constraint names matched intTypeRange, so Box(5_000_000_000)
with `$K: Count` (Count :: u32) compiled and bound a truncated value.
resolveValueParamArg (shared by both the struct AND type-fn binder) now
resolves the constraint to its underlying builtin via
canonicalIntConstraintName (Count -> u32, Small -> s8) before
range-checking, so an aliased integer constraint behaves exactly like
the builtin it names.
2. A named const with an expression RHS (M :: 2; N :: M + 1) did not fold
as a count — moduleConstInt read only a literal RHS node. It now folds
every const's RHS through the shared evalConstIntExpr, cycle-guarded
(mutual / self cycles fold to null, not a stack overflow), and pass-0
pre-registers expression-RHS consts. N :: M + 1 == 3 at every consumer:
dim (direct + alias), Vector lane, value-param (struct + type-fn),
inline for.
3. Stateful resolveArrayLen still fabricated length 0 after a failed fold;
it now returns null -> the .unresolved sentinel (no fabrication). The
binding's lowering never reaches sizeOf (alloca defers it; hasErrors
aborts first) and a field access on an already-diagnosed .unresolved
value is poison-suppressed (emitFieldError), so a failed-fold dim emits
ONE clean diagnostic with no panic.
Regressions: examples/0146 (full positive matrix — every consumer x leaf
form), 1135 (aliased u32 + s8 overflow), 1136 (direct non-const dim halts
cleanly). The cascade cleanup also tightened 1502/1503 to one diagnostic.
Unit test added for moduleConstInt expression-folding + cycle detection.
Item 2 (Agra ruling): a compile-time INTEGRAL float (`4.0`, `N : f64 :
4.0`, `N :: 4.0`) used as an array dimension / Vector lane / generic
value-param count / `inline for` bound now folds to its integer at the
shared leaf — `program_index.floatToIntExact`, used by both the
`.float_literal` arm of `evalConstIntExpr` and `moduleConstInt`. All four
consumers route through the one evaluator, so `[4.0]s64` lays out the same
`[4]s64` uniformly; a non-integral (`4.5`) or negative value stays
rejected by the downstream `foldDimU32` gate. Pass-0 now pre-registers
float-valued module consts for forward-alias parity with int consts.
Item 1: a generic value-param bind (`Box($K: u32)`) never range-checked
the folded arg, so `Box(5_000_000_000)` compiled and ran. The bind now
range-checks against the param's declared type — a `u32` count through the
shared `foldDimU32` gate (making program_index's "single u32 gate for
value-param counts" doc true), any other integer type through the new
`program_index.intTypeRange` — and emits a clean "value N does not fit in
u32 parameter K" otherwise. The declared type is threaded via a new
`TemplateParam.value_type`.
Regressions: examples 0145 (integral-float array dim), 1504 (Vector lane),
0611 (inline-for bound), 0209 (value-param integral-float), 1132
(non-integral float dim rejected), 1133 (negative float dim rejected),
1134 (oversized u32 value-param rejected) + program_index float-fold unit
tests. Gate: zig build, zig build test, 406/0 run_examples.
The stateless alias-registration array-dim path collapsed foldDimU32's
distinct .too_large / .below_min outcomes into null, so an oversized type
alias (Big :: [5000000000]s64) emitted the FALSE 'an array dimension is not
a compile-time integer constant' message while the direct form correctly
reported 'array dimension 5000000000 does not fit in u32'.
Add program_index.reportDimError as the single source of dim-error wording
(the stateful path now emits through it too) and type_bridge.foldArrayDim to
surface the DimU32 reason at the alias-registration site. An oversized/negative
alias dim now routes to reportDimError for the same precise message as the
direct form; a genuinely non-const alias dim keeps the alias-specific message.
Regression: examples/1131-diagnostics-array-dim-oversized-u32-alias.sx
Two remaining siblings in F0.4's comptime-int path.
1. Type-returning function with a value param used as a TYPE annotation
(`b : Make(N, s64)` where `Make :: ($K: u32, $T: Type) -> Type`):
- `isValueParamPosition` (semantic_diagnostics) now also skips a value
param of a `fn_ast_map` type-returning function, so `N` is not walked
as the type name "N" ("unknown type 'N'").
- `resolveParameterizedWithBindings` routes a type-returning-function
name to `instantiateTypeFunction` (the `.call` path already did).
- `instantiateTypeFunction` resolves a general return-type expression
(`return [K]T`) with bindings active — not just struct/union returns.
`Make(N, s64)`, `Make(M + 1, s64)`, `Make(3, s64)` all resolve to one
`[3]s64`.
2. Oversized dim/lane fold panicked the compiler (0087): an array dim /
Vector lane folded to a valid i64 (5e9) then narrowed to u32 with an
unchecked `@intCast`. New single gate `program_index.foldDimU32` folds
via `evalConstIntExpr` then range-checks `[min, maxInt(u32)]`; the three
narrowing sites (resolveArrayLen stateful + stateless, resolveVectorLane)
all route through it and emit a clean diagnostic + halt instead of
panicking. Value-param args stay i64 until used as a dim/lane, where the
same gate checks them.
Regressions: examples/0208 (value-param type function), examples/1130
(oversized array dim clean halt), examples/1503 (oversized Vector lane
clean halt). Marks issue 0087 RESOLVED.
Gate: zig build, zig build test, bash tests/run_examples.sh — 398 passed,
0 failed, 0 timed out.
Attempts 1–4 fixed the array-dimension paths but the same length-0
fabrication class survived on every other site that resolves a
compile-time integer. Unify them all on the single shared
`program_index.evalConstIntExpr` so they cannot diverge:
- All three Vector lane resolvers (resolveTypeCallWithBindings,
resolveParameterizedWithBindings, resolveArrayLiteralType) and both
generic value-param binders (instantiateGenericStruct,
instantiateTypeFunction) hand-rolled an `else => 0` switch. A
module-const lane `Vector(N, f32)` fabricated a 0-lane `<0 x float>`
(LLVM "huge alignment" abort); a value-param `Vec(N, f32)` fabricated
a 0 binding / wrong mangled name. They now fold through the shared
evaluator and emit a clean diagnostic + `.unresolved` on a non-const
operand (resolveVectorLane / resolveValueParamArg) — never 0.
- evalComptimeInt (inline-for bounds) delegated to the shared evaluator,
so `inline for 0..M` / `0..(M+1)` fold like array dims. The `<pack>.len`
leaf moved into the shared folder via a new `ctx.lookupPackLen`.
- The unknown-type semantic checker no longer walks a value-param
position (`Vector(N, …)` / `Vec(N, …)`) as a type name (was reporting
"unknown type 'N'").
- The parameterized-type-arg parser and the function-body lookahead
(hasFnBodyAfterArrow) accept a const-EXPRESSION in a value position, so
`Vector(M + 1, f32)` and `[M + 1]T` parse as a return type too (the
latter a pre-existing array-dim sibling that the same heuristic broke).
Regressions: examples/1501 (named-const + const-expr lane, direct +
alias, 3/4-lane reads), 1502 (runtime lane clean-halts, exit 1, no LLVM
crash), 0207 (Vec(N)/Vec(M+1) == Vec(3) instantiation), 0610 (inline-for
const bounds). Shared-evaluator unit test extended with the pack-len arm.
zig build && zig build test && bash tests/run_examples.sh: 395 passed,
0 failed.
A constant-FOLDABLE expression array dimension (`[M + 1]`, `[M * N]`,
`[N - M]`, nested `[M + N - 1]`, parenthesised `[(M + 1) * 2]`, mixing
untyped and typed module consts) was wrongly rejected as "not a
compile-time integer constant" even though every operand is
compile-time-known. Attempts 1-3 resolved only a bare named-const dim or
a literal; an expression dim must be EVALUATED, not rejected.
Fix: the shared dim resolver now routes the dimension through a single
constant integer-expression evaluator (`program_index.evalConstIntExpr`)
that folds integer `+ - * / %` and unary negate over literals and
named/typed module consts, recursively (parentheses carry no AST node).
The leaf-name lookup is delegated via `ctx.lookupDimName`, so the
stateful body-lowering path (`Lowering`, which also sees comptime
constants and generic `$N` values) and the stateless registration path
(`type_bridge.StatelessInner`, module consts only) share the EXACT SAME
folding logic and cannot diverge — an expression dim via a type alias
resolves identically to the direct form.
No-fabrication discipline unchanged: a genuinely non-comptime dimension
(runtime local, non-comptime call, unbound name) or arithmetic that
overflows / divides by zero still yields null -> `.unresolved` -> the
same clean compile-halting diagnostic, never a fabricated length.
- examples/0144-types-const-expr-array-dim.sx: every expression form,
direct vs alias, scalar / string / struct element types (fails on the
pre-fix compiler, passes after).
- examples/1129 re-pointed at a genuinely non-const dimension
(`[get()]s64`, a runtime call) so it still proves the stateless
clean-halt (a foldable expression is no longer an error).
- program_index.test.zig: unit test for evalConstIntExpr folding and
clean-halt-on-non-const.
A type alias whose dimension is a named const (`Arr :: [N]T`) resolves its
dimension eagerly during scanDecls pass 1, on the stateless registration path,
which can only read `module_const_map`. Typed consts (`N : s64 : 16`) register
only in pass 2 and a forward-declared untyped const had not registered yet, so
the stateless resolver saw an empty table, printed a non-fatal warning,
fabricated length 0, and continued — yielding a 0-byte alloca, garbage reads,
and a segfault for slice/struct elements.
- scanDecls pass 0 pre-registers every integer-valued module const before any
type alias resolves, so typed, untyped, and forward-referenced consts all
resolve identically.
- Both dim resolvers now share `program_index.moduleConstInt`, so the stateful
body-lowering path and the stateless registration path cannot diverge.
- `resolveArrayLen` returns `?u32`; `resolveCompound` yields `.unresolved` on
null instead of a 0-length array. The stateful path emits a diagnostic; the
alias-registration path surfaces an unresolved alias as a clean compile error
that aborts the build. The Vector lane-count `else => 0` is fixed the same way.
Regressions: examples/0143 (typed-const dim direct + via alias for s64/string/
struct, forward-ref alias, nested) and examples/1129 (an unresolvable computed
dim halts with a clean diagnostic + non-zero exit). Both fail on the pre-fix
compiler (garbage/segfault; warning+exit0) and pass after.
Makes the F0.4 fixes exhaustive across every resolution / nesting path.
0083 — named-const array dimension, stateless paths. Attempt 1 fixed the
stateful resolver (direct local decls, struct fields, params, returns) but the
binding-free registration-time resolver (`type_bridge`, used for type aliases
`Arr :: [N]T` and inline union/enum field types) still resolved a named dim with
a silent `else 0`, so `Arr :: [N]s64; a : Arr` and `union { a: [N]s64 }` were
still miscompiled (garbage / bus error). Thread the module-global const table
(`ProgramIndex.module_const_map`) into `type_bridge` alongside the alias map, so
`StatelessInner.resolveArrayLen` resolves a named module-const dim to the same
length everywhere. The remaining unresolvable case (a computed/comptime dim on
the binding-free path, which the stateful path hard-errors) now bails LOUDLY
instead of fabricating a 0 length.
0085 — nested slice-literal elements. `lowerArrayLiteral` lowered each element
with the element type as target but appended the raw value. A nested `.[...]`
element at a slice element type (`[][]s64`) still lowers to an aggregate array
`[N]T`, so the outer aggregate held raw arrays where slice {ptr,len} headers
were expected — indexing the inner slice read a garbage pointer and segfaulted.
After lowering each element, coerce a same-element array to the slice element
type via the existing `array_to_slice` op. The coercion recurses with the
nesting, so `[][]T` and deeper materialize at every level — local-bound AND
direct-call-argument forms.
Regressions (fail-before/pass-after demonstrated on the pre-fix compiler):
examples/0140-types-named-const-array-dim.sx — extended with type-alias,
nested [N][M]T, and union-field named dims (s64 / string / struct elems)
examples/0142-types-nested-slice-literal-elements.sx — [][]s64 + [][]string,
local-bound vs direct-arg
src/ir/type_bridge.test.zig — named-const dim resolves to literal length
Gate: zig build, zig build test, bash tests/run_examples.sh (388 passed).
Issues 0083 and 0085 marked RESOLVED.
Two silent-miscompile codegen fixes:
0083 — named-const array dimension. `TypeResolver.resolveCompound`'s array
arm resolved the dimension with `if int_literal ... else 0`, so a named const
(`N :: 16; [N]T`) hit the silent `else 0`: the array became 0-length / 0-byte
and element access ran out of bounds (garbage for scalars, bus error for
slice/pointer/struct elements). The arm now delegates the dimension to
`inner.resolveArrayLen` (symmetric with `inner.resolveInner` for the element).
The stateful `Lowering.resolveArrayLen` evaluates it as a compile-time integer
across the comptime-constant / generic-value / module-global const tables and
emits a diagnostic — no fabricated length — when it isn't one.
0084 — `.[...]` literal passed directly as a call arg. `lowerArrayLiteral`
always yields an aggregate array value; the array→slice conversion is the
caller's job. The local-bound var-decl path did it, but the call-arg coercion
path had no array→slice arm, so `classify([N]T, []T)` returned `.none` and the
raw array was passed where a slice was expected (callee read its {ptr,len}
header off the wrong bytes → 0 / garbage / segfault). `classify` now returns a
new `.array_to_slice` plan for same-element `[N]T → []T`, and `coerceToType`
emits the existing `array_to_slice` op — identical to the local-bound path.
Regressions (fail-before/pass-after demonstrated on the pre-fix compiler):
examples/0140-types-named-const-array-dim.sx (s64 + string + struct elems)
examples/0141-types-slice-literal-direct-call-arg.sx (string + []s64)
Gate: zig build, zig build test, bash tests/run_examples.sh (387 passed).
Issues 0083 and 0084 marked RESOLVED.
The global-init constant serializers in emit_llvm.zig printed a diagnostic
on an unserializable value and then RETURNED an undef/null placeholder and
CONTINUED emitting. For a comptime `#run` global that yields a function
reference (`fp :: #run pick();` where pick returns a function), the build
fell through to the JIT and segfaulted calling through the undef pointer
(exit 134) — a silent miscompile dressed up as a printed error.
Route every genuine bail in the serialization family through a new
`failGlobalInit` helper: it sets `comptime_failed` (so core.generateCode
aborts with a non-zero exit after emit()) and returns an undef placeholder
that never ships, because the halt fires before object emission / JIT. This
covers the comptime func_ref leaf, the require_resolved aggregate func_ref
leaf, the top-level + vtable func_ref globals, the comptime-init catch, and
the remaining heap-walk / aggregate-shape bails. Unresolved-function
diagnostics now name the function instead of its (stdlib-unstable) IR index.
The require_resolved=false Pass-0 placeholder is unchanged (func_map is
empty until Pass 1; the aggregate is re-emitted with require_resolved=true).
Regression: examples/1128-diagnostics-comptime-global-funcref-rejected.sx —
a `#run` global returning a function ref now exits 1 with the diagnostic
(was: exit 134 segfault). Fail-before/pass-after verified.
A module-global initialized with an enum literal silently zero-initialized
to the first tag (`chosen : Color = .green` read back as `.red`), and an
enum tag inside a global array/struct was rejected as non-constant. The
constant serializer had no enum-literal arm.
Add `Lowering.constEnumLiteral`: serialize an enum literal to a
`ConstantValue.int` holding the variant's tag value, resolved against the
destination enum type and respecting explicit variant values; the global's
type drives the backing width at emit time. Wired into `globalInitValue`
(scalar global) and `constExprValue` (array element / struct field / nested
aggregate). A non-enum destination or unknown variant is diagnosed loudly,
never silently zero-initialized. The compiler-injected OS/ARCH globals now
serialize to their real `.unknown` tag (6 / 4); runtime reads are unchanged
(they resolve through comptime_constants), so only the static initializer in
the pinned .ir snapshots changes.
Remove the silent `func_ref => orelse LLVMConstNull` fallbacks in the LLVM
constant emitters: aggregate func_ref leaves carry a `require_resolved` flag
(transient null in Pass 0, loud diagnostic if still unresolved in the
Pass-1.5 re-emit), a top-level func_ref global is resolved in
initVtableGlobals, and the comptime (#run) path bails loudly instead of
emitting a null function pointer.
Regression: examples/0139-types-global-enum-literal-init.sx (scalar, array,
struct field, explicit-value enum u16 stride, struct-array with enum field);
negative: examples/1127-diagnostics-global-enum-literal-bad-variant.sx.
Mark issue 0082 RESOLVED.
A module-global aggregate initializer rejected a `null` literal in a
pointer (or optional-pointer) field as "must be initialized by a
compile-time constant". `Lowering.constExprValue` had no `.null_literal`
arm, so the null leaf returned no constant and the whole aggregate looked
non-constant — even though `null` is the compile-time zero pointer (a
top-level scalar `p : *s64 = null;` already serialized fine).
Add `.null_literal => .null_val` to constExprValue. While here, make the
two LLVM constant emitters exhaustive: emitConstAggregate and the
top-level init_val switch in emit_llvm.zig previously ended in a silent
`else => LLVMConstNull(...)` catch-all (the silent-arm class CLAUDE.md
mandates rooting out). They now handle every ConstantValue tag explicitly
(.null_val/.zeroinit -> all-zero constant, .undef -> LLVMGetUndef,
.func_ref resolved, nested .vtable is a hard @panic tripwire). The
reject-loud path for genuinely non-constant fields is preserved.
Regression: examples/0138 (array-of-struct null ptr fields, array of
all-null pointers, nested struct-in-struct null ptr) and the negative
examples/1126 (null ptr field beside a non-const field still errors).
Fail-before/pass-after verified.
A module-global array of struct literals (`pairs : [2]Pair = .[ .{...}, .{...} ]`)
was emitted as `zeroinitializer`, silently dropping every declared field — reads
returned 0 with no diagnostic. Global struct literals and struct-with-array
already worked; the gap was struct literals used as ARRAY elements.
Root cause: `Lowering.constExprValue` (the const-aggregate serializer for global
initializers) had no `.struct_literal` arm. `constArrayLiteral` serialized each
element through `constExprValue`, so a struct-literal element returned null,
collapsing the whole array initializer to null; `globalInitValue` then emitted no
payload and the LLVM backend zero-initialized the global — the same silent-zero
class as 0071/0072, one level inside an array literal.
Fix: make `constExprValue` type-aware — thread the destination element/field
TypeId so a struct-literal leaf routes through `constStructLiteral` and a nested
array-literal through `constArrayLiteral` with the correct element type.
`constArrayLiteral` derives its element type from the array TypeId;
`constStructLiteral` passes each field's type. A global aggregate initializer that
still does not fully reduce to a compile-time constant is now rejected loudly
(`diagnoseNonConstGlobal`) instead of silently zeroing. `emitConstAggregate`
already recurses over nested aggregates, so `sx run` (JIT) and `sx build` (AOT)
both materialize the declared values.
Regression: examples/0137-types-global-aggregate-literal-init.sx (global
[N]Struct literal, global struct literal, struct-with-array, nested
array-of-struct-with-array; values read back with no prior store, plus a store on
top). Fails on the pre-fix compiler (array-of-struct fields read 0), passes after.
Marks issues 0079 (already resolved) and 0080 RESOLVED.
A store to a module-global array element (`g[i] = v`) was silently dropped:
a subsequent `g[i]` read the array's initializer, not `v`. Constant index,
variable index, and cross-function stores were all affected, in both `sx run`
and `sx build`. Global scalars and local arrays were fine.
Root cause: `Lowering.lowerExprAsPtr` (the lvalue/address path) handled only
local identifiers. A module-global identifier fell through to the value
fallback `lowerExpr`, which emits `global_get` — loading the whole array by
value. The LLVM backend's `emitIndexGep` then allocas a throwaway temp, copies
the value in, and GEPs into the temp, so the store wrote a discarded copy.
Fix: teach `lowerExprAsPtr`'s identifier arm about globals — emit `global_addr`
(a pointer into the global's live storage), or `global_get` for a pointer-typed
global (mirroring the local pointer case). Route the `address_of(index_expr)`
array base through `lowerExprAsPtr` too so `&g[i]` is likewise an lvalue into
the global. `index_gep` now GEPs directly into the global for const and variable
index, across functions. This also fixes global struct field stores, which
shared the same root cause.
Regression: examples/0136-types-global-array-element-store.sx (const-index,
var-index, cross-function store on a scalar global array; struct-element array
for stride; nested-array global for the recursive lvalue). Fails on the pre-fix
compiler, passes after.
A string `==`/`!=` used as an operand of a short-circuit `and`/`or` emitted
invalid LLVM (`PHI node entries do not match predecessors!`). String compares
expand into their own memcmp sub-CFG during LLVM emission, so the operand
finishes in a later basic block (`str.merge`) than the one the IR block
started in. `fixupPhiNodes` wired the short-circuit merge PHI's incoming edge
to `block_map[ir_block]` (the block the IR block started as), recording a
stale predecessor (`%entry`/`%and.rhs.0`).
Fix: record the builder's actual insertion block after emitting each IR
block's instructions (`term_block_map`, via `LLVMGetInsertBlock`) and use it
as the PHI predecessor. General — corrects the incoming block for any operand
that emitted intermediate basic blocks (string `==`, value `match`, …), not
just string `==`.
Regression: examples/0045-basic-string-eq-short-circuit.sx (string `==` on
both sides of `and` and of `or`, plus a match-value + enum-payload `==` shape).
Fails (LLVM abort) pre-fix, passes after.
The reserved-type-name binding diagnostic fired correctly but underlined the
enclosing statement / if / while / for / match / protocol / #objc_class block
because every binding-name check reused the parent `node.span`.
Thread each binding name's own span through the AST and parser, and pass it to
`checkBindingNames`:
- ast: add name spans to VarDecl, DestructureDecl, If/WhileExpr, ForExpr
(capture + index), MatchArm, Catch/OnFailStmt, Protocol/ForeignMethodDecl.
- parser: populate each span at the binding site from the name token's loc;
destructure reuses each target identifier's own span.
- semantic_diagnostics: every checkBindingName call now passes the binding's
own span — no site falls back to node.span. fn/lambda params already used
Param.name_span.
Carets now land on the offending identifier itself. New regression
examples/1125 asserts the protocol default-body and sx-defined #objc_class
method param spans; 0125/1119-1124 expected updated to the precise carets.
The reserved/builtin-type-name binding diagnostic was a hand-walked subset
of binding-bearing AST nodes with a silent `else => {}`, so each review
found another syntactic binding form that bypassed it and hit the original
LLVM verifier abort: destructure names (`s2, x := …`), `impl` method
params/locals, and `if` / `while` / `for` / match-arm / `catch` / `onfail`
captures.
Rewrite `checkBindingNames` (src/ir/semantic_diagnostics.zig) as an
EXHAUSTIVE `switch` over every `Node.Data` tag with NO `else` arm — a future
binding-bearing node type now fails to compile until it is handled here, so
coverage is enforced by the compiler instead of a hand-maintained list. The
check stays in the pre-lowering semantic pass rather than moving to the
`Scope.put` scope-registration choke point: lowering is lazy, so an
uncalled function's bindings never reach `Scope.put`, yet they must still be
rejected at their declaration (e.g. the never-called `takes_u8` in 1119).
No lowering special-case; `lower.zig` unchanged.
Regression tests (fail-before: LLVM abort or silent accept → pass-after:
clean diagnostic, exit 1):
- 1121 control-flow: destructure, if/while bindings, for capture+index,
match-arm capture
- 1122 impl-block method: reserved param AND reserved local
- 1123 catch + onfail tag bindings
- 1124 destructure name reserved in an imported module
Existing 0125 / 1119 / 0135 / 1120 tests kept; full suite 368 passed.
The issue-0076 reserved-type-name binding diagnostic only ran over main-file
decls, so an imported module (or the stdlib) could still declare `s2 := ...`
and reach lowering, where the address-of family loads the whole aggregate and
passes it by value to a `ptr` param — LLVM verifier abort.
Extend coverage to every compiled module: a dedicated `checkBindingNames` walk
(in semantic_diagnostics.zig) visits every var/`:=`/typed-local binding name and
function/lambda/struct-method parameter at any depth, with NO main-file filter,
descending the `namespace_decl` that a `mod :: #import` wraps so imported-module
decls are reached. It tracks each module's source_file (save/restore per node)
so the diagnostic renders against the imported module's text. Rejection still
defers to the parser's `Type.fromName` classifier; the unknown-type check (0064)
stays main-file-only. No lowering special-case; `.identifier`-only address-of
paths are unchanged.
Stdlib audit: the only reserved-name bindings under library/ were two `u1`
locals in ui/renderer.sx (UV coords) — renamed to u_min/u_max/v_min/v_max.
Regression test: examples/1120-diagnostics-imported-reserved-type-name.sx (+
companion mod.sx) — an imported `s2 := ...` now emits the clean diagnostic at
the import's declaration site (exit 1), not an LLVM abort.
Resolves issues 0076 (coverage extension) and 0077.
A value binding (local/global `var` or a parameter) spelled as a
reserved/builtin type name parses as a `.type_expr` rather than an
`.identifier` (parser.zig, via `Type.fromName`), so the address-of
family in lower.zig never saw a scoped local and mis-lowered it —
loading the aggregate and passing it by value to a `ptr` parameter
(LLVM verifier abort, or a silent `*self`-mutation-losing copy).
Add a declaration-site diagnostic in semantic_diagnostics.zig
(`UnknownTypeChecker.checkBindingName`): reject any parameter name or
`var` binding name (`:=` / typed-local / global forms) whose spelling
collides with a reserved type name. `isReservedTypeName` defers to the
parser's own classifier (`types.Type.fromName`) so the rejected set
never drifts from the set that would parse as a type — the named
builtins (bool/string/void/f32/f64/usize/isize/Any) and `[su]N` over
sx's 1-64 range. Bare value names (`s`, `self`, `index`) are untouched.
No lowering special-case; the `.identifier`-only address-of paths are
correct once type-shaped names can never be bound. The rejected
attempt-1 `bareVarName` approach was never landed.
Tests:
- 0125-types-type-named-var-rejected: `:=` form (s2) rejected
(repurposed from the old test that asserted the now-illegal behavior).
- 1119-diagnostics-reserved-type-name-as-identifier: parameter (u8),
typed-local (s64, bool), `:=` (string) forms rejected.
- 0135-types-self-streaming-nonreserved: positive — `*self` streaming
with non-reserved names accumulates correctly via both call styles.
- 0904-optionals: renamed incidental locals s1/s2 -> filled/empty.
The `type_name` / `type_eq` reflection builtins resolved their Type arg's IR
type via `getRefIRType(...) orelse TypeId.s64`, then gated `== .any`. A failed
must-succeed lookup silently became `.s64` (`!= .any`), classifying a boxed
`Any` arg as bare i64 and reading the wrong value with no diagnostic.
Add the sibling classifier `LLVMEmitter.reflectArgRepr`, which routes the
lookup through `argIRTypeOrFail` (the issue-0074 `.unresolved` resolver) and
returns `{ boxed, bare, unresolved }`. The three emit sites in ops.zig
(`type_name` + `type_eq` x2) now switch on it: `.boxed` extracts the Any value
field, `.bare` uses the value directly, `.unresolved` hits a hard `@panic`
tripwire — never silently treated as bare. Real args always resolve, so the
happy path is byte-identical (suite stays 361/0, zero snapshot churn).
Secondary `lower.zig` `null_literal`/`undef_literal => target_type orelse .void`
confirmed intentional (typeless-literal default deliberately handled by
emitConstNull/emitConstUndef as null-ptr / undef-i64) — left with an invariant
comment, not the `.unresolved` tripwire.
Regression test in emit_llvm.test.zig asserts the loud path: fail-before with
`orelse .s64` yields `.bare`; pass-after yields `.unresolved`.
Four FFI call-arg lowering sites resolved an argument's IR type via
`getRefIRType(arg_ref) orelse .void` — a silent fallback to the load-bearing
real type `.void`. A failed lookup there is a codegen invariant violation, but
`.void` is treated by downstream `toLLVMType` → `abiCoerceParamType` →
`coerceArg` as a legitimate void-typed foreign argument, corrupting the call
ABI with no diagnostic.
Add one shared resolver `LLVMEmitter.argIRTypeOrFail` that returns the
dedicated `.unresolved` sentinel on a failed lookup — never `.void`/`.s64` — so
the failure cannot masquerade as a real type and trips `toLLVMType`'s existing
hard `@panic` tripwire at the call site. Route all four sites through it:
- src/ir/emit_llvm.zig JNI constructor (NewObject) arg loop
- src/backend/llvm/ops.zig objc_msgSend arg loop
- src/backend/llvm/ops.zig JNI non-virtual call arg loop
- src/backend/llvm/ops.zig JNI Call<Type>Method arg loop
Happy path is byte-identical (every real arg already has a resolved type); FFI
examples stay green with zero snapshot churn.
Regression test (fail-before/pass-after) in src/ir/emit_llvm.test.zig asserts an
unresolvable FFI arg ref now yields `.unresolved`, not the old silent `.void`.
Remove the legacy parallel type model's compiler-like surface. The
compiler pipeline resolves/lowers/lays out against canonical
src/ir/types.zig (TypeId/TypeTable); src/types.zig.Type is now strictly
editor-indexing + parse-time name metadata.
- src/types.zig: delete the type-resolution surface (widen, bitWidth,
isImplicitlyConvertibleTo) and every helper left dead once it was gone
(eql, isInt/isFloat/isSigned/isUnsigned, isTuple/isVector, and the
already-unused classification predicates isEnum/isUnion/isString/
isStringLike/isAny/optionalChild/sliceElementType/manyPointerElementType/
vectorElementType/isFunctionType/isClosureType/isCallable). Keep the Type
union plus the display/name-classification helpers sema/lsp/parser use
(fromName, fromTypeExpr, toName, displayName, isStruct/isOptional/isSlice/
isPointer/isManyPointer/isArray, pointerPointeeType). Seal the file with a
doc comment.
- src/sema.zig: inferExprType no longer calls Type.widen for arithmetic;
it approximates the display type as the left operand's (no second
resolver in the editor index).
- src/ir/type_bridge.zig: delete the dead bridgeType (legacy Type -> TypeId)
function + its sole sx_types import; resolveAstType and the AST->TypeId
path are untouched.
- src/ir/ir.zig: drop the bridgeType re-export.
- src/ir/type_bridge.test.zig: drop the two bridgeType tests (function gone).
Gate: zig build, zig build test (exit 0), tests/run_examples.sh 361/0,
zero examples/expected churn.
Remove the last compiler dependency on sema as semantic truth and stop
publishing as-you-type sema diagnostics from the LSP.
- core.zig: drop dead `Compilation.analyze()`, the `sema_result` field,
and the sema->diagnostics merge; drop the now-orphaned sema import.
The CLI pipeline (parse -> resolveImports -> generateCode) never called
analyze(), so this removes only dead code.
- lsp/server.zig: rename `analyzeAndPublish` -> `refreshEditorIndex` and
delete its sema-diagnostic publish (and the now-unused `semaToLspDiags`).
The editor index (doc.sema) is still refreshed for nav/refs/completion/
tokens. On-save/on-open diagnostics still come solely from the canonical
compiler pipeline in `runProjectCheck` (unchanged).
- Document sema as an editor-indexing API (doc.sema field comment).
Intended behavior change: as-you-type sema diagnostics no longer publish;
on-save canonical diagnostics are the sole source. CLI compile output and
the 361-example suite are unchanged (361/0, zero snapshot churn).
Move the final inline emitInst handler groups (terminators, box/unbox-Any,
reflection, switch-branch, closure-creation, vector, block-param, misc) into
the Ops facade in src/backend/llvm/ops.zig. emitInst is now pure dispatch:
every arm delegates to self.ops().*, leaving only setInstDebugLocation plus
one-line delegations.
Widen the shared infra the moved bodies reach (emitFailableMainRet, getBlock,
anyTag, isSignedTypeEx, coerceToI64/coerceToI64Signed/coerceFromI64,
emitFieldValueGet) to pub on LLVMEmitter; helper and ref-tracking sections
stay put. Pure relocation: emitted LLVM IR byte-identical, zero snapshot churn.
Relocate the struct, enum, union, array/slice, tuple, and optional
opcode handler bodies out of emitInst into the existing Ops facade.
Each moved arm now delegates via self.ops().emit<Op>(...); shared infra
stays on LLVMEmitter, with resolveAggregate/resolveGepStructType widened
to pub as the GEP handlers require. Pure relocation, behavior-preserving:
zero snapshot churn (361/0).
Relocate the Calls (objc_msg_send / jni_msg_send / call / call_indirect)
and Call-extensions (call_builtin / compiler_call / call_closure) emitInst
handler groups out of emit_llvm.zig into the existing Ops facade. Each
emitInst arm now delegates via self.ops().emit<Op>(...). Behavior-preserving
pure relocation; emitted LLVM IR is byte-identical (361/0 examples, no
snapshot churn).
Shared call infra stays on LLVMEmitter, widened pub only as the moved
bodies require: extractSlicePtr, loadJniFn, getObjcMsgSendValue, the math
F32/F64 declarators + types, getOrDeclareWrite/getWriteType, ffiCtors,
materializeByvalArg, emitCStringGlobal, emitJniConstructor, and the Jni
slot-offset constants. emitJniConstructor remains in emit_llvm.zig (A7.3
decision); the moved jni arm calls it via self.e.emitJniConstructor(...).
Relocate the `// ── Memory ──`, `// ── Globals ──`, `// ── Conversions ──`,
and `// ── Pointer ops ──` opcode handler bodies out of `emitInst` in
src/ir/emit_llvm.zig into the existing `Ops` facade in
src/backend/llvm/ops.zig. Each `emitInst` arm now delegates via
`self.ops().emit<Op>(...)`. Widen `emitConversion`, `coerceArg`, and
`getRefIRType` to `pub` (the only helpers the moved bodies call).
Pure relocation: zero snapshot churn.
Move the Constants/Arithmetic/Bitwise/Comparisons/Logical opcode handler
bodies out of emitInst into a new Ops facade in src/backend/llvm/ops.zig.
emitInst's scalar arms now delegate via self.ops().*; the shared infra they
call (mapRef/resolveRef/matchBinOpTypes/emitCmp/emitCmpOrdered/emitStrCmp/
emitStringConstant/reflection + isFloatOrVecFloat/isSignedType) stays on
LLVMEmitter, widened to pub as needed. Pure relocation: zero snapshot churn.
Move getOrCreateJniSlots (the cls/methodid slot-cache builder) out of
emit_llvm.zig into the FfiCtors backend *LLVMEmitter facade. Behavior-preserving
— self.* -> self.e.* only.
- FfiCtors gains getOrCreateJniSlots (pub). The jni_slots cache + mangleJniKey
stay on LLVMEmitter; mangleJniKey is widened to pub (the facade calls it back,
like lazyDeclareCRuntime/emitPrivateCString), and JniSlotPair is widened to pub
(the facade returns it; the call site consumes it). 1 call site routed via
ffiCtors().
- emitJniConstructor intentionally NOT moved in this slice: it is emission-heavy
(resolveRef/mapRef/coerceArg/getRefIRType/extractSlicePtr/loadJniFn/
emitCStringGlobal — 100+ internal callers for the first two), so relocating it
would pub-expose the emitter's core value-emission machinery. Consistent with
A7.2 keeping emitFieldValueGet in emit_llvm.zig. Pending an explicit decision.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(JNI anchors 1402/1408/1418/1425 green, no churn).
Move the DWARF debug-info emission out of emit_llvm.zig into a DebugInfo backend
*LLVMEmitter facade (field `e`). Behavior-preserving relocation — self.* ->
self.e.* only.
- src/backend/llvm/debug.zig (DebugInfo): debugEnabled + diFileFor (private) +
initDebugInfo / beginFunctionDebug / endFunctionDebug / setInstDebugLocation /
finalizeDebugInfo (pub). The mutable DI state (di_builder/di_cu/di_files/
di_scope/current_func_file) + the shared source map (import_sources/main_file)
stay on LLVMEmitter; the facade reads/writes them via self.e.*.
- Routed the 5 pass-order call sites in LLVMEmitter.emit (init/finalize/
begin/end/setInstDebugLocation) through a new debugInfo() accessor.
- setDebugContext stays on LLVMEmitter (shared-state setter; callers in main.zig/
core.zig/test). sourceForFile stays on LLVMEmitter and is widened to pub — it is
shared with reflection's trace-frame emission (emitTraceFrame), not debug-only.
- No DI logic / module-flag / DWARF-version / scope-line change.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0 (no churn).
Move the LLVM type-mapping and C-ABI coercion helpers out of emit_llvm.zig into
the first src/backend/llvm/ modules. Behavior-preserving relocation — the only
rewrites are module plumbing and self.* -> self.e.* facade access.
- src/backend/llvm/types.zig (TypeLowering): toLLVMType + toLLVMTypeInfo.
- src/backend/llvm/abi.zig (AbiLowering): abiCoerceParamType / abiCoerceParamTypeEx
/ needsByval / materializeByvalArg.
- Both are backend *LLVMEmitter facades (field `e`) — the backend analogue of the
IR-side *Lowering facades, NOT a *Lowering facade. They reach the cached LLVM
handles, IR type table, module data layout, builder, and the memoizing
composite-type getters via self.e.*.
- LLVMEmitter stays the facade: toLLVMType (~97 callers) + abiCoerceParamType /
abiCoerceParamTypeEx / needsByval / materializeByvalArg kept as thin wrappers
delegating through new typeLowering()/abiLowering() accessors. Zero caller
churn. toLLVMTypeInfo deleted (sole caller moved).
- Widened getStringStructType / getAnyStructType / getClosureStructType to pub
(the moved toLLVMTypeInfo calls them back; their memoization stays on
LLVMEmitter). verifySizes stays in emit_llvm.zig (size-assertion pass, not type/
ABI lowering). No ABI/type logic, branch order, diagnostic text, or snapshot
changed. Circular import (emit_llvm <-> backend/llvm) resolves via the pointer
facade.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(1202 .ir + the 2 ABI unit tests unchanged, no churn).
Test-first scaffolding for LLVM backend modularization (Phase A7.1) before the
type/ABI helpers move into src/backend/llvm/{types,abi}.zig. Visibility-only
change to the targets — no behavior change. Closes the ARCH-SAFETY "no generic
ABI snapshot" gap.
- 2 new emit_llvm.test.zig tests:
- abiCoerceParamType across every C-ABI size bucket: <=8 -> i64, 9-16 ->
[2 x i64], >16 -> ptr, HFA (all-float/all-double, <=4 fields) -> unchanged,
string -> ptr, slice -> ptr, scalar -> unchanged. Built via a local
internStruct helper (field slice in the module arena -> no testing-allocator
leak); asserts against emitter.cached_* + LLVMArrayType2.
- needsByval: true only for >16-byte non-HFA struct; false for <=16 / HFA /
string / slice / non-struct.
- 1 new .ir snapshot: 1202-ffi-cc-c-large-aggregate (the canonical callconv(.c)
>16-byte byval example that directly documents abiCoerceParamType) — pins the
byval param path end-to-end (5 byval + entry reload + 2 sret from Arena.init).
Path-free + idempotent (verified across two captures). Suite count unchanged
(snapshot added to an existing example).
- Widened abiCoerceParamType + needsByval to pub (visibility only;
abiCoerceParamTypeEx/materializeByvalArg/verifySizes stay private — move with
callers in sub-step 2). No logic touched.
- Recorded the A7.1 coverage inventory + residual gaps (wasm32 usize->i32 branch,
fn-ptr large-aggregate 1203/1204) in ARCH-SAFETY.md.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0 (no churn
beyond the new 1202 .ir).
Relocate the two pure JNI decision helpers out of lower.zig into
jni_descriptor.zig (already the JNI helper module), alongside the descriptor
derivation. Behavior-preserving move — no facade, since neither takes *Lowering.
- jniMangleNativeName(allocator, foreign_path, method_name) and
isJniReturnTypeSupported(table, ret_ty) moved verbatim as pub free fns; added a
types import + TypeId alias to jni_descriptor.zig.
- Rerouted lower.zig's 2 call sites (synthesizeJniMainStub; the JNI return-type
guard at lower.zig:6000) through jni_descriptor.* — lower.zig already imported
the module.
- Moved the 2 unit tests lower.test.zig -> jni_descriptor.test.zig (re-pointed to
desc.*; a standalone TypeTable.init replaces the Module setup). Dropped the
now-unused lower_mod alias.
- Stayed in lower.zig per PLAN A6.2 step 5/6: jniMapParamType (trivial resolveType
wrapper), synthesizeJniMainStub(s), lowerJniCall, lowerJniConstructor,
lowerSuperCall, getJniEnvTlFids. Java rendering stays in jni_java_emit.zig.
Phase A6 complete.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(9 JNI .ir snapshots + 26 14xx examples green, no churn).
Test-first scaffolding for the JNI FFI domain (Phase A6.2) before the pure
helpers move out of lower.zig. Visibility-only change — no behavior change.
- 2 new lower.test.zig tests for the pure JNI helpers lacking unit coverage:
- jniMangleNativeName: `/`->`_` separator, `_`->`_1` escape (path AND method),
`Java_` prefix, `_sx_1` infix (2 cases lock all rules).
- isJniReturnTypeSupported: void/bool/s32/s64/f32/f64 + pointer/many-pointer
-> true; other widths (s8/s16/u8/u32/u64) + by-value struct -> false.
- JNI descriptor derivation (writeType/deriveMethod) is already extracted into
jni_descriptor.zig (15 tests) — not part of A6.2.
- Widened jniMangleNativeName -> pub (file-scope free fn; isJniReturnTypeSupported
already pub). Reached from the test via ir_mod.lower.*. No logic touched.
- Recorded the A6.2 coverage inventory + residual emission-bound gaps
(synthesizeJniMainStub*/lowerJniCall/lowerJniConstructor/lowerSuperCall/
getJniEnvTlFids stay in lower.zig; jniMapParamType is a trivial resolveType
wrapper) in ARCH-SAFETY.md.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(no .ir churn; 9 JNI .ir snapshots green).
Move the pure Obj-C decision helpers out of lower.zig into src/ir/ffi_objc.zig
behind an ObjcLowering *Lowering facade (Principle 5, like the A4/A5 resolvers).
Behavior-preserving relocation — the only non-self.l rewrites are facade
plumbing.
Moved verbatim (self. -> self.l. for Lowering members):
- deriveObjcSelector (selector derivation)
- objcTypeEncodingFromSignature + appendObjcEncoding + bailObjcEncoding +
the ObjcEncodingStack type
- objcPropertyKind + the ObjcPropertyKind enum
- isObjcClassPointer
- objcDefinedStateStructType + objcStateAllocatorType
Emission-heavy code stays in lower.zig per PLAN A6.1 step 6: emitObjc* IMP
builders, lowerObjc*Call, registerObjc*, declareObjc*, the lookupObjc* property/
state lookups, and the Self-substitution resolvers.
- Call sites rerouted through a new objc() accessor: 15 in lower.zig, 1 in
expr_typer.zig, 39 in lower.test.zig (the A6.1 scaffolding tests now drive the
facade). No Lowering wrappers kept. Barrel-wired ffi_objc + ObjcLowering.
- No new visibility widening beyond sub-step 1's two pubs — the facade reads
self.l.{alloc,module,program_index,diagnostics} (fields) + the already-pub
resolveType. lower.zig -478 (->16615); ffi_objc.zig 428.
- Doc-only re-home: the property-IMP getter/setter comment was attached (a
pre-existing artifact) to the moving ObjcPropertyKind enum, two decls away from
its real subject emitObjcDefinedClassPropertyImps (which had no doc). Re-homed
it there so the move neither orphans a `///` block (Zig errors on a dangling doc
comment) nor misattributes it to ensureArcRuntimeDecls.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(48 13xx Obj-C examples + 4 Obj-C .ir snapshots green, no churn).
Codex review of 0012228 noted isObjcClassPointer's contract is
`fcd.runtime == .objc_class or fcd.runtime == .objc_protocol`, but the new tests
only exercised the class case. Test-only fix (no visibility/behavior change —
still exactly the two pub widenings from the parent commit):
- isObjcClassPointer: add a *NSCopying case where NSCopying is a registered
.objc_protocol foreign class -> true (alongside the .objc_class *NSString case).
- objcPropertyKind: add a *NSCoding protocol-pointer field -> strong default
assertion, since it uses the same class/protocol object-pointer predicate.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0.
Move the diagnostic-only Pass 1e (ERR E1.7 cleanup-absorption + E1.8 value-slot
liveness) out of lower.zig into src/ir/error_flow.zig behind an ErrorFlow
*Lowering facade (Principle 5, like ErrorAnalysis/CoercionResolver). Behavior
preserved exactly — pure relocation.
Moved verbatim (self. -> self.l. for Lowering members; sibling calls stay on the
facade; provenHas is a file-local free fn): checkErrorFlow, analyzeFnBody,
flowWalk, flowStmt, flowIf, flowMatch, flowExpr, applyRefinement,
provenAdd/provenClone/provenIntersect, registerFailableDestructure,
checkCleanupBody/checkCleanupNode/cleanupReject, plus the FlowCtx/ProvenSet types.
- lowerRoot routes the single call site through
self.errorFlow().checkErrorFlow(decls); no Lowering wrapper kept (only the
pipeline calls it, no unit-test caller). New errorFlow() accessor.
- The pass takes AST decls + ProgramIndex + diagnostics only — independent of IR
Builder state (PLAN-ARCH A5.2 success criterion).
- New pub: exprIsFailable (only widening; inferExprType/errorChannelOf already
pub). lower.zig -389 (->17030); error_flow.zig 407. Barrel-wired in ir.zig.
- No .test.zig: diagnostic-pass altitude (functions return only bool + emit
diagnostics) — guarded by example anchors 1046-1053 (incl. scaffolding
1051/1052/1053). Phase A5 complete.
Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(anchors 1046-1053 all ok, no .ir churn).
A closure literal declared inside a `defer` body segfaulted the compiler.
Root cause: lowerLambda never opened its own `func_defer_base` window. Every
other function-lowering entry (lowerFunction / monomorphizeFunction /
monomorphizePackFn) saves func_defer_base, sets it to defer_stack.items.len, and
restores it — lowerLambda didn't. So a lambda's `return` drained the ENCLOSING
function's defers; when the defer body itself declared the lambda, draining
re-lowered the lambda, which returned, which drained again → infinite recursion
→ stack-overflow SIGSEGV (the failable variant surfaced one frame out, in
expandCallDefaults→lookupFn reading a clobbered scope).
Fix: lowerLambda now saves func_defer_base + the defer_stack length, sets the
base to the current length (a fresh window), and restores both on exit — so a
lambda's `return` drains only its own defers.
Regression: examples/0310-closures-closure-literal-in-defer.sx — a closure
declared and called inside a `defer`; verifies `body` then `defer closure: 42`
at scope exit (exit 0). Issue 0073 marked RESOLVED; repro promoted from
issues/0073-*.sx.
zig build, zig build test, tests/run_examples.sh (358/0) all green.
Error-set convergence now lives in src/ir/error_analysis.zig behind a *Lowering
facade (ErrorAnalysis), mirroring the other domain extractions. Moved verbatim:
- convergeInferredErrorSets (whole-program inferred-`!` SCC fix-point),
- convergeClosureShapeSets,
- collectErrorSites / collectClosureShapes (the AST collectors).
Added ErrorFacts (the PLAN-ARCH shape: inferred_error_sets + shape_inferred_sets)
+ a facts() view over the maps, which stay on Lowering for now (consumers read
them via self.*). recordClosureShape and its deep type/shape helper web stay in
Lowering; it reaches the moved collectErrorSites via self.errorAnalysis().
Lowering keeps convergeInferredErrorSets / convergeClosureShapeSets as thin pub
wrappers (the lowering pipeline + the E1.4b unit test call them); collectErrorSites
/ collectClosureShapes are deleted (no fallback). New pub: isErrorTagLiteralNode /
callTargetName / astIsPureBareInferred / astPureNamedSet / containsTag /
namedSetTags / recordClosureShape (the moved collectors / facade reach them).
lower.zig net -216 lines.
The 2 convergence unit tests (transitive SCC across a try edge; closure-shape
union) moved from lower.test.zig to error_analysis.test.zig and now drive the
facade directly; the E1.4b test stays in lower.test.zig via the wrapper. Module
named error_analysis.zig, NOT errors.zig (src/errors.zig is the DiagnosticList).
zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir churn.
Test-first scaffolding ahead of extracting src/ir/error_analysis.zig — no code
change to the convergence targets (convergeInferredErrorSets /
convergeClosureShapeSets / collectErrorSites / collectClosureShapes).
Adds 2 unit tests via the already-pub convergence functions (no new exposure):
- convergeInferredErrorSets transitive/SCC: a `caller :: () -> ! { try raiser(); }`
with no direct raise converges to raiser's {Foo} across the try edge — the
whole-program fixpoint A5.1 must preserve. (Today's E1.4b test only covered a
direct raiser + the empty-set warning.)
- convergeClosureShapeSets: a bare-`!` closure literal `() -> ! { raise error.Bar }`
inside a host fn unions {Bar} into one shape_inferred_sets entry.
Adds 2 .ir snapshots (first .ir for these error forms), vetted clean
(idempotent, path-free, no #run): 1006-errors-inferred-error-sets (inferred-set
error-channel shapes) and 1009-errors-catch (catch lowering). 1004-errors-try
was already pinned.
PLAN-ERR is complete/idle, so the A5 overlap risk is low (the target functions
are stable, not in-flight). The sub-step-2 module will be named
src/ir/error_analysis.zig, NOT errors.zig (src/errors.zig is the DiagnosticList).
zig build, zig build test, tests/run_examples.sh (357/0) all green.
Coercion classification now lives in src/ir/conversions.zig behind a *Lowering
facade (CoercionResolver), mirroring CallResolver / GenericResolver /
ProtocolResolver. Two pure classifiers:
- classify(src, dst) -> CoercionPlan (15 kinds: no_op / unbox_any / box_any /
closure_to_fn_reject / tuple_elementwise / optional_unwrap / void_to_optional /
optional_wrap / erase_protocol / int_to_float / float_to_int / ptr_int_bitcast /
widen / narrow / none) — the built-in coercion ladder.
- classifyXX(src, dst) -> XXPlan (unbox_any / no_op / erase_protocol /
protocol_to_pointer / coerce) — the xx-operator head.
coerceToType and lowerXX now `switch (classify…)` then emit; branch order
mirrors the originals exactly and every arm reproduces the prior lowering — the
f32/f64 Any match dispatch, buildProtocolErasure (lowerXX) vs buildProtocolValue
(coerceToType), tuple/optional recursion, and the user-Into fallback + pointer
materialization + recursion-guard/diagnostics (which stay in lowerXX /
tryUserConversion). IR emission stays entirely in Lowering; the classifiers are
pure. lowerXX keeps the operand's lowered Ref type as src_ty. `.none` means no
built-in applies (pass through; the Into fallback runs) — no silent default.
New pub: isFloat / isIntEx / typeBitsEx / resolveConcreteTypeName (the classifier
reads them); coercionResolver() accessor. lower.zig net -54 lines.
conversions.test.zig drives CoercionResolver directly: the full classify ladder
(no-op, Any box/unbox, widen/narrow, int<->float, ptr<->int, optional
wrap/unwrap, void->optional, tuple, closure-reject, .none for two unrelated
structs), erase_protocol for a concrete source, and classifyXX (all 5 kinds incl.
protocol-to-pointer vs coerce and pointer-materialization -> coerce).
zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir churn.
Test-first scaffolding ahead of extracting src/ir/conversions.zig — no code
change to the coercion targets (lowerXX / coerceToType / coerceOrErase /
buildProtocolErasure / tryUserConversion / failable-adapter selection).
Adds 4 .ir snapshots (first .ir for 01xx/09xx/10xx), each captured surgically
via `sx ir | normalize_ir`, path-free, idempotent, and print-free at IR-gen time
(0114-types-build-block-convert was rejected — it prints `--- void / 0 args ---`
+ sx source at IR-gen):
- 0107-types-int-cmp-in-float-ternary numeric int<->float coercion
- 0903-optionals-optional-roundtrip optional wrap/unwrap
- 0904-optionals-any-to-string-optional xx unbox_any + optional
- 1004-errors-try error-channel adapter/coercion
Protocol erasure + user Into are already pinned by the 04xx snapshots
(0400/0413/0414/0416); duplicate-conversion rejection by the 0410/0411/0412
anchors.
Adds 1 unit test via the public surface (no new exposure, mirroring A4.1/A4.2
sub-step 1): optionalOfFlattened — the optional wrap/flatten coercion rule
(T -> ?T; ?T -> ?T, never ??T; contrasted with the non-flattening optionalOf).
The lowerXX/coerceToType/coerceOrErase/buildProtocolErasure decisions are private
+ emission-bound, so their CoercionPlan unit tests land with the extracted module
in sub-step 2.
zig build, zig build test, tests/run_examples.sh (357/0) all green.
Factor the lookup/planning half of the protocol emission functions into
protocols.zig, keeping IR emission in Lowering (PLAN-ARCH A4.2 final increment):
- protocolMethodInfos(proto) — the dispatch method table = which methods
getOrCreateThunks must thunk. getOrCreateThunks now does PLANNING via this +
EMISSION (createProtocolThunk loop) in Lowering.
- findVisibleImpls(entries, out) — moved verbatim (pure BFS over the import
graph; the cross-module visibility selection behind the 0410 path).
tryUserConversion calls it via the resolver.
- matchPackImpl(src_ty, pack_key) -> ?PackImplMatch — the pure pack-impl
matching loop (prefix + return match) + convert-method find, returning the
matched entry + convert fd + src params/ret. tryPackImplMatch consumes it; the
binding + monomorphise + call emission stays in Lowering.
Emission untouched: createProtocolThunk, buildProtocolValue, and the
monomorphise+call tails of tryUserConversion / tryPackImplMatch remain in
Lowering. The reentrancy guard, key-build, and the Into no-visible / duplicate /
recursive diagnostics stay in tryUserConversion (byte-for-byte). lower.zig net
-94 lines. No new pub exposure (uses the existing ParamImplEntry /
PackParamImplEntry / formatTypeName surface).
protocols.test.zig +3: protocolMethodInfos (method table + null-for-unknown, no
silent empty default); findVisibleImpls (falls open with no graph; filters to
here + transitive imports); matchPackImpl (selects on prefix+return; null for
non-closure source / unknown key).
zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir
churn; the 0410/0411/0412 diagnostics are byte-for-byte preserved.
Move the registration functions behind the protocols.zig facade, per PLAN-ARCH
A4.2 ("then registration", keeping IR emission in Lowering):
- registerProtocolDecl (protocol struct + dispatch method table + vtable type),
- registerImplBlock (concrete impl -> <Target>.<method> in fn_ast_map + default-
method synthesis),
- registerParamImpl (parameterised impl -> param_impl_map / param_impl_pack_map
+ the same-file duplicate diagnostic),
- synthesizeDefaultMethod (facade-private; its only caller moved too).
Moved verbatim with self. -> self.l. facade rewrites. Emission stays in
Lowering: the registry calls self.l.declareFunction (the extern-stub primitive)
but the thunk/value builders (createProtocolThunk / buildProtocolValue /
tryUserConversion / getOrCreateThunks) are NOT moved.
Lowering keeps registerProtocolDecl as a thin pub wrapper (scan pass + 7
unit-test callers); registerImplBlock / registerParamImpl /
synthesizeDefaultMethod deleted (no fallback), the 2 scan call sites routed
through protocolResolver(). New pub: declareFunction (8 callers, emission infra),
ParamImplEntry / PackParamImplEntry (the registry constructs them; stay as
Lowering nested types). State maps remain on Lowering; the facade reads/writes
self.l.* (migrate once planning lands).
protocols.test.zig +2: registerImplBlock records Circle.draw in fn_ast_map (and
packArgConformsTo then sees it); registerParamImpl flags a same-file duplicate
impl Into(s64) for IntCell (the 0412-class, unit level).
zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir
churn; the 0410/0411/0412 rejection diagnostics are byte-for-byte preserved.
Move the pure protocol/impl conformance lookups into one module,
src/ir/protocols.zig, behind a *Lowering facade (ProtocolResolver), mirroring
GenericResolver / CallResolver. Per PLAN-ARCH A4.2 ("move pure lookup first;
keep emission in Lowering"), this increment moves only the read-only queries:
- getProtocolInfo (is a type a registered protocol + its method table),
- hasImplPlain (have the (protocol, type) thunks been materialized),
- packArgConformsTo (impl-declaration-level conformance for ..xs: P).
Registration (registerProtocolDecl / registerImplBlock / registerParamImpl) and
all IR emission (createProtocolThunk / buildProtocolValue / tryUserConversion /
getOrCreateThunks) stay in Lowering for the later increments. The state maps
(protocol_thunk_map / param_impl_map on Lowering, protocol_decl_map /
protocol_ast_map in ProgramIndex) stay put; the facade reads them via self.l.* —
no map migration.
Lowering keeps getProtocolInfo as a thin pub wrapper (~9 callers incl.
calls.zig); hasImplPlain + packArgConformsTo are deleted (no fallback), their 3
call sites (computeHasImpl x2, the pack-conformance check x1) routed through
self.protocolResolver(). formatTypeName widened to pub (the lookups use it);
protocolResolver() accessor added.
protocols.test.zig (wired into the barrel) drives ProtocolResolver directly:
getProtocolInfo (registered vs builtin/plain-struct + wrapper delegation),
hasImplPlain (thunk-map materialization), packArgConformsTo (non-parameterised
requires <ty>.<m> in fn_ast_map; trivially-true for an erased protocol value;
false for unknown protocol).
zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir
snapshot churn; the 0410/0411/0412 rejection anchors still pass.
Adds the one deferred A4.1 coverage item: a focused unit test for
GenericResolver.buildTypeBindings inferring a type param from value args
(strategy 2) with widest-match — add(1,2) => T=s64, and add(1.0,2) / add(1,2.0)
=> T=f64 regardless of argument order.
Previously this inference path was guarded only by the 0200 .ir snapshot; the
unit test pins it directly against the new generics.zig API. Test-only.
zig build test and tests/run_examples.sh (357/0) green.