16 Lowering map fields and 8 ProgramIndex map fields were declared with
`= ....init(std.heap.page_allocator)` field defaults that init() never
replaced — every instance really allocated page-at-a-time outside the
compilation allocator, invisible to leak checking and never reclaimed.
All 24 now init explicitly with the compilation allocator (module.alloc
/ the init alloc param), which is arena-backed in both the driver
(main's arena) and the test suites (per-test arenas), so backing is
reclaimed at teardown. ProgramIndex's struct doc no longer claims the
page_allocator defaults.
Six lower.test.zig tests that constructed Module with bare
std.testing.allocator leaked once the checker could finally see these
maps; they now use the same per-test ArenaAllocator idiom as the rest
of the file and the facade test suites.
Gate: zig build OK; zig build test 426/426 (6/6 steps, leak-clean);
run_examples 541/0; zero expected/ snapshot churn.
Review follow-up to the ARCH-B split (comment/import hygiene only, no
code changes):
- Section banners that travelled to the wrong file with the B1-B8 cuts
are reworded to describe the section that actually follows (e.g.
stmt.zig's trailing "Expression lowering", expr.zig's "Control flow"
before lowerChainedComparison) or deleted where nothing follows
(4 trailing-at-EOF banners). ffi.zig's facade note no longer claims
the IMP builders "stay here" (they live in lower/objc_class.zig);
protocol.zig's namespace-lookup banner now points at
pack.zig:resolvePackProjection for the orchestrator.
- lower.zig's two lower/expr.zig alias blocks (B8.1 + B8.2 appends)
merged into one.
- 448 unused header decls pruned from the 15 lower/*.zig files (each
had inherited lower.zig's full import block; pruned to fixpoint so
cascading type-extraction consts went too).
Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
Verbatim relocation of the 5-method closure cluster (lowerLambda,
bare-fn trampoline, closure-to-bare-fn adapter, capture collection, env
sizing) into src/ir/lower/closure.zig. 5 aliases on Lowering keep all
call sites unchanged. Method pub-flip: typeAlignBytes.
Resolves the B7.1 flag: CaptureInfo relocates from lower/call.zig to
lower/closure.zig (its domain home, next to collectCaptures); the
Lowering type alias is repointed so external references are unchanged.
Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
Verbatim relocation of the 30-method expression cluster (struct/array/
tuple/enum/tagged-enum literals, init blocks, field access on values and
types, optional chains, numeric limits, indexing, slicing, deref, force
unwrap, null coalesce) into src/ir/lower/expr.zig — one contiguous
1,372-line cut. 30 aliases on Lowering keep all call sites unchanged.
Nested StructConstInfo stays on Lowering (field type of
struct_const_map), flipped pub and reached via an alias const, alongside
headNameOfCallee.
Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
Verbatim relocation of the 18-method call cluster (lowerCall moved
whole, context diagnostics, foreign-call helper, builtin/function
resolution, generic + runtime-dispatch calls, reflection calls + guards,
default-arg expansion, call param typing) into src/ir/lower/call.zig.
18 fn aliases keep all call sites unchanged.
CaptureInfo (closure-domain type that sat inside the run) travelled and
is re-exposed via a Lowering type alias; candidate to relocate to
lower/closure.zig in B8.3.
Method pub-flips: callResolver, createBareFnTrampoline,
ensureGenericInstanceMethodLowered, fixupMethodReceiver,
getStructTypeName, isStaticTypeArg, lowerPackFnCall, packSpreadRefs,
packVariadicCallArgs, refCapturePointee, resolveParamTypeInSource,
typeSizeBytes, headNameOfCallee.
Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
Verbatim relocation of the 23-method defined-class cluster (IMP/property
emission: class/alloc/static/dealloc IMPs, property getters/setters +
ARC runtime decls, defined-state field access, property/method chain
lookup, string-constant globals) plus the single-home
ObjcDefinedStateField type into src/ir/lower/objc_class.zig. 23 aliases
on Lowering keep all call sites (incl. expr_typer.zig facade and
lower/stmt.zig) unchanged. Zero pub-flips — all callees were already
public from earlier steps.
Gate: zig build OK; zig build test 426/426; run_examples 541/0; all 37
.ir snapshots byte-identical, zero expected/ churn.
Verbatim relocation of the 19-method coercion cluster (lowerXX, user
conversions, protocol erasure, default-value construction, zero values,
coerceToType implicit/explicit ladder, C-variadic promotion, call-arg
coercion) plus the nested single-home CoerceMode enum into
src/ir/lower/coerce.zig. 19 aliases on Lowering keep all call sites
unchanged.
Method pub-flip: prependCtxIfNeeded. ParamImplEntry stays a Lowering
nested type (field type of param_impl_map) and is reached via an alias
const.
Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
Verbatim relocation of the 13-method protocol cluster (protocol decl
registration, param-protocol instantiation, thunk creation, vtable
globals, protocol-value construction, dispatch emission, impl lookup)
into src/ir/lower/protocol.zig. 13 fn aliases on Lowering keep all call
sites unchanged.
Two pub nested types travelled with the run (ProjectionPosition,
PackProjection) and are re-exposed via Lowering type aliases; they are
pack-domain types and may relocate to lower/pack.zig in B7.2.
Method pub-flips: allocViaContext, callForeign, genericInstanceMethod,
monomorphizeFunction.
Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
Verbatim relocation of the 23-method nominal-type cluster (struct/enum/
union/error-set registration, anon-type qualification, nominal-id
stamping, shadow-slot reservation, named-type interning, generic struct
templates + alias registration) plus the nested ShadowTypeDecl union
into src/ir/lower/nominal.zig. 23 aliases on Lowering keep all call
sites unchanged.
Method pub-flip: instantiateGenericStruct. nominal.zig reaches
VisibleStructAuthor and structDeclOfRaw (both relocated to decl.zig in
B4.1) via Lowering-namespace alias consts.
Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
Verbatim relocation of the 53-method error cluster (error typing,
raise/failable, try/catch/or, inferred-set convergence, trace runtime
hooks) out of the Lowering struct into src/ir/lower/error.zig as free
functions taking *Lowering. Each gets a pub-const alias on Lowering, so
every call site compiles unchanged (decl-alias method resolution).
Pub-flips (callees now referenced cross-file): lowerExpr, coerceToType,
freshBlock, freshBlockWithParams, emitErrorCleanup,
currentBlockHasTerminator, lowerBlock, lowerBlockValue.
Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero
expected/ snapshot churn.
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-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.
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.