Merge branch 'flow/distribution/fix-0102d' into wt-fix-0102-base

This commit is contained in:
agra
2026-06-06 16:51:29 +03:00
38 changed files with 412 additions and 13 deletions

View 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
}

View 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(); }

View 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(); }

View 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
}

View 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(); }

View 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(); }

View 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
}

View 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(); }

View 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(); }

View 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
}

View 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; }

View 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; }

View 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
}

View 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; }

View 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; }

View 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
}

View File

@@ -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(); }

View File

@@ -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();
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
from_a binds a.cfg default (10): ok
from_b binds b.cfg default (20): ok

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -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

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,2 @@
from_a v.bump() binds a.bump (+1): ok
from_b v.bump() binds b.bump (+100): ok

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
a.sx #run binds a.compute (7): ok
b.sx #run binds b.compute (70): ok

View File

@@ -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());
| ^^^^^

View File

@@ -0,0 +1 @@
from_b_value = 2

View 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).

View File

@@ -1433,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) {
@@ -3242,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()) {
@@ -7272,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.
@@ -8075,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;
@@ -11866,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