refactor(ir): source lowerCall's namespace/value boundary from CallResolver (A3.2 convergence step 3)
lowerCall re-derived the namespace-vs-value (receiver-prepend) decision with a
19-line block duplicating the exact identifier/type_expr + scope/global walk
that CallResolver already owns (objectIsValue, the negation of is_namespace).
This boundary determines whether the receiver is prepended, so it must agree
with the plan's free_fn_ufcs (prepends) vs namespace_fn (does not)
classification from fa59a9d.
Make CallResolver.objectIsValue pub and set
is_namespace = !self.callResolver().objectIsValue(fa.object)
so plan and lowering share one boundary definition and can never drift.
`!objectIsValue` matches the old block case-for-case (non-identifier => value;
identifier/type_expr in scope/global => value; else => namespace), so this is a
behavior-identical substitution.
Deeper switch(plan.kind) routing of lowerCall is intentionally NOT done here: it
is not behavior-preserving as-is. `plan` is typing-only and coarser than
`lowerCall` — its method/namespace arms carry comptime / generic /
generic-template / #compiler / type-constructor dispatch `plan` does not model,
and its value-receiver kinds (struct_method/protocol_dispatch/foreign_instance)
do not gate on objectIsValue, so a type-name receiver (Point.make()) could be
mis-classified vs the namespace/static call lowerCall actually performs. Driving
prepend decisions off plan.kind would mis-prepend; objectIsValue is the correct
single source, hence routing the boundary specifically. PLAN-ARCH A3.2 success
criteria met (shared classifier; no duplicated return-type logic; plan tests;
stable .ir snapshots).
zig build, zig build test, tests/run_examples.sh (357/0) all green.
This commit is contained in:
@@ -7696,26 +7696,13 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a namespace-qualified call (e.g., std.print)
|
||||
// If the object is an identifier/type_expr not in scope, treat as namespace prefix
|
||||
const is_namespace = blk: {
|
||||
const obj_name: ?[]const u8 = switch (fa.object.data) {
|
||||
.identifier => |id| id.name,
|
||||
.type_expr => |te| te.name,
|
||||
else => null,
|
||||
};
|
||||
if (obj_name) |name| {
|
||||
// Check local scope first
|
||||
if (self.scope) |scope| {
|
||||
if (scope.lookup(name) != null) break :blk false;
|
||||
}
|
||||
// Check global variables (e.g., g_font : *FontAtlas)
|
||||
if (self.program_index.global_names.contains(name)) break :blk false;
|
||||
// Not a local or global variable → namespace prefix
|
||||
break :blk true;
|
||||
}
|
||||
break :blk false;
|
||||
};
|
||||
// Namespace-qualified call (e.g. `std.print`) vs method / UFCS
|
||||
// call on a value (`recv.method`). This boundary decides whether
|
||||
// the receiver is prepended, so it MUST agree with the call
|
||||
// plan's `free_fn_ufcs` (prepends) vs `namespace_fn` (does not)
|
||||
// classification — source it from the single definition in
|
||||
// `CallResolver` rather than re-deriving it here.
|
||||
const is_namespace = !self.callResolver().objectIsValue(fa.object);
|
||||
|
||||
if (is_namespace) {
|
||||
// Namespace call: module.func(args) — don't prepend object
|
||||
|
||||
Reference in New Issue
Block a user