diff --git a/examples/0728-modules-flat-same-name-paramtype.sx b/examples/0728-modules-flat-same-name-paramtype.sx new file mode 100644 index 0000000..2810bb9 --- /dev/null +++ b/examples/0728-modules-flat-same-name-paramtype.sx @@ -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 +} diff --git a/examples/0728-modules-flat-same-name-paramtype/a.sx b/examples/0728-modules-flat-same-name-paramtype/a.sx new file mode 100644 index 0000000..4b15eb4 --- /dev/null +++ b/examples/0728-modules-flat-same-name-paramtype/a.sx @@ -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); } diff --git a/examples/0728-modules-flat-same-name-paramtype/b.sx b/examples/0728-modules-flat-same-name-paramtype/b.sx new file mode 100644 index 0000000..ebaa0ec --- /dev/null +++ b/examples/0728-modules-flat-same-name-paramtype/b.sx @@ -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; } diff --git a/examples/expected/0728-modules-flat-same-name-paramtype.exit b/examples/expected/0728-modules-flat-same-name-paramtype.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0728-modules-flat-same-name-paramtype.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0728-modules-flat-same-name-paramtype.stderr b/examples/expected/0728-modules-flat-same-name-paramtype.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0728-modules-flat-same-name-paramtype.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0728-modules-flat-same-name-paramtype.stdout b/examples/expected/0728-modules-flat-same-name-paramtype.stdout new file mode 100644 index 0000000..7b8008c --- /dev/null +++ b/examples/expected/0728-modules-flat-same-name-paramtype.stdout @@ -0,0 +1,2 @@ +from_a binds a.apply (value param): ok +from_b binds b.apply (pointer param): ok diff --git a/src/ir/lower.zig b/src/ir/lower.zig index afc9bb1..7530a24 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -12080,6 +12080,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)];