Commit Graph

179 Commits

Author SHA1 Message Date
agra
0b13498e25 fix(0115): source-aware global selection — own-wins for module globals
The globals registry (global_names) was last-wins across modules with no
per-importer gate: any module's bare K could read/write/type against an
unrelated module's same-named global (hash.sx's K table hijacked every
user K once std's namespace tail pulled hash into the program), and an
own const of an unsupported shape borrowed another module's const and
panicked at the unresolved-type tripwire.

- var_decl joins RawDeclRef: module globals are selectable raw authors.
- selectGlobalAuthor (the globals analogue of F2's selectModuleConst):
  own author wins, one flat-visible author resolves, >=2 distinct flat
  authors diagnose loudly, authored-but-not-visible diagnoses, and a
  compiler-synthesized global (no raw author) emits untracked. A var_decl
  author whose per-source registration was deduped at flat-merge (two
  modules declaring the same extern symbol) serves the symbol's
  registration.
- All bare-identifier global sites route through it: value read, addr-of,
  assignment (store + compound), lvalue address, fn-ptr call, call param
  typing, and expression type inference.
- selectModuleConst gains .own_opaque: an own const author with no
  materialized per-source value (e.g. an array '::' const) blocks
  borrowing another module's same-named const — the read diagnoses
  cleanly instead of panicking.
- The fn-as-VALUE arm admits raw-facts-only authors: an own fn whose name
  a flat-merge collision dropped from the global decl list (first-wins)
  now resolves via author selection for func_ref/closure/Any shapes too.

Regressions: examples 0835 (own const vs flat array global), 0836 (main
const vs namespaced array global, incl. inference), 0837 (own array
const never borrows cross-module — clean unresolved).
2026-06-11 10:47:30 +03:00
agra
fbbfcb268c fix(0114): gate alias-qualified calls to one-level carry, pin to target
The lowerCall namespace branch routed alias.fn() through the global
qualified registration (first-wins) at any import depth, and through the
global last-wins bare map for comptime/generic members. Plain-identifier
alias roots now resolve via the carry-aware namespaceAliasVerdict:

- visible alias (own edge or ONE flat hop): the member dispatches the
  TARGET module's own fn (namespaceFnMember + fd-keyed bareAuthorFuncId),
  so two modules' same-named aliases each call their own target.
- two direct flat imports carrying the alias to distinct targets:
  loud ambiguity diagnostic.
- alias only reachable beyond one hop: "namespace 'X' is not visible".
- foreign / builtin / #compiler members keep the literal-symbol path.

Regressions: examples 0832 (two-hop), 0833 (carried collision),
0834 (own-target pin / first-wins repair).
2026-06-11 09:16:03 +03:00
agra
698f75d79a lang: reject dir-vs-file ambiguous #import
An extensionless import path that names a directory next to a same-named
.sx file ('modules/std' with both modules/std.sx and modules/std/ present)
no longer silently resolves to the directory — it errors and asks for the
explicit .sx spelling. Exemption: a file importing its own companion
directory (X.sx importing X/, the multi-file test layout) stays legal —
the sibling file is the importer itself, so the directory is the only
sensible target.
2026-06-11 08:37:36 +03:00
agra
12bf61a9fc std: restructure step 3 — ffi/ moves, build.sx, math dir spelling, fixtures
- objc.sx, objc_block.sx (from std/) + sdl3/opengl/raylib/stb/stb_truetype/
  wasm vendor bindings (from modules/ root) -> modules/ffi/
- std/uikit.sx deleted: platform/uikit.sx already declares UIApplicationMain
  and imports objc; '#framework "UIKit"' cannot live in a file imported on
  macOS targets (unconditional link directive, UIKit is iOS-only), so the
  three iOS-only examples carry the 3-line glue inline. 1607/1608/1616 also
  un-rotted (dead ns_string -> 'xx "..."' Into conversions, callconv(.c)
  msgSend fn-ptrs) — all three build for ios-sim/ios again.
- math/math.sx -> math/scalar.sx; one spelling '#import "modules/math"'
  everywhere (4 pinned IR snapshots regenerated: dir import adds Vec2/Mat4
  to the type tables).
- compiler.sx -> build.sx (imports, CLAUDE.md bundling table, specs.md).
- testpkg/ + test_c.sx -> tests/fixtures/ (resolve CWD-relative from repo
  root, same as vendors/).
- library-internal imports use full modules/... paths (std.sx tail,
  platform/bundle.sx, fixtures).
2026-06-11 08:37:22 +03:00
agra
59f0aa7716 std: restructure — std/ modules, namespace tail, std/xml.sx
allocators/fs/process/socket/log/trace/test move under modules/std/
(allocators.sx becomes std/mem.sx; the Allocator protocol moves into
the std.sx prelude, impls stay in mem.sx). New std/xml.sx holds
xml_escape as xml.escape. std.sx gains the carried namespace tail —
flat-importing std.sx now also provides mem./xml./log. — with the
remaining modules (fs/process/socket/json/cli/hash/test) deferred from
the tail until the global last-wins maps are fully own-wins (pulling
them into every closure collides bare names corpus-wide; they stay
direct imports: modules/std/fs.sx etc.). log.sx's internal emit
renamed log_emit (it clobbered consumer fns named emit program-wide).
bundle.sx uses xml.escape via the carried alias. Consumer import paths
swept mechanically; .ir snapshots recaptured for the larger std
closure. m3te + game build unchanged.
2026-06-11 06:10:59 +03:00
agra
ee00db849c lang: qualified namespace members in value position + alias carry
Two coupled capabilities on the road to the std restructure
(current/PLAN-STDLIB.md, issue 0114):

1. alias.Type.method() / alias.Type as a call head, alias.CONST, and
   alias.Enum.variant now resolve — previously only alias.fn() and
   type-position alias.Type worked. objectIsValue treats an
   alias-rooted field_access as a type head; the call path strips the
   alias to the existing Type.method machinery; lowerFieldAccess
   resolves alias.CONST pinned to the target module and alias.Enum.x
   as a typed enum literal; resolveTypeWithBindings resolves qualified
   type_exprs pinned to the target.

2. The carry rule: namespaceAliasTarget resolves an alias from the
   file's own edges first, then from DIRECT flat imports (one level),
   diagnosing two distinct carried targets as ambiguous. All qualified
   shapes work through a carried alias — the std.sx namespace tail
   (mem.GPA.init() etc.) is now expressible.

Regression: examples/0831-modules-namespace-alias-carry.sx (direct +
carried, all seven shapes).
2026-06-11 05:52:10 +03:00
agra
83ec2536af lang: catch/onfail error bindings take parens
try foo() catch (e) { }   // legal
try foo() catch e { }     // parse error with a migration hint

Same capture style as the for-loop. All four catch shapes keep working
with the parenthesized binding — block, bare-expression body, and the
== match sugar — and the no-binding forms are unchanged. onfail follows
the same rule (onfail (e) { }); its expression-cleanup form is
disambiguated by the paren-group-before-brace lookahead, so
onfail (f()); stays an expression cleanup.

AST unchanged; the printer renders the parens; the #run escape help
text updated. Corpus migrated (57 catch + 3 onfail bindings, in-source
parser test strings, specs incl. grammar rules, readme untouched —
no catch examples there).

Regression: examples/1157-diagnostics-catch-binding-needs-parens.sx;
re-captured stderr for 1010/1013/1037/1123 (migrated source echoed in
carets + help text).
2026-06-10 23:05:02 +03:00
agra
12149eb548 fix(0113): negated-literal global initializers fold as constants
globalInitValue had no unary_op arm, so g : s64 = -1; fell into the
catch-all 'must be initialized by a compile-time constant' even though
constExprValue already folds negate(literal) for the module-const
identifier route. The new arm routes through constExprValue and applies
the direct-literal rules to the folded value: checkIntLiteralFits on
ints (g : s8 = -300 gets the range diagnostic), and a negated float at
an integer global narrows only when integral (-4.0 folds to -4, -4.5
errors). Binary-op initializers keep the specific non-constant
diagnostic.

Regression: examples/0175-types-negative-literal-global.sx.
2026-06-10 22:39:52 +03:00
agra
67313e1dad fix(0112): out-of-range int literals error instead of silently wrapping
checkIntLiteralFits range-checks a literal against its integer target
(builtins + custom widths via intLiteralRange; width-64 types skip —
every representable literal is a legal bit pattern there) and diagnoses
with the type's range and an xx/cast hint. Wired into the .int_literal
arm (covers decls, assignments, call args, struct-literal fields),
lowerStructConstant, and globalInitValue.

A negated literal now folds to a single constant so -128 range-checks
as -128 rather than as an out-of-range +128 intermediate. An explicit
xx operand skips the check — truncation stays available on request
(cast(T) was already exempt: its value arg lowers without the target).

examples/0300-closures-lambda.sx pinned 133 wrapping to -3 through an
s3 param — the exact class this outlaws; updated to a fitting value.

Found during the fix and filed separately: issue 0113 (negated-literal
global initializers rejected as non-constant; pre-existing).

Regressions: examples/1156-diagnostics-int-literal-out-of-range.sx,
examples/0174-types-int-literal-boundaries.sx.
2026-06-10 22:28:24 +03:00
agra
fea5617e4e lang: slice ranges take the same bound markers as for-header ranges
xs[1..=3] (end inclusive), xs[0<..<4] (both exclusive), xs[..=2]
(prefix form with markers, implicit 0 start), xs[2<..] (open end,
exclusive start), and xs[..] (whole collection) — lowered as lo+1 /
hi+1 on the existing subslice op. Strings slice through the same path.
An explicit end marker requires an end expression, matching the
for-header rule.

Regression: examples/0052-basic-slice-range-bounds.sx.
2026-06-10 22:12:45 +03:00
agra
f513c11ea6 test(0051): pin expression bounds at either end of range tokens
x+2..=42 (expression start, 39 iterations summing 897),
x+2<..<x*21 (expressions both ends, 5..41), 0..x*3 (expression end).
Expression parsing stops at the range lexeme from either side, so any
expression works in either position — now pinned.
2026-06-10 22:02:34 +03:00
agra
fd14ab5694 lang: range bound markers — '=' inclusive / '<' exclusive on either side of '..'
Each side of '..' takes an optional bound marker, defaulting to
start-inclusive, end-exclusive (a..b == a=..<b; a..=b stays the short
end-inclusive spelling):

    for 0<..<N (i) { }   // 1 .. N-1   (both exclusive)
    for 0=..=N (i) { }   // 0 .. N     (both inclusive)
    for 0<..=N (i) { }   // 1 .. N
    for 0..<N  (i) { }   // 0 .. N-1   (explicit default)
    for xs, 2<.. (x, i)  // open range, exclusive start: i = 3, 4, ...

The nine lexemes are single tokens (maximal munch on '<'/'='/'..'), so
expression parsing never sees the leading marker as a comparison; '<',
'<<', '<=', '==', '=>' lex unchanged. An explicit end marker makes the
end expression mandatory; open forms are a.. / a<.. / a=... Works in
runtime, multi-iterable, and inline-for headers.

Regression: examples/0051-basic-for-range-bounds.sx (full matrix, open
start-marked ranges, comptime unroll, runtime bounds, lexer
non-regression); 1152's pinned message generalized.
2026-06-10 20:55:31 +03:00
agra
116af2359e lang: multi-iterable for loops — drop ':', add '..=', open ranges, arrow bodies
The for header is now a comma-separated list of iterables with a
positional capture group and no ':' separator:

    for xs (x) { }                    // collection
    for 0..n (i) { }                  // range (end exclusive)
    for 1..=5 (a) { }                 // ..= inclusive end
    for xs, 0.. (x, i) { }            // index idiom (replaces (x, i))
    for xs, ys (x, y) { }             // parallel (zip) iteration
    for xs (x) => sum += x;           // arrow body (full statement)

First-iterable-wins: the first iterable's length drives the loop and
must be bounded; the other positions follow by their own cursors (a
non-first range's end is not consulted or evaluated; a shorter
non-first collection is read past its length on mismatch). The old
single-iterable index capture is replaced by the trailing open range.

Capture/call disambiguation is positional: the paren group immediately
before '{' or '=>' is the capture, every earlier top-level group is a
call. 'for zip(a, b) (x, y)' calls zip; 'for f(n) { }' reads (n) as
the capture and errors with a parenthesize/add-capture hint. The old
':' form errors with a migration hint.

Lowering is unified across forms: one cursor slot per position (ranges
start at their start, collections at 0), all advanced together, the
first position's bound terminating. inline for keeps the single
bounded comptime range.

Migrated the full corpus (examples, library modules, issue repros,
in-source test strings). New coverage: examples/0050 (the full feature
surface) and examples/1149-1155 (seven diagnostic faces). specs.md For
Loop section + grammar rewritten; readme teaser updated.
2026-06-10 20:30:55 +03:00
agra
3cc34d54c1 fix(0108): break/continue run the loop body's pending defers
lowerBreak/lowerContinue emitted a bare br, and the enclosing block's
emitBlockDefers — seeing the terminator — discarded the pending entries
on the assumption a return had already drained them. The breaking
iteration's defers were silently skipped, leaking whatever the cleanup
released.

Lowering.loop_defer_base records the defer-stack height at each loop's
body start (while / for / range-for, saved and restored alongside
break_target); break/continue drain non-onfail entries down to it in
LIFO order via the non-truncating emitLoopExitDefers before branching.
Truncation stays with the lexical block exits — the same entries still
belong to the fall-through path after the branch containing the break.
break/continue outside a loop now diagnose instead of no-op'ing.

Regression: examples/0049-basic-defer-break-continue.sx (for and while,
break and continue, nested-block LIFO drain).
2026-06-10 17:43:58 +03:00
agra
bf47146085 fix(0110): for-over-array by-value fetch reads one element, not a full copy
lowerFor's by-value element fetch emitted index_get on the array VALUE;
the emitter realizes that as a whole-array spill to a stack temp + GEP,
per iteration — O(N^2) bytes copied per loop (and pre-0109 it also grew
the stack per iteration, segfaulting a [4096]s64 loop).

When the iterable is an array with addressable storage (and not deref'd
from a pointer, whose identifier alloca holds the pointer rather than
the array), the fetch is now index_gep on the storage + one element
load. Storage-less arrays keep the index_get fallback. The loaded
element remains a copy — mutating the capture does not write back.

Regression: examples/0048-basic-for-array-large.sx (sum over 4096
elements + by-value copy-guard).
2026-06-10 17:34:35 +03:00
agra
878c4226a6 fix(0109): hoist all per-instruction allocas to the function entry block
An alloca built at its use site re-executes on every pass through that
block, and LLVM reclaims allocas only at ret — so loop-body locals,
nested-loop index slots, and emitter spill temps (ig.tmp, sret slots, ABI
coercion temps, byval materialization) grew the stack per iteration and
long loops segfaulted on stack exhaustion.

New LLVMEmitter.buildEntryAlloca inserts after existing entry-block
allocas and restores the builder position; every LLVMBuildAlloca site
reachable during instruction emission now routes through it.
Initialization stores stay at the use site (per-iteration re-init is
unchanged), and entry slots become mem2reg-promotable. The 35 .ir
snapshot diffs are pure alloca position moves (type multisets verified
identical per file).

Regression: examples/0047-basic-loop-local-stack-reuse.sx (segfaulted
pre-fix on both the 1M-iteration body-local loop and the 3M-iteration
nested loop).
2026-06-10 17:27:11 +03:00
agra
e81780e32e fix(0111): unannotated decl literals no longer adopt the fn return type
lowerVarDecl (unannotated) and lowerDestructureDecl now clear target_type
around the initializer lowering: a declaration without annotation provides
no target, so int/float literals take their spec defaults (s64/f64) instead
of the enclosing function's implicit-return type (x := 0 in a -> s8 fn was
s8; big := 3000000000 in -> s32 silently wrapped to -1294967296).

Regression: examples/0173-types-int-literal-default-s64.sx. The remaining
explicit-annotation wrap (x : s8 = 300) is filed as issue 0112.
2026-06-10 17:21:44 +03:00
agra
010e644897 fix(R1): own const_decl pending alias wins over flat imports (issue 0107)
When a module declares `A :: B; B :: u64;` and both a flat import and a
namespaced import export `B :: u8`, the flat import's B was discovered by
flatTypeAuthorCount before the own B :: u64 was processed — binding A to
u8 and silently truncating values.

Fix: ownConstDeclIsPendingAlias guard added to selectNominalLeaf between
the own-alias check and the flat-import walk. If the querying module has
an own const_decl for the name that is not yet in type_aliases_by_source,
return .pending so the forward-alias fixpoint resolves it correctly.

Regression: examples/0830-modules-flat-ns-same-name-forward-alias.sx
(x : A = 300 prints 300, not 44). 541/541 tests pass.
2026-06-09 22:16:47 +03:00
agra
1ce3a4e9e0 docs(fork-c/S0): setup contract — byte-baseline + commit-discipline, E6b disposition + two-corpus partition, A–E6 reuse/delete ledger
S0 of the ratified Fork C plan (zero-legacy name-resolution redesign, S0→S6).
Pure setup/documentation: NO production code change, NO behavior change.
Single-author output byte-identical to wt-stdlib-base by construction.

Deliverables under docs/fork-c/ (docs/, not current/, because current/ is
gitignored and the contract must be committed):

S0.1 — byte-baseline + commit-discipline: the committed examples/expected/*
snapshots are the single-author byte-identity reference; the zero-diff repro is
`zig build && zig build test && bash tests/run_examples.sh`. Resolver-target set
explicitly excluded + listed. Commit-classification rule: mirror | consumer-cutover | deletion.

S0.2 — E6b disposition + two-corpus partition: transitional E6b src NOT merged
(grep-clean: no resolveRegistrationSigTypeInSource / sig_registration_mode /
e6br_gate.test.zig on baseline). Harvested 0811–0829 trees + goldens (never the
src), empirically partitioned by running each through the base compiler vs the
E6b target:
  - baseline-green (mirror-equivalence): 0795–0798 (merged) + 0823, 0828 — given
    examples/expected/ markers, locked into the S0 baseline.
  - resolver-target (known-wrong old behavior): 0811–0822, 0824–0827, 0829 + the
    re-filed E6BR-5 nested-pattern regression — a listed xfail harness under
    tests/resolver-target/ (manifest + TARGET goldens, NO active marker), flips
    active+green at S3.9. 0811/0829 noted as old-selector-wrong on the E6b-unmerged
    base; E6BR-5 subsumed by the whole-AST resolver, NOT an E6b attempt-6.

S0.3 — A–E6 reuse/delete ledger: every load-bearing A–E6 artifact mapped REUSED
(Fork C home) or DELETED/TRANSITIONAL (S3/S6 phase); E6c/d/e dropped, F/H/I/K
absorbed/superseded.

Gate over the baseline-green corpus: zig build + zig build test (LSP corpus sweep
574 files, no crash) + bash tests/run_examples.sh (540 passed, 0 failed) all exit 0.
2026-06-09 10:29:23 +03:00
agra
b9a67d1042 fix(stdlib/E6a): adopt forward struct stub for recursive enum/union (E6A-1)
attempt-1's per-decl enum/union register path panicked on any valid
self- or mutually-referential top-level enum/union: a `*Name` field in
the body is resolved through the stateless `type_resolver.resolveNamed`,
which has no kind context and forward-stubs an as-yet-unregistered name
as a STRUCT. `internNamedTypeDecl` then `findByName`-adopted that struct
stub and called `updatePreservingKey`, whose kind-stability assert tripped
on struct -> enum/union (types.zig:446). The corpus had no recursive
enum/union, so the gate missed it.

Fix: when the slot `findByName` returns is a wrong-kind forward struct
placeholder (empty-fields struct) for an enum/union/tagged_union
registration, re-key it in place (`replaceKeyedInfo`) under the same
TypeId instead of `updatePreservingKey`. This mirrors how a self-ref
struct adopts its own (same-kind) forward stub; the new helper
`adoptsForwardStructStub` gates the re-key precisely to that case, so a
struct adopting a struct stub and every non-recursive enum/union stay on
the byte-identical `updatePreservingKey`/fresh-intern path.

Regression 0799 (single-author): self-ref union linked cells
(`next: *Node`), self-ref enum/tagged-union (`branch: *Tree`), and a
mutual-ref pair (A holds *B, B holds *A); builds and walks each recursive
link. Fail-before: panic at registerUnionDecl on eed2f99. Pass-after:
exit 0, "union=7 enum=42 mutual=99".

Gate: zig build && zig build test && run_examples.sh all exit 0
(538 passed, 0 failed; 0795-0798 + 0752-0794 + FFI byte-identical);
m3te ios-sim build via the main binary exit 0.
2026-06-08 23:55:46 +03:00
agra
eed2f99f76 feat(stdlib/E6a): per-decl nominal identity for enum + union decls
Give top-level ENUM and UNION decls per-decl nominal identity so two
same-name flat enums/unions intern DISTINCT nominal TypeIds instead of
collapsing to one global last-wins entry. Establishes the reusable
non-struct register path the later E6 kind-steps (E6b error-set, E6c
protocol, E6d foreign-class) extend.

Registration side (was: stateless `type_bridge.resolveInlineEnum/Union`
`findByName` last-wins short-circuit, no Lowering access):
- Split the type_bridge inline builders into a body-BUILDER
  (`buildEnumInfo` / `buildUnionInfo`) + the existing thin interner
  wrappers (field-type positions keep the legacy single-slot path).
- Add `Lowering.registerEnumDecl` / `registerUnionDecl` mirroring
  `registerStructDecl`: build the TypeInfo, intern via
  `internNamedTypeDecl(decl_key, name_id, info, nominal_id)` under the
  per-decl nominal identity (reserved slot id, else `shadowNominalId`).
- Reroute all six enum/union registration dispatch sites (scanDecls
  const-wrapped + top-level, lowerDecls/comptime, block-local, local
  const) to the new path.

Shared infra generalized ONCE:
- Pass-0b genuine-shadow pre-pass now reserves struct/enum/union shadow
  slots of the MATCHING kind, grouped by (kind, name), via a kind-generic
  `topLevelTypeDecl` / `reserveShadowSlot`. A forward/self/mutual ref to a
  shadow name binds to the reserved nominal TypeId.
- `namedRefTid` consults `type_decl_tids` for `.enum_decl`/`.union_decl`
  before the global `findByName`.

No new per-kind resolution path: selectNominalLeaf / headTypeGate /
flatTypeAuthorCount already gate every kind. Single-author /
phantom-double-spelling names keep nominal_id 0 (byte-identical corpus).

Regressions 0795-0798 (enum + union: ambiguity over every bare-type form,
and own-wins with distinct nominal TypeIds), fail-before/pass-after:
0795/0797 exit 0 -> exit 1 with the loud "type is ambiguous" diagnostic;
0796 silently printed `own=.east` -> correct `own=.north`; 0798 hard
`field 'm' not found` error -> correct `own=5 dep=9`.

Gate: zig build && zig build test (423/423) && run_examples.sh (537/537)
all exit 0; m3te ios-sim build via the main binary exit 0.
2026-06-08 23:18:29 +03:00
agra
919c7bd855 fix(stdlib/E5): source-aware value-const TYPE inference (F4)
Value-const SELECTION was source-aware for emission/folding (F2/R1/F1), but
expression TYPE inference still read the global last-wins `module_const_map`,
so an inferred return type / coercion on a same-name const borrowed another
module's const TYPE (mixed-type same-name consts were never exercised by the
attempt-1 same-typed goldens).

- expr_typer.zig: the `.identifier` const path now selects via the source-aware
  `selectModuleConst` (own-wins / one-flat-visible) instead of the global
  `module_const_map`. The global map still gates "is this a const name?"; an
  unpartitioned registration-only author emits its global type, and an ambiguous
  bare reference yields `.unresolved` (the emission path diagnoses loudly).
- lower.zig: expose `selectModuleConst` so the type-inference path shares the one
  author selector emission/folding already use.

Audited every `module_const_map` read: emission (4102) and global-init copy
(1447) were already source-aware (attempt-1); the binds-a-value predicate (6400)
is a boolean, not a type read; the in-`selectModuleConst` read (13842) is the
unwired fallback. No sibling inference site leaks.

examples: 0793 mixed-type own-wins inference (A's `K:s32` yields `1`, not the
global `f64`'s `1.000000`); 0794 mixed-type bare → loud ambiguous (exit 1), the
inference change does not mask the ambiguity. Prior E5 surfaces (0786-0792), the
0105 set (0752-0758), E1-E4 type surfaces (0763-0785) and FFI byte-identical;
533 markers green.
2026-06-08 22:07:12 +03:00
agra
189774712f test(stdlib/E5): pin 0786-0792 value-const same-name golden markers
Track the 21 examples/expected/078x golden markers (exit/stdout/stderr for
0786-0792) generated alongside 5df4ac6. The E5 source change and example
sources were committed there; these regression markers were generated on disk
(the example gate passes against them) but left untracked, leaving the tree
dirty and the new regressions unpinned in git. No source or golden content
changes — markers verified byte-for-byte against the current binary via
run_examples.sh (531 passed, 0 failed).

- 0786 own-wins (a=1 b=2)
- 0787 bare same-name two-flat-visible -> loud ambiguous (exit 1)
- 0788 expr-chain value+dimension coherent (a_len=2 a_val=2 b_len=11 b_val=11)
- 0789 imported expr-const nested leaves pinned to author source (val=2 len=2)
- 0790 cross-module same-name cycle-guard, no false cycle (m=3 len=3)
- 0791 multi-level cross-module chain (big=102 bk=11)
- 0792 struct-field registration-time dimension (a_sz=2 b_sz=7)
2026-06-08 21:40:33 +03:00
agra
6406d0fb1f fix(stdlib/E4): collapse generic-struct author matrix into four choke-points
The generic-struct author-selection matrix {bare,qualified} × {site} × {layout,
body} drifted per-site across 12 attempts because method bodies were resolved by
bare template name in `fn_ast_map["Box.method"]`, independent of which author
produced the instance's layout. Collapse it into four choke-points so
layout-author ≡ body-author by construction:

  CP-1 `selectGenericStructHead` — the single layout-head selector every generic
       struct head site funnels through (alias-RHS .call/.parameterized, array-
       literal, static head, resolveTypeCall/ParameterizedWithBindings). Emits the
       visibility / missing-member diagnostics inline; returns a control-flow-only
       union. No head site reads `struct_template_map` for selection directly.
  CP-2 author stamp — non-optional `decl: *StructDecl` on `StructTemplate` (set at
       the sole producer `buildGenericStructTemplate`) + `struct_instance_author`
       written at `instantiateGenericStruct` from the SAME `tmpl` that builds the
       layout; re-stamped on the dedup fast-path so an instance is never returned
       without an author.
  CP-3 alias metadata copy — mirror template/bindings/author from the mangled
       instance onto the alias display name, so an `ABox`-typed receiver is a
       first-class dispatch instance (Counter-2).
  CP-4 `genericInstanceMethod` / `ensureGenericInstanceMethodLowered` — the single
       body reader: inline methods select via the stamped author (`structMethodFn`,
       source-pin follows for free); impl-block methods fall back to the template-
       keyed `fn_ast_map` entry. Routes the four bespoke body sites (static head,
       instance dispatch, param typing, protocol thunk) + the new qualified static
       head (`a.Box(s64).make(7)`, finding #2).

A debug assert locks `struct_instance_author` / `struct_instance_template` keyset
coincidence so a future third writer that forgets the author trips a test.

Goldens 0777/0778/0780 (bare instance method — ptr/by-value/param-typed, finding
#1), 0779/0785 (qualified static head + missing member, finding #2), 0783 (alias
instance dispatch, Counter-2), 0782 (ambiguity containment). 0414/0415/0543 and
the FFI suites stay green.
2026-06-08 20:34:53 +03:00
agra
7ba64d5756 fix(stdlib/E4): bare generic static-method head selects the visible author (type + method)
The static-method-call head `Box(s64).make(7)` was the last uncovered bare-
generic-head instantiation site: it gated visibility with `headTypeLeak` but
then instantiated the global last-wins `struct_template_map` entry and ran the
name-keyed `Box.make` from `fn_ast_map`, so a NON-visible 2-flat-hop same-name
template (and its method) won. `size_of(Box(s64))` picked the visible `b.Box`
(8) while `Box(s64).make(7)` returned a `c.Box`-shaped (16) value.

Route the static-method head through the single bare-VISIBLE author for BOTH
the instantiated type layout AND the method body: split the existing visible-
author selection into `bareVisibleStructDecl` (returns the StructDecl + source;
single selection point, `bareVisibleStructTemplate` now delegates to it — no
drift) and source-pin the method body via the author's own `sd.methods`
(`structMethodFn`) instead of the last-wins `fn_ast_map`. Ambiguity (>1 visible
author) is already diagnosed by the pre-existing `headTypeLeak` gate.

Exhaustive bare-head instantiation-site audit (all callers reaching
`instantiateGenericStruct` / `struct_template_map` for a bare head): .call alias,
.parameterized_type_expr alias, resolveType .call, resolveTypeCallWithBindings,
resolveParameterizedWithBindings — all already route through the visible-author
selection; the static-method head was the only remaining one and is now covered.

Regression 0776: bare generic static-method head with a 2-hop same-name template
asserts the visible author's layout (xtype=8, x reachable); fail-before xtype=16.
2026-06-08 19:06:13 +03:00
agra
246883073c fix(stdlib/E4): bare generic head selects visible author; qualified missing-member diagnoses
E4 non-transitive type rule had two generic-head author-selection holes:

#1 A BARE generic struct head / alias with a single bare-VISIBLE author still
   instantiated a NON-visible 2-flat-hop same-name template, because the
   `.unregistered` gate arm fell through to the global last-wins
   `struct_template_map` winner. Add `bareVisibleStructTemplate`: after the
   visibility gate passes, select the source-keyed template authored by the
   single bare-visible author (own-wins, else the one 1-hop flat author) and
   instantiate THAT instead of the global map's last-wins entry. Null (→ the
   global map, byte-identical) when the visible author IS the canonical one
   (the common single-author case) or the picture isn't a clean single author.
   Applied at every bare generic-struct head/alias site (annotation `.call` /
   `.parameterized_type_expr`, alias-registration `.call` /
   `.parameterized_type_expr`, array-literal head).

#2 A QUALIFIED head `a.Box(..)` whose namespace `a` authors no member `Box`
   silently fell back to the bare global template, instantiating an unrelated
   module's `Box`. Add `qualifiedMemberMissing`: a qualified head whose known
   namespace lacks the member now emits "namespace 'a' has no member 'Box'" and
   poisons with `.unresolved`; a qualified head NEVER reaches the bare global map.

Regressions: 0774 (bare head + bare alias, 2-hop same-name → size=8 alias=8,
fail-before 16 16); 0775 (qualified missing member → diagnostic + exit 1,
fail-before size=16 exit 0).
2026-06-08 18:39:53 +03:00
agra
8c59acbd25 fix(stdlib/E4): qualified generic alias head a.Box(..) selects the namespace author
The const-decl alias-registration path treated a qualified generic head
(`ABox :: a.Box(s64)`) only as a gate exemption, then read the bare last-wins
`struct_template_map` — so `ABox` and `BBox` both instantiated whichever
same-name template won globally (both size 16). attempt-9 routed the annotation
head sites through `qualifiedStructTemplate`; this applies the same selection to
the two alias-registration branches (.call and .parameterized_type_expr) before
the bare fallback, and extracts the shared instantiate-and-register logic into
`registerGenericStructAlias`.

ABox :: a.Box(s64) now resolves to a's template (size 8); BBox :: b.Box(s64) to
b's (size 16). Regression 0773 pins it (fail-before alias a=16 b=16, after a=8
b=16).
2026-06-08 17:56:29 +03:00
agra
eb7636d0f3 fix(stdlib/E4): qualified generic head ns.Box(..) selects the namespace author
A qualified generic type head `ns.Box(args)` was stripped to its bare name and
read from the last-wins `struct_template_map`, so the namespace qualifier never
selected the template author: `a.Box(s64)` and `b.Box(s64)` (two namespaces each
authoring a same-name `Box($T)` with different layouts) both instantiated the
global same-name template. The documented ambiguity escape hatch ("qualify it as
ns.Box") silently produced the wrong layout.

Select the template via the namespace edge (importer -> alias -> NamespaceTarget)
instead of the bare map, at both the .call and parameterized-type-expr head
sites. Two same-name templates instantiated with the same args would also collide
on the mangled name `Box__s64`, so tag the non-canonical author's mangled name
with its source (the canonical bare-map author keeps the untagged name -> no
churn for single-author generics).

Extract `buildGenericStructTemplate` so the bare registration and the new
namespace-qualified selection share one template builder.

Regression: examples/0772 — two namespaces each authoring Box($T) with different
layouts; ns_a.Box(s64) and ns_b.Box(s64) resolve to their own module's template
(sizes 8 and 16). Fail-before on 566de96 (a=16 b=16), pass-after (a=8 b=16).
2026-06-08 17:19:41 +03:00
agra
566de96821 fix(stdlib/E4): type-fn head gate selects the TYPE-FUNCTION author (ordinary fn must not vouch)
attempt-7 made the type-fn head gate kind-aware (a non-function no longer
vouches), but it still accepted ANY function author: a directly-visible
ORDINARY function (`Make :: () -> s32`, zero `$`-params) authorized a hidden
2-flat-hop type-function head (`Make :: ($T) -> Type`), so `size_of(Make(s64))`
silently instantiated the 2-hop type-fn and printed `size=8` at exit 0.

Narrow the author view from "any fn_decl" to "a TYPE-FUNCTION" via a new
`typeFnAuthor` predicate (`fnDeclOfRaw` + `type_params.len > 0`), the same
discriminator every instantiation site uses to recognize a type-fn head. Both
`flatFnAuthorVisible` and `flatFnAuthorAmbiguous` now count only type-fn
authors, so a same-name ordinary function — which cannot be the type head being
instantiated — does not vouch for a 2-hop type-fn head.

Regression 0771: main -> b (`Make :: () -> s32` ordinary fn + flat-imports c)
-> c (`Make :: ($T) -> Type`); `size_of(Make(s64))` -> "type 'Make' is not
visible", exit 1 (fail-before on 94c3cd7: size=8 exit 0). 0770 (non-fn vouch),
0769 (type-fn ambiguity), 0768/0767/0766-0763, 0208/0210 (valid type-fn heads),
0544/0706/0105 and FFI all green & byte-identical.
2026-06-08 16:43:01 +03:00
agra
94c3cd7507 fix(stdlib/E4): kind-aware type-fn head gate (non-fn must not vouch)
The type-fn head visibility check (`headFnLeak`) used the module-scope
NAME predicate `isNameVisible`, so a same-name 1-hop NON-function (a value
const `Make :: 123`) reported the name "visible" and let the global
`fn_ast_map` type-fn — whose real author is 2 flat hops away — silently
instantiate. `size_of(Make(s64))` printed 8 at exit 0 instead of a
visibility diagnostic.

Decide visibility from the ELIGIBLE FUNCTION authors directly reachable
from the use site (`flatFnAuthorVisible`, mirroring `flatFnAuthorAmbiguous`'s
fn-only author view): visible iff the own author or a 1-hop flat-import
author is a `fn_decl`. A non-function does not vouch. Guarded to fall open
when the import facts aren't wired (comptime / directory imports), mirroring
`headTypeGate`. Own / scope-local / 1-hop / directly-imported type-fn heads
still resolve; 0769 ambiguity unchanged.

Regression: examples/0770-modules-type-fn-head-non-transitive (main → b
[`Make :: 123` + flat-imports c] → c [`Make :: ($T) -> Type`]); the bare
`Make(s64)` head emits "type 'Make' is not visible", exit 1.
2026-06-08 16:03:23 +03:00
agra
cb9ef381b5 fix(stdlib/E4): own-wins at non-leaf bare-type sites + type-fn head ambiguity
attempt-6: address Adi's two in-scope findings (#3 deferred to E6).

#1 E4-own-author-type-arg (silent-wrong): the bare-TYPE gate returned
`.proceed` for the querying source's OWN author, so the non-leaf sites
(reflection / type-arg / array-literal / type-value / match arm) dropped it
and re-resolved a same-name flat import via global `findByName`. headTypeGate
now resolves the own author to ITS per-source TypeId (mirroring
selectNominalLeaf's own-wins, 0754); the type-as-value and type-match sites,
which only consumed the poison bit and re-resolved globally, now route through
the gate and use the `.resolved` author. size_of(Widget) with an own + imported
Widget now yields main's own size, not the import's.

#2 E4-type-fn-head-ambiguity (silent-wrong): headFnLeak only checked
isNameVisible, so two flat same-name type-returning functions both reported
"visible" and one was silently instantiated. It now diagnoses >=2 distinct
direct flat type-fn authors (no own author) as ambiguous before the
isNameVisible short-circuit, consistent with the parameterized struct /
protocol heads and the leaf (0755/0767). Own / single / diamond-collapse
type-fn heads still resolve.

Regressions: 0768 (own-wins at every non-leaf bare-type site, fail-before
reflection=16 -> pass-after 8) and 0769 (two flat Make type-fns -> ambiguity
diagnostic exit 1). README: own-wins + type-fn-head ambiguity at every bare-type
site.
2026-06-08 15:22:10 +03:00
agra
382f78f49b fix(stdlib/E4): carry full author outcome through the bare-TYPE gate (ambiguity at every site)
attempt-4 gated every bare-type-reference site for VISIBILITY via a boolean
leak-check that only caught not-visible and DROPPED the ambiguous outcome, so two
DIRECT flat same-name type authors (the 0755/0105 ambiguity case) fell through to
a global findByName / struct_template_map pick at the non-leaf sites.

Unified author-outcome fix (one path, every site consumes it):

- flatTypeAuthorCount: ≥2 distinct flat authors that do NOT all collapse onto one
  shared TypeId are now `.ambiguous` even when none carries a concrete TypeId yet —
  two same-name GENERIC TEMPLATES (template name registered in no findByName slot)
  are a genuine collision, exactly like two registered structs. Identical-target
  authors (diamond import / two aliases onto the same target) still collapse to
  `.one`, so all valid cases stay byte-identical.

- headTypeGate: the complete source-aware author outcome (.proceed / .resolved /
  .ambiguous / .not_visible) for an unqualified bare TYPE head, emitting the loud
  ambiguity diagnostic (consistent with the leaf / 0755) or the not-visible
  diagnostic. headTypeLeak is now its poison-vs-proceed projection, so every head /
  instantiation / alias-decl / match site poisons on ambiguity with the right
  message. Reflection / type-arg and array/vector-literal identifier heads consume
  `.resolved` to use the source-keyed TypeId, never a global findByName pick.

Regression examples/0767: size_of(Thing) / Nums.[1,2] / Box(s64) / t:Type=Thing /
case Thing: with two direct flat same-name authors each emit the ambiguity
diagnostic, exit 1 (fail-before on bb8f7dc: exit 0 / cascade). 0763/0764/0765/0766
/0755/0706/0544/0105 + FFI byte-identical. README: bare-type ambiguity is enforced
at every reference site.
2026-06-08 14:15:34 +03:00
agra
bb8f7dc5ec fix(stdlib/E4): route reflection/literal/value/match bare-type sites through the non-transitive gate
attempt-3 closed the leaf + parameterized-head leaks but several more
sites still resolved an UNQUALIFIED type name via the global
type_alias_map / findByName / type_bridge.resolveAstType without the
single-hop visibility gate, so a 2-flat-hop bare type leaked through:

  - resolveTypeArg (reflection / size_of / align_of / type_name / type_eq):
    identifier + type_expr leaves now gate via headTypeLeak; the wrapped /
    structural forms (*T, [N]T, []T, ?T, fn-ptr, tuple) route through the
    already-gated resolveTypeWithBindings so each inner leaf recurses the
    source-aware resolveNominalLeaf.
  - resolveTupleLiteralTypeArg: each element leaf is resolved through the
    source-aware resolver before the delegated build, so (COnly, s64) is
    gated.
  - resolveArrayLiteralType (T.[...] typed array/vector-literal head):
    identifier + type_expr leaves gate via headTypeLeak.
  - type-as-value lowerExpr identifier (x: Type = COnly, x == COnly).
  - type-category match arm (case COnly:).

Qualified ns.X / 1-hop / source-pinned library-internal references stay
exempt (the gate falls through for reachable / unauthored names, and
returns the existing "unresolved type" diagnostic for genuinely-undeclared
names). README notes the type gate holds wherever a bare type name is
named. New regressions 0765 (2-hop reject) / 0766 (1-hop pass).
2026-06-08 13:18:51 +03:00
agra
4f99fb0d85 fix(stdlib/E4): gate unqualified parameterized type heads non-transitively
attempt-3: extend the E4 single-hop bare-TYPE gate to parameterized type
HEADS (the constructor-head analog of the bare-leaf gate). Before this, the
head lookup hit the global struct_template_map / protocol_ast_map /
fn_ast_map *before* any source-aware visibility check, so a 2-flat-hop
imported generic struct/protocol/type-fn remained bare-visible (e.g.
`Box(s64)` when main imports only b.sx and b.sx imports c.sx).

- headTypeLeak: generic-struct / parameterized-protocol heads use the same
  type-author single-hop model as the bare-leaf gate (moduleTypeAuthor +
  flatTypeAuthorCount + localTypeInSource + nameAuthoredAsTypeAnywhere).
- headFnLeak: type-returning-function heads use single-hop function
  visibility (isNameVisible), exempting scope-local mangled type-fns.
- Gated at every unqualified head site: resolveParameterizedWithBindings,
  resolveTypeCallWithBindings, the scanDecls alias-decl dispatch (poisoning
  the alias with .unresolved on leak), resolveArrayLiteralType, and the
  generic-static-method call path. Namespaced (`ns.Box(..)`) heads are an
  explicit qualified reach and stay exempt. Source-pinned instantiation
  (E3/E4) is preserved, so library-internal heads still resolve where they
  are visible.

Regression: examples/0764-modules-import-generic-head-non-transitive
(2-hop `Box(s64)` -> "type 'Box' is not visible", exit 1; direct #import
resolves). Fails-before on a250964 (printed 3), passes-after.

README: note the non-transitive rule covers parameterized type heads.

Gate: zig build 0, zig build test 0 (LSP 522, 423/423), run_examples
505/0, FFI 12xx/13xx/14xx green, 0706/0763/0544/0105 green & byte-identical,
m3te ios-sim build+launch exit 0.
2026-06-08 12:37:00 +03:00
agra
a250964ced fix(stdlib/E4): source-pin pack-fn fixed-prefix param types to the defining module
E4's pack-fn source-pin was incomplete: an imported pack function's
fixed-prefix (non-pack) parameter types were resolved in the CALLER's
module, so a param whose type is bare-visible only in the pack fn's own
module was wrongly rejected with "type 'X' is not visible" — even though
the equivalent plain fn (typed via the source-pinned call-arg path) ran
fine.

Two sites in the pack-mono path re-resolved the fixed-prefix param type
in the caller's context:
  - lowerPackFnCall: the call-site arg-typing pass (to contextually type
    the arg from its param) — fires first.
  - monomorphizePackFn: the body parameter binding, after the caller
    source was restored from the signature build.

Both now resolve via resolveParamTypeInSource(fd.body.source_file, &p),
pinning to the pack fn's defining module — matching the already-pinned
signature build, the body lowering, and the cross-module call-arg typing
sites. The call-site arg itself is still lowered AFTER, in the caller's
context (issue 0106).

Regression: examples/0544-packs-imported-pack-fn-fixed-param-source-pin
(main -> lib -> dep; `Needs` two flat hops away, never named in main).
Fails pre-fix with "type 'Needs' is not visible"; passes after. A control
plain fn in the same lib already ran, isolating the pack-mono path.
2026-06-08 11:52:23 +03:00
agra
33a6f5c650 wip(E4): partial source-pin + non-transitive flip [stdlib E4 attempt-1 WIP checkpoint]
Incomplete WIP from a worker killed at the 55-min wall (large blast radius:
core source-pin + ~8 example migrations + ~10 library module migrations).
Committed so the resumed session continues on a clean tree. May not build.
2026-06-08 11:12:08 +03:00
agra
3816bfff47 fix(diag): source-key local_type_names so a caller block-local can't leak into an imported template field [stdlib E3 attempt-5]
A block-local type is visible only within the source that declares it. The
global `local_type_names` set was source-insensitive, so an imported generic
template's field (resolved in the template's source context, attempt-4) could
bind a type the CALLER declared block-local — silently compiling an undeclared
imported field instead of diagnosing it.

Key `local_type_names` by declaring source. The bare-TYPE gate now resolves a
local only when the query originates in the local's own source (R2 preserved);
a same-name block-local of a DIFFERENT source routes to the undeclared path so
the leak surfaces (`unknown type '...'`, exit 1) instead of escaping via the
`registered` catch-all that would otherwise resolve the globally-registered
cross-source local.

Regression: examples/0762 — imported `Bad :: struct($T) { x: T; y: LocalOnly; }`
with `LocalOnly` declared only in the caller `main` now errors in lib.sx
(fail-before on 8162170 printed `1 9` exit 0).
2026-06-08 10:02:33 +03:00
agra
81621703ca fix(diag): imported generic struct field with bad type → diagnostic, not .unresolved/silent stub [stdlib E3 attempt-4]
attempt-3 closed the MAIN-file value-param-as-type quadrant (0172) in the
UnknownTypeChecker, but the checker only walks main-file decls — an IMPORTED
generic struct's field with a bad type name was never checked. Worse, the
generic-struct INSTANTIATION resolved its field type nodes in the (possibly
cross-module) instantiation site's source context, not the template's module.
So for `Bad :: struct($N: u32) { x: N; }` declared in an imported module and
used as `Bad(3)` from main, the field `x: N` resolved against the main file:
the value-param-as-type leaf poisoned it with `.unresolved` and PANICKED at
LLVM emission, and the genuinely-undeclared sibling (`y: Missing` in a generic
import, distinct from the non-generic 0759 case) silently fabricated a 0-field
stub.

Root cause + uniform fix: capture the declaring module on each StructTemplate
and resolve its field type nodes in THAT source context during
instantiateGenericStruct. The source-aware nominal leaf then classifies main vs
imported by the TEMPLATE's file, so both failure modes are diagnosed at the
right authority for every quadrant — main + imported, undeclared name + value
param used as a type:
- imported `.undeclared` field → the existing leaf emits "unknown type 'X'"
  (now reached because `from` is the template's module, not main).
- imported value-param-as-type → the `is_generic` leaf, when the name is bound
  as a comptime VALUE (`comptime_value_bindings`), emits the tailored
  "'N' is a value parameter, not a type" hint (gated to non-main; the
  UnknownTypeChecker owns the main-file case). Caught in every type position
  (`x: N`, `*N`, `[3]N`, `?N`). A genuinely-unbound type param (`$R`) stays a
  silent `.unresolved`.

No `.unresolved` reaches LLVM for these cases (hasErrors halts after lowering);
the emit_llvm `.unresolved` @panic tripwire stays as the last-resort sentinel.
Valid value-param VALUE positions (`[N]u8` dim, `Vector(N,T)` lane) and
`$T:Type`/`$T:Protocol` type-param fields still resolve.

Regressions:
- 0760-modules-imported-generic-value-param-as-field-type (panic-before / clean
  diagnostic-after).
- 0761-modules-imported-generic-undeclared-field (silent-compile-before / clean
  diagnostic-after).
0171/0172/0759 stay green; main-file quadrants emit exactly one error.

Gate: zig build; zig build test (423/423 + LSP corpus sweep); run_examples 501
passed / 0 failed (prior 499 byte-identical); m3te ios-sim build exit 0.
2026-06-08 09:37:52 +03:00
agra
a0390a63ab fix(diag): generic VALUE param ($N: u32) used as a field/annotation type → diagnostic (no .unresolved LLVM panic) [stdlib E3 attempt-3]
The generic-struct field checker (attempt-2) accepted ALL struct type
params as valid type-name leaves, including VALUE params. The parser
marks any reference to a struct's own param `is_generic` (so `x: T`
resolves without `$`), and it marks a value param `$N: u32` the same
way — so `Bad :: struct($N: u32) { x: N; }` instantiated `Bad(3)` slipped
past the unknown-type walk, resolved the field's type leaf to the
`.unresolved` sentinel, and panicked at LLVM emission instead of
diagnosing.

Distinguish TYPE params (`$T: Type`, `$T: SomeProtocol`, the `..$Ts`
pack) from VALUE params (`$N: u32`) using the binder's own classification
rule (lower.zig). A value param named in a type position now gets the
tailored "'N' is a value parameter, not a type" hint, exit 1, before
codegen. Two dispatch paths covered: the `is_generic` struct-field path
(reportIfValueParamInTypePosition) and the non-generic annotation path
(reportIfUnknownType in-scope filter). A value param in a VALUE position
(array dim `[N]u8`, `Vector` lane) still resolves.

Regression: 0172-types-value-param-as-field-type (panic-before / clean
diagnostic-after). 0171 and 0759 stay green; 499 markers, prior
byte-identical.
2026-06-08 09:02:54 +03:00
agra
a4906975bd fix(diag): undeclared type in a main-file generic struct field → diagnostic (no silent stub) [stdlib E3 attempt-2]
Closes the main-file carveout left by attempt-1 (4072689): a genuinely-
undeclared type used as a field type inside a MAIN-file GENERIC struct still
fell through the type leaf's empty-struct stub and silently compiled —
`Box :: struct($T: Type) { good: T; bad: MissingType; }` with `b : Box(s64)`
exited 0 and printed a value instead of reporting `unknown type 'MissingType'`.

Root cause: `UnknownTypeChecker` is the main-file diagnostic authority (the
type leaf defers to it for `.undeclared` names there), but
`checkStructFieldTypes` SKIPPED every generic struct outright ("its fields
reference `$T`, resolved at instantiation"), so the undeclared name was never
examined. The sibling `walkBodyTypes` `.struct_decl` arm skipped body-local
generic structs the same way.

Fix (semantic_diagnostics.zig, checker only — no leaf change):
- `checkStructFieldTypes`: stop skipping generic structs; walk the field
  types with the struct's OWN type params (`$T`, `$N`, `..$Ts`) passed as the
  in-scope set. A param name resolves; any OTHER bare name that is neither
  declared nor a generic param is reported. Value-param positions (a `Vector`
  lane count, a `$N: u32` arg) are still skipped inside
  `checkTypeNodeForUnknown` / `isValueParamPosition`.
- `walkBodyTypes` `.struct_decl`: same close for body-local structs — the
  local struct's own type params join the enclosing scope's in-scope params
  (so it can name both the outer fn's `$T` and its own), any other bare field
  type is still flagged.

The `..$Ts` pack field `(..$Ts)` parses to a `spread_expr` inside the tuple,
which hits `checkTypeNodeForUnknown`'s `else` arm — never walked — so the pack
examples (0538-0543, 0414) stay green. The checker walks only MAIN-file decls,
so library generic structs (List, Map) are untouched.

Regression: examples/0171-types-undeclared-type-in-generic-struct-field — the
reviewer's exact shape; `unknown type 'MissingType'` at the field, exit 1.
Fail-before on 4072689 (prints 7, exit 0), pass-after.

Gate: zig build; zig build test (423/423 + LSP corpus sweep 514); run_examples
498 passed / 0 failed (prior 497 byte-identical); m3te ios-sim build exit 0.
2026-06-08 08:36:45 +03:00
agra
4072689afe fix(lower): genuinely-undeclared type → diagnostic + .unresolved (no silent stub) [stdlib E3]
Phase E3: remove the silent empty-struct fall-throughs in type resolution for
genuinely-undeclared names, replacing them with a real "unknown type" diagnostic
+ the dedicated `.unresolved` sentinel (already present, with the sizeOf @panic
tripwire) — the REJECTED-PATTERN this project bans.

Split `TypeHeadResolution.undeclared` into `.forward` (a real author not interned
yet — self/forward/mutual/foreign reference, adopted on registration via
internNamedTypeDecl) vs `.undeclared` (NO author anywhere). `resolveNominalLeaf`:
- `.pending` / `.forward` keep the empty-struct stub the type adopts on register.
- `.undeclared` in a NON-main (imported/library) module — which the
  UnknownTypeChecker trusts and never walks — emits "unknown type 'X'" + poisons
  with `.unresolved`. In the MAIN file the checker owns the diagnostic (and a
  valid unbound generic leaf legitimately lands here), so the leaf keeps the
  legacy stub and does not double-report.

Also convert the `parameterized_type_expr` constructor-head fallback
(resolveParameterizedWithBindings): an unresolvable base now emits + returns
`.unresolved` (mirroring the `.call`-node sibling) instead of a 0-field stub
that mis-sizes `b.field` / `b.len`. Threads the reference span through both
callers.

Triage of the other empty-struct sites (all load-bearing on the green suite or
unable to distinguish forward from undeclared — KEPT): resolveNamed's legacy
namer (forward/generic/Self/foreign-opaque: R/Self/Object/Array), the
foreign-class struct + JNI Self placeholders, the shadow-slot reservation, the
type_bridge stateless pack/generic namer, and the struct-literal inference
fallback (front-run by the leaf; 0 suite hits).

Regression: examples/0759-modules-undeclared-type-in-import — an undeclared type
in an imported module now errors (exit 1) instead of silently compiling (the
pre-fix code printed `thing.x = 42`, exit 0).

Gate: zig build; zig build test (423/423 + LSP corpus sweep); run_examples 497
passed / 0 failed (prior 496 byte-identical); m3te ios-sim build exit 0.
2026-06-08 08:10:42 +03:00
agra
f8efa25416 revert(stdlib): narrow E2 to the 0105 type/alias close; defer value consts to E5 [stdlib E2 attempt-6]
Scope-narrowing revert of the value-const same-name sub-area (attempts 3-5),
per PO/Agra ruling. The 0105 type/alias close (per-source nominal struct
identity, source-keyed type aliases, F1 self/mutual refs, anon-struct
regression) is kept intact; cross-module same-name VALUE consts move to step E5.

- imports.zig: narrow `isPerSourceDecl` so a `const_decl` is retained
  per-source ONLY when its RHS introduces a TYPE (alias / inline type decl).
  VALUE consts (literal / value-expression RHS) and functions keep the pre-E2
  first-wins name-merge. Restores value-const reads to exactly the
  wt-stdlib-base (pre-E2) first-wins behavior.
- lower.zig / program_index.zig: restored to the pre-value-const state
  (66d10c0) — removes selectModuleConst / SourceConstCtx / pinConstAuthorSource
  / SelectedConst and the rewired comptimeIntNamed / float / runtime /
  global-init const reads; value-const reads return to the global path.
- examples: drop 0759-0762 (value-const own-wins / ambiguous / expr-chain-dim
  / leaf-author-pin) — they move to E5.

Kept green: 0752-0758 (same-name structs distinct + own-wins + ambiguous + self
/mutual ref), 0756 (alias per-source), 0170 (anon-struct field distinct).

Gate: zig build + zig build test (423/423, LSP sweep 513 no-crash) +
run_examples (496/0, prior markers byte-identical) + m3te ios-sim build exit 0.
2026-06-08 07:28:31 +03:00
agra
4666fb1941 fix(lower): pin nested-leaf source to the SELECTED const's author — close F1 R1 [stdlib E2 attempt-5]
A same-name expression const read from another module folded its nested
leaves (`M` inside `K :: M + 1`) from the CALLER's source, not the source
that authored the selected const. A unique imported `K` became ambiguous
when the reading module also flat-imported a different same-name `M`.

`selectModuleConst` now returns the author SOURCE alongside the const info
(`SelectedConst`), and the fold/lower of a selected const's RHS pins
`current_source_file` to that author for the duration (`pinConstAuthorSource`)
— so `K :: M + 1` defined in `a.sx` always folds `M` against `a.sx`,
coherently whether `K` is read as a runtime value or used as an array
dimension. Each recursion level pins to its own selected author's source.

Single-author programs pin to the source they were already in → byte-
identical (499 prior examples unchanged). Genuine ambiguity at the read
site (0760) is still caught before any pin.

Regression: examples/0762-modules-same-name-const-leaf-author-pin
(`a.sx M::1; K::M+1`, `b.sx M::10`, main flat-imports both, reads K as
value AND `[K]u8` dimension → val=2 len=2). Fail-before on 8518b66
(`'M' is ambiguous` / "array dimension must be a compile-time integer
constant"), pass-after.
2026-06-08 07:02:59 +03:00
agra
8518b66cec fix(lower): propagate source-aware const selection into expression-chain folds — close F2 R1 [stdlib E2 attempt-4]
attempt-3 made the value-const READ source-aware (own-wins / ambiguous) but
the dimension/count fold of a SELECTED const's RHS still recursed through the
global last-wins `module_const_map`, so a nested same-name leaf came from the
wrong module. Reviewer R1: a.sx `M::1; K::M+1`, b.sx `M::10; K::M+1`, with both
`[K]u8` (a_len) and `return K` (a_val) — pre-fix `a_len=11 a_val=2`, an
INCOHERENCE for the same const `K` (a_val read A's chain; a_len read B's `M`).

`comptimeIntNamed` delegated to `moduleConstIntWith(global_map, ...)`, whose
leaf ctx (`ModuleConstCtx`) resolved nested names through the global map. The
value path (`emitModuleConst` -> `foldCountI64(ci.value, self)`) folds through
`self`, so its leaves bounce back to the source-aware `comptimeIntNamed` — which
is why a_val was already correct.

- New `SourceConstCtx` (lower.zig): the leaf-resolution twin of `ModuleConstCtx`,
  but every nested const leaf re-selects its OWN source author via
  `selectModuleConst` (own-wins / ambiguous), never the global last-wins map.
  `ConstFoldFrame` cycle-guards a const whose RHS references another const.
- `comptimeIntNamed` / `lookupFloatName` / `nameIsFloatTyped` now fold the
  selected `ci`'s RHS through `SourceConstCtx` (via `foldSourceConstInt` /
  `foldSourceConstFloat` / `sourceConstIsFloatTyped`). This makes the dimension
  and value reads of a shadowed expression-chain const coherent.
- Drop the now-unused `moduleConst{Int,Float,IsFloatTyped}With` wrappers from
  program_index.zig; expose `isCountableConstType` / `isFloatConstType`.

Single-author -> byte-identical (the selected `ci` IS the global one and every
nested leaf has one author). The stateless `type_bridge` registration-time const
reader still folds leaves through the global map, but realistic dim sites (struct
fields, array aliases — probed) resolve via the stateful path and stay coherent
under import-order swaps; no reachable wrong-dimension found (tracked follow-up,
byte-identical single-author).

Regression: examples/0761-modules-same-name-const-expr-chain-dim — a_len=2
a_val=2, b_len=11 b_val=11. Fail-before on 72f06a1 (`a_len=11`), pass-after.

Gate: zig build + zig build test (423/423, LSP sweep 515 clean) + run_examples
(499/0, 498 prior byte-identical + 0761) + m3te ios-sim build exit 0.
2026-06-08 01:06:44 +03:00
agra
72f06a109b fix(lower): source-aware value-const resolution (own-wins / ambiguous) — close F2 [stdlib E2 attempt-3]
E2 retained per-source const declarations but left the const READ path on the
global last-wins `module_const_map`, so a module's OWN reference to a same-name
const bound the LAST global author (F2: a.sx `K::1`, b.sx `K::2`, main flat-imports
both → both read B's K). Complete the const analog of the type (`selectNominalLeaf`)
and callable (`selectPlainCallableAuthor`) source-aware models.

- `selectModuleConst`: own-wins; exactly one flat-visible author → it; ≥2 distinct
  flat-visible → `.ambiguous` (loud diagnostic, consistent with 0755/0724); none
  → `.none`. Reads the SELECTED author's per-source value (`module_consts_by_source`)
  and folds its RHS over the global leaf map, so a const-EXPRESSION chain
  (`N :: M + 1`, M flat-imported) still resolves M.
- Rewire `comptimeIntNamed` / `lookupFloatName` / `nameIsFloatTyped`, the runtime
  identifier path, and the global-init-from-const path through it; drop the now
  subsumed `moduleConstBareInvisible` gate.
- program_index: `moduleConst{Int,Float,IsFloatTyped}With` fold a selected `ci`.

Examples: 0759 (own-wins value const, a=1 b=2) + 0760 (two-flat-visible →
ambiguous). Single-author byte-identical (run_examples 498/0, 496 prior unchanged;
zig build test 423/423; corpus sweep 515 no-crash; m3te ios-sim exit 0).
2026-06-08 00:32:07 +03:00
agra
66d10c00bb fix(lower): reserve genuine same-name struct shadows before fields — close F1 [stdlib E2 attempt-2]
A self / forward / mutual reference inside a same-name struct shadow bound to
the FIRST same-name author (another module's struct) instead of its own nominal
TypeId: registerStructDecl resolved a shadow's field types BEFORE registering its
decl key in type_decl_tids, so namedRefTid fell through to the name-only
findByName first-author fallback (F1).

Fix: a genuine same-name struct shadow (≥2 DISTINCT struct decls author the name
in the scanned decl set) reserves ALL its authors' distinct nominal slots up-front
in scanDecls — the first at id 0, the rest at fresh nonzero ids — BEFORE any field
resolves. Every self / forward / mutual ref to the shadow name then resolves via
type_decl_tids to its OWN nominal TypeId.

Gating on the scanned decls, not nameHasMultipleTypeAuthors (the raw import facts
over-count a single file reached via two un-normalized import spellings, e.g.
math/matrix44), keeps single-real-decl names on the legacy id-0 post-field path —
byte-identical (494 prior markers unchanged, single-author old==new).

internNamedTypeDecl now takes the precomputed nominal_id; no-drift + single
graph-walk invariants untouched; generics / enum / union / error-set stay legacy.

Regressions: 0757 (self-ref *Box → reads B's own field), 0758 (forward + mutual
*Node/*Box between two shadows). Fail-before on d98ad5c
("field 'y'/'m' not found"), pass-after.
2026-06-07 23:51:46 +03:00
agra
d98ad5c14f feat(stdlib): per-decl nominal identity + same-name shadows — close 0105 [stdlib E2]
Make same-name top-level types in different sources DISTINCT nominal types
instead of collapsing last-wins in the type table (issue 0105).

Registration:
- internNamedTypeDecl assigns a per-decl nominal_id and populates
  type_decl_tids. The first author of a name keeps nominal_id 0 (byte-identical
  to pre-E2); a genuine cross-module shadow (>=2 distinct normalized-path
  authors per the import facts) gets a fresh id -> a distinct TypeId.
- mergeFlat/addOwnDecl stop first-wins-dropping per-source decls (named types +
  non-fn const_decls) so every same-name author reaches registration; functions
  and var_decls (incl. #foreign extern globals) keep first-wins.

Resolution (selectNominalLeaf):
- own-author wins; else flatTypeAuthorCount over the transitive flat closure:
  >=2 distinct -> .ambiguous (loud diagnostic + poison); exactly one -> resolved;
  a flat author not yet findByName-registered -> .undeclared stub (not a leak).
- struct-literal type names route through the same source-aware leaf.
- lazyLowerFunction pins the function's own source before resolving its return
  type, so a shadowed signature type resolves in its module, not the caller's.

Codegen:
- mangleTypeName appends __n<id> for nonzero nominal_id so same-name shadows get
  distinct monomorph symbols (struct_to_string__Box vs __Box__n1).

Library hygiene:
- rename trace.sx's compiler-contracted Frame -> TraceFrame (+ the two compiler
  findByName sites) so it never collides with a UI/geometry Frame; the layout is
  structural (getFrameStructType / SxFrame), name-independent.

Examples: 0752-0756 pin the five 0105 cases (distinct fields / same fields /
own-wins / ambiguous / alias per-source); 0170 pins the folded anon-struct-field
regression.
2026-06-07 22:57:28 +03:00
agra
d2eb4c2af4 fix(lower): source-aware initial scan registration for identifier-RHS aliases [stdlib E1.5 attempt-2]
E1.5 attempt-1 made the forward-alias FIXPOINT source-aware but left the
EARLIER path — the `scanDecls` identifier-RHS alias branch — resolving the
RHS through the GLOBAL `type_alias_map` / global `findByName` (last-wins
across modules). When a namespaced import is scanned BEFORE a forward alias
`A :: B; B :: u64;`, dep's same-name `B :: u8` already sits in the global map,
so the early scan bound `A` to dep's `u8` and the per-source fixpoint guard
(`aliasResolvedInSource`) then skipped `A` — re-opening 0105 one layer down
(reviewer R1).

Cut the scan registration over to `selectNominalLeaf(rhs, src, is_raw)`,
resolving `B` AS SEEN FROM the alias's OWN source. Only the `.resolved`
outcome is written via the unified `putTypeAlias`; `.pending` / `.undeclared`
/ `.not_visible` leave `A` UNWRITTEN so the source-aware fixpoint re-tries it
once the local `B` registers. No raw `type_alias_map.put` / global `findByName`
selection reintroduced (E1 no-drift invariant). resolver.zig untouched
(single graph-walk invariant).

Also thread the backtick raw flag (`identifier.is_raw`) into BOTH the scan
registration and the fixpoint `selectNominalLeaf` calls, so a raw-RHS alias
(`` RawAlias :: `s2 ``) resolves to the nominal `` `s2 `` author, not the
builtin `s2` spelling (fixes 0154 under the new scan path; closes the same
latent hardcode in the fixpoint).

Regression: examples/0751-modules-forward-alias-ns-before — the reviewer's
exact ordering (ns import with `B :: u8` BEFORE `A :: B; B :: u64;`). Fails
on 2d34993 (`forward A` = 44, dep's u8) and passes after (= 300, local u64).
0750 + 0132/0133 + the full suite stay byte-identical (488/0).
2026-06-07 21:12:33 +03:00
agra
2d34993586 feat(lower): source-aware forward-alias fixpoint [stdlib E1.5]
resolveForwardIdentifierAliases now resolves a forward alias A :: B against
B AS SEEN FROM A's own source via selectNominalLeaf (E1's source-keyed
nominal leaf over type_aliases_by_source / moduleTypeAuthor), never the
global type_alias_map / global findByName. The already-resolved guard is
per-source (aliasResolvedInSource). .pending routes back into the fixpoint;
.undeclared / .not_visible leave A unwritten (no global last-wins leak).

This is the sequencing pin before E2: a global fixpoint binds A to a
same-name B authored by a different module (e.g. a namespaced import that
pollutes the global alias map last-wins), re-opening 0105 one layer down
once shadows register. Writes stay on the unified putTypeAlias helper (E1
no-drift invariant); the single graph-walk in resolver.zig is untouched.

Regression: examples/0750-modules-forward-alias-source-aware — a forward
alias A :: B with main's own B :: u64 and a namespaced same-name B :: u8;
A must bind main's u64 (300), not the global last-wins u8 (44).
2026-06-07 20:43:01 +03:00
agra
78ef2ea3d8 fix(lower): single unified writer for the three decl-fact maps; close param-alias leak [stdlib E1 attempt-5]
Route EVERY write of type_alias_map / module_const_map / global_names (and
their *_by_source analogues) through one helper per map
(putTypeAlias/putModuleConst/putGlobal/dropModuleConst). The global put and the
by-source put are now inseparable, so no write-site can mirror one side and
miss the other — the dual-write drift that leaked ns-only aliases past the
source-aware bare-TYPE gate. Grep-clean: no raw .put/.remove to the three maps
outside the helpers (mirrors the no-raw-TypeTable.update discipline).

The generic-struct instantiation alias sites (Secret :: Box(s32), both the
.call and .parameterized_type_expr branches) previously registered only a named
struct in the TypeTable and never reached type_aliases_by_source, so
moduleTypeAuthor missed them and a bare ns-only use leaked (exit 42, no
diagnostic). Routing those writes through the unified putTypeAlias lands the
alias in the per-source cache and the leak closes BY CONSTRUCTION — a flat use
still resolves to the same TypeId findByName would, a ns-only use is rejected.

Regression 0749 (ns-only Secret :: Box(s32) bare -> "type 'Secret' is not
visible"): fail-before on daf4bbc exit 42 no diagnostic, pass-after exit 1.
Single-author resolution byte-identical (486 passed / 0 failed). resolver.zig
single graph-walk untouched; generic/param-protocol/Vector/type-fn stay legacy.
2026-06-07 19:31:13 +03:00