Fix fold_rev static dispatch bug

This commit is contained in:
Richard Feldman 2025-12-10 12:57:42 -05:00
parent 762f9ed61d
commit 2f6fe8867c
No known key found for this signature in database
22 changed files with 404 additions and 15 deletions

View file

@ -1048,3 +1048,23 @@ test "fx platform index out of bounds in instantiate regression" {
// Currently it fails with a panic in instantiate.zig.
try checkSuccess(run_result);
}
test "fx platform fold_rev static dispatch regression" {
// Regression test: Calling fold_rev with static dispatch (method syntax) panics,
// but calling it qualified as List.fold_rev(...) works fine.
//
// The panic occurs with: [1].fold_rev([], |elem, acc| acc.append(elem))
// But this works: List.fold_rev([1], [], |elem, acc| acc.append(elem))
const allocator = testing.allocator;
const run_result = try runRoc(allocator, "test/fx/fold_rev_static_dispatch.roc", .{});
defer allocator.free(run_result.stdout);
defer allocator.free(run_result.stderr);
try checkSuccess(run_result);
// Verify the expected output
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Start reverse") != null);
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Reversed: 3 elements") != null);
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Done") != null);
}

View file

@ -15011,22 +15011,63 @@ pub const Interpreter = struct {
try self.active_closures.append(method_func);
// Bind receiver first
try self.bindings.append(.{
.pattern_idx = params[0],
.value = receiver_value,
.expr_idx = null, // expr_idx not used for method call parameter bindings
.source_env = self.env,
});
// Save the current flex_type_context before adding parameter mappings
// This will be restored in call_cleanup (like call_invoke_closure does)
var saved_flex_type_context = try self.flex_type_context.clone();
errdefer saved_flex_type_context.deinit();
// Bind explicit arguments
// Bind receiver using patternMatchesBind (like call_invoke_closure does)
// This creates a copy of the value for the binding
const receiver_param_rt_var = try self.translateTypeVar(self.env, can.ModuleEnv.varFrom(params[0]));
// Propagate flex mappings for receiver (needed for polymorphic type propagation)
const receiver_rt_resolved = self.runtime_types.resolveVar(dac.receiver_rt_var);
if (receiver_rt_resolved.desc.content == .structure) {
const receiver_param_ct_var = can.ModuleEnv.varFrom(params[0]);
try self.propagateFlexMappings(self.env, receiver_param_ct_var, dac.receiver_rt_var);
}
if (!try self.patternMatchesBind(params[0], receiver_value, receiver_param_rt_var, roc_ops, &self.bindings, null)) {
// Pattern match failed - cleanup and error
self.env = saved_env;
_ = self.active_closures.pop();
method_func.decref(&self.runtime_layout_store, roc_ops);
receiver_value.decref(&self.runtime_layout_store, roc_ops);
for (arg_values) |arg| arg.decref(&self.runtime_layout_store, roc_ops);
if (saved_rigid_subst) |*saved| saved.deinit();
self.flex_type_context.deinit();
self.flex_type_context = saved_flex_type_context;
self.poly_context_generation +%= 1;
return error.TypeMismatch;
}
// Decref the original receiver value since patternMatchesBind made a copy
receiver_value.decref(&self.runtime_layout_store, roc_ops);
// Bind explicit arguments using patternMatchesBind
for (arg_values, 0..) |arg, idx| {
try self.bindings.append(.{
.pattern_idx = params[1 + idx],
.value = arg,
.expr_idx = null, // expr_idx not used for method call parameter bindings
.source_env = self.env,
});
const param_rt_var = try self.translateTypeVar(self.env, can.ModuleEnv.varFrom(params[1 + idx]));
// Propagate flex mappings for each argument (needed for polymorphic type propagation)
const arg_rt_resolved = self.runtime_types.resolveVar(arg.rt_var);
if (arg_rt_resolved.desc.content == .structure) {
const param_ct_var = can.ModuleEnv.varFrom(params[1 + idx]);
try self.propagateFlexMappings(self.env, param_ct_var, arg.rt_var);
}
if (!try self.patternMatchesBind(params[1 + idx], arg, param_rt_var, roc_ops, &self.bindings, null)) {
// Pattern match failed - cleanup and error
self.env = saved_env;
_ = self.active_closures.pop();
method_func.decref(&self.runtime_layout_store, roc_ops);
for (arg_values[idx..]) |remaining_arg| remaining_arg.decref(&self.runtime_layout_store, roc_ops);
if (saved_rigid_subst) |*saved| saved.deinit();
self.flex_type_context.deinit();
self.flex_type_context = saved_flex_type_context;
self.poly_context_generation +%= 1;
return error.TypeMismatch;
}
// Decref the original argument value since patternMatchesBind made a copy
arg.decref(&self.runtime_layout_store, roc_ops);
}
try work_stack.push(.{ .apply_continuation = .{ .call_cleanup = .{
@ -15037,7 +15078,7 @@ pub const Interpreter = struct {
.did_instantiate = did_instantiate,
.call_ret_rt_var = null,
.saved_rigid_subst = saved_rigid_subst,
.saved_flex_type_context = null,
.saved_flex_type_context = saved_flex_type_context,
.arg_rt_vars_to_free = null,
} } });
try work_stack.push(.{ .eval_expr = .{

View file

@ -0,0 +1,13 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
Stdout.line!("Testing qualified fold_rev")
r1 = List.fold_rev([1], 0, |elem, acc| acc + elem)
Stdout.line!("Qualified result: ${r1.to_str()}")
Stdout.line!("Testing method fold_rev")
r2 = [1].fold_rev(0, |elem, acc| acc + elem)
Stdout.line!("Method result: ${r2.to_str()}")
}

View file

@ -0,0 +1,15 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
# Test that fold_rev works with qualified call
# This should work fine
main! = || {
Stdout.line!("Start reverse (qualified)")
rev = List.fold_rev([1, 2, 3], [], |elem, acc| {
acc.append(elem)
})
# rev should be [3, 2, 1]
Stdout.line!("Reversed: ${rev.len().to_str()} elements")
Stdout.line!("Done")
}

View file

@ -0,0 +1,16 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
# Test that fold_rev works with static dispatch (method syntax)
# Previously this panicked while List.fold_rev(...) worked fine
main! = || {
Stdout.line!("Start reverse")
rev =
[1, 2, 3].fold_rev([], |elem, acc| {
acc.append(elem)
})
# rev should be [3, 2, 1]
Stdout.line!("Reversed: ${rev.len().to_str()} elements")
Stdout.line!("Done")
}

View file

@ -0,0 +1,12 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
my_len = |list| list.len()
main! = || {
Stdout.line!("Testing bound list len")
list = [1, 2, 3]
r = my_len(list)
Stdout.line!("Result: ${r.to_str()}")
}

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

@ -0,0 +1,12 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
list = [1, 2, 3]
Stdout.line!("Qualified first")
match List.first(list) {
Ok(v) => Stdout.line!("Qualified first: ${v.to_str()}")
Err(_e) => Stdout.line!("Error")
}
}

View file

@ -0,0 +1,15 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
list = [1]
Stdout.line!("List created")
# Try first with method syntax
Stdout.line!("Calling method list.first()")
match list.first() {
Ok(v) => Stdout.line!("First: ${v.to_str()}")
Err(_e) => Stdout.line!("Empty")
}
}

View file

@ -0,0 +1,9 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
Stdout.line!("Testing fold (not fold_rev) method")
r = [1, 2, 3].fold(0, |acc, elem| acc + elem)
Stdout.line!("fold method result: ${r.to_str()}")
}

View file

@ -0,0 +1,18 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
list = [1]
Stdout.line!("List created")
# Try qualified call first (works)
Stdout.line!("Calling List.fold_rev (qualified)")
r1 = List.fold_rev(list, 0, |elem, acc| elem + acc)
Stdout.line!("Qualified result: ${r1.to_str()}")
# Now try method call (crashes)
Stdout.line!("Calling list.fold_rev (method)")
r2 = list.fold_rev(0, |elem, acc| elem + acc)
Stdout.line!("Method result: ${r2.to_str()}")
}

View file

@ -0,0 +1,13 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
list = [1]
Stdout.line!("List created")
# Method call FIRST this time
Stdout.line!("Calling method list.fold_rev")
r1 = list.fold_rev(0, |elem, acc| elem + acc)
Stdout.line!("Method result: ${r1.to_str()}")
}

View file

@ -0,0 +1,13 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
list = [1]
Stdout.line!("List created")
# Simpler fold_rev without append - just sum
Stdout.line!("Calling method fold_rev with sum")
r = list.fold_rev(0, |elem, acc| elem + acc)
Stdout.line!("Result: ${r.to_str()}")
}

18
test/fx/test_get.roc Normal file
View file

@ -0,0 +1,18 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
list = [1, 2, 3]
Stdout.line!("Method get")
match list.get(0) {
Ok(v) => Stdout.line!("Method get: ${v.to_str()}")
Err(_e) => Stdout.line!("Error")
}
Stdout.line!("Qualified get")
match List.get(list, 0) {
Ok(v) => Stdout.line!("Qualified get: ${v.to_str()}")
Err(_e) => Stdout.line!("Error")
}
}

View file

@ -0,0 +1,12 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
list = [1, 2, 3]
Stdout.line!("Qualified get")
match List.get(list, 0) {
Ok(v) => Stdout.line!("Qualified get: ${v.to_str()}")
Err(_e) => Stdout.line!("Error")
}
}

View file

@ -0,0 +1,15 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
list = [1]
Stdout.line!("List created")
# Try last with method syntax (also uses list_get_unsafe)
Stdout.line!("Calling method list.last()")
match list.last() {
Ok(v) => Stdout.line!("Last: ${v.to_str()}")
Err(_e) => Stdout.line!("Empty")
}
}

14
test/fx/test_len.roc Normal file
View file

@ -0,0 +1,14 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
list = [1, 2, 3]
Stdout.line!("Method len")
len = list.len()
Stdout.line!("Method len: ${len.to_str()}")
Stdout.line!("Qualified len")
len2 = List.len(list)
Stdout.line!("Qualified len: ${len2.to_str()}")
}

View file

@ -0,0 +1,25 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
# Simple function that uses while loop, called via method dispatch
my_while = |start| {
var $i = start
var $result = 0
while $i > 0 {
$i = $i - 1
$result = $result + 1
}
$result
}
main! = || {
Stdout.line!("Testing method while")
# Call via qualified syntax
r1 = my_while(3)
Stdout.line!("Qualified: ${r1.to_str()}")
# # Call via method syntax (shouldn't work - my_while not attached to a type)
# Actually can't call my_while via method syntax since it's not a method
Stdout.line!("Done")
}

View file

@ -0,0 +1,21 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
Stdout.line!("Testing simple methods")
list = [1, 2, 3]
# len works
Stdout.line!("len: ${list.len().to_str()}")
# fold works
r1 = list.fold(0, |acc, elem| acc + elem)
Stdout.line!("fold: ${r1.to_str()}")
# append works?
list2 = list.append(4)
Stdout.line!("append: ${list2.len().to_str()}")
Stdout.line!("Done")
}

View file

@ -0,0 +1,30 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
my_sum = |list| {
var $state = 0
var $index = list.len()
while $index > 0 {
$index = $index - 1
# Use list.get with method syntax
match list.get($index) {
Ok(elem) => { $state = $state + elem }
Err(_e) => {}
}
}
$state
}
main! = || {
Stdout.line!("Testing while loop with method lookup")
list = [1, 2, 3]
Stdout.line!("Using user-defined my_sum")
r1 = my_sum(list)
Stdout.line!("User my_sum: ${r1.to_str()}")
Stdout.line!("Using my_sum as method")
# This is different - it doesn't go through method dispatch
# because my_sum is in the app module, not List module
}

View file

@ -0,0 +1,24 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
my_sum = |list| {
var $state = 0
var $index = list.len()
while $index > 0 {
$index = $index - 1
# Use List.get instead of list_get_unsafe (which we can't access)
match List.get(list, $index) {
Ok(elem) => { $state = $state + elem }
Err(_e) => {}
}
}
$state
}
main! = || {
Stdout.line!("Testing while loop lookup")
list = [1, 2, 3]
r = my_sum(list)
Stdout.line!("Sum: ${r.to_str()}")
}

View file

@ -0,0 +1,16 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
# Wrap fold_rev to test if the issue is with method dispatch
my_fold_rev = |list, init, step| List.fold_rev(list, init, step)
main! = || {
list = [1]
Stdout.line!("List created")
# Call our wrapper (which uses qualified internally)
Stdout.line!("Calling my_fold_rev")
r = my_fold_rev(list, 0, |elem, acc| elem + acc)
Stdout.line!("Result: ${r.to_str()}")
}

View file

@ -0,0 +1,17 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
# Wrap fold_rev - but can't call this via method syntax since it's in app module
# Let's test if method dispatch on a simple wrapper works
my_wrapper = |list| List.fold_rev(list, 0, |elem, acc| elem + acc)
main! = || {
list = [1]
Stdout.line!("List created")
# Call wrapper directly
Stdout.line!("Calling my_wrapper(list)")
r = my_wrapper(list)
Stdout.line!("Result: ${r.to_str()}")
}