Fix InvalidMethodReceiver crash on Try type method dispatch (#8665)

When calling methods on nominal types like Try using dot notation
(e.g., `List.get(list, 0).ok_or("fallback")`), the interpreter would
crash with InvalidMethodReceiver. The function call syntax worked
(`Try.ok_or(List.get(list, 0), "fallback")`).

Root cause: In the e_nominal handler, when evaluating a nominal type's
backing expression, we extracted the backing tag union type and passed
it as expected_rt_var. The resulting value's rt_var was then set to
the tag union instead of the nominal type, causing method dispatch
to fail when looking up methods defined on the nominal type.

Fix: Added a nominal_wrap continuation that wraps the backing expression's
result with the outer nominal type's rt_var after evaluation. This ensures
method dispatch can find methods defined on the nominal type.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Richard Feldman 2025-12-13 16:04:17 -05:00
parent 9f884cf80b
commit a6b23224d0
No known key found for this signature in database
4 changed files with 87 additions and 9 deletions

View file

@ -1162,3 +1162,18 @@ test "external platform memory alignment regression" {
try checkSuccess(run_result);
}
test "fx platform Try.ok_or static dispatch regression" {
// Regression test for issue #8665: InvalidMethodReceiver crash on static dispatch for Try type
// The traditional function call syntax works:
// _str1 = Try.ok_or(List.get(list, 0), "")
// But the method call syntax crashes:
// _str2 = List.get(list, 0).ok_or("")
const allocator = testing.allocator;
const run_result = try runRoc(allocator, "test/fx/issue8665.roc", .{});
defer allocator.free(run_result.stdout);
defer allocator.free(run_result.stderr);
try checkSuccess(run_result);
}

View file

@ -9330,6 +9330,10 @@ pub const Interpreter = struct {
/// Negate boolean result on value stack (for != operator).
negate_bool: void,
/// Wrap backing expression result with nominal type's rt_var.
/// This ensures method dispatch finds the nominal type info.
nominal_wrap: NominalWrap,
pub const DecrefValue = struct {
value: StackValue,
};
@ -9447,6 +9451,12 @@ pub const Interpreter = struct {
/// Return the value on the stack as an early return.
pub const EarlyReturn = struct {};
/// Wrap backing expression result with nominal type's rt_var.
pub const NominalWrap = struct {
/// The nominal type's rt_var to set on the result
nominal_rt_var: types.Var,
};
pub const TagCollect = struct {
/// Number of collected payload values on the value stack
collected_count: usize,
@ -10769,8 +10779,12 @@ pub const Interpreter = struct {
// Use expected_rt_var if available - this carries the correctly instantiated type
// from the call site (with concrete type args), avoiding re-translation from
// the builtins module which would have rigid type args.
const backing_rt_var = if (nom.nominal_type_decl == self.builtins.bool_stmt)
try self.getCanonicalBoolRuntimeVar()
//
// Also track the outer nominal rt_var so we can wrap the result with it.
// This is needed for method dispatch to find methods defined on the nominal type.
const BackingInfo = struct { backing: types.Var, nominal: ?types.Var };
const backing_info: BackingInfo = if (nom.nominal_type_decl == self.builtins.bool_stmt)
.{ .backing = try self.getCanonicalBoolRuntimeVar(), .nominal = null }
else if (expected_rt_var) |expected| blk: {
// Use the expected type's backing - but we need to set up rigid substitution
// because the backing may still have rigids that need to map to concrete type args
@ -10816,11 +10830,12 @@ pub const Interpreter = struct {
try self.rigid_subst.put(rigids.items[i], concrete_type);
}
}
break :blk backing;
// Return backing and preserve the nominal type for wrapping
break :blk BackingInfo{ .backing = backing, .nominal = expected };
},
else => break :blk expected,
else => break :blk BackingInfo{ .backing = expected, .nominal = null },
},
else => break :blk expected,
else => break :blk BackingInfo{ .backing = expected, .nominal = null },
}
} else blk: {
// Fall back to translating from current env
@ -10829,16 +10844,28 @@ pub const Interpreter = struct {
const nominal_resolved = self.runtime_types.resolveVar(nominal_rt_var);
break :blk switch (nominal_resolved.desc.content) {
.structure => |st| switch (st) {
.nominal_type => |nt| self.runtime_types.getNominalBackingVar(nt),
else => nominal_rt_var,
.nominal_type => |nt| BackingInfo{
.backing = self.runtime_types.getNominalBackingVar(nt),
.nominal = nominal_rt_var,
},
else => BackingInfo{ .backing = nominal_rt_var, .nominal = null },
},
else => nominal_rt_var,
else => BackingInfo{ .backing = nominal_rt_var, .nominal = null },
};
};
// If we extracted backing from a nominal, push continuation to wrap result
// with the nominal type's rt_var (for method dispatch to find nominal methods)
if (backing_info.nominal) |nominal_rt_var| {
try work_stack.push(.{ .apply_continuation = .{ .nominal_wrap = .{
.nominal_rt_var = nominal_rt_var,
} } });
}
// Schedule evaluation of the backing expression
try work_stack.push(.{ .eval_expr = .{
.expr_idx = nom.backing_expr,
.expected_rt_var = backing_rt_var,
.expected_rt_var = backing_info.backing,
} });
},
@ -16158,6 +16185,17 @@ pub const Interpreter = struct {
try value_stack.push(negated);
return true;
},
.nominal_wrap => |nw| {
// Wrap the backing expression result with the nominal type's rt_var.
// This ensures method dispatch can find methods defined on the nominal type.
var result = value_stack.pop() orelse {
self.triggerCrash("nominal_wrap: expected value on stack", false, roc_ops);
return error.Crash;
};
result.rt_var = nw.nominal_rt_var;
try value_stack.push(result);
return true;
},
}
}
};

View file

@ -1406,3 +1406,16 @@ test "List.len returns proper U64 nominal type for method calls - regression" {
\\}
, "3", .no_trace);
}
test "List.get method dispatch on Try type - issue 8665" {
// Regression test for issue #8665: InvalidMethodReceiver crash when calling
// ok_or() method on the result of List.get() using dot notation.
// The function call syntax works: Try.ok_or(List.get(list, 0), "fallback")
// But method syntax crashes: List.get(list, 0).ok_or("fallback")
try runExpectStr(
\\{
\\ list = ["hello"]
\\ List.get(list, 0).ok_or("fallback")
\\}
, "hello", .no_trace);
}

12
test/fx/issue8665.roc Normal file
View file

@ -0,0 +1,12 @@
app [main!] { pf: platform "./platform/main.roc" }
# Regression test for issue #8665: InvalidMethodReceiver crash on static dispatch for Try type
# The traditional function call syntax works:
# _str1 = Try.ok_or(List.get(list, 0), "")
# But the method call syntax crashes:
# _str2 = List.get(list, 0).ok_or("")
main! = || {
list = [""]
_str1 = Try.ok_or(List.get(list, 0), "")
_str2 = List.get(list, 0).ok_or("")
}