plum

#treesitter#compiler#wasm

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

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


d2640d2Peter John 2026-08-09T19:04:08+05:30
feat(plum-wasm-codegen): compile enum discriminant-value construction and field access
plum-checker/tests/checker_tests.rs CHANGED
@@ -115,6 +115,24 @@ fn method_self_unknown_field_is_error() {
115
115
  assert!(result.is_err());
116
116
  }
117
117
 
118
+ #[test]
119
+ fn nested_method_type_checks_identically_to_top_level_receiver_form() {
120
+ let nested_src = "type Cat =\n name: Str\n age: Int\n\n fun getName(self) -> Str =\n self.name\n";
121
+ let top_level_src = "type Cat =\n name: Str\n age: Int\n\nfun getName<Cat>() -> Str =\n self.name\n";
122
+ let nested_result = check_source(&parse(nested_src));
123
+ let top_level_result = check_source(&parse(top_level_src));
124
+ assert!(nested_result.is_ok(), "expected Ok, got {:?}", nested_result.err());
125
+ assert!(top_level_result.is_ok(), "expected Ok, got {:?}", top_level_result.err());
126
+ }
127
+
128
+ #[test]
129
+ fn nested_method_unknown_field_is_error_identically_to_top_level_receiver_form() {
130
+ let nested_src = "type Cat =\n name: Str\n\n fun getAge(self) -> Int =\n self.age\n";
131
+ let top_level_src = "type Cat =\n name: Str\n\nfun getAge<Cat>() -> Int =\n self.age\n";
132
+ assert!(check_source(&parse(nested_src)).is_err());
133
+ assert!(check_source(&parse(top_level_src)).is_err());
134
+ }
135
+
118
136
  #[test]
119
137
  fn self_outside_method_is_error() {
120
138
  let src = "fun bad() -> Int =\n self\n";
plum-wasm-codegen/src/lib.rs CHANGED
@@ -1746,12 +1746,25 @@ impl<'a> Collector<'a> {
1746
1746
  }
1747
1747
  }
1748
1748
  }
1749
+ ast::Expr::TypeName(n) => {
1750
+ // A bare discriminant-variant reference (e.g. `READ_MIN_OCCURANCES`) heap-
1751
+ // allocates via the SAME `compile_variant_construction` path a payload-carrying
1752
+ // `FnCall` variant construction uses (see the `Expr::TypeName` arm in
1753
+ // `compile_expr`), so it needs the same scratch slot registered here.
1754
+ let carries_baked_in_payload = self.cctx.enum_variants.get(n)
1755
+ .map(|info| !info.values.is_empty())
1756
+ .unwrap_or(false);
1757
+ if carries_baked_in_payload {
1758
+ let idx = self.next_classcall_slot;
1759
+ self.next_classcall_slot += 1;
1760
+ self.classcall_scratch.insert(expr as *const ast::Expr as usize, idx);
1761
+ }
1762
+ }
1749
1763
  ast::Expr::Int(_)
1750
1764
  | ast::Expr::Float(_)
1751
1765
  | ast::Expr::String(_)
1752
1766
  | ast::Expr::Self_
1753
- | ast::Expr::Var(_)
1754
- | ast::Expr::TypeName(_) => {}
1767
+ | ast::Expr::Var(_) => {}
1755
1768
  // A closure literal needs two construction scratch slots in the *enclosing*
1756
1769
  // function (env struct ptr, closure struct ptr). Its body's own locals are
1757
1770
  // NOT this function's — they belong to the separate closure function — so we
@@ -2524,6 +2537,12 @@ fn compile_variant_eq_arm(
2524
2537
  }
2525
2538
  let tag = info.tag;
2526
2539
  Instruction::LocalGet(scratch_local).encode(body);
2540
+ if !info.field_types.is_empty() {
2541
+ // A payload-carrying variant's scratch local holds a heap pointer, not the
2542
+ // tag itself (unlike a payload-free variant, which stores the tag directly
2543
+ // as its runtime value) — dereference slot 0 to compare against the tag.
2544
+ emit_load(ValType::I32, 0, body);
2545
+ }
2527
2546
  Instruction::I32Const(tag).encode(body);
2528
2547
  Instruction::I32Eq.encode(body);
2529
2548
  Instruction::If(block_type_for(result_vt)).encode(body);
@@ -2652,6 +2671,11 @@ fn compile_field_patterns(
2652
2671
  }
2653
2672
  Instruction::LocalGet(container_local).encode(body);
2654
2673
  emit_load(field_vt, offset, body);
2674
+ if !info.field_types.is_empty() {
2675
+ // Payload-carrying nested variant: the just-loaded field is a heap
2676
+ // pointer, not the tag — dereference slot 0 to compare against it.
2677
+ emit_load(ValType::I32, 0, body);
2678
+ }
2655
2679
  Instruction::I32Const(info.tag).encode(body);
2656
2680
  Instruction::I32Eq.encode(body);
2657
2681
  Instruction::If(block_type_for(result_vt)).encode(body);
@@ -2938,6 +2962,16 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
2938
2962
  Some(info) if info.field_types.is_empty() => {
2939
2963
  Instruction::I32Const(info.tag).encode(body);
2940
2964
  }
2965
+ Some(info) if !info.values.is_empty() => {
2966
+ // A discriminant variant's own declared literal values ARE its
2967
+ // construction arguments — there is no call site to take them from, so
2968
+ // build one synthetically and reuse the existing payload-variant path.
2969
+ let synthetic_call = ast::FnCall {
2970
+ name: n.clone(),
2971
+ args: info.values.iter().cloned().map(ast::Arg::Positional).collect(),
2972
+ };
2973
+ compile_variant_construction(info, &synthetic_call, expr, body, ctx, state)?;
2974
+ }
2941
2975
  Some(_) => return Err(format!("codegen: '{}' carries a payload — construct it with '{}(...)'", n, n)),
2942
2976
  None => return Err(format!("codegen: type name '{}' is not yet supported as a value", n)),
2943
2977
  },
@@ -2990,22 +3024,43 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
2990
3024
  PlumType::TNamed(n) => n.clone(),
2991
3025
  other => return Err(format!("codegen: cannot access field '{}' on non-class type {}", field_name, other)),
2992
3026
  };
3027
+ match ctx.classes.get(&class_name) {
3028
+ Some(fields) => {
3029
+ let (field_idx, field_ty) = fields
3030
+ .iter()
3031
+ .position(|(n, _)| n == field_name)
3032
+ .map(|i| (i, fields[i].1.clone()))
3033
+ .ok_or_else(|| format!("codegen: no field '{}' on class '{}'", field_name, class_name))?;
3034
+ compile_expr(&attr.object, body, ctx, state)?;
3035
+ let offset = (field_idx as u64) * 8;
3036
+ match plum_type_to_valtype(&field_ty) {
3037
+ ValType::I64 => Instruction::I64Load(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
3038
+ ValType::F64 => Instruction::F64Load(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
3039
+ _ => Instruction::I32Load(MemArg { offset, align: 2, memory_index: 0 }).encode(body),
3040
+ };
3041
+ }
3042
+ // Not a class: fall back to a discriminant enum's shared params.
3043
+ // Slot 0 is always the variant's tag, so field N lives at slot N+1 —
3044
+ // NOT the same offset a class field of the same index would use.
3045
+ None => {
2993
- let fields = ctx
3046
+ let params = ctx
2994
- .classes
3047
+ .enum_params
2995
- .get(&class_name)
3048
+ .get(&class_name)
2996
- .ok_or_else(|| format!("codegen: unknown class '{}'", class_name))?;
3049
+ .ok_or_else(|| format!("codegen: unknown class '{}'", class_name))?;
2997
- let (field_idx, field_ty) = fields
3050
+ let (field_idx, field_ty) = params
2998
- .iter()
3051
+ .iter()
2999
- .position(|(n, _)| n == field_name)
3052
+ .position(|(n, _)| n == field_name)
3000
- .map(|i| (i, fields[i].1.clone()))
3053
+ .map(|i| (i, params[i].1.clone()))
3001
- .ok_or_else(|| format!("codegen: no field '{}' on class '{}'", field_name, class_name))?;
3054
+ .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
3002
- compile_expr(&attr.object, body, ctx, state)?;
3055
+ compile_expr(&attr.object, body, ctx, state)?;
3003
- let offset = (field_idx as u64) * 8;
3056
+ let offset = ((field_idx + 1) as u64) * 8;
3004
- match plum_type_to_valtype(&field_ty) {
3057
+ match plum_type_to_valtype(&field_ty) {
3005
- ValType::I64 => Instruction::I64Load(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
3058
+ ValType::I64 => Instruction::I64Load(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
3006
- ValType::F64 => Instruction::F64Load(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
3059
+ ValType::F64 => Instruction::F64Load(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
3007
- _ => Instruction::I32Load(MemArg { offset, align: 2, memory_index: 0 }).encode(body),
3060
+ _ => Instruction::I32Load(MemArg { offset, align: 2, memory_index: 0 }).encode(body),
3008
- };
3061
+ };
3062
+ }
3063
+ }
3009
3064
  }
3010
3065
  ast::AttrKind::Method(call) => {
3011
3066
  let class_name = match &obj_ty {
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -403,6 +403,63 @@ fun main() -> Int =
403
403
  assert_eq!(run_main(&bytes), 7);
404
404
  }
405
405
 
406
+ #[test]
407
+ fn nested_method_declaration_runs_correctly() {
408
+ let src = "\
409
+ type Cat =
410
+ name: Str
411
+ age: Int
412
+
413
+ fun getAge(self) -> Int =
414
+ self.age
415
+
416
+ fun main() -> Int =
417
+ c = Cat(name: \"x\", age: 7)
418
+ c.getAge()
419
+ ";
420
+ let source = parse(src);
421
+ let bytes = compile_source(&source).expect("compile failed");
422
+ assert_eq!(run_main(&bytes), 7);
423
+ }
424
+
425
+ #[test]
426
+ fn enum_discriminant_value_field_access_runs_correctly_for_each_variant() {
427
+ let src = "\
428
+ enum Step(n: Int) =
429
+ | ReadMin(10)
430
+ | ReadMax(20)
431
+
432
+ fun toNumber(self) -> Int =
433
+ self.n
434
+
435
+ fun main() -> Int =
436
+ ReadMin.toNumber() * 100 + ReadMax.toNumber()
437
+ ";
438
+ let source = parse(src);
439
+ let bytes = compile_source(&source).expect("compile failed");
440
+ assert_eq!(run_main(&bytes), 1020);
441
+ }
442
+
443
+ #[test]
444
+ fn enum_discriminant_value_matches_by_variant_name_correctly() {
445
+ let src = "\
446
+ enum Step(n: Int) =
447
+ | ReadMin(10)
448
+ | ReadMax(20)
449
+
450
+ fun toNumber(s: Step) -> Int =
451
+ match s
452
+ ReadMin => 1
453
+ ReadMax => 2
454
+
455
+ fun main() -> Int =
456
+ toNumber(ReadMin) * 10 + toNumber(ReadMax)
457
+ ";
458
+ let source = parse(src);
459
+ let bytes = compile_source(&source).expect("compile failed");
460
+ assert_eq!(run_main(&bytes), 12);
461
+ }
462
+
406
463
  #[test]
407
464
  fn nested_class_call_runs_correctly() {
408
465
  let src = "\