Fix List.get with method syntax causing cycle in layout computation

When calling List.get with method syntax (my_list.get(0)), the
interpreter was causing a cycle in layout computation because rigid
type variables weren't being properly resolved.

The fix unifies the method's first parameter type with a copy of the
receiver type before instantiation. This properly resolves rigid type
variables (like `item` in List.get) to concrete types. A copy of the
receiver type is created before unification to avoid corrupting the
original type, since unification modifies both sides.

This is the same approach used for no-args method dispatch (like
List.first), but with the additional copy step needed because the
multi-args path may reuse types across multiple method invocations.

Fixes #8662

🤖 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 15:43:09 -05:00
parent 9f884cf80b
commit 79a08d5d0f
No known key found for this signature in database
3 changed files with 51 additions and 34 deletions

View file

@ -238,6 +238,11 @@ pub const io_spec_tests = [_]TestSpec{
.io_spec = "0<short|1>short|0<|1>",
.description = "Regression test: Stdin.line! in while loop with short input (small string optimization)",
},
.{
.roc_file = "test/fx/list_method_get.roc",
.io_spec = "1>is ok",
.description = "Regression test: List.get with method syntax (issue #8662)",
},
};
/// Get the total number of IO spec tests

View file

@ -15330,44 +15330,43 @@ pub const Interpreter = struct {
var saved_rigid_subst: ?std.AutoHashMap(types.Var, types.Var) = null;
var did_instantiate = false;
// Unify the method's first parameter with the receiver type to properly
// resolve rigid type variables (like `item` in List.get).
// This is the same approach used for no-args method dispatch.
// IMPORTANT: Create a copy of the receiver type before unification because
// unification modifies BOTH sides, which would corrupt the receiver's type.
const fn_args = switch (lambda_resolved.desc.content.structure) {
.fn_pure => |f| self.runtime_types.sliceVars(f.args),
.fn_effectful => |f| self.runtime_types.sliceVars(f.args),
.fn_unbound => |f| self.runtime_types.sliceVars(f.args),
else => &[_]types.Var{},
};
if (fn_args.len >= 1) {
// Create a copy of the receiver's type to avoid corrupting the original
const recv_resolved = self.runtime_types.resolveVar(dac.receiver_rt_var);
const recv_copy = try self.runtime_types.register(.{
.content = recv_resolved.desc.content,
.rank = recv_resolved.desc.rank,
.mark = types.Mark.none,
});
_ = unify.unifyWithConf(
self.env,
self.runtime_types,
&self.problems,
&self.snapshots,
&self.type_writer,
&self.unify_scratch,
&self.unify_scratch.occurs_scratch,
fn_args[0],
recv_copy,
unify.Conf{ .ctx = .anon, .constraint_origin_var = null },
) catch {};
}
if (should_instantiate_method) {
// Instantiate the method type (replaces rigid vars with fresh flex vars)
_ = try self.instantiateType(lambda_rt_var, &method_subst_map);
// Map the fresh flex vars to concrete types from the receiver.
const recv_type_resolved = self.runtime_types.resolveVar(dac.receiver_rt_var);
if (recv_type_resolved.desc.content == .structure and
recv_type_resolved.desc.content.structure == .nominal_type)
{
const receiver_nom = recv_type_resolved.desc.content.structure.nominal_type;
const receiver_args = self.runtime_types.sliceNominalArgs(receiver_nom);
const fn_args = switch (lambda_resolved.desc.content.structure) {
.fn_pure => |f| self.runtime_types.sliceVars(f.args),
.fn_effectful => |f| self.runtime_types.sliceVars(f.args),
.fn_unbound => |f| self.runtime_types.sliceVars(f.args),
else => &[_]types.Var{},
};
if (fn_args.len > 0) {
const first_param_resolved = self.runtime_types.resolveVar(fn_args[0]);
if (first_param_resolved.desc.content == .structure and
first_param_resolved.desc.content.structure == .nominal_type)
{
const param_nom = first_param_resolved.desc.content.structure.nominal_type;
const param_args = self.runtime_types.sliceNominalArgs(param_nom);
const min_args = @min(param_args.len, receiver_args.len);
for (0..min_args) |arg_idx| {
const param_arg_resolved = self.runtime_types.resolveVar(param_args[arg_idx]);
if (param_arg_resolved.desc.content == .rigid) {
try method_subst_map.put(param_arg_resolved.var_, receiver_args[arg_idx]);
}
}
}
}
}
// Save and update rigid_subst
saved_rigid_subst = try self.rigid_subst.clone();
var subst_iter = method_subst_map.iterator();

View file

@ -0,0 +1,13 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
my_list = [8]
foo = my_list.get(0)
if Try.is_ok(foo) {
Stdout.line!("is ok")
} else {
Stdout.line!("is err")
}
}