plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
a577529
— Peter John
2026-09-03T20:39:58+05:30
feat(plum): let match destructure a plain class like an enum variant
- examples/match.plum +35 -0
- plum-checker/src/lib.rs +24 -6
- plum-checker/tests/checker_tests.rs +33 -0
- plum-wasm-codegen/src/lib.rs +105 -10
examples/match.plum
CHANGED
|
@@ -189,3 +189,38 @@ fun bothOptions(a: Option, b: Option) -> Int =
|
|
|
189
189
|
|
|
190
190
|
test "multi subject match with generic enum variant runs correctly"
|
|
191
191
|
expect bothOptions(Some(3), Some(4)) == 7
|
|
192
|
+
|
|
193
|
+
type Point =
|
|
194
|
+
x: Int
|
|
195
|
+
y: Int
|
|
196
|
+
|
|
197
|
+
fun sumPoint(p: Point) -> Int =
|
|
198
|
+
match p
|
|
199
|
+
Point(x, y) => x + y
|
|
200
|
+
|
|
201
|
+
test "match destructures a plain class the same way it destructures an enum variant"
|
|
202
|
+
expect sumPoint(Point(x: 3, y: 4)) == 7
|
|
203
|
+
|
|
204
|
+
fun classifyPoint(p: Point) -> Str =
|
|
205
|
+
match p
|
|
206
|
+
Point(0, 0) => "origin"
|
|
207
|
+
Point(x, 0) => "on x axis"
|
|
208
|
+
Point(_, _) => "elsewhere"
|
|
209
|
+
|
|
210
|
+
test "match on a plain class supports literal/wildcard sub-patterns and case fallthrough"
|
|
211
|
+
expect classifyPoint(Point(x: 0, y: 0)) == "origin"
|
|
212
|
+
expect classifyPoint(Point(x: 5, y: 0)) == "on x axis"
|
|
213
|
+
expect classifyPoint(Point(x: 5, y: 5)) == "elsewhere"
|
|
214
|
+
|
|
215
|
+
enum Shape =
|
|
216
|
+
| Circle[Point]
|
|
217
|
+
| Square[Point]
|
|
218
|
+
|
|
219
|
+
fun shapeMeasure(s: Shape) -> Int =
|
|
220
|
+
match s
|
|
221
|
+
Circle(Point(x, y)) => x + y
|
|
222
|
+
Square(Point(x, y)) => x * y
|
|
223
|
+
|
|
224
|
+
test "a plain class nested inside an enum variant pattern destructures correctly"
|
|
225
|
+
expect shapeMeasure(Circle(Point(x: 3, y: 4))) == 7
|
|
226
|
+
expect shapeMeasure(Square(Point(x: 3, y: 4))) == 12
|
plum-checker/src/lib.rs
CHANGED
|
@@ -885,13 +885,31 @@ fn checkPattern(pat: &ast::CasePattern, subject_ty: &PlumType, env: &mut TypeEnv
|
|
|
885
885
|
}
|
|
886
886
|
Ok(())
|
|
887
887
|
}
|
|
888
|
+
// Not an enum variant — a PLAIN CLASS is destructured the same way (see
|
|
889
|
+
// `plum-wasm-codegen`'s matching `compileClassDestructureArm`): every
|
|
888
|
-
//
|
|
890
|
+
// instance is already "the one variant", so there's no tag to check,
|
|
891
|
+
// just its declared fields (in declaration order) to match against.
|
|
889
|
-
None => {
|
|
892
|
+
None => match ctx.classes.get(name) {
|
|
890
|
-
|
|
893
|
+
Some(class_fields) => {
|
|
894
|
+
if fields.len() != class_fields.len() {
|
|
895
|
+
return Err(format!(
|
|
896
|
+
"constructor pattern '{}' expects {} field(s), got {}",
|
|
897
|
+
name, class_fields.len(), fields.len()
|
|
898
|
+
));
|
|
899
|
+
}
|
|
900
|
+
for (f, (_, fty)) in fields.iter().zip(class_fields.iter()) {
|
|
891
|
-
|
|
901
|
+
checkPattern(f, fty, env, ctx)?;
|
|
902
|
+
}
|
|
903
|
+
Ok(())
|
|
892
904
|
}
|
|
905
|
+
// Unmodeled/builtin variant or class: allow, codegen will catch.
|
|
906
|
+
None => {
|
|
907
|
+
for f in fields {
|
|
908
|
+
checkPattern(f, &PlumType::TVar("_".to_string()), env, ctx)?;
|
|
909
|
+
}
|
|
893
|
-
|
|
910
|
+
Ok(())
|
|
894
|
-
|
|
911
|
+
}
|
|
912
|
+
},
|
|
895
913
|
},
|
|
896
914
|
}
|
|
897
915
|
}
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -1032,3 +1032,36 @@ type MyErr(Err) =
|
|
|
1032
1032
|
let source = parse(src);
|
|
1033
1033
|
assert!(checkSource(&source).is_ok(), "expected Ok, got: {:?}", checkSource(&source));
|
|
1034
1034
|
}
|
|
1035
|
+
|
|
1036
|
+
#[test]
|
|
1037
|
+
fn matchDestructuresPlainClassAndBindsFieldTypes() {
|
|
1038
|
+
let src = "\
|
|
1039
|
+
type Point =
|
|
1040
|
+
x: Int
|
|
1041
|
+
y: Int
|
|
1042
|
+
|
|
1043
|
+
fun sumPoint(p: Point) -> Int =
|
|
1044
|
+
match p
|
|
1045
|
+
Point(x, y) => x + y
|
|
1046
|
+
";
|
|
1047
|
+
let source = parse(src);
|
|
1048
|
+
assert!(checkSource(&source).is_ok(), "expected Ok, got: {:?}", checkSource(&source));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
#[test]
|
|
1052
|
+
fn matchClassPatternWrongFieldCountIsError() {
|
|
1053
|
+
let src = "\
|
|
1054
|
+
type Point =
|
|
1055
|
+
x: Int
|
|
1056
|
+
y: Int
|
|
1057
|
+
|
|
1058
|
+
fun sumPoint(p: Point) -> Int =
|
|
1059
|
+
match p
|
|
1060
|
+
Point(x) => x
|
|
1061
|
+
";
|
|
1062
|
+
let source = parse(src);
|
|
1063
|
+
let result = checkSource(&source);
|
|
1064
|
+
assert!(result.is_err());
|
|
1065
|
+
let errs = result.unwrap_err();
|
|
1066
|
+
assert!(errs.iter().any(|e| e.message.contains("expects 2 field(s), got 1")), "got: {:?}", errs);
|
|
1067
|
+
}
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -505,6 +505,16 @@ fn withGcTypes<R>(f: impl FnOnce(&GcTypeRegistry) -> R) -> R {
|
|
|
505
505
|
})
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
/// A constructor-pattern name (`nested_class_scratch_types`'s recorded name, or any
|
|
509
|
+
/// other spot a pattern's own concrete GC type index is needed) is EITHER an enum
|
|
510
|
+
/// variant or a plain class — see `compileClassDestructureArm`. Checks both
|
|
511
|
+
/// registries so a scratch local narrows correctly either way.
|
|
512
|
+
fn classOrVariantTypeIdx(r: &GcTypeRegistry, name: &str) -> u32 {
|
|
513
|
+
*r.variant_type_idx.get(name)
|
|
514
|
+
.or_else(|| r.class_type_idx.get(name))
|
|
515
|
+
.unwrap_or_else(|| panic!("internal codegen error: '{}' missing from the GC type registry (neither a variant nor a class)", name))
|
|
516
|
+
}
|
|
517
|
+
|
|
508
518
|
/// Resolves an `ast::Type`/`ast::ParamType`'s bare name (e.g. from a function
|
|
509
519
|
/// signature, before any `PlumType`/checker involvement) to its wasm-gc `ValType`.
|
|
510
520
|
fn astTypeToWasm(name: &str) -> Option<ValType> {
|
|
@@ -2496,6 +2506,11 @@ impl<'a> Collector<'a> {
|
|
|
2496
2506
|
for (f, fty) in fields.iter().zip(field_types.iter()) {
|
|
2497
2507
|
self.collectPattern(f, fty);
|
|
2498
2508
|
}
|
|
2509
|
+
} else if let Some(class_fields) = self.cctx.classes.get(name) {
|
|
2510
|
+
let field_types: Vec<PlumType> = class_fields.iter().map(|(_, t)| t.clone()).collect();
|
|
2511
|
+
for (f, fty) in fields.iter().zip(field_types.iter()) {
|
|
2512
|
+
self.collectPattern(f, fty);
|
|
2513
|
+
}
|
|
2499
2514
|
}
|
|
2500
2515
|
}
|
|
2501
2516
|
_ => {}
|
|
@@ -2734,8 +2749,7 @@ fn compileFnBody(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Resu
|
|
|
2734
2749
|
// holding the narrowed (`ref.cast`) value to be statically typed as that exact
|
|
2735
2750
|
// variant, and different slots very likely narrow to different variants.
|
|
2736
2751
|
for vname in &collector.nested_class_scratch_types {
|
|
2737
|
-
let variant_idx = withGcTypes(|r|
|
|
2752
|
+
let variant_idx = withGcTypes(|r| classOrVariantTypeIdx(r, vname));
|
|
2738
|
-
.unwrap_or_else(|| panic!("internal codegen error: variant '{}' missing from the GC type registry", vname)));
|
|
2739
2753
|
groups.push(gcRef(variant_idx));
|
|
2740
2754
|
idx += 1;
|
|
2741
2755
|
}
|
|
@@ -3483,7 +3497,11 @@ fn compileCasePositions(
|
|
|
3483
3497
|
ast::CasePattern::String(_) => Err("codegen: string match patterns are not yet supported".to_string()),
|
|
3484
3498
|
ast::CasePattern::Float(_) => Err("codegen: float match patterns are not yet supported".to_string()),
|
|
3485
3499
|
ast::CasePattern::Class { name, fields } => {
|
|
3500
|
+
if ctx.enum_variants.contains_key(name) {
|
|
3486
|
-
|
|
3501
|
+
compileVariantConstructorArm(pat, name, fields, subject_vt, scratch_local, case, pos, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
|
|
3502
|
+
} else {
|
|
3503
|
+
compileClassDestructureArm(name, fields, subject_vt, scratch_local, case, pos, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
|
|
3504
|
+
}
|
|
3487
3505
|
}
|
|
3488
3506
|
}
|
|
3489
3507
|
}
|
|
@@ -3590,6 +3608,51 @@ fn compileVariantConstructorArm(
|
|
|
3590
3608
|
Ok(())
|
|
3591
3609
|
}
|
|
3592
3610
|
|
|
3611
|
+
/// Destructures a PLAIN CLASS constructor pattern (`Point(x, y)` where `Point` is a
|
|
3612
|
+
/// `type`, not an enum variant) — the counterpart to `compileVariantConstructorArm`.
|
|
3613
|
+
/// A class subject has only "the one shape": there's no discriminant to test, and
|
|
3614
|
+
/// the subject's own static type already IS this exact concrete GC struct type (the
|
|
3615
|
+
/// checker already verified `name` matches it), so this always matches — just
|
|
3616
|
+
/// destructure fields straight off `scratch_local` itself, no `ref.test`/`ref.cast`
|
|
3617
|
+
/// narrowing or separate scratch local needed at all.
|
|
3618
|
+
#[allow(clippy::too_many_arguments)]
|
|
3619
|
+
fn compileClassDestructureArm(
|
|
3620
|
+
name: &str,
|
|
3621
|
+
fields: &[ast::CasePattern],
|
|
3622
|
+
subject_vt: ValType,
|
|
3623
|
+
scratch_local: u32,
|
|
3624
|
+
case: &ast::Case,
|
|
3625
|
+
pos: usize,
|
|
3626
|
+
all_subjects: &[(ValType, u32)],
|
|
3627
|
+
rest: &[ast::Case],
|
|
3628
|
+
result_vt: Option<ValType>,
|
|
3629
|
+
exhaustive_fallback: bool,
|
|
3630
|
+
body: &mut Vec<u8>,
|
|
3631
|
+
ctx: &LocalCtx,
|
|
3632
|
+
state: &mut ModuleState,
|
|
3633
|
+
) -> Result<(), String> {
|
|
3634
|
+
let class_fields = ctx
|
|
3635
|
+
.classes
|
|
3636
|
+
.get(name)
|
|
3637
|
+
.ok_or_else(|| format!("codegen: unknown constructor '{}' (neither an enum variant nor a class)", name))?;
|
|
3638
|
+
if !matches!(subject_vt, ValType::Ref(_)) {
|
|
3639
|
+
return Err(format!("codegen: constructor pattern '{}' against a non-class subject", name));
|
|
3640
|
+
}
|
|
3641
|
+
if fields.len() != class_fields.len() {
|
|
3642
|
+
return Err(format!(
|
|
3643
|
+
"codegen: constructor pattern '{}' expects {} field(s), got {}",
|
|
3644
|
+
name, class_fields.len(), fields.len()
|
|
3645
|
+
));
|
|
3646
|
+
}
|
|
3647
|
+
let class_type_idx = *ctx.gc_types.class_type_idx.get(name)
|
|
3648
|
+
.ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", name))?;
|
|
3649
|
+
let field_types: Vec<PlumType> = class_fields.iter().map(|(_, t)| t.clone()).collect();
|
|
3650
|
+
compileFieldPatterns(
|
|
3651
|
+
fields, &field_types, 0, scratch_local, class_type_idx, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state,
|
|
3652
|
+
&mut |body, state| compileCasePositions(case, pos + 1, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state),
|
|
3653
|
+
)
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3593
3656
|
/// Checks `fields[fpos..]` (a constructor pattern's own sub-patterns, e.g. the `v` in
|
|
3594
3657
|
/// `Some(v)`, or — recursively — the `Some(v)` in `Wrap(Some(v))`) against the
|
|
3595
3658
|
/// already-loaded value in `container_local`, one field at a time. Once every field
|
|
@@ -3675,11 +3738,8 @@ fn compileFieldPatterns(
|
|
|
3675
3738
|
}
|
|
3676
3739
|
ast::CasePattern::String(_) => Err("codegen: string match patterns are not yet supported".to_string()),
|
|
3677
3740
|
ast::CasePattern::Float(_) => Err("codegen: float match patterns are not yet supported".to_string()),
|
|
3678
|
-
ast::CasePattern::Class { name, fields: inner_fields } => {
|
|
3741
|
+
ast::CasePattern::Class { name, fields: inner_fields } if ctx.enum_variants.contains_key(name) => {
|
|
3679
|
-
let info = ctx
|
|
3680
|
-
.enum_variants
|
|
3681
|
-
.get(name)
|
|
3682
|
-
|
|
3742
|
+
let info = ctx.enum_variants.get(name).expect("just checked contains_key");
|
|
3683
3743
|
if !matches!(plumTypeToValtype(field_ty), ValType::Ref(_)) {
|
|
3684
3744
|
return Err(format!("codegen: constructor pattern '{}' against a non-enum field", name));
|
|
3685
3745
|
}
|
|
@@ -3716,6 +3776,42 @@ fn compileFieldPatterns(
|
|
|
3716
3776
|
Instruction::End.encode(body);
|
|
3717
3777
|
Ok(())
|
|
3718
3778
|
}
|
|
3779
|
+
// A plain class nested inside another constructor pattern's fields (see
|
|
3780
|
+
// `compileClassDestructureArm`'s doc comment) — there's only ever "the one
|
|
3781
|
+
// shape" to match, so no `ref.test`/`ref.cast` narrowing is needed; just
|
|
3782
|
+
// load the field into its own local (`compileFieldPatterns` needs a LOCAL
|
|
3783
|
+
// to repeatedly `struct.get` against, not a bare stack value) and recurse.
|
|
3784
|
+
ast::CasePattern::Class { name, fields: inner_fields } => {
|
|
3785
|
+
let class_fields = ctx
|
|
3786
|
+
.classes
|
|
3787
|
+
.get(name)
|
|
3788
|
+
.ok_or_else(|| format!("codegen: unknown constructor '{}' (neither an enum variant nor a class)", name))?;
|
|
3789
|
+
if !matches!(plumTypeToValtype(field_ty), ValType::Ref(_)) {
|
|
3790
|
+
return Err(format!("codegen: constructor pattern '{}' against a non-class field", name));
|
|
3791
|
+
}
|
|
3792
|
+
if inner_fields.len() != class_fields.len() {
|
|
3793
|
+
return Err(format!(
|
|
3794
|
+
"codegen: constructor pattern '{}' expects {} field(s), got {}",
|
|
3795
|
+
name, class_fields.len(), inner_fields.len()
|
|
3796
|
+
));
|
|
3797
|
+
}
|
|
3798
|
+
let inner_class_type_idx = *ctx.gc_types.class_type_idx.get(name)
|
|
3799
|
+
.ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", name))?;
|
|
3800
|
+
let key = pat as *const ast::CasePattern as usize;
|
|
3801
|
+
let slot = *ctx
|
|
3802
|
+
.nested_class_scratch
|
|
3803
|
+
.get(&key)
|
|
3804
|
+
.ok_or_else(|| "internal codegen error: missing nested constructor pattern scratch slot".to_string())?;
|
|
3805
|
+
let nested_local = ctx.nested_class_scratch_base + slot;
|
|
3806
|
+
let inner_field_types: Vec<PlumType> = class_fields.iter().map(|(_, t)| t.clone()).collect();
|
|
3807
|
+
|
|
3808
|
+
Instruction::LocalGet(container_local).encode(body);
|
|
3809
|
+
Instruction::StructGet { struct_type_index: container_type_idx, field_index: fpos as u32 }.encode(body);
|
|
3810
|
+
Instruction::LocalSet(nested_local).encode(body);
|
|
3811
|
+
compileFieldPatterns(inner_fields, &inner_field_types, 0, nested_local, inner_class_type_idx, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, &mut |body, state| {
|
|
3812
|
+
compileFieldPatterns(fields, field_types, fpos + 1, container_local, container_type_idx, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, on_match)
|
|
3813
|
+
})
|
|
3814
|
+
}
|
|
3719
3815
|
}
|
|
3720
3816
|
}
|
|
3721
3817
|
|
|
@@ -4494,8 +4590,7 @@ fn compileClosureBody(
|
|
|
4494
4590
|
// holding the narrowed (`ref.cast`) value to be statically typed as that exact
|
|
4495
4591
|
// variant, and different slots very likely narrow to different variants.
|
|
4496
4592
|
for vname in &collector.nested_class_scratch_types {
|
|
4497
|
-
let variant_idx = withGcTypes(|r|
|
|
4593
|
+
let variant_idx = withGcTypes(|r| classOrVariantTypeIdx(r, vname));
|
|
4498
|
-
.unwrap_or_else(|| panic!("internal codegen error: variant '{}' missing from the GC type registry", vname)));
|
|
4499
4594
|
groups.push(gcRef(variant_idx));
|
|
4500
4595
|
idx += 1;
|
|
4501
4596
|
}
|