plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
b1c3ec8
— Peter John
2026-09-05T15:21:03+05:30
improve enum
- .codegraph/.gitignore +5 -0
- examples/types.plum +38 -0
- plum-checker/src/lib.rs +125 -36
- plum-checker/tests/checker_tests.rs +96 -4
- plum-wasm-codegen/src/lib.rs +35 -14
.codegraph/.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# CodeGraph data files — local to each machine, not for committing.
|
|
2
|
+
# Ignore everything in .codegraph/ except this file itself, so transient
|
|
3
|
+
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
|
4
|
+
*
|
|
5
|
+
!.gitignore
|
examples/types.plum
CHANGED
|
@@ -37,6 +37,11 @@ enum Color =
|
|
|
37
37
|
| Green
|
|
38
38
|
| Blue
|
|
39
39
|
|
|
40
|
+
# A bare enum variant name can be used directly as a type: `v: Red` means
|
|
41
|
+
# "a `Color` value that is specifically the `Red` variant".
|
|
42
|
+
fun stringifyColor(v: Red) -> Str =
|
|
43
|
+
"Red"
|
|
44
|
+
|
|
40
45
|
fun makeIntBox() -> Box =
|
|
41
46
|
Box(value: 5)
|
|
42
47
|
|
|
@@ -107,6 +112,13 @@ fun area(s: ShapeKind) -> Int =
|
|
|
107
112
|
Circle(r) =>
|
|
108
113
|
return r * r
|
|
109
114
|
|
|
115
|
+
enum Vec2 =
|
|
116
|
+
| Vec2(x: Int, y: Int)
|
|
117
|
+
|
|
118
|
+
enum ShapeWithFields =
|
|
119
|
+
| CircleField(radius: Int)
|
|
120
|
+
| SquareField(side: Int)
|
|
121
|
+
|
|
110
122
|
type OptionBox =
|
|
111
123
|
value: Option[Int]
|
|
112
124
|
|
|
@@ -148,6 +160,32 @@ test "payload variant construction compiles and runs"
|
|
|
148
160
|
test "multi field variant construction compiles and runs"
|
|
149
161
|
assert area(Rect(3, 4)) == 12
|
|
150
162
|
|
|
163
|
+
test "single-variant named-payload enum field access works like a class"
|
|
164
|
+
# `Vec2` has exactly one variant, so its fields unambiguously describe
|
|
165
|
+
# every `Vec2` value — `.x`/`.y` resolve directly, no `match` needed,
|
|
166
|
+
# for both named and positional construction.
|
|
167
|
+
named := Vec2(x: 1, y: 2)
|
|
168
|
+
positional := Vec2(3, 4)
|
|
169
|
+
assert named.x == 1
|
|
170
|
+
assert named.y == 2
|
|
171
|
+
assert positional.x == 3
|
|
172
|
+
assert positional.y == 4
|
|
173
|
+
|
|
174
|
+
test "named-payload field access on a multi-variant enum value works via a checked downcast"
|
|
175
|
+
# Unlike `Vec2` above, `ShapeWithFields` has more than one variant — `.field`
|
|
176
|
+
# here isn't statically provable to always succeed the way it is on a
|
|
177
|
+
# single-variant enum. It's still allowed because `radius`/`side` each
|
|
178
|
+
# belong to exactly one variant (no ambiguity) — codegen compiles it as a
|
|
179
|
+
# ref.cast down to that one variant's own struct, which would trap at
|
|
180
|
+
# runtime if the value were ever the OTHER variant instead.
|
|
181
|
+
c := CircleField(radius: 5)
|
|
182
|
+
s := SquareField(side: 9)
|
|
183
|
+
assert c.radius == 5
|
|
184
|
+
assert s.side == 9
|
|
185
|
+
|
|
186
|
+
test "enum variant used directly as a type checks and runs correctly"
|
|
187
|
+
assert stringifyColor(Red) == "Red"
|
|
188
|
+
|
|
151
189
|
test "enum class field construct and destructure runs correctly"
|
|
152
190
|
b := OptionBox(value: Some(42))
|
|
153
191
|
assert b.unwrap(0) == 42
|
plum-checker/src/lib.rs
CHANGED
|
@@ -48,7 +48,7 @@ pub fn methodReceiverName(ty: &PlumType) -> Option<String> {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
pub fn unify(t1: &PlumType, t2: &PlumType) -> Result<(), String> {
|
|
51
|
+
pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String> {
|
|
52
52
|
match (t1, t2) {
|
|
53
53
|
(PlumType::TVar(_), _) | (_, PlumType::TVar(_)) => Ok(()),
|
|
54
54
|
(PlumType::TInt, PlumType::TInt) => Ok(()),
|
|
@@ -74,16 +74,74 @@ pub fn unify(t1: &PlumType, t2: &PlumType) -> Result<(), String> {
|
|
|
74
74
|
(PlumType::TByteSlice, PlumType::TByteSlice) => Ok(()),
|
|
75
75
|
(PlumType::TUnit, PlumType::TUnit) => Ok(()),
|
|
76
76
|
(PlumType::TNamed(a), PlumType::TNamed(b)) if a == b => Ok(()),
|
|
77
|
+
// A bare enum variant name (`Red`) can be used directly as a type — e.g.
|
|
78
|
+
// a fn param `v: Red` — meaning "a `Color` value that is specifically
|
|
79
|
+
// the `Red` variant". A variant's OWN constructions/references always
|
|
80
|
+
// infer as the whole enum's type (`TNamed("Color")`, see
|
|
81
|
+
// `bareVariantWrapType`/`inferExpr`'s `TypeName` arm), never the
|
|
82
|
+
// narrower per-variant type, so accept either direction here rather
|
|
83
|
+
// than requiring the (never-produced) narrower type to show up as an
|
|
84
|
+
// "actual" type.
|
|
85
|
+
(PlumType::TNamed(a), PlumType::TNamed(b))
|
|
86
|
+
if ctx.enum_variants.get(a).is_some_and(|info| &info.enum_name == b)
|
|
87
|
+
|| ctx.enum_variants.get(b).is_some_and(|info| &info.enum_name == a) =>
|
|
88
|
+
{
|
|
89
|
+
Ok(())
|
|
90
|
+
}
|
|
77
91
|
(PlumType::TFun(ps1, r1), PlumType::TFun(ps2, r2)) if ps1.len() == ps2.len() => {
|
|
78
92
|
for (p1, p2) in ps1.iter().zip(ps2.iter()) {
|
|
79
|
-
unify(p1, p2)?;
|
|
93
|
+
unify(p1, p2, ctx)?;
|
|
80
94
|
}
|
|
81
|
-
unify(r1, r2)
|
|
95
|
+
unify(r1, r2, ctx)
|
|
82
96
|
}
|
|
83
97
|
_ => Err(format!("type mismatch: expected {}, found {}", t1, t2)),
|
|
84
98
|
}
|
|
85
99
|
}
|
|
86
100
|
|
|
101
|
+
/// True if `expr` (looking through parens) is a direct construction/reference
|
|
102
|
+
/// of exactly the enum variant named `variant_name` — a bare payload-free
|
|
103
|
+
/// reference (`Red`), a positional-payload construction (`Circle(1)`), or a
|
|
104
|
+
/// named-payload construction (`Circle(radius: 1)`). The checker has no flow
|
|
105
|
+
/// typing, so this is necessarily syntactic: it can't tell that a variable
|
|
106
|
+
/// holds a particular variant, only that an expression directly constructs
|
|
107
|
+
/// or names one.
|
|
108
|
+
fn exprIsVariant(expr: &ast::Expr, variant_name: &str) -> bool {
|
|
109
|
+
match expr {
|
|
110
|
+
ast::Expr::Paren(inner) => exprIsVariant(inner, variant_name),
|
|
111
|
+
ast::Expr::TypeName(n) => n == variant_name,
|
|
112
|
+
ast::Expr::FnCall(call) => call.name == variant_name,
|
|
113
|
+
ast::Expr::ClassCall(call) => call.type_name == variant_name,
|
|
114
|
+
_ => false,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Like `unify`, but for a value-producing position where the actual
|
|
119
|
+
/// EXPRESSION (not just its inferred type) is available — a call argument, a
|
|
120
|
+
/// return value, an assignment's RHS. `unify` alone treats a specific variant
|
|
121
|
+
/// type (`v: Red`) as compatible with any value of its enum in EITHER
|
|
122
|
+
/// direction (see its `TNamed`/`TNamed` variant-vs-enum arm), because a bare
|
|
123
|
+
/// variant construction always infers as the whole enum's type, never the
|
|
124
|
+
/// narrower per-variant one (`bareVariantWrapType`/`inferExpr`'s `TypeName`
|
|
125
|
+
/// arm) — so by itself it can't reject `stringifyColor(Blue)` against a
|
|
126
|
+
/// declared `stringifyColor(v: Red)`. This adds that rejection back by
|
|
127
|
+
/// checking the actual expression directly names/constructs the expected
|
|
128
|
+
/// variant whenever `unify` only accepted it via that widening rule.
|
|
129
|
+
fn unifyArg(expected: &PlumType, actual: &PlumType, actual_expr: &ast::Expr, ctx: &CheckCtx) -> Result<(), String> {
|
|
130
|
+
unify(expected, actual, ctx)?;
|
|
131
|
+
if let (PlumType::TNamed(variant_name), PlumType::TNamed(actual_name)) = (expected, actual) {
|
|
132
|
+
if variant_name != actual_name
|
|
133
|
+
&& ctx.enum_variants.contains_key(variant_name)
|
|
134
|
+
&& !exprIsVariant(actual_expr, variant_name)
|
|
135
|
+
{
|
|
136
|
+
return Err(format!(
|
|
137
|
+
"type mismatch: expected variant '{}', found a different value of its enum",
|
|
138
|
+
variant_name
|
|
139
|
+
));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
Ok(())
|
|
143
|
+
}
|
|
144
|
+
|
|
87
145
|
pub fn lookup(env: &TypeEnv, name: &str) -> Result<PlumType, String> {
|
|
88
146
|
env.get(name)
|
|
89
147
|
.map(|s| *s.body.clone())
|
|
@@ -420,7 +478,7 @@ pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
|
|
|
420
478
|
match inferExpr(value, &global_env, &ctx) {
|
|
421
479
|
Ok(actual) => {
|
|
422
480
|
let expected = plumTypeFromAst(¶m.ty);
|
|
423
|
-
if let Err(msg) = unify(&expected, &actual) {
|
|
481
|
+
if let Err(msg) = unify(&expected, &actual, &ctx) {
|
|
424
482
|
errors.push(CheckError {
|
|
425
483
|
message: format!("enum '{}' variant '{}': param '{}': {}", e.name, v.name, param.name, msg),
|
|
426
484
|
});
|
|
@@ -520,7 +578,7 @@ fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError>
|
|
|
520
578
|
ast::FnBody::Expr(e) => {
|
|
521
579
|
match inferExpr(e, &env, ctx) {
|
|
522
580
|
Ok(t) => {
|
|
523
|
-
if let Err(msg) =
|
|
581
|
+
if let Err(msg) = unifyArg(&declared_ret, &t, e, ctx) {
|
|
524
582
|
errors.push(CheckError { message: format!("fn '{}': return type mismatch: {}", f.name, msg) });
|
|
525
583
|
}
|
|
526
584
|
}
|
|
@@ -534,7 +592,7 @@ fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError>
|
|
|
534
592
|
if let Some(ast::Stmt::Expr(last_expr)) = block.stmts.last() {
|
|
535
593
|
match inferExpr(last_expr, &env, ctx) {
|
|
536
594
|
Ok(t) => {
|
|
537
|
-
if let Err(msg) =
|
|
595
|
+
if let Err(msg) = unifyArg(&declared_ret, &t, last_expr, ctx) {
|
|
538
596
|
errors.push(CheckError { message: format!("fn '{}': return type mismatch: {}", f.name, msg) });
|
|
539
597
|
}
|
|
540
598
|
}
|
|
@@ -593,7 +651,7 @@ fn checkStmt(stmt: &ast::Stmt, env: &mut TypeEnv, declared_ret: &PlumType, fn_na
|
|
|
593
651
|
match inferExpr(value, env, ctx) {
|
|
594
652
|
Ok(t) => {
|
|
595
653
|
let existing = lookup(env, name).expect("already_declared just confirmed this succeeds");
|
|
596
|
-
if let Err(msg) =
|
|
654
|
+
if let Err(msg) = unifyArg(&existing, &t, value, ctx) {
|
|
597
655
|
errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, name, msg) });
|
|
598
656
|
} else if matches!(existing, PlumType::TVar(_)) {
|
|
599
657
|
// `unify` never narrows a `TVar` itself —
|
|
@@ -622,7 +680,7 @@ fn checkStmt(stmt: &ast::Stmt, env: &mut TypeEnv, declared_ret: &PlumType, fn_na
|
|
|
622
680
|
fields.iter().find(|(n, _)| n == field_name).map(|(_, ty)| ty.clone())
|
|
623
681
|
}) {
|
|
624
682
|
Some(field_ty) => {
|
|
625
|
-
if let Err(msg) =
|
|
683
|
+
if let Err(msg) = unifyArg(&field_ty, &value_ty, value, ctx) {
|
|
626
684
|
errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, label, msg) });
|
|
627
685
|
}
|
|
628
686
|
}
|
|
@@ -640,7 +698,7 @@ fn checkStmt(stmt: &ast::Stmt, env: &mut TypeEnv, declared_ret: &PlumType, fn_na
|
|
|
640
698
|
ast::Stmt::Return(Some(e)) => {
|
|
641
699
|
match inferExpr(e, env, ctx) {
|
|
642
700
|
Ok(t) => {
|
|
643
|
-
if let Err(msg) =
|
|
701
|
+
if let Err(msg) = unifyArg(declared_ret, &t, e, ctx) {
|
|
644
702
|
errors.push(CheckError { message: format!("fn '{}': return type mismatch: {}", fn_name, msg) });
|
|
645
703
|
}
|
|
646
704
|
}
|
|
@@ -648,14 +706,14 @@ fn checkStmt(stmt: &ast::Stmt, env: &mut TypeEnv, declared_ret: &PlumType, fn_na
|
|
|
648
706
|
}
|
|
649
707
|
}
|
|
650
708
|
ast::Stmt::Return(None) => {
|
|
651
|
-
if let Err(msg) = unify(declared_ret, &PlumType::TUnit) {
|
|
709
|
+
if let Err(msg) = unify(declared_ret, &PlumType::TUnit, ctx) {
|
|
652
710
|
errors.push(CheckError { message: format!("fn '{}': bare return in non-Unit function: {}", fn_name, msg) });
|
|
653
711
|
}
|
|
654
712
|
}
|
|
655
713
|
ast::Stmt::If(if_) => {
|
|
656
714
|
match inferExpr(&if_.condition, env, ctx) {
|
|
657
715
|
Ok(t) => {
|
|
658
|
-
if let Err(msg) = unify(&PlumType::TBool, &t) {
|
|
716
|
+
if let Err(msg) = unify(&PlumType::TBool, &t, ctx) {
|
|
659
717
|
errors.push(CheckError { message: format!("fn '{}': if condition must be Bool: {}", fn_name, msg) });
|
|
660
718
|
}
|
|
661
719
|
}
|
|
@@ -665,7 +723,7 @@ fn checkStmt(stmt: &ast::Stmt, env: &mut TypeEnv, declared_ret: &PlumType, fn_na
|
|
|
665
723
|
for ei in &if_.else_ifs {
|
|
666
724
|
match inferExpr(&ei.condition, env, ctx) {
|
|
667
725
|
Ok(t) => {
|
|
668
|
-
if let Err(msg) = unify(&PlumType::TBool, &t) {
|
|
726
|
+
if let Err(msg) = unify(&PlumType::TBool, &t, ctx) {
|
|
669
727
|
errors.push(CheckError { message: format!("fn '{}': else if condition must be Bool: {}", fn_name, msg) });
|
|
670
728
|
}
|
|
671
729
|
}
|
|
@@ -680,7 +738,7 @@ fn checkStmt(stmt: &ast::Stmt, env: &mut TypeEnv, declared_ret: &PlumType, fn_na
|
|
|
680
738
|
ast::Stmt::While(w) => {
|
|
681
739
|
match inferExpr(&w.condition, env, ctx) {
|
|
682
740
|
Ok(t) => {
|
|
683
|
-
if let Err(msg) = unify(&PlumType::TBool, &t) {
|
|
741
|
+
if let Err(msg) = unify(&PlumType::TBool, &t, ctx) {
|
|
684
742
|
errors.push(CheckError { message: format!("fn '{}': while condition must be Bool: {}", fn_name, msg) });
|
|
685
743
|
}
|
|
686
744
|
}
|
|
@@ -722,7 +780,7 @@ fn checkStmt(stmt: &ast::Stmt, env: &mut TypeEnv, declared_ret: &PlumType, fn_na
|
|
|
722
780
|
ast::Stmt::Assert(c) => {
|
|
723
781
|
match inferExpr(&c.cond, env, ctx) {
|
|
724
782
|
Ok(t) => {
|
|
725
|
-
if let Err(msg) = unify(&PlumType::TBool, &t) {
|
|
783
|
+
if let Err(msg) = unify(&PlumType::TBool, &t, ctx) {
|
|
726
784
|
errors.push(CheckError { message: format!("fn '{}': assert must be Bool: {}", fn_name, msg) });
|
|
727
785
|
}
|
|
728
786
|
}
|
|
@@ -770,7 +828,7 @@ fn checkMatch(m: &ast::Match, env: &TypeEnv, declared_ret: &PlumType, fn_name: &
|
|
|
770
828
|
if let Some(guard) = &case.guard {
|
|
771
829
|
match inferExpr(guard, &case_env, ctx) {
|
|
772
830
|
Ok(t) => {
|
|
773
|
-
if let Err(msg) = unify(&PlumType::TBool, &t) {
|
|
831
|
+
if let Err(msg) = unify(&PlumType::TBool, &t, ctx) {
|
|
774
832
|
errors.push(CheckError { message: format!("fn '{}': match case guard must be Bool: {}", fn_name, msg) });
|
|
775
833
|
}
|
|
776
834
|
}
|
|
@@ -816,7 +874,7 @@ fn inferClassCallRaw(call: &ast::ClassCall, env: &TypeEnv, ctx: &CheckCtx) -> Re
|
|
|
816
874
|
match fields.iter().find(|(n, _)| n == &fa.name) {
|
|
817
875
|
Some((_, expected)) => {
|
|
818
876
|
let actual = inferExpr(&fa.value, env, ctx)?;
|
|
819
|
-
|
|
877
|
+
unifyArg(expected, &actual, &fa.value, ctx)
|
|
820
878
|
.map_err(|e| format!("class '{}' field '{}': {}", call.type_name, fa.name, e))?;
|
|
821
879
|
}
|
|
822
880
|
None => return Err(format!("unknown field '{}' on class '{}'", fa.name, call.type_name)),
|
|
@@ -841,7 +899,7 @@ fn inferClassCallRaw(call: &ast::ClassCall, env: &TypeEnv, ctx: &CheckCtx) -> Re
|
|
|
841
899
|
Some(i) => {
|
|
842
900
|
let expected = &info.field_types[i];
|
|
843
901
|
let actual = inferExpr(&fa.value, env, ctx)?;
|
|
844
|
-
|
|
902
|
+
unifyArg(expected, &actual, &fa.value, ctx)
|
|
845
903
|
.map_err(|e| format!("variant '{}' field '{}': {}", call.type_name, fa.name, e))?;
|
|
846
904
|
}
|
|
847
905
|
None => return Err(format!("unknown field '{}' on variant '{}'", fa.name, call.type_name)),
|
|
@@ -879,9 +937,9 @@ fn bareVariantWrapType(type_name: &str, ctx: &CheckCtx) -> PlumType {
|
|
|
879
937
|
fn checkPattern(pat: &ast::CasePattern, subject_ty: &PlumType, env: &mut TypeEnv, ctx: &CheckCtx) -> Result<(), String> {
|
|
880
938
|
match pat {
|
|
881
939
|
ast::CasePattern::Wildcard => Ok(()),
|
|
882
|
-
ast::CasePattern::Int(_) => unify(subject_ty, &PlumType::TInt),
|
|
940
|
+
ast::CasePattern::Int(_) => unify(subject_ty, &PlumType::TInt, ctx),
|
|
883
|
-
ast::CasePattern::Float(_) => unify(subject_ty, &PlumType::TFloat),
|
|
941
|
+
ast::CasePattern::Float(_) => unify(subject_ty, &PlumType::TFloat, ctx),
|
|
884
|
-
ast::CasePattern::String(_) => unify(subject_ty, &PlumType::TStr),
|
|
942
|
+
ast::CasePattern::String(_) => unify(subject_ty, &PlumType::TStr, ctx),
|
|
885
943
|
ast::CasePattern::Name(n) => {
|
|
886
944
|
let is_known_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
887
945
|
&& ctx.enum_variants.contains_key(n);
|
|
@@ -1069,35 +1127,35 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1069
1127
|
}
|
|
1070
1128
|
ast::Expr::Not(inner) => {
|
|
1071
1129
|
let t = inferExpr(inner, env, ctx)?;
|
|
1072
|
-
unify(&PlumType::TBool, &t)?;
|
|
1130
|
+
unify(&PlumType::TBool, &t, ctx)?;
|
|
1073
1131
|
Ok(PlumType::TBool)
|
|
1074
1132
|
}
|
|
1075
1133
|
ast::Expr::Unary(u) => inferExpr(&u.operand, env, ctx),
|
|
1076
1134
|
ast::Expr::Binary(b) => {
|
|
1077
1135
|
let lt = inferExpr(&b.left, env, ctx)?;
|
|
1078
1136
|
let rt = inferExpr(&b.right, env, ctx)?;
|
|
1079
|
-
unify(<, &rt).map_err(|e| format!("binary op: {}", e))?;
|
|
1137
|
+
unify(<, &rt, ctx).map_err(|e| format!("binary op: {}", e))?;
|
|
1080
1138
|
Ok(lt)
|
|
1081
1139
|
}
|
|
1082
1140
|
ast::Expr::Bool(b) => {
|
|
1083
1141
|
let lt = inferExpr(&b.left, env, ctx)?;
|
|
1084
1142
|
let rt = inferExpr(&b.right, env, ctx)?;
|
|
1085
|
-
unify(&PlumType::TBool, <).map_err(|e| format!("bool op left: {}", e))?;
|
|
1143
|
+
unify(&PlumType::TBool, <, ctx).map_err(|e| format!("bool op left: {}", e))?;
|
|
1086
|
-
unify(&PlumType::TBool, &rt).map_err(|e| format!("bool op right: {}", e))?;
|
|
1144
|
+
unify(&PlumType::TBool, &rt, ctx).map_err(|e| format!("bool op right: {}", e))?;
|
|
1087
1145
|
Ok(PlumType::TBool)
|
|
1088
1146
|
}
|
|
1089
1147
|
ast::Expr::Compare(c) => {
|
|
1090
1148
|
let lt = inferExpr(&c.left, env, ctx)?;
|
|
1091
1149
|
let rt = inferExpr(&c.right, env, ctx)?;
|
|
1092
|
-
unify(<, &rt).map_err(|e| format!("compare op: {}", e))?;
|
|
1150
|
+
unify(<, &rt, ctx).map_err(|e| format!("compare op: {}", e))?;
|
|
1093
1151
|
Ok(PlumType::TBool)
|
|
1094
1152
|
}
|
|
1095
1153
|
ast::Expr::Ternary(t) => {
|
|
1096
1154
|
let ct = inferExpr(&t.condition, env, ctx)?;
|
|
1097
|
-
unify(&PlumType::TBool, &ct).map_err(|e| format!("ternary condition: {}", e))?;
|
|
1155
|
+
unify(&PlumType::TBool, &ct, ctx).map_err(|e| format!("ternary condition: {}", e))?;
|
|
1098
1156
|
let tt = inferExpr(&t.then, env, ctx)?;
|
|
1099
1157
|
let et = inferExpr(&t.else_, env, ctx)?;
|
|
1100
|
-
unify(&tt, &et).map_err(|e| format!("ternary branches: {}", e))?;
|
|
1158
|
+
unify(&tt, &et, ctx).map_err(|e| format!("ternary branches: {}", e))?;
|
|
1101
1159
|
Ok(tt)
|
|
1102
1160
|
}
|
|
1103
1161
|
ast::Expr::FnCall(call) => {
|
|
@@ -1146,7 +1204,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1146
1204
|
}
|
|
1147
1205
|
_ => inferExpr(arg_expr, env, ctx)?,
|
|
1148
1206
|
};
|
|
1149
|
-
|
|
1207
|
+
unifyArg(expected, &actual, arg_expr, ctx).map_err(|e| format!("variant '{}' arg {}: {}", call.name, i, e))?;
|
|
1150
1208
|
}
|
|
1151
1209
|
return Ok(PlumType::TNamed(info.enum_name.clone()));
|
|
1152
1210
|
}
|
|
@@ -1181,7 +1239,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1181
1239
|
ast::Arg::Pair { value, .. } => value,
|
|
1182
1240
|
};
|
|
1183
1241
|
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
1184
|
-
|
|
1242
|
+
unifyArg(expected, &actual, arg_expr, ctx).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?;
|
|
1185
1243
|
}
|
|
1186
1244
|
for (i, arg) in call.args.iter().enumerate().skip(fixed.len()) {
|
|
1187
1245
|
let arg_expr = match arg {
|
|
@@ -1190,7 +1248,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1190
1248
|
ast::Arg::Pair { value, .. } => value,
|
|
1191
1249
|
};
|
|
1192
1250
|
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
1193
|
-
|
|
1251
|
+
unifyArg(elem, &actual, arg_expr, ctx).map_err(|e| format!("call '{}' variadic arg {}: {}", call.name, i, e))?;
|
|
1194
1252
|
}
|
|
1195
1253
|
Ok(*ret)
|
|
1196
1254
|
}
|
|
@@ -1211,7 +1269,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1211
1269
|
ast::Arg::Pair { value, .. } => value,
|
|
1212
1270
|
};
|
|
1213
1271
|
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
1214
|
-
|
|
1272
|
+
unifyArg(expected, &actual, arg_expr, ctx).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?;
|
|
1215
1273
|
}
|
|
1216
1274
|
Ok(*ret)
|
|
1217
1275
|
}
|
|
@@ -1252,8 +1310,39 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1252
1310
|
.find(|(n, _)| n == field_name)
|
|
1253
1311
|
.map(|(_, t)| t.clone())
|
|
1254
1312
|
.ok_or_else(|| format!("no field '{}' on type '{}'", field_name, class_name)),
|
|
1313
|
+
// Not a discriminant enum either: `class_name` might
|
|
1314
|
+
// still be an ordinary enum whose value provably came
|
|
1315
|
+
// from a named-payload variant (`Circle(radius: 5)`,
|
|
1316
|
+
// widened to `TNamed("Shape")` by construction — see
|
|
1317
|
+
// `inferClassCallRaw`). If exactly ONE of the enum's
|
|
1318
|
+
// variants declares a field with this name, `.field`
|
|
1319
|
+
// is safe to allow here: codegen compiles it as a
|
|
1320
|
+
// checked downcast to that one variant (traps at
|
|
1321
|
+
// runtime if the value turns out to be a different
|
|
1322
|
+
// variant), so no static proof the value IS that
|
|
1323
|
+
// variant is required. If more than one variant
|
|
1324
|
+
// shares the field name, which one's value would win
|
|
1325
|
+
// is genuinely ambiguous — require a real `match`
|
|
1326
|
+
// instead. Zero owners means an unmodeled type
|
|
1255
|
-
//
|
|
1327
|
+
// (unresolved generic, etc.) — allow, codegen will
|
|
1328
|
+
// catch a genuine mismatch.
|
|
1329
|
+
None => {
|
|
1330
|
+
let owners: Vec<&EnumVariantInfo> = ctx.enum_variants.values()
|
|
1331
|
+
.filter(|info| info.enum_name == class_name
|
|
1332
|
+
&& info.field_names.iter().any(|n| n == field_name))
|
|
1333
|
+
.collect();
|
|
1334
|
+
match owners.as_slice() {
|
|
1335
|
+
[info] => {
|
|
1336
|
+
let idx = info.field_names.iter().position(|n| n == field_name).expect("just filtered on this");
|
|
1337
|
+
Ok(info.field_types[idx].clone())
|
|
1338
|
+
}
|
|
1256
|
-
|
|
1339
|
+
[] => Ok(PlumType::TVar("_".to_string())),
|
|
1340
|
+
_ => Err(format!(
|
|
1341
|
+
"field '{}' is ambiguous across multiple variants of enum '{}' — use a match",
|
|
1342
|
+
field_name, class_name
|
|
1343
|
+
)),
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1257
1346
|
},
|
|
1258
1347
|
}
|
|
1259
1348
|
}
|
|
@@ -1288,7 +1377,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1288
1377
|
ast::Arg::Pair { value, .. } => value,
|
|
1289
1378
|
};
|
|
1290
1379
|
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
1291
|
-
|
|
1380
|
+
unifyArg(expected, &actual, arg_expr, ctx)
|
|
1292
1381
|
.map_err(|e| format!("method '{}.{}' arg {}: {}", class_name, call.name, i, e))?;
|
|
1293
1382
|
}
|
|
1294
1383
|
for (i, arg) in call.args.iter().enumerate().skip(fixed.len()) {
|
|
@@ -1298,7 +1387,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1298
1387
|
ast::Arg::Pair { value, .. } => value,
|
|
1299
1388
|
};
|
|
1300
1389
|
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
1301
|
-
|
|
1390
|
+
unifyArg(elem, &actual, arg_expr, ctx)
|
|
1302
1391
|
.map_err(|e| format!("method '{}.{}' variadic arg {}: {}", class_name, call.name, i, e))?;
|
|
1303
1392
|
}
|
|
1304
1393
|
Ok(*ret.clone())
|
|
@@ -1322,7 +1411,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1322
1411
|
ast::Arg::Pair { value, .. } => value,
|
|
1323
1412
|
};
|
|
1324
1413
|
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
1325
|
-
|
|
1414
|
+
unifyArg(expected, &actual, arg_expr, ctx)
|
|
1326
1415
|
.map_err(|e| format!("method '{}.{}' arg {}: {}", class_name, call.name, i, e))?;
|
|
1327
1416
|
}
|
|
1328
1417
|
Ok(*ret.clone())
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
2
|
use plum_checker::types::*;
|
|
3
|
-
use plum_checker::{checkSource, plumTypeFromAst, unify};
|
|
3
|
+
use plum_checker::{checkSource, plumTypeFromAst, unify, CheckCtx};
|
|
4
4
|
use plum_core::ast::Type as AstType;
|
|
5
5
|
use plum_core::{ast::*, AstParser};
|
|
6
6
|
|
|
7
|
+
fn emptyCtx() -> CheckCtx<'static> {
|
|
8
|
+
use std::sync::OnceLock;
|
|
9
|
+
static CLASSES: OnceLock<plum_checker::ClassEnv> = OnceLock::new();
|
|
10
|
+
static METHODS: OnceLock<plum_checker::MethodEnv> = OnceLock::new();
|
|
11
|
+
static ENUM_VARIANTS: OnceLock<plum_checker::EnumVariants> = OnceLock::new();
|
|
12
|
+
static ENUM_PARAMS: OnceLock<plum_checker::EnumParams> = OnceLock::new();
|
|
13
|
+
static MIN_REQUIRED: OnceLock<plum_checker::MinRequiredArgs> = OnceLock::new();
|
|
14
|
+
CheckCtx {
|
|
15
|
+
classes: CLASSES.get_or_init(Default::default),
|
|
16
|
+
methods: METHODS.get_or_init(Default::default),
|
|
17
|
+
enum_variants: ENUM_VARIANTS.get_or_init(Default::default),
|
|
18
|
+
enum_params: ENUM_PARAMS.get_or_init(Default::default),
|
|
19
|
+
min_required: MIN_REQUIRED.get_or_init(Default::default),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
fn parse(src: &str) -> Source {
|
|
8
24
|
let mut parser = tree_sitter::Parser::new();
|
|
9
25
|
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
@@ -26,13 +42,15 @@ fn astTypeUnknownMapsToNamed() {
|
|
|
26
42
|
|
|
27
43
|
#[test]
|
|
28
44
|
fn unifySameTypesOk() {
|
|
45
|
+
let ctx = emptyCtx();
|
|
29
|
-
assert!(unify(&PlumType::TInt, &PlumType::TInt).is_ok());
|
|
46
|
+
assert!(unify(&PlumType::TInt, &PlumType::TInt, &ctx).is_ok());
|
|
30
|
-
assert!(unify(&PlumType::TFloat, &PlumType::TFloat).is_ok());
|
|
47
|
+
assert!(unify(&PlumType::TFloat, &PlumType::TFloat, &ctx).is_ok());
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
#[test]
|
|
34
51
|
fn unifyDifferentTypesErr() {
|
|
52
|
+
let ctx = emptyCtx();
|
|
35
|
-
assert!(unify(&PlumType::TInt, &PlumType::TFloat).is_err());
|
|
53
|
+
assert!(unify(&PlumType::TInt, &PlumType::TFloat, &ctx).is_err());
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
#[test]
|
|
@@ -852,6 +870,40 @@ fun useSumAll() -> Int =
|
|
|
852
870
|
assert!(checkSource(&source).is_ok(), "expected Ok");
|
|
853
871
|
}
|
|
854
872
|
|
|
873
|
+
#[test]
|
|
874
|
+
fn enumVariantAsParamTypeAcceptsThatVariant() {
|
|
875
|
+
let src = "\
|
|
876
|
+
enum Color =
|
|
877
|
+
| Red
|
|
878
|
+
| Blue
|
|
879
|
+
|
|
880
|
+
fun stringifyColor(v: Red) -> Int =
|
|
881
|
+
0
|
|
882
|
+
|
|
883
|
+
fun useIt() -> Int =
|
|
884
|
+
stringifyColor(Red)
|
|
885
|
+
";
|
|
886
|
+
let source = parse(src);
|
|
887
|
+
assert!(checkSource(&source).is_ok(), "expected Ok");
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
#[test]
|
|
891
|
+
fn enumVariantAsParamTypeRejectsADifferentVariant() {
|
|
892
|
+
let src = "\
|
|
893
|
+
enum Color =
|
|
894
|
+
| Red
|
|
895
|
+
| Blue
|
|
896
|
+
|
|
897
|
+
fun stringifyColor(v: Red) -> Int =
|
|
898
|
+
0
|
|
899
|
+
|
|
900
|
+
fun useIt() -> Int =
|
|
901
|
+
stringifyColor(Blue)
|
|
902
|
+
";
|
|
903
|
+
let source = parse(src);
|
|
904
|
+
assert!(checkSource(&source).is_err());
|
|
905
|
+
}
|
|
906
|
+
|
|
855
907
|
#[test]
|
|
856
908
|
fn variadicCallWithMismatchedTrailingArgTypeIsError() {
|
|
857
909
|
let src = "\
|
|
@@ -1132,3 +1184,43 @@ fun bad() -> Shape =
|
|
|
1132
1184
|
let errs = result.unwrap_err();
|
|
1133
1185
|
assert!(errs.iter().any(|e| e.message.contains("unknown field 'z' on variant 'Square'")), "got: {:?}", errs);
|
|
1134
1186
|
}
|
|
1187
|
+
|
|
1188
|
+
#[test]
|
|
1189
|
+
fn fieldAccessOnMultiVariantEnumValueResolvesViaTheUniqueOwningVariant() {
|
|
1190
|
+
// `radius` belongs to exactly one of `Shape`'s variants, so `.radius` on a
|
|
1191
|
+
// `Shape`-typed value type-checks even though `Shape` itself has more than
|
|
1192
|
+
// one variant — codegen compiles it as a checked downcast to `Circle`.
|
|
1193
|
+
let src = "\
|
|
1194
|
+
enum Shape =
|
|
1195
|
+
| Circle(radius: Int)
|
|
1196
|
+
| Square(side: Int)
|
|
1197
|
+
|
|
1198
|
+
fun getRadius(s: Shape) -> Int =
|
|
1199
|
+
r := Circle(radius: 5)
|
|
1200
|
+
return r.radius
|
|
1201
|
+
";
|
|
1202
|
+
let source = parse(src);
|
|
1203
|
+
let result = checkSource(&source);
|
|
1204
|
+
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
#[test]
|
|
1208
|
+
fn fieldAccessOnMultiVariantEnumValueIsAmbiguousAcrossSameNamedFields() {
|
|
1209
|
+
let src = "\
|
|
1210
|
+
enum Shape =
|
|
1211
|
+
| Circle(radius: Int)
|
|
1212
|
+
| Square(radius: Int)
|
|
1213
|
+
|
|
1214
|
+
fun bad() -> Int =
|
|
1215
|
+
r := Circle(radius: 5)
|
|
1216
|
+
return r.radius
|
|
1217
|
+
";
|
|
1218
|
+
let source = parse(src);
|
|
1219
|
+
let result = checkSource(&source);
|
|
1220
|
+
assert!(result.is_err());
|
|
1221
|
+
let errs = result.unwrap_err();
|
|
1222
|
+
assert!(
|
|
1223
|
+
errs.iter().any(|e| e.message.contains("field 'radius' is ambiguous across multiple variants of enum 'Shape'")),
|
|
1224
|
+
"got: {:?}", errs
|
|
1225
|
+
);
|
|
1226
|
+
}
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -4456,20 +4456,41 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
|
|
|
4456
4456
|
// `buildGcTypeRegistry`'s `EnumSuper` arm) — no `ref.cast` to any
|
|
4457
4457
|
// particular variant needed, since every variant has the exact
|
|
4458
4458
|
// same field list as the supertype itself.
|
|
4459
|
+
None => match ctx.enum_params.get(&class_name) {
|
|
4460
|
+
Some(params) => {
|
|
4461
|
+
let field_idx = params
|
|
4462
|
+
.iter()
|
|
4463
|
+
.position(|(n, _)| n == field_name)
|
|
4464
|
+
.ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
|
|
4465
|
+
let super_type_idx = *ctx.gc_types.enum_super_type_idx.get(&class_name)
|
|
4466
|
+
.ok_or_else(|| format!("codegen: enum '{}' missing from the GC type registry", class_name))?;
|
|
4467
|
+
compileExpr(&attr.object, body, ctx, state)?;
|
|
4468
|
+
Instruction::StructGet { struct_type_index: super_type_idx, field_index: field_idx as u32 }.encode(body);
|
|
4469
|
+
}
|
|
4470
|
+
// Not a discriminant enum either: `class_name` is an
|
|
4471
|
+
// ordinary enum, and `plum-checker`'s `inferExpr` already
|
|
4472
|
+
// verified exactly ONE of its variants declares a field
|
|
4473
|
+
// with this name (ambiguity across sibling variants is a
|
|
4474
|
+
// checker error, never reaches codegen). Compile `.field`
|
|
4475
|
+
// as a checked downcast to that one variant's own
|
|
4476
|
+
// concrete struct type (same `ref.cast` idiom `match`
|
|
4477
|
+
// pattern-matching already uses, e.g. `compileCasePositions`),
|
|
4478
|
+
// then read the field off it — traps at runtime if the
|
|
4479
|
+
// value turns out to be a different variant, so no static
|
|
4480
|
+
// proof the value IS that variant is required here.
|
|
4459
|
-
|
|
4481
|
+
None => {
|
|
4460
|
-
|
|
4482
|
+
let (variant_name, info) = ctx.enum_variants.iter()
|
|
4461
|
-
.enum_params
|
|
4462
|
-
.get(&class_name)
|
|
4463
|
-
|
|
4483
|
+
.find(|(_, info)| info.enum_name == class_name && info.field_names.iter().any(|n| n == field_name))
|
|
4464
|
-
let field_idx = params
|
|
4465
|
-
.iter()
|
|
4466
|
-
.position(|(n, _)| n == field_name)
|
|
4467
|
-
|
|
4484
|
+
.ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
|
|
4485
|
+
let field_idx = info.field_names.iter().position(|n| n == field_name)
|
|
4486
|
+
.expect("just found by this field name");
|
|
4468
|
-
|
|
4487
|
+
let variant_type_idx = *ctx.gc_types.variant_type_idx.get(variant_name)
|
|
4469
|
-
|
|
4488
|
+
.ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", variant_name))?;
|
|
4470
|
-
|
|
4489
|
+
compileExpr(&attr.object, body, ctx, state)?;
|
|
4490
|
+
Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx)).encode(body);
|
|
4471
|
-
|
|
4491
|
+
Instruction::StructGet { struct_type_index: variant_type_idx, field_index: field_idx as u32 }.encode(body);
|
|
4472
|
-
|
|
4492
|
+
}
|
|
4493
|
+
},
|
|
4473
4494
|
}
|
|
4474
4495
|
}
|
|
4475
4496
|
ast::AttrKind::Method(call) => {
|