From 1007e2356174f504dc513833981fcb5d1f7c0939 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 2 Jun 2026 20:53:13 +0300 Subject: [PATCH] refactor(ir): source lowerCall's namespace/value boundary from CallResolver (A3.2 convergence step 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/ir/calls.zig | 4 +++- src/ir/lower.zig | 27 +++++++-------------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/ir/calls.zig b/src/ir/calls.zig index daa502f..5214d5c 100644 --- a/src/ir/calls.zig +++ b/src/ir/calls.zig @@ -413,7 +413,9 @@ pub const CallResolver = struct { /// (so `pkg.fn(...)` is a namespace call). This is exactly the negation of /// `lowerCall`'s `is_namespace`: a non-identifier object is always a value; /// an identifier / type_expr is a value iff it names a local or a global. - fn objectIsValue(self: CallResolver, obj: *const Node) bool { + /// `pub` so `lowerCall` sources its namespace/value boundary here rather + /// than re-deriving it — one definition, shared by typing and lowering. + pub fn objectIsValue(self: CallResolver, obj: *const Node) bool { const obj_name: []const u8 = switch (obj.data) { .identifier => |id| id.name, .type_expr => |te| te.name, diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 1455bb8..d795af5 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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