fix(lower): resolved author drives call param target typing [0102c F2]

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).
This commit is contained in:
agra
2026-06-06 15:07:51 +03:00
parent 8f9c00dcdb
commit 8c88504849
7 changed files with 61 additions and 0 deletions

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

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

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

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
from_a binds a.apply (value param): ok
from_b binds b.apply (pointer param): ok

View File

@@ -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)];