Add List.for_each!

This commit is contained in:
Richard Feldman 2025-12-01 11:25:47 -05:00
parent 99d4758d81
commit adfa93d77e
No known key found for this signature in database
15 changed files with 517 additions and 1 deletions

View file

@ -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

View file

@ -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;

View file

@ -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 => {},
}
}

View file

@ -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);
},
}

View file

@ -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,

View file

@ -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);

View file

@ -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));

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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);
},
}

View file

@ -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

View file

@ -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),

View file

@ -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 },

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

@ -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}")
})
}