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.
4.1 KiB
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):
- Module-qualified identity. A namespaced import's OWN plain functions
are now registered under their qualified name (
ns.fn) infn_ast_map, givingcli.parse/json.parseindependent identities. The qualified resolution paths inCallResolver.planandlowerCallalready preferns.fn— they just had nothing to find.NamespaceDeclcarries the module'sown_decls(populated inimports.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 plainresolveFuncByNamepath, so a qualified alias would strand their per-call type bindings. The qualified function is declared + lowered on demand bylazyLowerFunction's null-FuncId path (no eagerdeclareFunction, which would resolve types before the forward-alias fixpoint). - First-wins bare registration.
scanDeclsno longer lets a later namespace recursion clobber an existing barefn_ast_mapentry, aligning it withmergeFlat/resolveFuncByName. A bareparsewith 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.
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 codeatsrc/ir/lower.zig:1606(lazyLowerFunction) vialowerCall. - Expected: each
pkg.fn(...)resolves to its own module's function; the program compiles and runs.
Reproduction
#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→ 0zig build test→ 0 (incl. LSP corpus sweep, 471 examples)bash tests/run_examples.sh→ 454 passed, 0 failedexamples/0719-modules-cli-and-json.sx: panics pre-fix, passes post-fix.
Regression test: examples/0719-modules-cli-and-json.sx.