Commit Graph

30 Commits

Author SHA1 Message Date
agra
ea35a05b26 fix(lower): bare-call resolver binds same-name flat authors per source [0102c]
Third of four fix-0102 sub-steps — the behaviour fix for NORMAL call sites.
Adds THE bare-name resolver `resolveBareCallee(name, caller_file)` over
fix-0102a's `module_fns` + `flat_import_graph` and routes the primary call
path through it:

- own-author wins: a file's bare call to a name IT authors binds its OWN
  author, not the first-wins merge winner. (When the winner already is the
  caller's own — every single-author and first-importer case — the resolver
  returns `.none` so the existing path binds it byte-for-byte.)
- a bare call to a name two or more FLAT imports both provide is `.ambiguous`
  and rejected with a loud diagnostic ("declared by multiple imported
  modules — qualify the call"); a namespaced author never collides.
- a single flat-reachable author that differs from the winner binds that
  author; otherwise `.none`.

The resolved shadow author lowers into its OWN FuncId via fix-0102b's
identity-addressable `lowerFunctionBodyInto` (shared `bareAuthorFuncId`
helper, also used by `lowerRetainedSameNameAuthors`). Only plain free
functions route — generic / comptime / foreign / builtin authors and any
scope-mangled / UFCS-aliased / locally-shadowed name fall straight to the
existing dispatch, so single-author / local / std / qualified resolution is
unchanged (full example suite stays green, including bundle.sx and the
comptime format/pack examples).

Examples 0722 (flat file per-source bind), 0723 (flat vs namespaced, no false
ambiguity), 0724 (ambiguous → diagnostic), 0725 (flat directory per-source
bind), 0727 (user namespace literally named __m0). Each fails on
wt-fix-0102-base (first-wins mis-bind / no diagnostic) and passes here. The
fix-0102b unit test now calls a per-module wrapper (main can't bare-call the
2-author name) and asserts the resolver's three variants directly.

Gate: zig build, zig build test (400/400), bash tests/run_examples.sh
(462 passed) all green.
2026-06-06 14:04:03 +03:00
agra
bb1ed7294b fix(lower): key fn_decl_fids by stable AST pointer in scanDecls [0102b F1]
scanDecls declared a bare `.fn_decl` via `declareFunction(&fd, ...)`,
where `fd` is the switch-capture COPY of `decl.data.fn_decl`. Its
address is a per-iteration stack temporary, so the winner author's
`fn_decl_fids` entry was keyed by an address no later decl-identity
lookup can reproduce — `fn_ast_map` and `module_fns` carry the stable
`&decl.data.fn_decl`, so a lookup by that pointer missed the winner's
FuncId. fix-0102c routes calls through exactly these stable pointers,
so the key has to match.

Record the entry under `&decl.data.fn_decl` (the persistent AST node
field) to match `fn_ast_map`/`module_fns`. The other declareFunction
sites already pass stable pointers (const_decl field, module_fns entry,
fn_ast_map entry, struct-method node field, heap-synthesized objc decl);
`lowered_fids` keys by FuncId value, so neither has the temporary-address
mistake.

Strengthen the fix-0102b regression test: assert the identity map
round-trips by the STABLE pointer for BOTH same-name authors — the
winner's `fn_ast_map` pointer resolves to the first-wins FuncId, and the
shadow's `module_fns` pointer resolves to a distinct FuncId. This
assertion fails on the pre-fix code (winner keyed by `&fd` → null) and
passes after. Call resolution unchanged (name path still default).

Gate (this worktree): zig build, zig build test (400/400),
bash tests/run_examples.sh (457 passed) all green.
2026-06-06 13:22:11 +03:00
agra
237f794585 feat(lower): identity-addressable function body lowering [0102b]
Second of four fix-0102 sub-steps. Makes function declaration + body
lowering addressable by decl/FuncId IDENTITY instead of name-first-wins,
so two same-name authors can each carry their OWN body in their OWN
FuncId. Purely additive: the existing name path stays the sole resolver,
so the suite is byte-for-byte unchanged (no call rerouting — that is
0102c).

- declareFunction records `*const FnDecl -> FuncId` in a new identity map
  (`fn_decl_fids`), alongside the existing name-keyed function table.
- Extract the body-lowering tail of lazyLowerFunction into a reusable
  `lowerFunctionBodyInto(fd, fid, name)` that promotes a SPECIFIC extern
  stub into a real body by EXPLICIT FuncId — not by name lookup (which
  returns the first author). The shared save/restore preamble becomes a
  `FnBodyReentry` guard struct, used by both lazyLowerFunction's found
  path and the null-FuncId `ns.fn` alias path; issue-0100 F1/F2 behaviour
  (own-import source context, block_terminated transparency) is preserved.
- Add `lowerRetainedSameNameAuthors`: walks fix-0102a's `module_fns`,
  and for each SHADOWED flat author (a same-name author that is not the
  fn_ast_map winner, in a direct flat import of the main file) declares a
  fresh same-name FuncId + lowers its body in its own module's visibility
  context. FuncId-keyed `lowered_fids` tracks which slots already have a
  body. Not invoked during a default compile (the name path stays the
  default); 0102c wires it into bare-flat-call routing.
- lower.test.zig: regression that compiles two flat-imported modules each
  authoring `greet` and asserts ONE real body before the pass (winner
  only; shadow dropped) and TWO distinct non-extern bodies after — the
  shadowed author is no longer dropped/extern.

Gate (this worktree): zig build, zig build test (400/400),
bash tests/run_examples.sh (457 passed) all green.
2026-06-06 13:02:49 +03:00
agra
ed7665f8ae fix(ir): single-assign field store delegates to fieldLvaluePtr, completing the lvalue consolidation [F0.10]
Migrate lowerAssignment's `.field_access` target onto the shared
`fieldLvaluePtr` resolver, deleting its duplicated union / promoted /
tuple / vector / struct walk. All three lvalue field-store sites —
single-assign, address-of (lowerExprAsPtr), and multi-assign
(lowerMultiAssign) — now resolve through the one resolver, removing the
issue-0083 two-resolver divergence.

Fold vector-lane resolution into `fieldLvaluePtr` (reusing
vectorLaneIndex) so the single resolver covers struct fields, union
direct fields, promoted anonymous-struct union members, tuple elements,
and vector lanes — null only on a genuine miss, which every caller turns
into the read path's `emitFieldError` diagnostic.

`fieldLvaluePtr` now types every field GEP `*field_ty` (the convention
the single-assign path always used), not the bare field value type:
emitStore unwraps one pointer level to find the stored value's type.
The earlier lowerExprAsPtr / lowerMultiAssign walks typed the GEP with
the bare field type, so a field whose own type is a pointer-to-aggregate
(`*Pair`, a two-pointer struct) made emitStore unwrap to the aggregate
and coerceArg's closure auto-promotion store a 16-byte `{ptr,null}`
struct over the 8-byte slot, clobbering the neighbouring field.
Consolidating onto the one `*field_ty` resolver preserves single-assign
and fixes that pre-existing multi-assign / address-of clobber.

The types.zig `.unresolved` tripwire is untouched; no `.s64` / `.void` /
`.unresolved` default remains.

Regression: examples/0167-types-ptr-to-aggregate-field-store.sx (a
`*Pair` field stored via all three lvalue sites leaves the neighbour
intact) + a lowering unit test asserting the `*field_ty` GEP convention.
2026-06-05 14:40:06 +03:00
agra
c98bebc4e3 fix(ir): missing-field multi-assign + promoted-union-member lvalue [F0.10]
Completes the issue-0094 fix. attempt-1 made single-assign and address-of
diagnose a missing struct field; the stress-review found two remaining defects
in that change:

1. lowerMultiAssign's `.field_access` target kept the pre-fix shape — a
   struct-only loop that defaulted `field_idx 0` / `field_ty .unresolved` on a
   miss, then built the GEP and stored unconditionally. A missing field
   (`p.q, y = 2, 3`) silently wrote field 0 (printed `x=2 y=3`, no diagnostic),
   and a valid promoted-union / tuple member at a non-zero offset corrupted
   field 0 instead of its own slot.

2. attempt-1's new union branch in lowerExprAsPtr resolved only DIRECT union
   field names, so `@v.x` on a promoted anonymous-struct member reported
   "field 'x' not found on type 'Vec2'" even though `v.x = 41` worked.

Both lvalue-pointer sites and the multi-assign store now route through one
shared resolver, `fieldLvaluePtr`, that handles struct fields, union direct
fields, promoted anonymous-struct union members, and tuple elements, and
returns null (no field-0 / `.unresolved` default) on a genuine miss. Each
caller emits the read path's `emitFieldError` on null. This collapses the
three previously-divergent field-lvalue walks into one, fixing the
multi-assign missing-field corruption, the promoted-member over-rejection,
and (as a side effect of correct resolution) non-zero-offset promoted-union
and tuple multi-assign stores. The types.zig tripwire is untouched.

Regression tests:
- examples/1145 extended: multi-assign missing field (`p.r, y`) errors, exit 1.
- examples/0166 (new): promoted union member written and address-of'd,
  including a non-zero-offset member (`@v.y`), compiles and runs.
- src/ir/lower.test.zig: multi-assign missing-field field-not-found unit test.
2026-06-05 14:00:24 +03:00
agra
e13518e8aa fix(ir): missing struct field assignment errors cleanly, no LLVM panic [F0.10]
Assigning to a nonexistent struct field (`p.q = 2` where Point has no `q`)
aborted the compiler with the `.unresolved` LLVM tripwire instead of a source
diagnostic (issue 0094). The lvalue field lookup never diagnosed a miss:

- `lowerAssignment`'s `.field_access` target left `field_ty = .unresolved` when
  no struct field matched, then built `ptrTo(field_ty)` and stored — so a
  pointer-to-`.unresolved` reached LLVM emission and tripped the panic.
- `lowerExprAsPtr`'s `.field_access` fallback returned
  `structGepTyped(obj_ptr, 0, .s64, obj_ty)` on a miss — a silent field-0/`.s64`
  default that mislowered the lvalue.

Both sites now reuse the read path's `emitFieldError` (the exact facility
`lowerFieldAccessOnType` uses), so read and write reject identically with
`field 'q' not found on type 'Point'`. `lowerExprAsPtr` also resolves
union/tagged-union fields via `union_gep` (the old `.s64` fallback was silently
standing in for union field access — e.g. `u.a[0] = v`), so that path is fixed,
not just made loud. The `types.zig` tripwire is untouched: the fix is to never
produce `.unresolved` for a missing-field store.

Regression tests:
- examples/1145-diagnostics-missing-struct-field-assign.sx — negative, both
  sites error, exit 1.
- examples/0165-types-nested-struct-field-assign.sx — positive, nested struct
  field write + address-of a matched field still work.
- src/ir/lower.test.zig — lowering unit test asserting the field-not-found
  diagnostic for a missing-field assignment.
2026-06-05 13:24:15 +03:00
agra
b053c64149 fix(ir): reject non-type args to the 7 type-introspection builtins [F0.8]
size_of, align_of, field_count, type_name, type_eq, type_is_unsigned,
and is_flags silently reinterpreted a value argument as a type:
type_is_unsigned(6) read 6 as a TypeId index (types[6] = u8 -> true),
size_of(6)/size_of(true) sized its typeof (8), type_name(6) returned
types[6]'s name. Per Agra's ruling, all 7 now strictly require a type
(compile-time): a value argument is a compile error.

One shared guard (Lowering.reflectionTypeArgGuard, run at the top of
tryLowerReflectionCall) classifies each arg via reflectionArgIsType: a
spelled / compile-time type or generic type parameter (isStaticTypeArg),
or a runtime Type value (static type .any -- type_of(x), a []Type
element list[i], a Type-typed local/field/param) is accepted; anything
else is rejected with "<builtin> expects a type, got '<type>'". The
runtime path for type_name / type_is_unsigned is preserved (the {}
formatter calls type_is_unsigned(type_of(val)) at runtime). The 5
comptime-only builtins stay comptime-only (runtime reflection deferred).

Regression: examples/1144-diagnostics-reflection-builtin-needs-type.sx
(reject cases across all 7, exit 1). Unit test: reflectionArgIsType in
lower.test.zig. specs.md / readme.md document the strict type
requirement (and add the previously-undocumented align_of, type_eq,
type_is_unsigned). issues/0090 RESOLVED banner updated.
2026-06-05 11:22:59 +03:00
agra
a7ee179577 fix(ir): vector lane store resolves lane element type [F0.5]
Writing a Vector lane (`v.x = …`, `.y/.z/.w` + colour aliases) panicked
with "unresolved type reached LLVM emission". The store path had no
vector branch: a `.field_access` target on a Vector fell through to
struct-field lookup, matched nothing, left `field_ty = .unresolved`, and
built a `ptrTo(.unresolved)` that tripped the LLVM emission guard. The
read path resolved the lane fine — the two had diverged (issue-0083
two-resolver class).

Extract a shared `Lowering.vectorLaneIndex` resolver and route BOTH paths
through it. The read path (`lowerFieldAccessOnType`) delegates to it,
dropping its silent `else 0` fallback. A new vector branch in
`lowerAssignment` GEPs a typed pointer to the lane (`structGepTyped`) and
stores via `storeOrCompound` (plain + compound). `emitStructGep` now
addresses a vector base type with a `[0, lane]` GEP. A non-lane field now
reports field-not-found on both paths instead of silent-lane-0 / panic.

Regression: examples/1506-vectors-lane-store.sx (panicked pre-fix, now
reads back written values) + a vectorLaneIndex unit test. Resolves issue
0086; spec documents element assignment.
2026-06-05 01:32:35 +03:00
agra
20c767e336 refactor(ir): move pure JNI helpers into jni_descriptor.zig (A6.2 step 2)
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).
2026-06-03 08:28:41 +03:00
agra
0a4a240e31 test(ir): lock pure JNI decision helpers before A6.2 extraction (A6.2 scaffolding step 1)
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).
2026-06-03 08:14:46 +03:00
agra
9bde1dd590 refactor(ir): extract ObjcLowering (ffi_objc.zig) for pure Obj-C decision helpers (A6.1 step 2)
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).
2026-06-03 08:00:42 +03:00
agra
b5119e8587 test(ir): cover Obj-C protocol pointers in isObjcClassPointer/objcPropertyKind (A6.1 scaffolding review fix)
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.
2026-06-03 07:45:10 +03:00
agra
0012228796 test(ir): lock pure Obj-C decision helpers before A6.1 extraction (A6.1 scaffolding step 1)
Test-first scaffolding for the Obj-C FFI domain (Phase A6.1) before the pure
helpers move into src/ir/ffi_objc.zig. Visibility-only change to the targets —
no behavior change.

- 3 new lower.test.zig tests for the pure helpers the ARCH-SAFETY A6.1 row names
  that lacked direct unit coverage:
  - deriveObjcSelector: niladic (bare name) / single-keyword (name:) /
    multi-keyword (_ -> : + trailing) / #selector(...) override (verbatim,
    keyword_count = #colons).
  - objcPropertyKind: assign default (primitive), strong default (object ptr),
    explicit weak/copy/assign win over the default.
  - isObjcClassPointer: pointer-to-foreign-Obj-C-class true; plain-struct ptr /
    *void / builtin false.
- objcTypeEncodingFromSignature (x6) + objcDefinedStateStructType (x3) already
  covered — no new tests.
- Widened deriveObjcSelector + objcPropertyKind to pub (they become facade
  methods in step 2; the ObjcPropertyKind enum stays private — tests compare via
  enum-literal == .strong). No logic touched.
- Recorded the A6.1 coverage inventory + residual gaps (resolveObjcParentName,
  class-method metadata, property/state lookups — example-guarded) in
  ARCH-SAFETY.md.

Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(no .ir churn; Obj-C snapshots 1309/1329/1332/1347 green).
2026-06-03 07:15:56 +03:00
agra
667192c718 refactor(ir): extract ErrorAnalysis (error_analysis.zig) for error-set convergence (A5.1 step 2)
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.
2026-06-02 23:11:18 +03:00
agra
9153f958ea test(ir): lock error-set convergence before A5.1 extraction (A5.1 scaffolding step 1)
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.
2026-06-02 22:57:39 +03:00
agra
50dd2cc3d8 test(ir): lock coercion forms before A4.3 extraction (A4.3 scaffolding step 1)
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.
2026-06-02 22:32:01 +03:00
agra
df386a422e test(ir): lock protocol/impl lookup before A4.2 extraction (A4.2 scaffolding step 1)
Test-first scaffolding ahead of extracting src/ir/protocols.zig — no code change
to the refactor targets (registerProtocolDecl / registerImplBlock /
registerParamImpl / hasImplPlain / tryUserConversion / tryPackImplMatch /
createProtocolThunk / buildProtocolValue).

Adds 4 .ir snapshots (only 0400 existed for 04xx), each captured surgically via
`sx ir | normalize_ir`, path-free, idempotent, and print-free at IR-gen time
(the 0524 contamination lesson):
- 0413-protocols-parameterized-protocol-value  parameterized protocol
                                               (registerParamImpl + tryUserConversion)
- 0414-protocols-generic-struct-protocol-erase generic-struct erasure
                                               (createProtocolThunk + buildProtocolValue)
- 0416-protocols-auto-type-erasure             auto erasure (buildProtocolValue + thunk)
- 0528-packs-protocol-pack-methods             pack-variadic impl (tryPackImplMatch)

With existing 0400 (impl-for-builtin) they pin erasure (auto/generic/builtin) +
parameterized + pack-variadic + dispatch; the 0410/0411/0412 runtime anchors
already pin cross-module visibility + duplicate-impl rejections.

Adds 1 unit test via the public surface (no new exposure, mirroring A4.1
sub-step 1): registerProtocolDecl -> getProtocolInfo builds the dispatch method
table (method names, param_types with self excluded, concrete vs Self return
with ret_is_self + *void encoding). The impl-lookup / conversion plan-object
tests (hasImplPlain, tryUserConversion, tryPackImplMatch — private today) land
with the registry in sub-step 2.

zig build, zig build test, tests/run_examples.sh (357/0) all green.
2026-06-02 21:44:01 +03:00
agra
3ca68189c0 refactor(ir): extract GenericResolver (generics.zig) for substitution + mono keys (A4.1 step 2)
Generic substitution and monomorphization-key construction now live in one
module, src/ir/generics.zig, behind a *Lowering facade (GenericResolver),
mirroring CallResolver / ExprTyper. Moved verbatim:
- mangleTypeName + mangleParamList (the mono-key fragment builder),
- mangleGenericName (generic mono key), appendComptimeValueMangle (comptime-value
  fragment),
- buildTypeBindings (call-site type-param inference), inferGenericReturnType
  (generic return resolution).

inferGenericReturnType now uses a scoped TypeBindingScope (enter/exit with defer)
instead of a manual type_bindings save/restore — the PLAN-ARCH A4.1 "scoped
substitution env" shape; a generics.test.zig assertion confirms the prior
bindings are restored (the issue-0048/0050 leak class, for this field).

Lowering keeps a thin pub mangleTypeName wrapper delegating to
genericResolver().mangleTypeName, because ~30 cross-cutting callers (impl-map
keys, conversion keys, shape keys) reach it well beyond generics. mangleParamList
(sole caller was mangleTypeName) moved fully. The other 4 originals are deleted
(no fallback); their 6 call sites now go through self.genericResolver()
(calls.zig via self.l.genericResolver()).

matchTypeParam / extractTypeParam / isTypeParamDecl widened to pub (the moved
substitution logic calls them); genericResolver() accessor added. The 2
mangleTypeName / inferGenericReturnType unit tests moved from lower.test.zig to
generics.test.zig (driving GenericResolver directly) and wired into the barrel.

monomorphizeFunction / monomorphizePackFn intentionally stay in lower.zig (they
save/restore three fields across nested mono and call emission helpers) — a
heavier scoped-env adoption deferred to an optional sub-step 3.

zig build, zig build test, and tests/run_examples.sh (357/0) all green — no .ir
snapshot churn, confirming the move preserved mono-key/substitution output.
2026-06-02 21:28:31 +03:00
agra
91e99f80c7 test(ir): lock generic substitution + mono keys before A4.1 extraction (A4.1 scaffolding step 1)
Test-first scaffolding ahead of extracting src/ir/generics.zig — no code change
to the refactor targets (buildTypeBindings / mangleGenericName / monomorphize* /
inferGenericReturnType / mangleTypeName).

Adds the first non-FFI generic/pack .ir snapshots (closing the ARCH-SAFETY §3
gap for this phase), each captured surgically via `sx ir | normalize_ir`,
path-free and idempotent:
- 0200-generics-generic            generic fn, type-param inference + mono
- 0201-generics-generic-struct     generic struct instantiation
- 0507-packs-pack-mono-dedup       mono-key dedup (same shape => one mono)
- 0518-packs-pack-value-dispatch   pack value dispatch (monomorphizePackFn)
- 0524-packs-generic-fn-pack-state-leak  pack-state isolation (issue-0048/0050
                                         class; guards the future scoped-env change)

Adds 2 unit tests via the existing public surface (no new pub exposure,
mirroring the A3.2 sub-step-1 cadence):
- mangleTypeName: pins the mono-key fragment encoding per type shape
  (s64 / ptr_X / opt_X / SL_X / mptr_X / AR_n_X / vec_n_X / struct-name / tu_X_Y).
- inferGenericReturnType: explicit type-arg path binds $T and resolves the
  -> T return (pair(s64,..) => s64, pair(f64,..) => f64).

The internal substitution/mono-key unit tests (comptime-value mangle,
buildTypeBindings strategies, scoped-env isolation) land with the generics.zig
extraction in sub-step 2, as A3.2's plan-object tests landed with CallPlan.

zig build, zig build test, tests/run_examples.sh (357/0) all green.
2026-06-02 21:05:33 +03:00
agra
fb262e9e59 refactor(ir): move declaration maps into ProgramIndex (A1.1b)
Architecture phase A1.1b — mechanical storage relocation. Move the 9
declaration-fact maps out of the Lowering state bag into ProgramIndex:

  high-fanout:   fn_ast_map, foreign_class_map, global_names, type_alias_map
  medium-fanout: struct_template_map, protocol_decl_map, protocol_ast_map,
                 module_const_map, ufcs_alias_map

168 self.<map> sites in lower.zig repointed to self.program_index.<map>;
external readers repointed too (core.zig foreign_class_map iteration;
lower.test.zig fn_ast_map / foreign_class_map). No duplicate storage, no
fallback path; zig build enforces no missed reference.

The four maps whose value types were Lowering-private pull those types into
program_index.zig as pub (GlobalInfo, StructTemplate + TemplateParam,
ProtocolDeclInfo + ProtocolMethodInfo, ModuleConstInfo); lower.zig aliases
them at file scope so call sites are unchanged.

Behavior is preserved exactly:
- per-map allocator unchanged — import_flags/fn_ast_map/global_names use the
  lowering allocator (ProgramIndex.init), the other 7 keep their page_allocator
  inline defaults;
- ProgramIndex.deinit frees only the 10 owned maps, never the borrowed
  module_scopes / import_graph;
- TypeTable.aliases still borrows &self.program_index.type_alias_map, loaned at
  lowerRoot with the same late-binding lifetime.

Extends program_index.test.zig with declaration-map round-trips (fn AST, type
alias, global, module const, foreign class, protocol decl/AST, struct template,
ufcs alias).

Registration logic (registerStructDecl / registerProtocolDecl /
registerForeignClassDecl, ...) stays in Lowering, writing through the index.

Gate green: zig build, zig build test, bash tests/run_examples.sh
(350 passed, 0 failed). lower.zig 19433 -> 19393 lines.
2026-06-02 12:30:11 +03:00
agra
bdd0e96d78 feat(lang): block value requires no trailing ; (Rust-style)
A block's value is now its last statement ONLY when that statement is a
trailing expression with no `;`. A trailing `;` discards the value,
leaving the block void. This makes value-vs-statement explicit and lets
the compiler reject "this block was supposed to produce a value".

Compiler:
- Parser records `Block.produces_value` (last stmt is a no-`;` trailing
  expression) + `Block.discarded_semi` (the `;` that discarded a value),
  via `expectSemicolonAfter`. A trailing expression before `}` may now
  omit its `;` (previously a parse error). Match-arm and else-arm bodies
  are built value-producing regardless of the arm `;` (arms are exempt —
  the `;` is an arm terminator).
- Lowering: `lowerBlockValue` / the block-expr path / `inferExprType`
  respect `produces_value`. A value-position block that discards its value
  is a hard error (`lowerValueBody` for function bodies; the value-context
  `.block` path for if/else branches, `catch` bodies, value bindings,
  match arms). Pure-failable `-> !` bodies (value rides the error channel)
  and a value-if whose branches are void are handled without false errors.
- `defer`/`onfail` cleanup bodies lower as statements (void), so a
  trailing `;` there is fine.

Migration (behavior-preserving — output unchanged):
- stdlib + ~210 examples: dropped the trailing `;` on value-position last
  expressions. `format` now ends with an explicit `#insert "return
  result;"` (it relied on `#insert`-as-block-value, which `;` discards).
- Two `main :: () -> s32` examples that relied on the old silent
  default-return got an explicit trailing `0`.
- Rejection snapshots 0412 / 1013 regenerated (their quoted source lines
  lost a `;`); the diagnostics themselves are unchanged.

Docs/tests: specs.md "Block values" section; examples 0040 (rules) + 0041
(rejection); 3 parser unit tests. Filed issue 0066 (pre-existing
match-arm negated-literal phi-width quirk, surfaced not caused here).

Gates: zig build, zig build test, run_examples.sh -> 343 passed,
cross_compile.sh -> 7 passed (also refreshed its stale example names).
2026-06-02 09:23:50 +03:00
agra
b44a5d05ef ERR/E3.0 (slice 1): thread source spans into IR instructions
Foundation for DWARF line-info (E3.0). The `Inst.span` field existed but was
never populated — `emit()` always passed the empty `{0,0}` default, so every
instruction had no source location (the lone reader, the interp's comptime
bail-offset, was always 0).

- Builder gains a `current_span`; `emit`/`emitVoid` stamp it onto each
  instruction.
- `lowerExpr` / `lowerStmt` set `current_span` from the AST node's span on
  entry and restore it on exit (save/restore), so a parent's later emits keep
  the parent's span after a child lowers; the empty default is skipped so
  synthetic nodes don't reset a meaningful enclosing span.

Behavior-neutral: codegen never reads spans, and the only consumer (the interp
bail-offset) merely gains real offsets. 290 examples pass unchanged, no `.ir`
snapshot drift. New unit test asserts an emitted `add` carries its `a + b` span.

Next (slice 2): bind `llvm-c/DebugInfo.h`, emit DICompileUnit / DISubprogram /
DIFile / DILocation from these spans, gate on debug/trace mode.
2026-06-01 12:52:14 +03:00
agra
696a749bd5 ERR/E1.4c: noreturn plumbing for divergence shapes
Type the divergence shapes as `noreturn` so a `catch` body that diverges
(E1.5) unifies with the failable's success type. The plan's original
"E1.4b", renumbered E1.4c (the SCC slice took the "E1.4b" label).

- inferExprType: `return` / `raise` / `break` / `continue` -> .noreturn
  (removed `.return_stmt` from the statements-are-`.void` group)
- if-else unification: a `.noreturn` branch yields the other branch's type;
  both diverging -> `.noreturn`
- block-ending-in-divergence propagates `.noreturn` (existing block arm)
- calls to `-> noreturn` already type via Function.ret (verified)
- made inferExprType pub for the unit test

Scope: the essential divergence shapes. Deferred `unreachable` (not a
keyword in sx — a separate feature, no current consumer) and infinite-loop
`noreturn` detection (rare). No observable consumer until E1.5's catch body,
so validated by a unit test, not an example.

Tests: unit test `E1.4c noreturn typing` in lower.test.zig (each shape ->
noreturn; block propagation; if-else unification). Gates: zig build,
zig build test, 262/262 examples (no new examples).
2026-05-31 20:33:13 +03:00
agra
d2cba4e460 ERR/E1.4b: whole-program inferred error sets + empty-inferred warning
The type-convergence side of E1.4 (the SCC slice). A bare `-> !` function's
error set is now converged whole-program from its literal raises plus the
sets of the pure-failable functions it `try`s.

- convergeInferredErrorSets: a pre-lowering fix-point pass (lowerRoot Pass
  1d, after scanDecls / before body lowering) that walks each top-level
  bare-`!` function's body AST (collectErrorSites, stopping at nested-fn
  boundaries) for literal `raise error.X` tags + pure `try g()` edges, then
  unions each set with its edges' sets until stable. Stored in a side map
  `inferred_error_sets` (fn name -> sorted []u32) — sidesteps the name-only
  error-set interning collision (the shared `!` placeholder stays empty).
- lowerTry widening: a named caller `try`-ing a bare-`!` callee now checks
  the callee's converged set (previously a false-negative — the empty
  placeholder was trivially a subset). Factored diagTagsNotInSet out of
  checkErrorSetSubset.
- empty-inferred warning: a top-level non-main bare-`!` function with an
  empty converged set warns. Not user-visible yet (the compile driver
  renders diagnostics only on failure — a LANG follow-up), so unit-tested
  on the DiagnosticList.
- corrected two now-stale bail messages (failable-`or` -> E2.4;
  value-carrying `try` -> E2).

Deferred to E2.4: failable-`or` chains / value-terminators (and `try`
fallback routing) — gated on the value-carrying tuple ABI.

Tests: examples/223-inferred-error-sets.sx (transitive convergence +
widening passes, exit 7), examples/224-inferred-widening-reject.sx
(transitive widening rejection, exit 1), unit test in lower.test.zig.
Gates: zig build, zig build test, 262/262 examples.
2026-05-31 20:21:44 +03:00
agra
fac235950d lang 2.2: protocol-arg lookup + position-driven pack projection
Add the name-resolution primitives a `..pack.<name>` projection needs
(Decision 4). A protocol exposes two namespaces: type-args (the
`protocol($T, ...)` params) and runtime accessors (its methods — protocols
have no fields). Resolution is position-driven with no cross-namespace
fallback:

- lookupProtocolArg(protocol, name) -> ?u32   (type_params index)
- lookupProtocolField(protocol, name) -> ?u32 (methods index)
- resolvePackProjection(protocol, name, pos)  (.type_arg | .method | .not_found)

registerProtocolDecl now warns when a type-arg and a method share a name
(allowed, but `..pack.<name>` then resolves by position, which surprises
readers). 3 unit tests cover both namespaces, the position rule, and the
shadowing warning + deterministic resolution despite a shadow.

Projecting a *bound* pack (producing a new Pack of per-element results) waits
for call-site binding in Step 2.4; these primitives are what it will call
per element.
2026-05-29 16:00:03 +03:00
agra
4defadf513 test: make zig build test actually run all tests + fix latent rot
root.zig had no `test` block, so the test binary discovered zero tests and
trivially "passed" — every src test had silently rotted. Add
`refAllDecls(@This())` to root.zig so all 185 tests run, then fix the rot it
surfaced:

- emit_llvm.test: operands were constants, so LLVM folded the very
  instructions being asserted (fadd/sub/icmp/insertvalue/extractvalue/sext).
  Rewrite to use function-parameter operands; `main` now returns i32 (entry
  convention); tagged-union enum_init lowers via memory, not insertvalue.
- interp.test: switch the per-test allocator to an arena (the interpreter is
  arena-style and intentionally frees little) — clears the transient-Value
  leaks without an ownership-ambiguous source change.
- lower.test: pass `is_imported` to lowerFunction; mark two helpers `pub`; the
  if/else block test now uses a runtime (param) condition since lowering folds
  `if true`.
- print.test: SSA numbering — params occupy %0/%1, so consts start at %2.
- jni_java_emit.test: nested-class refs render in Java source form
  (`SurfaceHolder.Callback`), not the JNI `$` form.

Leaks fixed at the source where ownership was clear: Module gains an arena for
the operand slices the Builder dupes (struct/call/branch/switch args, block
params, lowerFunction params); objcDefinedStateStructType builds its field
slice in that arena and frees its temp name string.
2026-05-29 15:25:00 +03:00
agra
6d258ad82b ffi M1.2 A.1 follow-up: struct args/returns in Obj-C type encoding
`appendObjcEncoding` previously bailed on `.@"struct"`, which blocked
sx-defined `#objc_class` methods from declaring CGPoint / CGRect /
NSRange-shape signatures — the `class_addMethod` registration path
would emit a "type kind not yet supported by Obj-C encoding"
diagnostic. The helper now emits Apple's `{Name=field0field1...}`
form recursively, with a small `ObjcEncodingStack` (cap 16) that
breaks transitive struct→struct cycles by emitting the abbreviated
`{Name}` form instead of recursing forever.

`{Point=dd}`, `{_NSRange=QQ}`, `{CGRect={CGPoint=dd}{CGSize=dd}}`
all flow through the existing `objc_msg_send` + `class_addMethod`
path with no further plumbing.

Tests:
- `lower.test.zig` gains four cases: optional unwrap (single + nested),
  flat struct (CGPoint, NSRange shape), nested struct (CGRect with
  CGPoint+CGSize), bringing the helper's test coverage from
  primitives + pointers to the full encoding table.
- `examples/ffi-objc-defined-class-02-struct-encoding.sx` exercises
  a sx-defined `SxMover` class with `goto(p: Point)` setter and
  `here() -> Point` getter end-to-end on macOS; the IR snapshot
  confirms `v@:{Point=dd}` and `{Point=dd}@:` land in
  `OBJC_METH_VAR_TYPE_` constants wired to `class_addMethod`.

Checkpoint cleanup: the "Next step (M1.2 A.1 — type-encoding
derivation table)" header in CHECKPOINT-FFI.md was stale (A.1
shipped in 6cc016c; A.0–A.7 all done; commit list now linked).
The encoding table stays as reference material.

224/224 example tests pass; zig build test green.
2026-05-28 14:24:02 +03:00
agra
7b98b3ae78 ffi M1.2 A.2a: objcDefinedStateStructType helper
Builds (and interns) the hidden sx-state struct type for an
sx-defined '#objc_class'. Layout:

    __<ClassName>State {
        user_field_0,
        user_field_1,
        ...
    }

This struct is what the runtime's '__sx_state' ivar points at —
separate from the Obj-C object itself, which stays opaque. The
sx method bodies will operate on '*__SxFooState' (after '*Self'
substitution in A.2b) so 'self.field' resolves to a plain struct
field access — A.3's 'free if types align' premise.

M1.2 A.5 will prepend '__sx_allocator: Allocator' so dealloc can
free through the per-instance allocator. Field-by-name access
stays correct across the future repositioning.

Methods / '#extends' / '#implements' members are ignored — only
'.field' contributes. Three unit tests pin: typical-field case,
empty-class case, mixed-member case.

Dead code at this commit — helper isn't called yet. A.2b (body
lowering with '*Self' substitution) wires it in. 170 example
tests + zig build test green.
2026-05-25 21:51:07 +03:00
agra
6cc016cd4f ffi M1.2 A.1: objcTypeEncodingFromSignature helper + encoding table
Derives Apple's runtime type-encoding string from an IR method
signature. Called by class_addMethod(cls, sel, imp, types) when
M1.2 A.4+ synthesise IMPs for sx-defined classes.

Layout: <ret> @ : <param0> <param1> ...   — @ is the receiver,
: is _cmd. Caller passes user-declared params AFTER stripping
'self: *Self'.

Encoding table:
  v=void  B=bool  c=s8/BOOL  s=s16  i=s32  q=s64
  C=u8    S=u16   I=u32      Q=u64  f=f32  d=f64
  @=foreign Obj-C class ptr        #=Class  :=SEL
  *=[*]u8 (C string)               ^v=any other ptr

bool (sx i1) maps to 'B' (C99 _Bool); s8 to 'c' (Apple's BOOL).
Foreign-class pointers detected via foreign_class_map lookup on
the pointee struct name. Other pointers fall to ^v — encoding is
metadata, not ABI, so conservative is safe.

Struct / slice / closure / etc. BAIL via diagnostic
(ObjcEncodingUnsupported) rather than silently mis-encoding, per
CLAUDE.md rejected-patterns rule. Future passes will widen the
table as new shapes show up in real IMPs.

Dead code at this commit — helper isn't called yet. Three unit
tests in src/ir/lower.test.zig pin the primitive / pointer /
Obj-C-class-pointer encodings before A.2 wires the helper in.

170 example tests + zig build test green.
2026-05-25 21:43:53 +03:00
agra
dd14f1206b ir 2026-02-26 02:25:02 +02:00