plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
d2640d2
— Peter 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::
|
|
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
|
-
|
|
3046
|
+
let params = ctx
|
|
2994
|
-
|
|
3047
|
+
.enum_params
|
|
2995
|
-
|
|
3048
|
+
.get(&class_name)
|
|
2996
|
-
|
|
3049
|
+
.ok_or_else(|| format!("codegen: unknown class '{}'", class_name))?;
|
|
2997
|
-
|
|
3050
|
+
let (field_idx, field_ty) = params
|
|
2998
|
-
|
|
3051
|
+
.iter()
|
|
2999
|
-
|
|
3052
|
+
.position(|(n, _)| n == field_name)
|
|
3000
|
-
|
|
3053
|
+
.map(|i| (i, params[i].1.clone()))
|
|
3001
|
-
|
|
3054
|
+
.ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
|
|
3002
|
-
|
|
3055
|
+
compile_expr(&attr.object, body, ctx, state)?;
|
|
3003
|
-
|
|
3056
|
+
let offset = ((field_idx + 1) as u64) * 8;
|
|
3004
|
-
|
|
3057
|
+
match plum_type_to_valtype(&field_ty) {
|
|
3005
|
-
|
|
3058
|
+
ValType::I64 => Instruction::I64Load(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
|
|
3006
|
-
|
|
3059
|
+
ValType::F64 => Instruction::F64Load(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
|
|
3007
|
-
|
|
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 = "\
|