headTypeGate and bareVisibleStructDecl were using the same
moduleTypeAuthor + flatTypeAuthorCount pattern that selectNominalLeaf
used before R2. Migrated both to a single collectVisibleAuthors call
with inline type-specific resolution, matching the R2 pattern.
Deleted now-unused helpers: moduleTypeAuthor, FlatTypeAuthor,
moduleTypeAuthorTid, FlatTypeAuthorCount, flatTypeAuthorCount.
Net: -76 lines.
541/541 regression tests pass. 426/426 unit tests pass.
Replaces the 3 separate author-collection calls inside selectNominalLeaf
(moduleTypeAuthor + ownConstDeclIsPendingAlias + flatTypeAuthorCount +
forwardAliasOrUndeclared) with a single collectVisibleAuthors call plus
inline type-specific resolution. The flat walk now handles:
- own named type: resolved or forward (slot not yet interned)
- own const_decl: resolved alias or pending (own wins over flat)
- flat named types: ambiguous / resolved / forward
- flat const_decl pending aliases: pending (for forward aliases in imports)
Deletes 3 now-unused helpers: forwardAliasOrUndeclared, constAuthor,
ownConstDeclIsPendingAlias. Net: -17 lines.
541/541 regression tests pass. Issue 0107 repro still outputs 300.
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.
Removes the S2.x pre-pass and its 10 NodeRefTable maps — 1934 net lines
deleted. The Resolver gains two lazy functions: resolveBare(name, from,
domain) and resolveQualified(target, name), each returning ResolvedAuthors
(verdict + author set). verdictOver and authoredAsDomainAnywhere move from
ResolvePass to Resolver as private methods. All domain-predicate helpers
(eligibleKind, structDeclOf, fnDeclOf, etc.) are promoted to pub.
Test file trimmed from 1352 to 396 lines; old pre-pass population tests
replaced by focused resolveBare / resolveQualified verdict tests.
540/540 regression tests pass. Zero behavior change.
Move the author-SELECTION SEMANTICS (the verdicts) into the resolver. Every
`.authors` ResolvedRef now carries the verdict the resolver COMPUTES above the
collector — own-wins / single-flat-visible / ≥2-ambiguous / not-visible /
type-vs-value domain-filtered — evaluated over the DOMAIN-ELIGIBLE subset of the
collected author set (`eligibleKind`). This folds the per-kind selection the old
lower-side selectors carried (selectNominalLeaf / flatTypeAuthorCount /
selectModuleConst / selectPlainCallableAuthor / selectGenericStructHead /
headTypeGate / headFnLeak) into ONE uniform computation, closing the
protocol / error-set / foreign per-kind surfaces (E6c/d/e) as resolver behavior.
Template/pack grammar stays carried as `.template` / `.pack` refs — NO
`sig_registration_mode`.
ADDITIVE / PARALLEL / UNCONSUMED: lowering still reads the old selectors, so the
verdict changes no generated byte. No file outside resolver.zig reads
ResolvedRef, so byte-identity is structural. ResolvedRef.authors is wrapped into
{ set, verdict } (the RAW set is preserved; the verdict filters).
Resolver unit tests prove the verdicts on real Phase A facts: the five bare-type
outcomes incl. the type-vs-value filter, and the resolver-target classes the old
selectors get WRONG — 0811 error-set / 0821 protocol-head / 0829 generic-struct
all → ambiguous (two flat authors, none own) and → own-wins (own author present).
The resolver-target corpus stays xfail in run_examples (unconsumed until S3.9);
verdicts asserted via the harness, not by flipping goldens.
Gate: zig build && zig build test (430) && tests/run_examples.sh (540 byte-identical),
all exit 0; tests/resolver-target 18 xfail unchanged.
On the S2.1a owning traversal, populate the last three ResolvedProgram side
tables, closing planspec S2.1's full-population acceptance (all ten domains):
- foreign_class_refs: a bare reference whose collected author is a
foreign_class_decl is routed here (its own domain) instead of the bare
type/value/callable table.
- struct_const_refs: a Type.CONST field access whose base resolves to a
struct author carrying that const member (mirrors lowering's struct_const_map).
- ufcs_refs: a ufcs_alias decl (alias -> target author) plus its UFCS-rewrite
call sites (alias(args), incl. pipe-desugared), via a global traversal-ordered
alias map mirroring lowering's flat ufcs_alias_map.
Still PARALLEL / UNCONSUMED / RAW: lowering reads the OLD selectors, no consumer
cut over, ResolvedRef stays raw. Byte-identical vs baseline (540 examples).
Population proof extended: a new resolver.test fixture exercises all ten domains
at once and asserts each side table non-empty + node-keyed for the three new ones.
On the S2.1a exhaustive traversal, populate four more ResolvedProgram side
tables, still RAW / PARALLEL / UNCONSUMED:
- namespace-qualified references: an `alias.member` field_access whose base
alias is a NamespaceEdges[ambient_source] target resolves via
collectNamespaceAuthors into namespace_refs, keyed by the access node.
- the three HEAD domains at parameterized_type_expr heads, binned by the
resolved author's decl kind: a struct with type params -> generic_struct_heads,
a fn/const-wrapped fn with type params -> type_fn_heads, a protocol ->
protocol_heads. RAW: the whole author set is recorded with no winner picked;
a name authored as >1 head kind lands a distinct entry in every matching table.
Lowering still reads the old selectors and resolved_program has no consumer, so
generated output is byte-identical. ResolvedRef stays RAW (selection is S2.2);
generics stay symbolic. S2.1c (foreign-class / struct-const / UFCS) owns the
remaining three tables.
Extends the population proof: a resolver unit test asserting all four tables are
non-empty + node-keyed with the expected RAW authors.
Gate (all exit 0): zig build; zig build test (All 427 mod + exe + LSP sweep 574);
tests/run_examples.sh (540 passed, byte-identical); tests/resolver-target
(18 xfail, 0 leaked); m3te ios-sim via the main sx binary.
Turn src/ir/resolver.zig from a raw author-collection facade into the OWNING
resolution pass: one exhaustive recursive AST walk (exhaustive switch over
ast.Node.Data with NO else arm, so a new node kind is a compile error here
rather than a silently unvisited subtree) populating a ResolvedProgram.
- ResolvedProgram: all 10 node-keyed side tables declared as
AutoHashMap(*const ast.Node, ResolvedRef) + symbolic TemplateParamId/
PackParamId registries. ResolvedRef is the S2.1 RAW form — collected author
identity (AuthorSet, own ∪ flat), NO verdict (own-wins/ambiguity is S2.2).
- Populate the 3 bare-name domains (type / value-const / callable heads) via
collectVisibleAuthors(.user_bare_flat); record $T / ..$Ts / $pack[i] as
SYMBOLIC template/pack refs, never TypeIds. The 7 head/qualified/foreign
domains stay declared-but-empty (S2.1b/c own them).
- Slot via Compilation.resolveProgram() after the program_index facts are
wired and before lowerRoot; ResolvedProgram owned on Compilation, borrowed
*ResolvedProgram lent to ProgramIndex (lowerToIR signature unchanged).
- Population proof unit test over real Phase A facts: the 3 tables are
non-empty, keyed by node identity, and carry symbolic template/pack refs.
ADDITIVE / PARALLEL / UNCONSUMED: lowering still reads the OLD selectors, so
single-author output is byte-identical. Gate green: zig build; zig build test
(425/425, LSP smoke 574 files no crash); run_examples (540 passed, 0 failed,
byte-identical incl. FFI 12xx-14xx + 1615 ios-sim); resolver-target (18 xfail
unchanged).
Delete module_fns as a separate function-author fact source. Its authors
already live in the module_decls raw facts, so lowerRetainedSameNameAuthors now
reads function authors straight out of module_decls (filtered to *FnDecl via
fnDeclOfRaw) — the same path → name → RawDeclRef store, fn-filtered. Remove
imports.ModuleFns / FnIndex / indexModuleFns / buildModuleFns / fnDeclOf, the
Compilation.module_fns field + its build + wiring, and ProgramIndex.module_fns.
Remove VisibilityMode.legacy_direct_any (the quarantined own-scope-plus-full-
import_graph mode): no production caller passed it, so the collectVisibleAuthors
and isVisible switch arms that handled it are dead and go too, collapsing
VisEdgeSet to the single flat-import walk. No semantic fallback is introduced;
import_graph stays the transitive-visibility source for findVisibleImpls.
Additive: the old maps stay active and lowering still consumes them — no
lowering consumer is cut over to the DeclTable (that is S3), and no resolution
behavior changes. Tests that drove the removed symbols are rerouted through
module_decls / the flat-edge walk.
Gate over the baseline-green corpus: zig build, zig build test (424/424),
bash tests/run_examples.sh (540 passed) — all exit 0; single-author output
byte-identical; multi-author 0722–0740 stdout/exit unchanged.
Build a DeclTable in parallel with the import facts: every RawDeclRef
(source / imported / namespaced / C-imported) gets a stable DeclId carrying
source path, display name, AST node identity, span, and DeclKind. Namespace
targets record their members' DeclIds (NamespaceTarget.member_ids). A generic
struct's template is keyed by DeclId in a parallel struct_template_by_decl
store, written alongside the live name-keyed struct_template_map.
A Debug-only round-trip cross-check (RawDeclRef -> DeclId -> AST node ptr)
asserts the table identifies the same node across the corpus, run from
buildDeclTable and pinned by a unit test.
Additive (S0.1 class: mirror): the old maps stay active and lowering still
consumes them; nothing reads the DeclTable / struct_template_by_decl for
selection yet (the S4 cutover does). Generated IR + output bytes are unchanged
by construction.
Gate over the baseline-green corpus: zig build, zig build test (424/424),
bash tests/run_examples.sh (540 passed) — all exit 0; single-author output
byte-identical (37 .ir snapshots unchanged).
attempt-2 review fixes (docs-only; contract mechanics confirmed sound):
- README + S0.2 grep-clean: 'S0 HEAD == base' / 'S0 == base' were inaccurate
(HEAD carries the docs/examples/tests diff). Reword to: production/compiler
behavior is base-equivalent — zero src/ changes, single-author output
byte-identical to base by construction — HEAD is a distinct commit, not base.
- S0.3 ledger: drop the stale '116-class corpus' FFI wording for the grounded
live count (96 entry trees / 95 active markers), matching the S0.1 count note.
No partition / manifest / examples / harness change. Gate green:
zig build + zig build test (LSP sweep 574, no crash) + run_examples (540/0);
m3te ios-sim build via main binary exit 0.
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.
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.
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.
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.
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)
Re-land the value-const analog of the E1-E4 type work, reconciled onto the
current source-keyed resolver and hardened. A same-name VALUE const declared in
multiple flat-imported modules is now resolved per declaring source, not the
global last-wins `module_const_map`.
- imports.zig: `isPerSourceDecl` retains every non-function `const_decl`
per-source (value consts + type aliases), so each same-name author reaches
registration as a distinct author of its own module. Functions and var_decls
keep first-wins.
- lower.zig:
* `selectModuleConst` over `module_consts_by_source` — own-wins; exactly one
flat-visible resolves; >=2 flat-visible bare -> loud ambiguous (consistent
with the 0755 type / 0724 fn / 0782 generic ambiguities). Rewires every
consumer: `comptimeIntNamed`, the runtime-id read, the global-init read,
and the float-name path (`lookupFloatName` / `nameIsFloatTyped`).
* `SourceConstCtx` + `foldSourceConstInt`/`Float` + `sourceConstIsFloatTyped`
fold a selected const's RHS with nested same-name leaves re-selected in
their own author source, so VALUE and array-DIMENSION results are coherent.
* `pinConstAuthorSource` pins each fold level to the SELECTED const's author
(F1), including multi-level cross-module chains.
* cycle guard keyed on (name, author-source), not name alone (F3), so
same-name nested consts across modules do not trip a false cycle.
* `emitModuleConst` takes the author source and pins while folding/lowering.
Registration-time struct/inline-type field dimensions route through the now
source-aware stateful reader; the type-alias dimension path resolves each
alias against its own author's consts.
- program_index.zig: expose `isFloatConstType` / `isCountableConstType` for the
source-aware folds.
examples: 0786 own-wins, 0787 ambiguous (exit 1), 0788 expr-chain value+dim
coherent, 0789 leaf-author-pin, 0790 cross-module cycle-guard (F3), 0791
multi-level cross-module chain, 0792 struct-field registration-time dim.
Single-author corpus byte-identical (524 prior markers green); 531 total.
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.
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.
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).
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).
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).
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.
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.
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.
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.
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).
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.
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.
Final E4 piece: the IMP trampolines emitted for an sx-defined #objc_class
resolved their method-signature types (e.g. -> BOOL) at whatever lowering
site triggered emission, not the class's defining module — so under the
single-hop bare-TYPE gate a 2-flat-hop objc type (BOOL via uikit->objc)
leaked as 'not visible' when m3te's main triggered emission.
- ast.ForeignClassDecl gains source_file (stamped by resolveImports, like
ProtocolDecl/StructTemplate); stampFnBodySource stamps the decl + each
bodied method body.
- emitObjcDefinedClassImps pins current_source_file to fcd.source_file for
the whole per-class emission (alloc/dealloc/method/property IMPs).
- Removes the BOOLLEAF debug probe.
Completes E4: bare-TYPE visibility is single-hop non-transitive across all
member kinds; every instantiation kind (generic struct/fn, pack fn, param
protocol, type fn, objc-block, objc-class IMP) is source-pinned to its
defining module. Full gate green; m3te ios-sim builds + launches (exit 0).
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.
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).
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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).
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).
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.
R4: a type alias is a `const_decl`, not a named-type decl, so the bare-TYPE
visibility gate ignored aliases — a namespaced-only alias leaked bare (silent
empty-struct stub, no diagnostic) and a flat-visible alias was poisoned by an
invisible same-name named type. Unify both type-author kinds (named type AND
alias) behind one per-module predicate `moduleTypeAuthor`, returning the author
KIND so resolution is decoupled from `findByName` timing (a forward/self
reference like `next: *ArenaChunk`, unregistered mid-registration, is still
recognised as an author and falls to the legacy stub instead of a false
"not visible"). The leak detector `nameAuthoredAsTypeAnywhere` now also scans
`type_aliases_by_source`. Single source of truth across named types, top-level
aliases, and parameterized/type-fn aliases — leak side and false-rejection side.
Behavior-preserving for single-author names (full suite byte-identical, paths
normalized). Generic / parameterized-protocol / Vector / type-function heads
stay legacy (0210). Block-local `Name :: <type>` remains a value const under the
reserved-name duality (pre-existing; the gate handles it safely, no leak).
Regressions: 0747 (ns-only alias bare -> not visible), 0748 (flat-visible alias
not poisoned by ns-only same-name struct). Both fail-before on 4bd57c8 /
pass-after here.
R1 (type-author-aware gate): the bare-TYPE visibility gate now requires a
flat-import-reachable TYPE author (struct/enum/union/error-set/protocol/foreign
class). A same-name flat VALUE/FUNCTION no longer makes a namespaced-only TYPE
bare-visible — the name-only `m.names.contains` check (attempt-2) is replaced by
`moduleAuthorsType` (kind-checked via `RawDeclRef`). Regression 0745.
R2 (no local false-positive): a block-local type clobbers the global type-table
entry for its name (`registerStructDecl`'s findByName-orelse-intern +
updatePreservingKey), so it IS the resolved type — never a namespaced-only leak.
A new `local_type_names` set, populated at both block-local type-decl paths,
exempts such names from the gate. Regression 0746.
readme.md: drop the false "transitively" claim — flat-import bare visibility for
functions and constants is NON-transitive (0706).
R3 (foundational model consistency) is ESCALATED, not resolved here — see the
attempt-3 worker report. Ground truth: making the TYPE gate single-hop (to match
the value/function model) breaks ~19 tests, ~13 of them library-INTERNAL generic
refs (e.g. `List.append`'s `alloc: Allocator`, lowered in the caller's source
context). That needs source-pinning generic instantiation to the template's
defining module — a separate architectural piece beyond E1's leaf-cut scope, and
proven risky (a `monomorphizeFunction` pin broke 4 FFI objc-block tests and did
not even take, since template method bodies lack a reliable `source_file`). The
TYPE gate therefore stays on the (type-author-aware) transitive flat closure for
E1; the non-transitive reconciliation is a routed follow-up.
Completes the F1 deliverable the reviewer flagged: the bare TYPE leaf still
returned the global `findByName` match BEFORE any visibility check, so a type
declared only behind a namespaced import leaked bare. Now the registered-type
branch of `selectNominalLeaf` is gated on bare-flat visibility (the type analog
of Phase B's value/function tightening): a bare reference to a namespaced-only
import's TYPE errors ("type 'X' is not visible; #import the module that declares
it") and poisons to `.unresolved` — never the leaked global match, never a
silent empty-struct stub.
Visibility gate is the TRANSITIVE flat-import closure (`typeBareVisible`), not
the single-hop `collectVisibleAuthors`/`isNameVisible`: a flat import is
transitive for resolution, so a type two flat hops away (`CAllocator`, via
`main → std.sx → allocators.sx`) stays bare-visible while a namespaced-only type
(reached solely over a namespace edge) does not. The gate applies ONLY to a
TOP-LEVEL author (`module_decls`) — a LOCAL type / generic-param / fabricated
empty-struct stub is findByName-registered but authored in no module, so it
resolves ungated and byte-identically (its own diagnostics still fire). The
compiler-synthesized default-Context emission falls open (`CAllocator` is
infrastructure, independent of the program's import style). The closure walk
lives in lower.zig, so resolver.zig keeps its single graph-walk.
A namespaced callee's declared return type now resolves in the callee's own
module context (`resolveTypeInSource` over `qualified_fn_source`) — a `Value`
returned by `json.parse` is visible inside `json.sx`, not at the call site
(issue-0100-F1 source-pin analog).
Migrates 0719 (flat-imports `cli` for its types, keeps `cli` namespaced for the
same-name `cli.parse`). Adds 0743 (bare ns-only struct → not visible) and 0744
(bare ns-only enum → not visible) regressions. 0742 (ns-only const) + 0210
(generics stay legacy) unchanged. readme updated.
Gate: zig build / zig build test (LSP sweep, no crash) / run_examples 481/0;
m3te ios-sim exit 0; 0743/0744 fail-before on 7cd12b0 (compiled, no diagnostic)
/ pass-after (clean "not visible").
Route the Lowering-side bare type leaf through the source-keyed caches (E0):
nominal author via collectVisibleAuthors(.user_bare_flat) + alias via
type_aliases_by_source, instead of the global findByName first-match. The
binding-free resolveAstType path + registration sites stay on the global
compat readers (move later). Single-author resolution byte-identical (no
shadows yet). Folded req #1: a namespaced-only import's const is no longer
bare-visible in array-dim/comptime-scalar position. Adds regression 0742
(ns-only bare const) and 0210 (generics/Vector/type-fn stay legacy).
Salvaged from a worker killed at the wall before commit; manager verified
the gate at ground truth (zig build test exit 0; run_examples 479/0 with
0210+0742 ok, prior 477 byte-identical; m3te ios-sim exit 0; folded fix
confirmed fail-before on master 7ffc0c1 exit 0 / pass-after exit 1).
Phase E0 of the unified resolver (R5 §#4): add the source-partitioned
analogues of the global `type_alias_map` / `module_const_map` /
`global_names`, keyed `source path -> name -> X`, and POPULATE them from
the existing scan. Purely additive and behavior-preserving — the global
maps remain the ONLY readers; the read-side cutover to
`selectedAuthor.source` is E1.
ProgramIndex:
- type_aliases_by_source / module_consts_by_source / globals_by_source
(StringHashMap of inner StringHashMap), owned + freed on deinit.
- put{TypeAlias,ModuleConst,Global}BySource + removeModuleConstBySource
helpers; retain `module.alloc` to lazily create inner per-source maps.
lower.zig scan: every global `type_alias_map`/`module_const_map`/
`global_names` write (and each module_const_map.remove) now mirrors into
its by-source analogue, keyed by the registering decl's source
(decl.source_file / current_source_file), the analogue of module_fns.
Tests:
- program_index.test.zig: same alias/const/global name under two sources
lands two distinct entries (not last-wins); compat globals stay
single-keyed; removeModuleConstBySource scoped to its source.
- lower.test.zig: end-to-end two-source namespace fixture — the scan
populates the by-source caches per declaring source while the global
maps stay single-keyed by name.
Gate: zig build + zig build test (423, incl. 2 new) + run_examples
(477, byte-identical) + m3te ios-sim build, all exit 0.
Phase D of the unified resolver: make the TypeTable safe to key by nominal
identity before same-name type shadows land (Phase E). Behavior-preserving —
nominal_id=0 means structural (today's keying, byte-identical); single-author
names intern to the same TypeId as before.
types.zig:
- StructInfo/EnumInfo/UnionInfo/TaggedUnionInfo/ErrorSetInfo gain
`nominal_id: u32 = 0`. hash/eql fold it into the nominal arms ONLY, and only
when nonzero, so legacy (structural) interning hashes/compares byte-identically.
- internNominal(info, nominal_id): stamps the id into the nominal arm then
interns; nonzero id on a non-nominal info trips an assert.
- updatePreservingKey(id, info): field-fill that asserts the intern key is
unchanged (replaces the forward-decl stub→full pattern).
- replaceKeyedInfo(id, info): the one legitimate re-key (anon rename
__anon → Parent.field); removes the stale key and installs the new one.
- findUniqueByName: quarantined findByName that asserts ≤1 match.
- type_decl_tids: decl-node → TypeId identity map (the fn_decl_fids analogue),
consumed by the resolver in Phase E.
Ban raw TypeTable.update outside types.zig (the acceptance bar): every caller
in lower.zig / type_bridge.zig / protocols.zig is reclassified — forward-decl
field fills route through updatePreservingKey, qualifyAnonType's rename through
replaceKeyedInfo. The raw `update` method is removed. Inline named type-decl
registration ("current winners") routes through internNominal(info, 0).
Tests (types.test.zig): forward-decl field fill (stable key), anon rename
(re-key), generic struct instantiation, type-returning function, parameterized
protocol value struct, same display-name → distinct nominal ids, plus an
old==new assertion (internNominal(.,0) byte-identical to legacy intern),
findUniqueByName, and the type_decl_tids identity map.
Gate: zig build (0), zig build test (421/421), run_examples (477, byte-identical),
m3te ios-sim build via worktree binary (0). No shadows registered; stubs intact.
lowerCall's early pack/comptime/generic dispatch keyed off the first-wins
winner (`fn_ast_map.get(early_name)`) BEFORE the main dispatch consumes the
selected same-name author. Under a genuine flat same-name collision where the
caller's own author is a plain free fn but the first-wins winner is a comptime
pack `(..$args)` (or comptime-param / generic), the early path invoked the
WINNER — so `CallResolver.plan` (which selects the own plain author) and
lowering disagreed about which function a bare call names.
Confirms reviewer finding C-review-1. The earlier manager ground-truth got
`show_b=2` because it used a slice variadic `(..xs: []s64)` — NOT a pack fn
(`isPackParam` false), so it never hit the early dispatch. The reviewer used a
comptime pack `(..$args)` (`isPackFn` true), which does. Both observations are
correct for their respective shapes; the bug is real for the comptime-pack
winner.
Fix: the early dispatch reads the SAME author the selector chose
(`sel_author.decl`) when a collision rerouted the call, else the winner
(common path, byte-identical). The selector only ever returns a plain free fn
(`isPlainFreeFn` excludes type-params / comptime / pack), so a selected author
falls through to the main dispatch that binds it via `SelectedFunc`.
Regression: examples/0741-modules-flat-same-name-bare-pack-winner — a.sx
(imported first) authors `f` as a comptime pack (first-wins winner); b.sx
authors its own plain `f`; b's bare `f()` must return 2 (own author), not 1
(the pack). Fails on 2dd6c3c (b: f() = 1), passes after.
Gate: zig build + zig build test (412/412) + run_examples (477/0) +
m3te ios-sim exit 0.
Address Phase C review (C-1, C-2): make CallResolver.plan's SelectedFunc the
single shared call author consumed by the lower-call sites instead of each
re-resolving; route free-fn value-receiver UFCS through the selector in plan so
plan typing and lowering pick the same author under a flat same-name collision.
Adds regression 0740-modules-flat-same-name-ufcs-typing.
Salvaged from a worker killed at the wall during its final gate step; manager
verified the gate at ground truth (zig build test exit 0; run_examples 476/0 with
0722-0735 + 0740 ok; m3te ios-sim exit 0).
Phase C of the unified resolver (R5 §C, §#3). Re-base the plain bare-name
call author onto the Phase B collector behind one shared SelectedFunc, so
every call-path consumer reads ONE author and they can no longer disagree
(fix-0102 F2). Behavior-preserving: 0722-0735 byte-identical, run_examples
stays at 475.
- SelectedFunc {decl, source, materialized?} replaces ResolvedAuthor in
BareCallee.func; CallPlan.Target gains a `selected` arm (calls.zig).
- selectPlainCallableAuthor: resolveBareCallee's body verbatim over
resolver.collectVisibleAuthors (.user_bare_flat) — the ONE graph-walk.
fnDeclOfRaw mirrors imports.fnDeclOf so the collector's all-domain authors
reproduce module_fns' fn-only view; every byte of the negative space is
preserved (own==winner → .none; non-plain-free → .none; filter-before-count;
≥2 distinct → .ambiguous). No eager materialization.
- selectedFuncId materializes the FuncId on demand (shadow-only), caching into
materialized — null until a site needs it (0102d: a shadow taken as a value
never lowers the winner).
- Six consumers route through the one selector: lowerCall variadic packing,
free-fn UFCS, fn-value, closure(fn), resolveCallParamTypes, and
expandCallDefaults (decl-only, no materialization). plan() produces the
SelectedFunc as `.selected`. Generic/comptime/foreign/builtin stay legacy.
- lower.test.zig: wire module_decls; selectPlainCallableAuthor verdicts
(own-winner → .none; ≥2 flat → .ambiguous; own-shadow → decl+source, fid
round-trips, materialized null).
Gate: zig build + zig build test (412 ok) + run_examples (475, byte-identical)
+ m3te ios-sim build exit 0.
The namespaced-only bare-visibility behavior is non-uniform and partial during
the resolver migration: runtime const/fn use errors, comptime/array-dim const
positions still compile, const-aliases report 'unresolved', and bare types still
resolve. Rewrite the note to state the durable rule (a namespaced import binds
only its alias; reach members as m.name; bare-name visibility joins over flat
imports only) and that bare references to ns-only members are being phased out
and do not yet resolve uniformly across name kinds. No specific diagnostic
string, no completeness/uniformity claim. Doc-only; no code path touched.
Phase B tightened bare VALUE/FUNCTION visibility through a namespaced-only
import (isNameVisible/isCImportVisible -> 'not visible'). Bare TYPE names
from such an import still resolve today; type-name visibility tightens in a
later resolver phase. Correct the README so it no longer claims all bare
names from a namespaced import error.
lowerComptimeCall stamped the caller's source onto fixed comptime `$`-params
so their substituted bare names resolve in the caller's visibility context,
but the variadic comptime pack branch (`..$args`) recorded the pack-arg slice
without stamping. Those nodes are later re-lowered via packArgNodeAt under the
defining-module pin, so a caller-owned helper in a formatted-arg position
(`std.print("{}", caller_fn())`) was checked against the metaprogram's module
and rejected as "not visible". Stamp every pack-arg node with the caller source,
mirroring the fixed-param treatment — completing Problem 1 for pack args.
Regression: examples/0739-modules-comptime-pack-arg-caller-context.sx
(two caller-owned s64 helpers in std.print pack positions; fail-before both
"not visible", pass-after prints "42 7"). No exemption flag, no silent default.
attempt-3 pinned current_source_file to the metaprogram's defining module
across the whole body lowering (lowerComptimeCall / monomorphizePackFn). That
pin also covered caller-provided comptime $-arg nodes spliced into the body by
substituteComptimeNodes — but those are CALLER-authored and must resolve in the
caller's visibility context, not the callee's. Result: a caller-owned helper
passed to an imported metaprogram errored "'<name>' is not visible".
Fix: stamp each comptime $-arg node with the caller's source_file at the cpn
build site (stampCallerSource, in lowerComptimeCall + monomorphizePackFn);
lowerExpr switches current_source_file to a node's source_file when present, so
the substituted subtree resolves against the caller while the surrounding callee
code keeps the defining-module pin. No exemption / fall-open.
Regression: examples/0738-modules-comptime-arg-caller-context.sx — a caller-owned
helper passed as a comptime-ONLY $-arg through a namespaced import. Fail-before
(attempt-3 binary): "'caller_name' is not visible". Pass-after: prints
"hello world", exit 0. Comptime-only, so it does not exercise issue 0107.
0106 RESOLVED banner extended (point 3: body=defining context, substituted
$-args=caller context). run_examples 473 -> 474; zig build test 412/412.
ROOT FIX for issue 0106's library-metaprogram half — no exemption.
attempt-2 masked the 0106 fallout with an `in_insert_expansion` flag that
made the visibility adapters fall open during ANY `#insert` expansion,
including a USER's `#insert <expr>` — so a bare reach into a namespaced-only
import from user `#insert` code wrongly compiled (Adi's blocker). The flag
was the wrong shape. This removes it and fixes the real cause.
Root cause: a metaprogram's body (`std.print` / `std.format` / `log.*`,
whose `#insert build_format(fmt)` + `#insert "out(result);"` reference
std-internal bare names) was lowered under the CALL SITE's
`current_source_file`, so those names were policed against the consumer's
imports. Normal functions get this right via `lowerFunctionBodyInto`, which
pins `func.source_file`; the two monomorphizers don't:
- `monomorphizePackFn` — bare `print(...)` / `format(...)` (pack path).
- `lowerComptimeCall` — namespaced `std.print` / `log.warn` (reached via
the field-access `hasComptimeParams` branch).
Fix: both paths now save/set/restore `current_source_file` to the body's
DEFINING module around the BODY lowering only (call-site args stay in the
caller's context). The defining path is stamped onto each function body node
by `resolveImports` (`stampFnBodySource`), mirroring `Function.source_file`.
So library internals resolve in std.sx/log.sx naturally, while a USER's
`#insert <expr>` is still checked in the user's context.
- Exemption GONE: `in_insert_expansion` flag + both adapter fall-open checks
deleted; `isNameVisible`/`isCImportVisible` are byte-identical adapters.
- New pinned regression: examples/0737-modules-insert-bare-not-visible.sx
(+ a.sx) — a USER `#insert secret()` into a namespaced-only import errors
('secret' is not visible). fail-before exit 0 on the attempt-2 binary /
pass-after exit 1.
- face #1 (0736) still errors; face #2 (0015/0700/0718/1030) pass again WITH
NO exemption — the metaprogram body resolves in its own module.
- run_examples 472 -> 473; zig build test 412/412; m3te ios-sim build exit 0.
- issues/0106 RESOLVED banner updated (root cause + no-exemption fix).
Folds the coupled 0106 fix into Phase B. attempt-1 tightened the bare-name
visibility adapters (isNameVisible/isCImportVisible) to the flat_import_graph
edge set via the unified isVisible(.user_bare_flat/.c_import_bare) predicate;
that surfaced issue 0106 — std.print / log.* expand `#insert build_format(fmt)`
(comptime call) and `#insert "out(result);"` (inserted stmt) in the CONSUMER's
current_source_file, so their library-internal bare names were policed against
the consumer's imports and errored (run_examples 471 -> 467).
Fix: a precise, named exemption. Lowering.in_insert_expansion is set across
lowerInsertExprValue (the comptime eval + the parsed-back statements); the two
visibility adapters fall open while it is set — mirroring the existing
UFCS-alias / mangled-local "compiler indirection" exemptions. NOT a blanket
skip: scoped to #insert-expanded code, ordinary bare references stay policed.
Library-internal call bodies (build_format's concat/substr) already resolve in
the defining module — lowerFunctionBodyInto pins their current_source_file.
The flat tightening stays: a bare reference to a namespaced-only import's
internal name now correctly errors ('<name>' is not visible). This is the
Agra-ratified user-visible semantics change.
- face #1 pinned: examples/0736-modules-namespaced-only-bare-not-visible.sx
(+ a.sx) — exit 1 + stderr; fail-before (import_graph compiled it, exit 0) /
pass-after (flat set errors, exit 1).
- face #2 restored: examples 0015 / 0700 / 0718 / 1030 pass again.
- run_examples 471 -> 472 (the new regression).
- issues/0106 marked RESOLVED; readme.md documents namespaced-only visibility.
Collectors + unified predicate from attempt-1 (resolver.zig) unchanged; nothing
routes resolution AUTHOR-SELECTION through them yet (that is Phase C).
Two defects from the Phase A attempt-1 review.
F1 — duplicate-name diagnostic missed NAMESPACE ALIASES (silent first-win).
`addNamespace` unconditionally put the alias into scope/own_decls, so a
same-module collision between an authored decl and a `dup :: #import "…"`
alias compiled clean in the fn-then-alias order (the scalar
ModuleRawDeclIndex silently first-won). Now `addNamespace` returns a bool
and refuses a same-module duplicate (mirroring addOwnDecl); the call site
surfaces it via the new `reportDuplicateName` (the import_decl node has no
declName, so the alias name is passed explicitly). The C-import namespace
site gets the same guard. Both orders now emit "duplicate top-level
declaration 'X'" and exit nonzero (alias-then-fn was already caught by
addOwnDecl seeing the alias in scope).
F2 — buildImportFacts errors were swallowed by `else |_| {}` in core.zig
(REJECTED-PATTERN catch-all leaving the borrowed store silently empty).
`resolveImports` returns !void, so the call is now a plain `try` and a
build failure propagates instead of producing a stale/empty store.
Tests: extend the dup-name regression with fn-vs-namespace-alias
collisions in both orders. No resolution behavior change (no lower.zig
edits; run_examples 471 byte-identical); m3te ios-sim builds via the
worktree binary.
Phase A of the unified resolver (R5 locked design). Additive infrastructure
with NO behavior change — builds the import-side raw-fact store; nothing
consumes it yet.
- imports.zig: add RawDeclRef / RawAuthor / ModuleRawDeclIndex / ModuleDecls /
NamespaceTarget / NamespaceEdges, plus buildImportFacts (mirrors
buildModuleFns) producing a scalar per-module name→RawDeclRef index + the
namespace edges. Callable without IR lowering (LSP reuses it later).
- ast.zig: NamespaceDecl gains target_module_path, captured at resolution time
(the resolved_path otherwise lost on the node) so the namespace edge records
the alias target.
- imports.zig: same-module duplicate top-level name is now DIAGNOSED
("duplicate top-level declaration 'X'") where addOwnDecl would silently drop
the second author — replaces the discarded `_ =` at the three call sites.
- program_index.zig: borrowed views module_decls / namespace_edges (like
module_fns); deinit does not free them.
- core.zig: build the facts alongside buildModuleFns and point the borrowed
views at them.
- imports.test.zig: index unit tests (flat / directory / namespaced file /
namespaced directory / C-import namespace / same-name fn / same-name struct /
value-vs-type same spelling / raw const_decl) + the duplicate-name diagnostic
regression (fails pre-fix, passes after).
Gate (worktree): zig build, zig build test (incl. LSP corpus sweep), and
run_examples (471, byte-identical) all green; m3te ios-sim build exits 0.
2026-06-06 23:34:32 +03:00
531 changed files with 9654 additions and 696 deletions
for S3/S6 deletion. Its semantics goldens are harvested; its src is never merged.
- **This branch:** `flow/stdlib/S0` (branched from the base). **Production/compiler
behavior is base-equivalent** — zero `src/` changes, single-author output
byte-identical to base by construction — but S0 HEAD is a distinct commit carrying
the docs/examples/tests diff (it does **not** equal base).
## Contents
| file | sub-step | what |
|---|---|---|
| `S0.1-byte-baseline-and-commit-discipline.md` | S0.1 | the byte-identity reference + the zero-diff reproduction command + resolver-target exclusion + the `mirror \| consumer-cutover \| deletion` commit-classification discipline |
| `S0.2-e6b-disposition-and-two-corpus-partition.md` | S0.2 | E6b src not merged (grep-clean) + the harvested corpus partitioned baseline-green vs resolver-target + 0811/0829 placement + the E6BR-5 re-file + the mirror/flip statement |
| `S0.3-reuse-delete-ledger.md` | S0.3 | 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 |
| `../../tests/resolver-target/` | S0.2 | the listed resolver-target harness: `manifest.md` (18 cases), `expected/` TARGET goldens, the E6BR-5 reproducer under `cases/`, and `run_resolver_target.sh` (xfail runner — NOT part of the gate) |
## The two-corpus law (the one thing the next 26 steps must never conflate)
1.**BASELINE-GREEN / mirror-equivalence corpus** — tests where the old selector is
`wt-stdlib-base @ 1f755284d98c6e8ebba953045c06e35d8cbe6278` (A–E6a merged). This is
documentation only — no production code change, no behavior change.
## 1. The byte-identity reference
The single-author byte-identity reference that **every later Fork C commit is checked
against** is the committed `examples/expected/*` snapshot set on the baseline commit
above. We do **not** copy every file into this doc; the snapshots ARE the reference,
and the reproduction is a documented zero-diff command (§2). Single-author byte
identity is held structurally by `nominal_id == 0 ≡ structural intern`
(`src/ir/types.zig``internNominal`), which S1–S2 keep additive and S3 preserves
through ordinal-0 materialization.
The baseline-green corpus that the reference covers:
| segment | what | how it is exercised | count @ S0 |
|---|---|---|---|
| baseline-green examples | every `examples/<name>.sx` with an active `examples/expected/<name>.exit` marker (incl. the 6 harvested baseline-green cases `0795–0798`, `0823`, `0828`) | `bash tests/run_examples.sh` | **540** active markers |
| FFI corpus | `examples/12xx–14xx` (96 entry trees; 95 with active markers; ~418 files incl. module/`.c`/`.h`/`.m` companions) | same runner (markers) | 95 active markers |
| LSP completion/hover smoke | LSP unit tests under `zig build test` — `analyzeDocument` flat/namespaced import + the `lsp corpus sweep: every examples/*.sx analyzes without panicking` sweep + definition/references/inlayHint | `zig build test` | — |
> Count note: `reconciled.md §1` cites "116 files in `examples/12xx–14xx`". The live
> tree has **96 entry `.sx` trees** (95 with active markers); the "116" is a stale
> historical figure. What is load-bearing is the invariant — *all FFI 12xx–14xx
> examples stay byte-stable* — which `run_examples.sh` enforces via their markers,
> not the exact historical count.
## 2. The zero-diff reproduction method
`tests/run_examples.sh` runs `sx run <entry>` for every `<name>.sx` that has an
`expected/<name>.exit` marker, normalizes stdout/stderr identically for expected and
actual (address hashing → `0xADDR`; absolute `…/examples|issues/` paths → repo-relative),
and diffs exit + stdout + stderr (+ optional `.ir`). A **zero-diff** run is the
byte-identity check:
```sh
exportPATH="$HOME/.zvm/bin:$PATH"
zig build # build the compiler under test
zig build test# unit tests incl. the LSP completion/hover smoke + corpus sweep
- enumerated in `tests/resolver-target/manifest.md`, with TARGET goldens in
`tests/resolver-target/expected/`, held inactive (no `examples/expected/` marker) and
asserted currently-failing by `tests/resolver-target/run_resolver_target.sh`;
- full disposition in `S0.2-e6b-disposition-and-two-corpus-partition.md`.
They flip to active + green at **S3.9** and only then join the baseline.
## 4. Commit-classification discipline
Every future Fork C migration commit (S1→S6) is tagged with exactly one of three
classes, stated in the commit subject/body so a reviewer knows what byte-effect to
expect:
| tag | meaning | expected byte-effect on the baseline-green corpus |
|---|---|---|
| **`mirror`** | builds new facts / a new resolver path **in parallel**, while lowering still consumes the old path (S1–S2; the assert-only Debug mirror) | **zero** — single-author output byte-identical; provably zero byte-risk |
| **`consumer-cutover`** | switches a consumer from the old path to the resolved facts (S3 materializer / calls / consts / protocol-registration / `#using`; S5 LSP) | **zero on baseline-green** — byte-identical by ordinal-0 materialization + payload-preserving facts; the only commits that may change resolver-target (the S3.9 flip is a cutover) |
| **`deletion`** | removes a now-dead artifact (old name selectors, `*_by_source` mirrors, `type_bridge`, `findByName`, the grep gate, the S2 mirror) | **zero** — the deleted code had no live readers after its cutover; a surviving reader fails to compile |
Rules:
- A commit is exactly one class; a cutover that also deletes its now-dead source is
still a `consumer-cutover` if the delete is the same atomic cutover (e.g. S3.10
removes the last old selector **and** the S2 mirror in the cutover commit).
- `mirror` and `deletion` commits MUST be byte-zero on baseline-green; if a `mirror`
commit changes a byte, it was not actually parallel — stop.
- Only `consumer-cutover` may legitimately change output, and only the **resolver-target**
corpus (never baseline-green) — that is the S3.9 flip.
## 5. Acceptance (S0.1) — self-check
- ✅ Byte-baseline of all baseline-green examples + FFI 12xx–14xx + LSP smoke captured
and reproducible via the documented zero-diff command (§1–§2); the reference is the
committed `examples/expected/*` at the baseline commit, re-checked by a zero-`FAIL`
`run_examples.sh`.
- ✅ Resolver-target set explicitly excluded from the byte-baseline AND the active
`run_examples.sh` set, and recorded/listed (§3) — not silently absent.
- ✅ The `mirror | consumer-cutover | deletion` classification rule is written (§4).
- ✅ `zig build && zig build test && bash tests/run_examples.sh` green over the
baseline-green corpus; no behavior change (S0 adds no production code).
1f75528`. Symbol/file refs are grounded against the base tree. This is documentation
only — no code change.
This ledger is the contract the later phases execute against: every load-bearing A–E6
artifact is mapped to **REUSED** (with its Fork C home) or **DELETED/TRANSITIONAL**
(with the S3/S6 phase that removes it). A–E6a stays merged; the transitional E6b src
is never merged (see `S0.2-…`).
## A. REUSED — A–E6 work that becomes Fork C infrastructure
| A–E6 artifact (base location) | Fork C home | phase |
|---|---|---|
| **Phase A import facts** — `RawDeclRef` / `RawAuthor` / `ModuleDecls` / `NamespaceEdges` (`src/imports.zig`), built in `resolveImports` (`core.zig`) | **seed `DeclId` construction** — `DeclTable` keys every `RawDeclRef` into a stable `DeclId` (source + name + AST ptr + `DeclKind`); namespace members get ids | S1 |
| **Phase B visibility** — `collectVisibleAuthors` / `collectNamespaceAuthors` (`src/ir/resolver.zig`), "the one graph iterator" over `flat_import_graph`/`namespace_edges` | **resolver internals** — become the resolver's visibility walk, with own-wins / single-flat-visible / ≥2-ambiguous **verdicts above them**, producing `ResolvedRef` | S2 |
| **Phase C callable selection** | **`ResolvedRef.function` / `.type_function`** keyed by `DeclId` | S2 (select) → S3 (consume) |
| **Phase D nominal identity** — `internNominal` / `updatePreservingKey` + the **`nominal_id == 0 ≡ structural intern` ordinal-0 byte-identity rule** (`src/ir/types.zig`, `lower.zig`) | **reused inside materialization** — `materializeType(ResolvedTypeNode)` interns in old scan order with ordinal 0 for non-colliding decls ⇒ byte-identical single-author output | S3 (+ green-lock every phase) |
| **E-series selection rules** — own-wins / not-visible / ambiguity / direct-flat (the E1–E6a behaviors) | **resolver behavior + regression tests** (the baseline-green corpus is the mirror oracle) | S2 behavior; regressions locked S0 |
| **CP rule** — body-author == layout-author | **keyed by `InstantiationId{template_decl, resolved_args}`** in the fact store | S4 |
| **E6BR routed-signature cases** (the E6BR-1…4 behavioral cells) | **resolver-signature regressions** — the resolver walks every signature reference position; cases live in the resolver-target corpus, flip at S3.9 | S3.9 |
| **FFI `foreign_class_map` consumers + FFI corpus (96 entry trees / 95 active markers)** | parallel `DeclId`s land at S1 (map still the consumer); foreign classes keyed by `DeclId` at S4; runtime names stay **payload strings on facts** | S1 → S4 |
| **Old name selectors** — `selectNominalLeaf`, `resolveNominalLeaf`, `moduleTypeAuthor`, `namedRefTid`, `flatTypeAuthorCount`, `nameAuthoredAsTypeAnywhere`, `selectModuleConst` (+ const-source pins), `selectGenericStructHead`, `headTypeGate`, `headFnLeak`, `flatFnAuthor*`, the name-selection in `resolveTypeCallWithBindings`/`resolveParameterizedWithBindings` (`src/ir/lower.zig`) | the duality leak — replaced by `ResolvedRef`/`ResolvedTypeNode` consumed in lowering | S3 |
| **`*_by_source` mirrors + source pins** (`src/ir/program_index.zig`) + their writers + the `lower.zig` unified writers | dual-write mirrors of the global maps — superseded by the `DeclId`-keyed fact store | S4 |
| **`ShadowTypeDecl` / shadow-slot reservation helpers** (`src/ir/lower.zig`) + lower-side nominal selectors | shadow reservation is a name-keyed pre-pass artifact — subsumed by `DeclId` pre-pass | S3/S4 |
| **`TypeTable.findByName` / `findUniqueByName`** (`src/ir/types.zig`) | the global name table — deleted **last** (after the ~15 category-(b) stdlib lookups are re-homed to resolved-once `DeclId`s, per the §6 critical ordering constraint) | S6 |
| **the type-reference choke-point + route-all engine** (`resolveRegistrationSigTypeInSource` / `sig_registration_mode`) | **transitional E6b src — never merged**; destined for deletion under Fork C | S3/S6 (already off-baseline) |
| **the grep gate `e6br_gate.test.zig`** (+ its `ir.zig` import) | **transitional E6b src — never merged**; unnecessary once the leaf it polices is gone | S6 (already off-baseline) |
| **the S2→S3 assert-only Debug mirror** | a test oracle, not a code path — must be deleted in the **same S3.10 commit** that removes the last old selector | S3.10 |
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.