plum

#treesitter#compiler#wasm

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

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


7ac1d37Peter 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 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 -> owning enum name, e.g. `"True" -> "Bool"`.
56
+ /// Enum variant name -> its info, e.g. `"True" -> { enum_name: "Bool", tag: 1, field_types: [] }`.
48
- pub type EnumVariants = BTreeMap<String, 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 &e.variants {
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(), e.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 an unconstrained type since enum variants don't carry per-field type info (v1.5).
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: _, fields } => {
368
+ ast::CasePattern::Class { name, fields } => match ctx.enum_variants.get(name) {
353
- for f in fields {
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
- check_pattern(f, &PlumType::TVar("_".to_string()), env, ctx)?;
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
- Ok(())
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
- other => Ok(PlumType::TNamed(other.to_string())),
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
+ }