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.
6.0 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.
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 inregisterQualifiedFnfrom the decl's own source. lazyLowerFunction's null-FuncId branch restores that source viasetCurrentSourceFilebefore callinglowerFunction, sons.fn's body lowers in its own module's context and its own-import callees resolve.lowerFunctionnow recordsFunction.source_file = current_source_fileon the freshly-begun function (matchingdeclareFunction), 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 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, 472 examples)bash tests/run_examples.sh→ 455 passed, 0 failedexamples/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).