# 0100 — cross-module same-name function lowering collision **RESOLVED.** Two modules each exporting a top-level function with the same short name (`std.cli.parse`, 3 params; `std.json.parse`, 2 params) collided in IR lowering's bare-name function table. `fn_ast_map` (short name → AST) was **last-wins**, while `module.functions` / `resolveFuncByName` are **first-wins**, so importing both modules and calling one bound the AST of one function against the FuncId of the other and tripped `lazyLowerFunction`'s param-count assert (`src/ir/lower.zig:1606`, `func.params.len == fd.params.len + ctx_slots`) — `panic: reached unreachable code`. Qualified imports (`j :: #import`) did not help: lowering keyed everything by short name, so `j.parse` and a bare `parse` resolved to the same colliding entry. **Fix** (`src/ir/lower.zig`, `src/ast.zig`, `src/imports.zig`): 1. **Module-qualified identity.** A namespaced import's OWN plain functions are now registered under their qualified name (`ns.fn`) in `fn_ast_map`, giving `cli.parse` / `json.parse` independent identities. The qualified resolution paths in `CallResolver.plan` and `lowerCall` already prefer `ns.fn` — they just had nothing to find. `NamespaceDecl` carries the module's `own_decls` (populated in `imports.addNamespace`) so the registration covers authored decls, not transitive flat imports. Generic / comptime / pack / extern functions are excluded — they dispatch by monomorphization off the bare template name, not the plain `resolveFuncByName` path, so a qualified alias would strand their per-call type bindings. The qualified function is declared + lowered on demand by `lazyLowerFunction`'s null-FuncId path (no eager `declareFunction`, which would resolve types before the forward-alias fixpoint). 2. **First-wins bare registration.** `scanDecls` no longer lets a later namespace recursion clobber an existing bare `fn_ast_map` entry, aligning it with `mergeFlat` / `resolveFuncByName`. A bare `parse` with one module flat-imported now consistently resolves to the first (unqualified-scope) function instead of splitting AST/FuncId across modules. Regression: `examples/0719-modules-cli-and-json.sx` imports BOTH `std.cli` and `std.json` under distinct namespaces and calls both `cli.parse` (dispatch) and `json.parse` (document read), asserting correct results. Panics on pre-fix code; passes after. ## F1 follow-up — qualified alias must lower in its own source context The identity fix above registered `ns.fn` in `fn_ast_map` WITHOUT an eager `declareFunction`, so the qualified alias is lowered through `lazyLowerFunction`'s **null-FuncId** `lowerFunction` path — which had no `Function.source_file` to restore (the non-null path does `setCurrentSourceFile(func.source_file)`). The alias therefore lowered in the **caller's** visibility context, and a qualified function calling a helper from **its own module's flat import** was rejected: ``` m :: #import "m.sx"; // m.sx: `#import "helper.sx"; foo :: () { helper() }` main :: () -> i32 { print("{}\n", m.foo()); 0 } // → 'helper' is not visible ``` **Fix** (`src/ir/program_index.zig`, `src/ir/lower.zig`): - New `ProgramIndex.qualified_fn_source` (qualified name → declaring source file), populated in `registerQualifiedFn` from the decl's own source. - `lazyLowerFunction`'s null-FuncId branch restores that source via `setCurrentSourceFile` before calling `lowerFunction`, so `ns.fn`'s body lowers in its own module's context and its own-import callees resolve. - `lowerFunction` now 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. ## F2 follow-up — null-FuncId path must restore the FULL caller lowering state The F1 fix patched the **source file** in `lazyLowerFunction`'s null-FuncId branch, but that branch still restored only a SUBSET of the caller state the non-null branch restores — it omitted `self.block_terminated`. A qualified alias whose body terminates (e.g. a constant-folded `if true { return … }`) leaves `block_terminated = true` after `lowerFunction`; the null branch then returned without resetting it, so the flag leaked into the **caller's** body lowering and the caller's own trailing statements / `return` were treated as dead-after-terminator: ``` m :: #import "m.sx"; // m.sx: `#import "helper.sx"; foo :: () -> i64 { if true { return helper(); } return 0; }` main :: () -> i32 { x := m.foo(); print("after\n"); // dropped return 0; // → error: body produces no value } ``` **Fix** (`src/ir/lower.zig`): the three exit paths of `lazyLowerFunction` (the null-FuncId branch, the already-promoted early return, and the bottom of the non-null branch) duplicated the restore, and the null branch's copy drifted. They are now collapsed into a **single `defer`** registered right after the state is saved, so every exit path restores the identical full set and the class can't diverge again. The fields the defer now restores on all paths: - `current_source_file` (via `setCurrentSourceFile`, which also resyncs `diagnostics.current_source_file`) — F1 - `scope` - `func_defer_base` - `block_terminated` — **F2** (was missing on the null path) - `force_block_value` - `builder.func` - `builder.current_block` - `builder.inst_counter` (The `current_runtime_class`, `jni_env_stack_base`, and pack-mono / `inline_return_target` fields already had their own `defer`s and apply on all paths; they are unchanged.) Regression: `examples/0721-modules-qualified-terminating-callee.sx` — `m.foo` (a qualified alias) folds `if true { return helper(); }` and is followed by caller statements + the caller's own `return 0`. Reports `body produces no value` on the attempt-2 code; prints `terminating-callee: ok` / `after` and exits 0 after. 0719 and 0720 stay green. ## Symptom - **Observed:** a program that imports two modules each exporting a same-named top-level function AND calls one crashes IR lowering: `panic: reached unreachable code` at `src/ir/lower.zig:1606` (`lazyLowerFunction`) via `lowerCall`. - **Expected:** each `pkg.fn(...)` resolves to its own module's function; the program compiles and runs. ## Reproduction ```sx #import "modules/std.sx"; cli :: #import "modules/std/cli.sx"; json :: #import "modules/std/json.sx"; main :: () -> i32 { gpa := GPA.init(); arena := Arena.init(xx gpa, 8192); defer arena.deinit(); cmds : []Command = .[ Command.{ group = "ci", command = "publish", flags = .[] } ]; argv : []string = .["ci", "publish"]; d : Diag = .{}; p, e := cli.parse(argv, cmds, @d); // 3-param cli.parse if e { return 64; } v, je := json.parse("[1,2,3]", xx arena); // 2-param json.parse if je { return 65; } return 0; } ``` Pre-fix: `panic: reached unreachable code` at `src/ir/lower.zig:1606`. Post-fix: compiles and runs (exit 0). ## Root cause `fn_ast_map`, `module.functions` (matched by interned name), and `lowered_functions` were all keyed by a function's SHORT name. Two functions sharing a short name across modules occupied the same key; the `put`-order mismatch (AST last-wins vs FuncId first-wins) drove `lazyLowerFunction` to lower one signature against the other's body. The qualified-call resolution machinery already existed but was never fed module-qualified entries. ## Fix verification - `zig build` → 0 - `zig build test` → 0 (incl. LSP corpus sweep, 473 examples; 397/397 tests) - `bash tests/run_examples.sh` → 456 passed, 0 failed - `examples/0719-modules-cli-and-json.sx`: panics pre-fix, passes post-fix. - `examples/0720-modules-qualified-own-import.sx`: `'… is not visible'` on the attempt-1 code, passes after the F1 fix. - `examples/0721-modules-qualified-terminating-callee.sx`: `body produces no value` on the attempt-2 code, passes after the F2 fix. Regression tests: `examples/0719-modules-cli-and-json.sx` (collision), `examples/0720-modules-qualified-own-import.sx` (F1 own-import visibility), `examples/0721-modules-qualified-terminating-callee.sx` (F2 terminating qualified callee — caller state transparency).