From adfa93d77e988ce0ef5f3abce48dc98e86e7d098 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 1 Dec 2025 11:25:47 -0500 Subject: [PATCH] Add List.for_each! --- src/build/roc/Builtin.roc | 5 + src/canonicalize/Can.zig | 80 +++++++ src/canonicalize/DependencyGraph.zig | 5 + src/canonicalize/Expression.zig | 31 +++ src/canonicalize/Node.zig | 1 + src/canonicalize/NodeStore.zig | 15 +- src/canonicalize/test/node_store_test.zig | 7 + src/check/Check.zig | 31 +++ src/cli/test/fx_platform_test.zig | 26 +++ src/eval/interpreter.zig | 246 ++++++++++++++++++++++ src/parse/AST.zig | 24 +++ src/parse/Node.zig | 5 + src/parse/NodeStore.zig | 15 ++ src/parse/Parser.zig | 15 ++ test/fx/list_for_each.roc | 12 ++ 15 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 test/fx/list_for_each.roc diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index d634086b42..2eef24fecd 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -82,6 +82,11 @@ Builtin :: [].{ Try.Err(OutOfBounds) } + for_each! : List(item), (item => {}) => {} + for_each! = |items, cb!| for item in items { + cb!(item) + } + map : List(a), (a -> b) -> List(b) map = |list, transform| # Implement using fold + concat for now diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index d9efb51e9f..208c14dea4 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -5592,6 +5592,86 @@ pub fn canonicalizeExpr( .block => |e| { return try self.canonicalizeBlock(e); }, + .for_expr => |for_expr| { + // Tmp state to capture free vars from both expr & body + // + // This is stored as a map, so we can avoid adding duplicate captures + // if both the expr and the body reference the same var + var captures = std.AutoHashMapUnmanaged(Pattern.Idx, void){}; + defer captures.deinit(self.env.gpa); + + // Canonicalize the list expr + // for item in [1,2,3] { ... } + // ^^^^^^^ + const list_expr = blk: { + const body_free_vars_start = self.scratch_free_vars.top(); + defer self.scratch_free_vars.clearFrom(body_free_vars_start); + + const czerd_expr = try self.canonicalizeExprOrMalformed(for_expr.expr); + + // Copy free vars into scratch array + const free_vars_slice = self.scratch_free_vars.sliceFromSpan(czerd_expr.free_vars orelse DataSpan.empty()); + for (free_vars_slice) |fv| { + try captures.put(self.env.gpa, fv, {}); + } + + break :blk czerd_expr; + }; + + // Canonicalize the pattern + // for item in [1,2,3] { ... } + // ^^^^ + const ptrn = try self.canonicalizePatternOrMalformed(for_expr.patt); + + // Collect bound vars from pattern + var for_bound_vars = std.AutoHashMapUnmanaged(Pattern.Idx, void){}; + defer for_bound_vars.deinit(self.env.gpa); + try self.collectBoundVars(ptrn, &for_bound_vars); + + // Canonicalize the body + // for item in [1,2,3] { + // print!(item.toStr()) <<<< + // } + const body = blk: { + const body_free_vars_start = self.scratch_free_vars.top(); + defer self.scratch_free_vars.clearFrom(body_free_vars_start); + + const body_expr = try self.canonicalizeExprOrMalformed(for_expr.body); + + // Copy free vars into scratch array + const body_free_vars_slice = self.scratch_free_vars.sliceFromSpan(body_expr.free_vars orelse DataSpan.empty()); + for (body_free_vars_slice) |fv| { + if (!for_bound_vars.contains(fv)) { + try captures.put(self.env.gpa, fv, {}); + } + } + + break :blk body_expr; + }; + + // Get captures and copy to free_vars for parent + const free_vars_start = self.scratch_free_vars.top(); + var captures_iter = captures.keyIterator(); + while (captures_iter.next()) |capture| { + try self.scratch_free_vars.append(capture.*); + } + const free_vars = if (self.scratch_free_vars.top() > free_vars_start) + self.scratch_free_vars.spanFrom(free_vars_start) + else + null; + + // Create the for expression + const region = self.parse_ir.tokenizedRegionToRegion(for_expr.region); + const for_expr_idx = try self.env.addExpr(Expr{ + .e_for = .{ + .patt = ptrn, + .expr = list_expr.idx, + .body = body.idx, + }, + }, region); + + return CanonicalizedExpr{ .idx = for_expr_idx, .free_vars = free_vars }; + }, .malformed => |malformed| { // We won't touch this since it's already a parse error. _ = malformed; diff --git a/src/canonicalize/DependencyGraph.zig b/src/canonicalize/DependencyGraph.zig index 2f1ad7e737..17bec750f3 100644 --- a/src/canonicalize/DependencyGraph.zig +++ b/src/canonicalize/DependencyGraph.zig @@ -277,6 +277,11 @@ fn collectExprDependencies( try collectExprDependencies(cir, ret.expr, dependencies, allocator); }, + .e_for => |for_expr| { + try collectExprDependencies(cir, for_expr.expr, dependencies, allocator); + try collectExprDependencies(cir, for_expr.body, dependencies, allocator); + }, + .e_runtime_error => {}, } } diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index 8d7c517e8c..f332872a3e 100644 --- a/src/canonicalize/Expression.zig +++ b/src/canonicalize/Expression.zig @@ -393,6 +393,19 @@ pub const Expr = union(enum) { expr: Expr.Idx, }, + /// For expression that iterates over a list and executes a body for each element. + /// The for expression evaluates to the empty record `{}`. + /// This is the expression form of a for loop, allowing it to be used in expression contexts. + /// + /// ```roc + /// for_each! = |items, cb!| for item in items { cb!(item) } + /// ``` + e_for: struct { + patt: CIR.Pattern.Idx, + expr: Expr.Idx, + body: Expr.Idx, + }, + /// A hosted function that will be provided by the platform at runtime. /// This represents a lambda/function whose implementation is provided by the host application /// via the RocOps.hosted_fns array. @@ -1840,6 +1853,24 @@ pub const Expr = union(enum) { // Add inner expression try ir.store.getExpr(ret.expr).pushToSExprTree(ir, tree, ret.expr); + try tree.endNode(begin, attrs); + }, + .e_for => |for_expr| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("e-for"); + const region = ir.store.getExprRegion(expr_idx); + try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); + const attrs = tree.beginNode(); + + // Add pattern + try ir.store.getPattern(for_expr.patt).pushToSExprTree(ir, tree, for_expr.patt); + + // Add list expression + try ir.store.getExpr(for_expr.expr).pushToSExprTree(ir, tree, for_expr.expr); + + // Add body expression + try ir.store.getExpr(for_expr.body).pushToSExprTree(ir, tree, for_expr.body); + try tree.endNode(begin, attrs); }, } diff --git a/src/canonicalize/Node.zig b/src/canonicalize/Node.zig index f817c2b70b..8d1d72289d 100644 --- a/src/canonicalize/Node.zig +++ b/src/canonicalize/Node.zig @@ -84,6 +84,7 @@ pub const Tag = enum { expr_hosted_lambda, expr_low_level, expr_expect, + expr_for, expr_record_builder, expr_return, match_branch, diff --git a/src/canonicalize/NodeStore.zig b/src/canonicalize/NodeStore.zig index d57e9506f0..0e193b420c 100644 --- a/src/canonicalize/NodeStore.zig +++ b/src/canonicalize/NodeStore.zig @@ -144,7 +144,7 @@ pub fn relocate(store: *NodeStore, offset: isize) void { /// Count of the diagnostic nodes in the ModuleEnv pub const MODULEENV_DIAGNOSTIC_NODE_COUNT = 59; /// Count of the expression nodes in the ModuleEnv -pub const MODULEENV_EXPR_NODE_COUNT = 38; +pub const MODULEENV_EXPR_NODE_COUNT = 39; /// Count of the statement nodes in the ModuleEnv pub const MODULEENV_STATEMENT_NODE_COUNT = 16; /// Count of the type annotation nodes in the ModuleEnv @@ -716,6 +716,13 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { .body = @enumFromInt(node.data_1), } }; }, + .expr_for => { + return CIR.Expr{ .e_for = .{ + .patt = @enumFromInt(node.data_1), + .expr = @enumFromInt(node.data_2), + .body = @enumFromInt(node.data_3), + } }; + }, .expr_if_then_else => { const extra_start = node.data_1; const extra_data = store.extra_data.items.items[extra_start..]; @@ -1795,6 +1802,12 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) Allocator node.tag = .expr_expect; node.data_1 = @intFromEnum(e.body); }, + .e_for => |e| { + node.tag = .expr_for; + node.data_1 = @intFromEnum(e.patt); + node.data_2 = @intFromEnum(e.expr); + node.data_3 = @intFromEnum(e.body); + }, } const node_idx = try store.nodes.append(store.gpa, node); diff --git a/src/canonicalize/test/node_store_test.zig b/src/canonicalize/test/node_store_test.zig index 75b5c79a41..d77a54f77b 100644 --- a/src/canonicalize/test/node_store_test.zig +++ b/src/canonicalize/test/node_store_test.zig @@ -403,6 +403,13 @@ test "NodeStore round trip - Expressions" { .expr = rand_idx(CIR.Expr.Idx), }, }); + try expressions.append(gpa, CIR.Expr{ + .e_for = .{ + .patt = rand_idx(CIR.Pattern.Idx), + .expr = rand_idx(CIR.Expr.Idx), + .body = rand_idx(CIR.Expr.Idx), + }, + }); for (expressions.items, 0..) |expr, i| { const region = from_raw_offsets(@intCast(i * 100), @intCast(i * 100 + 50)); diff --git a/src/check/Check.zig b/src/check/Check.zig index cb508842d5..f7b16aa9d8 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -3583,6 +3583,32 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) does_fx = try self.checkExpr(expect.body, env, expected) or does_fx; try self.unifyWith(expr_var, .{ .structure = .empty_record }, env); }, + .e_for => |for_expr| { + // Check the pattern + try self.checkPattern(for_expr.patt, env, .no_expectation); + const for_ptrn_var: Var = ModuleEnv.varFrom(for_expr.patt); + + // Check the list expression + does_fx = try self.checkExpr(for_expr.expr, env, .no_expectation) or does_fx; + const for_expr_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(for_expr.expr)); + const for_expr_var: Var = ModuleEnv.varFrom(for_expr.expr); + + // Check that the expr is list of the ptrn + const list_content = try self.mkListContent(for_ptrn_var, env); + const list_var = try self.freshFromContent(list_content, env, for_expr_region); + _ = try self.unify(list_var, for_expr_var, env); + + // Check the body + does_fx = try self.checkExpr(for_expr.body, env, .no_expectation) or does_fx; + const for_body_var: Var = ModuleEnv.varFrom(for_expr.body); + + // Check that the for body evaluates to {} + const body_ret = try self.freshFromContent(.{ .structure = .empty_record }, env, for_expr_region); + _ = try self.unify(body_ret, for_body_var, env); + + // The for expression itself evaluates to {} + try self.unifyWith(expr_var, .{ .structure = .empty_record }, env); + }, .e_ellipsis => { try self.unifyWith(expr_var, .{ .flex = Flex.init() }, env); }, @@ -3968,6 +3994,11 @@ fn unifyEarlyReturns(self: *Self, expr_idx: CIR.Expr.Idx, return_var: Var, env: try self.unifyEarlyReturns(branch.value, return_var, env); } }, + .e_for => |for_expr| { + // Check the list expression and body for returns + try self.unifyEarlyReturns(for_expr.expr, return_var, env); + try self.unifyEarlyReturns(for_expr.body, return_var, env); + }, // Lambdas create a new scope for returns - don't recurse into them .e_lambda, .e_closure => {}, // All other expressions don't contain statements diff --git a/src/cli/test/fx_platform_test.zig b/src/cli/test/fx_platform_test.zig index 1fdf2acd18..7e5511367b 100644 --- a/src/cli/test/fx_platform_test.zig +++ b/src/cli/test/fx_platform_test.zig @@ -722,6 +722,32 @@ test "numeric fold" { try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Sum: 15") != null); } +test "List.for_each! with effectful callback" { + // Tests List.for_each! which iterates over a list and calls an effectful callback + const allocator = testing.allocator; + + try ensureRocBinary(allocator); + + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + "./zig-out/bin/roc", + "test/fx/list_for_each.roc", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + // Verify each item is printed + const has_apple = std.mem.indexOf(u8, run_result.stdout, "Item: apple") != null; + const has_banana = std.mem.indexOf(u8, run_result.stdout, "Item: banana") != null; + const has_cherry = std.mem.indexOf(u8, run_result.stdout, "Item: cherry") != null; + + try testing.expect(has_apple); + try testing.expect(has_banana); + try testing.expect(has_cherry); +} + test "string literal pattern matching" { // Tests pattern matching on string literals in match expressions. const allocator = testing.allocator; diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index 8c3071a0c0..17b382d665 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -7618,6 +7618,12 @@ pub const Interpreter = struct { /// For loop - process body result and continue to next iteration. for_loop_body_done: ForLoopBodyDone, + /// For expression - iterate over list elements after list is evaluated. + for_expr_iterate: ForExprIterate, + + /// For expression - process body result and continue to next iteration. + for_expr_body_done: ForExprBodyDone, + /// While loop - check condition and decide whether to continue. while_loop_check: WhileLoopCheck, @@ -7996,6 +8002,52 @@ pub const Interpreter = struct { loop_bindings_start: usize, }; + /// For expression - iterate over list elements (simpler than statement version) + pub const ForExprIterate = struct { + /// The list value being iterated (stored to access elements) + list_value: StackValue, + /// Current iteration index + current_index: usize, + /// Total number of elements in the list + list_len: usize, + /// Element size in bytes + elem_size: usize, + /// Element layout + elem_layout: layout.Layout, + /// Pattern to bind each element to + pattern: can.CIR.Pattern.Idx, + /// Pattern runtime type variable + patt_rt_var: types.Var, + /// Body expression to evaluate for each element + body: can.CIR.Expr.Idx, + /// Bindings length at for-expr start (for cleanup after completion) + bindings_start: usize, + }; + + /// For expression - cleanup after body evaluation (simpler than statement version) + pub const ForExprBodyDone = struct { + /// The list value being iterated + list_value: StackValue, + /// Current iteration index (just completed) + current_index: usize, + /// Total number of elements in the list + list_len: usize, + /// Element size in bytes + elem_size: usize, + /// Element layout + elem_layout: layout.Layout, + /// Pattern to bind each element to + pattern: can.CIR.Pattern.Idx, + /// Pattern runtime type variable + patt_rt_var: types.Var, + /// Body expression to evaluate for each element + body: can.CIR.Expr.Idx, + /// Bindings length at for-expr start (for cleanup after completion) + bindings_start: usize, + /// Bindings length at iteration start (for per-iteration cleanup) + loop_bindings_start: usize, + }; + /// While loop - check condition pub const WhileLoopCheck = struct { /// Condition expression @@ -8206,6 +8258,14 @@ pub const Interpreter = struct { // Decref the list value fl.list_value.decref(&self.runtime_layout_store, roc_ops); }, + .for_expr_iterate => |fl| { + // Decref the list value + fl.list_value.decref(&self.runtime_layout_store, roc_ops); + }, + .for_expr_body_done => |fl| { + // Decref the list value + fl.list_value.decref(&self.runtime_layout_store, roc_ops); + }, .sort_compare_result => |sc| { // Decref the list and compare function sc.list_value.decref(&self.runtime_layout_store, roc_ops); @@ -9033,6 +9093,39 @@ pub const Interpreter = struct { } }); }, + .e_for => |for_expr| { + // For expression: first evaluate the list, then set up iteration + const expr_ct_var = can.ModuleEnv.varFrom(for_expr.expr); + const expr_rt_var = try self.translateTypeVar(self.env, expr_ct_var); + + // Get the element type for binding + const patt_ct_var = can.ModuleEnv.varFrom(for_expr.patt); + const patt_rt_var = try self.translateTypeVar(self.env, patt_ct_var); + + // Push for_expr_iterate continuation (will be executed after list is evaluated) + try work_stack.push(.{ + .apply_continuation = .{ + .for_expr_iterate = .{ + .list_value = undefined, // Will be set when list is evaluated + .current_index = 0, + .list_len = 0, // Will be set when list is evaluated + .elem_size = 0, // Will be set when list is evaluated + .elem_layout = undefined, // Will be set when list is evaluated + .pattern = for_expr.patt, + .patt_rt_var = patt_rt_var, + .body = for_expr.body, + .bindings_start = self.bindings.items.len, + }, + }, + }); + + // Evaluate the list expression + try work_stack.push(.{ .eval_expr = .{ + .expr_idx = for_expr.expr, + .expected_rt_var = expr_rt_var, + } }); + }, + // ================================================================ // Function calls // ================================================================ @@ -12514,6 +12607,159 @@ pub const Interpreter = struct { } }); return true; }, + .for_expr_iterate => |fl_in| { + // For expression iteration: list has been evaluated, start iterating + const list_value = value_stack.pop() orelse { + self.triggerCrash("for_expr_iterate: value_stack empty", false, roc_ops); + return error.Crash; + }; + + // Get the list layout + if (list_value.layout.tag != .list) { + list_value.decref(&self.runtime_layout_store, roc_ops); + return error.TypeMismatch; + } + const elem_layout_idx = list_value.layout.data.list; + const elem_layout = self.runtime_layout_store.getLayout(elem_layout_idx); + const elem_size: usize = @intCast(self.runtime_layout_store.layoutSize(elem_layout)); + + // Get the RocList header + const list_header: *const RocList = @ptrCast(@alignCast(list_value.ptr.?)); + const list_len = list_header.len(); + + // Create the proper for_expr_iterate with list info filled in + var fl = fl_in; + fl.list_value = list_value; + fl.list_len = list_len; + fl.elem_size = elem_size; + fl.elem_layout = elem_layout; + + // If list is empty, push empty record result and we're done + if (list_len == 0) { + list_value.decref(&self.runtime_layout_store, roc_ops); + // Push empty record {} as result + const empty_record_layout_idx = try self.runtime_layout_store.ensureEmptyRecordLayout(); + const empty_record_layout = self.runtime_layout_store.getLayout(empty_record_layout_idx); + const empty_record_value = try self.pushRaw(empty_record_layout, 0); + try value_stack.push(empty_record_value); + return true; + } + + // Process first element + const elem_ptr = if (list_header.bytes) |buffer| + buffer + else { + list_value.decref(&self.runtime_layout_store, roc_ops); + return error.TypeMismatch; + }; + + var elem_value = StackValue{ + .ptr = elem_ptr, + .layout = elem_layout, + .is_initialized = true, + }; + elem_value.incref(&self.runtime_layout_store); + + // Bind the pattern + const loop_bindings_start = self.bindings.items.len; + if (!try self.patternMatchesBind(fl.pattern, elem_value, fl.patt_rt_var, roc_ops, &self.bindings, @enumFromInt(0))) { + elem_value.decref(&self.runtime_layout_store, roc_ops); + list_value.decref(&self.runtime_layout_store, roc_ops); + return error.TypeMismatch; + } + elem_value.decref(&self.runtime_layout_store, roc_ops); + + // Push body_done continuation + try work_stack.push(.{ .apply_continuation = .{ .for_expr_body_done = .{ + .list_value = fl.list_value, + .current_index = 0, + .list_len = fl.list_len, + .elem_size = fl.elem_size, + .elem_layout = fl.elem_layout, + .pattern = fl.pattern, + .patt_rt_var = fl.patt_rt_var, + .body = fl.body, + .bindings_start = fl.bindings_start, + .loop_bindings_start = loop_bindings_start, + } } }); + + // Evaluate body + try work_stack.push(.{ .eval_expr = .{ + .expr_idx = fl.body, + .expected_rt_var = null, + } }); + return true; + }, + .for_expr_body_done => |fl| { + // For expression body completed, clean up and continue to next iteration + const body_result = value_stack.pop() orelse { + self.triggerCrash("for_expr_body_done: value_stack empty", false, roc_ops); + return error.Crash; + }; + body_result.decref(&self.runtime_layout_store, roc_ops); + + // Clean up bindings for this iteration + self.trimBindingList(&self.bindings, fl.loop_bindings_start, roc_ops); + + // Move to next element + const next_index = fl.current_index + 1; + if (next_index >= fl.list_len) { + // Loop complete, decref list and push empty record result + fl.list_value.decref(&self.runtime_layout_store, roc_ops); + // Push empty record {} as result + const empty_record_layout_idx = try self.runtime_layout_store.ensureEmptyRecordLayout(); + const empty_record_layout = self.runtime_layout_store.getLayout(empty_record_layout_idx); + const empty_record_value = try self.pushRaw(empty_record_layout, 0); + try value_stack.push(empty_record_value); + return true; + } + + // Get next element + const list_header: *const RocList = @ptrCast(@alignCast(fl.list_value.ptr.?)); + const elem_ptr = if (list_header.bytes) |buffer| + buffer + next_index * fl.elem_size + else { + fl.list_value.decref(&self.runtime_layout_store, roc_ops); + return error.TypeMismatch; + }; + + var elem_value = StackValue{ + .ptr = elem_ptr, + .layout = fl.elem_layout, + .is_initialized = true, + }; + elem_value.incref(&self.runtime_layout_store); + + // Bind the pattern + const new_loop_bindings_start = self.bindings.items.len; + if (!try self.patternMatchesBind(fl.pattern, elem_value, fl.patt_rt_var, roc_ops, &self.bindings, @enumFromInt(0))) { + elem_value.decref(&self.runtime_layout_store, roc_ops); + fl.list_value.decref(&self.runtime_layout_store, roc_ops); + return error.TypeMismatch; + } + elem_value.decref(&self.runtime_layout_store, roc_ops); + + // Push body_done continuation for next iteration + try work_stack.push(.{ .apply_continuation = .{ .for_expr_body_done = .{ + .list_value = fl.list_value, + .current_index = next_index, + .list_len = fl.list_len, + .elem_size = fl.elem_size, + .elem_layout = fl.elem_layout, + .pattern = fl.pattern, + .patt_rt_var = fl.patt_rt_var, + .body = fl.body, + .bindings_start = fl.bindings_start, + .loop_bindings_start = new_loop_bindings_start, + } } }); + + // Evaluate body + try work_stack.push(.{ .eval_expr = .{ + .expr_idx = fl.body, + .expected_rt_var = null, + } }); + return true; + }, .while_loop_check => |wl| { // While loop: condition has been evaluated const cond_value = value_stack.pop() orelse return error.Crash; diff --git a/src/parse/AST.zig b/src/parse/AST.zig index a5a47248ae..1277e03c3c 100644 --- a/src/parse/AST.zig +++ b/src/parse/AST.zig @@ -2396,6 +2396,12 @@ pub const Expr = union(enum) { region: TokenizedRegion, }, block: Block, + for_expr: struct { + patt: Pattern.Idx, + expr: Expr.Idx, + body: Expr.Idx, + region: TokenizedRegion, + }, malformed: struct { reason: Diagnostic.Tag, region: TokenizedRegion, @@ -2444,6 +2450,7 @@ pub const Expr = union(enum) { .block => |e| e.region, .record_builder => |e| e.region, .ellipsis => |e| e.region, + .for_expr => |e| e.region, .malformed => |e| e.region, .string_part => |e| e.region, .single_quote => |e| e.region, @@ -2774,6 +2781,23 @@ pub const Expr = union(enum) { // Push child expression try ast.store.getExpr(a.expr).pushToSExprTree(gpa, env, ast, tree); + try tree.endNode(begin, attrs); + }, + .for_expr => |f| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("e-for"); + try ast.appendRegionInfoToSexprTree(env, tree, f.region); + const attrs = tree.beginNode(); + + // Push pattern + try ast.store.getPattern(f.patt).pushToSExprTree(gpa, env, ast, tree); + + // Push list expression + try ast.store.getExpr(f.expr).pushToSExprTree(gpa, env, ast, tree); + + // Push body expression + try ast.store.getExpr(f.body).pushToSExprTree(gpa, env, ast, tree); + try tree.endNode(begin, attrs); }, } diff --git a/src/parse/Node.zig b/src/parse/Node.zig index 58a5859b6a..519dfbe49f 100644 --- a/src/parse/Node.zig +++ b/src/parse/Node.zig @@ -434,6 +434,11 @@ pub const Tag = enum { /// * lhs - first statement node /// * rhs - number of statements block, + /// A for expression (for loop used as an expression, evaluates to {}) + /// * main_token - node index for pattern for loop variable + /// * lhs - node index for loop initializing expression + /// * rhs - node index for loop body expression + for_expr, /// DESCRIPTION /// Example: EXAMPLE /// * lhs - LHS DESCRIPTION diff --git a/src/parse/NodeStore.zig b/src/parse/NodeStore.zig index fecca4bbc3..314c85f868 100644 --- a/src/parse/NodeStore.zig +++ b/src/parse/NodeStore.zig @@ -739,6 +739,13 @@ pub fn addExpr(store: *NodeStore, expr: AST.Expr) std.mem.Allocator.Error!AST.Ex node.data.lhs = body.statements.span.start; node.data.rhs = body.statements.span.len; }, + .for_expr => |f| { + node.tag = .for_expr; + node.region = f.region; + node.main_token = @intFromEnum(f.patt); + node.data.lhs = @intFromEnum(f.expr); + node.data.rhs = @intFromEnum(f.body); + }, .ellipsis => |e| { node.tag = .ellipsis; node.region = e.region; @@ -1649,6 +1656,14 @@ pub fn getExpr(store: *const NodeStore, expr_idx: AST.Expr.Idx) AST.Expr { .region = node.region, } }; }, + .for_expr => { + return .{ .for_expr = .{ + .patt = @enumFromInt(node.main_token), + .expr = @enumFromInt(node.data.lhs), + .body = @enumFromInt(node.data.rhs), + .region = node.region, + } }; + }, .malformed => { return .{ .malformed = .{ .reason = @enumFromInt(node.data.lhs), diff --git a/src/parse/Parser.zig b/src/parse/Parser.zig index 605b665593..c57cac2b34 100644 --- a/src/parse/Parser.zig +++ b/src/parse/Parser.zig @@ -2153,6 +2153,21 @@ pub fn parseExprWithBp(self: *Parser, min_bp: u8) Error!AST.Expr.Idx { .expr = e, } }); }, + .KwFor => { + self.advance(); + const patt = try self.parsePattern(.alternatives_forbidden); + self.expect(.KwIn) catch { + return try self.pushMalformed(AST.Expr.Idx, .for_expected_in, self.pos); + }; + const list_expr = try self.parseExpr(); + const body = try self.parseExpr(); + expr = try self.store.addExpr(.{ .for_expr = .{ + .region = .{ .start = start, .end = self.pos }, + .patt = patt, + .expr = list_expr, + .body = body, + } }); + }, .TripleDot => { expr = try self.store.addExpr(.{ .ellipsis = .{ .region = .{ .start = start, .end = self.pos }, diff --git a/test/fx/list_for_each.roc b/test/fx/list_for_each.roc new file mode 100644 index 0000000000..f439cab483 --- /dev/null +++ b/test/fx/list_for_each.roc @@ -0,0 +1,12 @@ +app [main!] { pf: platform "./platform/main.roc" } + +import pf.Stdout + +# Tests List.for_each! with effectful callbacks + +main! = || { + items = ["apple", "banana", "cherry"] + List.for_each!(items, |item| { + Stdout.line!("Item: ${item}") + }) +}