plum

#treesitter#compiler#wasm

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

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


85ed0b1 — Peter John 2026-10-26T14:41:12+05:30
fix: fn-field nested call args, heap-valued consts, value-enum nested patterns and field assign through heap fields
plum-checker/src/lib.rs CHANGED
@@ -1300,6 +1300,7 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
1300
1300
  // `buildGlobalTables` only had literals to go on; now that every method/enum
1301
1301
  // table exists, re-infer each one with the full inferer. Two passes let a const
1302
1302
  // reference one declared later.
1303
+ drainCalls();
1303
1304
  for _ in 0..2 {
1304
1305
  for item in &source.items {
1305
1306
  if let ast::Item::Const(c) = item {
@@ -1309,6 +1310,9 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
1309
1310
  }
1310
1311
  }
1311
1312
  }
1313
+ // The calls inside a const initializer (`List(1, 2)`) are instantiated like a
1314
+ // root fn's: codegen compiles each initializer once, outside any instantiation.
1315
+ let const_calls = drainCalls();
1312
1316
 
1313
1317
  errors.extend(checkValueEnums(source));
1314
1318
 
@@ -1388,6 +1392,12 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
1388
1392
  file,
1389
1393
  };
1390
1394
  drainCalls();
1395
+ CALLS.with(|c| {
1396
+ if let Some(v) = c.borrow_mut().as_mut() {
1397
+ v.extend(const_calls);
1398
+ }
1399
+ });
1400
+ registerCalls(&mut inst, &fn_by_key, &ctx, None);
1391
1401
 
1392
1402
  // Roots: every non-generic fn and every test. A generic fn is only
1393
1403
  // name-resolved here (its body is type-checked per instantiation below).
plum-std/Heap.plum CHANGED
@@ -29,9 +29,7 @@ enum Heap[T] =
29
29
  self.items.data.set(j, a)
30
30
 
31
31
  fun less(self, i: Int, j: Int) -> Bool =
32
- a := self.items.data.get(i)
33
- b := self.items.data.get(j)
34
- return self.key(a) < self.key(b)
32
+ return self.key(self.items.data.get(i)) < self.key(self.items.data.get(j))
35
33
 
36
34
  fun siftUp(self, start: Int) -> Unit =
37
35
  i := start
plum-wasm-codegen/src/lib.rs CHANGED
@@ -2439,6 +2439,30 @@ fn compileSourceInner(
2439
2439
  Instruction::GlobalSet(global_idx).encode(&mut start_body);
2440
2440
  }
2441
2441
 
2442
+ // Heap-valued consts (`TABLE = List(..)`): one lazily-initialised cached global and
2443
+ // one accessor function each (`const#NAME`), compiled below.
2444
+ let mut const_inits: Vec<(String, ConstInit, u32)> = Vec::new();
2445
+ for item in &source.items {
2446
+ let ast::Item::Const(c) = item else { continue };
2447
+ if !isBuiltConstInit(&c.value) {
2448
+ continue;
2449
+ }
2450
+ let Some(ty) = expr_types.get(&(&c.value as *const ast::Expr as usize)) else { continue };
2451
+ if !matches!(ty, PlumType::TNamed(_) | PlumType::TApp(..)) || isValueTy(ty) {
2452
+ continue;
2453
+ }
2454
+ let vt = plumTypeToValtype(ty);
2455
+ let ValType::Ref(rt) = vt else { continue };
2456
+ let mut init = Vec::new();
2457
+ Instruction::RefNull(rt.heap_type).encode(&mut init);
2458
+ let global = module.addGlobal(vt, true, &init);
2459
+ let t = module.addType(&[], &[vt]);
2460
+ let func_idx = module.addFunction(t, &[]);
2461
+ module.nameFunction(func_idx, &format!("const.{}", c.name));
2462
+ func_ids.insert(format!("const#{}", c.name), func_idx);
2463
+ const_inits.push((c.name.clone(), ConstInit { expr: &c.value, ty: ty.clone(), global }, func_idx));
2464
+ }
2465
+
2442
2466
  Instruction::End.encode(&mut start_body);
2443
2467
  let mut final_start_body = vec![0u8];
2444
2468
  final_start_body.extend(start_body);
@@ -2501,6 +2525,25 @@ fn compileSourceInner(
2501
2525
  module.functions[slot].1 = body.clone();
2502
2526
  }
2503
2527
 
2528
+ for (name, ci, func_idx) in &const_inits {
2529
+ let stand_in = ast::Fn {
2530
+ name: format!("const#{name}"),
2531
+ line: 0,
2532
+ column: 0,
2533
+ type_param: None,
2534
+ is_extern: false,
2535
+ params: Vec::new(),
2536
+ returns: None,
2537
+ body: ast::FnBody::Block(ast::Block { stmts: Vec::new() }),
2538
+ };
2539
+ let no_subst = Subst::new();
2540
+ let uref = UnitRef { idx: usize::MAX, inst: None, subst: &no_subst, types: &expr_types };
2541
+ let body = compileFnBodyImpl(&stand_in, Some(ci), &ctx, &mut state, &uref)
2542
+ .map_err(|e| format!("{e} (in const '{name}')"))?;
2543
+ let slot = (*func_idx - module.func_import_count) as usize;
2544
+ module.functions[slot].1 = body;
2545
+ }
2546
+
2504
2547
  // Compile each closure literal's own body into its reserved function slot.
2505
2548
  let mut closure_bodies: Vec<(u32, Vec<u8>)> = Vec::new();
2506
2549
  for ((ui, ptr), info) in &ctx.closures {
@@ -3854,6 +3897,22 @@ impl<'a> Collector<'a> {
3854
3897
  }
3855
3898
  ast::AssignTarget::Field(object, _) => {
3856
3899
  self.walkExpr(object);
3900
+ // `p.pos.x = v` through a heap field copies each value-enum
3901
+ // link of the chain out into a scratch (see `compileFieldStore`).
3902
+ let mut cur: &ast::Expr = object;
3903
+ loop {
3904
+ let t = self.ty(cur);
3905
+ if !isValueTy(&t) {
3906
+ break;
3907
+ }
3908
+ self.bind(&rmwLocal(cur), t);
3909
+ match cur {
3910
+ ast::Expr::Attribute(at) if matches!(at.attr, ast::AttrKind::Field(_)) => {
3911
+ cur = &at.object
3912
+ }
3913
+ _ => break,
3914
+ }
3915
+ }
3857
3916
  }
3858
3917
  }
3859
3918
  }
@@ -4049,6 +4108,37 @@ fn compileFnBody(
4049
4108
  ctx: &CompileCtx,
4050
4109
  state: &mut ModuleState,
4051
4110
  unit: &UnitRef,
4111
+ ) -> Result<Vec<u8>, String> {
4112
+ compileFnBodyImpl(f, None, ctx, state, unit)
4113
+ }
4114
+
4115
+ /// A heap-valued top-level const (`TABLE = List(1, 2)`): its initializer expression
4116
+ /// (the ORIGINAL AST node, so the checker's per-expression tables apply), the const's
4117
+ /// type, and the mutable global caching the built value.
4118
+ struct ConstInit<'e> {
4119
+ expr: &'e ast::Expr,
4120
+ ty: PlumType,
4121
+ global: u32,
4122
+ }
4123
+
4124
+ /// Whether a const initializer builds a fresh object — compiled once into a cached
4125
+ /// global (see `ConstInit`) rather than inlined (re-evaluated) at every use.
4126
+ fn isBuiltConstInit(e: &ast::Expr) -> bool {
4127
+ match e {
4128
+ ast::Expr::ClassCall(_) | ast::Expr::FnCall(_) => true,
4129
+ ast::Expr::Attribute(a) => matches!(a.attr, ast::AttrKind::Method(_)),
4130
+ _ => false,
4131
+ }
4132
+ }
4133
+
4134
+ /// Compiles a function body. With `const_init`, `f` is a bodyless stand-in and the
4135
+ /// body is the lazily-run, cache-once initializer of that const, returning its value.
4136
+ fn compileFnBodyImpl(
4137
+ f: &ast::Fn,
4138
+ const_init: Option<&ConstInit>,
4139
+ ctx: &CompileCtx,
4140
+ state: &mut ModuleState,
4141
+ unit: &UnitRef,
4052
4142
  ) -> Result<Vec<u8>, String> {
4053
4143
  let mut body = Vec::new();
4054
4144
 
@@ -4078,7 +4168,9 @@ fn compileFnBody(
4078
4168
  variadic_for_scratch: HashMap::new(),
4079
4169
  next_variadic_for_slot: 0,
4080
4170
  };
4171
+ if let Some(ci) = const_init {
4172
+ collector.walkExpr(ci.expr);
4081
- if let ast::FnBody::Block(block) = &f.body {
4173
+ } else if let ast::FnBody::Block(block) = &f.body {
4082
4174
  collector.walkBlock(block);
4083
4175
  } else if let ast::FnBody::Expr(e) = &f.body {
4084
4176
  // An expression-bodied fn can still contain a closure literal (e.g.
@@ -4195,6 +4287,17 @@ fn compileFnBody(
4195
4287
  Some(plumTypeToValtype(&ret_decl))
4196
4288
  };
4197
4289
 
4290
+ if let Some(ci) = const_init {
4291
+ Instruction::GlobalGet(ci.global).encode(&mut body);
4292
+ Instruction::RefIsNull.encode(&mut body);
4293
+ Instruction::If(BlockType::Empty).encode(&mut body);
4294
+ compileExprInto(ci.expr, &ci.ty, &mut body, &local_ctx, state)?;
4295
+ Instruction::GlobalSet(ci.global).encode(&mut body);
4296
+ Instruction::End.encode(&mut body);
4297
+ Instruction::GlobalGet(ci.global).encode(&mut body);
4298
+ Instruction::End.encode(&mut body);
4299
+ return Ok(body);
4300
+ }
4198
4301
  match &f.body {
4199
4302
  ast::FnBody::Expr(e) => {
4200
4303
  compileExpr(e, &mut body, &local_ctx, state)?;
@@ -4393,6 +4496,161 @@ fn compileBlockAsFnBody(
4393
4496
  }
4394
4497
  }
4395
4498
 
4499
+ /// What `compileFieldStore` assigns: its type and a pusher for it (natural form —
4500
+ /// flat leaves for a value enum — converted for a destination of the given type).
4501
+ struct StoreSrc<'a> {
4502
+ ty: PlumType,
4503
+ push: &'a dyn Fn(&PlumType, &mut Vec<u8>, &LocalCtx, &mut ModuleState) -> Result<(), String>,
4504
+ }
4505
+
4506
+ /// The reserved scratch local holding a copy of the value-enum expression `e`
4507
+ /// while one of its fields is assigned (see `compileFieldStore`).
4508
+ fn rmwLocal(e: &ast::Expr) -> String {
4509
+ format!("rmw#{}", e as *const ast::Expr as usize)
4510
+ }
4511
+
4512
+ /// `object.field_name = src`. Heap objects get a `struct.set`. A field of a
4513
+ /// flattened value enum is assigned in the local's leaves when `object` is a local
4514
+ /// place; otherwise (`p.pos.x = 1.0` through a heap field) the value is copied out
4515
+ /// into a scratch local, the field set there, and the whole value stored back into
4516
+ /// the place it came from.
4517
+ fn compileFieldStore(
4518
+ object: &ast::Expr,
4519
+ field_name: &str,
4520
+ src: &StoreSrc,
4521
+ body: &mut Vec<u8>,
4522
+ ctx: &LocalCtx,
4523
+ state: &mut ModuleState,
4524
+ ) -> Result<(), String> {
4525
+ let obj_ty = inferLocalType(object, ctx);
4526
+ if let Some((off, fty)) = valueFieldRange(&obj_ty, field_name) {
4527
+ if let Some((base, _)) = valueLocalPlace(object, ctx) {
4528
+ (src.push)(&fty, body, ctx, state)?;
4529
+ emitSetLocals(base + off, &fty, body);
4530
+ return Ok(());
4531
+ }
4532
+ let ast::Expr::Attribute(a) = object else {
4533
+ return Err(format!(
4534
+ "codegen: cannot assign field '{}' of a value enum that is not a variable or field; assign the whole value instead",
4535
+ field_name
4536
+ ));
4537
+ };
4538
+ let ast::AttrKind::Field(parent_field) = &a.attr else {
4539
+ return Err(format!(
4540
+ "codegen: cannot assign field '{}' of a value enum returned by a call; assign the whole value instead",
4541
+ field_name
4542
+ ));
4543
+ };
4544
+ let tmp = *ctx
4545
+ .locals
4546
+ .get(&rmwLocal(object))
4547
+ .ok_or_else(|| "internal codegen error: missing value-field assignment scratch".to_string())?;
4548
+ compileExpr(object, body, ctx, state)?;
4549
+ emitSetLocals(tmp, &obj_ty, body);
4550
+ (src.push)(&fty, body, ctx, state)?;
4551
+ emitSetLocals(tmp + off, &fty, body);
4552
+ let whole = StoreSrc {
4553
+ ty: obj_ty.clone(),
4554
+ push: &|_, body, _, _| {
4555
+ emitGetLocals(tmp, &obj_ty, body);
4556
+ Ok(())
4557
+ },
4558
+ };
4559
+ return compileFieldStore(&a.object, parent_field, &whole, body, ctx, state);
4560
+ }
4561
+ let class_name = match plum_checker::nominalName(&obj_ty) {
4562
+ Some(n) => n.to_string(),
4563
+ None => {
4564
+ return Err(format!(
4565
+ "codegen: cannot assign field '{}' on non-class type {}",
4566
+ field_name, obj_ty
4567
+ ))
4568
+ }
4569
+ };
4570
+ // struct.set expects [(ref null $t) value] on the stack (ref
4571
+ // pushed first/deeper, value second/on top) — same push order
4572
+ // this already used for the old memory store. Mirrors the read
4573
+ // side's discriminant-enum-param / single-owning-variant
4574
+ // fallback chain (`AttrKind::Field` below) — this also covers a
4575
+ // record-shaped enum's own field (the `type X = ...`
4576
+ // replacement), whose one variant is trivially its own unique
4577
+ // owner of every field it declares.
4578
+ match ctx.enum_params.get(&class_name) {
4579
+ Some(params) => {
4580
+ let field_idx = params
4581
+ .iter()
4582
+ .position(|(n, _)| n == field_name)
4583
+ .ok_or_else(|| {
4584
+ format!(
4585
+ "codegen: no field '{}' on enum '{}'",
4586
+ field_name, class_name
4587
+ )
4588
+ })?;
4589
+ let super_type_idx = ctx
4590
+ .gc_types
4591
+ .superIdx(&obj_ty)
4592
+ .ok_or_else(|| {
4593
+ format!(
4594
+ "codegen: enum '{}' missing from the GC type registry",
4595
+ class_name
4596
+ )
4597
+ })?;
4598
+ let sub_s: Subst = ctx
4599
+ .enum_variants
4600
+ .values()
4601
+ .find(|i| i.enum_name == class_name)
4602
+ .map(|i| match &obj_ty {
4603
+ PlumType::TApp(_, a) => i.generics.iter().cloned().zip(a.iter().cloned()).collect(),
4604
+ _ => Subst::new(),
4605
+ })
4606
+ .unwrap_or_default();
4607
+ let fty = plum_checker::types::substitute(&params[field_idx].1, &sub_s);
4608
+ compileExpr(object, body, ctx, state)?;
4609
+ (src.push)(&fty, body, ctx, state)?;
4610
+ boxIfValue(&src.ty, body);
4611
+ Instruction::StructSet {
4612
+ struct_type_index: super_type_idx,
4613
+ field_index: field_idx as u32,
4614
+ }
4615
+ .encode(body);
4616
+ }
4617
+ None => {
4618
+ let (variant_name, info) = ctx
4619
+ .enum_variants
4620
+ .iter()
4621
+ .find(|(_, info)| {
4622
+ info.enum_name == class_name
4623
+ && info.field_names.iter().any(|n| n == field_name)
4624
+ })
4625
+ .ok_or_else(|| {
4626
+ format!(
4627
+ "codegen: no field '{}' on enum '{}'",
4628
+ field_name, class_name
4629
+ )
4630
+ })?;
4631
+ let field_idx = info
4632
+ .field_names
4633
+ .iter()
4634
+ .position(|n| n == field_name)
4635
+ .expect("just found by this field name");
4636
+ let variant_type_idx = ctx.gc_types.variantIdx(&obj_ty, variant_name)
4637
+ .ok_or_else(|| format!("codegen: variant '{}' of {} missing from the GC type registry", variant_name, obj_ty))?;
4638
+ let fty = plum_checker::variantFields(info, &obj_ty)[field_idx].clone();
4639
+ compileExpr(object, body, ctx, state)?;
4640
+ Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx))
4641
+ .encode(body);
4642
+ (src.push)(&fty, body, ctx, state)?;
4643
+ boxIfValue(&src.ty, body);
4644
+ Instruction::StructSet {
4645
+ struct_type_index: variant_type_idx,
4646
+ field_index: field_idx as u32,
4647
+ }
4648
+ .encode(body);
4649
+ }
4650
+ }
4651
+ Ok(())
4652
+ }
4653
+
4396
4654
  fn compileStmt(
4397
4655
  stmt: &ast::Stmt,
4398
4656
  body: &mut Vec<u8>,
@@ -4421,110 +4679,11 @@ fn compileStmt(
4421
4679
  .insert(name.clone(), TypeScheme::mono(vty));
4422
4680
  }
4423
4681
  ast::AssignTarget::Field(object, field_name) => {
4682
+ let src = StoreSrc {
4424
- let obj_ty = inferLocalType(object, ctx);
4683
+ ty: inferLocalType(value, ctx),
4425
- // A field of a flattened value enum: assign straight into the
4426
- // local copy's leaves (the original it was copied from is untouched).
4427
- if let Some((off, fty)) = valueFieldRange(&obj_ty, field_name) {
4428
- let (base, _) = valueLocalPlace(object, ctx).ok_or_else(|| {
4429
- format!(
4430
- "codegen: cannot assign field '{}' of a value enum that is not a local variable; assign the whole value instead",
4431
- field_name
4432
- )
4433
- })?;
4434
- compileExpr(value, body, ctx, state)?;
4684
+ push: &|dest, body, ctx, state| compileExprInto(value, dest, body, ctx, state),
4435
- emitSetLocals(base + off, &fty, body);
4436
- continue;
4437
- }
4438
- let class_name = match plum_checker::nominalName(&obj_ty) {
4439
- Some(n) => n.to_string(),
4440
- None => {
4441
- return Err(format!(
4442
- "codegen: cannot assign field '{}' on non-class type {}",
4443
- field_name, obj_ty
4444
- ))
4445
- }
4446
4685
  };
4447
- // struct.set expects [(ref null $t) value] on the stack (ref
4448
- // pushed first/deeper, value second/on top) — same push order
4449
- // this already used for the old memory store. Mirrors the read
4450
- // side's discriminant-enum-param / single-owning-variant
4451
- // fallback chain (`AttrKind::Field` below) — this also covers a
4452
- // record-shaped enum's own field (the `type X = ...`
4453
- // replacement), whose one variant is trivially its own unique
4454
- // owner of every field it declares.
4455
- match ctx.enum_params.get(&class_name) {
4456
- Some(params) => {
4457
- let field_idx = params
4458
- .iter()
4459
- .position(|(n, _)| n == field_name)
4460
- .ok_or_else(|| {
4461
- format!(
4462
- "codegen: no field '{}' on enum '{}'",
4463
- field_name, class_name
4464
- )
4465
- })?;
4466
- let super_type_idx = ctx
4467
- .gc_types
4468
- .superIdx(&obj_ty)
4469
- .ok_or_else(|| {
4470
- format!(
4471
- "codegen: enum '{}' missing from the GC type registry",
4472
- class_name
4473
- )
4474
- })?;
4475
- let sub_s: Subst = ctx
4476
- .enum_variants
4477
- .values()
4478
- .find(|i| i.enum_name == class_name)
4479
- .map(|i| match &obj_ty {
4480
- PlumType::TApp(_, a) => i.generics.iter().cloned().zip(a.iter().cloned()).collect(),
4481
- _ => Subst::new(),
4482
- })
4483
- .unwrap_or_default();
4484
- let fty = plum_checker::types::substitute(&params[field_idx].1, &sub_s);
4485
- compileExpr(object, body, ctx, state)?;
4486
- compileExprInto(value, &fty, body, ctx, state)?;
4686
+ compileFieldStore(object, field_name, &src, body, ctx, state)?;
4487
- boxIfValue(&inferLocalType(value, ctx), body);
4488
- Instruction::StructSet {
4489
- struct_type_index: super_type_idx,
4490
- field_index: field_idx as u32,
4491
- }
4492
- .encode(body);
4493
- }
4494
- None => {
4495
- let (variant_name, info) = ctx
4496
- .enum_variants
4497
- .iter()
4498
- .find(|(_, info)| {
4499
- info.enum_name == class_name
4500
- && info.field_names.iter().any(|n| n == field_name)
4501
- })
4502
- .ok_or_else(|| {
4503
- format!(
4504
- "codegen: no field '{}' on enum '{}'",
4505
- field_name, class_name
4506
- )
4507
- })?;
4508
- let field_idx = info
4509
- .field_names
4510
- .iter()
4511
- .position(|n| n == field_name)
4512
- .expect("just found by this field name");
4513
- let variant_type_idx = ctx.gc_types.variantIdx(&obj_ty, variant_name)
4514
- .ok_or_else(|| format!("codegen: variant '{}' of {} missing from the GC type registry", variant_name, obj_ty))?;
4515
- let fty = plum_checker::variantFields(info, &obj_ty)[field_idx].clone();
4516
- compileExpr(object, body, ctx, state)?;
4517
- Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx))
4518
- .encode(body);
4519
- compileExprInto(value, &fty, body, ctx, state)?;
4520
- boxIfValue(&inferLocalType(value, ctx), body);
4521
- Instruction::StructSet {
4522
- struct_type_index: variant_type_idx,
4523
- field_index: field_idx as u32,
4524
- }
4525
- .encode(body);
4526
- }
4527
- }
4528
4687
  }
4529
4688
  }
4530
4689
  }
@@ -5472,6 +5631,7 @@ fn compileVariantConstructorArm(
5472
5631
  0,
5473
5632
  scratch_local,
5474
5633
  if name == "Ok" { NULLABLE_OK } else { NULLABLE_ERR },
5634
+ None,
5475
5635
  rest,
5476
5636
  all_subjects,
5477
5637
  result_vt,
@@ -5507,12 +5667,11 @@ fn compileVariantConstructorArm(
5507
5667
  // Payload types instantiated by the subject's type arguments.
5508
5668
  let inst_types = instFields(info, &sty);
5509
5669
  let field_types = inst_types.clone();
5510
- if isValueTy(&sty) && inst_types.iter().any(isValueTy) {
5511
- return Err(format!(
5512
- "codegen: cannot destructure nested value-enum fields in a pattern for '{}'; bind the whole value and use its fields",
5670
+ // A value enum's box struct holds its fields FLATTENED into leaves, so field `i`
5671
+ // of the pattern lives at leaf `value_leaf_map[i]` (nested value fields span several).
5672
+ let value_leaf_map: Option<Vec<u32>> = valueFieldsOf(&sty).map(|fs| {
5513
- name
5673
+ fs.iter().map(|(n, _)| valueFieldRange(&sty, n).map(|(o, _)| o).unwrap_or(0)).collect()
5514
- ));
5674
+ });
5515
- }
5516
5675
 
5517
5676
  // This pattern's own narrowly-typed scratch local (declared with variant's exact
5518
5677
  // concrete ref type — see `nested_class_scratch_types`), distinct from
@@ -5543,6 +5702,7 @@ fn compileVariantConstructorArm(
5543
5702
  0,
5544
5703
  narrow_local,
5545
5704
  variant_idx,
5705
+ value_leaf_map.as_deref(),
5546
5706
  rest,
5547
5707
  all_subjects,
5548
5708
  result_vt,
@@ -5592,6 +5752,7 @@ fn emitContainerField(
5592
5752
  container_local: u32,
5593
5753
  container_type_idx: u32,
5594
5754
  fpos: usize,
5755
+ leaf_map: Option<&[u32]>,
5595
5756
  ) {
5596
5757
  match container_type_idx {
5597
5758
  NULLABLE_OK => Instruction::LocalGet(container_local).encode(body),
@@ -5600,7 +5761,7 @@ fn emitContainerField(
5600
5761
  Instruction::LocalGet(container_local).encode(body);
5601
5762
  Instruction::StructGet {
5602
5763
  struct_type_index: idx,
5603
- field_index: fpos as u32,
5764
+ field_index: leaf_map.map(|m| m[fpos]).unwrap_or(fpos as u32),
5604
5765
  }
5605
5766
  .encode(body);
5606
5767
  }
@@ -5622,6 +5783,7 @@ fn compileFieldPatterns(
5622
5783
  fpos: usize,
5623
5784
  container_local: u32,
5624
5785
  container_type_idx: u32,
5786
+ leaf_map: Option<&[u32]>,
5625
5787
  rest: &[ast::Case],
5626
5788
  all_subjects: &[(ValType, u32)],
5627
5789
  result_vt: Option<ValType>,
@@ -5646,7 +5808,7 @@ fn compileFieldPatterns(
5646
5808
  pat,
5647
5809
  tags,
5648
5810
  &|b: &mut Vec<u8>| {
5649
- emitContainerField(b, ctx, container_local, container_type_idx, fpos);
5811
+ emitContainerField(b, ctx, container_local, container_type_idx, fpos, leaf_map);
5650
5812
  },
5651
5813
  result_vt,
5652
5814
  body,
@@ -5660,6 +5822,7 @@ fn compileFieldPatterns(
5660
5822
  fpos + 1,
5661
5823
  container_local,
5662
5824
  container_type_idx,
5825
+ leaf_map,
5663
5826
  rest,
5664
5827
  all_subjects,
5665
5828
  result_vt,
@@ -5693,6 +5856,7 @@ fn compileFieldPatterns(
5693
5856
  fpos + 1,
5694
5857
  container_local,
5695
5858
  container_type_idx,
5859
+ leaf_map,
5696
5860
  rest,
5697
5861
  all_subjects,
5698
5862
  result_vt,
@@ -5704,7 +5868,7 @@ fn compileFieldPatterns(
5704
5868
  ),
5705
5869
  // A nested `True`/`False` against a `Bool` field: i32 compare.
5706
5870
  ast::CasePattern::Name(n) if n == "True" || n == "False" => {
5707
- emitContainerField(body, ctx, container_local, container_type_idx, fpos);
5871
+ emitContainerField(body, ctx, container_local, container_type_idx, fpos, leaf_map);
5708
5872
  if n == "False" {
5709
5873
  Instruction::I32Eqz.encode(body);
5710
5874
  }
@@ -5716,6 +5880,7 @@ fn compileFieldPatterns(
5716
5880
  fpos + 1,
5717
5881
  container_local,
5718
5882
  container_type_idx,
5883
+ leaf_map,
5719
5884
  rest,
5720
5885
  all_subjects,
5721
5886
  result_vt,
@@ -5748,12 +5913,22 @@ fn compileFieldPatterns(
5748
5913
  ctx.locals.get(n).copied().ok_or_else(|| {
5749
5914
  format!("internal codegen error: missing binding local '{}'", n)
5750
5915
  })?;
5751
- emitContainerField(body, ctx, container_local, container_type_idx, fpos);
5916
+ if let (true, Some(m)) = (isValueTy(field_ty), leaf_map) {
5917
+ // Flattened container: the nested value's leaves are consecutive fields.
5752
- if isValueTy(field_ty) {
5918
+ for j in 0..leafCount(field_ty) {
5919
+ Instruction::LocalGet(container_local).encode(body);
5920
+ Instruction::StructGet { struct_type_index: container_type_idx, field_index: m[fpos] + j }
5753
- emitUnbox(field_ty, body);
5921
+ .encode(body);
5922
+ }
5754
5923
  emitSetLocals(idx, field_ty, body);
5755
5924
  } else {
5925
+ emitContainerField(body, ctx, container_local, container_type_idx, fpos, leaf_map);
5926
+ if isValueTy(field_ty) {
5927
+ emitUnbox(field_ty, body);
5928
+ emitSetLocals(idx, field_ty, body);
5929
+ } else {
5756
- Instruction::LocalSet(idx).encode(body);
5930
+ Instruction::LocalSet(idx).encode(body);
5931
+ }
5757
5932
  }
5758
5933
  ctx.type_env
5759
5934
  .borrow_mut()
@@ -5765,6 +5940,7 @@ fn compileFieldPatterns(
5765
5940
  fpos + 1,
5766
5941
  container_local,
5767
5942
  container_type_idx,
5943
+ leaf_map,
5768
5944
  rest,
5769
5945
  all_subjects,
5770
5946
  result_vt,
@@ -5790,7 +5966,7 @@ fn compileFieldPatterns(
5790
5966
  let variant_idx = ctx.gc_types.variantIdx(field_ty, n).ok_or_else(|| {
5791
5967
  format!("codegen: variant '{}' of {} missing from the GC type registry", n, field_ty)
5792
5968
  })?;
5793
- emitContainerField(body, ctx, container_local, container_type_idx, fpos);
5969
+ emitContainerField(body, ctx, container_local, container_type_idx, fpos, leaf_map);
5794
5970
  Instruction::RefTestNonNull(HeapType::Concrete(variant_idx)).encode(body);
5795
5971
  Instruction::If(blockTypeFor(result_vt)).encode(body);
5796
5972
  compileFieldPatterns(
@@ -5800,6 +5976,7 @@ fn compileFieldPatterns(
5800
5976
  fpos + 1,
5801
5977
  container_local,
5802
5978
  container_type_idx,
5979
+ leaf_map,
5803
5980
  rest,
5804
5981
  all_subjects,
5805
5982
  result_vt,
@@ -5826,7 +6003,7 @@ fn compileFieldPatterns(
5826
6003
  if inst_vt != ValType::I64 {
5827
6004
  return Err("codegen: integer match pattern against a non-Int field".to_string());
5828
6005
  }
5829
- emitContainerField(body, ctx, container_local, container_type_idx, fpos);
6006
+ emitContainerField(body, ctx, container_local, container_type_idx, fpos, leaf_map);
5830
6007
  Instruction::I64Const(*n).encode(body);
5831
6008
  Instruction::I64Eq.encode(body);
5832
6009
  Instruction::If(blockTypeFor(result_vt)).encode(body);
@@ -5837,6 +6014,7 @@ fn compileFieldPatterns(
5837
6014
  fpos + 1,
5838
6015
  container_local,
5839
6016
  container_type_idx,
6017
+ leaf_map,
5840
6018
  rest,
5841
6019
  all_subjects,
5842
6020
  result_vt,
@@ -5887,6 +6065,71 @@ fn compileFieldPatterns(
5887
6065
  inner_fields.len()
5888
6066
  ));
5889
6067
  }
6068
+ // A nested VALUE-enum field (`Rect(Vec2(px, py), size)`): there is exactly
6069
+ // one constructor, so nothing to test — destructure its leaves in place.
6070
+ // Inside a flattened (value) container they are consecutive fields of the
6071
+ // same struct; inside a heap container the field holds the value's box.
6072
+ if let Some(vfields) = valueFieldsOf(field_ty) {
6073
+ let box_idx = ctx.gc_types.classIdx(field_ty).ok_or_else(|| {
6074
+ format!("codegen: value enum '{}' missing from the GC type registry", field_ty)
6075
+ })?;
6076
+ let (inner_container, inner_idx, base, prelude_nested) = match leaf_map {
6077
+ Some(m) => (container_local, container_type_idx, m[fpos], false),
6078
+ None => {
6079
+ let key = pat as *const ast::CasePattern as usize;
6080
+ let slot = *ctx.nested_class_scratch.get(&key).ok_or_else(|| {
6081
+ "internal codegen error: missing nested constructor pattern scratch slot"
6082
+ .to_string()
6083
+ })?;
6084
+ (ctx.nested_class_scratch_base + slot, box_idx, 0, true)
6085
+ }
6086
+ };
6087
+ if prelude_nested {
6088
+ emitContainerField(body, ctx, container_local, container_type_idx, fpos, leaf_map);
6089
+ Instruction::RefCastNonNull(HeapType::Concrete(box_idx)).encode(body);
6090
+ Instruction::LocalSet(inner_container).encode(body);
6091
+ }
6092
+ let inner_map: Vec<u32> = vfields
6093
+ .iter()
6094
+ .map(|(n, _)| base + valueFieldRange(field_ty, n).map(|(o, _)| o).unwrap_or(0))
6095
+ .collect();
6096
+ let inner_types: Vec<PlumType> = vfields.iter().map(|(_, t)| t.clone()).collect();
6097
+ return compileFieldPatterns(
6098
+ inner_fields,
6099
+ &inner_types,
6100
+ &inner_types,
6101
+ 0,
6102
+ inner_container,
6103
+ inner_idx,
6104
+ Some(&inner_map),
6105
+ rest,
6106
+ all_subjects,
6107
+ result_vt,
6108
+ exhaustive_fallback,
6109
+ body,
6110
+ ctx,
6111
+ state,
6112
+ &mut |body, state| {
6113
+ compileFieldPatterns(
6114
+ fields,
6115
+ field_types,
6116
+ inst_types,
6117
+ fpos + 1,
6118
+ container_local,
6119
+ container_type_idx,
6120
+ leaf_map,
6121
+ rest,
6122
+ all_subjects,
6123
+ result_vt,
6124
+ exhaustive_fallback,
6125
+ body,
6126
+ ctx,
6127
+ state,
6128
+ on_match,
6129
+ )
6130
+ },
6131
+ );
6132
+ }
5890
6133
  let inner_variant_idx = ctx.gc_types.variantIdx(field_ty, name).ok_or_else(|| {
5891
6134
  format!(
5892
6135
  "codegen: variant '{}' of {} missing from the GC type registry",
@@ -5902,11 +6145,11 @@ fn compileFieldPatterns(
5902
6145
  let inner_inst_types = instFields(info, field_ty);
5903
6146
  let inner_field_types = inner_inst_types.clone();
5904
6147
 
5905
- emitContainerField(body, ctx, container_local, container_type_idx, fpos);
6148
+ emitContainerField(body, ctx, container_local, container_type_idx, fpos, leaf_map);
5906
6149
  Instruction::RefTestNonNull(HeapType::Concrete(inner_variant_idx)).encode(body);
5907
6150
  Instruction::If(blockTypeFor(result_vt)).encode(body);
5908
6151
  // Narrow into `nested_local` now that ref.test just above proved it's safe.
5909
- emitContainerField(body, ctx, container_local, container_type_idx, fpos);
6152
+ emitContainerField(body, ctx, container_local, container_type_idx, fpos, leaf_map);
5910
6153
  Instruction::RefCastNonNull(HeapType::Concrete(inner_variant_idx)).encode(body);
5911
6154
  Instruction::LocalSet(nested_local).encode(body);
5912
6155
  compileFieldPatterns(
@@ -5916,6 +6159,7 @@ fn compileFieldPatterns(
5916
6159
  0,
5917
6160
  nested_local,
5918
6161
  inner_variant_idx,
6162
+ None,
5919
6163
  rest,
5920
6164
  all_subjects,
5921
6165
  result_vt,
@@ -5931,6 +6175,7 @@ fn compileFieldPatterns(
5931
6175
  fpos + 1,
5932
6176
  container_local,
5933
6177
  container_type_idx,
6178
+ leaf_map,
5934
6179
  rest,
5935
6180
  all_subjects,
5936
6181
  result_vt,
@@ -6772,6 +7017,9 @@ fn compileExprCore(
6772
7017
  None => {
6773
7018
  let const_value = CURRENT_CONSTS.with(|c| c.borrow().get(n).cloned());
6774
7019
  match const_value {
7020
+ Some(_) if ctx.func_ids.contains_key(&format!("const#{n}")) => {
7021
+ Instruction::Call(ctx.func_ids[&format!("const#{n}")]).encode(body);
7022
+ }
6775
7023
  Some(value) => compileExpr(&value, body, ctx, state)?,
6776
7024
  None if n
6777
7025
  .chars()
@@ -7190,13 +7438,7 @@ fn compileMethodCall(
7190
7438
  if !ctx.func_ids.contains_key(&key) && ctx.locals.contains_key(&field_local) {
7191
7439
  compileFieldRead(object, &call.name, obj_ty, result_expr, body, ctx, state)?;
7192
7440
  Instruction::LocalSet(ctx.locals[&field_local]).encode(body);
7193
- let field_call = ast::FnCall {
7194
- name: field_local,
7195
- args: call.args.clone(),
7196
- line: call.line,
7197
- column: call.column,
7198
- };
7199
- return compileClosureCall(&field_call, result_expr, body, ctx, state);
7441
+ return compileClosureCallArgs(&field_local, &call.args, body, ctx, state);
7200
7442
  }
7201
7443
  let func_idx = *ctx
7202
7444
  .func_ids
@@ -7534,20 +7776,33 @@ fn compileClosureCall(
7534
7776
  body: &mut Vec<u8>,
7535
7777
  ctx: &LocalCtx,
7536
7778
  state: &mut ModuleState,
7779
+ ) -> Result<(), String> {
7780
+ compileClosureCallArgs(&call.name, &call.args, body, ctx, state)
7781
+ }
7782
+
7783
+ /// `compileClosureCall` over the local `name` with explicit `args` — the args must be
7784
+ /// the ORIGINAL AST nodes (not clones): the checker's per-call tables are keyed by
7785
+ /// expression address, so a cloned arg containing a call would not be found.
7786
+ fn compileClosureCallArgs(
7787
+ name: &str,
7788
+ args: &[ast::Arg],
7789
+ body: &mut Vec<u8>,
7790
+ ctx: &LocalCtx,
7791
+ state: &mut ModuleState,
7537
7792
  ) -> Result<(), String> {
7538
7793
  let closure_local = *ctx
7539
7794
  .locals
7540
- .get(&call.name)
7795
+ .get(name)
7541
- .ok_or_else(|| format!("codegen: closure '{}' is not a local", call.name))?;
7796
+ .ok_or_else(|| format!("codegen: closure '{}' is not a local", name))?;
7542
7797
  let closure_type_idx = ctx.gc_types.closure_type_idx;
7543
- let Some(PlumType::TFun(ps, r)) = ctx.local_decl_types.get(&call.name) else {
7798
+ let Some(PlumType::TFun(ps, r)) = ctx.local_decl_types.get(name) else {
7544
- return Err(format!("codegen: '{}' is not a function-typed local", call.name));
7799
+ return Err(format!("codegen: '{}' is not a function-typed local", name));
7545
7800
  };
7546
7801
  let key = closureSigKey(ps, r, ctx.gc_types);
7547
7802
  let type_index = *ctx.closure_call_types.get(&key).ok_or_else(|| {
7548
7803
  format!(
7549
7804
  "internal codegen error: no call_indirect type for closure '{}' of type fn{:?}",
7550
- call.name, key
7805
+ name, key
7551
7806
  )
7552
7807
  })?;
7553
7808
 
@@ -7558,7 +7813,7 @@ fn compileClosureCall(
7558
7813
  field_index: 1,
7559
7814
  }
7560
7815
  .encode(body);
7561
- for (i, arg) in call.args.iter().enumerate() {
7816
+ for (i, arg) in args.iter().enumerate() {
7562
7817
  match ps.get(i) {
7563
7818
  Some(p) => compileExprInto(argExprOf(arg), p, body, ctx, state)?,
7564
7819
  None => compileExpr(argExprOf(arg), body, ctx, state)?,
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -2382,3 +2382,74 @@ fn extern_value_multi_value() {
2382
2382
  let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
2383
2383
  assert_eq!(main.call(&mut store, ()).unwrap(), 1);
2384
2384
  }
2385
+
2386
+ // ---- fixes: nested call as fn-field arg, collection consts, value-enum patterns/assign ----
2387
+
2388
+ #[test]
2389
+ fn fn_field_nested_call_arg() {
2390
+ let src = format!("{}\
2391
+ import std/Array
2392
+
2393
+ enum Keyed[T] =
2394
+ | Keyed(items: List[T], key: fn(T) -> Int)
2395
+
2396
+ fun first(self) -> Int =
2397
+ self.key(self.items.data.get(0))
2398
+
2399
+ fun main() -> Int =
2400
+ k := Keyed[Int](items: List(5, 6), key: |n| n * 8)
2401
+ k.first() + 2
2402
+ ", STD_HEAD);
2403
+ assert_eq!(runMerged::<i64>("fnfieldnested", &src), 42);
2404
+ }
2405
+
2406
+ #[test]
2407
+ fn const_list_table() {
2408
+ let src = format!("{}\
2409
+ TABLE = List(10, 20, 30)
2410
+
2411
+ fun total() -> Int =
2412
+ TABLE.reduce(0, |a, b| a + b)
2413
+
2414
+ fun main() -> Int =
2415
+ TABLE.push(4)
2416
+ TABLE.get(1).unwrap() + TABLE.length() + total() - 78 + 4
2417
+ ", STD_HEAD);
2418
+ // TABLE is a single shared object, initialised once: push(4) persists, so
2419
+ // total() sees 64 (60 + 4), length 4, get(1) 20 -> 20 + 4 + 64 - 78 + 4 = 14
2420
+ assert_eq!(runMerged::<i64>("constlist", &src), 14);
2421
+ }
2422
+
2423
+ #[test]
2424
+ fn value_nested_pattern() {
2425
+ let src = format!(
2426
+ "{VEC2}value enum Rect =\n | Rect(pos: Vec2, size: Vec2)\n\nfun f(r: Rect) -> Float =\n match r\n Rect(Vec2(px, py), size) => px + py * 10.0 + size.x * 100.0 + size.y * 1000.0\n\nfun g(v: Vec2) -> Float =\n match v\n Vec2(a, b) => a - b\n\nfun main() -> Int =\n r := Rect(pos: Vec2(x: 1.0, y: 2.0), size: Vec2(x: 3.0, y: 4.0))\n if f(r) == 4321.0 && g(r.pos) == 0.0 - 1.0\n return 1\n return 0\n"
2427
+ );
2428
+ assert_eq!(runMerged::<i64>("value_nested_pattern", &src), 1);
2429
+ }
2430
+
2431
+ #[test]
2432
+ fn local_value_subfield_assign() {
2433
+ let src = format!(
2434
+ "{VEC2}value enum Rect =\n | Rect(pos: Vec2, size: Vec2)\n\nfun main() -> Int =\n r := Rect(pos: Vec2(x: 1.0, y: 2.0), size: Vec2(x: 3.0, y: 4.0))\n r.pos.x = 7.0\n r.size.y = 9.0\n if r.pos.x == 7.0 && r.pos.y == 2.0 && r.size.x == 3.0 && r.size.y == 9.0\n return 1\n return 0\n"
2435
+ );
2436
+ assert_eq!(runMerged::<i64>("local_value_subfield_assign", &src), 1);
2437
+ }
2438
+
2439
+ #[test]
2440
+ fn heap_field_value_subfield_assign() {
2441
+ let src = format!(
2442
+ "{VEC2}enum Player =\n | Player(pos: Vec2, hp: Int)\n\nvalue enum Rect =\n | Rect(pos: Vec2, size: Vec2)\n\nenum Body =\n | Body(bounds: Rect, id: Int)\n\nfun main() -> Int =\n p := Player(pos: Vec2(x: 1.0, y: 2.0), hp: 5)\n p.pos.x = 8.0\n b := Body(bounds: Rect(pos: Vec2(x: 1.0, y: 2.0), size: Vec2(x: 3.0, y: 4.0)), id: 1)\n b.bounds.size.y = 6.0\n if p.pos.x == 8.0 && p.pos.y == 2.0 && p.hp == 5 && b.bounds.size.y == 6.0 && b.bounds.size.x == 3.0 && b.bounds.pos.y == 2.0\n return 1\n return 0\n"
2443
+ );
2444
+ assert_eq!(runMerged::<i64>("heap_field_value_subfield_assign", &src), 1);
2445
+ }
2446
+
2447
+ #[test]
2448
+ fn value_nested_pattern_mixed() {
2449
+ // Whole-value binding next to a nested destructure, and a heap container
2450
+ // (class-like enum) holding a value-enum field.
2451
+ let src = format!(
2452
+ "{VEC2}value enum Rect =\n | Rect(pos: Vec2, size: Vec2)\n\nenum Player =\n | Player(pos: Vec2, hp: Float)\n\nfun f(r: Rect) -> Float =\n match r\n Rect(p, Vec2(w, h)) => p.x + p.y * 10.0 + w * 100.0 + h * 1000.0\n\nfun g(p: Player) -> Float =\n match p\n Player(Vec2(x, y), hp) => x + y * 10.0 + hp * 100.0\n\nfun main() -> Int =\n r := Rect(pos: Vec2(x: 1.0, y: 2.0), size: Vec2(x: 3.0, y: 4.0))\n p := Player(pos: Vec2(x: 1.0, y: 2.0), hp: 3.0)\n if f(r) == 4321.0 && g(p) == 321.0\n return 1\n return 0\n"
2453
+ );
2454
+ assert_eq!(runMerged::<i64>("value_nested_pattern_mixed", &src), 1);
2455
+ }