plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


4ba0db3Peter John 2026-07-20T22:22:15+05:30
feat(plum-wasm-codegen): compile closure literals and closure calls via a function table
plum-core/src/parser.rs CHANGED
@@ -519,16 +519,24 @@ impl<'a> AstParser<'a> {
519
519
  }
520
520
 
521
521
  fn parse_closure(&self, node: Node) -> Closure {
522
- // closure: "|" var_identifier,* "|" body
522
+ // closure: "|" var_identifier,* "|" (expression | body)
523
523
  let params: Vec<String> = self.children_of_kind(node, "var_identifier")
524
524
  .into_iter()
525
525
  .map(|n| self.text(n))
526
526
  .collect();
527
+ // The body is either an indented `body` block or a single inline expression
528
+ // (`|v| v`); normalize the inline form into a one-statement block so codegen and
529
+ // the checker only ever see a `Block`.
527
- let body = self.children_of_kind(node, "body")
530
+ let body = match self.children_of_kind(node, "body").into_iter().next() {
528
- .into_iter()
529
- .next()
530
- .map(|n| self.parse_block(n))
531
+ Some(block_node) => self.parse_block(block_node),
532
+ None => match node.child_by_field_name("body") {
533
+ Some(expr_node) => {
534
+ let unwrapped = self.unwrap_expr_node(expr_node);
535
+ Block { stmts: vec![Stmt::Expr(self.parse_expression(unwrapped))] }
536
+ }
531
- .unwrap_or(Block { stmts: vec![] });
537
+ None => Block { stmts: vec![] },
538
+ },
539
+ };
532
540
  Closure { params, body }
533
541
  }
534
542
 
plum-wasm-codegen/src/lib.rs CHANGED
@@ -214,8 +214,34 @@ pub struct FuncSig {
214
214
  pub ret: Option<ValType>,
215
215
  }
216
216
 
217
+ /// Everything codegen needs to know about one closure *literal* found in the program.
218
+ /// wasm has no native closures: each literal `|v| body` becomes its own real wasm
219
+ /// function (registered in the funcref table), and a closure *value* is a single i32
220
+ /// pointer to a heap pair `{table_index: i32 @0, env_pointer: i32 @8}`. The env is a
221
+ /// separate heap allocation: one 8-byte slot per captured (free) variable, in
222
+ /// `free_vars` order.
223
+ pub struct ClosureInfo {
224
+ /// Reserved wasm function index for this closure's compiled body.
225
+ pub func_idx: u32,
226
+ /// Index of `func_idx` in the funcref table (the `i32` stored at struct offset 0).
227
+ pub table_idx: u32,
228
+ /// Closure param val types (NOT including the implicit leading env pointer).
229
+ pub param_vts: Vec<ValType>,
230
+ /// Closure param plum types (for the closure body's own type env).
231
+ pub param_ptypes: Vec<PlumType>,
232
+ /// Closure return val type (`None` for a `Unit`-returning closure).
233
+ pub ret_vt: Option<ValType>,
234
+ /// Free variables captured by value, in a stable (first-appearance) order; the
235
+ /// index into this vec IS the variable's env-struct slot (offset = idx * 8).
236
+ pub free_vars: Vec<(String, PlumType)>,
237
+ }
238
+
239
+ /// Key for deduplicating `call_indirect` function-type indices: the full wasm
240
+ /// signature (leading env-ptr param included) of a closure.
241
+ type ClosureSigKey = (Vec<ValType>, Option<ValType>);
242
+
217
243
  /// Global, read-only lookup tables shared by every function body being compiled.
218
- pub struct CompileCtx {
244
+ pub struct CompileCtx<'a> {
219
245
  pub func_ids: HashMap<String, u32>,
220
246
  pub func_sigs: HashMap<String, FuncSig>,
221
247
  pub classes: ClassEnv,
@@ -223,6 +249,13 @@ pub struct CompileCtx {
223
249
  pub enum_variants: EnumVariants,
224
250
  pub global_env: TypeEnv,
225
251
  pub bump_global: u32,
252
+ /// Closure literal (keyed by `&Expr::Closure` pointer identity) -> its `ClosureInfo`.
253
+ pub closures: HashMap<usize, ClosureInfo>,
254
+ /// The AST of each discovered closure literal, keyed the same way, so its body can
255
+ /// be compiled in a second pass after all closures are registered.
256
+ pub closure_asts: HashMap<usize, &'a ast::Closure>,
257
+ /// Closure wasm signature -> function-type index, for `call_indirect` at call sites.
258
+ pub closure_call_types: HashMap<ClosureSigKey, u32>,
226
259
  }
227
260
 
228
261
  /// Per-module state that accumulates as function bodies are compiled: the running
@@ -242,8 +275,15 @@ struct LocalCtx<'a> {
242
275
  match_scratch_base: u32,
243
276
  /// `Match` stmt identity (pointer address) -> scratch slot offset.
244
277
  match_scratch_index: HashMap<usize, u32>,
278
+ /// First local index reserved for closure-construction scratch temporaries; each
279
+ /// closure literal uses two consecutive slots (env struct ptr, closure struct ptr).
280
+ closure_scratch_base: u32,
281
+ /// `Closure` expr identity (pointer address) -> offset of its first (of two) slots.
282
+ closure_scratch: HashMap<usize, u32>,
245
283
  func_ids: &'a HashMap<String, u32>,
246
284
  func_sigs: &'a HashMap<String, FuncSig>,
285
+ closures: &'a HashMap<usize, ClosureInfo>,
286
+ closure_call_types: &'a HashMap<ClosureSigKey, u32>,
247
287
  classes: &'a ClassEnv,
248
288
  methods: &'a MethodEnv,
249
289
  enum_variants: &'a EnumVariants,
@@ -286,9 +326,24 @@ fn plum_type_to_valtype(t: &PlumType) -> ValType {
286
326
  match t {
287
327
  PlumType::TInt => ValType::I64,
288
328
  PlumType::TFloat => ValType::F64,
329
+ // A closure value is a single i32 pointer to its heap-allocated
330
+ // `{table_index, env_pointer}` pair, so `TFun` is an i32 like every other
331
+ // heap reference (`TBool`/`TStr`/`TNamed`).
289
- PlumType::TBool | PlumType::TStr | PlumType::TNamed(_) => ValType::I32,
332
+ PlumType::TBool | PlumType::TStr | PlumType::TNamed(_) | PlumType::TFun(_, _) => ValType::I32,
290
- PlumType::TVar(_) | PlumType::TUnit | PlumType::TFun(_, _) => ValType::I64,
333
+ PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
334
+ }
335
+ }
336
+
337
+ /// Maps an `ast::ParamType::Fn(params, ret)` to the wasm signature of the *closure
338
+ /// function* it compiles to: an implicit leading `env_ptr: I32`, then one param per
339
+ /// declared param type, returning `ret`'s val type (or nothing for `Unit`).
340
+ fn fn_param_type_to_wasm_sig(params: &[ast::Type], ret: &Option<Box<ast::Type>>) -> (Vec<ValType>, Option<ValType>) {
341
+ let mut vts = vec![ValType::I32]; // env pointer
342
+ for p in params {
343
+ vts.push(ast_type_to_wasm(&p.name).unwrap_or(ValType::I32));
291
344
  }
345
+ let ret_vt = ret.as_ref().and_then(|t| ast_type_to_wasm(&t.name));
346
+ (vts, ret_vt)
292
347
  }
293
348
 
294
349
  fn block_type_for(result_vt: Option<ValType>) -> BlockType {
@@ -341,6 +396,9 @@ pub fn compile_source(source: &ast::Source) -> Result<Vec<u8>, String> {
341
396
 
342
397
  let mut func_ids: HashMap<String, u32> = HashMap::new();
343
398
  let mut func_sigs: HashMap<String, FuncSig> = HashMap::new();
399
+ // Closure wasm signature -> function-type index, deduped so every closure/call site
400
+ // of the same shape shares one `call_indirect` type.
401
+ let mut closure_call_types: HashMap<ClosureSigKey, u32> = HashMap::new();
344
402
 
345
403
  // Register every function AND method signature up front (methods get an implicit
346
404
  // leading `self: pointer` param and are keyed as "Receiver::method").
@@ -360,16 +418,89 @@ pub fn compile_source(source: &ast::Source) -> Result<Vec<u8>, String> {
360
418
  let key = fn_key(f);
361
419
  func_ids.insert(key.clone(), func_idx);
362
420
  func_sigs.insert(key, FuncSig { params: param_types, ret });
421
+
422
+ // Any `fn(...) -> ...`-typed param is callable via `call_indirect`; register
423
+ // its wasm signature (leading env-ptr param included) so call sites can
424
+ // resolve a consistent type index even if no matching closure literal exists.
425
+ for p in &f.params {
426
+ if let ast::ParamType::Fn(params, ret) = &p.ty {
427
+ let (sig_params, ret_vt) = fn_param_type_to_wasm_sig(params, ret);
428
+ let sig_key: ClosureSigKey = (sig_params.clone(), ret_vt);
429
+ if !closure_call_types.contains_key(&sig_key) {
430
+ let results: Vec<ValType> = ret_vt.into_iter().collect();
431
+ let tidx = module.add_type(&sig_params, &results);
432
+ closure_call_types.insert(sig_key, tidx);
433
+ }
434
+ }
435
+ }
363
436
  }
364
437
  }
365
438
 
366
- let ctx = CompileCtx { func_ids, func_sigs, classes, methods, enum_variants, global_env, bump_global };
367
-
368
439
  let fns: Vec<&ast::Fn> = source.items.iter().filter_map(|item| match item {
369
440
  ast::Item::Fn(f) => Some(f),
370
441
  _ => None,
371
442
  }).collect();
372
443
 
444
+ // ---- Discovery pre-pass: find every closure literal in every function body. ----
445
+ // (Runs on the monomorphized source, so any generic types in a closure's context
446
+ // are already concrete.) Registers each closure as its own wasm function + table
447
+ // element and records the free variables it must capture.
448
+ let fn_decls: HashMap<String, &ast::Fn> = fns.iter().map(|f| (f.name.clone(), *f)).collect();
449
+ let mut raw_closures: Vec<RawClosure> = Vec::new();
450
+ for f in &fns {
451
+ let mut env = global_env.clone();
452
+ if let Some(recv) = &f.type_param {
453
+ env.insert("self".to_string(), TypeScheme::mono(PlumType::TNamed(recv.clone())));
454
+ }
455
+ for p in &f.params {
456
+ env.insert(p.name.clone(), TypeScheme::mono(param_plum_type(&p.ty)));
457
+ }
458
+ let mut walker = ClosureWalker {
459
+ env,
460
+ cctx: check_ctx_of(&classes, &methods, &enum_variants),
461
+ fn_decls: &fn_decls,
462
+ found: Vec::new(),
463
+ };
464
+ match &f.body {
465
+ ast::FnBody::Block(block) => walker.walk_block(block),
466
+ ast::FnBody::Expr(e) => walker.walk_expr(e, None),
467
+ }
468
+ raw_closures.extend(walker.found);
469
+ }
470
+
471
+ let mut closures: HashMap<usize, ClosureInfo> = HashMap::new();
472
+ let mut closure_asts: HashMap<usize, &ast::Closure> = HashMap::new();
473
+ for rc in &raw_closures {
474
+ let mut sig_params = vec![ValType::I32]; // env pointer
475
+ sig_params.extend(rc.param_vts.iter().copied());
476
+ let sig_key: ClosureSigKey = (sig_params.clone(), rc.ret_vt);
477
+ let type_idx = match closure_call_types.get(&sig_key) {
478
+ Some(t) => *t,
479
+ None => {
480
+ let results: Vec<ValType> = rc.ret_vt.into_iter().collect();
481
+ let t = module.add_type(&sig_params, &results);
482
+ closure_call_types.insert(sig_key, t);
483
+ t
484
+ }
485
+ };
486
+ let func_idx = module.add_function(type_idx, &[]);
487
+ let table_idx = module.add_table_element(func_idx);
488
+ closures.insert(rc.ptr, ClosureInfo {
489
+ func_idx,
490
+ table_idx,
491
+ param_vts: rc.param_vts.clone(),
492
+ param_ptypes: rc.param_ptypes.clone(),
493
+ ret_vt: rc.ret_vt,
494
+ free_vars: rc.free_vars.clone(),
495
+ });
496
+ closure_asts.insert(rc.ptr, rc.closure);
497
+ }
498
+
499
+ let ctx = CompileCtx {
500
+ func_ids, func_sigs, classes, methods, enum_variants, global_env, bump_global,
501
+ closures, closure_asts, closure_call_types,
502
+ };
503
+
373
504
  let mut state = ModuleState { next_string_offset: STRING_BASE, data_segments: Vec::new() };
374
505
 
375
506
  let mut compiled_bodies: Vec<(String, Vec<u8>)> = Vec::new();
@@ -385,6 +516,18 @@ pub fn compile_source(source: &ast::Source) -> Result<Vec<u8>, String> {
385
516
  module.functions[slot].1 = body.clone();
386
517
  }
387
518
 
519
+ // Compile each closure literal's own body into its reserved function slot.
520
+ let mut closure_bodies: Vec<(u32, Vec<u8>)> = Vec::new();
521
+ for (ptr, info) in &ctx.closures {
522
+ let cl = ctx.closure_asts.get(ptr).expect("every registered closure has its AST recorded");
523
+ let body = compile_closure_body(cl, info, &ctx, &mut state)?;
524
+ closure_bodies.push((info.func_idx, body));
525
+ }
526
+ for (func_idx, body) in &closure_bodies {
527
+ let slot = (*func_idx - module.func_import_count) as usize;
528
+ module.functions[slot].1 = body.clone();
529
+ }
530
+
388
531
  for (offset, bytes) in &state.data_segments {
389
532
  module.add_data_segment(*offset, bytes);
390
533
  }
@@ -396,6 +539,352 @@ pub fn compile_source(source: &ast::Source) -> Result<Vec<u8>, String> {
396
539
  Ok(module.finish())
397
540
  }
398
541
 
542
+ /// The `PlumType` of a declared parameter, including `fn(...)`-typed params as `TFun`.
543
+ fn param_plum_type(pt: &ast::ParamType) -> PlumType {
544
+ match pt {
545
+ ast::ParamType::Type(t) => plum_checker::plum_type_from_ast(t),
546
+ ast::ParamType::Variadic(t) => plum_checker::plum_type_from_ast(t),
547
+ ast::ParamType::Fn(params, ret) => {
548
+ let param_types = params.iter().map(plum_checker::plum_type_from_ast).collect();
549
+ let ret_ty = ret.as_ref().map(|r| plum_checker::plum_type_from_ast(r)).unwrap_or(PlumType::TUnit);
550
+ PlumType::TFun(param_types, Box::new(ret_ty))
551
+ }
552
+ }
553
+ }
554
+
555
+ /// One closure literal discovered by the pre-pass, before it is registered as a wasm
556
+ /// function. `ptr` is the closure's `&ast::Closure` pointer identity (its stable key).
557
+ struct RawClosure<'a> {
558
+ ptr: usize,
559
+ closure: &'a ast::Closure,
560
+ param_vts: Vec<ValType>,
561
+ param_ptypes: Vec<PlumType>,
562
+ ret_vt: Option<ValType>,
563
+ free_vars: Vec<(String, PlumType)>,
564
+ }
565
+
566
+ /// Walks a function body (maintaining an evolving type env, exactly like `Collector`)
567
+ /// to find every closure literal and determine its concrete signature and captured
568
+ /// free variables. A closure passed directly as a `fn(...)`-typed call argument takes
569
+ /// its signature from that declared param type; any other closure (e.g. one assigned to
570
+ /// a local) falls back to the checker's inference of the closure expression itself.
571
+ struct ClosureWalker<'a, 'c> {
572
+ env: TypeEnv,
573
+ cctx: plum_checker::CheckCtx<'c>,
574
+ fn_decls: &'a HashMap<String, &'a ast::Fn>,
575
+ found: Vec<RawClosure<'a>>,
576
+ }
577
+
578
+ impl<'a, 'c> ClosureWalker<'a, 'c> {
579
+ fn walk_block(&mut self, block: &'a ast::Block) {
580
+ for s in &block.stmts {
581
+ self.walk_stmt(s);
582
+ }
583
+ }
584
+
585
+ fn walk_stmt(&mut self, stmt: &'a ast::Stmt) {
586
+ match stmt {
587
+ ast::Stmt::Assign(a) => {
588
+ for (target, value) in a.targets.iter().zip(a.values.iter()) {
589
+ self.walk_expr(value, None);
590
+ let ty = plum_checker::infer_expr(value, &self.env, &self.cctx).unwrap_or(PlumType::TInt);
591
+ self.env.insert(target.clone(), TypeScheme::mono(ty));
592
+ }
593
+ }
594
+ ast::Stmt::Return(Some(e)) => self.walk_expr(e, None),
595
+ ast::Stmt::Return(None) => {}
596
+ ast::Stmt::If(i) => {
597
+ self.walk_expr(&i.condition, None);
598
+ self.walk_block(&i.body);
599
+ for ei in &i.else_ifs {
600
+ self.walk_expr(&ei.condition, None);
601
+ self.walk_block(&ei.body);
602
+ }
603
+ if let Some(e) = &i.else_ {
604
+ self.walk_block(e);
605
+ }
606
+ }
607
+ ast::Stmt::While(w) => {
608
+ self.walk_expr(&w.condition, None);
609
+ self.walk_block(&w.body);
610
+ }
611
+ ast::Stmt::For(f) => {
612
+ self.walk_expr(&f.iter, None);
613
+ for v in &f.vars {
614
+ self.env.insert(v.clone(), TypeScheme::mono(PlumType::TInt));
615
+ }
616
+ self.walk_block(&f.body);
617
+ }
618
+ ast::Stmt::Expr(e) => self.walk_expr(e, None),
619
+ ast::Stmt::Assert(e) => self.walk_expr(e, None),
620
+ ast::Stmt::Match(m) => {
621
+ for s in &m.subjects {
622
+ self.walk_expr(s, None);
623
+ }
624
+ for case in &m.cases {
625
+ self.walk_block(&case.body);
626
+ }
627
+ }
628
+ ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
629
+ }
630
+ }
631
+
632
+ /// `expected_fn` carries the declared `fn(params) -> ret` type when this expression is
633
+ /// a call argument in a `fn`-typed parameter position, giving a closure literal its
634
+ /// concrete signature.
635
+ fn walk_expr(&mut self, expr: &'a ast::Expr, expected_fn: Option<&'a ast::ParamType>) {
636
+ match expr {
637
+ ast::Expr::Closure(cl) => self.record_closure(cl, expected_fn),
638
+ ast::Expr::Binary(b) => { self.walk_expr(&b.left, None); self.walk_expr(&b.right, None); }
639
+ ast::Expr::Bool(b) => { self.walk_expr(&b.left, None); self.walk_expr(&b.right, None); }
640
+ ast::Expr::Compare(c) => { self.walk_expr(&c.left, None); self.walk_expr(&c.right, None); }
641
+ ast::Expr::Not(inner) => self.walk_expr(inner, None),
642
+ ast::Expr::Unary(u) => self.walk_expr(&u.operand, None),
643
+ ast::Expr::Paren(inner) => self.walk_expr(inner, None),
644
+ ast::Expr::Ternary(t) => {
645
+ self.walk_expr(&t.condition, None);
646
+ self.walk_expr(&t.then, None);
647
+ self.walk_expr(&t.else_, None);
648
+ }
649
+ ast::Expr::FnCall(call) => {
650
+ let callee = self.fn_decls.get(&call.name).copied();
651
+ for (i, arg) in call.args.iter().enumerate() {
652
+ let arg_expr = arg_expr_of(arg);
653
+ let expected = callee.and_then(|f| f.params.get(i)).map(|p| &p.ty)
654
+ .filter(|pt| matches!(pt, ast::ParamType::Fn(_, _)));
655
+ self.walk_expr(arg_expr, expected);
656
+ }
657
+ }
658
+ ast::Expr::ClassCall(call) => {
659
+ for fa in &call.fields {
660
+ self.walk_expr(&fa.value, None);
661
+ }
662
+ }
663
+ ast::Expr::Attribute(a) => {
664
+ self.walk_expr(&a.object, None);
665
+ if let ast::AttrKind::Method(call) = &a.attr {
666
+ for arg in &call.args {
667
+ self.walk_expr(arg_expr_of(arg), None);
668
+ }
669
+ }
670
+ }
671
+ ast::Expr::Int(_)
672
+ | ast::Expr::Float(_)
673
+ | ast::Expr::String(_)
674
+ | ast::Expr::Self_
675
+ | ast::Expr::Var(_)
676
+ | ast::Expr::TypeName(_) => {}
677
+ }
678
+ }
679
+
680
+ fn record_closure(&mut self, cl: &'a ast::Closure, expected_fn: Option<&'a ast::ParamType>) {
681
+ let (param_vts, param_ptypes, ret_vt) = match expected_fn {
682
+ Some(ast::ParamType::Fn(params, ret)) => {
683
+ let param_vts = params.iter().map(|t| ast_type_to_wasm(&t.name).unwrap_or(ValType::I32)).collect();
684
+ let param_ptypes = params.iter().map(plum_checker::plum_type_from_ast).collect();
685
+ let ret_vt = ret.as_ref().and_then(|t| ast_type_to_wasm(&t.name));
686
+ (param_vts, param_ptypes, ret_vt)
687
+ }
688
+ _ => {
689
+ // Not a direct `fn`-typed call argument: infer the closure's own type.
690
+ match plum_checker::infer_expr(&ast::Expr::Closure(Box::new(cl.clone())), &self.env, &self.cctx) {
691
+ Ok(PlumType::TFun(ptypes, ret)) => {
692
+ let param_vts = ptypes.iter().map(plum_type_to_valtype).collect();
693
+ let ret_vt = match *ret {
694
+ PlumType::TUnit => None,
695
+ other => Some(plum_type_to_valtype(&other)),
696
+ };
697
+ (param_vts, ptypes, ret_vt)
698
+ }
699
+ _ => {
700
+ let param_vts = cl.params.iter().map(|_| ValType::I64).collect();
701
+ let param_ptypes = cl.params.iter().map(|_| PlumType::TInt).collect();
702
+ (param_vts, param_ptypes, Some(ValType::I64))
703
+ }
704
+ }
705
+ }
706
+ };
707
+ let free_vars = collect_free_vars(cl, &self.env);
708
+ self.found.push(RawClosure {
709
+ ptr: cl as *const ast::Closure as usize,
710
+ closure: cl,
711
+ param_vts,
712
+ param_ptypes,
713
+ ret_vt,
714
+ free_vars,
715
+ });
716
+ }
717
+ }
718
+
719
+ fn arg_expr_of(arg: &ast::Arg) -> &ast::Expr {
720
+ match arg {
721
+ ast::Arg::Positional(e) => e,
722
+ ast::Arg::Keyword { value, .. } => value,
723
+ ast::Arg::Pair { value, .. } => value,
724
+ }
725
+ }
726
+
727
+ /// Determines a closure's captured free variables: every `Var` referenced in its body
728
+ /// that is neither one of the closure's own params nor assigned locally inside the body,
729
+ /// in first-appearance order. Each free variable's type is looked up in the *enclosing*
730
+ /// scope's type env.
731
+ fn collect_free_vars(cl: &ast::Closure, env: &TypeEnv) -> Vec<(String, PlumType)> {
732
+ let mut bound: std::collections::HashSet<String> = cl.params.iter().cloned().collect();
733
+ fv_collect_bound_block(&cl.body, &mut bound);
734
+
735
+ let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
736
+ let mut free: Vec<(String, PlumType)> = Vec::new();
737
+ fv_collect_refs_block(&cl.body, &bound, &mut seen, &mut free, env);
738
+ free
739
+ }
740
+
741
+ fn fv_collect_bound_block(block: &ast::Block, bound: &mut std::collections::HashSet<String>) {
742
+ for s in &block.stmts {
743
+ match s {
744
+ ast::Stmt::Assign(a) => {
745
+ for t in &a.targets {
746
+ bound.insert(t.clone());
747
+ }
748
+ }
749
+ ast::Stmt::For(f) => {
750
+ for v in &f.vars {
751
+ bound.insert(v.clone());
752
+ }
753
+ fv_collect_bound_block(&f.body, bound);
754
+ }
755
+ ast::Stmt::If(i) => {
756
+ fv_collect_bound_block(&i.body, bound);
757
+ for ei in &i.else_ifs {
758
+ fv_collect_bound_block(&ei.body, bound);
759
+ }
760
+ if let Some(e) = &i.else_ {
761
+ fv_collect_bound_block(e, bound);
762
+ }
763
+ }
764
+ ast::Stmt::While(w) => fv_collect_bound_block(&w.body, bound),
765
+ ast::Stmt::Match(m) => {
766
+ for case in &m.cases {
767
+ for p in &case.patterns {
768
+ fv_collect_pattern_bindings(p, bound);
769
+ }
770
+ fv_collect_bound_block(&case.body, bound);
771
+ }
772
+ }
773
+ _ => {}
774
+ }
775
+ }
776
+ }
777
+
778
+ fn fv_collect_pattern_bindings(pat: &ast::CasePattern, bound: &mut std::collections::HashSet<String>) {
779
+ match pat {
780
+ ast::CasePattern::Name(n) => { bound.insert(n.clone()); }
781
+ ast::CasePattern::Class { fields, .. } => {
782
+ for f in fields {
783
+ fv_collect_pattern_bindings(f, bound);
784
+ }
785
+ }
786
+ _ => {}
787
+ }
788
+ }
789
+
790
+ fn fv_collect_refs_block(
791
+ block: &ast::Block,
792
+ bound: &std::collections::HashSet<String>,
793
+ seen: &mut std::collections::HashSet<String>,
794
+ free: &mut Vec<(String, PlumType)>,
795
+ env: &TypeEnv,
796
+ ) {
797
+ for s in &block.stmts {
798
+ match s {
799
+ ast::Stmt::Assign(a) => {
800
+ for v in &a.values {
801
+ fv_collect_refs_expr(v, bound, seen, free, env);
802
+ }
803
+ }
804
+ ast::Stmt::Return(Some(e)) | ast::Stmt::Expr(e) | ast::Stmt::Assert(e) => {
805
+ fv_collect_refs_expr(e, bound, seen, free, env);
806
+ }
807
+ ast::Stmt::If(i) => {
808
+ fv_collect_refs_expr(&i.condition, bound, seen, free, env);
809
+ fv_collect_refs_block(&i.body, bound, seen, free, env);
810
+ for ei in &i.else_ifs {
811
+ fv_collect_refs_expr(&ei.condition, bound, seen, free, env);
812
+ fv_collect_refs_block(&ei.body, bound, seen, free, env);
813
+ }
814
+ if let Some(e) = &i.else_ {
815
+ fv_collect_refs_block(e, bound, seen, free, env);
816
+ }
817
+ }
818
+ ast::Stmt::While(w) => {
819
+ fv_collect_refs_expr(&w.condition, bound, seen, free, env);
820
+ fv_collect_refs_block(&w.body, bound, seen, free, env);
821
+ }
822
+ ast::Stmt::For(f) => {
823
+ fv_collect_refs_expr(&f.iter, bound, seen, free, env);
824
+ fv_collect_refs_block(&f.body, bound, seen, free, env);
825
+ }
826
+ ast::Stmt::Match(m) => {
827
+ for subj in &m.subjects {
828
+ fv_collect_refs_expr(subj, bound, seen, free, env);
829
+ }
830
+ for case in &m.cases {
831
+ fv_collect_refs_block(&case.body, bound, seen, free, env);
832
+ }
833
+ }
834
+ _ => {}
835
+ }
836
+ }
837
+ }
838
+
839
+ fn fv_collect_refs_expr(
840
+ expr: &ast::Expr,
841
+ bound: &std::collections::HashSet<String>,
842
+ seen: &mut std::collections::HashSet<String>,
843
+ free: &mut Vec<(String, PlumType)>,
844
+ env: &TypeEnv,
845
+ ) {
846
+ match expr {
847
+ ast::Expr::Var(name) => {
848
+ if !bound.contains(name) && seen.insert(name.clone()) {
849
+ let ty = plum_checker::lookup(env, name).unwrap_or(PlumType::TInt);
850
+ free.push((name.clone(), ty));
851
+ }
852
+ }
853
+ ast::Expr::Binary(b) => { fv_collect_refs_expr(&b.left, bound, seen, free, env); fv_collect_refs_expr(&b.right, bound, seen, free, env); }
854
+ ast::Expr::Bool(b) => { fv_collect_refs_expr(&b.left, bound, seen, free, env); fv_collect_refs_expr(&b.right, bound, seen, free, env); }
855
+ ast::Expr::Compare(c) => { fv_collect_refs_expr(&c.left, bound, seen, free, env); fv_collect_refs_expr(&c.right, bound, seen, free, env); }
856
+ ast::Expr::Not(inner) => fv_collect_refs_expr(inner, bound, seen, free, env),
857
+ ast::Expr::Unary(u) => fv_collect_refs_expr(&u.operand, bound, seen, free, env),
858
+ ast::Expr::Paren(inner) => fv_collect_refs_expr(inner, bound, seen, free, env),
859
+ ast::Expr::Ternary(t) => {
860
+ fv_collect_refs_expr(&t.condition, bound, seen, free, env);
861
+ fv_collect_refs_expr(&t.then, bound, seen, free, env);
862
+ fv_collect_refs_expr(&t.else_, bound, seen, free, env);
863
+ }
864
+ ast::Expr::FnCall(call) => {
865
+ for arg in &call.args {
866
+ fv_collect_refs_expr(arg_expr_of(arg), bound, seen, free, env);
867
+ }
868
+ }
869
+ ast::Expr::ClassCall(call) => {
870
+ for fa in &call.fields {
871
+ fv_collect_refs_expr(&fa.value, bound, seen, free, env);
872
+ }
873
+ }
874
+ ast::Expr::Attribute(a) => {
875
+ fv_collect_refs_expr(&a.object, bound, seen, free, env);
876
+ if let ast::AttrKind::Method(call) = &a.attr {
877
+ for arg in &call.args {
878
+ fv_collect_refs_expr(arg_expr_of(arg), bound, seen, free, env);
879
+ }
880
+ }
881
+ }
882
+ // A nested closure isn't supported by the discovery pre-pass; its own free-var
883
+ // references are not hoisted here.
884
+ _ => {}
885
+ }
886
+ }
887
+
399
888
  /// Walks a function body once to determine: (1) every locally-assigned/bound name and
400
889
  /// its inferred type, (2) how many `ClassCall` scratch temporaries it needs, and (3)
401
890
  /// the subject type for every `match` statement (for its own scratch temporary).
@@ -407,6 +896,9 @@ struct Collector<'a> {
407
896
  classcall_scratch: HashMap<usize, u32>,
408
897
  match_scratch: HashMap<usize, PlumType>,
409
898
  next_classcall_slot: u32,
899
+ /// `Closure` expr identity -> offset of its first (of two) construction scratch slots.
900
+ closure_scratch: HashMap<usize, u32>,
901
+ next_closure_slot: u32,
410
902
  }
411
903
 
412
904
  impl<'a> Collector<'a> {
@@ -558,9 +1050,15 @@ impl<'a> Collector<'a> {
558
1050
  | ast::Expr::Self_
559
1051
  | ast::Expr::Var(_)
560
1052
  | ast::Expr::TypeName(_) => {}
1053
+ // A closure literal needs two construction scratch slots in the *enclosing*
1054
+ // function (env struct ptr, closure struct ptr). Its body's own locals are
561
- // TODO: closures aren't compiled yet; codegen for closure bodies lands
1055
+ // NOT this function's they belong to the separate closure function — so we
562
- // in a later task.
1056
+ // don't recurse into the body here.
563
- ast::Expr::Closure(_) => {}
1057
+ ast::Expr::Closure(cl) => {
1058
+ let base = self.next_closure_slot;
1059
+ self.next_closure_slot += 2;
1060
+ self.closure_scratch.insert(cl.as_ref() as *const ast::Closure as usize, base);
1061
+ }
564
1062
  }
565
1063
  }
566
1064
 
@@ -601,9 +1099,15 @@ fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Re
601
1099
  classcall_scratch: HashMap::new(),
602
1100
  match_scratch: HashMap::new(),
603
1101
  next_classcall_slot: 0,
1102
+ closure_scratch: HashMap::new(),
1103
+ next_closure_slot: 0,
604
1104
  };
605
1105
  if let ast::FnBody::Block(block) = &f.body {
606
1106
  collector.walk_block(block);
1107
+ } else if let ast::FnBody::Expr(e) = &f.body {
1108
+ // An expression-bodied fn can still contain a closure literal (e.g.
1109
+ // `main() -> Int = each(|v| v)`), which needs construction scratch slots.
1110
+ collector.walk_expr(e);
607
1111
  }
608
1112
 
609
1113
  // ---- assign local indices: [self?][params][named...][classcall scratch...][match scratch...] ----
@@ -641,6 +1145,12 @@ fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Re
641
1145
  idx += 1;
642
1146
  }
643
1147
 
1148
+ let closure_scratch_base = idx;
1149
+ let closure_scratch_count = collector.closure_scratch.values().copied().max().map(|m| m + 2).unwrap_or(0);
1150
+ for _ in 0..closure_scratch_count {
1151
+ groups.push(ValType::I32);
1152
+ }
1153
+
644
1154
  if groups.is_empty() {
645
1155
  body.push(0);
646
1156
  } else {
@@ -657,8 +1167,12 @@ fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Re
657
1167
  classcall_scratch: collector.classcall_scratch,
658
1168
  match_scratch_base,
659
1169
  match_scratch_index,
1170
+ closure_scratch_base,
1171
+ closure_scratch: collector.closure_scratch,
660
1172
  func_ids: &ctx.func_ids,
661
1173
  func_sigs: &ctx.func_sigs,
1174
+ closures: &ctx.closures,
1175
+ closure_call_types: &ctx.closure_call_types,
662
1176
  classes: &ctx.classes,
663
1177
  methods: &ctx.methods,
664
1178
  enum_variants: &ctx.enum_variants,
@@ -905,7 +1419,14 @@ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
905
1419
  /// Returns true if the expression leaves a value on the wasm stack.
906
1420
  fn expr_has_result(expr: &ast::Expr, ctx: &LocalCtx) -> bool {
907
1421
  match expr {
1422
+ ast::Expr::FnCall(call) => {
1423
+ if ctx.locals.contains_key(&call.name) {
1424
+ if let PlumType::TFun(_, ret) = infer_local_type(&ast::Expr::Var(call.name.clone()), ctx) {
1425
+ return !matches!(*ret, PlumType::TUnit);
1426
+ }
1427
+ }
908
- ast::Expr::FnCall(call) => ctx.func_sigs.get(&call.name).map(|s| s.ret.is_some()).unwrap_or(true),
1428
+ ctx.func_sigs.get(&call.name).map(|s| s.ret.is_some()).unwrap_or(true)
1429
+ }
909
1430
  ast::Expr::Attribute(attr) => match &attr.attr {
910
1431
  ast::AttrKind::Method(call) => {
911
1432
  if let PlumType::TNamed(class_name) = infer_local_type(&attr.object, ctx) {
@@ -1293,7 +1814,13 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
1293
1814
  Instruction::End.encode(body);
1294
1815
  }
1295
1816
  ast::Expr::FnCall(call) => {
1817
+ // A call whose callee name is a *local* of function type is a closure call,
1818
+ // dispatched via `call_indirect` — not a direct `Call` to a named function.
1819
+ let is_closure_call = ctx.locals.contains_key(&call.name)
1820
+ && matches!(infer_local_type(&ast::Expr::Var(call.name.clone()), ctx), PlumType::TFun(_, _));
1821
+ if is_closure_call {
1822
+ compile_closure_call(call, body, ctx, state)?;
1296
- if let Some(info) = ctx.enum_variants.get(&call.name) {
1823
+ } else if let Some(info) = ctx.enum_variants.get(&call.name) {
1297
1824
  compile_variant_construction(info, call, expr, body, ctx, state)?;
1298
1825
  } else {
1299
1826
  for arg in &call.args {
@@ -1434,10 +1961,8 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
1434
1961
  state.data_segments.push((offset, data));
1435
1962
  Instruction::I32Const(offset as i32).encode(body);
1436
1963
  }
1437
- // TODO: closure codegen (capturing free variables, emitting an indirect-callable
1438
- // value) lands in a later task.
1439
- ast::Expr::Closure(_) => {
1964
+ ast::Expr::Closure(cl) => {
1440
- return Err("codegen: closures are not yet supported".to_string());
1965
+ compile_closure_literal(cl, body, ctx, state)?;
1441
1966
  }
1442
1967
  }
1443
1968
  Ok(())
@@ -1503,3 +2028,253 @@ fn compile_variant_construction(
1503
2028
  Instruction::LocalGet(scratch_local).encode(body);
1504
2029
  Ok(())
1505
2030
  }
2031
+
2032
+ /// Emits a width-appropriate store for a heap slot: `[addr, value]` -> memory.
2033
+ fn emit_store(vt: ValType, offset: u64, body: &mut Vec<u8>) {
2034
+ match vt {
2035
+ ValType::I64 => Instruction::I64Store(MemArg { offset, align: 3, memory_index: 0 }),
2036
+ ValType::F64 => Instruction::F64Store(MemArg { offset, align: 3, memory_index: 0 }),
2037
+ _ => Instruction::I32Store(MemArg { offset, align: 2, memory_index: 0 }),
2038
+ }.encode(body);
2039
+ }
2040
+
2041
+ /// Emits a width-appropriate load for a heap slot: `[addr]` -> value.
2042
+ fn emit_load(vt: ValType, offset: u64, body: &mut Vec<u8>) {
2043
+ match vt {
2044
+ ValType::I64 => Instruction::I64Load(MemArg { offset, align: 3, memory_index: 0 }),
2045
+ ValType::F64 => Instruction::F64Load(MemArg { offset, align: 3, memory_index: 0 }),
2046
+ _ => Instruction::I32Load(MemArg { offset, align: 2, memory_index: 0 }),
2047
+ }.encode(body);
2048
+ }
2049
+
2050
+ /// Compiles a closure *literal* at its construction site. Snapshots each captured free
2051
+ /// variable's CURRENT value into a fresh heap env struct, then builds the 2-word closure
2052
+ /// struct `{table_index @0, env_pointer @8}` and leaves its pointer on the stack.
2053
+ fn compile_closure_literal(
2054
+ cl: &ast::Closure,
2055
+ body: &mut Vec<u8>,
2056
+ ctx: &LocalCtx,
2057
+ _state: &mut ModuleState,
2058
+ ) -> Result<(), String> {
2059
+ let key = cl as *const ast::Closure as usize;
2060
+ let info = ctx
2061
+ .closures
2062
+ .get(&key)
2063
+ .ok_or_else(|| "codegen: closure literal was not discovered (nested closures are not supported)".to_string())?;
2064
+ let scratch_off = *ctx
2065
+ .closure_scratch
2066
+ .get(&key)
2067
+ .ok_or_else(|| "internal codegen error: missing closure scratch slot".to_string())?;
2068
+ let env_scratch = ctx.closure_scratch_base + scratch_off;
2069
+ let closure_scratch = env_scratch + 1;
2070
+
2071
+ // Bump-allocate the env struct: one 8-byte slot per captured variable.
2072
+ let env_size = (info.free_vars.len() as i32) * 8;
2073
+ Instruction::GlobalGet(ctx.bump_global).encode(body);
2074
+ Instruction::LocalSet(env_scratch).encode(body);
2075
+ Instruction::GlobalGet(ctx.bump_global).encode(body);
2076
+ Instruction::I32Const(env_size).encode(body);
2077
+ Instruction::I32Add.encode(body);
2078
+ Instruction::GlobalSet(ctx.bump_global).encode(body);
2079
+
2080
+ // Snapshot each free variable's current value from the enclosing function's local.
2081
+ for (i, (name, ty)) in info.free_vars.iter().enumerate() {
2082
+ let local_idx = *ctx
2083
+ .locals
2084
+ .get(name)
2085
+ .ok_or_else(|| format!("codegen: captured variable '{}' is not a local in the enclosing scope", name))?;
2086
+ Instruction::LocalGet(env_scratch).encode(body);
2087
+ Instruction::LocalGet(local_idx).encode(body);
2088
+ emit_store(plum_type_to_valtype(ty), (i as u64) * 8, body);
2089
+ }
2090
+
2091
+ // Bump-allocate the 2-word closure struct.
2092
+ Instruction::GlobalGet(ctx.bump_global).encode(body);
2093
+ Instruction::LocalSet(closure_scratch).encode(body);
2094
+ Instruction::GlobalGet(ctx.bump_global).encode(body);
2095
+ Instruction::I32Const(16).encode(body);
2096
+ Instruction::I32Add.encode(body);
2097
+ Instruction::GlobalSet(ctx.bump_global).encode(body);
2098
+
2099
+ // table_index @ offset 0
2100
+ Instruction::LocalGet(closure_scratch).encode(body);
2101
+ Instruction::I32Const(info.table_idx as i32).encode(body);
2102
+ Instruction::I32Store(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(body);
2103
+ // env_pointer @ offset 8
2104
+ Instruction::LocalGet(closure_scratch).encode(body);
2105
+ Instruction::LocalGet(env_scratch).encode(body);
2106
+ Instruction::I32Store(MemArg { offset: 8, align: 2, memory_index: 0 }).encode(body);
2107
+
2108
+ // The closure value is its struct pointer.
2109
+ Instruction::LocalGet(closure_scratch).encode(body);
2110
+ Ok(())
2111
+ }
2112
+
2113
+ /// Compiles a call to a closure-typed local via `call_indirect`. Stack order matches the
2114
+ /// closure function's signature `(env_ptr, ...args)`: push the env pointer, then each
2115
+ /// argument, then the table index (the `call_indirect` operand).
2116
+ fn compile_closure_call(
2117
+ call: &ast::FnCall,
2118
+ body: &mut Vec<u8>,
2119
+ ctx: &LocalCtx,
2120
+ state: &mut ModuleState,
2121
+ ) -> Result<(), String> {
2122
+ let closure_local = *ctx
2123
+ .locals
2124
+ .get(&call.name)
2125
+ .ok_or_else(|| format!("codegen: closure '{}' is not a local", call.name))?;
2126
+
2127
+ let (param_ptypes, ret_ptype) = match infer_local_type(&ast::Expr::Var(call.name.clone()), ctx) {
2128
+ PlumType::TFun(p, r) => (p, *r),
2129
+ other => return Err(format!("codegen: '{}' is not callable (type {:?})", call.name, other)),
2130
+ };
2131
+
2132
+ let mut sig_params = vec![ValType::I32]; // env pointer
2133
+ for p in &param_ptypes {
2134
+ sig_params.push(plum_type_to_valtype(p));
2135
+ }
2136
+ let ret_vt = match ret_ptype {
2137
+ PlumType::TUnit => None,
2138
+ other => Some(plum_type_to_valtype(&other)),
2139
+ };
2140
+ let sig_key: ClosureSigKey = (sig_params, ret_vt);
2141
+ let type_index = *ctx
2142
+ .closure_call_types
2143
+ .get(&sig_key)
2144
+ .ok_or_else(|| format!("internal codegen error: no call_indirect type for closure '{}'", call.name))?;
2145
+
2146
+ // env pointer (struct offset 8)
2147
+ Instruction::LocalGet(closure_local).encode(body);
2148
+ Instruction::I32Load(MemArg { offset: 8, align: 2, memory_index: 0 }).encode(body);
2149
+ // real arguments
2150
+ for arg in &call.args {
2151
+ compile_expr(arg_expr_of(arg), body, ctx, state)?;
2152
+ }
2153
+ // table index (struct offset 0) — the call_indirect operand
2154
+ Instruction::LocalGet(closure_local).encode(body);
2155
+ Instruction::I32Load(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(body);
2156
+ Instruction::CallIndirect { type_index, table_index: 0 }.encode(body);
2157
+ Ok(())
2158
+ }
2159
+
2160
+ /// Compiles a closure literal's own body into a standalone wasm function. Local 0 is the
2161
+ /// implicit env pointer; the closure's params follow; then each captured free variable
2162
+ /// gets a local loaded from the env struct at function entry (restoring the snapshot).
2163
+ fn compile_closure_body(
2164
+ cl: &ast::Closure,
2165
+ info: &ClosureInfo,
2166
+ ctx: &CompileCtx,
2167
+ state: &mut ModuleState,
2168
+ ) -> Result<Vec<u8>, String> {
2169
+ let mut body = Vec::new();
2170
+
2171
+ // Base type env: globals + captured free vars + closure params.
2172
+ let mut base_env = ctx.global_env.clone();
2173
+ for (name, ty) in &info.free_vars {
2174
+ base_env.insert(name.clone(), TypeScheme::mono(ty.clone()));
2175
+ }
2176
+ for (name, pty) in cl.params.iter().zip(info.param_ptypes.iter()) {
2177
+ base_env.insert(name.clone(), TypeScheme::mono(pty.clone()));
2178
+ }
2179
+
2180
+ let mut collector = Collector {
2181
+ env: base_env.clone(),
2182
+ cctx: check_ctx_of(&ctx.classes, &ctx.methods, &ctx.enum_variants),
2183
+ named: Vec::new(),
2184
+ named_set: Default::default(),
2185
+ classcall_scratch: HashMap::new(),
2186
+ match_scratch: HashMap::new(),
2187
+ next_classcall_slot: 0,
2188
+ closure_scratch: HashMap::new(),
2189
+ next_closure_slot: 0,
2190
+ };
2191
+ collector.walk_block(&cl.body);
2192
+
2193
+ // ---- local index layout ----
2194
+ // [env_ptr][closure params][free-var locals][named...][classcall][match][closure scratch]
2195
+ let mut locals: HashMap<String, u32> = HashMap::new();
2196
+ let mut groups: Vec<ValType> = Vec::new();
2197
+ let mut idx = 0u32;
2198
+
2199
+ idx += 1; // local 0 = env pointer (a param, so not declared below)
2200
+ for name in &cl.params {
2201
+ locals.insert(name.clone(), idx);
2202
+ idx += 1;
2203
+ }
2204
+ for (name, ty) in &info.free_vars {
2205
+ locals.insert(name.clone(), idx);
2206
+ groups.push(plum_type_to_valtype(ty));
2207
+ idx += 1;
2208
+ }
2209
+ for (name, ty) in &collector.named {
2210
+ if locals.contains_key(name) {
2211
+ continue;
2212
+ }
2213
+ locals.insert(name.clone(), idx);
2214
+ groups.push(plum_type_to_valtype(ty));
2215
+ idx += 1;
2216
+ }
2217
+
2218
+ let classcall_scratch_base = idx;
2219
+ let classcall_count = collector.classcall_scratch.values().copied().max().map(|m| m + 1).unwrap_or(0);
2220
+ for _ in 0..classcall_count {
2221
+ groups.push(ValType::I32);
2222
+ idx += 1;
2223
+ }
2224
+
2225
+ let match_scratch_base = idx;
2226
+ let mut match_scratch_index: HashMap<usize, u32> = HashMap::new();
2227
+ for (ptr, ty) in collector.match_scratch.iter() {
2228
+ match_scratch_index.insert(*ptr, idx - match_scratch_base);
2229
+ groups.push(plum_type_to_valtype(ty));
2230
+ idx += 1;
2231
+ }
2232
+
2233
+ let closure_scratch_base = idx;
2234
+ let closure_scratch_count = collector.closure_scratch.values().copied().max().map(|m| m + 2).unwrap_or(0);
2235
+ for _ in 0..closure_scratch_count {
2236
+ groups.push(ValType::I32);
2237
+ }
2238
+
2239
+ if groups.is_empty() {
2240
+ body.push(0);
2241
+ } else {
2242
+ body.extend(encode_leb128_u32(groups.len() as u32));
2243
+ for g in &groups {
2244
+ body.extend(encode_leb128_u32(1));
2245
+ g.encode(&mut body);
2246
+ }
2247
+ }
2248
+
2249
+ // Restore each captured free variable from the env struct (local 0) at entry.
2250
+ for (i, (name, ty)) in info.free_vars.iter().enumerate() {
2251
+ let local_idx = *locals.get(name).expect("free var local was assigned above");
2252
+ Instruction::LocalGet(0).encode(&mut body); // env pointer
2253
+ emit_load(plum_type_to_valtype(ty), (i as u64) * 8, &mut body);
2254
+ Instruction::LocalSet(local_idx).encode(&mut body);
2255
+ }
2256
+
2257
+ let local_ctx = LocalCtx {
2258
+ locals,
2259
+ classcall_scratch_base,
2260
+ classcall_scratch: collector.classcall_scratch,
2261
+ match_scratch_base,
2262
+ match_scratch_index,
2263
+ closure_scratch_base,
2264
+ closure_scratch: collector.closure_scratch,
2265
+ func_ids: &ctx.func_ids,
2266
+ func_sigs: &ctx.func_sigs,
2267
+ closures: &ctx.closures,
2268
+ closure_call_types: &ctx.closure_call_types,
2269
+ classes: &ctx.classes,
2270
+ methods: &ctx.methods,
2271
+ enum_variants: &ctx.enum_variants,
2272
+ type_env: RefCell::new(base_env),
2273
+ bump_global: ctx.bump_global,
2274
+ };
2275
+
2276
+ compile_block_as_fn_body(&cl.body, &mut body, &local_ctx, state, info.ret_vt)?;
2277
+
2278
+ Instruction::End.encode(&mut body);
2279
+ Ok(body)
2280
+ }
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -900,3 +900,64 @@ fn wasm_module_with_a_table_element_validates_and_call_indirect_works() {
900
900
  let main = instance.get_typed_func::<(), i64>(&mut store, "main").expect("main should have signature () -> i64");
901
901
  assert_eq!(main.call(&mut store, ()).expect("main should not trap"), 42);
902
902
  }
903
+
904
+
905
+ #[test]
906
+ fn non_capturing_closure_passed_and_called_runs_correctly() {
907
+ // The brief's plan scopes closures as "passable as an ordinary call argument
908
+ // (`each(|v| ...)`)"; the inline single-expression body is that supported form.
909
+ // (A multi-line indented closure body immediately followed by `)` on the same line
910
+ // needs the external scanner to emit a dedent at a closing bracket, which is a
911
+ // parser/scanner concern outside this codegen task — see task-5-report.md.)
912
+ let src = "\
913
+ each(cb: fn(Int) -> Int) -> Int =
914
+ cb(5)
915
+
916
+ main() -> Int =
917
+ each(|v| v)
918
+ ";
919
+ let source = parse(src);
920
+ let bytes = compile_source(&source).expect("compile failed");
921
+ assert_eq!(run_main(&bytes), 5);
922
+ }
923
+
924
+ #[test]
925
+ fn capturing_closure_snapshots_value_at_creation_time_runs_correctly() {
926
+ let src = "\
927
+ each(cb: fn(Int) -> Int) -> Int =
928
+ cb(0)
929
+
930
+ useClosure() -> Int =
931
+ x = 10
932
+ cb = |v|
933
+ x + v
934
+ x = 999
935
+ each(cb)
936
+
937
+ main() -> Int =
938
+ useClosure()
939
+ ";
940
+ let source = parse(src);
941
+ let bytes = compile_source(&source).expect("compile failed");
942
+ // The closure must see x==10 (its value when the closure was created), not 999
943
+ // (its value when `each(cb)` is actually called) - proving snapshot-by-value
944
+ // capture, not a live/shared reference.
945
+ assert_eq!(run_main(&bytes), 10);
946
+ }
947
+
948
+ #[test]
949
+ fn closure_passed_through_already_generic_higher_order_function_runs_correctly() {
950
+ let src = "\
951
+ identity(value: a) -> a =
952
+ value
953
+
954
+ each(cb: fn(Int) -> Int) -> Int =
955
+ cb(identity(7))
956
+
957
+ main() -> Int =
958
+ each(|v| v * 2)
959
+ ";
960
+ let source = parse(src);
961
+ let bytes = compile_source(&source).expect("compile failed");
962
+ assert_eq!(run_main(&bytes), 14);
963
+ }
tooling/tree-sitter-plum/grammar.js CHANGED
@@ -372,7 +372,10 @@ module.exports = grammar({
372
372
  "|",
373
373
  field("parameters", optional(commaSep1($.var_identifier))),
374
374
  "|",
375
+ // A closure body is either a single inline expression (`|v| v`, usable as an
376
+ // ordinary call argument like `each(|v| v)`) or an indented block — the same
377
+ // `choice($.expression, $.body)` shape already used by `fn` and `case` bodies.
375
- field("body", $.body),
378
+ field("body", choice($.expression, $.body)),
376
379
  ),
377
380
 
378
381
  // The member name always lexes as `fn_identifier` (a superset of `var_identifier`,
tooling/tree-sitter-plum/src/grammar.json CHANGED
Binary file
tooling/tree-sitter-plum/src/node-types.json CHANGED
Binary file
tooling/tree-sitter-plum/src/parser.c CHANGED
Binary file
tooling/tree-sitter-plum/test/corpus/function.txt CHANGED
@@ -356,3 +356,30 @@ each(cb: fn(a) -> b) -> Bool =
356
356
  (expression
357
357
  (primary_expression
358
358
  (type_identifier))))))
359
+
360
+ ================================================================================
361
+ function - inline closure literal as a call argument
362
+ ================================================================================
363
+
364
+ main() -> Int =
365
+ each(|v| v)
366
+
367
+ --------------------------------------------------------------------------------
368
+
369
+ (source
370
+ (fn
371
+ name: (fn_identifier)
372
+ returns: (return_type
373
+ (type_identifier))
374
+ body: (body
375
+ (expression
376
+ (primary_expression
377
+ (fn_call
378
+ function: (var_identifier)
379
+ arguments: (fn_argument_list
380
+ (expression
381
+ (closure
382
+ parameters: (var_identifier)
383
+ body: (expression
384
+ (primary_expression
385
+ (var_identifier))))))))))))