From 8c885048497349397b8a8ce1586c33c48640444e Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 6 Jun 2026 15:07:51 +0300 Subject: [PATCH] fix(lower): resolved author drives call param target typing [0102c F2] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attempt-3 fix for the F2 review finding. After resolveBareCallee picks a shadowed same-name author at a normal call site, the call's PARAMETER TARGET TYPING still ran first-wins: resolveCallParamTypes' bare-identifier branch resolved param types via resolveFuncByName(name) / fn_ast_map.get(name) — both keyed by name, not by the resolved author. Because that runs in lowerCall BEFORE the resolveBareCallee routing, a shadow author whose parameter TYPE differs from the first-wins winner had its args lowered against the WINNER's signature (no implicit address-of for a *T param typed as T), then the correctly-resolved shadow FuncId was called with the mis-typed arg — a value bit-cast to a pointer → segfault. The bare-identifier branch now routes through the SAME resolveBareCallee resolver one layer earlier and takes the param target types from the RESOLVED author's lowered func.params (userParamTypes). Only the .func (single resolved author) outcome reroutes; .ambiguous keeps the existing loud call-site diagnostic and .none keeps the first-wins fallback, so single-author / local / std / qualified resolution is byte-for-byte unchanged. Method-call / namespace / foreign / generic branches of resolveCallParamTypes are untouched. The resolver is idempotent (bareAuthorFuncId guards body lowering via lowered_fids) so the extra call from param-type resolution is safe; lowerFunctionBodyInto already saves/restores all lowering state for mid-call reentry. Regression: examples/0728-modules-flat-same-name-paramtype — two flat file imports each author `apply` with a divergent param type (a.sx value `s64` winner, b.sx pointer `*s64` shadow). b.sx's from_b passes a value local to its pointer-param author via implicit address-of (×2 → 42); a.sx's from_a (own == winner) is unchanged (value + 1 → 11). Fails on the pre-fix typing (segfault at from_b); passes after. Gate (worktree): zig build, zig build test (400/400), bash tests/run_examples.sh (464 passed / 0 failed) all green. Matrix 0722-0727 unchanged. Guardrail: m3te builds via the worktree binary (sx build --target ios-sim, exit 0) — single- author / local resolution intact. Default-arg / closure / UFCS / comptime SITES remain first-wins (fix-0102d). --- .../0728-modules-flat-same-name-paramtype.sx | 22 ++++++++++++++++++ .../a.sx | 5 ++++ .../b.sx | 7 ++++++ ...0728-modules-flat-same-name-paramtype.exit | 1 + ...28-modules-flat-same-name-paramtype.stderr | 1 + ...28-modules-flat-same-name-paramtype.stdout | 2 ++ src/ir/lower.zig | 23 +++++++++++++++++++ 7 files changed, 61 insertions(+) create mode 100644 examples/0728-modules-flat-same-name-paramtype.sx create mode 100644 examples/0728-modules-flat-same-name-paramtype/a.sx create mode 100644 examples/0728-modules-flat-same-name-paramtype/b.sx create mode 100644 examples/expected/0728-modules-flat-same-name-paramtype.exit create mode 100644 examples/expected/0728-modules-flat-same-name-paramtype.stderr create mode 100644 examples/expected/0728-modules-flat-same-name-paramtype.stdout 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)];