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.
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).
The bare-fn-as-value site (func_ref / fn-ptr / closure coercion) eagerly
lazily-lowered the name-keyed first-wins WINNER before the resolveBareCallee
block could reroute a genuine flat same-name collision to its per-source
author. Taking a SHADOW author's fn value therefore lowered (and could
mis-diagnose) the unused winner's body. Move lazyLowerFunction INSIDE blk_fv
onto the `.none` fallback only, mirroring the closure(fn) and free-function
UFCS sites: on `.func` use the resolved author's FuncId and never touch the
winner; on `.none` fall through to lazy-lower + resolveFuncByName the winner.
Regression: examples/0735-modules-flat-same-name-fn-value-winner — the
first-wins winner's body is independently broken and never used; a shadow
taken as a function value binds the shadow and runs (exit 0) while the winner
is not lowered. Fails-before (unresolved symbol in the winner), passes-after.
Final 0102 sub-step. fix-0102c landed resolveBareCallee and routed the
primary call path + parameter target typing through it, leaving four other
bare-name consumer sites on the old first-wins path. Route the SAME resolver
through all four, gated exactly as the call path (plain top-level identifier,
no scope-mangle / UFCS alias / local shadow; act on .func / .ambiguous, fall
through on .none so single-author / local / std / qualified / foreign-single
resolution is byte-for-byte unchanged):
1. Default-argument expansion (expandCallDefaults): omitted trailing args
fill from the RESOLVED author's defaults, not the winner's.
2. Function-value conversion (closure(fn) and the bare-fn-as-value func_ref /
fn-ptr / closure-coercion path): captures the resolved author's FuncId.
3. Free-function UFCS (recv.fn() -> fn(recv, ...)): dispatches the resolved
author for the receiver's source.
4. Comptime #run of a bare call: lowerMainAndComptime now sets
current_source_file per decl, so a `NAME :: #run f()` in an imported
module resolves f from THAT module's flat imports (own-author wins) instead
of the main file's perspective (which made it spuriously ambiguous).
Regression tests: examples/0730-0734 (default-arg, closure+fn-value, UFCS,
comptime #run, UFCS-ambiguity), each fails on pre-fix code and passes after.
issues/0102-flat-import-same-signature-collision.md written RESOLVED with the
4-sub-step root cause and regression-test paths.
resolveBareCallee's flat-collect branch counted ALL same-name authors —
including #foreign / generic / builtin / #compiler — before the
isPlainFreeFn filter, so two flat-imported modules each #foreign-ing the
same libc symbol under one sx name returned `.ambiguous` and errored,
instead of falling to `.none` and the existing first-wins foreign path
(master behavior). Filter authors to plain free functions DURING
collection, before the count/ambiguity determination: a non-plain
collision now yields 0 reroutable authors -> `.none`; genuine plain-fn
collisions still yield >= 2 -> `.ambiguous` (0724 unchanged). The
now-redundant single-author isPlainFreeFn check is dropped.
Regression: examples/0729-modules-flat-same-name-foreign — two flat FILE
imports each #foreign the same libc "abs" under name `absval`; a bare
call resolves first-wins and runs (exit 0). Fails-before on this branch
(ambiguity error), passes-after.
Attempt-3 fix for the F2 review finding. After resolveBareCallee picks a
shadowed same-name author at a normal call site, the call's PARAMETER TARGET
TYPING still ran first-wins: resolveCallParamTypes' bare-identifier branch
resolved param types via resolveFuncByName(name) / fn_ast_map.get(name) — both
keyed by name, not by the resolved author. Because that runs in lowerCall
BEFORE the resolveBareCallee routing, a shadow author whose parameter TYPE
differs from the first-wins winner had its args lowered against the WINNER's
signature (no implicit address-of for a *T param typed as T), then the
correctly-resolved shadow FuncId was called with the mis-typed arg — a value
bit-cast to a pointer → segfault.
The bare-identifier branch now routes through the SAME resolveBareCallee
resolver one layer earlier and takes the param target types from the RESOLVED
author's lowered func.params (userParamTypes). Only the .func (single resolved
author) outcome reroutes; .ambiguous keeps the existing loud call-site
diagnostic and .none keeps the first-wins fallback, so single-author / local /
std / qualified resolution is byte-for-byte unchanged. Method-call / namespace /
foreign / generic branches of resolveCallParamTypes are untouched. The resolver
is idempotent (bareAuthorFuncId guards body lowering via lowered_fids) so the
extra call from param-type resolution is safe; lowerFunctionBodyInto already
saves/restores all lowering state for mid-call reentry.
Regression: examples/0728-modules-flat-same-name-paramtype — two flat file
imports each author `apply` with a divergent param type (a.sx value `s64`
winner, b.sx pointer `*s64` shadow). b.sx's from_b passes a value local to its
pointer-param author via implicit address-of (×2 → 42); a.sx's from_a (own ==
winner) is unchanged (value + 1 → 11). Fails on the pre-fix typing (segfault at
from_b); passes after.
Gate (worktree): zig build, zig build test (400/400), bash tests/run_examples.sh
(464 passed / 0 failed) all green. Matrix 0722-0727 unchanged. Guardrail: m3te
builds via the worktree binary (sx build --target ios-sim, exit 0) — single-
author / local resolution intact. Default-arg / closure / UFCS / comptime SITES
remain first-wins (fix-0102d).
Attempt-2 fix for the F1 review finding. After `resolveBareCallee` picks a
shadowed same-name author's FuncId at a normal call site, the call path still
re-fetched the FIRST-WINS function AST by name to drive variadic argument
packing. When the resolved (shadow) author's variadic shape differs from the
first-wins author's, arguments were packed against the WRONG signature — a
fixed-arity shadow packed as if variadic, or a variadic shadow not packed at
all — producing IR with the wrong argument count (LLVM verification failure).
The `.func` arm now carries the resolved `*FnDecl` alongside its FuncId
(`BareCallee.func: ResolvedAuthor`), so `packVariadicCallArgs` reads THE
resolved author's signature. The rest of the arm already used the resolved
FuncId's IR function (ret/params/ctx/coercion), so the callee now has one
source of truth in the whole call lowering — no re-fetch by name after
resolution. Default-arg / closure / UFCS / comptime *sites* remain first-wins
(fix-0102d); `expandCallDefaults` runs before resolution and is a default site.
Regression: examples/0726-modules-flat-same-name-variadic — two flat file
imports each author `combine` and `pick` with OPPOSITE variadic shapes (a.sx
fixed `combine` / variadic `pick`; b.sx variadic `combine` / fixed `pick`).
Each module's bare call must pack against ITS OWN author. Fails on the pre-fix
re-lookup (LLVM "Incorrect number of arguments passed to called function" for
both `combine.1` and `pick.2`); passes after.
Gate: zig build, zig build test (400/400), bash tests/run_examples.sh
(463 passed) all green. Matrix 0722-0725/0727 unchanged; single-author / local
resolution byte-for-byte unchanged (the `.func` arm never runs for them).
Third of four fix-0102 sub-steps — the behaviour fix for NORMAL call sites.
Adds THE bare-name resolver `resolveBareCallee(name, caller_file)` over
fix-0102a's `module_fns` + `flat_import_graph` and routes the primary call
path through it:
- own-author wins: a file's bare call to a name IT authors binds its OWN
author, not the first-wins merge winner. (When the winner already is the
caller's own — every single-author and first-importer case — the resolver
returns `.none` so the existing path binds it byte-for-byte.)
- a bare call to a name two or more FLAT imports both provide is `.ambiguous`
and rejected with a loud diagnostic ("declared by multiple imported
modules — qualify the call"); a namespaced author never collides.
- a single flat-reachable author that differs from the winner binds that
author; otherwise `.none`.
The resolved shadow author lowers into its OWN FuncId via fix-0102b's
identity-addressable `lowerFunctionBodyInto` (shared `bareAuthorFuncId`
helper, also used by `lowerRetainedSameNameAuthors`). Only plain free
functions route — generic / comptime / foreign / builtin authors and any
scope-mangled / UFCS-aliased / locally-shadowed name fall straight to the
existing dispatch, so single-author / local / std / qualified resolution is
unchanged (full example suite stays green, including bundle.sx and the
comptime format/pack examples).
Examples 0722 (flat file per-source bind), 0723 (flat vs namespaced, no false
ambiguity), 0724 (ambiguous → diagnostic), 0725 (flat directory per-source
bind), 0727 (user namespace literally named __m0). Each fails on
wt-fix-0102-base (first-wins mis-bind / no diagnostic) and passes here. The
fix-0102b unit test now calls a per-module wrapper (main can't bare-call the
2-author name) and asserts the resolver's three variants directly.
Gate: zig build, zig build test (400/400), bash tests/run_examples.sh
(462 passed) all green.
scanDecls declared a bare `.fn_decl` via `declareFunction(&fd, ...)`,
where `fd` is the switch-capture COPY of `decl.data.fn_decl`. Its
address is a per-iteration stack temporary, so the winner author's
`fn_decl_fids` entry was keyed by an address no later decl-identity
lookup can reproduce — `fn_ast_map` and `module_fns` carry the stable
`&decl.data.fn_decl`, so a lookup by that pointer missed the winner's
FuncId. fix-0102c routes calls through exactly these stable pointers,
so the key has to match.
Record the entry under `&decl.data.fn_decl` (the persistent AST node
field) to match `fn_ast_map`/`module_fns`. The other declareFunction
sites already pass stable pointers (const_decl field, module_fns entry,
fn_ast_map entry, struct-method node field, heap-synthesized objc decl);
`lowered_fids` keys by FuncId value, so neither has the temporary-address
mistake.
Strengthen the fix-0102b regression test: assert the identity map
round-trips by the STABLE pointer for BOTH same-name authors — the
winner's `fn_ast_map` pointer resolves to the first-wins FuncId, and the
shadow's `module_fns` pointer resolves to a distinct FuncId. This
assertion fails on the pre-fix code (winner keyed by `&fd` → null) and
passes after. Call resolution unchanged (name path still default).
Gate (this worktree): zig build, zig build test (400/400),
bash tests/run_examples.sh (457 passed) all green.
Second of four fix-0102 sub-steps. Makes function declaration + body
lowering addressable by decl/FuncId IDENTITY instead of name-first-wins,
so two same-name authors can each carry their OWN body in their OWN
FuncId. Purely additive: the existing name path stays the sole resolver,
so the suite is byte-for-byte unchanged (no call rerouting — that is
0102c).
- declareFunction records `*const FnDecl -> FuncId` in a new identity map
(`fn_decl_fids`), alongside the existing name-keyed function table.
- Extract the body-lowering tail of lazyLowerFunction into a reusable
`lowerFunctionBodyInto(fd, fid, name)` that promotes a SPECIFIC extern
stub into a real body by EXPLICIT FuncId — not by name lookup (which
returns the first author). The shared save/restore preamble becomes a
`FnBodyReentry` guard struct, used by both lazyLowerFunction's found
path and the null-FuncId `ns.fn` alias path; issue-0100 F1/F2 behaviour
(own-import source context, block_terminated transparency) is preserved.
- Add `lowerRetainedSameNameAuthors`: walks fix-0102a's `module_fns`,
and for each SHADOWED flat author (a same-name author that is not the
fn_ast_map winner, in a direct flat import of the main file) declares a
fresh same-name FuncId + lowers its body in its own module's visibility
context. FuncId-keyed `lowered_fids` tracks which slots already have a
body. Not invoked during a default compile (the name path stays the
default); 0102c wires it into bare-flat-call routing.
- lower.test.zig: regression that compiles two flat-imported modules each
authoring `greet` and asserts ONE real body before the pass (winner
only; shadow dropped) and TWO distinct non-extern bodies after — the
shadowed author is no longer dropped/extern.
Gate (this worktree): zig build, zig build test (400/400),
bash tests/run_examples.sh (457 passed) all green.
lazyLowerFunction's three exit paths (non-null branch, already-promoted
early return, null-FuncId `ns.fn` qualified-alias branch) each duplicated
the caller-state restore, and the null branch's copy had drifted: it
restored every saved field EXCEPT `block_terminated`. A qualified alias
whose body terminates (e.g. a constant-folded `if true { return ... }`)
leaves `block_terminated = true` after lowerFunction; the null path
returned without resetting it, so the flag leaked into the CALLER's body
lowering and the caller's own trailing statements / `return` were rejected
as dead-after-terminator ("function ... body produces no value").
Fix: collapse the three restores into a single `defer` registered right
after the state is saved, so every exit path restores the identical full
set and the class cannot diverge again. Fields restored on all paths:
current_source_file (F1), scope, func_defer_base, block_terminated (F2),
force_block_value, builder.func/current_block/inst_counter. The
foreign-class / jni-env / pack-mono / inline-return fields already had
their own defers and are unchanged.
Regression: examples/0721-modules-qualified-terminating-callee.sx — a
qualified alias `m.foo` folds `if true { return helper(); }` (helper from
m.sx's own import) and is followed by caller statements + the caller's own
`return 0`. Reports "body produces no value" pre-fix; prints
"terminating-callee: ok" / "after" and exits 0 after. 0719 (collision) and
0720 (F1 own-import visibility) stay green. issues/0100 RESOLVED banner
extended with the F2 follow-up.
The 0100 identity fix registers a namespaced import's own functions under a
module-qualified name (ns.fn) in fn_ast_map WITHOUT an eager declareFunction,
so the alias is lowered through lazyLowerFunction's null-FuncId lowerFunction
path. That path had no Function.source_file to restore (the non-null path does
setCurrentSourceFile(func.source_file)), so the alias lowered in the CALLER's
visibility context. A qualified function that called a helper from its OWN
module's flat import was then rejected "not visible".
Fix:
- ProgramIndex.qualified_fn_source maps each ns.fn alias to its declaring
source file, populated in registerQualifiedFn (current_source_file is
pinned to the decl's source by registerNamespaceQualifiedFns).
- lazyLowerFunction's null-FuncId branch restores that source before
lowerFunction, so ns.fn's body lowers in its own module's context and its
intra-module / own-import callees resolve.
- lowerFunction records Function.source_file = current_source_file on the
freshly-begun function (matching declareFunction), so the lowered alias
carries its own module for diagnostics/emit.
Regression: examples/0720-modules-qualified-own-import.sx — calc.compute (a
qualified alias) calls triple/base from calc.sx's own flat import; reports
"'triple' is not visible" on the attempt-1 code, passes after. 0719's
cross-module dual-parse assertion stays green. issues/0100 RESOLVED banner
extended with the F1 follow-up.
Two modules each exporting a top-level function with the same short name
(std.cli.parse 3-param, std.json.parse 2-param) collided in IR lowering's
bare-name function table. fn_ast_map (name -> AST) was last-wins while
module.functions / resolveFuncByName are first-wins, so importing both and
calling one bound one function's AST against the other's FuncId and tripped
lazyLowerFunction's param-count assert (lower.zig:1606) — reached
unreachable code.
Fix:
- Register a namespaced import's OWN plain functions under their qualified
name (ns.fn) in fn_ast_map, giving cli.parse / json.parse independent
identities. The qualified resolution paths in CallResolver.plan /
lowerCall already prefer ns.fn. NamespaceDecl now carries own_decls
(populated in imports.addNamespace). Generic/comptime/pack/foreign
functions are excluded (they dispatch by monomorphization off the bare
template name); no eager declareFunction (it would resolve types before
the forward-alias fixpoint).
- Make scanDecls' bare fn_ast_map registration first-wins so a later
namespace recursion cannot clobber an earlier (flat) entry, aligning it
with mergeFlat / resolveFuncByName.
Regression: examples/0719-modules-cli-and-json.sx imports both std.cli and
std.json under distinct namespaces and calls both parses; panics pre-fix,
passes after. issues/0100 marked RESOLVED.
Attempt-1 narrowed lowerReturn's target to failableSuccessType(ret_ty) for
every value-carrying failable. That fixed the bare-enum success slot but
introduced two defects (attempt-2 review):
F1 — explicit full failable tuple `return (.v, error.X)` panicked. With the
target narrowed to the value type, the trailing error element no longer
resolved against the error set, leaving an `.unresolved` tuple field that
tripped "unresolved type reached LLVM emission" in backend/llvm/types.zig.
F2 — a `-> (Enum, !E)` body with a comptime parameter is inlined
(lowerComptimeCall), so its success `return .red` took the inline-return path,
which the first cut skipped: it stored `{value, undef}` (error slot undef) into
the inline slot, so the success error slot read garbage at runtime.
Fix: choose the return-expr target via failableReturnTarget(ret_ty, value_node)
— a BARE value resolves against failableSuccessType (real enum ordinal), while
an EXPLICIT full failable tuple literal (arity == full-tuple field count) keeps
the full-tuple target and is forwarded as-is. This applies on the inline path
too, and the inline value-failable return now routes through
lowerFailableSuccessReturn (whose emitTupleRet stores `{value, 0}` into the
inline slot + branches), so the success error slot is 0 there as well.
Regression: examples/1056-errors-enum-value-failable-tuple-and-comptime.sx —
F1 explicit-tuple error return + bare-value success in one fn (no panic, slot 0
on success, tag 1 on error); F2 comptime-param enum value-failable read at
runtime on the success path (cast, bare if, == error.X) + error path. Reads the
slot at runtime so an undef is caught, not masked by the `if !e` proof.
examples/1055 + the original 0097 repro still pass. Gate: zig build 0,
zig build test 0, run_examples.sh 453 ok / 0 failed / 0 timed out.
A `-> (Enum, !E)` `return .variant` lowered the enum literal with
`target_type` set to the full failable tuple `(Enum, !E)` instead of the
success value type. The bare literal resolves its tag against `target_type`;
against a tuple it matched no variant (silent tag 0) and was stamped with the
tuple type, so `lowerFailableSuccessReturn` saw `val_ty == ret_ty` and took the
forwarding branch — returning the half-built `{value, undef}` aggregate and
never appending the `0` error slot. Every runtime read of the slot on the
success path (`cast(s64) e`, bare `if e`, `e == error.X`) saw garbage nonzero;
only the compile-time `if !e` proof masked it. The s32 case was already correct
because integer literals don't resolve variants against `target_type`.
Fix: in lowerReturn, narrow `target_type` to `failableSuccessType(ret_ty)` for
a value-carrying failable before lowering the returned expression. The enum
literal then resolves to its real ordinal and is typed as the value type, so
the success path correctly appends `0`. Forwarding (`return call()` / explicit
`(v, e)`) is unaffected — those still yield a value typed equal to the tuple.
Regression: examples/1055-errors-enum-value-failable-error-slot.sx reads the
error slot at runtime on the success path (cast, bare if, == error.X), checks a
non-zero ordinal (.blue=2, also corrupted to 0 pre-fix), and asserts the error
path still carries the right tag + error_tag_name. Fails pre-fix, passes after.
The shared compile-time integer folder (`evalConstIntExpr`) accepts an
integral float literal/const as an integer leaf (`[4.0]` → 4) and then
applied INTEGER arithmetic to the whole expression — so `5.0 / 2.0` folded
as `divTrunc(5,2)` = 2 instead of float division (`2.5`). The bug fired at
all FIVE unified-rule sites (typed local, field default, param default,
typed const, array dimension), because the typed sites evaluate through
`evalConstFloatExpr` (which delegates the node to the int folder) and the
count sites through `foldCountI64` (int folder first).
Fix at the single root: `evalConstIntExpr`'s `.div` arm refuses to fold a
division whose lhs/rhs is float-valued (`isFloatValuedExpr`), so the value
surfaces through `evalConstFloatExpr` + the unified rule — an integral
quotient (`6.0 / 2.0` → 3) folds, a non-integral one (`5.0 / 2.0` = 2.5,
mixed `5 / 2.0`, float-const `F / G`) errors. Genuine integer `/` (`5 / 2`
→ 2) is unchanged; `*`/`+`/`-` need no guard (they agree between int and
float for the integral operands the int folder ever sees).
`isFloatValuedExpr` judges a const-leaf by VALUE (`moduleConstIsFloatTyped`
recurses into the const's value with the existing cycle-guard frame), so an
untyped float-EXPRESSION const (`ME :: 4.0 + 1.0`, placeholder type s64) is
caught at both the count path and — via `foldComptimeFloatInit`'s guard —
the typed-binding path. A backtick RAW receiver (`` `f64.epsilon ``) is a
field read, not a float limit (is_raw check, issues 0092/0093).
Regression: examples/1147 (negative — `5.0 / 2.0` errors at all five sites
plus untyped float-EXPR const div); 0168 extended (positive — `6.0 / 2.0`,
`12.0 / 4.0`, `[6.0/2.0]`, `xx (5.0/2.0)` → 2); unit tests "the int folder
refuses a FLOAT division" and "moduleConstIsFloatTyped judges a const by
VALUE". specs.md + readme.md state the float-`/` rule.
The compile-time count fold (array dimension / Vector lane / value-param) was
integer-only: it folded a DIRECT integral float literal (`[4.0]`, `[N]` with
`N : f64 : 4.0`) but rejected an INTEGRAL expression built from a non-integral
float-const leaf (`[F + 1.5]` = 4.0, `F : f64 : 2.5`) — and a const folded from
one (`[K]` with `K : s64 : F + 1.5`) — as "must be a compile-time integer
constant". This was the last of issue 0095's five narrowing sites (local /
field / param / const / array-dim) still diverging.
Route the count fold through the SAME compile-time float evaluation the other
four sites use:
- New `program_index.foldCountI64` — the single int-or-integral-float count
fold: `evalConstIntExpr` first, then (only on failure) `evalConstFloatExpr` +
`floatToIntExact`. `foldDimU32` (dim/lane/u32 value-param), the non-u32
value-param gate, and `emitModuleConst`'s integer-const materialization all
delegate to it, so a const's emitted value and its use as a count come from
one fold (no parallel integral check, no two-resolver divergence — issue 0083).
- New `DimU32.non_integral_float` variant carries a non-integral float dim to a
distinct, accurate diagnostic ("array dimension must be an integer, but '2.75'
is a non-integral float") — the cast-escape advice the binding sites give does
not apply in a count position, so the dim wording omits it. `reportDimError`,
the Vector-lane resolver, and the top-level array-alias diagnostic all handle
the new variant, so the DIRECT and type-ALIAS forms emit the identical message.
- `type_bridge.StatelessInner.lookupFloatName` (via `moduleConstFloat`) is the
float twin of its `lookupDimName`, so the registration-time alias path folds a
float-const-leaf dimension to the SAME count as the stateful direct path.
`inline for` range bounds are spec endpoints, not counts (specs.md §2), so they
keep the int-only fold deliberately (no silent-truncation bug there).
Relaxes the F0.4 `examples/1132` wording: a non-integral float const dim now
reports the precise "non-integral float" message (it still errors).
Regression: 0168 (positive — `[F + 1.5]s64`, `[KF]s64`, alias `ArrFE` all fold
to len 4), 1146 (negative — `[F + 0.25]s64` errors), 1132 (precise wording), and
a `foldCountI64`/`foldDimU32` unit test. issues/0095 marked RESOLVED (attempt 4).
specs.md + readme.md state the unified rule across all five sites.
Completes issue 0095: a non-integral float→int narrowing via a FLOAT-const
leaf (`F : f64 : 2.5; y : s64 = F + 0.25` = 2.75) silently truncated to 2.
`evalConstFloatExpr` delegated only INTEGER leaves to `evalConstIntExpr` and
had no float-const leaf arm, so the unified rule never saw the value.
- program_index.zig: add `moduleConstFloat`/`moduleConstFloatFramed` — the f64
twin of `moduleConstInt` (same `isCountableConstType` gate, same cyclic-
definition frame), recovering a numeric module const's value via
`evalConstFloatExpr`. Add `lookupFloatName` to `ModuleConstCtx` and the
`.identifier`/`.type_expr` leaf arms to `evalConstFloatExpr` that call it.
Integer / integral-float leaves keep resolving through the existing
`evalConstIntExpr` delegation, so the unified rule now applies to ANY
compile-time-constant float expression — literal, int-const leaf, float-const
leaf, and combinations — at every binding site.
- lower.zig: add `Lowering.lookupFloatName` delegating to `moduleConstFloat`.
Route `typedConstInitFits`' integral-fold check through `evalConstFloatExpr` +
`floatToIntExact` (the SAME facility `foldComptimeFloatInit` uses) instead of
the int-only `evalComptimeInt`, which folded leaf-by-leaf in i64 and so
rejected an integral SUM built from a non-integral float leaf
(`K : s64 : F + 1.5` = 4.0 now folds; `K : s64 : F + 0.25` errors).
A LOCAL `::` const leaf is a scope ref (not in the const tables) so neither
the int nor float evaluator folds it — float now matches int exactly there.
Regression: examples/1146 (negative) + 0168 (positive) extended with
float-const-leaf cases at local/field/param/const; unit test in
program_index.test.zig covers the leaf resolution (F→2.5, F+0.25→2.75,
F+1.5→4.0). specs.md + readme.md state the rule covers any compile-time-const
float expression incl. float-typed const leaves. issues/0095 banner updated.
Gate: zig build + zig build test green; 447 examples pass, 0 failed.
Completes issue 0095 (attempt 2). The attempt-1 coerce arm only caught a direct
`const_float` literal, so a non-integral const-folded float EXPRESSION still
truncated silently at a typed local / field default / param default:
M :: 2;
local : s64 = M + 0.5; // → 2 (silent truncation — BUG; now ERRORS)
fld : s64 = M + 0.5; // field default — same
take(x : s64 = M + 0.5) // param default — same
while the typed-CONST site already errored. The integral expression
(`M + 2.0` → 4) folded but the runtime/explicit-cast paths must stay untouched.
Fix:
- New `program_index.evalConstFloatExpr` — the f64 counterpart to
`evalConstIntExpr`, delegating every integer subtree back to it (no parallel
integer logic) and adding only the float literal / unary-negate / `+ - * /`
arms. Pure (no diagnostics, no resolution side effects).
- `Lowering.foldComptimeFloatInit` applies the unified rule to a typed-binding
initializer EXPRESSION: an integral comptime float folds to its `constInt`, a
non-integral one errors, a genuine runtime float / `xx`-cast falls through to
the normal path. It runs `evalConstFloatExpr` FIRST (pure) so a `$pack[i]`
argument is never spuriously type-resolved outside an active binding, then
gates on `isFloat(inferExprType)` so a plain comptime int is left alone.
Wired into the typed-local path, the three struct field-default sites (via a
shared `lowerCoercedDefault`), and the call-argument loop (covers expanded
param defaults).
- One `Lowering.diagNonIntegralNarrow` now emits the narrowing wording at all
five sites (coerce arm, global init, const-expr value, the typed-binding
sites, and the typed-const path). The typed-CONST non-integral diagnostic
therefore reads "cannot implicitly narrow non-integral float …" instead of
the stale "initializer is a float literal / floating-point expression".
Tests: examples/1146 (negative) extended with non-integral const-EXPRESSION
cases at local/field/param; examples/0168 (positive) extended with integral
const-EXPRESSION folds and `xx (M + 0.5)` truncation; examples/1143 reconciled
to the aligned const message (G/BAD/BAD2 stay errors); unit test
`evalConstFloatExpr folds comptime float expressions`. Full gate green (447).
Issue 0095: a typed local/param/field silently TRUNCATED a float initializer
to an integer annotation (`y : s64 = 1.5` → 1) with no diagnostic. Agra ruled
the UNIFIED rule (Option B): an implicit float→int in a typed binding behaves
like the array-dimension rule —
- an INTEGRAL compile-time float FOLDS to its int (`4.0` → 4, `-2.0` → -2);
- a NON-integral float is a COMPILE ERROR (`1.5`, `4.5`);
- explicit `xx` / `cast(T)` ALWAYS truncates (the escape hatch).
Applied consistently to typed local / param-default / field-default, typed
module CONST, and array dim — all reusing the single
`program_index.floatToIntExact` / `evalConstIntExpr` facility (no second
integral check).
- `Builder.constFloatInfo` reads a compile-time `const_float` back from its
Ref (value + span).
- `coerceToType` is now the IMPLICIT path: its `.float_to_int` arm folds an
integral const-float to `constInt`, else emits the narrowing diagnostic.
`coerceExplicit` is the raw truncating path; `xx` (lowerXX) and `cast(T)`
route through it so the escape still truncates.
- Field-default lowering (struct-literal pad, named-field default,
buildDefaultValue) now coerces the default to the field type at the IR level
(was silently bit-coerced by emitStructInit).
- Const path: `typedConstInitFits` accepts an integral float (literal or a
`M + 2.0`-style expression folding via `evalComptimeInt`); `emitModuleConst`
/ `constExprValue` / `globalInitValue` fold an integral float to its int and
reject a non-integral one — relaxing F0.7's blanket float rejection.
Tests: examples/0168 (positive: local/field/param/const fold, xx/cast
truncate), examples/1146 (negative: local/param/field error), integral-float
const cases added to examples/0162; non-integral const cases in 1143 stay
errors. specs.md + readme.md document the unified rule, cross-referencing the
array-dim rule. issues/0095 marked RESOLVED.
Migrate lowerAssignment's `.field_access` target onto the shared
`fieldLvaluePtr` resolver, deleting its duplicated union / promoted /
tuple / vector / struct walk. All three lvalue field-store sites —
single-assign, address-of (lowerExprAsPtr), and multi-assign
(lowerMultiAssign) — now resolve through the one resolver, removing the
issue-0083 two-resolver divergence.
Fold vector-lane resolution into `fieldLvaluePtr` (reusing
vectorLaneIndex) so the single resolver covers struct fields, union
direct fields, promoted anonymous-struct union members, tuple elements,
and vector lanes — null only on a genuine miss, which every caller turns
into the read path's `emitFieldError` diagnostic.
`fieldLvaluePtr` now types every field GEP `*field_ty` (the convention
the single-assign path always used), not the bare field value type:
emitStore unwraps one pointer level to find the stored value's type.
The earlier lowerExprAsPtr / lowerMultiAssign walks typed the GEP with
the bare field type, so a field whose own type is a pointer-to-aggregate
(`*Pair`, a two-pointer struct) made emitStore unwrap to the aggregate
and coerceArg's closure auto-promotion store a 16-byte `{ptr,null}`
struct over the 8-byte slot, clobbering the neighbouring field.
Consolidating onto the one `*field_ty` resolver preserves single-assign
and fixes that pre-existing multi-assign / address-of clobber.
The types.zig `.unresolved` tripwire is untouched; no `.s64` / `.void` /
`.unresolved` default remains.
Regression: examples/0167-types-ptr-to-aggregate-field-store.sx (a
`*Pair` field stored via all three lvalue sites leaves the neighbour
intact) + a lowering unit test asserting the `*field_ty` GEP convention.
Completes the issue-0094 fix. attempt-1 made single-assign and address-of
diagnose a missing struct field; the stress-review found two remaining defects
in that change:
1. lowerMultiAssign's `.field_access` target kept the pre-fix shape — a
struct-only loop that defaulted `field_idx 0` / `field_ty .unresolved` on a
miss, then built the GEP and stored unconditionally. A missing field
(`p.q, y = 2, 3`) silently wrote field 0 (printed `x=2 y=3`, no diagnostic),
and a valid promoted-union / tuple member at a non-zero offset corrupted
field 0 instead of its own slot.
2. attempt-1's new union branch in lowerExprAsPtr resolved only DIRECT union
field names, so `@v.x` on a promoted anonymous-struct member reported
"field 'x' not found on type 'Vec2'" even though `v.x = 41` worked.
Both lvalue-pointer sites and the multi-assign store now route through one
shared resolver, `fieldLvaluePtr`, that handles struct fields, union direct
fields, promoted anonymous-struct union members, and tuple elements, and
returns null (no field-0 / `.unresolved` default) on a genuine miss. Each
caller emits the read path's `emitFieldError` on null. This collapses the
three previously-divergent field-lvalue walks into one, fixing the
multi-assign missing-field corruption, the promoted-member over-rejection,
and (as a side effect of correct resolution) non-zero-offset promoted-union
and tuple multi-assign stores. The types.zig tripwire is untouched.
Regression tests:
- examples/1145 extended: multi-assign missing field (`p.r, y`) errors, exit 1.
- examples/0166 (new): promoted union member written and address-of'd,
including a non-zero-offset member (`@v.y`), compiles and runs.
- src/ir/lower.test.zig: multi-assign missing-field field-not-found unit test.
Assigning to a nonexistent struct field (`p.q = 2` where Point has no `q`)
aborted the compiler with the `.unresolved` LLVM tripwire instead of a source
diagnostic (issue 0094). The lvalue field lookup never diagnosed a miss:
- `lowerAssignment`'s `.field_access` target left `field_ty = .unresolved` when
no struct field matched, then built `ptrTo(field_ty)` and stored — so a
pointer-to-`.unresolved` reached LLVM emission and tripped the panic.
- `lowerExprAsPtr`'s `.field_access` fallback returned
`structGepTyped(obj_ptr, 0, .s64, obj_ty)` on a miss — a silent field-0/`.s64`
default that mislowered the lvalue.
Both sites now reuse the read path's `emitFieldError` (the exact facility
`lowerFieldAccessOnType` uses), so read and write reject identically with
`field 'q' not found on type 'Point'`. `lowerExprAsPtr` also resolves
union/tagged-union fields via `union_gep` (the old `.s64` fallback was silently
standing in for union field access — e.g. `u.a[0] = v`), so that path is fixed,
not just made loud. The `types.zig` tripwire is untouched: the fix is to never
produce `.unresolved` for a missing-field store.
Regression tests:
- examples/1145-diagnostics-missing-struct-field-assign.sx — negative, both
sites error, exit 1.
- examples/0165-types-nested-struct-field-assign.sx — positive, nested struct
field write + address-of a matched field still work.
- src/ir/lower.test.zig — lowering unit test asserting the field-not-found
diagnostic for a missing-field assignment.
size_of, align_of, field_count, type_name, type_eq, type_is_unsigned,
and is_flags silently reinterpreted a value argument as a type:
type_is_unsigned(6) read 6 as a TypeId index (types[6] = u8 -> true),
size_of(6)/size_of(true) sized its typeof (8), type_name(6) returned
types[6]'s name. Per Agra's ruling, all 7 now strictly require a type
(compile-time): a value argument is a compile error.
One shared guard (Lowering.reflectionTypeArgGuard, run at the top of
tryLowerReflectionCall) classifies each arg via reflectionArgIsType: a
spelled / compile-time type or generic type parameter (isStaticTypeArg),
or a runtime Type value (static type .any -- type_of(x), a []Type
element list[i], a Type-typed local/field/param) is accepted; anything
else is rejected with "<builtin> expects a type, got '<type>'". The
runtime path for type_name / type_is_unsigned is preserved (the {}
formatter calls type_is_unsigned(type_of(val)) at runtime). The 5
comptime-only builtins stay comptime-only (runtime reflection deferred).
Regression: examples/1144-diagnostics-reflection-builtin-needs-type.sx
(reject cases across all 7, exit 1). Unit test: reflectionArgIsType in
lower.test.zig. specs.md / readme.md document the strict type
requirement (and add the previously-undocumented align_of, type_eq,
type_is_unsigned). issues/0090 RESOLVED banner updated.
Resolves issue 0090. The `{}` integer formatter mis-rendered both ends of
the 64-bit range:
- `int_to_string` computed the magnitude as `0 - n`, which overflows for
`s64::MIN` (its magnitude is unrepresentable as a positive s64) — the
value stayed negative, the digit loop ran zero times, so only `-`
printed. It now extracts digits straight from `n` (per-digit
`|n % 10|`, `n` truncating toward zero), never negating MIN.
- `any_to_string`'s `case int:` formatted every integer as s64, so a u64
all-ones value printed as `-1`. There was no `uint` type-category to
distinguish signedness. Added an additive `type_is_unsigned(T)`
reflection builtin (static fold + dynamic interp/LLVM paths, mirroring
`type_name`), backed by the new `TypeTable.isUnsignedInt` predicate, and
a `uint_to_string` formatter (unsigned decimal via long-division over
four 16-bit limbs). `case int:` routes through `type_is_unsigned(type)`.
The 16-bit-limb split is factored into a shared `decompose_u16x4`, now
reused by `int_to_hex_string` (no second unsigned-math routine).
Regression: examples/0046-basic-int-formatter-extremes pins both extremes
plus a width spread; unit tests cover `isUnsignedInt`. Docs (specs.md
representation note, readme std API) updated for unsigned/extreme `{}`
behavior. IR snapshots refreshed for the two new std functions.
`ExprTyper.inferType`'s binary-op arm inferred every non-comparison op
from the LHS alone, so `M + 0.5` (s64 + f64) statically typed as s64
while `0.5 + M` typed as f64 — operand-order-dependent. The value path
(`lowerBinaryOp`) already promoted int×float → float, so static
inference disagreed with the value: `M + 0.5` formatted as a truncated
int and a typed const `BAD : s64 : M + 0.5` was accepted+truncated
(issue 0088 mixed-numeric escape).
Extract the value path's inline promotion into a shared
`Lowering.arithResultType(lhs, rhs)` and reuse it at both sites, so
arithmetic / bitwise / shift inference reports exactly the type the
lowered value carries — int LHS × float RHS → the float, order-
independent. The value-path behavior is unchanged (the block is moved
verbatim into the helper), so no IR shifts; the suite stays green. The
typed-const validation reuses `inferExprType`, so this auto-closes the
escape with no change to the validation logic.
- examples/1143: BAD/BAD2 (`s64 : M + 0.5`, `s64 : 0.5 + M`) rejected
in both operand orders.
- examples/0162: MF/MFR (`f64 : M + 0.5`, `f64 : 0.5 + M`) fold to 2.5.
- examples/0163 (new): pins the inference fix in a value context
(`print("{}", n + 0.5)` formats the float, both orders, +-*/, f32).
- expr_typer.test.zig: arithResultType + mixed-arithmetic inference.
- specs.md / readme.md: document the numeric-promotion rule.
- issues/0088: RESOLVED banner notes the inferExprType root fix.
Attempt 1 rejected only LITERAL initializers that mismatch a typed module
const's annotation; a const-EXPRESSION initializer escaped, so the same
issue-0088 root remained for `M :: 2; N : string : M + 2` — accepted at exit 0,
folding `[N]s64` to 4 and printing N as an integer.
Root cause: `registerTypedModuleConst` validated only the enumerated literal
node kinds; any other kind fell through to `else => {}`, and pass 0
pre-registers binary_op/unary_op consts as a `.s64` placeholder that was never
reconciled with the annotation.
Fix — validate by TYPE, not by node kind:
- lower.zig: `registerTypedModuleConst` now covers literals AND const-expressions
(binary_op/unary_op) through one path. `typedConstInitFits` keeps the literal
arms and routes any non-literal through the new `constExprInitFits`, which
compares the initializer's INFERRED type (`inferExprType`, the existing
type-inference facility — no second const evaluator) to the annotation with the
same integer/float compatibility. A mismatch emits the `type mismatch` diagnostic
(a const-expression is described by its inferred type, e.g. "an integer
expression") and evicts the pass-0 placeholder; a match registers the const at
its resolved annotation type (the same `put` the literal path always did), so a
const-expression folds and emits at its declared type.
- `literalKindName` → `initializerDescription` (+ `constExprDescription`) so the
message is accurate for both a literal and a const-expression initializer.
Regression:
- examples/1143: extended with `E : string : M + 2` and `V : string : -M`
(const-expr mismatches → exit 1, pinned diagnostics).
- examples/0162: extended with `KE : s64 : M + 2` (used as a count + printed) and
`WE : f32 : M + 2` (over-rejection guard — valid const-exprs still work).
- program_index.test.zig: count-gate test extended with a binary_op value node
declared `string` (must not fold as a count).
Docs: specs.md §3 + readme.md generalized from "initializer literal" to cover
constant expressions; issues/0088 RESOLVED banner updated.
A typed module-level constant whose initializer did not match its
annotation was silently accepted: `N : string : 4` compiled, then
`print(N)` segfaulted (an integer emitted as a `string` const → a bogus
pointer) and `[N]s64` folded `N` to 4 as an integer count. Issue 0088.
Root cause: `registerTypedModuleConst` stored the annotation type but never
validated the initializer literal against it, and
`program_index.moduleConstInt` folded a const into a count by inspecting
the initializer node alone, ignoring `ModuleConstInfo.ty`.
Fix at the declaration (kills both symptoms):
- lower.zig: `registerTypedModuleConst` now validates the initializer via
`typedConstInitFits` (arms mirror `emitModuleConst`'s faithful-emit
precondition: int→int/float, float→float, bool→bool, string→string,
null→pointer/optional, `---`→any). A mismatch emits a `type mismatch`
diagnostic at the initializer span and does not register the const (also
evicting the pass-0 placeholder). Not routed through
`coercionResolver().classify`: that runtime-coercion planner is unsound
here (null's natural type is void → false-rejects `*T`; bool is 1 bit →
false-accepts s64).
- program_index.zig: `moduleConstInt` now takes the `TypeTable` and gates
the fold on `isCountableConstType(ci.ty)` (integer of any width, or a
float), so a non-numeric typed const can never fold into a count off its
initializer node. Callers in lower.zig and type_bridge.zig updated.
Regression:
- examples/1143-diagnostics-typed-module-const-mismatch.sx (negative, exit 1)
- examples/0162-types-typed-module-const-roundtrip.sx (positive)
- program_index.test.zig: gate-on-declared-type unit test
Docs: specs.md §3 Constant Binding + readme.md note the compatibility rule.