diff --git a/src/cli/test/fx_platform_test.zig b/src/cli/test/fx_platform_test.zig index 006041d5ec..d274453b91 100644 --- a/src/cli/test/fx_platform_test.zig +++ b/src/cli/test/fx_platform_test.zig @@ -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); +} diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index eea0f505cf..6433f4b28d 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -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 = .{ diff --git a/test/fx/compare_fold_rev.roc b/test/fx/compare_fold_rev.roc new file mode 100644 index 0000000000..c719a20201 --- /dev/null +++ b/test/fx/compare_fold_rev.roc @@ -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()}") +} diff --git a/test/fx/fold_rev_qualified.roc b/test/fx/fold_rev_qualified.roc new file mode 100644 index 0000000000..5280c1a2d2 --- /dev/null +++ b/test/fx/fold_rev_qualified.roc @@ -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") +} diff --git a/test/fx/fold_rev_static_dispatch.roc b/test/fx/fold_rev_static_dispatch.roc new file mode 100644 index 0000000000..c578ee82f1 --- /dev/null +++ b/test/fx/fold_rev_static_dispatch.roc @@ -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") +} diff --git a/test/fx/test_bound_len.roc b/test/fx/test_bound_len.roc new file mode 100644 index 0000000000..4b991e5bda --- /dev/null +++ b/test/fx/test_bound_len.roc @@ -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()}") +} diff --git a/test/fx/test_first.roc b/test/fx/test_first.roc new file mode 100644 index 0000000000..d5bb558519 --- /dev/null +++ b/test/fx/test_first.roc @@ -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") + } +} diff --git a/test/fx/test_first_method.roc b/test/fx/test_first_method.roc new file mode 100644 index 0000000000..21068510d7 --- /dev/null +++ b/test/fx/test_first_method.roc @@ -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") + } +} diff --git a/test/fx/test_fold_method.roc b/test/fx/test_fold_method.roc new file mode 100644 index 0000000000..9ce457729f --- /dev/null +++ b/test/fx/test_fold_method.roc @@ -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()}") +} diff --git a/test/fx/test_fold_rev_debug.roc b/test/fx/test_fold_rev_debug.roc new file mode 100644 index 0000000000..6f158b12f8 --- /dev/null +++ b/test/fx/test_fold_rev_debug.roc @@ -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()}") +} diff --git a/test/fx/test_fold_rev_order.roc b/test/fx/test_fold_rev_order.roc new file mode 100644 index 0000000000..747fcb16ca --- /dev/null +++ b/test/fx/test_fold_rev_order.roc @@ -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()}") +} diff --git a/test/fx/test_fold_rev_simple.roc b/test/fx/test_fold_rev_simple.roc new file mode 100644 index 0000000000..6dc129b6a0 --- /dev/null +++ b/test/fx/test_fold_rev_simple.roc @@ -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()}") +} diff --git a/test/fx/test_get.roc b/test/fx/test_get.roc new file mode 100644 index 0000000000..9b5aea09f5 --- /dev/null +++ b/test/fx/test_get.roc @@ -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") + } +} diff --git a/test/fx/test_get_qualified.roc b/test/fx/test_get_qualified.roc new file mode 100644 index 0000000000..6c6ea3fd7e --- /dev/null +++ b/test/fx/test_get_qualified.roc @@ -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") + } +} diff --git a/test/fx/test_last_method.roc b/test/fx/test_last_method.roc new file mode 100644 index 0000000000..899885a725 --- /dev/null +++ b/test/fx/test_last_method.roc @@ -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") + } +} diff --git a/test/fx/test_len.roc b/test/fx/test_len.roc new file mode 100644 index 0000000000..0d5cfa6907 --- /dev/null +++ b/test/fx/test_len.roc @@ -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()}") +} diff --git a/test/fx/test_method_while.roc b/test/fx/test_method_while.roc new file mode 100644 index 0000000000..483446c876 --- /dev/null +++ b/test/fx/test_method_while.roc @@ -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") +} diff --git a/test/fx/test_simple_method.roc b/test/fx/test_simple_method.roc new file mode 100644 index 0000000000..2e556b5b33 --- /dev/null +++ b/test/fx/test_simple_method.roc @@ -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") +} diff --git a/test/fx/test_sum_method.roc b/test/fx/test_sum_method.roc new file mode 100644 index 0000000000..143d69704d --- /dev/null +++ b/test/fx/test_sum_method.roc @@ -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 +} diff --git a/test/fx/test_while_lookup.roc b/test/fx/test_while_lookup.roc new file mode 100644 index 0000000000..9f5bce39e1 --- /dev/null +++ b/test/fx/test_while_lookup.roc @@ -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()}") +} diff --git a/test/fx/test_wrap_fold.roc b/test/fx/test_wrap_fold.roc new file mode 100644 index 0000000000..50e588dbb6 --- /dev/null +++ b/test/fx/test_wrap_fold.roc @@ -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()}") +} diff --git a/test/fx/test_wrap_method.roc b/test/fx/test_wrap_method.roc new file mode 100644 index 0000000000..81db3fe0e7 --- /dev/null +++ b/test/fx/test_wrap_method.roc @@ -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()}") +}