mirror of
https://github.com/roc-lang/roc.git
synced 2025-12-23 08:48:03 +00:00
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:
parent
9f884cf80b
commit
a6b23224d0
4 changed files with 87 additions and 9 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
12
test/fx/issue8665.roc
Normal 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("")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue