plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
7ac1d37
— Peter John
2026-07-20T11:33:47+05:30
feat(plum-checker): general enum variant tags, field types, and construction checks
- plum-checker/src/lib.rs +66 -13
- plum-checker/tests/checker_tests.rs +101 -0
plum-checker/src/lib.rs
CHANGED
|
@@ -44,8 +44,17 @@ pub fn lookup(env: &TypeEnv, name: &str) -> Result<PlumType, String> {
|
|
|
44
44
|
pub type ClassEnv = BTreeMap<String, Vec<(String, PlumType)>>;
|
|
45
45
|
/// `(receiver type, method name) -> TFun` for every `name<Receiver>(...)` method.
|
|
46
46
|
pub type MethodEnv = BTreeMap<(String, String), PlumType>;
|
|
47
|
+
/// Info about one `enum` variant: which enum it belongs to, its 0-based runtime tag
|
|
48
|
+
/// (numbering is shared across all of that enum's variants), and its payload field
|
|
49
|
+
/// types (empty for a payload-free variant like `Red` or `None`).
|
|
50
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
51
|
+
pub struct EnumVariantInfo {
|
|
52
|
+
pub enum_name: String,
|
|
53
|
+
pub tag: i32,
|
|
54
|
+
pub field_types: Vec<PlumType>,
|
|
55
|
+
}
|
|
47
|
-
/// Enum variant name ->
|
|
56
|
+
/// Enum variant name -> its info, e.g. `"True" -> { enum_name: "Bool", tag: 1, field_types: [] }`.
|
|
48
|
-
pub type EnumVariants = BTreeMap<String,
|
|
57
|
+
pub type EnumVariants = BTreeMap<String, EnumVariantInfo>;
|
|
49
58
|
|
|
50
59
|
/// Shared, read-only lookup tables built once from the whole source, threaded through
|
|
51
60
|
/// every check/infer call alongside the (mutable, scope-local) `TypeEnv`.
|
|
@@ -66,8 +75,8 @@ pub fn build_global_tables(source: &ast::Source) -> (TypeEnv, ClassEnv, MethodEn
|
|
|
66
75
|
let mut enum_variants: EnumVariants = BTreeMap::new();
|
|
67
76
|
// `Bool`'s variants are built in (see `infer_expr`'s TypeName handling) rather
|
|
68
77
|
// than requiring every source file to redeclare `enum Bool = | True | False`.
|
|
69
|
-
enum_variants.insert("True".to_string(), "Bool".to_string());
|
|
78
|
+
enum_variants.insert("True".to_string(), EnumVariantInfo { enum_name: "Bool".to_string(), tag: 1, field_types: vec![] });
|
|
70
|
-
enum_variants.insert("False".to_string(), "Bool".to_string());
|
|
79
|
+
enum_variants.insert("False".to_string(), EnumVariantInfo { enum_name: "Bool".to_string(), tag: 0, field_types: vec![] });
|
|
71
80
|
|
|
72
81
|
// First pass: register class fields and enum variants so later passes can
|
|
73
82
|
// resolve `self.field`, `ClassName(...)`, and bare enum-tag patterns.
|
|
@@ -80,8 +89,15 @@ pub fn build_global_tables(source: &ast::Source) -> (TypeEnv, ClassEnv, MethodEn
|
|
|
80
89
|
classes.insert(c.name.clone(), fields);
|
|
81
90
|
}
|
|
82
91
|
ast::Item::Enum(e) => {
|
|
83
|
-
for v in
|
|
92
|
+
for (tag, v) in e.variants.iter().enumerate() {
|
|
93
|
+
let field_types = v.fields.iter()
|
|
94
|
+
.map(|f| plum_type_from_ast(&ast::Type { name: f.clone(), generics: vec![] }))
|
|
95
|
+
.collect();
|
|
84
|
-
enum_variants.insert(v.name.clone(),
|
|
96
|
+
enum_variants.insert(v.name.clone(), EnumVariantInfo {
|
|
97
|
+
enum_name: e.name.clone(),
|
|
98
|
+
tag: tag as i32,
|
|
99
|
+
field_types,
|
|
100
|
+
});
|
|
85
101
|
}
|
|
86
102
|
}
|
|
87
103
|
_ => {}
|
|
@@ -332,7 +348,7 @@ fn check_match(m: &ast::Match, env: &TypeEnv, declared_ret: &PlumType, fn_name:
|
|
|
332
348
|
|
|
333
349
|
/// Checks a single case pattern against the type of the subject it matches, binding any
|
|
334
350
|
/// new names it introduces into `env`. Constructor-payload sub-patterns (`Some(x)`) bind
|
|
335
|
-
/// against
|
|
351
|
+
/// against that variant's declared field types (see `EnumVariantInfo::field_types`).
|
|
336
352
|
fn check_pattern(pat: &ast::CasePattern, subject_ty: &PlumType, env: &mut TypeEnv, ctx: &CheckCtx) -> Result<(), String> {
|
|
337
353
|
match pat {
|
|
338
354
|
ast::CasePattern::Wildcard => Ok(()),
|
|
@@ -349,12 +365,27 @@ fn check_pattern(pat: &ast::CasePattern, subject_ty: &PlumType, env: &mut TypeEn
|
|
|
349
365
|
Ok(())
|
|
350
366
|
}
|
|
351
367
|
}
|
|
352
|
-
ast::CasePattern::Class { name
|
|
368
|
+
ast::CasePattern::Class { name, fields } => match ctx.enum_variants.get(name) {
|
|
353
|
-
|
|
369
|
+
Some(info) => {
|
|
370
|
+
if fields.len() != info.field_types.len() {
|
|
371
|
+
return Err(format!(
|
|
372
|
+
"constructor pattern '{}' expects {} field(s), got {}",
|
|
373
|
+
name, info.field_types.len(), fields.len()
|
|
374
|
+
));
|
|
375
|
+
}
|
|
376
|
+
for (f, fty) in fields.iter().zip(info.field_types.iter()) {
|
|
354
|
-
|
|
377
|
+
check_pattern(f, fty, env, ctx)?;
|
|
378
|
+
}
|
|
379
|
+
Ok(())
|
|
355
380
|
}
|
|
381
|
+
// Unmodeled/builtin variant: allow, codegen will catch.
|
|
382
|
+
None => {
|
|
383
|
+
for f in fields {
|
|
384
|
+
check_pattern(f, &PlumType::TVar("_".to_string()), env, ctx)?;
|
|
385
|
+
}
|
|
356
|
-
|
|
386
|
+
Ok(())
|
|
357
|
-
|
|
387
|
+
}
|
|
388
|
+
},
|
|
358
389
|
}
|
|
359
390
|
}
|
|
360
391
|
|
|
@@ -367,7 +398,11 @@ pub fn infer_expr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plu
|
|
|
367
398
|
ast::Expr::Self_ => lookup(env, "self"),
|
|
368
399
|
ast::Expr::TypeName(n) => match n.as_str() {
|
|
369
400
|
"True" | "False" => Ok(PlumType::TBool),
|
|
401
|
+
_ => match ctx.enum_variants.get(n) {
|
|
402
|
+
Some(info) => Ok(PlumType::TNamed(info.enum_name.clone())),
|
|
403
|
+
// Unmodeled/builtin type name: allow, codegen will catch.
|
|
370
|
-
|
|
404
|
+
None => Ok(PlumType::TNamed(n.to_string())),
|
|
405
|
+
},
|
|
371
406
|
},
|
|
372
407
|
ast::Expr::Paren(inner) => infer_expr(inner, env, ctx),
|
|
373
408
|
ast::Expr::Not(inner) => {
|
|
@@ -407,6 +442,24 @@ pub fn infer_expr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plu
|
|
|
407
442
|
Ok(tt)
|
|
408
443
|
}
|
|
409
444
|
ast::Expr::FnCall(call) => {
|
|
445
|
+
if let Some(info) = ctx.enum_variants.get(&call.name) {
|
|
446
|
+
if call.args.len() != info.field_types.len() {
|
|
447
|
+
return Err(format!(
|
|
448
|
+
"variant '{}': expected {} arg(s), got {}",
|
|
449
|
+
call.name, info.field_types.len(), call.args.len()
|
|
450
|
+
));
|
|
451
|
+
}
|
|
452
|
+
for (i, (arg, expected)) in call.args.iter().zip(info.field_types.iter()).enumerate() {
|
|
453
|
+
let arg_expr = match arg {
|
|
454
|
+
ast::Arg::Positional(e) => e,
|
|
455
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
456
|
+
ast::Arg::Pair { value, .. } => value,
|
|
457
|
+
};
|
|
458
|
+
let actual = infer_expr(arg_expr, env, ctx)?;
|
|
459
|
+
unify(expected, &actual).map_err(|e| format!("variant '{}' arg {}: {}", call.name, i, e))?;
|
|
460
|
+
}
|
|
461
|
+
return Ok(PlumType::TNamed(info.enum_name.clone()));
|
|
462
|
+
}
|
|
410
463
|
match lookup(env, &call.name) {
|
|
411
464
|
Ok(PlumType::TFun(param_types, ret)) => {
|
|
412
465
|
if call.args.len() != param_types.len() {
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -191,3 +191,104 @@ fn match_int_pattern_against_str_subject_is_error() {
|
|
|
191
191
|
let result = check_source(&source);
|
|
192
192
|
assert!(result.is_err());
|
|
193
193
|
}
|
|
194
|
+
|
|
195
|
+
#[test]
|
|
196
|
+
fn bare_enum_tag_unifies_with_owning_enum_type() {
|
|
197
|
+
// Regression: a bare non-Bool tag like `None` used to type as `TNamed("None")`
|
|
198
|
+
// (itself, not its enum), so comparing it against an `Option` value would wrongly
|
|
199
|
+
// fail with a type mismatch.
|
|
200
|
+
let src = "\
|
|
201
|
+
enum Option =
|
|
202
|
+
| Some(Int)
|
|
203
|
+
| None
|
|
204
|
+
|
|
205
|
+
isNone(o: Option) -> Bool =
|
|
206
|
+
o == None
|
|
207
|
+
";
|
|
208
|
+
let source = parse(src);
|
|
209
|
+
let result = check_source(&source);
|
|
210
|
+
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[test]
|
|
214
|
+
fn variant_construction_checks_arg_count_and_types() {
|
|
215
|
+
let src = "\
|
|
216
|
+
enum Option =
|
|
217
|
+
| Some(Int)
|
|
218
|
+
| None
|
|
219
|
+
|
|
220
|
+
makeSome(v: Int) -> Option =
|
|
221
|
+
Some(v)
|
|
222
|
+
";
|
|
223
|
+
let source = parse(src);
|
|
224
|
+
let result = check_source(&source);
|
|
225
|
+
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#[test]
|
|
229
|
+
fn variant_construction_wrong_arg_type_is_error() {
|
|
230
|
+
let src = "\
|
|
231
|
+
enum Option =
|
|
232
|
+
| Some(Int)
|
|
233
|
+
| None
|
|
234
|
+
|
|
235
|
+
bad() -> Option =
|
|
236
|
+
Some(\"x\")
|
|
237
|
+
";
|
|
238
|
+
let source = parse(src);
|
|
239
|
+
let result = check_source(&source);
|
|
240
|
+
assert!(result.is_err());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[test]
|
|
244
|
+
fn variant_construction_wrong_arg_count_is_error() {
|
|
245
|
+
let src = "\
|
|
246
|
+
enum Shape =
|
|
247
|
+
| Rect(Float, Float)
|
|
248
|
+
| Circle(Float)
|
|
249
|
+
|
|
250
|
+
bad() -> Shape =
|
|
251
|
+
Rect(1.0)
|
|
252
|
+
";
|
|
253
|
+
let source = parse(src);
|
|
254
|
+
let result = check_source(&source);
|
|
255
|
+
assert!(result.is_err());
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#[test]
|
|
259
|
+
fn constructor_pattern_binds_fields_to_declared_types() {
|
|
260
|
+
let src = "\
|
|
261
|
+
enum Shape =
|
|
262
|
+
| Rect(Float, Float)
|
|
263
|
+
| Circle(Float)
|
|
264
|
+
|
|
265
|
+
area(s: Shape) -> Float =
|
|
266
|
+
match s
|
|
267
|
+
Rect(w, h) =>
|
|
268
|
+
w * h
|
|
269
|
+
Circle(r) =>
|
|
270
|
+
r * r
|
|
271
|
+
";
|
|
272
|
+
let source = parse(src);
|
|
273
|
+
let result = check_source(&source);
|
|
274
|
+
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#[test]
|
|
278
|
+
fn constructor_pattern_wrong_field_count_is_error() {
|
|
279
|
+
let src = "\
|
|
280
|
+
enum Shape =
|
|
281
|
+
| Rect(Float, Float)
|
|
282
|
+
| Circle(Float)
|
|
283
|
+
|
|
284
|
+
bad(s: Shape) -> Float =
|
|
285
|
+
match s
|
|
286
|
+
Rect(w) =>
|
|
287
|
+
w
|
|
288
|
+
_ =>
|
|
289
|
+
0.0
|
|
290
|
+
";
|
|
291
|
+
let source = parse(src);
|
|
292
|
+
let result = check_source(&source);
|
|
293
|
+
assert!(result.is_err());
|
|
294
|
+
}
|