Merge branch 'wt-fix-0102-base'
This commit is contained in:
18
examples/0722-modules-flat-same-name-own.sx
Normal file
18
examples/0722-modules-flat-same-name-own.sx
Normal file
@@ -0,0 +1,18 @@
|
||||
// fix-0102c (issue 0102): two flat FILE imports each author a same-name free
|
||||
// function `greet`. The first-wins import merge keeps exactly one `greet` in
|
||||
// the merged scope, but each module's OWN code must bind its OWN author when it
|
||||
// calls `greet` bare. `from_a` (in a.sx) returns 1; `from_b` (in b.sx) returns
|
||||
// 2 — per-source binding, resolved by identity, not first-wins.
|
||||
#import "modules/std.sx";
|
||||
#import "0722-modules-flat-same-name-own/a.sx";
|
||||
#import "0722-modules-flat-same-name-own/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
report("from_a binds a.greet", from_a() == 1);
|
||||
report("from_b binds b.greet", from_b() == 2);
|
||||
0
|
||||
}
|
||||
5
examples/0722-modules-flat-same-name-own/a.sx
Normal file
5
examples/0722-modules-flat-same-name-own/a.sx
Normal file
@@ -0,0 +1,5 @@
|
||||
// a.sx authors `greet`. Its own `from_a` calls `greet` bare — under fix-0102c
|
||||
// that binds a.sx's OWN author (own-author wins), even though b.sx also
|
||||
// authors `greet` and the first-wins merge keeps only one in the merged scope.
|
||||
greet :: () -> s64 { return 1; }
|
||||
from_a :: () -> s64 { return greet(); }
|
||||
4
examples/0722-modules-flat-same-name-own/b.sx
Normal file
4
examples/0722-modules-flat-same-name-own/b.sx
Normal file
@@ -0,0 +1,4 @@
|
||||
// b.sx authors its OWN `greet`. `from_b`'s bare `greet` must bind b.sx's
|
||||
// author (2), not the first-wins winner from a.sx.
|
||||
greet :: () -> s64 { return 2; }
|
||||
from_b :: () -> s64 { return greet(); }
|
||||
17
examples/0723-modules-flat-vs-namespaced.sx
Normal file
17
examples/0723-modules-flat-vs-namespaced.sx
Normal file
@@ -0,0 +1,17 @@
|
||||
// fix-0102c (issue 0102): one FLAT and one NAMESPACED author of `value`. The
|
||||
// bare call `value()` binds the FLAT author (10); the namespaced author is
|
||||
// reached only through `nm.value()` (20). A namespaced author must NOT make the
|
||||
// bare call ambiguous — only flat authors collide.
|
||||
#import "modules/std.sx";
|
||||
#import "0723-modules-flat-vs-namespaced/flat.sx";
|
||||
nm :: #import "0723-modules-flat-vs-namespaced/named.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
report("bare binds flat", value() == 10);
|
||||
report("nm.value binds named", nm.value() == 20);
|
||||
0
|
||||
}
|
||||
3
examples/0723-modules-flat-vs-namespaced/flat.sx
Normal file
3
examples/0723-modules-flat-vs-namespaced/flat.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
// Flat-imported author of `value`. A bare `value()` in the consumer binds THIS
|
||||
// one — the only bare (flat) author of the name.
|
||||
value :: () -> s64 { return 10; }
|
||||
4
examples/0723-modules-flat-vs-namespaced/named.sx
Normal file
4
examples/0723-modules-flat-vs-namespaced/named.sx
Normal file
@@ -0,0 +1,4 @@
|
||||
// Namespaced-imported author of `value`. Reachable only as `nm.value`; it never
|
||||
// enters the flat merge, so it neither shadows the flat author nor makes the
|
||||
// bare call ambiguous.
|
||||
value :: () -> s64 { return 20; }
|
||||
12
examples/0724-modules-flat-same-name-ambiguous.sx
Normal file
12
examples/0724-modules-flat-same-name-ambiguous.sx
Normal file
@@ -0,0 +1,12 @@
|
||||
// fix-0102c (issue 0102): a genuinely-ambiguous bare call. `main` flat-imports
|
||||
// two modules that each author `dup` and neither is `main`'s own — a bare
|
||||
// `dup()` can't pick one, so the compiler rejects it with a loud diagnostic
|
||||
// instead of silently first-wins-binding one. Qualify the call to disambiguate.
|
||||
#import "modules/std.sx";
|
||||
#import "0724-modules-flat-same-name-ambiguous/a.sx";
|
||||
#import "0724-modules-flat-same-name-ambiguous/b.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
print("{}\n", dup());
|
||||
0
|
||||
}
|
||||
3
examples/0724-modules-flat-same-name-ambiguous/a.sx
Normal file
3
examples/0724-modules-flat-same-name-ambiguous/a.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
// One of two flat authors of `dup`. A consumer that flat-imports BOTH and calls
|
||||
// `dup` bare cannot pick between them.
|
||||
dup :: () -> s64 { return 1; }
|
||||
2
examples/0724-modules-flat-same-name-ambiguous/b.sx
Normal file
2
examples/0724-modules-flat-same-name-ambiguous/b.sx
Normal file
@@ -0,0 +1,2 @@
|
||||
// The second flat author of `dup`.
|
||||
dup :: () -> s64 { return 2; }
|
||||
17
examples/0725-modules-flat-dir-same-name.sx
Normal file
17
examples/0725-modules-flat-dir-same-name.sx
Normal file
@@ -0,0 +1,17 @@
|
||||
// fix-0102c (issue 0102): two flat DIRECTORY imports each author a same-name
|
||||
// `tag`. A directory flat-import exposes the directory's authored functions, so
|
||||
// `caller1`/`caller2` are visible here, and each binds its OWN directory's `tag`
|
||||
// when it calls bare — per-source binding across directory imports (100 / 200).
|
||||
#import "modules/std.sx";
|
||||
#import "0725-modules-flat-dir-same-name/d1";
|
||||
#import "0725-modules-flat-dir-same-name/d2";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
report("caller1 binds d1.tag", caller1() == 100);
|
||||
report("caller2 binds d2.tag", caller2() == 200);
|
||||
0
|
||||
}
|
||||
3
examples/0725-modules-flat-dir-same-name/d1/one.sx
Normal file
3
examples/0725-modules-flat-dir-same-name/d1/one.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
// d1's author of `tag`. `caller1` (also in d1) binds d1's own `tag` (100).
|
||||
tag :: () -> s64 { return 100; }
|
||||
caller1 :: () -> s64 { return tag(); }
|
||||
4
examples/0725-modules-flat-dir-same-name/d2/two.sx
Normal file
4
examples/0725-modules-flat-dir-same-name/d2/two.sx
Normal file
@@ -0,0 +1,4 @@
|
||||
// d2's author of `tag`. `caller2` (also in d2) binds d2's own `tag` (200),
|
||||
// even though d1's `tag` is the first-wins merge winner.
|
||||
tag :: () -> s64 { return 200; }
|
||||
caller2 :: () -> s64 { return tag(); }
|
||||
23
examples/0726-modules-flat-same-name-variadic.sx
Normal file
23
examples/0726-modules-flat-same-name-variadic.sx
Normal file
@@ -0,0 +1,23 @@
|
||||
// fix-0102c F1 (issue 0102): two flat FILE imports each author same-name free
|
||||
// functions whose VARIADIC SHAPE differs. `combine` is fixed-arity in a.sx
|
||||
// (the first-wins winner) but variadic in b.sx (the shadow); `pick` is the
|
||||
// reverse. Each module's bare call must pack arguments against ITS OWN
|
||||
// author's signature — not the first-wins author's. Pre-fix the call path
|
||||
// re-fetched the first-wins AST by name to drive variadic packing, so b.sx's
|
||||
// variadic `combine` was packed as if fixed (and its fixed `pick` as if
|
||||
// variadic) → wrong lowering. Regression for the F1 review finding.
|
||||
#import "modules/std.sx";
|
||||
#import "0726-modules-flat-same-name-variadic/a.sx";
|
||||
#import "0726-modules-flat-same-name-variadic/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
report("from_a combine fixed", from_a_combine() == 30);
|
||||
report("from_b combine variadic", from_b_combine() == 10);
|
||||
report("from_a pick variadic", from_a_pick() == 6);
|
||||
report("from_b pick fixed", from_b_pick() == 5);
|
||||
0
|
||||
}
|
||||
11
examples/0726-modules-flat-same-name-variadic/a.sx
Normal file
11
examples/0726-modules-flat-same-name-variadic/a.sx
Normal file
@@ -0,0 +1,11 @@
|
||||
// a.sx is the first-wins winner for both names. `combine` is FIXED arity;
|
||||
// `pick` is VARIADIC. `from_a_*` call them bare — a authors the winner, so
|
||||
// they resolve through the existing path and pack against a's own shapes.
|
||||
combine :: (x: s64, y: s64) -> s64 { return x + y; }
|
||||
pick :: (..xs: []s64) -> s64 {
|
||||
result := 0;
|
||||
for xs: (it) { result = result + it; }
|
||||
result
|
||||
}
|
||||
from_a_combine :: () -> s64 { return combine(10, 20); }
|
||||
from_a_pick :: () -> s64 { return pick(1, 2, 3); }
|
||||
12
examples/0726-modules-flat-same-name-variadic/b.sx
Normal file
12
examples/0726-modules-flat-same-name-variadic/b.sx
Normal file
@@ -0,0 +1,12 @@
|
||||
// b.sx is the SHADOW author for both names, with the OPPOSITE shapes:
|
||||
// `combine` is VARIADIC, `pick` is FIXED. Each `from_b_*` bare call must pack
|
||||
// against b's OWN author's signature (the F1 fix) — combine sums its variadic
|
||||
// pack, pick subtracts its two fixed args.
|
||||
combine :: (..xs: []s64) -> s64 {
|
||||
result := 0;
|
||||
for xs: (it) { result = result + it; }
|
||||
result
|
||||
}
|
||||
pick :: (a: s64, b: s64) -> s64 { return b - a; }
|
||||
from_b_combine :: () -> s64 { return combine(1, 2, 3, 4); }
|
||||
from_b_pick :: () -> s64 { return pick(2, 7); }
|
||||
20
examples/0727-modules-user-ns-m0.sx
Normal file
20
examples/0727-modules-user-ns-m0.sx
Normal file
@@ -0,0 +1,20 @@
|
||||
// fix-0102c (issue 0102): a user namespace alias literally named `__m0`
|
||||
// coexists with flat same-name imports. fix-0102 resolves same-name authors by
|
||||
// FnDecl IDENTITY — there are no synthetic `__m0`-style names to collide with —
|
||||
// so a user namespace spelled `__m0` is just an ordinary namespace: `call_a`
|
||||
// binds a.ping (1), `call_b` binds b.ping (2), and `__m0.ping` reaches m.ping (99).
|
||||
#import "modules/std.sx";
|
||||
#import "0727-modules-user-ns-m0/a.sx";
|
||||
#import "0727-modules-user-ns-m0/b.sx";
|
||||
__m0 :: #import "0727-modules-user-ns-m0/m.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
report("call_a binds a.ping", call_a() == 1);
|
||||
report("call_b binds b.ping", call_b() == 2);
|
||||
report("__m0.ping binds m.ping", __m0.ping() == 99);
|
||||
0
|
||||
}
|
||||
3
examples/0727-modules-user-ns-m0/a.sx
Normal file
3
examples/0727-modules-user-ns-m0/a.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
// Flat author of `ping`; `call_a` binds a.sx's own `ping` (1).
|
||||
ping :: () -> s64 { return 1; }
|
||||
call_a :: () -> s64 { return ping(); }
|
||||
3
examples/0727-modules-user-ns-m0/b.sx
Normal file
3
examples/0727-modules-user-ns-m0/b.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
// Second flat author of `ping`; `call_b` binds b.sx's own `ping` (2).
|
||||
ping :: () -> s64 { return 2; }
|
||||
call_b :: () -> s64 { return ping(); }
|
||||
3
examples/0727-modules-user-ns-m0/m.sx
Normal file
3
examples/0727-modules-user-ns-m0/m.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
// Imported under a user namespace literally named `__m0`. Reached as
|
||||
// `__m0.ping` (99); coexists with the flat `ping` collision.
|
||||
ping :: () -> s64 { return 99; }
|
||||
22
examples/0728-modules-flat-same-name-paramtype.sx
Normal file
22
examples/0728-modules-flat-same-name-paramtype.sx
Normal file
@@ -0,0 +1,22 @@
|
||||
// fix-0102c F2 (issue 0102): two flat FILE imports each author a same-name free
|
||||
// function `apply` with a DIFFERENT parameter TYPE — a.sx takes a value
|
||||
// (`x: s64`), b.sx takes a pointer (`x: *s64`). The first-wins import merge
|
||||
// keeps a.sx's value-typed `apply`, but each module's bare call must type its
|
||||
// arguments against ITS OWN author. b.sx's `from_b` passes a local `v` to its
|
||||
// pointer-param `apply` via implicit address-of; before the fix the arg was
|
||||
// typed against the first-wins (value) winner, lowered as a value, then the
|
||||
// resolved pointer-param author was called with that value bit-cast to a
|
||||
// pointer — a segfault. Regression: per-source parameter target typing.
|
||||
#import "modules/std.sx";
|
||||
#import "0728-modules-flat-same-name-paramtype/a.sx";
|
||||
#import "0728-modules-flat-same-name-paramtype/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
report("from_a binds a.apply (value param)", from_a() == 11);
|
||||
report("from_b binds b.apply (pointer param)", from_b() == 42);
|
||||
0
|
||||
}
|
||||
5
examples/0728-modules-flat-same-name-paramtype/a.sx
Normal file
5
examples/0728-modules-flat-same-name-paramtype/a.sx
Normal file
@@ -0,0 +1,5 @@
|
||||
// a.sx authors `apply` taking a VALUE. It is imported first, so it is the
|
||||
// first-wins merge winner. `from_a` calls `apply` bare on a value local — its
|
||||
// own author wins (own == winner → existing path, byte-for-byte unchanged).
|
||||
apply :: (x: s64) -> s64 { return x + 1; }
|
||||
from_a :: () -> s64 { v : s64 = 10; return apply(v); }
|
||||
7
examples/0728-modules-flat-same-name-paramtype/b.sx
Normal file
7
examples/0728-modules-flat-same-name-paramtype/b.sx
Normal file
@@ -0,0 +1,7 @@
|
||||
// b.sx authors its OWN `apply` taking a POINTER. `from_b` passes a value local
|
||||
// `v` bare; the pointer param must drive implicit address-of so the callee
|
||||
// mutates `v` in place (×2 → 42). Before the fix, `v` was typed against a.sx's
|
||||
// value-param winner, lowered as a value, then the resolved pointer-param
|
||||
// author was called with that value forced to a pointer (segfault).
|
||||
apply :: (x: *s64) { x.* = x.* * 2; }
|
||||
from_b :: () -> s64 { v : s64 = 21; apply(v); return v; }
|
||||
14
examples/0729-modules-flat-same-name-foreign.sx
Normal file
14
examples/0729-modules-flat-same-name-foreign.sx
Normal file
@@ -0,0 +1,14 @@
|
||||
// fix-0102c (issue 0102) F3 regression: two flat FILE imports each `#foreign`
|
||||
// the SAME libc symbol under the SAME sx name `absval`. The bare-call resolver
|
||||
// must NOT count `#foreign` (non-plain) authors when deciding ambiguity — it
|
||||
// filters them out, returns "no rerouting", and the existing first-wins foreign
|
||||
// dispatch binds the call. A same-name foreign collision therefore compiles and
|
||||
// runs (master behavior), it does NOT error as ambiguous.
|
||||
#import "modules/std.sx";
|
||||
#import "0729-modules-flat-same-name-foreign/a.sx";
|
||||
#import "0729-modules-flat-same-name-foreign/b.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
print("absval = {}\n", absval(-7));
|
||||
0
|
||||
}
|
||||
5
examples/0729-modules-flat-same-name-foreign/a.sx
Normal file
5
examples/0729-modules-flat-same-name-foreign/a.sx
Normal file
@@ -0,0 +1,5 @@
|
||||
// One of two flat authors of `absval`, a `#foreign` libc binding. A consumer
|
||||
// flat-importing BOTH must NOT see this as an ambiguous bare-call collision —
|
||||
// foreign authors are never rerouted by the bare-call resolver, so the call
|
||||
// falls to the existing first-wins foreign dispatch.
|
||||
absval :: (n: s32) -> s32 #foreign libc "abs";
|
||||
2
examples/0729-modules-flat-same-name-foreign/b.sx
Normal file
2
examples/0729-modules-flat-same-name-foreign/b.sx
Normal file
@@ -0,0 +1,2 @@
|
||||
// The second flat author of `absval` — the identical `#foreign` libc binding.
|
||||
absval :: (n: s32) -> s32 #foreign libc "abs";
|
||||
20
examples/0730-modules-flat-same-name-default-arg.sx
Normal file
20
examples/0730-modules-flat-same-name-default-arg.sx
Normal file
@@ -0,0 +1,20 @@
|
||||
// fix-0102d site 1 (issue 0102): two flat FILE imports each author a same-name
|
||||
// free function `cfg` with a DIFFERENT default value for its trailing param —
|
||||
// a.sx defaults to 10, b.sx to 20. Each module calls `cfg()` bare with the arg
|
||||
// OMITTED. The omitted trailing arg must be filled from the RESOLVED author's
|
||||
// default (own-author wins), not the first-wins winner's. Before the fix,
|
||||
// `from_b`'s `cfg()` expanded to the winner a.sx's default (10) and returned 10.
|
||||
// Regression: per-source default-argument expansion.
|
||||
#import "modules/std.sx";
|
||||
#import "0730-modules-flat-same-name-default-arg/a.sx";
|
||||
#import "0730-modules-flat-same-name-default-arg/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
report("from_a binds a.cfg default (10)", from_a() == 10);
|
||||
report("from_b binds b.cfg default (20)", from_b() == 20);
|
||||
0
|
||||
}
|
||||
5
examples/0730-modules-flat-same-name-default-arg/a.sx
Normal file
5
examples/0730-modules-flat-same-name-default-arg/a.sx
Normal file
@@ -0,0 +1,5 @@
|
||||
// a.sx authors `cfg` defaulting to 10. Imported first, so it is the first-wins
|
||||
// merge winner. `from_a` calls `cfg()` with the arg omitted — own == winner →
|
||||
// existing default-expansion path, byte-for-byte unchanged.
|
||||
cfg :: (n: s64 = 10) -> s64 { return n; }
|
||||
from_a :: () -> s64 { return cfg(); }
|
||||
5
examples/0730-modules-flat-same-name-default-arg/b.sx
Normal file
5
examples/0730-modules-flat-same-name-default-arg/b.sx
Normal file
@@ -0,0 +1,5 @@
|
||||
// b.sx authors its OWN `cfg` defaulting to 20. `from_b`'s `cfg()` omits the
|
||||
// arg; the omitted trailing default must come from b.sx's author (20), not the
|
||||
// first-wins winner from a.sx (10).
|
||||
cfg :: (n: s64 = 20) -> s64 { return n; }
|
||||
from_b :: () -> s64 { return cfg(); }
|
||||
22
examples/0731-modules-flat-same-name-closure.sx
Normal file
22
examples/0731-modules-flat-same-name-closure.sx
Normal file
@@ -0,0 +1,22 @@
|
||||
// fix-0102d site 2 (issue 0102): two flat FILE imports each author a same-name
|
||||
// free function `pick` (a.sx returns 1, b.sx returns 2). Each module takes
|
||||
// `pick` as a function VALUE — both as `closure(pick)` and as a bare-name
|
||||
// fn-pointer binding (`g : () -> s64 = pick`). The captured FuncId must be the
|
||||
// RESOLVED author's (own-author wins), not the first-wins winner's. Before the
|
||||
// fix, b.sx's `closure(pick)` / `pick`-as-value both captured a.sx's winner
|
||||
// (1). Regression: per-source function-value conversion (closure + func_ref).
|
||||
#import "modules/std.sx";
|
||||
#import "0731-modules-flat-same-name-closure/a.sx";
|
||||
#import "0731-modules-flat-same-name-closure/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
report("from_a closure binds a.pick (1)", from_a_closure() == 1);
|
||||
report("from_b closure binds b.pick (2)", from_b_closure() == 2);
|
||||
report("from_a fn-value binds a.pick (1)", from_a_value() == 1);
|
||||
report("from_b fn-value binds b.pick (2)", from_b_value() == 2);
|
||||
0
|
||||
}
|
||||
6
examples/0731-modules-flat-same-name-closure/a.sx
Normal file
6
examples/0731-modules-flat-same-name-closure/a.sx
Normal file
@@ -0,0 +1,6 @@
|
||||
// a.sx authors `pick` returning 1. Imported first → first-wins winner.
|
||||
// `from_a_closure` / `from_a_value` take a.sx's own author (own == winner →
|
||||
// existing path, byte-for-byte unchanged).
|
||||
pick :: () -> s64 { return 1; }
|
||||
from_a_closure :: () -> s64 { f := closure(pick); return f(); }
|
||||
from_a_value :: () -> s64 { g : () -> s64 = pick; return g(); }
|
||||
6
examples/0731-modules-flat-same-name-closure/b.sx
Normal file
6
examples/0731-modules-flat-same-name-closure/b.sx
Normal file
@@ -0,0 +1,6 @@
|
||||
// b.sx authors its OWN `pick` returning 2. Taking `pick` as a value —
|
||||
// `closure(pick)` or `g : () -> s64 = pick` — must capture b.sx's author (2),
|
||||
// not the first-wins winner from a.sx (1).
|
||||
pick :: () -> s64 { return 2; }
|
||||
from_b_closure :: () -> s64 { f := closure(pick); return f(); }
|
||||
from_b_value :: () -> s64 { g : () -> s64 = pick; return g(); }
|
||||
19
examples/0732-modules-flat-same-name-ufcs.sx
Normal file
19
examples/0732-modules-flat-same-name-ufcs.sx
Normal file
@@ -0,0 +1,19 @@
|
||||
// fix-0102d site 3 (issue 0102): two flat FILE imports each author a same-name
|
||||
// free function `bump` (a.sx adds 1, b.sx adds 100). Each module dispatches it
|
||||
// via free-function UFCS — `v.bump()` lowers to `bump(v)`. The dispatched
|
||||
// author must be the RESOLVED one for the receiver's source (own-author wins),
|
||||
// not the first-wins winner. Before the fix, b.sx's `v.bump()` dispatched
|
||||
// a.sx's winner (+1 → 11). Regression: per-source free-function UFCS dispatch.
|
||||
#import "modules/std.sx";
|
||||
#import "0732-modules-flat-same-name-ufcs/a.sx";
|
||||
#import "0732-modules-flat-same-name-ufcs/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
report("from_a v.bump() binds a.bump (+1)", from_a_ufcs() == 11);
|
||||
report("from_b v.bump() binds b.bump (+100)", from_b_ufcs() == 110);
|
||||
0
|
||||
}
|
||||
5
examples/0732-modules-flat-same-name-ufcs/a.sx
Normal file
5
examples/0732-modules-flat-same-name-ufcs/a.sx
Normal file
@@ -0,0 +1,5 @@
|
||||
// a.sx authors `bump` adding 1. Imported first → first-wins winner. `from_a`'s
|
||||
// `v.bump()` resolves a.sx's own author (own == winner → existing UFCS path,
|
||||
// byte-for-byte unchanged).
|
||||
bump :: (x: s64) -> s64 { return x + 1; }
|
||||
from_a_ufcs :: () -> s64 { v : s64 = 10; return v.bump(); }
|
||||
4
examples/0732-modules-flat-same-name-ufcs/b.sx
Normal file
4
examples/0732-modules-flat-same-name-ufcs/b.sx
Normal file
@@ -0,0 +1,4 @@
|
||||
// b.sx authors its OWN `bump` adding 100. `from_b`'s `v.bump()` must dispatch
|
||||
// b.sx's author (+100 → 110), not the first-wins winner from a.sx (+1).
|
||||
bump :: (x: s64) -> s64 { return x + 100; }
|
||||
from_b_ufcs :: () -> s64 { v : s64 = 10; return v.bump(); }
|
||||
21
examples/0733-modules-flat-same-name-comptime-run.sx
Normal file
21
examples/0733-modules-flat-same-name-comptime-run.sx
Normal file
@@ -0,0 +1,21 @@
|
||||
// fix-0102d site 4 (issue 0102): two flat FILE imports each author a same-name
|
||||
// free function `compute` (a.sx returns 7, b.sx returns 70) and each evaluates
|
||||
// it at comptime via `NAME :: #run compute();`. The #run body must resolve the
|
||||
// bare callee from ITS OWN module's source context (own-author wins), so a.sx's
|
||||
// const is 7 and b.sx's is 70. Before the fix, the #run body lowered with the
|
||||
// main file's source perspective, where `compute` is authored by two flat
|
||||
// imports and neither is main's own — so it was reported AMBIGUOUS and the
|
||||
// build failed. Regression: per-source comptime #run callee resolution.
|
||||
#import "modules/std.sx";
|
||||
#import "0733-modules-flat-same-name-comptime-run/a.sx";
|
||||
#import "0733-modules-flat-same-name-comptime-run/b.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
report("a.sx #run binds a.compute (7)", get_a() == 7);
|
||||
report("b.sx #run binds b.compute (70)", get_b() == 70);
|
||||
0
|
||||
}
|
||||
6
examples/0733-modules-flat-same-name-comptime-run/a.sx
Normal file
6
examples/0733-modules-flat-same-name-comptime-run/a.sx
Normal file
@@ -0,0 +1,6 @@
|
||||
// a.sx authors `compute` returning 7 and evaluates it at comptime. Imported
|
||||
// first → first-wins winner; own == winner, but the #run must still lower in
|
||||
// a.sx's source context so the bare `compute` resolves at all (not ambiguous).
|
||||
compute :: () -> s64 { return 7; }
|
||||
A_VAL :: #run compute();
|
||||
get_a :: () -> s64 { return A_VAL; }
|
||||
6
examples/0733-modules-flat-same-name-comptime-run/b.sx
Normal file
6
examples/0733-modules-flat-same-name-comptime-run/b.sx
Normal file
@@ -0,0 +1,6 @@
|
||||
// b.sx authors its OWN `compute` returning 70. Its `#run compute()` must bind
|
||||
// b.sx's author (70) — own-author wins in b.sx's source context — not the
|
||||
// first-wins winner from a.sx (7).
|
||||
compute :: () -> s64 { return 70; }
|
||||
B_VAL :: #run compute();
|
||||
get_b :: () -> s64 { return B_VAL; }
|
||||
16
examples/0734-modules-flat-same-name-ufcs-ambiguous.sx
Normal file
16
examples/0734-modules-flat-same-name-ufcs-ambiguous.sx
Normal file
@@ -0,0 +1,16 @@
|
||||
// fix-0102d site 3 ambiguity (issue 0102): two flat FILE imports each author a
|
||||
// same-name free function `dup`, and the MAIN file (which authors neither)
|
||||
// dispatches it via free-function UFCS `v.dup()`. With two distinct flat
|
||||
// authors reachable and no own-author to prefer, the call is ambiguous — the
|
||||
// UFCS dispatch site must emit the loud "qualify the call" diagnostic rather
|
||||
// than silently binding the first-wins winner. Mirrors 0724 (the bare-call
|
||||
// ambiguity) one site over.
|
||||
#import "modules/std.sx";
|
||||
#import "0734-modules-flat-same-name-ufcs-ambiguous/a.sx";
|
||||
#import "0734-modules-flat-same-name-ufcs-ambiguous/b.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
v : s64 = 10;
|
||||
print("{}\n", v.dup());
|
||||
0
|
||||
}
|
||||
2
examples/0734-modules-flat-same-name-ufcs-ambiguous/a.sx
Normal file
2
examples/0734-modules-flat-same-name-ufcs-ambiguous/a.sx
Normal file
@@ -0,0 +1,2 @@
|
||||
// a.sx authors `dup` (+1). One of two distinct flat authors of `dup`.
|
||||
dup :: (x: s64) -> s64 { return x + 1; }
|
||||
3
examples/0734-modules-flat-same-name-ufcs-ambiguous/b.sx
Normal file
3
examples/0734-modules-flat-same-name-ufcs-ambiguous/b.sx
Normal file
@@ -0,0 +1,3 @@
|
||||
// b.sx authors its OWN `dup` (+2) — the second distinct flat author. Main
|
||||
// imports both and authors neither, so `v.dup()` from main is ambiguous.
|
||||
dup :: (x: s64) -> s64 { return x + 2; }
|
||||
17
examples/0735-modules-flat-same-name-fn-value-winner.sx
Normal file
17
examples/0735-modules-flat-same-name-fn-value-winner.sx
Normal file
@@ -0,0 +1,17 @@
|
||||
// fix-0102d site 2 / attempt-2 (issue 0102): the first-wins winner's body is
|
||||
// independently BROKEN (references an undefined symbol) and is never used. A
|
||||
// shadow author from a later flat import takes its OWN `pick` as a function
|
||||
// VALUE (`g : () -> s64 = pick`). The value must bind the shadow (own-author
|
||||
// wins) and the broken winner must NOT be lowered — a rerouted fn value never
|
||||
// uses the winner. Before the fix the fn-value site eagerly lazily-lowered the
|
||||
// name-keyed winner BEFORE the resolver rerouted, surfacing the winner's
|
||||
// `unresolved 'missing_from_a'` for a function the value never touches.
|
||||
// Regression: per-source function-value conversion must not pre-lower the winner.
|
||||
#import "modules/std.sx";
|
||||
#import "0735-modules-flat-same-name-fn-value-winner/a.sx";
|
||||
#import "0735-modules-flat-same-name-fn-value-winner/b.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
print("from_b_value = {}\n", from_b_value());
|
||||
0
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// a.sx authors `pick` (imported first → the first-wins name-keyed winner) but
|
||||
// its body references an undefined symbol, so lowering a.pick AT ALL is an
|
||||
// error. Nothing uses a.pick — taking b.pick as a value must not pre-lower it.
|
||||
pick :: () -> s64 { return missing_from_a(); }
|
||||
@@ -0,0 +1,7 @@
|
||||
// b.sx authors its OWN `pick` (returns 2) and takes it as a function VALUE. The
|
||||
// value binds b.pick (own-author wins), never the broken winner from a.sx.
|
||||
pick :: () -> s64 { return 2; }
|
||||
from_b_value :: () -> s64 {
|
||||
g : () -> s64 = pick;
|
||||
return g();
|
||||
}
|
||||
1
examples/expected/0722-modules-flat-same-name-own.exit
Normal file
1
examples/expected/0722-modules-flat-same-name-own.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
examples/expected/0722-modules-flat-same-name-own.stderr
Normal file
1
examples/expected/0722-modules-flat-same-name-own.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
2
examples/expected/0722-modules-flat-same-name-own.stdout
Normal file
2
examples/expected/0722-modules-flat-same-name-own.stdout
Normal file
@@ -0,0 +1,2 @@
|
||||
from_a binds a.greet: ok
|
||||
from_b binds b.greet: ok
|
||||
1
examples/expected/0723-modules-flat-vs-namespaced.exit
Normal file
1
examples/expected/0723-modules-flat-vs-namespaced.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
examples/expected/0723-modules-flat-vs-namespaced.stderr
Normal file
1
examples/expected/0723-modules-flat-vs-namespaced.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
2
examples/expected/0723-modules-flat-vs-namespaced.stdout
Normal file
2
examples/expected/0723-modules-flat-vs-namespaced.stdout
Normal file
@@ -0,0 +1,2 @@
|
||||
bare binds flat: ok
|
||||
nm.value binds named: ok
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
error: 'dup' is ambiguous; declared by multiple imported modules — qualify the call
|
||||
--> examples/0724-modules-flat-same-name-ambiguous.sx:10:19
|
||||
|
|
||||
10 | print("{}\n", dup());
|
||||
| ^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/expected/0725-modules-flat-dir-same-name.exit
Normal file
1
examples/expected/0725-modules-flat-dir-same-name.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
examples/expected/0725-modules-flat-dir-same-name.stderr
Normal file
1
examples/expected/0725-modules-flat-dir-same-name.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
2
examples/expected/0725-modules-flat-dir-same-name.stdout
Normal file
2
examples/expected/0725-modules-flat-dir-same-name.stdout
Normal file
@@ -0,0 +1,2 @@
|
||||
caller1 binds d1.tag: ok
|
||||
caller2 binds d2.tag: ok
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from_a combine fixed: ok
|
||||
from_b combine variadic: ok
|
||||
from_a pick variadic: ok
|
||||
from_b pick fixed: ok
|
||||
1
examples/expected/0727-modules-user-ns-m0.exit
Normal file
1
examples/expected/0727-modules-user-ns-m0.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
examples/expected/0727-modules-user-ns-m0.stderr
Normal file
1
examples/expected/0727-modules-user-ns-m0.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
3
examples/expected/0727-modules-user-ns-m0.stdout
Normal file
3
examples/expected/0727-modules-user-ns-m0.stdout
Normal file
@@ -0,0 +1,3 @@
|
||||
call_a binds a.ping: ok
|
||||
call_b binds b.ping: ok
|
||||
__m0.ping binds m.ping: ok
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from_a binds a.apply (value param): ok
|
||||
from_b binds b.apply (pointer param): ok
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
absval = 7
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from_a binds a.cfg default (10): ok
|
||||
from_b binds b.cfg default (20): ok
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from_a closure binds a.pick (1): ok
|
||||
from_b closure binds b.pick (2): ok
|
||||
from_a fn-value binds a.pick (1): ok
|
||||
from_b fn-value binds b.pick (2): ok
|
||||
1
examples/expected/0732-modules-flat-same-name-ufcs.exit
Normal file
1
examples/expected/0732-modules-flat-same-name-ufcs.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from_a v.bump() binds a.bump (+1): ok
|
||||
from_b v.bump() binds b.bump (+100): ok
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
a.sx #run binds a.compute (7): ok
|
||||
b.sx #run binds b.compute (70): ok
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
error: 'dup' is ambiguous; declared by multiple imported modules — qualify the call
|
||||
--> examples/0734-modules-flat-same-name-ufcs-ambiguous.sx:14:19
|
||||
|
|
||||
14 | print("{}\n", v.dup());
|
||||
| ^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from_b_value = 2
|
||||
102
issues/0102-flat-import-same-signature-collision.md
Normal file
102
issues/0102-flat-import-same-signature-collision.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# 0102 — flat-import same-name function collision (per-source binding)
|
||||
|
||||
**RESOLVED.** Two **flat** imports (bare `#import "a.sx"` / a flat directory
|
||||
import, NOT a namespaced `ns :: #import`) that each author a top-level free
|
||||
function with the **same short name** collided in IR lowering. The flat/
|
||||
directory merge keeps exactly **one** author per name in the merged decl list
|
||||
(first-wins), and every bare-name consumer site — call dispatch, default-arg
|
||||
expansion, function-value capture, free-function UFCS, comptime `#run` — read
|
||||
that one **name-keyed** winner. So when module `b.sx` authored its own `greet`
|
||||
but `a.sx` was imported first, `b.sx`'s own bare `greet()` silently bound
|
||||
`a.sx`'s author. Unlike issue 0100 (which crashed on a param-count assert when
|
||||
the AST/FuncId split across modules), this miscompiled **silently**: the wrong
|
||||
same-name author ran, with no diagnostic.
|
||||
|
||||
The defect had two faces, both rooted in name-keyed identity across a flat
|
||||
collision:
|
||||
|
||||
1. **Lowering** keyed function bodies by short name (`fn_ast_map` /
|
||||
`resolveFuncByName` are first-wins), so a shadowed author never got its own
|
||||
FuncId or body — there was nothing to bind even if a consumer wanted the
|
||||
per-source author.
|
||||
2. **Resolution** at every bare-name consumer site re-looked-up the winner by
|
||||
name, so even once shadow authors had distinct FuncIds, the consumer sites
|
||||
kept binding the first-wins winner.
|
||||
|
||||
## Fix — four sub-steps (`src/imports.zig`, `src/ir/lower.zig`)
|
||||
|
||||
- **0102a — retain dup authors + identity indexes.** The flat/directory merge
|
||||
keeps first-wins in the merged scope (unchanged), but now *also* retains
|
||||
every dropped same-name author in `program_index.module_fns`
|
||||
(`path → name → *FnDecl`) plus a `flat_import_graph` (`file → flat-import
|
||||
edges`). Resolution is untouched at this step — the indexes just make the
|
||||
shadowed authors addressable.
|
||||
|
||||
- **0102b — identity-addressable function lowering.** `fn_decl_fids`
|
||||
(`*const ast.FnDecl → FuncId`) lets a body be declared + lowered against a
|
||||
**specific** `*FnDecl` (`lowerFunctionBodyInto` / `bareAuthorFuncId`) instead
|
||||
of a name. A shadow author gets a fresh same-name FuncId in its own module's
|
||||
visibility context; the winner keeps the name-keyed slot. `scanDecls` keys
|
||||
`fn_decl_fids` by the stable `module_fns` `*FnDecl`.
|
||||
|
||||
- **0102c — THE resolver + call path + param typing.**
|
||||
`resolveBareCallee(name, caller_file) -> .func(ResolvedAuthor) | .ambiguous |
|
||||
.none` (`src/ir/lower.zig`). It returns `.none` whenever the outcome would
|
||||
equal first-wins (single author, or own-author == winner), so every
|
||||
single-author / local / parameter / std / qualified / foreign / generic /
|
||||
builtin name resolves byte-for-byte as before. Only a genuine flat collision
|
||||
reroutes: own-author wins; else the caller's flat-reachable authors — `≥2`
|
||||
distinct → `.ambiguous` (loud "qualify the call" diagnostic), exactly one
|
||||
differing from the winner → bind it. Routed the **primary call path** and the
|
||||
call's **parameter target typing** (so a `*T`-param shadow gets implicit
|
||||
address-of, not a value bit-cast to a pointer → segfault).
|
||||
|
||||
- **0102d — the four remaining bare-name sites.** Routed the SAME resolver
|
||||
through every other site that resolved a bare callee/function-name by
|
||||
first-wins, each 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`):
|
||||
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. The winner's body is lazily lowered ONLY on the `.none`
|
||||
fallback — a rerouted value never uses the winner, so taking a shadow as a
|
||||
value must not pre-lower (and possibly mis-diagnose) the winner's body.
|
||||
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)
|
||||
rather than the main file's perspective (where two flat authors made it
|
||||
spuriously `.ambiguous` and failed the build).
|
||||
|
||||
## Regression tests
|
||||
|
||||
`examples/0722`–`0735` (each a focused multi-file flat-collision scene that
|
||||
fails on pre-fix code and passes after):
|
||||
|
||||
- `0722-modules-flat-same-name-own` — own-author wins on the call path.
|
||||
- `0723-modules-flat-vs-namespaced` — a flat author + a namespaced same-name
|
||||
author don't collide.
|
||||
- `0724-modules-flat-same-name-ambiguous` — `≥2` flat authors, bare call →
|
||||
loud diagnostic.
|
||||
- `0725-modules-flat-dir-same-name` — flat **directory** import collision.
|
||||
- `0726-modules-flat-same-name-variadic` — per-source variadic packing.
|
||||
- `0728-modules-flat-same-name-paramtype` — per-source parameter target typing
|
||||
(value vs pointer param).
|
||||
- `0729-modules-flat-same-name-foreign` — same-name `#foreign` authors are NOT
|
||||
rerouted (non-plain authors keep first-wins).
|
||||
- `0730-modules-flat-same-name-default-arg` — per-source default-arg expansion.
|
||||
- `0731-modules-flat-same-name-closure` — per-source `closure(fn)` + bare
|
||||
fn-value capture.
|
||||
- `0732-modules-flat-same-name-ufcs` — per-source free-function UFCS dispatch.
|
||||
- `0733-modules-flat-same-name-comptime-run` — per-source comptime `#run`
|
||||
callee.
|
||||
- `0734-modules-flat-same-name-ufcs-ambiguous` — `≥2` flat authors, UFCS call
|
||||
→ loud diagnostic (pre-fix: silently bound the winner).
|
||||
- `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 while the winner is NOT lowered (pre-fix: the
|
||||
fn-value site eagerly lowered the winner before the resolver rerouted,
|
||||
surfacing the winner's error for a function the value never touches).
|
||||
@@ -393,6 +393,12 @@ Direct C header import:
|
||||
math :: #import "modules/math.sx"; // namespaced import
|
||||
```
|
||||
|
||||
When two flat-imported modules each define a function of the same name, every
|
||||
module's own code binds its OWN author — a bare call resolves to the same-name
|
||||
function in the caller's module (or in its single flat import that provides it).
|
||||
A bare call to a name that two or more flat imports both provide is ambiguous and
|
||||
is rejected; qualify it with a namespaced import (`m :: #import …; m.fn()`).
|
||||
|
||||
### Implicit Context
|
||||
|
||||
Every program gets an implicit `context` with a default allocator:
|
||||
|
||||
16
src/core.zig
16
src/core.zig
@@ -26,6 +26,12 @@ pub const Compilation = struct {
|
||||
import_sources: std.StringHashMap([:0]const u8),
|
||||
module_scopes: std.StringHashMap(std.StringHashMap(void)),
|
||||
import_graph: std.StringHashMap(std.StringHashMap(void)),
|
||||
/// Flat-only subset of `import_graph` (bare `#import` edges, no namespaced
|
||||
/// `ns :: #import`). Borrowed by `ProgramIndex.flat_import_graph`.
|
||||
flat_import_graph: std.StringHashMap(std.StringHashMap(void)),
|
||||
/// Per-module authored-function index (`path → name → *const FnDecl`).
|
||||
/// Borrowed by `ProgramIndex.module_fns`.
|
||||
module_fns: imports.ModuleFns,
|
||||
ir_emitter: ?ir.LLVMEmitter = null,
|
||||
/// Lowered IR module, kept alive past `generateCode` so post-link
|
||||
/// callbacks can re-enter the interpreter to invoke sx functions
|
||||
@@ -52,6 +58,8 @@ pub const Compilation = struct {
|
||||
.import_sources = std.StringHashMap([:0]const u8).init(allocator),
|
||||
.module_scopes = std.StringHashMap(std.StringHashMap(void)).init(allocator),
|
||||
.import_graph = std.StringHashMap(std.StringHashMap(void)).init(allocator),
|
||||
.flat_import_graph = std.StringHashMap(std.StringHashMap(void)).init(allocator),
|
||||
.module_fns = imports.ModuleFns.init(allocator),
|
||||
.target_config = target_config,
|
||||
.stdlib_paths = stdlib_paths,
|
||||
};
|
||||
@@ -101,6 +109,7 @@ pub const Compilation = struct {
|
||||
&self.diagnostics,
|
||||
self.stdlib_paths,
|
||||
&self.import_graph,
|
||||
&self.flat_import_graph,
|
||||
self.comptimeContext(),
|
||||
) catch return error.CompileError;
|
||||
|
||||
@@ -111,6 +120,11 @@ pub const Compilation = struct {
|
||||
self.module_scopes.put(entry.key_ptr.*, entry.value_ptr.scope) catch {};
|
||||
}
|
||||
|
||||
// Per-module authored-function index, built from the SAME modules as
|
||||
// `module_scopes` (main + every cache entry). Keyed by path; same-name
|
||||
// cross-module authors stay distinct under their own paths.
|
||||
imports.buildModuleFns(self.allocator, self.file_path, mod, &cache, &self.module_fns) catch {};
|
||||
|
||||
// Store main file source in import_sources so error reporting can find it
|
||||
self.import_sources.put(self.file_path, self.source) catch {};
|
||||
|
||||
@@ -276,6 +290,8 @@ pub const Compilation = struct {
|
||||
lowering.diagnostics = &self.diagnostics;
|
||||
lowering.program_index.module_scopes = &self.module_scopes;
|
||||
lowering.program_index.import_graph = &self.import_graph;
|
||||
lowering.program_index.flat_import_graph = &self.flat_import_graph;
|
||||
lowering.program_index.module_fns = &self.module_fns;
|
||||
lowering.lowerRoot(root);
|
||||
if (self.diagnostics.hasErrors()) return error.CompileError;
|
||||
|
||||
|
||||
188
src/imports.test.zig
Normal file
188
src/imports.test.zig
Normal file
@@ -0,0 +1,188 @@
|
||||
// Tests for imports.zig — flat-import name-resolution data retention (fix-0102a).
|
||||
|
||||
const std = @import("std");
|
||||
const ast = @import("ast.zig");
|
||||
const parser = @import("parser.zig");
|
||||
const imports = @import("imports.zig");
|
||||
|
||||
var g_test_threaded: ?std.Io.Threaded = null;
|
||||
fn testIo() std.Io {
|
||||
if (g_test_threaded == null) {
|
||||
g_test_threaded = std.Io.Threaded.init(std.heap.page_allocator, .{});
|
||||
}
|
||||
return g_test_threaded.?.io();
|
||||
}
|
||||
|
||||
// Two flat-imported modules each author `greet`; a third is namespaced. The
|
||||
// step retains BOTH `greet` authors under their own paths in `module_fns` and
|
||||
// records the namespaced import in `import_graph` but NOT in `flat_import_graph`
|
||||
// — WITHOUT touching the merged scope: `mod.decls` stays byte-for-byte
|
||||
// first-wins (one `greet`, a.sx's), exactly as on `wt-fix-0102-base`.
|
||||
test "imports: module_fns retains same-name cross-module fns; flat_import_graph excludes namespaced edge" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
const io = testIo();
|
||||
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
|
||||
try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = "greet :: () -> s64 { 1 }\n" });
|
||||
try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "greet :: () -> s64 { 2 }\n" });
|
||||
try tmp.dir.writeFile(io, .{ .sub_path = "nsmod.sx", .data = "helper :: () -> s64 { 3 }\n" });
|
||||
const main_src =
|
||||
\\#import "a.sx";
|
||||
\\#import "b.sx";
|
||||
\\ns :: #import "nsmod.sx";
|
||||
\\main :: () -> s32 { 0 }
|
||||
\\
|
||||
;
|
||||
try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = main_src });
|
||||
|
||||
var dirbuf: [4096]u8 = undefined;
|
||||
const dirlen = try tmp.dir.realPath(io, &dirbuf);
|
||||
const absdir = dirbuf[0..dirlen];
|
||||
|
||||
const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir});
|
||||
const a_path = try std.fmt.allocPrint(alloc, "{s}/a.sx", .{absdir});
|
||||
const b_path = try std.fmt.allocPrint(alloc, "{s}/b.sx", .{absdir});
|
||||
const ns_path = try std.fmt.allocPrint(alloc, "{s}/nsmod.sx", .{absdir});
|
||||
|
||||
const main_bytes = try std.Io.Dir.readFileAlloc(.cwd(), io, main_path, alloc, .limited(1 << 20));
|
||||
const main_source = try alloc.dupeZ(u8, main_bytes);
|
||||
var p = parser.Parser.init(alloc, main_source);
|
||||
const root = p.parse() catch return error.ParseFailed;
|
||||
|
||||
var chain = std.StringHashMap(void).init(alloc);
|
||||
var cache = imports.ModuleCache.init(alloc);
|
||||
var import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc);
|
||||
var flat_import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc);
|
||||
const stdlib_paths = [_][]const u8{};
|
||||
|
||||
const mod = try imports.resolveImports(
|
||||
alloc,
|
||||
io,
|
||||
root,
|
||||
absdir,
|
||||
main_path,
|
||||
&chain,
|
||||
&cache,
|
||||
null,
|
||||
null,
|
||||
&stdlib_paths,
|
||||
&import_graph,
|
||||
&flat_import_graph,
|
||||
.{},
|
||||
);
|
||||
|
||||
var module_fns = imports.ModuleFns.init(alloc);
|
||||
try imports.buildModuleFns(alloc, main_path, mod, &cache, &module_fns);
|
||||
|
||||
// The MERGED scope the first-wins resolver consumes is unchanged: mergeFlat
|
||||
// still drops the second `greet`, so `mod.decls` carries exactly ONE — and
|
||||
// it is a.sx's author (the first flat import), not b.sx's.
|
||||
var greet_count: usize = 0;
|
||||
var merged_greet: ?*const ast.FnDecl = null;
|
||||
for (mod.decls) |decl| {
|
||||
const name = decl.data.declName() orelse continue;
|
||||
if (!std.mem.eql(u8, name, "greet")) continue;
|
||||
greet_count += 1;
|
||||
if (decl.data == .fn_decl) merged_greet = &decl.data.fn_decl;
|
||||
}
|
||||
try std.testing.expectEqual(@as(usize, 1), greet_count);
|
||||
|
||||
// module_fns retains BOTH authors of `greet`, keyed by their own paths —
|
||||
// the dropped author is recorded here (side index), not in the merged scope.
|
||||
const a_fns = module_fns.get(a_path) orelse return error.MissingAFns;
|
||||
const b_fns = module_fns.get(b_path) orelse return error.MissingBFns;
|
||||
const a_greet = a_fns.get("greet") orelse return error.MissingAGreet;
|
||||
const b_greet = b_fns.get("greet") orelse return error.MissingBGreet;
|
||||
// Distinct authoring decls — not the same node deduped down to one.
|
||||
try std.testing.expect(a_greet != b_greet);
|
||||
// First-wins: the surviving merged-scope `greet` is a.sx's author.
|
||||
try std.testing.expect(merged_greet == a_greet);
|
||||
|
||||
// flat_import_graph carries the two bare `#import` edges, NOT the
|
||||
// namespaced `ns :: #import` edge.
|
||||
const flat = flat_import_graph.get(main_path) orelse return error.MissingFlatEdges;
|
||||
try std.testing.expect(flat.contains(a_path));
|
||||
try std.testing.expect(flat.contains(b_path));
|
||||
try std.testing.expect(!flat.contains(ns_path));
|
||||
|
||||
// The full import_graph DOES record the namespaced edge (the contrast that
|
||||
// makes the flat-graph exclusion meaningful).
|
||||
const full = import_graph.get(main_path) orelse return error.MissingFullEdges;
|
||||
try std.testing.expect(full.contains(a_path));
|
||||
try std.testing.expect(full.contains(b_path));
|
||||
try std.testing.expect(full.contains(ns_path));
|
||||
}
|
||||
|
||||
// Mixed collision: a.sx authors `Widget` as a STRUCT (non-fn), b.sx authors it
|
||||
// as a FUNCTION. fix-0102a must NOT let the function-author retention shift the
|
||||
// merged scope — first-wins keeps a.sx's struct and drops b.sx's function,
|
||||
// exactly as on `wt-fix-0102-base`. (The fn author may still be indexed in
|
||||
// module_fns; resolution is what must be untouched.)
|
||||
test "imports: mixed non-fn/fn same-name collision stays first-wins in merged scope" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
const io = testIo();
|
||||
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
|
||||
try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = "Widget :: struct { x: s64 }\n" });
|
||||
try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "Widget :: () -> s64 { 7 }\n" });
|
||||
const main_src =
|
||||
\\#import "a.sx";
|
||||
\\#import "b.sx";
|
||||
\\main :: () -> s32 { 0 }
|
||||
\\
|
||||
;
|
||||
try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = main_src });
|
||||
|
||||
var dirbuf: [4096]u8 = undefined;
|
||||
const dirlen = try tmp.dir.realPath(io, &dirbuf);
|
||||
const absdir = dirbuf[0..dirlen];
|
||||
const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir});
|
||||
|
||||
const main_bytes = try std.Io.Dir.readFileAlloc(.cwd(), io, main_path, alloc, .limited(1 << 20));
|
||||
const main_source = try alloc.dupeZ(u8, main_bytes);
|
||||
var p = parser.Parser.init(alloc, main_source);
|
||||
const root = p.parse() catch return error.ParseFailed;
|
||||
|
||||
var chain = std.StringHashMap(void).init(alloc);
|
||||
var cache = imports.ModuleCache.init(alloc);
|
||||
var import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc);
|
||||
var flat_import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc);
|
||||
const stdlib_paths = [_][]const u8{};
|
||||
|
||||
const mod = try imports.resolveImports(
|
||||
alloc,
|
||||
io,
|
||||
root,
|
||||
absdir,
|
||||
main_path,
|
||||
&chain,
|
||||
&cache,
|
||||
null,
|
||||
null,
|
||||
&stdlib_paths,
|
||||
&import_graph,
|
||||
&flat_import_graph,
|
||||
.{},
|
||||
);
|
||||
|
||||
// Exactly ONE `Widget` survives the merged scope, and it is a.sx's STRUCT —
|
||||
// the function author did not displace or duplicate it.
|
||||
var widget_count: usize = 0;
|
||||
var merged_is_struct = false;
|
||||
for (mod.decls) |decl| {
|
||||
const name = decl.data.declName() orelse continue;
|
||||
if (!std.mem.eql(u8, name, "Widget")) continue;
|
||||
widget_count += 1;
|
||||
merged_is_struct = decl.data == .struct_decl;
|
||||
}
|
||||
try std.testing.expectEqual(@as(usize, 1), widget_count);
|
||||
try std.testing.expect(merged_is_struct);
|
||||
}
|
||||
@@ -5,6 +5,18 @@ const errors = @import("errors.zig");
|
||||
const c_import = @import("c_import.zig");
|
||||
const Node = ast.Node;
|
||||
|
||||
/// The `*const ast.FnDecl` a function-authoring decl carries, or null when the
|
||||
/// decl is not a function — either a bare `fn_decl` (`f :: (…) -> T { … }`) or a
|
||||
/// `const_decl` whose value is a function. Drives the per-module `module_fns`
|
||||
/// identity index (fix-0102a).
|
||||
fn fnDeclOf(decl: *const Node) ?*const ast.FnDecl {
|
||||
return switch (decl.data) {
|
||||
.fn_decl => &decl.data.fn_decl,
|
||||
.const_decl => |cd| if (cd.value.data == .fn_decl) &cd.value.data.fn_decl else null,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Comptime evaluation context for the inline-if hoisting pass below.
|
||||
/// Mirrors the values `injectComptimeConstants` will later push into the
|
||||
/// lowering's `comptime_constants` map (OS / ARCH / POINTER_SIZE), but
|
||||
@@ -392,6 +404,43 @@ pub const ResolvedModule = struct {
|
||||
/// Module cache: maps resolved file paths to their ResolvedModules.
|
||||
pub const ModuleCache = std.StringHashMap(ResolvedModule);
|
||||
|
||||
/// Per-module function identity index: function NAME → the `*const FnDecl` that
|
||||
/// module AUTHORS. Mirrors a single module's slice of `module_scopes`.
|
||||
pub const FnIndex = std.StringHashMap(*const ast.FnDecl);
|
||||
|
||||
/// `path → name → *const FnDecl`, mirroring `module_scopes`. One entry per
|
||||
/// resolved module keyed by its path (a directory's combined module keyed by
|
||||
/// `dir_path`); each entry indexes only what that module AUTHORS. Two modules
|
||||
/// each authoring `f` are retained under their own paths — the identity index
|
||||
/// fix-0102c's bare-name disambiguation consults to bind a flat call to the
|
||||
/// right author.
|
||||
pub const ModuleFns = std.StringHashMap(FnIndex);
|
||||
|
||||
/// Index a single module's authored functions (`own_decls`) into `out[path]`.
|
||||
/// First-wins WITHIN a module mirrors the scan pass; cross-module same-name
|
||||
/// authors live under their own `path` keys.
|
||||
fn indexModuleFns(allocator: std.mem.Allocator, out: *ModuleFns, path: []const u8, own_decls: []const *Node) !void {
|
||||
const gop = try out.getOrPut(path);
|
||||
if (!gop.found_existing) gop.value_ptr.* = FnIndex.init(allocator);
|
||||
for (own_decls) |decl| {
|
||||
const fd = fnDeclOf(decl) orelse continue;
|
||||
const name = decl.data.declName() orelse continue;
|
||||
if (gop.value_ptr.contains(name)) continue;
|
||||
try gop.value_ptr.put(name, fd);
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the per-module function index from a resolved program: the main module
|
||||
/// (keyed by `main_path`) plus every cached module (keyed by its own path).
|
||||
/// Mirrors how `core.zig` fills `module_scopes` from `mod.scope` + the cache.
|
||||
pub fn buildModuleFns(allocator: std.mem.Allocator, main_path: []const u8, main_mod: ResolvedModule, cache: *const ModuleCache, out: *ModuleFns) !void {
|
||||
try indexModuleFns(allocator, out, main_path, main_mod.own_decls);
|
||||
var it = cache.iterator();
|
||||
while (it.next()) |entry| {
|
||||
try indexModuleFns(allocator, out, entry.key_ptr.*, entry.value_ptr.own_decls);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolveImports(
|
||||
allocator: std.mem.Allocator,
|
||||
io: std.Io,
|
||||
@@ -404,6 +453,7 @@ pub fn resolveImports(
|
||||
diagnostics: ?*errors.DiagnosticList,
|
||||
stdlib_paths: []const []const u8,
|
||||
import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
|
||||
flat_import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
|
||||
comptime_ctx: ComptimeContext,
|
||||
) !ResolvedModule {
|
||||
// Record this file's edge set so `param_impl_map` lookups can filter
|
||||
@@ -414,6 +464,15 @@ pub fn resolveImports(
|
||||
try g.put(file_path, std.StringHashMap(void).init(allocator));
|
||||
}
|
||||
}
|
||||
// FLAT-only edge set: identical to `import_graph` but records ONLY bare
|
||||
// `#import "…"` edges (`imp.name == null`), never a namespaced
|
||||
// `ns :: #import "…"`. fix-0102c's bare-name disambiguation walks this to
|
||||
// decide which same-name authors a flat importer can actually reach.
|
||||
if (flat_import_graph) |g| {
|
||||
if (!g.contains(file_path)) {
|
||||
try g.put(file_path, std.StringHashMap(void).init(allocator));
|
||||
}
|
||||
}
|
||||
var mod = ResolvedModule{
|
||||
.path = file_path,
|
||||
.decls = &.{},
|
||||
@@ -536,6 +595,17 @@ pub fn resolveImports(
|
||||
set.put(resolved_path, {}) catch {};
|
||||
}
|
||||
}
|
||||
// The same edge, FLAT-only: recorded only for a bare `#import`
|
||||
// (`imp.name == null`), excluding a namespaced `ns :: #import`. Covers
|
||||
// both a flat file import and a flat directory import (`resolved_path`
|
||||
// is the directory in the latter case).
|
||||
if (imp.name == null) {
|
||||
if (flat_import_graph) |g| {
|
||||
if (g.getPtr(file_path)) |set| {
|
||||
set.put(resolved_path, {}) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Circular import check — only along the current chain
|
||||
if (chain.contains(resolved_path)) continue;
|
||||
@@ -563,7 +633,7 @@ pub fn resolveImports(
|
||||
// Push onto chain before recursing, pop after
|
||||
try chain.put(resolved_path, {});
|
||||
const imp_dir = dirName(resolved_path);
|
||||
const result = try resolveImports(allocator, io, imp_root, imp_dir, resolved_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph, comptime_ctx);
|
||||
const result = try resolveImports(allocator, io, imp_root, imp_dir, resolved_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph, flat_import_graph, comptime_ctx);
|
||||
_ = chain.remove(resolved_path);
|
||||
|
||||
// Cache
|
||||
@@ -571,7 +641,7 @@ pub fn resolveImports(
|
||||
break :blk result;
|
||||
} else |_| {
|
||||
// File read failed — try as directory import
|
||||
const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths, import_graph, comptime_ctx) catch {
|
||||
const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths, import_graph, flat_import_graph, comptime_ctx) catch {
|
||||
if (diagnostics) |diags| {
|
||||
diags.addFmt(.err, decl.span, "cannot read import '{s}' (not a file or directory)", .{resolved_path});
|
||||
}
|
||||
@@ -605,6 +675,7 @@ fn resolveDirectoryImport(
|
||||
span: ast.Span,
|
||||
stdlib_paths: []const []const u8,
|
||||
import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
|
||||
flat_import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
|
||||
comptime_ctx: ComptimeContext,
|
||||
) anyerror!ResolvedModule {
|
||||
// Open the directory with iteration capability
|
||||
@@ -679,7 +750,7 @@ fn resolveDirectoryImport(
|
||||
};
|
||||
|
||||
try chain.put(file_path, {});
|
||||
const result = try resolveImports(allocator, io, imp_root, dir_path, file_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph, comptime_ctx);
|
||||
const result = try resolveImports(allocator, io, imp_root, dir_path, file_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph, flat_import_graph, comptime_ctx);
|
||||
_ = chain.remove(file_path);
|
||||
|
||||
try cache.put(file_path, result);
|
||||
|
||||
@@ -10,6 +10,9 @@ const Ref = ir_mod.Ref;
|
||||
const FuncId = ir_mod.FuncId;
|
||||
const Lowering = ir_mod.Lowering;
|
||||
|
||||
const parser = @import("../parser.zig");
|
||||
const imports = @import("../imports.zig");
|
||||
|
||||
test "lower: simple function with arithmetic" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
@@ -1264,3 +1267,180 @@ test "lower: reflectionArgIsType accepts spelled types, rejects plain values (is
|
||||
try std.testing.expect(!l.reflectionArgIsType(&float_node));
|
||||
try std.testing.expect(!l.reflectionArgIsType(&bool_node));
|
||||
}
|
||||
|
||||
var g_lower_test_threaded: ?std.Io.Threaded = null;
|
||||
fn lowerTestIo() std.Io {
|
||||
if (g_lower_test_threaded == null) {
|
||||
g_lower_test_threaded = std.Io.Threaded.init(std.heap.page_allocator, .{});
|
||||
}
|
||||
return g_lower_test_threaded.?.io();
|
||||
}
|
||||
|
||||
/// Count functions named `name` that carry a REAL body (promoted from the extern
|
||||
/// stub: not `is_extern`, at least one basic block).
|
||||
fn countRealBodies(module: *ir_mod.Module, name: []const u8) usize {
|
||||
var n: usize = 0;
|
||||
for (module.functions.items) |func| {
|
||||
if (!std.mem.eql(u8, module.types.getString(func.name), name)) continue;
|
||||
if (func.is_extern) continue;
|
||||
if (func.blocks.items.len == 0) continue;
|
||||
n += 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// fix-0102b: two flat-imported modules each author `greet`. The first-wins merge
|
||||
// keeps a.sx's author in the merged decl list (the WINNER) and drops b.sx's,
|
||||
// which `module_fns` still retains (0102a). `main` itself can't bare-call `greet`
|
||||
// — under fix-0102c two flat authors make that ambiguous — so it calls a.sx's
|
||||
// `use_greet` wrapper, whose own-author call to `greet` binds a.sx's winner.
|
||||
// BEFORE the identity-addressable pass, only the winner has a real body — the
|
||||
// shadowed author has no slot at all (the pre-fix symptom: one `greet`).
|
||||
// `lowerRetainedSameNameAuthors` declares the shadowed author its OWN same-name
|
||||
// FuncId and lowers its body there, so BOTH authors carry distinct, non-extern
|
||||
// bodies, and `resolveFuncByName` still returns the winner (the name-keyed slot).
|
||||
test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102b)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
const io = lowerTestIo();
|
||||
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
|
||||
try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = "greet :: () -> s64 { 1 }\nuse_greet :: () -> s64 { greet() }\n" });
|
||||
try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "greet :: () -> s64 { 2 }\n" });
|
||||
const main_src =
|
||||
\\#import "a.sx";
|
||||
\\#import "b.sx";
|
||||
\\main :: () -> s64 { use_greet() }
|
||||
\\
|
||||
;
|
||||
try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = main_src });
|
||||
|
||||
var dirbuf: [4096]u8 = undefined;
|
||||
const dirlen = try tmp.dir.realPath(io, &dirbuf);
|
||||
const absdir = dirbuf[0..dirlen];
|
||||
|
||||
const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir});
|
||||
const main_bytes = try std.Io.Dir.readFileAlloc(.cwd(), io, main_path, alloc, .limited(1 << 20));
|
||||
const main_source = try alloc.dupeZ(u8, main_bytes);
|
||||
var p = parser.Parser.init(alloc, main_source);
|
||||
const root = p.parse() catch return error.ParseFailed;
|
||||
|
||||
var chain = std.StringHashMap(void).init(alloc);
|
||||
var cache = imports.ModuleCache.init(alloc);
|
||||
var import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc);
|
||||
var flat_import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc);
|
||||
const stdlib_paths = [_][]const u8{};
|
||||
|
||||
const mod = try imports.resolveImports(
|
||||
alloc,
|
||||
io,
|
||||
root,
|
||||
absdir,
|
||||
main_path,
|
||||
&chain,
|
||||
&cache,
|
||||
null,
|
||||
null,
|
||||
&stdlib_paths,
|
||||
&import_graph,
|
||||
&flat_import_graph,
|
||||
.{},
|
||||
);
|
||||
|
||||
// Per-module visibility scopes + authored-function index, wired exactly as
|
||||
// `core.zig` does before `lowerRoot`.
|
||||
var module_scopes = std.StringHashMap(std.StringHashMap(void)).init(alloc);
|
||||
try module_scopes.put(main_path, mod.scope);
|
||||
var cache_it = cache.iterator();
|
||||
while (cache_it.next()) |entry| {
|
||||
try module_scopes.put(entry.key_ptr.*, entry.value_ptr.scope);
|
||||
}
|
||||
var module_fns = imports.ModuleFns.init(alloc);
|
||||
try imports.buildModuleFns(alloc, main_path, mod, &cache, &module_fns);
|
||||
|
||||
const resolved_root = try alloc.create(Node);
|
||||
resolved_root.* = .{ .span = root.span, .data = .{ .root = .{ .decls = mod.decls } } };
|
||||
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var diagnostics = errors.DiagnosticList.init(alloc, main_source, main_path);
|
||||
var lowering = Lowering.init(&module);
|
||||
lowering.main_file = main_path;
|
||||
lowering.resolved_root = resolved_root;
|
||||
lowering.diagnostics = &diagnostics;
|
||||
lowering.program_index.module_scopes = &module_scopes;
|
||||
lowering.program_index.import_graph = &import_graph;
|
||||
lowering.program_index.flat_import_graph = &flat_import_graph;
|
||||
lowering.program_index.module_fns = &module_fns;
|
||||
|
||||
lowering.lowerRoot(resolved_root);
|
||||
try std.testing.expect(!diagnostics.hasErrors());
|
||||
|
||||
// Pre-fix symptom: only the winner `greet` (a.sx) has a real body — lowered
|
||||
// because `main` calls it; the shadowed author (b.sx) was dropped entirely.
|
||||
try std.testing.expectEqual(@as(usize, 1), countRealBodies(&module, "greet"));
|
||||
|
||||
// Identity-addressable pass: the shadowed author gets its OWN FuncId + body.
|
||||
lowering.lowerRetainedSameNameAuthors();
|
||||
try std.testing.expect(!diagnostics.hasErrors());
|
||||
|
||||
// Both `greet` authors now carry distinct, real (non-extern) bodies, and the
|
||||
// two FuncIds are distinct.
|
||||
try std.testing.expectEqual(@as(usize, 2), countRealBodies(&module, "greet"));
|
||||
|
||||
const name_id = module.types.internString("greet");
|
||||
var first: ?FuncId = null;
|
||||
var second: ?FuncId = null;
|
||||
for (module.functions.items, 0..) |func, i| {
|
||||
if (func.name != name_id) continue;
|
||||
if (func.is_extern or func.blocks.items.len == 0) continue;
|
||||
if (first == null) first = FuncId.fromIndex(@intCast(i)) else second = FuncId.fromIndex(@intCast(i));
|
||||
}
|
||||
try std.testing.expect(first != null and second != null);
|
||||
try std.testing.expect(first.? != second.?);
|
||||
|
||||
// F1 (attempt-2): the identity map must be keyed by the STABLE AST field
|
||||
// pointer for BOTH same-name authors — the exact pointers `fn_ast_map` and
|
||||
// `module_fns` carry — not a per-iteration switch-capture temporary. If the
|
||||
// winner were keyed by `&fd` (the scanDecls bug), this lookup by the stable
|
||||
// `fn_ast_map` pointer would miss (null). fix-0102c routes calls through
|
||||
// exactly these pointers, so the round-trip must hold here.
|
||||
const winner_fd = lowering.program_index.fn_ast_map.get("greet").?;
|
||||
const winner_fid = lowering.fn_decl_fids.get(winner_fd);
|
||||
try std.testing.expect(winner_fid != null);
|
||||
// Round-trips to the first-wins winner FuncId (resolveFuncByName's pick).
|
||||
try std.testing.expectEqual(lowering.resolveFuncByName("greet").?, winner_fid.?);
|
||||
|
||||
// The shadowed author's stable pointer lives in `module_fns`; find the one
|
||||
// that is NOT the winner and confirm IT round-trips to a DISTINCT FuncId.
|
||||
var shadow_fd: ?*const ast.FnDecl = null;
|
||||
var mf_it = module_fns.iterator();
|
||||
while (mf_it.next()) |path_entry| {
|
||||
if (path_entry.value_ptr.get("greet")) |fd| {
|
||||
if (fd != winner_fd) shadow_fd = fd;
|
||||
}
|
||||
}
|
||||
try std.testing.expect(shadow_fd != null);
|
||||
const shadow_fid = lowering.fn_decl_fids.get(shadow_fd.?);
|
||||
try std.testing.expect(shadow_fid != null);
|
||||
try std.testing.expect(shadow_fid.? != winner_fid.?);
|
||||
|
||||
// fix-0102c: THE bare-name resolver routes per caller file. `main` flat-
|
||||
// imports two `greet` authors and is its own author of neither → a bare
|
||||
// `greet()` from `main` is ambiguous. a.sx authors the WINNER, so its bare
|
||||
// `greet` resolves through the existing path (`.none`). b.sx authors the
|
||||
// SHADOW, so own-author-wins binds b.sx's distinct FuncId — not first-wins.
|
||||
const a_path = try std.fmt.allocPrint(alloc, "{s}/a.sx", .{absdir});
|
||||
const b_path = try std.fmt.allocPrint(alloc, "{s}/b.sx", .{absdir});
|
||||
try std.testing.expect(lowering.resolveBareCallee("greet", main_path) == .ambiguous);
|
||||
try std.testing.expect(lowering.resolveBareCallee("greet", a_path) == .none);
|
||||
switch (lowering.resolveBareCallee("greet", b_path)) {
|
||||
.func => |resolved| try std.testing.expectEqual(shadow_fid.?, resolved.fid),
|
||||
else => return error.TestUnexpectedResult,
|
||||
}
|
||||
// A name no module authors (and no flat import provides) never routes.
|
||||
try std.testing.expect(lowering.resolveBareCallee("nonexistent", b_path) == .none);
|
||||
}
|
||||
|
||||
732
src/ir/lower.zig
732
src/ir/lower.zig
@@ -126,6 +126,17 @@ pub const Lowering = struct {
|
||||
comptime_param_nodes: ?std.StringHashMap(*const Node) = null, // active comptime substitutions
|
||||
target_type: ?TypeId = null, // target type for struct/enum literals without explicit names
|
||||
lowered_functions: std.StringHashMap(void), // tracks which functions have been fully lowered
|
||||
/// Identity map: authoring `*const ast.FnDecl` → the FuncId `declareFunction`
|
||||
/// created for it. The name-keyed function table (`resolveFuncByName`) returns
|
||||
/// the FIRST author of a name, so two same-name authors collide there; this
|
||||
/// map addresses each author's OWN slot by decl identity (fix-0102b), letting
|
||||
/// a SHADOWED author lower its body into a distinct FuncId.
|
||||
fn_decl_fids: std.AutoHashMap(*const ast.FnDecl, FuncId),
|
||||
/// FuncId-keyed lowered tracking — the identity twin of `lowered_functions`
|
||||
/// (which keys by name). A shadowed same-name author shares the winner's name
|
||||
/// but not its FuncId, so name-keyed tracking can't tell them apart; this
|
||||
/// records which specific FuncIds have had a real body lowered (fix-0102b).
|
||||
lowered_fids: std.AutoHashMap(FuncId, void),
|
||||
local_fn_counter: u32 = 0, // unique counter for mangling local function names
|
||||
/// Declaration-name / import / visibility facts (architecture phase A1,
|
||||
/// `ProgramIndex`). Owns `import_flags`; borrows `module_scopes` /
|
||||
@@ -286,12 +297,89 @@ pub const Lowering = struct {
|
||||
ret_var_name: ?[]const u8,
|
||||
};
|
||||
|
||||
/// Caller-state protection for lowering a function body re-entrantly — a
|
||||
/// lazily lowered callee, a qualified `ns.fn` alias, or an out-of-line
|
||||
/// same-name author. `enter` snapshots the in-progress builder / scope /
|
||||
/// flag / pack / jni state and installs a fresh set for the nested body;
|
||||
/// `restore` puts the caller's state back. Lowering a callee must be
|
||||
/// transparent to the caller's own lowering — notably `block_terminated`,
|
||||
/// which leaking back would mark the caller's trailing statements
|
||||
/// dead-after-terminator (issue 0100 F2).
|
||||
const FnBodyReentry = struct {
|
||||
l: *Lowering,
|
||||
func: ?FuncId,
|
||||
block: ?BlockId,
|
||||
counter: u32,
|
||||
scope: ?*Scope,
|
||||
defer_base: usize,
|
||||
block_terminated: bool,
|
||||
force_block_value: bool,
|
||||
source_file: ?[]const u8,
|
||||
jni_env_base: usize,
|
||||
pack_arg_nodes: ?std.StringHashMap([]const *const Node),
|
||||
pack_param_count: ?std.StringHashMap(u32),
|
||||
pack_arg_types: ?std.StringHashMap([]const TypeId),
|
||||
inline_return_target: ?InlineReturnInfo,
|
||||
|
||||
fn enter(l: *Lowering) FnBodyReentry {
|
||||
const g = FnBodyReentry{
|
||||
.l = l,
|
||||
.func = l.builder.func,
|
||||
.block = l.builder.current_block,
|
||||
.counter = l.builder.inst_counter,
|
||||
.scope = l.scope,
|
||||
.defer_base = l.func_defer_base,
|
||||
.block_terminated = l.block_terminated,
|
||||
.force_block_value = l.force_block_value,
|
||||
.source_file = l.current_source_file,
|
||||
.jni_env_base = l.jni_env_stack_base,
|
||||
.pack_arg_nodes = l.pack_arg_nodes,
|
||||
.pack_param_count = l.pack_param_count,
|
||||
.pack_arg_types = l.pack_arg_types,
|
||||
.inline_return_target = l.inline_return_target,
|
||||
};
|
||||
// The `#jni_env` Ref stack is lexical to ONE function's instruction
|
||||
// stream; move the visible base to the current top. Pack-fn mono
|
||||
// state is likewise lexical to the pack-fn body — null it so a
|
||||
// callee sharing a param NAME with the active pack doesn't fold the
|
||||
// outer mono's arity into its own `<name>.len`.
|
||||
l.jni_env_stack_base = l.jni_env_stack.items.len;
|
||||
l.pack_arg_nodes = null;
|
||||
l.pack_param_count = null;
|
||||
l.pack_arg_types = null;
|
||||
l.inline_return_target = null;
|
||||
l.func_defer_base = l.defer_stack.items.len;
|
||||
l.block_terminated = false;
|
||||
l.force_block_value = false;
|
||||
return g;
|
||||
}
|
||||
|
||||
fn restore(g: FnBodyReentry) void {
|
||||
const l = g.l;
|
||||
l.setCurrentSourceFile(g.source_file);
|
||||
l.scope = g.scope;
|
||||
l.func_defer_base = g.defer_base;
|
||||
l.block_terminated = g.block_terminated;
|
||||
l.force_block_value = g.force_block_value;
|
||||
l.builder.func = g.func;
|
||||
l.builder.current_block = g.block;
|
||||
l.builder.inst_counter = g.counter;
|
||||
l.jni_env_stack_base = g.jni_env_base;
|
||||
l.pack_arg_nodes = g.pack_arg_nodes;
|
||||
l.pack_param_count = g.pack_param_count;
|
||||
l.pack_arg_types = g.pack_arg_types;
|
||||
l.inline_return_target = g.inline_return_target;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(module: *Module) Lowering {
|
||||
return .{
|
||||
.module = module,
|
||||
.builder = Builder.init(module),
|
||||
.alloc = module.alloc,
|
||||
.lowered_functions = std.StringHashMap(void).init(module.alloc),
|
||||
.fn_decl_fids = std.AutoHashMap(*const ast.FnDecl, FuncId).init(module.alloc),
|
||||
.lowered_fids = std.AutoHashMap(FuncId, void).init(module.alloc),
|
||||
.program_index = ProgramIndex.init(module.alloc),
|
||||
};
|
||||
}
|
||||
@@ -703,8 +791,14 @@ pub const Lowering = struct {
|
||||
self.program_index.fn_ast_map.put(fd.name, &decl.data.fn_decl) catch {};
|
||||
self.program_index.import_flags.put(fd.name, is_imported) catch {};
|
||||
}
|
||||
// Declare extern stub for all functions (bodies lowered lazily)
|
||||
self.declareFunction(&fd, fd.name);
|
||||
// Declare extern stub for all functions (bodies lowered
|
||||
// lazily). Key the identity map (`fn_decl_fids`, inside
|
||||
// `declareFunction`) by the STABLE AST field pointer — the
|
||||
// same `&decl.data.fn_decl` stored in `fn_ast_map` and
|
||||
// `module_fns` — not the switch-capture copy `fd`, whose
|
||||
// address is a per-iteration stack temporary that no later
|
||||
// decl-identity lookup can reproduce.
|
||||
self.declareFunction(&decl.data.fn_decl, fd.name);
|
||||
},
|
||||
.const_decl => |cd| {
|
||||
if (cd.value.data == .fn_decl) {
|
||||
@@ -1339,6 +1433,14 @@ pub const Lowering = struct {
|
||||
/// Pass 2: Lower main function body and comptime side-effects.
|
||||
fn lowerMainAndComptime(self: *Lowering, decls: []const *const Node) void {
|
||||
for (decls) |decl| {
|
||||
// A `#run` body lowers in its OWN module's source context (fix-0102d
|
||||
// site 4): `NAME :: #run f()` written in an imported module must
|
||||
// resolve a bare `f` from that module's flat imports, not the main
|
||||
// file's. Without this, `resolveBareCallee` runs with the main
|
||||
// file's perspective and reports a genuine per-source author as
|
||||
// ambiguous. Mirrors `scanDecls` / `lowerDecls`, which already set
|
||||
// the source file per decl.
|
||||
self.setCurrentSourceFile(decl.source_file);
|
||||
switch (decl.data) {
|
||||
.const_decl => |cd| {
|
||||
if (cd.value.data == .fn_decl) {
|
||||
@@ -1367,6 +1469,174 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lower every SHADOWED same-name function author into its OWN FuncId with a
|
||||
/// real (non-extern) body — the identity-addressable lowering PATH this step
|
||||
/// adds (fix-0102b). It does NOT run during a default compile: the name path
|
||||
/// stays the sole resolver, so the suite is byte-for-byte unchanged. fix-0102c
|
||||
/// invokes it as part of routing bare flat calls to the right author; until
|
||||
/// then it is exercised by the lower-test regression that asserts two distinct
|
||||
/// non-extern bodies for a same-name collision.
|
||||
///
|
||||
/// The first-wins flat/directory merge keeps exactly one author per name in
|
||||
/// the merged decl list; `scanDecls` declares that WINNER (lowered on demand
|
||||
/// through the name-keyed `lazyLowerFunction`). fix-0102a retained every
|
||||
/// dropped same-name author in `module_fns` (path → name → `*FnDecl`) without
|
||||
/// touching resolution; this walks that index and gives each shadowed author
|
||||
/// its own slot: `declareFunction` (identity-mapped to a fresh same-name
|
||||
/// FuncId) + `lowerFunctionBodyInto` (its body, in its own module's
|
||||
/// visibility context). Two same-name authors then carry distinct FuncIds and
|
||||
/// distinct bodies, while `resolveFuncByName` still returns the first (winner)
|
||||
/// author so existing calls bind first-wins.
|
||||
///
|
||||
/// Scoped to DIRECT flat imports of the main file: a `module_fns` entry whose
|
||||
/// path is the main file or one of its bare `#import` edges. A namespaced
|
||||
/// (`ns :: #import`) author has no bare-name winner and is excluded both by
|
||||
/// that flat-edge gate and by the `fn_ast_map` winner lookup below.
|
||||
pub fn lowerRetainedSameNameAuthors(self: *Lowering) void {
|
||||
const module_fns = self.program_index.module_fns orelse return;
|
||||
const main_file = self.main_file orelse return;
|
||||
const flat_graph = self.program_index.flat_import_graph orelse return;
|
||||
const main_flat_edges = flat_graph.get(main_file);
|
||||
|
||||
var path_it = module_fns.iterator();
|
||||
while (path_it.next()) |path_entry| {
|
||||
const path = path_entry.key_ptr.*;
|
||||
const is_eligible = std.mem.eql(u8, path, main_file) or
|
||||
(main_flat_edges != null and main_flat_edges.?.contains(path));
|
||||
if (!is_eligible) continue;
|
||||
|
||||
var fn_it = path_entry.value_ptr.iterator();
|
||||
while (fn_it.next()) |fn_entry| {
|
||||
const name = fn_entry.key_ptr.*;
|
||||
const fd = fn_entry.value_ptr.*;
|
||||
|
||||
// A name with no bare winner is namespaced-only (`ns.fn`) — it
|
||||
// never participated in the flat merge, so it has no shadow to
|
||||
// lower. The author already owning the name-keyed slot (the
|
||||
// first-wins winner) lowers through the normal lazy path.
|
||||
const winner = self.program_index.fn_ast_map.get(name) orelse continue;
|
||||
if (winner == fd) continue;
|
||||
|
||||
// Only plain free functions get an out-of-line slot; generic /
|
||||
// foreign / builtin / #compiler authors keep their existing
|
||||
// dispatch (mirrors lazyLowerFunction / declareFunction guards).
|
||||
if (!isPlainFreeFn(fd)) continue;
|
||||
|
||||
_ = self.bareAuthorFuncId(fd, name, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of bare-call disambiguation (fix-0102c).
|
||||
pub const BareCallee = union(enum) {
|
||||
/// Bind the call to this specific author — its identity-addressable
|
||||
/// FuncId (fix-0102b's `bareAuthorFuncId`) AND its `*FnDecl`. The decl
|
||||
/// travels with the FuncId so every callee-signature decision in the
|
||||
/// call path (variadic packing, …) reads the RESOLVED author, never a
|
||||
/// first-wins re-lookup by name (fix-0102c F1).
|
||||
func: ResolvedAuthor,
|
||||
/// ≥2 distinct flat authors are reachable from the caller and none is
|
||||
/// the caller's own — the bare call can't pick one; require a qualifier.
|
||||
ambiguous,
|
||||
/// 0 or 1 reachable author, or the resolved author IS the existing
|
||||
/// bare-name winner — defer to the existing path, byte-for-byte.
|
||||
none,
|
||||
};
|
||||
|
||||
/// A resolved bare-call author: its FuncId and the `*FnDecl` that defined
|
||||
/// it, kept together so the call path has ONE source of truth for the
|
||||
/// callee (no re-fetch by name after resolution).
|
||||
pub const ResolvedAuthor = struct { fid: FuncId, decl: *const ast.FnDecl };
|
||||
|
||||
/// THE bare-name call resolver (fix-0102c). One canonical traversal over
|
||||
/// fix-0102a's `module_fns` + `flat_import_graph` that routes a bare
|
||||
/// identifier call `name` from `caller_file` to the right same-name author
|
||||
/// when flat imports introduce a genuine collision. Every single-author /
|
||||
/// local / parameter / std / qualified name resolves through the EXISTING
|
||||
/// path unchanged: the resolver returns `.none` whenever the outcome would
|
||||
/// match first-wins, so nothing on the common path is perturbed.
|
||||
///
|
||||
/// - **own-author wins**: if `caller_file` authors `name` and the bare-name
|
||||
/// first-wins winner is a DIFFERENT author, bind the caller's own author.
|
||||
/// (When the winner already IS the caller's own — the single-author and
|
||||
/// first-importer cases — `.none` lets the existing path bind it.)
|
||||
/// - else collect the authors reachable via `caller_file`'s FLAT import
|
||||
/// edges (bare `#import` of a file or directory, never a namespaced
|
||||
/// `ns :: #import`), deduped by `FnDecl` identity (a diamond import of the
|
||||
/// same module is one author): `≥2 distinct` → `.ambiguous`; exactly one
|
||||
/// that DIFFERS from the winner → bind it; otherwise `.none`.
|
||||
///
|
||||
/// Generic / comptime / foreign / builtin authors are never rerouted — the
|
||||
/// existing dispatch owns those shapes — so the resolver returns `.none`.
|
||||
pub fn resolveBareCallee(self: *Lowering, name: []const u8, caller_file: []const u8) BareCallee {
|
||||
const module_fns = self.program_index.module_fns orelse return .none;
|
||||
const winner = self.program_index.fn_ast_map.get(name);
|
||||
|
||||
// own-author wins.
|
||||
if (module_fns.get(caller_file)) |own_fns| {
|
||||
if (own_fns.get(name)) |own| {
|
||||
if (winner != null and winner.? == own) return .none;
|
||||
if (!isPlainFreeFn(own)) return .none;
|
||||
return .{ .func = .{ .fid = self.bareAuthorFuncId(own, name, caller_file), .decl = own } };
|
||||
}
|
||||
}
|
||||
|
||||
// Caller does not author `name` → collect its flat-reachable authors.
|
||||
const flat_graph = self.program_index.flat_import_graph orelse return .none;
|
||||
const edges = flat_graph.get(caller_file) orelse return .none;
|
||||
var distinct = std.AutoHashMap(*const ast.FnDecl, []const u8).init(self.alloc);
|
||||
defer distinct.deinit();
|
||||
var edge_it = edges.iterator();
|
||||
while (edge_it.next()) |e| {
|
||||
const fns = module_fns.get(e.key_ptr.*) orelse continue;
|
||||
// Only plain free functions are eligible for rerouting; generic /
|
||||
// foreign / builtin / #compiler authors keep their existing
|
||||
// dispatch. Filtering BEFORE the count gate means a same-name
|
||||
// collision of non-plain authors (e.g. two flat-imported modules
|
||||
// each `#foreign`ing the same symbol) is NOT counted as ambiguous —
|
||||
// it falls through to `.none` and the existing first-wins path.
|
||||
if (fns.get(name)) |fd| {
|
||||
if (!isPlainFreeFn(fd)) continue;
|
||||
distinct.put(fd, e.key_ptr.*) catch {};
|
||||
}
|
||||
}
|
||||
if (distinct.count() == 0) return .none;
|
||||
if (distinct.count() >= 2) return .ambiguous;
|
||||
|
||||
var one_it = distinct.iterator();
|
||||
const entry = one_it.next().?;
|
||||
const the_one = entry.key_ptr.*;
|
||||
const the_path = entry.value_ptr.*;
|
||||
if (winner != null and winner.? == the_one) return .none;
|
||||
return .{ .func = .{ .fid = self.bareAuthorFuncId(the_one, name, the_path), .decl = the_one } };
|
||||
}
|
||||
|
||||
/// The FuncId for a resolved bare-call author, ensuring its body is lowered.
|
||||
/// Only ever called for a SHADOW (an author that is not the name-keyed
|
||||
/// winner): the winner owns the name-keyed slot and lowers through the
|
||||
/// normal lazy path, so `resolveBareCallee` returns `.none` for it. A shadow
|
||||
/// is declared a fresh same-name FuncId in its OWN module's visibility
|
||||
/// context and its body lowered into that slot via fix-0102b's identity-
|
||||
/// addressable `lowerFunctionBodyInto`. Idempotent: `lowered_fids` tracks
|
||||
/// which slots already carry a body.
|
||||
fn bareAuthorFuncId(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, path: []const u8) FuncId {
|
||||
if (self.fn_decl_fids.get(fd)) |fid| {
|
||||
if (!self.lowered_fids.contains(fid)) {
|
||||
self.lowered_fids.put(fid, {}) catch {};
|
||||
self.lowerFunctionBodyInto(fd, fid, name);
|
||||
}
|
||||
return fid;
|
||||
}
|
||||
const saved_src = self.current_source_file;
|
||||
self.setCurrentSourceFile(path);
|
||||
self.declareFunction(fd, name);
|
||||
self.setCurrentSourceFile(saved_src);
|
||||
const fid = self.fn_decl_fids.get(fd).?;
|
||||
self.lowered_fids.put(fid, {}) catch {};
|
||||
self.lowerFunctionBodyInto(fd, fid, name);
|
||||
return fid;
|
||||
}
|
||||
|
||||
/// Declare a function as an extern stub (signature only, no body).
|
||||
pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void {
|
||||
// Skip generic templates — they're monomorphized on demand, not declared as extern
|
||||
@@ -1421,6 +1691,7 @@ pub const Lowering = struct {
|
||||
func.is_variadic = is_variadic;
|
||||
func.has_implicit_ctx = wants_ctx;
|
||||
self.foreign_name_map.put(name, c_name) catch {};
|
||||
self.fn_decl_fids.put(fd, fid) catch {};
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1432,6 +1703,7 @@ pub const Lowering = struct {
|
||||
func.source_file = self.current_source_file;
|
||||
func.is_variadic = is_variadic;
|
||||
func.has_implicit_ctx = wants_ctx;
|
||||
self.fn_decl_fids.put(fd, fid) catch {};
|
||||
}
|
||||
|
||||
/// Register a namespaced import's OWN functions under their module-qualified
|
||||
@@ -1581,73 +1853,11 @@ pub const Lowering = struct {
|
||||
// Mark as lowered before lowering (prevents infinite recursion)
|
||||
self.lowered_functions.put(name, {}) catch {};
|
||||
|
||||
// Save builder state (same pattern as lambda lowering)
|
||||
const saved_func = self.builder.func;
|
||||
const saved_block = self.builder.current_block;
|
||||
const saved_counter = self.builder.inst_counter;
|
||||
const saved_scope = self.scope;
|
||||
const saved_defer_base = self.func_defer_base;
|
||||
const saved_block_terminated = self.block_terminated;
|
||||
const saved_force_block_value = self.force_block_value;
|
||||
const saved_source_file = self.current_source_file;
|
||||
// Lowering a callee must be transparent to the caller's lowering
|
||||
// state: restore the FULL saved context on EVERY exit path through one
|
||||
// defer so the three exits (non-null branch, already-promoted early
|
||||
// return, null-FuncId `ns.fn` alias branch) cannot drift. Notably
|
||||
// `block_terminated` — a qualified alias whose body terminates (e.g. a
|
||||
// constant-folded `if true { return … }`) leaves it true, and leaking
|
||||
// that into the caller marks the caller's own trailing statements
|
||||
// dead-after-terminator (issue 0100 F2). The jni/pack/foreign-class
|
||||
// fields keep their own defers above.
|
||||
defer {
|
||||
self.setCurrentSourceFile(saved_source_file);
|
||||
self.scope = saved_scope;
|
||||
self.func_defer_base = saved_defer_base;
|
||||
self.block_terminated = saved_block_terminated;
|
||||
self.force_block_value = saved_force_block_value;
|
||||
self.builder.func = saved_func;
|
||||
self.builder.current_block = saved_block;
|
||||
self.builder.inst_counter = saved_counter;
|
||||
}
|
||||
// The `#jni_env` Ref stack is lexical within ONE function's instruction
|
||||
// stream — Refs from the caller don't dereference correctly in this
|
||||
// callee's body. Move the visible base to the current top so
|
||||
// omitted-env `#jni_call` in this fn doesn't accidentally pick up the
|
||||
// caller's Refs. Defer covers all the early-return paths below.
|
||||
const saved_jni_env_base = self.jni_env_stack_base;
|
||||
self.jni_env_stack_base = self.jni_env_stack.items.len;
|
||||
defer self.jni_env_stack_base = saved_jni_env_base;
|
||||
// Pack-fn mono state is lexical to the pack-fn body. A lazily
|
||||
// lowered callee may share a param NAME with the active pack
|
||||
// (e.g. `walk(args: []Any)` called from `probe(..$args)`); without
|
||||
// isolation, `lowerFieldAccess`'s `<pack_name>.len` intercept
|
||||
// folds the callee's `args.len` to the outer mono's arity and
|
||||
// bakes the constant into the IR. Same shape for the AST-node
|
||||
// and per-element-type maps. Null out for the duration of the
|
||||
// body lowering and restore on exit.
|
||||
const saved_pan = self.pack_arg_nodes;
|
||||
const saved_ppc = self.pack_param_count;
|
||||
const saved_pat = self.pack_arg_types;
|
||||
const saved_iri = self.inline_return_target;
|
||||
self.pack_arg_nodes = null;
|
||||
self.pack_param_count = null;
|
||||
self.pack_arg_types = null;
|
||||
self.inline_return_target = null;
|
||||
defer {
|
||||
self.pack_arg_nodes = saved_pan;
|
||||
self.pack_param_count = saved_ppc;
|
||||
self.pack_arg_types = saved_pat;
|
||||
self.inline_return_target = saved_iri;
|
||||
}
|
||||
self.func_defer_base = self.defer_stack.items.len;
|
||||
self.block_terminated = false;
|
||||
self.force_block_value = false;
|
||||
|
||||
// Find the existing extern stub and replace it with a full body
|
||||
// Find the existing extern stub (from scanDecls), keyed by NAME — the
|
||||
// FIRST author of a name owns this slot. A shadowed same-name author is
|
||||
// not here (it has no name-keyed slot); it is lowered out-of-line into
|
||||
// its OWN FuncId by `lowerRetainedSameNameAuthors` (fix-0102b).
|
||||
const name_id = self.module.types.internString(name);
|
||||
const ret_ty = self.resolveReturnType(fd);
|
||||
|
||||
// Look up the existing function declaration (from scanDecls)
|
||||
var func_id: ?FuncId = null;
|
||||
for (self.module.functions.items, 0..) |func, i| {
|
||||
if (func.name == name_id) {
|
||||
@@ -1656,100 +1866,118 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
if (func_id == null) {
|
||||
// Function not yet declared — create it fresh via lowerFunction.
|
||||
// A module-qualified alias (`ns.fn`, issue 0100) is registered in
|
||||
// `fn_ast_map` without an eager `declareFunction`, so there's no
|
||||
// `Function.source_file` to switch to (the path above). Restore the
|
||||
// alias's OWN declaring source before lowering its body, otherwise
|
||||
// it lowers in the caller's visibility context and an own-import
|
||||
// callee (`foo` calling `helper` from `foo`'s module's flat import)
|
||||
// is reported "not visible" (issue 0100 F1).
|
||||
if (self.program_index.qualified_fn_source.get(name)) |src| {
|
||||
self.setCurrentSourceFile(src);
|
||||
}
|
||||
self.lowerFunction(fd, name, false);
|
||||
return; // caller state restored by the top-level defer
|
||||
}
|
||||
|
||||
if (func_id) |fid| {
|
||||
// Re-use the existing function slot — switch builder to it
|
||||
self.builder.func = fid;
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
self.setCurrentSourceFile(func.source_file);
|
||||
if (!func.is_extern) {
|
||||
// Already promoted (e.g., via lowerComptimeDeps) — skip.
|
||||
// Caller state restored by the top-level defer.
|
||||
return;
|
||||
}
|
||||
func.is_extern = false; // promote from extern stub to real function
|
||||
func.linkage = if (isExportedEntryName(name)) .external else .internal;
|
||||
if (fd.call_conv == .c) func.call_conv = .c;
|
||||
// Set inst_counter to param count (params occupy refs 0..N-1).
|
||||
// IR params = AST params + 1 if the function carries `__sx_ctx`
|
||||
// at slot 0.
|
||||
const ctx_slots: usize = if (func.has_implicit_ctx) 1 else 0;
|
||||
std.debug.assert(func.params.len == fd.params.len + ctx_slots);
|
||||
self.builder.inst_counter = @intCast(func.params.len);
|
||||
|
||||
// Create entry block
|
||||
const entry_name = self.module.types.internString("entry");
|
||||
const entry = self.builder.appendBlock(entry_name, &.{});
|
||||
self.builder.switchToBlock(entry);
|
||||
|
||||
// Create scope and bind params
|
||||
var scope = Scope.init(self.alloc, null);
|
||||
defer scope.deinit();
|
||||
self.scope = &scope;
|
||||
|
||||
// The implicit `__sx_ctx` param (when present) lives at slot 0;
|
||||
// user params shift by one. `current_ctx_ref` is bound to slot 0
|
||||
// so call-site lowering can prepend it to every sx-to-sx call.
|
||||
// For OS-called entry points (main / JNI hooks), there's no
|
||||
// ctx param at all — we synthesise `&__sx_default_context` and
|
||||
// bind `current_ctx_ref` to its address so the body's sx-to-sx
|
||||
// calls have a sensible Context to forward.
|
||||
const wants_ctx = self.funcWantsImplicitCtx(fd);
|
||||
const saved_ctx_ref = self.current_ctx_ref;
|
||||
defer self.current_ctx_ref = saved_ctx_ref;
|
||||
const user_param_base: u32 = if (wants_ctx) 1 else 0;
|
||||
if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
|
||||
|
||||
for (fd.params, 0..) |p, i| {
|
||||
const pty = self.resolveParamType(&p);
|
||||
const slot = self.builder.alloca(pty);
|
||||
const param_ref = Ref.fromIndex(@intCast(i + user_param_base));
|
||||
self.builder.store(slot, param_ref);
|
||||
scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
|
||||
}
|
||||
|
||||
// Inbound entry points and callconv(.c) sx functions: bind
|
||||
// current_ctx_ref to the static default before any user code
|
||||
// runs. C-callable sx functions don't get a __sx_ctx param,
|
||||
// but their bodies may call ctx-aware sx functions / fn-ptrs
|
||||
// and need a real Context to forward.
|
||||
if (!wants_ctx and self.implicit_ctx_enabled) {
|
||||
if (self.program_index.global_names.get("__sx_default_context")) |dctx_gi| {
|
||||
self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, self.module.types.ptrTo(.void));
|
||||
}
|
||||
}
|
||||
|
||||
// Lower the function body (set target_type to return type for implicit returns)
|
||||
const saved_target = self.target_type;
|
||||
self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null;
|
||||
if (ret_ty != .void and ret_ty != .noreturn) {
|
||||
self.lowerValueBody(fd.body, ret_ty);
|
||||
} else {
|
||||
// void / noreturn: no value to return — lower as statements and
|
||||
// let `ensureTerminator` close the block (ret void / unreachable).
|
||||
self.lowerBlock(fd.body);
|
||||
self.ensureTerminator(ret_ty);
|
||||
}
|
||||
self.target_type = saved_target;
|
||||
|
||||
self.builder.finalize();
|
||||
self.lowerFunctionBodyInto(fd, fid, name);
|
||||
return;
|
||||
}
|
||||
// Caller state restored by the top-level defer.
|
||||
|
||||
// Function not yet declared — create it fresh via lowerFunction. A
|
||||
// module-qualified alias (`ns.fn`, issue 0100) is registered in
|
||||
// `fn_ast_map` without an eager `declareFunction`, so there's no
|
||||
// `Function.source_file` to switch to. Restore the alias's OWN declaring
|
||||
// source before lowering its body, otherwise it lowers in the caller's
|
||||
// visibility context and an own-import callee (`foo` calling `helper`
|
||||
// from `foo`'s module's flat import) is reported "not visible" (0100 F1).
|
||||
// The reentry guard keeps the nested lowering transparent to the caller.
|
||||
var reentry = FnBodyReentry.enter(self);
|
||||
defer reentry.restore();
|
||||
if (self.program_index.qualified_fn_source.get(name)) |src| {
|
||||
self.setCurrentSourceFile(src);
|
||||
}
|
||||
self.lowerFunction(fd, name, false);
|
||||
}
|
||||
|
||||
/// Lower `fd`'s body into the SPECIFIC `fid`, promoting its extern stub to a
|
||||
/// real function. Identity-addressable: the caller passes the exact FuncId,
|
||||
/// so a SHADOWED same-name author lowers into its OWN slot instead of
|
||||
/// colliding on the name-keyed `resolveFuncByName` (which returns the first
|
||||
/// author, the very split that trips issue 0100's param-count assert). Self-
|
||||
/// contained — the `FnBodyReentry` guard makes the nested lowering
|
||||
/// transparent to any in-progress caller body (issue 0100 F2) — so it serves
|
||||
/// both `lazyLowerFunction`'s name-keyed found path and the out-of-line
|
||||
/// `lowerRetainedSameNameAuthors` pass.
|
||||
fn lowerFunctionBodyInto(self: *Lowering, fd: *const ast.FnDecl, fid: FuncId, name: []const u8) void {
|
||||
// objc-defined-class method context for `*Self` substitution (M1.2 A.2b);
|
||||
// the resolveReturnType / resolveParamType calls below consult it.
|
||||
const saved_fc = self.current_foreign_class;
|
||||
defer self.current_foreign_class = saved_fc;
|
||||
if (self.lookupObjcDefinedClassForMethod(name)) |fcd| {
|
||||
self.current_foreign_class = fcd;
|
||||
}
|
||||
|
||||
var reentry = FnBodyReentry.enter(self);
|
||||
defer reentry.restore();
|
||||
|
||||
const ret_ty = self.resolveReturnType(fd);
|
||||
|
||||
// Re-use the existing function slot — switch builder to it.
|
||||
self.builder.func = fid;
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
self.setCurrentSourceFile(func.source_file);
|
||||
if (!func.is_extern) {
|
||||
// Already promoted (e.g., via lowerComptimeDeps) — skip.
|
||||
return;
|
||||
}
|
||||
func.is_extern = false; // promote from extern stub to real function
|
||||
func.linkage = if (isExportedEntryName(name)) .external else .internal;
|
||||
if (fd.call_conv == .c) func.call_conv = .c;
|
||||
// Set inst_counter to param count (params occupy refs 0..N-1). IR params
|
||||
// = AST params + 1 if the function carries `__sx_ctx` at slot 0.
|
||||
const ctx_slots: usize = if (func.has_implicit_ctx) 1 else 0;
|
||||
std.debug.assert(func.params.len == fd.params.len + ctx_slots);
|
||||
self.builder.inst_counter = @intCast(func.params.len);
|
||||
|
||||
// Create entry block
|
||||
const entry_name = self.module.types.internString("entry");
|
||||
const entry = self.builder.appendBlock(entry_name, &.{});
|
||||
self.builder.switchToBlock(entry);
|
||||
|
||||
// Create scope and bind params
|
||||
var scope = Scope.init(self.alloc, null);
|
||||
defer scope.deinit();
|
||||
self.scope = &scope;
|
||||
|
||||
// The implicit `__sx_ctx` param (when present) lives at slot 0; user
|
||||
// params shift by one. `current_ctx_ref` is bound to slot 0 so call-site
|
||||
// lowering can prepend it to every sx-to-sx call. For OS-called entry
|
||||
// points (main / JNI hooks) there's no ctx param — synthesise
|
||||
// `&__sx_default_context` and bind `current_ctx_ref` to its address.
|
||||
const wants_ctx = self.funcWantsImplicitCtx(fd);
|
||||
const saved_ctx_ref = self.current_ctx_ref;
|
||||
defer self.current_ctx_ref = saved_ctx_ref;
|
||||
const user_param_base: u32 = if (wants_ctx) 1 else 0;
|
||||
if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
|
||||
|
||||
for (fd.params, 0..) |p, i| {
|
||||
const pty = self.resolveParamType(&p);
|
||||
const slot = self.builder.alloca(pty);
|
||||
const param_ref = Ref.fromIndex(@intCast(i + user_param_base));
|
||||
self.builder.store(slot, param_ref);
|
||||
scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
|
||||
}
|
||||
|
||||
// Inbound entry points + callconv(.c) sx functions: bind current_ctx_ref
|
||||
// to the static default before any user code runs.
|
||||
if (!wants_ctx and self.implicit_ctx_enabled) {
|
||||
if (self.program_index.global_names.get("__sx_default_context")) |dctx_gi| {
|
||||
self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, self.module.types.ptrTo(.void));
|
||||
}
|
||||
}
|
||||
|
||||
// Lower the function body (set target_type to return type for implicit returns)
|
||||
const saved_target = self.target_type;
|
||||
self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null;
|
||||
if (ret_ty != .void and ret_ty != .noreturn) {
|
||||
self.lowerValueBody(fd.body, ret_ty);
|
||||
} else {
|
||||
// void / noreturn: no value to return — lower as statements and let
|
||||
// `ensureTerminator` close the block (ret void / unreachable).
|
||||
self.lowerBlock(fd.body);
|
||||
self.ensureTerminator(ret_ty);
|
||||
}
|
||||
self.target_type = saved_target;
|
||||
|
||||
self.builder.finalize();
|
||||
}
|
||||
|
||||
/// Lower a single function declaration.
|
||||
@@ -3022,10 +3250,36 @@ pub const Lowering = struct {
|
||||
const str = self.builder.constString(sid);
|
||||
break :blk self.builder.boxAny(str, .string);
|
||||
}
|
||||
if (!self.lowered_functions.contains(eff_fn_name)) {
|
||||
self.lazyLowerFunction(eff_fn_name);
|
||||
}
|
||||
if (self.resolveFuncByName(eff_fn_name)) |fid| {
|
||||
// fix-0102d site 2: taking a bare same-name fn as a VALUE
|
||||
// (func_ref, fn-ptr / closure coercion) must capture the
|
||||
// RESOLVED author's FuncId for a genuine flat collision, not
|
||||
// the first-wins winner's. Plain bare name only; `.ambiguous`
|
||||
// → loud diagnostic; `.none` → existing first-wins path. The
|
||||
// winner is lazily lowered ONLY on `.none` — a rerouted value
|
||||
// never uses the winner, so its body must not be lowered.
|
||||
const value_fid: ?FuncId = blk_fv: {
|
||||
if (std.mem.eql(u8, eff_fn_name, id.name) and
|
||||
self.program_index.ufcs_alias_map.get(id.name) == null and
|
||||
(if (self.scope) |scope| scope.lookup(id.name) == null else true))
|
||||
{
|
||||
if (self.current_source_file) |caller_file| {
|
||||
switch (self.resolveBareCallee(id.name, caller_file)) {
|
||||
.func => |resolved| break :blk_fv resolved.fid,
|
||||
.ambiguous => {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, node.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{id.name});
|
||||
break :blk self.emitError(id.name, node.span);
|
||||
},
|
||||
.none => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!self.lowered_functions.contains(eff_fn_name)) {
|
||||
self.lazyLowerFunction(eff_fn_name);
|
||||
}
|
||||
break :blk_fv self.resolveFuncByName(eff_fn_name);
|
||||
};
|
||||
if (value_fid) |fid| {
|
||||
// Auto-promote bare function → closure when target_type is closure
|
||||
if (self.target_type) |tt| {
|
||||
if (!tt.isBuiltin()) {
|
||||
@@ -7052,10 +7306,32 @@ pub const Lowering = struct {
|
||||
// If argument is a bare function name, create a proper closure from it
|
||||
if (arg.data == .identifier) {
|
||||
const fn_name = arg.data.identifier.name;
|
||||
if (!self.lowered_functions.contains(fn_name)) {
|
||||
self.lazyLowerFunction(fn_name);
|
||||
}
|
||||
if (self.resolveFuncByName(fn_name)) |fid| {
|
||||
// fix-0102d site 2: `closure(fn)` over a genuine flat same-name
|
||||
// collision must capture the RESOLVED author's FuncId, not the
|
||||
// first-wins winner's. Plain bare name only; `.ambiguous`
|
||||
// → loud diagnostic; `.none` → existing first-wins path.
|
||||
const closure_fid: ?FuncId = blk_cl: {
|
||||
if (self.program_index.ufcs_alias_map.get(fn_name) == null and
|
||||
(if (self.scope) |scope| scope.lookup(fn_name) == null else true))
|
||||
{
|
||||
if (self.current_source_file) |caller_file| {
|
||||
switch (self.resolveBareCallee(fn_name, caller_file)) {
|
||||
.func => |resolved| break :blk_cl resolved.fid,
|
||||
.ambiguous => {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, arg.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fn_name});
|
||||
return Ref.none;
|
||||
},
|
||||
.none => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!self.lowered_functions.contains(fn_name)) {
|
||||
self.lazyLowerFunction(fn_name);
|
||||
}
|
||||
break :blk_cl self.resolveFuncByName(fn_name);
|
||||
};
|
||||
if (closure_fid) |fid| {
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
// Build closure type from user-visible params only —
|
||||
// skip the implicit __sx_ctx param.
|
||||
@@ -7338,6 +7614,41 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
}
|
||||
// fix-0102c: a genuine flat same-name collision — bind the
|
||||
// caller file's OWN author (or its single flat-reachable
|
||||
// author), or reject a bare call to a name ≥2 imported modules
|
||||
// author. Only a plain top-level identifier call routes here:
|
||||
// scope-mangled / UFCS-aliased / locally-shadowed names and
|
||||
// 0/1-author names fall straight to the existing path below
|
||||
// (`resolveBareCallee` returns `.none`).
|
||||
if (std.mem.eql(u8, func_name, id.name) and
|
||||
(if (self.scope) |scope| scope.lookup(id.name) == null else true))
|
||||
{
|
||||
if (self.current_source_file) |caller_file| {
|
||||
switch (self.resolveBareCallee(func_name, caller_file)) {
|
||||
.none => {},
|
||||
.ambiguous => {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{func_name});
|
||||
return Ref.none;
|
||||
},
|
||||
.func => |resolved| {
|
||||
const fid = resolved.fid;
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
const ret_ty = func.ret;
|
||||
const params = func.params;
|
||||
// The RESOLVED author's decl drives variadic
|
||||
// packing — not a first-wins re-lookup by name,
|
||||
// whose variadic shape may differ (fix-0102c F1).
|
||||
self.packVariadicCallArgs(resolved.decl, c, &args);
|
||||
const final_args = self.prependCtxIfNeeded(func, args.items);
|
||||
self.coerceCallArgs(final_args, params);
|
||||
if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len);
|
||||
return self.builder.call(fid, final_args, ret_ty);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for comptime-expanded or generic functions
|
||||
if (self.program_index.fn_ast_map.get(func_name)) |fd| {
|
||||
if (hasComptimeParams(fd)) {
|
||||
@@ -7820,12 +8131,33 @@ pub const Lowering = struct {
|
||||
// `recv.fn(args)` → `fn(recv, args)`). Lazily lower the body —
|
||||
// a function reached ONLY via UFCS would otherwise be declared
|
||||
// but never emitted (issue 0063: undefined symbol at link).
|
||||
if (self.program_index.fn_ast_map.get(fa.field)) |_| {
|
||||
if (!self.lowered_functions.contains(fa.field)) {
|
||||
self.lazyLowerFunction(fa.field);
|
||||
//
|
||||
// fix-0102d site 3: a free-function UFCS target with a genuine
|
||||
// flat same-name collision must dispatch to the RESOLVED author
|
||||
// for the receiver's source, not the first-wins winner. The
|
||||
// field name is never scope-mangled, so the only gate is a
|
||||
// known source file; `.ambiguous` → loud diagnostic; `.none`
|
||||
// → existing first-wins path.
|
||||
const ufcs_fid: ?FuncId = blk_uf: {
|
||||
if (self.current_source_file) |caller_file| {
|
||||
switch (self.resolveBareCallee(fa.field, caller_file)) {
|
||||
.func => |resolved| break :blk_uf resolved.fid,
|
||||
.ambiguous => {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fa.field});
|
||||
return Ref.none;
|
||||
},
|
||||
.none => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
if (self.resolveFuncByName(fa.field)) |fid| {
|
||||
if (self.program_index.fn_ast_map.get(fa.field)) |_| {
|
||||
if (!self.lowered_functions.contains(fa.field)) {
|
||||
self.lazyLowerFunction(fa.field);
|
||||
}
|
||||
}
|
||||
break :blk_uf self.resolveFuncByName(fa.field);
|
||||
};
|
||||
if (ufcs_fid) |fid| {
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
const ret_ty = func.ret;
|
||||
const params = func.params;
|
||||
@@ -11611,6 +11943,24 @@ pub const Lowering = struct {
|
||||
}
|
||||
break :blk2 scoped;
|
||||
};
|
||||
// fix-0102d site 1: for a genuine flat same-name collision the
|
||||
// omitted trailing args must be filled from the RESOLVED
|
||||
// author's defaults, not the first-wins winner's. Only a plain
|
||||
// top-level identifier with no scope-mangle / UFCS alias /
|
||||
// local shadow routes here; `.ambiguous` declines to expand
|
||||
// (the call path emits the single diagnostic); `.none` keeps
|
||||
// the existing first-wins winner, byte-for-byte.
|
||||
if (std.mem.eql(u8, eff_name, id.name) and
|
||||
(if (self.scope) |scope| scope.lookup(id.name) == null else true))
|
||||
{
|
||||
if (self.current_source_file) |caller_file| {
|
||||
switch (self.resolveBareCallee(id.name, caller_file)) {
|
||||
.func => |resolved| break :blk resolved.decl,
|
||||
.ambiguous => return null,
|
||||
.none => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
break :blk self.program_index.fn_ast_map.get(eff_name) orelse return null;
|
||||
},
|
||||
// Namespace call `mod.fn(args)` — args map directly to params
|
||||
@@ -11833,6 +12183,29 @@ pub const Lowering = struct {
|
||||
break :blk scoped;
|
||||
};
|
||||
|
||||
// fix-0102c F2: a genuine flat same-name collision must type this
|
||||
// call's args against the RESOLVED author's params, not the first-wins
|
||||
// winner's. Mirror the `lowerCall` routing one layer earlier so arg
|
||||
// lowering (implicit address-of, coercion) matches the author actually
|
||||
// called — otherwise a `*T`-param shadow gets a `T` value arg that is
|
||||
// later bit-cast to a pointer (segfault). Only a plain top-level
|
||||
// identifier with no scope-mangle / UFCS alias / local shadow routes
|
||||
// here; `.ambiguous` / `.none` fall to the existing first-wins path so
|
||||
// single-author / local / std resolution is byte-for-byte unchanged.
|
||||
if (std.mem.eql(u8, name, bare_name) and
|
||||
(if (self.scope) |scope| scope.lookup(bare_name) == null else true))
|
||||
{
|
||||
if (self.current_source_file) |caller_file| {
|
||||
switch (self.resolveBareCallee(bare_name, caller_file)) {
|
||||
.func => |resolved| {
|
||||
const func = &self.module.functions.items[@intFromEnum(resolved.fid)];
|
||||
return self.userParamTypes(func);
|
||||
},
|
||||
.ambiguous, .none => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check declared functions
|
||||
if (self.resolveFuncByName(name)) |fid| {
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
@@ -11890,6 +12263,19 @@ pub const Lowering = struct {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// A plain free function: no type params (not generic) and an ordinary sx
|
||||
/// body (not `#foreign` / `#builtin` / `#compiler`). Only these get an
|
||||
/// out-of-line identity-addressable slot — the bare-call disambiguation
|
||||
/// (fix-0102c) and the shadow-author lowering pass leave every other shape
|
||||
/// to the existing name-keyed dispatch.
|
||||
fn isPlainFreeFn(fd: *const ast.FnDecl) bool {
|
||||
if (fd.type_params.len > 0) return false;
|
||||
return switch (fd.body.data) {
|
||||
.foreign_expr, .builtin_expr, .compiler_expr => false,
|
||||
else => true,
|
||||
};
|
||||
}
|
||||
|
||||
/// Pack-fn: has a trailing heterogeneous pack param (`is_variadic
|
||||
/// AND is_comptime`). Mixed shapes — non-pack comptime params
|
||||
/// before the pack — are also accepted; the mono folds those
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const std = @import("std");
|
||||
const ast = @import("../ast.zig");
|
||||
const imports = @import("../imports.zig");
|
||||
const types = @import("types.zig");
|
||||
const inst = @import("inst.zig");
|
||||
const errors = @import("../errors.zig");
|
||||
@@ -570,9 +571,10 @@ pub const GlobalInfo = struct { id: inst.GlobalId, ty: TypeId };
|
||||
/// `self.program_index.<field>`; later phases hand collaborator modules a
|
||||
/// `*ProgramIndex` instead of `*Lowering`.
|
||||
///
|
||||
/// OWNS the declaration maps below. BORROWS `module_scopes` / `import_graph`
|
||||
/// (pointers into maps owned by the compilation driver, `core.zig`) — those
|
||||
/// are read-only views and are never freed here.
|
||||
/// OWNS the declaration maps below. BORROWS `module_scopes` / `import_graph` /
|
||||
/// `flat_import_graph` / `module_fns` (pointers into maps owned by the
|
||||
/// compilation driver, `core.zig`) — those are read-only views and are never
|
||||
/// freed here.
|
||||
///
|
||||
/// Per-map allocators are preserved exactly as they were on `Lowering`:
|
||||
/// `import_flags` / `fn_ast_map` / `global_names` use the lowering allocator
|
||||
@@ -587,6 +589,16 @@ pub const ProgramIndex = struct {
|
||||
/// Module path → set of directly imported paths (param_impl visibility
|
||||
/// filter). Borrowed view.
|
||||
import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null,
|
||||
/// Module path → set of directly FLAT-imported paths — the subset of
|
||||
/// `import_graph` edges from a bare `#import` (never a namespaced
|
||||
/// `ns :: #import`). fix-0102c's bare-name disambiguation walks this to
|
||||
/// decide which same-name authors a flat importer can reach. Borrowed view.
|
||||
flat_import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null,
|
||||
/// Module path → (function name → authoring `*const FnDecl`), mirroring
|
||||
/// `module_scopes`. Retains every same-name author under its own path so
|
||||
/// fix-0102c can resolve a flat call to the right module's function.
|
||||
/// Borrowed view.
|
||||
module_fns: ?*imports.ModuleFns = null,
|
||||
|
||||
// ── Declaration maps ──
|
||||
/// Function name → AST decl.
|
||||
@@ -627,7 +639,8 @@ pub const ProgramIndex = struct {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ProgramIndex) void {
|
||||
// Owned maps only — module_scopes / import_graph are borrowed.
|
||||
// Owned maps only — module_scopes / import_graph / flat_import_graph /
|
||||
// module_fns are borrowed.
|
||||
self.import_flags.deinit();
|
||||
self.fn_ast_map.deinit();
|
||||
self.qualified_fn_source.deinit();
|
||||
|
||||
@@ -13,6 +13,7 @@ pub const trace_runtime_tests = @import("runtime_trace.test.zig");
|
||||
pub const sema = @import("sema.zig");
|
||||
pub const sema_tests = @import("sema.test.zig");
|
||||
pub const imports = @import("imports.zig");
|
||||
pub const imports_tests = @import("imports.test.zig");
|
||||
pub const core = @import("core.zig");
|
||||
pub const c_import = @import("c_import.zig");
|
||||
pub const ir = @import("ir/ir.zig");
|
||||
|
||||
Reference in New Issue
Block a user