# 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 / foreign 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 :: () -> s32 { 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. ## 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 :: () -> s32 { 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, 472 examples) - `bash tests/run_examples.sh` → 455 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. Regression tests: `examples/0719-modules-cli-and-json.sx` (collision), `examples/0720-modules-qualified-own-import.sx` (F1 own-import visibility).