plum

#treesitter#compiler#wasm

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

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


5f2f962Peter John 2026-09-08T12:01:22+05:30
feat(lang): remove type/class entirely, enum is the only declaration form
Files changed (42) hide show
  1. README.md +18 -12
  2. plum-checker/src/lib.rs +128 -184
  3. plum-checker/src/monomorphize.rs +158 -367
  4. plum-checker/tests/checker_tests.rs +49 -82
  5. plum-checker/tests/monomorphize_tests.rs +2 -27
  6. plum-core/src/ast.rs +6 -16
  7. plum-core/src/builtin_usage.rs +0 -5
  8. plum-core/src/loader.rs +11 -11
  9. plum-core/src/parser.rs +15 -50
  10. plum-core/tests/formatter_test.rs +3 -4
  11. plum-core/tests/loader_test.rs +4 -4
  12. plum-core/tests/parser_test.rs +8 -8
  13. plum-examples/closures.plum +2 -2
  14. plum-examples/functions.plum +6 -6
  15. plum-examples/match.plum +2 -3
  16. plum-examples/methods.plum +4 -6
  17. plum-examples/oop_visitor.plum +8 -17
  18. plum-examples/types.plum +19 -24
  19. plum-std/Array.plum +3 -1
  20. plum-std/Buffer.plum +2 -3
  21. plum-std/Byte.plum +3 -1
  22. plum-std/ByteSlice.plum +3 -1
  23. plum-std/Http.plum +2 -4
  24. plum-std/Json.plum +4 -6
  25. plum-std/List.plum +4 -8
  26. plum-std/Map.plum +4 -6
  27. plum-std/Str.plum +2 -2
  28. plum-std/Time.plum +6 -8
  29. plum-std/Uuid.plum +2 -2
  30. plum-tooling/tree-sitter-plum/grammar.js +11 -16
  31. plum-tooling/tree-sitter-plum/queries/plum/format.scm +7 -32
  32. plum-tooling/tree-sitter-plum/queries/plum/highlights.scm +1 -1
  33. plum-tooling/tree-sitter-plum/queries/plum/indents.scm +0 -2
  34. plum-tooling/tree-sitter-plum/queries/plum/tags.scm +0 -3
  35. plum-tooling/tree-sitter-plum/queries/plum/textobjects.scm +0 -3
  36. plum-tooling/tree-sitter-plum/src/grammar.json +0 -0
  37. plum-tooling/tree-sitter-plum/src/node-types.json +0 -0
  38. plum-tooling/tree-sitter-plum/src/parser.c +0 -0
  39. plum-tooling/tree-sitter-plum/test/corpus/type.txt +64 -54
  40. plum-tooling/tree-sitter-plum/test/highlight/sample.plum +2 -3
  41. plum-wasm-codegen/src/lib.rs +158 -277
  42. plum-wasm-codegen/tests/codegen_tests.rs +6 -9
README.md CHANGED
@@ -170,13 +170,14 @@ $ plum test plum-examples/testing.plum
170
170
 
171
171
  ## Types: records, traits, enums
172
172
 
173
+ There's a single declaration form, `enum` — a record type is just a single-variant enum whose one variant shares the enum's own name:
174
+
173
175
  ```plum
174
- type Point =
176
+ enum Point =
175
- x: Int
177
+ | Point(x: Int, y: Int)
176
- y: Int
177
178
 
178
- type Named(ToStr) = # implements ToStr
179
+ enum Named(ToStr) = # implements ToStr
179
- name: Str
180
+ | Named(name: Str)
180
181
 
181
182
  trait Shape =
182
183
  area() -> Float
@@ -203,8 +204,8 @@ enum Step(n: Int) = # a shared field on every variant ...
203
204
  ## Generics
204
205
 
205
206
  ```plum
206
- type Box[T] =
207
+ enum Box[T] =
207
- value: T
208
+ | Box(value: T)
208
209
 
209
210
  trait Comparable[T: Ord] = # bounded generic param
210
211
  compareTo(other: T) -> Int
@@ -235,12 +236,11 @@ A closure literal is `|params| body`. Capture is snapshot-by-value: a closure co
235
236
 
236
237
  ## `self`, field access, and methods
237
238
 
238
- A `fun` declared indented directly inside a `type`/`enum` body is a method, with an implicit `self`:
239
+ A `fun` declared indented directly inside an `enum` body is a method, with an implicit `self`:
239
240
 
240
241
  ```plum
241
- type Cat =
242
+ enum Cat =
242
- name: Str
243
+ | Cat(name: Str, age: Int)
243
- age: Int
244
244
  fun getAge(self) -> Int =
245
245
  self.age
246
246
  fun birthday(self) -> Int =
@@ -259,7 +259,13 @@ fun main() -> Int =
259
259
  Cat(name: "Whiskers", age: 3)
260
260
  ```
261
261
 
262
- Construct a `type` value by calling its name with `field: value` pairs (any order; every field required).
262
+ Construct a record-shaped enum value by calling its name with `field: value` pairs (any order; every field required).
263
+
264
+ Fields are mutable (`c.age = c.age + 1`), and a Gleam-style spread updates a copy from an existing value, overriding just the fields you name:
265
+
266
+ ```plum
267
+ older := Cat(..c, age: c.age + 1)
268
+ ```
263
269
 
264
270
  ## `match`
265
271
 
plum-checker/src/lib.rs CHANGED
@@ -163,8 +163,6 @@ pub fn lookup(env: &TypeEnv, name: &str) -> Result<PlumType, String> {
163
163
  .ok_or_else(|| format!("undefined name '{}'", name))
164
164
  }
165
165
 
166
- /// Field names and types for every `type ClassName = ...` declaration in the source.
167
- pub type ClassEnv = BTreeMap<String, Vec<(String, PlumType)>>;
168
166
  /// `(receiver type, method name) -> TFun` for every `name<Receiver>(...)` method.
169
167
  pub type MethodEnv = BTreeMap<(String, String), PlumType>;
170
168
  /// Info about one `enum` variant: which enum it belongs to, its 0-based runtime tag
@@ -221,7 +219,6 @@ pub fn buildMinRequiredArgs(source: &ast::Source) -> MinRequiredArgs {
221
219
  /// Shared, read-only lookup tables built once from the whole source, threaded through
222
220
  /// every check/infer call alongside the (mutable, scope-local) `TypeEnv`.
223
221
  pub struct CheckCtx<'a> {
224
- pub classes: &'a ClassEnv,
225
222
  pub methods: &'a MethodEnv,
226
223
  pub enum_variants: &'a EnumVariants,
227
224
  pub enum_params: &'a EnumParams,
@@ -232,27 +229,23 @@ pub struct CheckCtx<'a> {
232
229
  /// signatures, enum variants) from a whole source. Shared by `checkSource` and by
233
230
  /// `plum-wasm-codegen`, which needs the same tables to resolve `self`, field access,
234
231
  /// and method dispatch during code generation.
235
- pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, ClassEnv, MethodEnv, EnumVariants, EnumParams) {
232
+ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVariants, EnumParams) {
236
233
  let mut global_env: TypeEnv = TypeEnv::new();
237
- let mut classes: ClassEnv = BTreeMap::new();
238
234
  let mut methods: MethodEnv = BTreeMap::new();
239
235
  let mut enum_variants: EnumVariants = BTreeMap::new();
240
236
  let mut enum_params: EnumParams = BTreeMap::new();
241
237
 
242
- // First pass: register class fields, THEN enum variants, so a bare variant
238
+ // First pass: find every "record-shaped" enum exactly one variant, whose
239
+ // name equals the enum's own name (the `type X = ...` replacement shape,
243
- // (`| FantasyBook`, no `[...]` payload) that names an already-declared
240
+ // e.g. `enum Cat = | Cat(name: Str)`) so a bare variant elsewhere naming
244
- // `type` can be recognized as sugar for wrapping that whole class as its
241
+ // one can wrap it as sugar (see the bare-wrap arm below), regardless of
245
- // single payload field — regardless of whether the `type` or the `enum`
246
- // appears first in the file. Resolves `self.field`, `ClassName(...)`, and
247
- // bare enum-tag patterns for later passes.
242
+ // which declaration appears first in the file.
248
- for item in &source.items {
243
+ let record_shaped: std::collections::BTreeSet<&str> = source.items.iter()
249
- if let ast::Item::Class(c) = item {
244
+ .filter_map(|item| match item {
250
- let fields = c.fields.iter()
251
- .map(|f| (f.name.clone(), plumTypeFromAst(&f.ty)))
245
+ ast::Item::Enum(e) if e.variants.len() == 1 && e.variants[0].name == e.name => Some(e.name.as_str()),
246
+ _ => None,
247
+ })
252
- .collect();
248
+ .collect();
253
- classes.insert(c.name.clone(), fields);
254
- }
255
- }
256
249
  for item in &source.items {
257
250
  match item {
258
251
  ast::Item::Enum(e) => {
@@ -274,31 +267,25 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, ClassEnv, MethodEnv,
274
267
  // `plum-std/Number.plum`) — the wrapped payload is the
275
268
  // dedicated primitive `PlumType` itself (`TInt`/
276
269
  // `TFloat`), not a `TNamed` self-reference. Checked
277
- // BEFORE the class bare-wrap case below: historically
270
+ // BEFORE the record-shaped bare-wrap case below, since
278
- // `Int`/`Float` were ALSO registered as zero-field
279
- // "method-holder" classes (`type Int = fun
280
- // abs(self)...`, attaching methods to the primitive
281
- // receiver, before their methods moved into `Number`
282
- // itself) — this ordering is what made a real
283
- // primitive win that name collision over the
284
- // incidentally-same-named class, and stays a
285
- // defensive safeguard against a future same-named
286
- // class reintroducing it. This is what lets a bare
287
- // `Int`/`Float` value be used directly wherever
271
+ // `Int`/`Float` have no such enum of their own to wrap.
272
+ // This is what lets a bare `Int`/`Float` value be used
288
- // `Number` is expected with no wrapper syntax — see
273
+ // directly wherever `Number` is expected with no wrapper
289
- // `unify`'s matching
274
+ // syntax — see `unify`'s matching
290
275
  // primitive-vs-enum arm, and
291
276
  // `monomorphize::wrapPrimitiveAgainstExpected`, which
292
277
  // does the actual AST rewrite into this variant's
293
278
  // constructor at specific expected-type usage sites.
294
279
  (vec![plumTypeFromName(&v.name)], Vec::new())
295
- } else if v.fields.is_empty() && classes.contains_key(&v.name) {
280
+ } else if v.fields.is_empty() && e.name != v.name && record_shaped.contains(v.name.as_str()) {
296
- // Bare-type variant sugar: `| FantasyBook` (no `[...]`)
281
+ // Bare-type variant sugar: `| Cat` (no `[...]`) naming a
297
- // naming a class declared elsewhere means "this variant
282
+ // DIFFERENT, already-declared record-shaped enum means "this
298
- // wraps one payload field of that same-named class" —
283
+ // variant wraps one payload field of that whole record" —
299
- // shorthand for the equivalent `| FantasyBook[FantasyBook]`,
284
+ // shorthand for the equivalent `| Cat[Cat]`, so `enum Animal
285
+ // = | Cat | Dog` needs no separate wrapper tag distinct from
286
+ // the record it holds. Excludes `e.name == v.name` (a plain
300
- // so `enum Book = | FantasyBook | ScifiBook` needs no
287
+ // payload-free singleton like `enum Foo = | Foo`, which is
301
- // separate wrapper tag distinct from the record it holds.
288
+ // already exactly what it says nothing else to wrap).
302
289
  (vec![PlumType::TNamed(v.name.clone())], Vec::new())
303
290
  } else {
304
291
  (v.fields.iter().map(plumTypeFromAst).collect(), v.field_names.clone())
@@ -359,7 +346,7 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, ClassEnv, MethodEnv,
359
346
  }
360
347
  }
361
348
 
362
- (global_env, classes, methods, enum_variants, enum_params)
349
+ (global_env, methods, enum_variants, enum_params)
363
350
  }
364
351
 
365
352
  /// A `Type`'s name for comparison purposes only — deliberately ignores `.generics`
@@ -406,21 +393,21 @@ fn checkTraitConformance(source: &ast::Source) -> Vec<CheckError> {
406
393
  .collect();
407
394
 
408
395
  for item in &source.items {
409
- let ast::Item::Class(c) = item else { continue };
396
+ let ast::Item::Enum(e) = item else { continue };
410
- for trait_name in &c.implements {
397
+ for trait_name in &e.implements {
411
398
  let Some(tr) = traits.get(trait_name.as_str()) else { continue };
412
399
  for tm in &tr.methods {
413
- let Some(f) = methods_by_receiver.get(&(c.name.as_str(), tm.name.as_str())) else {
400
+ let Some(f) = methods_by_receiver.get(&(e.name.as_str(), tm.name.as_str())) else {
414
401
  errors.push(CheckError {
415
- message: format!("class '{}' claims to implement trait '{}' but is missing method '{}'", c.name, trait_name, tm.name),
402
+ message: format!("enum '{}' claims to implement trait '{}' but is missing method '{}'", e.name, trait_name, tm.name),
416
403
  });
417
404
  continue;
418
405
  };
419
406
  if f.params.len() != tm.params.len() {
420
407
  errors.push(CheckError {
421
408
  message: format!(
422
- "class '{}' method '{}' (implementing trait '{}'): expected {} param(s), got {}",
409
+ "enum '{}' method '{}' (implementing trait '{}'): expected {} param(s), got {}",
423
- c.name, tm.name, trait_name, tm.params.len(), f.params.len()
410
+ e.name, tm.name, trait_name, tm.params.len(), f.params.len()
424
411
  ),
425
412
  });
426
413
  continue;
@@ -430,8 +417,8 @@ fn checkTraitConformance(source: &ast::Source) -> Vec<CheckError> {
430
417
  if actual != expected {
431
418
  errors.push(CheckError {
432
419
  message: format!(
433
- "class '{}' method '{}' (implementing trait '{}'): param {} ('{}'): expected type '{}', got '{}'",
420
+ "enum '{}' method '{}' (implementing trait '{}'): param {} ('{}'): expected type '{}', got '{}'",
434
- c.name, tm.name, trait_name, i, tp.name,
421
+ e.name, tm.name, trait_name, i, tp.name,
435
422
  expected.unwrap_or("<fn>"), actual.unwrap_or("<fn>")
436
423
  ),
437
424
  });
@@ -441,16 +428,16 @@ fn checkTraitConformance(source: &ast::Source) -> Vec<CheckError> {
441
428
  (Some(ft), Some(tt)) if ft.name != tt.name => {
442
429
  errors.push(CheckError {
443
430
  message: format!(
444
- "class '{}' method '{}' (implementing trait '{}'): expected return type '{}', got '{}'",
431
+ "enum '{}' method '{}' (implementing trait '{}'): expected return type '{}', got '{}'",
445
- c.name, tm.name, trait_name, tt.name, ft.name
432
+ e.name, tm.name, trait_name, tt.name, ft.name
446
433
  ),
447
434
  });
448
435
  }
449
436
  (None, Some(tt)) => {
450
437
  errors.push(CheckError {
451
438
  message: format!(
452
- "class '{}' method '{}' (implementing trait '{}'): expected return type '{}', got none",
439
+ "enum '{}' method '{}' (implementing trait '{}'): expected return type '{}', got none",
453
- c.name, tm.name, trait_name, tt.name
440
+ e.name, tm.name, trait_name, tt.name
454
441
  ),
455
442
  });
456
443
  }
@@ -467,33 +454,9 @@ pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
467
454
  // see `checkTraitConformance`'s own doc comment for why.
468
455
  let mut errors: Vec<CheckError> = checkTraitConformance(source);
469
456
  let source = monomorphize::monomorphizeSource(source).map_err(|e| vec![CheckError { message: e }])?;
470
- let (global_env, classes, methods, enum_variants, enum_params) = buildGlobalTables(&source);
457
+ let (global_env, methods, enum_variants, enum_params) = buildGlobalTables(&source);
471
458
  let min_required = buildMinRequiredArgs(&source);
472
- let ctx = CheckCtx { classes: &classes, methods: &methods, enum_variants: &enum_variants, enum_params: &enum_params, min_required: &min_required };
459
+ let ctx = CheckCtx { methods: &methods, enum_variants: &enum_variants, enum_params: &enum_params, min_required: &min_required };
473
-
474
- // A name that is both a class and an enum variant is ambiguous: `Name(...)`
475
- // could mean either construction, and downstream code (both the checker's
476
- // `inferExpr` and codegen) consults `enum_variants` first, so the class
477
- // constructor would be silently shadowed with no diagnostic. Reject it —
478
- // UNLESS it's the bare-type variant sugar (`enum Book = | FantasyBook`,
479
- // see `buildGlobalTables`), which deliberately reuses the class's own name
480
- // as its variant tag and self-wraps it (`field_types == [TNamed(name)]`).
481
- for name in classes.keys() {
482
- if let Some(info) = enum_variants.get(name) {
483
- // Same exemption, but for a "union" enum's primitive bare-wrap
484
- // variant (`enum Number = | Int | Float`) — `Int`/`Float` are
485
- // ALSO registered as zero-field "method-holder" classes (see
486
- // `buildGlobalTables`'s primitive-bare-wrap comment), so this
487
- // exact collision is expected and deliberate for them too.
488
- let is_bare_variant_wrap = info.field_types == [PlumType::TNamed(name.clone())]
489
- || info.field_types == [plumTypeFromName(name)];
490
- if !is_bare_variant_wrap {
491
- errors.push(CheckError {
492
- message: format!("'{}' is declared as both a class and an enum variant", name),
493
- });
494
- }
495
- }
496
- }
497
460
 
498
461
  // A discriminant enum (`enum Foo(n: Int) = ...`) requires every variant to supply
499
462
  // exactly one value per declared param, unified against that param's type. An
@@ -933,37 +896,14 @@ fn inferClassCallRaw(call: &ast::ClassCall, env: &TypeEnv, ctx: &CheckCtx) -> Re
933
896
  }
934
897
  }
935
898
  }
936
- match ctx.classes.get(&call.type_name) {
899
+ match ctx.enum_variants.get(&call.type_name) {
937
- Some(fields) => {
938
- for fa in &call.fields {
939
- match fields.iter().find(|(n, _)| n == &fa.name) {
940
- Some((_, expected)) => {
941
- let actual = inferExpr(&fa.value, env, ctx)?;
942
- unifyArg(expected, &actual, &fa.value, ctx)
943
- .map_err(|e| format!("class '{}' field '{}': {}", call.type_name, fa.name, e))?;
944
- }
945
- None => return Err(format!("unknown field '{}' on class '{}'", fa.name, call.type_name)),
946
- }
947
- }
948
- match &call.spread {
949
- Some(spread) => checkSpreadSource(spread, &call.type_name, &call.type_name, env, ctx)?,
950
- None => {
951
- for (field_name, _) in fields {
952
- if !call.fields.iter().any(|fa| &fa.name == field_name) {
953
- return Err(format!("class '{}' missing field '{}'", call.type_name, field_name));
954
- }
955
- }
956
- }
957
- }
958
- Ok(PlumType::TNamed(call.type_name.clone()))
959
- }
960
- // Not a class — a NAMED-payload enum variant (`Circle(radius: Int)`, as
900
+ // A NAMED-payload enum variant (`Circle(radius: Int)`, as opposed to the
961
- // opposed to the positional/generic-payload form `Some[T]`) is constructed
901
+ // positional/generic-payload form `Some[T]`, which never sets `field_names`
962
- // the same `Type(field: value, ...)` way a class is; `field_names` is empty
963
- // for the positional form, so this never fires for it (falls through to the
964
- // permissive "unmodeled" case below, exactly as before).
902
+ // and so falls through to the permissive "unmodeled" case below) this is
903
+ // ALSO how a record-shaped, single-variant enum (the `type X = ...`
904
+ // replacement, e.g. `enum Cat = | Cat(name: Str)`) constructs, with no
905
+ // separate class-construction path needed.
965
- None if ctx.enum_variants.get(&call.type_name).is_some_and(|info| !info.field_names.is_empty()) => {
906
+ Some(info) if !info.field_names.is_empty() => {
966
- let info = ctx.enum_variants.get(&call.type_name).expect("just checked is_some");
967
907
  for fa in &call.fields {
968
908
  match info.field_names.iter().position(|n| n == &fa.name) {
969
909
  Some(i) => {
@@ -987,8 +927,10 @@ fn inferClassCallRaw(call: &ast::ClassCall, env: &TypeEnv, ctx: &CheckCtx) -> Re
987
927
  }
988
928
  Ok(PlumType::TNamed(info.enum_name.clone()))
989
929
  }
930
+ // Positional/generic-payload variant (`Some[T]`, constructed via `Expr::FnCall`
931
+ // elsewhere instead), a payload-free variant, or an unmodeled (e.g.
990
- // Unmodeled (e.g. builtin/std) type: allow, codegen will catch.
932
+ // builtin/std) type: allow, codegen will catch a genuine mismatch.
991
- None => Ok(PlumType::TNamed(call.type_name.clone())),
933
+ _ => Ok(PlumType::TNamed(call.type_name.clone())),
992
934
  }
993
935
  }
994
936
 
@@ -1026,6 +968,10 @@ fn checkPattern(pat: &ast::CasePattern, subject_ty: &PlumType, env: &mut TypeEnv
1026
968
  }
1027
969
  }
1028
970
  ast::CasePattern::Class { name, fields } => match ctx.enum_variants.get(name) {
971
+ // Also how a record-shaped, single-variant enum (the `type X = ...`
972
+ // replacement) destructures — its one variant already IS the whole
973
+ // value, so there's no tag to check, just its declared fields (in
974
+ // declaration order) to match against, same as any other variant.
1029
975
  Some(info) => {
1030
976
  if fields.len() != info.field_types.len() {
1031
977
  return Err(format!(
@@ -1038,31 +984,13 @@ fn checkPattern(pat: &ast::CasePattern, subject_ty: &PlumType, env: &mut TypeEnv
1038
984
  }
1039
985
  Ok(())
1040
986
  }
1041
- // Not an enum variant — a PLAIN CLASS is destructured the same way (see
1042
- // `plum-wasm-codegen`'s matching `compileClassDestructureArm`): every
1043
- // instance is already "the one variant", so there's no tag to check,
1044
- // just its declared fields (in declaration order) to match against.
1045
- None => match ctx.classes.get(name) {
1046
- Some(class_fields) => {
1047
- if fields.len() != class_fields.len() {
1048
- return Err(format!(
1049
- "constructor pattern '{}' expects {} field(s), got {}",
1050
- name, class_fields.len(), fields.len()
1051
- ));
1052
- }
1053
- for (f, (_, fty)) in fields.iter().zip(class_fields.iter()) {
1054
- checkPattern(f, fty, env, ctx)?;
1055
- }
1056
- Ok(())
1057
- }
1058
- // Unmodeled/builtin variant or class: allow, codegen will catch.
987
+ // Unmodeled/builtin variant: allow, codegen will catch.
1059
- None => {
988
+ None => {
1060
- for f in fields {
989
+ for f in fields {
1061
- checkPattern(f, &PlumType::TVar("_".to_string()), env, ctx)?;
990
+ checkPattern(f, &PlumType::TVar("_".to_string()), env, ctx)?;
1062
- }
1063
- Ok(())
1064
991
  }
992
+ Ok(())
1065
- },
993
+ }
1066
994
  },
1067
995
  }
1068
996
  }
@@ -1073,7 +1001,7 @@ fn checkPattern(pat: &ast::CasePattern, subject_ty: &PlumType, env: &mut TypeEnv
1073
1001
  /// closure assigned to a variable (not passed directly as a call argument,
1074
1002
  /// which the checker never even sees as a `Closure` literal at all) gives
1075
1003
  /// every param a fresh, unresolved `TVar`, and `TVar` can't be looked up in
1076
- /// `ctx.classes` — any field access on it is a hard error, even though the
1004
+ /// `ctx.enum_variants` — any field access on it is a hard error, even though the
1077
1005
  /// exact same shape works fine once compiled (`plum-wasm-codegen`'s own
1078
1006
  /// `resolveClosureParamTypesFromUsage` already does this same resolution, just
1079
1007
  /// too late to help the type-CHECK pass, which runs first). Deliberately a
@@ -1095,10 +1023,10 @@ fn resolveClosureParamFromFieldUsage(
1095
1023
  ast::Expr::Attribute(a) => {
1096
1024
  if let (ast::AttrKind::Field(field_name), ast::Expr::Var(n)) = (&a.attr, &a.object) {
1097
1025
  if params.contains(n.as_str()) && !resolved.contains_key(n.as_str()) {
1098
- let mut matches = ctx.classes.iter()
1026
+ let mut matches = ctx.enum_variants.values()
1099
- .filter(|(_, fields)| fields.iter().any(|(fname, _)| fname == field_name));
1027
+ .filter(|info| info.field_names.iter().any(|fname| fname == field_name));
1100
- if let (Some((class_name, _)), None) = (matches.next(), matches.next()) {
1028
+ if let (Some(info), None) = (matches.next(), matches.next()) {
1101
- resolved.insert(n.clone(), PlumType::TNamed(class_name.clone()));
1029
+ resolved.insert(n.clone(), PlumType::TNamed(info.enum_name.clone()));
1102
1030
  }
1103
1031
  }
1104
1032
  }
@@ -1133,39 +1061,46 @@ fn resolveClosureParamFromFieldUsage(
1133
1061
  /// Resolves `class_name`'s field named `field_name` to its type — shared by
1134
1062
  /// `.field` reads (`inferExpr`'s `AttrKind::Field` arm) and `.field = value`
1135
1063
  /// writes (`checkStmt`'s `AssignTarget::Field` arm), so mutation gets the same
1136
- /// class / discriminant-enum-param / single-owning-variant fallback chain
1064
+ /// discriminant-enum-param / single-owning-variant fallback chain reads
1137
- /// reads already have. See the call sites' own comments for why each fallback
1065
+ /// already have. See the call sites' own comments for why each fallback is
1138
- /// is safe (in particular: exactly one variant may own a given field name;
1066
+ /// safe (in particular: exactly one variant may own a given field name; more
1139
- /// more than one is ambiguous and a compile error).
1067
+ /// than one is ambiguous and a compile error) — this also covers a
1068
+ /// record-shaped, single-variant enum (the `type X = ...` replacement), whose
1069
+ /// one variant is trivially its own unique owner of every field it declares.
1140
- fn lookupFieldType(class_name: &str, field_name: &str, ctx: &CheckCtx) -> Result<PlumType, String> {
1070
+ pub(crate) fn lookupFieldType(class_name: &str, field_name: &str, ctx: &CheckCtx) -> Result<PlumType, String> {
1141
- match ctx.classes.get(class_name) {
1071
+ match ctx.enum_params.get(class_name) {
1142
- Some(fields) => fields.iter()
1072
+ Some(params) => params.iter()
1143
1073
  .find(|(n, _)| n == field_name)
1144
1074
  .map(|(_, t)| t.clone())
1145
1075
  .ok_or_else(|| format!("no field '{}' on type '{}'", field_name, class_name)),
1146
- None => match ctx.enum_params.get(class_name) {
1147
- Some(params) => params.iter()
1148
- .find(|(n, _)| n == field_name)
1149
- .map(|(_, t)| t.clone())
1150
- .ok_or_else(|| format!("no field '{}' on type '{}'", field_name, class_name)),
1151
- None => {
1076
+ None => {
1152
- let owners: Vec<&EnumVariantInfo> = ctx.enum_variants.values()
1077
+ let owners: Vec<&EnumVariantInfo> = ctx.enum_variants.values()
1153
- .filter(|info| info.enum_name == class_name
1078
+ .filter(|info| info.enum_name == class_name
1154
- && info.field_names.iter().any(|n| n == field_name))
1079
+ && info.field_names.iter().any(|n| n == field_name))
1155
- .collect();
1080
+ .collect();
1156
- match owners.as_slice() {
1081
+ match owners.as_slice() {
1157
- [info] => {
1082
+ [info] => {
1158
- let idx = info.field_names.iter().position(|n| n == field_name).expect("just filtered on this");
1083
+ let idx = info.field_names.iter().position(|n| n == field_name).expect("just filtered on this");
1159
- Ok(info.field_types[idx].clone())
1084
+ Ok(info.field_types[idx].clone())
1160
- }
1161
- [] => Ok(PlumType::TVar("_".to_string())),
1162
- _ => Err(format!(
1163
- "field '{}' is ambiguous across multiple variants of enum '{}' — use a match",
1164
- field_name, class_name
1165
- )),
1166
1085
  }
1086
+ // Empty is genuinely ambiguous between two cases that look identical
1087
+ // from here: `class_name` names a real, known NAMED-payload-capable
1088
+ // enum that simply has no field by this name (a hard error), or it's
1089
+ // an unresolved/unmodeled type name, OR a real enum whose variants
1090
+ // are all positional/payload-free (`enum Option = | Some(Int) |
1091
+ // None`, no `field_names` at all to check against — permissive,
1092
+ // codegen will catch a genuine mismatch, exactly as it always has).
1093
+ // Distinguish by whether any variant belonging to this enum name
1094
+ // declares named fields at all.
1095
+ [] if ctx.enum_variants.values().any(|info| info.enum_name == class_name && !info.field_names.is_empty()) =>
1096
+ Err(format!("no field '{}' on type '{}'", field_name, class_name)),
1097
+ [] => Ok(PlumType::TVar("_".to_string())),
1098
+ _ => Err(format!(
1099
+ "field '{}' is ambiguous across multiple variants of enum '{}' — use a match",
1100
+ field_name, class_name
1101
+ )),
1167
1102
  }
1168
- },
1103
+ }
1169
1104
  }
1170
1105
  }
1171
1106
 
@@ -1310,6 +1245,31 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
1310
1245
  // handling just below, exactly as if this special case didn't
1311
1246
  // exist for this call.
1312
1247
  }
1248
+ // `Type(1, 2, 3)` — a call with bare POSITIONAL args on a record-shaped
1249
+ // type's name (as opposed to `Type(field: value, ...)`, which parses as a
1250
+ // `ClassCall` instead — see `class_argument_list` vs `fn_argument_list` in
1251
+ // the grammar) — is sugar for `Type.init(1, 2, 3)`, the same constructor
1252
+ // pattern as `Type()`'s own `ClassCall` handling above, letting `init`
1253
+ // take real params (e.g. `List`'s own variadic `init(values: ...T)`)
1254
+ // instead of being limited to zero args. Checked BEFORE the ordinary
1255
+ // positional-variant-construction case just below: a record-shaped
1256
+ // enum's own name is ALSO its one variant's name (see
1257
+ // `buildGlobalTables`), so without this ordering a call meant as
1258
+ // `List.init(1, 2, 3)` could instead misfire as "construct the List
1259
+ // variant positionally" whenever the arg count happens to match its
1260
+ // real field count — a record-shaped type never supported positional
1261
+ // field construction via bare `Type(...)` even before enums/classes
1262
+ // merged into one representation, only through `init` or the named
1263
+ // `Type(field: value, ...)` form.
1264
+ if ctx.enum_variants.get(&call.name).is_some_and(|info| info.enum_name == call.name)
1265
+ && ctx.methods.contains_key(&(call.name.clone(), "init".to_string()))
1266
+ {
1267
+ let synthetic = ast::Expr::Attribute(Box::new(ast::AttributeExpr {
1268
+ object: ast::Expr::TypeName(call.name.clone()),
1269
+ attr: ast::AttrKind::Method(ast::FnCall { name: "init".to_string(), args: call.args.clone() }),
1270
+ }));
1271
+ return inferExpr(&synthetic, env, ctx);
1272
+ }
1313
1273
  if let Some(info) = ctx.enum_variants.get(&call.name) {
1314
1274
  if call.args.len() != info.field_types.len() {
1315
1275
  return Err(format!(
@@ -1339,22 +1299,6 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
1339
1299
  }
1340
1300
  return Ok(PlumType::TNamed(info.enum_name.clone()));
1341
1301
  }
1342
- // `Type(1, 2, 3)` — a call with bare POSITIONAL args on a class name (as
1343
- // opposed to `Type(field: value, ...)`, which parses as a `ClassCall`
1344
- // instead — see `class_argument_list` vs `fn_argument_list` in the
1345
- // grammar) — is sugar for `Type.init(1, 2, 3)`, the same constructor
1346
- // pattern as `Type()`'s own `ClassCall` handling above, letting `init`
1347
- // take real params (e.g. `List`'s own variadic `init(values: ...T)`)
1348
- // instead of being limited to zero args.
1349
- if ctx.classes.contains_key(&call.name)
1350
- && ctx.methods.contains_key(&(call.name.clone(), "init".to_string()))
1351
- {
1352
- let synthetic = ast::Expr::Attribute(Box::new(ast::AttributeExpr {
1353
- object: ast::Expr::TypeName(call.name.clone()),
1354
- attr: ast::AttrKind::Method(ast::FnCall { name: "init".to_string(), args: call.args.clone() }),
1355
- }));
1356
- return inferExpr(&synthetic, env, ctx);
1357
- }
1358
1302
  match lookup(env, &call.name) {
1359
1303
  Ok(PlumType::TFun(param_types, ret)) => {
1360
1304
  match param_types.last() {
plum-checker/src/monomorphize.rs CHANGED
@@ -4,8 +4,8 @@ use crate::types::PlumType;
4
4
 
5
5
  /// A single uppercase letter (`T`, `U`, `K`, ...) is the grammar's only legal
6
6
  /// spelling for a generic type parameter — this is how we recognize one, since
7
- /// `ast::Fn` and `ast::Enum` (unlike `ast::Class`/`ast::Trait`) carry no explicit
7
+ /// `ast::Fn` (unlike `ast::Enum`/`ast::Trait`) carries no explicit generics
8
- /// generics declaration list.
8
+ /// declaration list.
9
9
  pub fn isGenericParamName(name: &str) -> bool {
10
10
  let mut chars = name.chars();
11
11
  match (chars.next(), chars.next()) {
@@ -14,11 +14,6 @@ pub fn isGenericParamName(name: &str) -> bool {
14
14
  }
15
15
  }
16
16
 
17
- /// The generic parameter names introduced by a `Class`, in declaration order.
18
- pub fn classGenericParams(c: &ast::Class) -> Vec<String> {
19
- c.generics.iter().map(|g| g.name.clone()).collect()
20
- }
21
-
22
17
  /// Collects every distinct single-uppercase-letter type name appearing
23
18
  /// anywhere in `t`, RECURSING into `t.generics` — so `List[T]` (a container
24
19
  /// type applied to the bare letter `T`) is recognized as introducing `T`,
@@ -54,8 +49,8 @@ pub fn fnGenericParams(f: &ast::Fn) -> Vec<String> {
54
49
  }
55
50
 
56
51
  /// The generic parameter names an `Enum` introduces — its OWN declared
57
- /// generics (`enum Option[T] = ...`, mirroring `classGenericParams`) if it
52
+ /// generics (`enum Option[T] = ...`) if it declares any, else (for an
58
- /// declares any, else (for an enum that doesn't) every distinct
53
+ /// enum that doesn't) every distinct
59
54
  /// single-uppercase-letter variant field type name, in first-appearance
60
55
  /// order — the older, purely-implicit inference this replaces.
61
56
  pub fn enumGenericParams(e: &ast::Enum) -> Vec<String> {
@@ -178,22 +173,6 @@ pub fn mangle(base: &str, type_args: &[PlumType]) -> String {
178
173
  out
179
174
  }
180
175
 
181
- /// Produces a concrete, specialized copy of a generic class under `mangled_name`,
182
- /// substituting every field whose declared type names one of the class's generic
183
- /// parameters with its resolved concrete type. The class's own `generics` list is
184
- /// cleared on the copy (it is now fully concrete).
185
- pub fn specializeClass(c: &ast::Class, subst: &Substitution, mangled_name: &str) -> ast::Class {
186
- ast::Class {
187
- name: mangled_name.to_string(),
188
- implements: c.implements.clone(),
189
- generics: vec![],
190
- fields: c.fields.iter().map(|f| ast::Field {
191
- name: f.name.clone(),
192
- ty: substituteType(&f.ty, subst),
193
- }).collect(),
194
- }
195
- }
196
-
197
176
  /// Produces a concrete, specialized copy of a generic function (or method) under
198
177
  /// `mangled_name`. `new_type_param` overrides the receiver-type name for a method
199
178
  /// whose receiver class was itself specialized (e.g. a method declared on `Box`
@@ -375,6 +354,7 @@ pub fn specializeEnum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -
375
354
  ast::Enum {
376
355
  name: mangled_name.to_string(),
377
356
  generics: vec![],
357
+ implements: e.implements.clone(),
378
358
  params: e.params.clone(),
379
359
  variants: e.variants.iter().map(|v| ast::EnumVariant {
380
360
  name: mangle(&v.name, &type_args),
@@ -387,10 +367,9 @@ pub fn specializeEnum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -
387
367
 
388
368
  use std::collections::BTreeSet;
389
369
  use crate::types::{TypeEnv, TypeScheme};
390
- use crate::{ClassEnv, MethodEnv, EnumVariants, EnumVariantInfo, EnumParams, CheckCtx};
370
+ use crate::{MethodEnv, EnumVariants, EnumVariantInfo, EnumParams, CheckCtx};
391
371
 
392
372
  enum PendingSpecialization<'a> {
393
- Class { base: &'a ast::Class, subst: Substitution, mangled: String },
394
373
  Fn { base: &'a ast::Fn, subst: Substitution, mangled: String, new_receiver: Option<String> },
395
374
  Enum { base: &'a ast::Enum, subst: Substitution, mangled: String },
396
375
  }
@@ -539,14 +518,10 @@ fn renameVarInArg(arg: &mut ast::Arg, old: &str, new: &str) {
539
518
  }
540
519
 
541
520
  struct Monomorphizer<'a> {
542
- classes_generic: BTreeMap<String, &'a ast::Class>,
543
521
  fns_generic: BTreeMap<String, &'a ast::Fn>,
544
- methods_generic_on: BTreeMap<String, Vec<&'a ast::Fn>>,
545
- /// Same as `methods_generic_on`, but for a method declared on a generic ENUM
522
+ /// Every method declared on a generic enum (e.g. `Result`'s `isOk`/`isErr`,
546
- /// (e.g. `Result`'s `isOk`/`isErr`) rather than a generic class a separate map
523
+ /// or a record-shaped `List`/`Map`'s own methods), keyed by the enum's own
547
- /// because the two need separate lookups keyed by their own base-name maps
548
- /// (`enums_generic_by_name` vs `classes_generic`) at both classification and
549
- /// specialization time.
524
+ /// bare name.
550
525
  methods_generic_on_enum: BTreeMap<String, Vec<&'a ast::Fn>>,
551
526
  /// Bare variant name (e.g. `"Some"`) -> the generic `Enum` it belongs to. Keyed
552
527
  /// by variant name because a construction site (`Some(5)`) parses as a `FnCall`
@@ -566,22 +541,21 @@ struct Monomorphizer<'a> {
566
541
  /// sites like `Some(5)`).
567
542
  enums_generic_by_name: BTreeMap<String, &'a ast::Enum>,
568
543
  /// Free functions that are NOT generic by `fnGenericParams`'s lowercase-letter
569
- /// convention, but whose param type(s) bare-name a generic class or enum (e.g.
544
+ /// convention, but whose param type(s) bare-name a generic enum (e.g.
570
545
  /// `unwrapOr(o: Option, ...)`) — such a function still needs its own
571
- /// per-call-site specialization, since its receiver generic class/enum is
546
+ /// per-call-site specialization, since its receiver generic enum is
572
547
  /// dropped from the monomorphized output and the bare name would otherwise
573
548
  /// resolve to nothing.
574
549
  fns_bare_generic: BTreeMap<String, &'a ast::Fn>,
575
- /// Mangled class/enum name -> (its own TEMPLATE name, the generic-param
550
+ /// Mangled enum name -> (its own TEMPLATE name, the generic-param
576
551
  /// bindings it was specialized with), e.g. `"List$Int" -> ("List", {"T":
577
- /// TInt})`. Populated wherever a class/enum specialization's `mangled`
552
+ /// TInt})`. Populated wherever an enum specialization's `mangled`
578
553
  /// name is first computed. Used by `resolveMethodOwnGenerics` to look up
579
554
  /// a method's OWNER template (to find its original, doubly-generic
580
555
  /// `ast::Fn`) and the receiver's own already-known bindings (e.g. `T`),
581
556
  /// given only the receiver's mangled type name at a method call site.
582
557
  class_specialization_info: BTreeMap<String, (String, BTreeMap<String, PlumType>)>,
583
558
  global_env: TypeEnv,
584
- classes: ClassEnv,
585
559
  methods: MethodEnv,
586
560
  enum_variants: EnumVariants,
587
561
  enum_params: EnumParams,
@@ -646,7 +620,7 @@ impl<'a> Monomorphizer<'a> {
646
620
  // silently no-op'ing on the very next statement (`l.map(...)`).
647
621
  let mut merged_env = self.global_env.clone();
648
622
  merged_env.extend(env.iter().map(|(k, v)| (k.clone(), v.clone())));
649
- let ctx = CheckCtx { classes: &self.classes, methods: &self.methods, enum_variants: &self.enum_variants, enum_params: &self.enum_params, min_required: &self.min_required };
623
+ let ctx = CheckCtx { methods: &self.methods, enum_variants: &self.enum_variants, enum_params: &self.enum_params, min_required: &self.min_required };
650
624
  crate::inferExpr(e, &merged_env, &ctx).unwrap_or(PlumType::TVar("_".to_string()))
651
625
  }
652
626
 
@@ -812,7 +786,7 @@ impl<'a> Monomorphizer<'a> {
812
786
  let needs = match &f.returns {
813
787
  None => resolve_return,
814
788
  Some(rt) => {
815
- // A generic class/enum named WITH concrete type args already
789
+ // A generic enum named WITH concrete type args already
816
790
  // (`-> List[Str]`, `-> Result[Int, Str]`) is already fully
817
791
  // resolved — only a genuinely BARE reference (`-> List`,
818
792
  // `-> Box`, no `[...]` at all) is the "unresolved" shape this
@@ -824,7 +798,6 @@ impl<'a> Monomorphizer<'a> {
824
798
  // class name — corrupting an otherwise-correct declaration.
825
799
  rt.generics.is_empty()
826
800
  && (isGenericParamName(&rt.name)
827
- || self.classes_generic.contains_key(&rt.name)
828
801
  || self.enums_generic_by_name.contains_key(&rt.name))
829
802
  }
830
803
  };
@@ -889,25 +862,6 @@ impl<'a> Monomorphizer<'a> {
889
862
  };
890
863
  self.manglePattern(f, fty, field_mangling.as_ref(), case_env, body, guard.as_deref_mut());
891
864
  }
892
- } else if let PlumType::TNamed(concrete_name) = ty {
893
- // A PLAIN CLASS pattern (`Node(value, prev, next)`, not an enum
894
- // variant) — `ty` is already this subject's own concrete, possibly
895
- // mangled class name (whatever resolved it — a field type, a
896
- // variant's own payload, ...— already went through the same
897
- // generic-field-type resolution ordinary `.field` ACCESS relies
898
- // on). Rewrite the pattern's bare/generic name to match (a no-op
899
- // if it's already concrete, e.g. a non-generic class) — without
900
- // this, a GENERIC class's pattern name is left as the bare
901
- // template name (`Node`), which no longer exists in the final
902
- // monomorphized `ClassEnv` (only `Node$Int`/`Node$Str`/... do),
903
- // so the checker/codegen's own `ctx.classes` lookup would silently
904
- // fall back to treating every field as an unresolved `TVar`.
905
- if let Some(class_fields) = self.classes.get(concrete_name).cloned() {
906
- *name = concrete_name.clone();
907
- for (f, (_, fty)) in fields.iter_mut().zip(class_fields.iter()) {
908
- self.manglePattern(f, fty, None, case_env, body, guard.as_deref_mut());
909
- }
910
- }
911
865
  }
912
866
  }
913
867
  _ => {}
@@ -926,9 +880,8 @@ impl<'a> Monomorphizer<'a> {
926
880
  // written into does.
927
881
  if let ast::AssignTarget::Field(object, field_name) = target {
928
882
  if let PlumType::TNamed(class_name) = self.infer(object, env) {
883
+ let ctx = CheckCtx { methods: &self.methods, enum_variants: &self.enum_variants, enum_params: &self.enum_params, min_required: &self.min_required };
929
- if let Some(field_ty) = self.classes.get(&class_name)
884
+ if let Ok(field_ty) = crate::lookupFieldType(&class_name, field_name, &ctx) {
930
- .and_then(|fields| fields.iter().find(|(n, _)| n == field_name).map(|(_, t)| t.clone()))
931
- {
932
885
  self.resolveBareVariantAgainstExpected(value, &field_ty);
933
886
  self.wrapPrimitiveAgainstExpected(value, &field_ty, env);
934
887
  }
@@ -1061,13 +1014,22 @@ impl<'a> Monomorphizer<'a> {
1061
1014
  Ok(())
1062
1015
  }
1063
1016
 
1017
+ /// Resolves a `Type(field: value, ...)` construction of a generic,
1018
+ /// record-shaped (single-variant, named-payload) enum — the `type X[T] =
1019
+ /// ...` replacement shape, e.g. `List[T]`/`Map[K, V]`. `ClassCall` is the
1020
+ /// shared AST node this call shape ALSO uses for a non-generic or already-
1021
+ /// mangled enum variant's own named-payload construction (see
1022
+ /// `plum-checker`'s `inferClassCallRaw`) — this only fires for one that's
1023
+ /// still a bare, generic TEMPLATE name.
1064
1024
  fn resolveClassInstantiation(&mut self, call: &mut ast::ClassCall, env: &TypeEnv) -> Result<(), String> {
1065
- let Some(class) = self.classes_generic.get(call.type_name.as_str()).copied() else { return Ok(()) };
1025
+ let Some(e) = self.enums_generic_by_name.get(call.type_name.as_str()).copied() else { return Ok(()) };
1066
- let params = classGenericParams(class);
1026
+ let params = enumGenericParams(e);
1027
+ let variant = &e.variants[0];
1067
1028
  let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
1068
1029
  for gp in &params {
1069
- if let Some(field) = class.fields.iter().find(|f| f.ty.name == *gp) {
1030
+ if let Some(idx) = variant.fields.iter().position(|t| t.name == *gp) {
1031
+ let field_name = &variant.field_names[idx];
1070
- if let Some(fa) = call.fields.iter().find(|fa| fa.name == field.name) {
1032
+ if let Some(fa) = call.fields.iter().find(|fa| &fa.name == field_name) {
1071
1033
  // A bare payload-free variant reference (`None`) carries
1072
1034
  // no type of its own to bind a generic param FROM — skip
1073
1035
  // it here; it gets resolved AGAINST the binding (once
@@ -1079,7 +1041,7 @@ impl<'a> Monomorphizer<'a> {
1079
1041
  }
1080
1042
  }
1081
1043
  }
1082
- // No field is EVER directly typed as a bare generic param for a class
1044
+ // No field is EVER directly typed as a bare generic param for a type
1083
1045
  // like `List[T]` (its fields are `Option[Node[T]]`/`Int`, never a bare
1084
1046
  // `T`) — and even where one exists, constructing with a payload-free
1085
1047
  // value (`List(head: None, ...)`) gives no VALUE to infer a type from
@@ -1095,7 +1057,7 @@ impl<'a> Monomorphizer<'a> {
1095
1057
  // `plumTypeFromAst` alone would silently flatten to the
1096
1058
  // bare, unmangled `TNamed("Pair")` (it drops `.generics`
1097
1059
  // entirely) — exactly the same recursive resolution
1098
- // `resolveFieldType` already does for a class FIELD's own
1060
+ // `resolveFieldType` already does for a field's own
1099
1061
  // declared type, just applied here to an explicit
1100
1062
  // call-site type argument instead.
1101
1063
  let resolved = self.resolveFieldType(gt);
@@ -1134,48 +1096,23 @@ impl<'a> Monomorphizer<'a> {
1134
1096
  call.type_name, call.type_name
1135
1097
  ));
1136
1098
  }
1137
- let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
1138
- let mangled = mangle(&call.type_name, &type_args);
1099
+ let mangled = self.ensureEnumSpecialized(e, &params, bindings);
1139
- self.class_specialization_info.entry(mangled.clone()).or_insert_with(|| (call.type_name.clone(), bindings.clone()));
1140
1100
 
1141
1101
  // Now that every generic param is bound, resolve any bare
1142
- // payload-free-variant field values (`None`) against THIS class's own
1102
+ // payload-free-variant field values (`None`) against THIS
1143
- // (about-to-be-specialized) field types — codegen only ever sees the
1103
+ // specialization's own field types — codegen only ever sees the
1144
- // mangled specializations (the generic template is dropped), so a
1104
+ // mangled specialization (the generic template is dropped), so a
1145
1105
  // still-bare `None` would be an unresolvable reference by the time it
1146
- // gets there. `specializeClass` is a pure function; calling it here
1147
- // ahead of the worklist actually processing this specialization is
1148
- // fine — the worklist dedups on `mangled` regardless of how many
1149
- // times it's computed.
1106
+ // gets there.
1150
- let spec_class = specializeClass(class, &Substitution(bindings.clone()), &mangled);
1107
+ if let Some(info) = self.enum_variants.get(&mangled).cloned() {
1151
- for fa in &mut call.fields {
1108
+ for fa in &mut call.fields {
1152
- if let Some(field) = spec_class.fields.iter().find(|f| f.name == fa.name) {
1109
+ if let Some(idx) = info.field_names.iter().position(|n| n == &fa.name) {
1153
- let field_ty = self.resolveFieldType(&field.ty);
1154
- let expected = crate::plumTypeFromAst(&field_ty);
1110
+ let expected = info.field_types[idx].clone();
1155
- self.resolveBareVariantAgainstExpected(&mut fa.value, &expected);
1111
+ self.resolveBareVariantAgainstExpected(&mut fa.value, &expected);
1156
- self.wrapPrimitiveAgainstExpected(&mut fa.value, &expected, env);
1112
+ self.wrapPrimitiveAgainstExpected(&mut fa.value, &expected, env);
1113
+ }
1157
1114
  }
1158
1115
  }
1159
-
1160
- if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
1161
- self.enqueued.insert(mangled.clone());
1162
- self.worklist.push(PendingSpecialization::Class { base: class, subst: Substitution(bindings.clone()), mangled: mangled.clone() });
1163
- }
1164
- // Register this specialization's own field types and its methods'
1165
- // signatures right now — a construction site like this one can appear
1166
- // inside an ORDINARY (non-generic) function, which gets rewritten in
1167
- // the pass BEFORE the worklist above ever runs. Any later statement in
1168
- // that SAME function body (e.g. `l.get(1)` followed by a `match` on
1169
- // its result) needs `self.methods`/`self.classes` to already know
1170
- // about "List$Int" right now, not once the worklist eventually
1171
- // catches up.
1172
- if !self.classes.contains_key(&mangled) {
1173
- let field_types: Vec<(String, PlumType)> = spec_class.fields.iter()
1174
- .map(|f| (f.name.clone(), crate::plumTypeFromAst(&self.resolveFieldType(&f.ty))))
1175
- .collect();
1176
- self.classes.insert(mangled.clone(), field_types);
1177
- }
1178
- self.registerClassMethodSignatures(class, &mangled, &bindings);
1179
1116
  call.type_name = mangled;
1180
1117
  Ok(())
1181
1118
  }
@@ -1212,8 +1149,7 @@ impl<'a> Monomorphizer<'a> {
1212
1149
  let mut method_and_bindings: Option<(&'a ast::Fn, BTreeMap<String, PlumType>)> = None;
1213
1150
  if let PlumType::TNamed(mangled_receiver) = self.inferConcrete(object, env) {
1214
1151
  if let Some((template_name, class_bindings)) = self.class_specialization_info.get(&mangled_receiver).cloned() {
1215
- let method = self.methods_generic_on.get(template_name.as_str())
1152
+ let method = self.methods_generic_on_enum.get(template_name.as_str())
1216
- .or_else(|| self.methods_generic_on_enum.get(template_name.as_str()))
1217
1153
  .and_then(|methods| methods.iter().find(|m| m.name == call.name).copied());
1218
1154
  if let Some(method) = method {
1219
1155
  method_and_bindings = Some((method, class_bindings));
@@ -1251,24 +1187,23 @@ impl<'a> Monomorphizer<'a> {
1251
1187
  }
1252
1188
 
1253
1189
  /// If `call` invokes a method that has generic param(s) of its OWN, beyond
1254
- /// its receiver class/enum's own (e.g. `List[T].map`'s `U`) — a shape
1190
+ /// its receiver enum's own (e.g. `List[T].map`'s `U`) — a shape
1255
- /// `registerClassMethodSignatures`/the `PendingSpecialization::Class`/`Enum`
1191
+ /// `registerEnumMethodSignatures`/the `PendingSpecialization::Enum`
1256
- /// worklist arms deliberately skip producing anything for — infers those
1192
+ /// worklist arm deliberately skip producing anything for — infers those
1257
1193
  /// extra param(s) from `call`'s own arguments (a plain arg's own type for a
1258
1194
  /// bare-letter param, or a closure argument's INFERRED BODY TYPE for a
1259
1195
  /// closure-typed param whose return is the letter), then specializes+
1260
1196
  /// registers a new per-CALL-SITE mangled method (e.g. `"map$Str"`) — the
1261
1197
  /// same idea as a truly generic free function's own `fns_generic`/
1262
1198
  /// `resolveFnInstantiation`, just combined with the receiver's own already-
1263
- /// known class-level bindings (e.g. `T = Int`) — and rewrites `call.name`
1199
+ /// known enum-level bindings (e.g. `T = Int`) — and rewrites `call.name`
1264
1200
  /// to it. No-op if `object`'s type isn't a specialization this pass has
1265
1201
  /// recorded `class_specialization_info` for, or the method has no extra
1266
1202
  /// generics of its own (the ordinary, already-handled case).
1267
1203
  fn resolveMethodOwnGenerics(&mut self, object: &ast::Expr, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
1268
1204
  let PlumType::TNamed(mangled_receiver) = self.inferConcrete(object, env) else { return Ok(()) };
1269
1205
  let Some((template_name, class_bindings)) = self.class_specialization_info.get(&mangled_receiver).cloned() else { return Ok(()) };
1270
- let method = self.methods_generic_on.get(template_name.as_str())
1206
+ let method = self.methods_generic_on_enum.get(template_name.as_str())
1271
- .or_else(|| self.methods_generic_on_enum.get(template_name.as_str()))
1272
1207
  .and_then(|methods| methods.iter().find(|m| m.name == call.name).copied());
1273
1208
  let Some(method) = method else { return Ok(()) };
1274
1209
  let owner_params: Vec<String> = class_bindings.keys().cloned().collect();
@@ -1394,22 +1329,7 @@ impl<'a> Monomorphizer<'a> {
1394
1329
  Ok(())
1395
1330
  }
1396
1331
 
1397
- /// Eagerly computes and registers (into `self.methods`) the `(mangled,
1398
- /// method_name) -> TFun` signature of every method declared on `class`,
1399
- /// for the specialization named `mangled` under `bindings` — without
1400
- /// producing the actual `ast::Fn` items (that still only happens once the
1401
- /// worklist entry for this specialization is popped, avoiding duplicate
1402
- /// emission). Needed so a call site that appears in a function processed
1403
- /// BEFORE the worklist runs (see callers) can still resolve a method call
1404
- /// against this specialization immediately.
1405
- fn registerClassMethodSignatures(&mut self, class: &'a ast::Class, mangled: &str, bindings: &BTreeMap<String, PlumType>) {
1406
- let Some(methods) = self.methods_generic_on.get(class.name.as_str()).cloned() else { return };
1407
- let owner_params = classGenericParams(class);
1408
- self.registerMethodSignatures(&class.name, mangled, methods, &owner_params, bindings);
1409
- }
1410
-
1411
- /// The enum counterpart to `registerClassMethodSignatures` — see its doc
1412
- /// comment. Without this, a method call chained directly onto a FRESH
1332
+ /// Without this, a method call chained directly onto a FRESH
1413
1333
  /// enum construction in the same statement (`Some(4).filter(...)`, or
1414
1334
  /// any `SomeVariant(...).method(...)` one-liner for a generic enum) could
1415
1335
  /// never resolve: the receiver's specialization is only ENQUEUED here,
@@ -1606,9 +1526,8 @@ impl<'a> Monomorphizer<'a> {
1606
1526
  if template_name != &declared.name {
1607
1527
  return;
1608
1528
  }
1609
- let owner_params: Vec<String> = self.classes_generic.get(template_name.as_str())
1529
+ let owner_params: Vec<String> = self.enums_generic_by_name.get(template_name.as_str())
1610
- .map(|c| classGenericParams(c))
1530
+ .map(|e| enumGenericParams(e))
1611
- .or_else(|| self.enums_generic_by_name.get(template_name.as_str()).map(|e| enumGenericParams(e)))
1612
1531
  .unwrap_or_default();
1613
1532
  for (declared_arg, owner_param) in declared.generics.iter().zip(owner_params.iter()) {
1614
1533
  if let Some(bound) = class_bindings.get(owner_param) {
@@ -1707,7 +1626,7 @@ impl<'a> Monomorphizer<'a> {
1707
1626
  continue;
1708
1627
  }
1709
1628
  let n = &t.name;
1710
- if (self.classes_generic.contains_key(n.as_str()) || self.enums_generic_by_name.contains_key(n.as_str()))
1629
+ if self.enums_generic_by_name.contains_key(n.as_str())
1711
1630
  && !names.iter().any(|x| x == n)
1712
1631
  {
1713
1632
  names.push(n.clone());
@@ -1824,24 +1743,25 @@ impl<'a> Monomorphizer<'a> {
1824
1743
  Ok(())
1825
1744
  }
1826
1745
 
1827
- /// `Type(1, 2, 3)` — a bare-positional-args call on a GENERIC class name that
1746
+ /// `Type(1, 2, 3)` — a bare-positional-args call on a GENERIC record-shaped
1828
- /// declares an `init` method (e.g. `List`'s own `init(values: ...T)`) — is sugar
1747
+ /// enum name that declares an `init` method (e.g. `List`'s own
1748
+ /// `init(values: ...T)`) — is sugar for `Type.init(1, 2, 3)` (see
1829
- /// for `Type.init(1, 2, 3)` (see `plum-checker`'s `inferExpr` / `plum-wasm-codegen`'s
1749
+ /// `plum-checker`'s `inferExpr` / `plum-wasm-codegen`'s matching `FnCall`
1830
- /// matching `FnCall` handling, which run AFTER monomorphization and only ever see
1750
+ /// handling, which run AFTER monomorphization and only ever see concrete,
1831
- /// concrete, mangled class names). Infers `init`'s own generic bindings from
1751
+ /// mangled names). Infers `init`'s own generic bindings from `call.args`'
1832
- /// `call.args`' types (mirroring `resolveEnumInstantiation`'s args-based
1752
+ /// types (mirroring `resolveEnumInstantiation`'s args-based inference for
1833
- /// inference for a variant's fields), specializes the class for them, and
1753
+ /// a variant's fields), specializes the enum for them, and rewrites
1834
- /// rewrites `call.name` to the mangled class name — same rewrite target
1754
+ /// `call.name` to the mangled name — same rewrite target
1835
- /// `resolveClassInstantiation` uses for the named `Type(field: value, ...)` call
1755
+ /// `resolveClassInstantiation` uses for the named `Type(field: value, ...)`
1836
- /// shape, just reached from a differently-shaped call site.
1756
+ /// call shape, just reached from a differently-shaped call site.
1837
1757
  fn resolveClassInitInstantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
1838
- let Some(class) = self.classes_generic.get(call.name.as_str()).copied() else { return Ok(()) };
1758
+ let Some(e) = self.enums_generic_by_name.get(call.name.as_str()).copied() else { return Ok(()) };
1839
- let Some(init_fn) = self.methods_generic_on.get(call.name.as_str())
1759
+ let Some(init_fn) = self.methods_generic_on_enum.get(call.name.as_str())
1840
1760
  .and_then(|methods| methods.iter().find(|f| f.name == "init").copied())
1841
1761
  else {
1842
1762
  return Ok(());
1843
1763
  };
1844
- let params = classGenericParams(class);
1764
+ let params = enumGenericParams(e);
1845
1765
  let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
1846
1766
  for (i, arg) in call.args.iter().enumerate() {
1847
1767
  let Some(param) = init_fn.params.get(i).or_else(|| init_fn.params.last()) else { break };
@@ -1879,21 +1799,7 @@ impl<'a> Monomorphizer<'a> {
1879
1799
  if bindings.len() != params.len() {
1880
1800
  return Ok(());
1881
1801
  }
1882
- let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
1883
- let mangled = mangle(&call.name, &type_args);
1802
+ let mangled = self.ensureEnumSpecialized(e, &params, bindings);
1884
- self.class_specialization_info.entry(mangled.clone()).or_insert_with(|| (call.name.clone(), bindings.clone()));
1885
- if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
1886
- self.enqueued.insert(mangled.clone());
1887
- self.worklist.push(PendingSpecialization::Class { base: class, subst: Substitution(bindings.clone()), mangled: mangled.clone() });
1888
- }
1889
- if !self.classes.contains_key(&mangled) {
1890
- let spec_class = specializeClass(class, &Substitution(bindings.clone()), &mangled);
1891
- let field_types: Vec<(String, PlumType)> = spec_class.fields.iter()
1892
- .map(|f| (f.name.clone(), crate::plumTypeFromAst(&self.resolveFieldType(&f.ty))))
1893
- .collect();
1894
- self.classes.insert(mangled.clone(), field_types);
1895
- }
1896
- self.registerClassMethodSignatures(class, &mangled, &bindings);
1897
1803
  call.name = mangled;
1898
1804
  Ok(())
1899
1805
  }
@@ -1913,6 +1819,16 @@ impl<'a> Monomorphizer<'a> {
1913
1819
  let mangled = mangle(&e.name, &type_args);
1914
1820
  self.class_specialization_info.entry(mangled.clone()).or_insert_with(|| (e.name.clone(), bindings.clone()));
1915
1821
  if !self.enum_variant_mangling.contains_key(&mangled) {
1822
+ // Insert a placeholder table BEFORE recursively resolving any variant's
1823
+ // own field types below — a self-referential generic enum (e.g. a
1824
+ // linked-list `Node[T]`'s own `next: Option[Node[T]]` field) would
1825
+ // otherwise recurse right back into specializing itself here before
1826
+ // this specialization is even marked as in-progress, infinitely. The
1827
+ // recursive re-entry only ever needs this specialization's MANGLED
1828
+ // NAME back (see `resolveFieldType`'s `ast::Type { name: mangled, ...
1829
+ // }` return), not its fully-resolved variant table — that gets
1830
+ // overwritten with the real one below once the recursion unwinds.
1831
+ self.enum_variant_mangling.insert(mangled.clone(), BTreeMap::new());
1916
1832
  let mut table = BTreeMap::new();
1917
1833
  for (tag, v) in e.variants.iter().enumerate() {
1918
1834
  let mangled_variant = mangle(&v.name, &type_args);
@@ -1955,7 +1871,7 @@ impl<'a> Monomorphizer<'a> {
1955
1871
  }
1956
1872
 
1957
1873
  /// Fully resolves a FIELD's declared type into something that will actually
1958
- /// exist after monomorphization. `specializeClass`'s own field substitution
1874
+ /// exist after monomorphization. `specializeEnum`'s own field substitution
1959
1875
  /// only replaces a bare generic-param NAME (`T` -> `Int`) — a field declared
1960
1876
  /// `Option[Node[T]]` becomes `Option[Node[Int]]` this way, which is now
1961
1877
  /// fully CONCRETE but still names the generic TEMPLATES `Option`/`Node`
@@ -1963,16 +1879,16 @@ impl<'a> Monomorphizer<'a> {
1963
1879
  /// their mangled specializations, e.g. `Node$Int`, survive). Recursively
1964
1880
  /// resolves any nested generic arguments first (so `Node[Int]` inside
1965
1881
  /// `Option[Node[Int]]` becomes `Node$Int` before `Option[...]` itself is
1966
- /// resolved), then — if the type names a known generic class/enum applied
1882
+ /// resolved), then — if the type names a known generic enum applied to
1967
- /// to arguments — mangles it to that specialization's real name and
1883
+ /// arguments — mangles it to that specialization's real name and enqueues
1968
- /// enqueues the specialization if it hasn't been already (via the same
1884
+ /// the specialization if it hasn't been already, via the same
1969
- /// `ensureEnumSpecialized` used for enum construction sites, for enums; classes
1885
+ /// `ensureEnumSpecialized` used for enum construction sites. A field
1970
- /// don't have an equivalent shared helper, so that half is inlined here).
1971
- /// A field that's already concrete (no generics), or whose name isn't a
1886
+ /// that's already concrete (no generics), or whose name isn't a known
1972
- /// known generic template, is returned unchanged (or with just its nested
1887
+ /// generic template, is returned unchanged (or with just its nested
1973
- /// generics resolved) — this is ALSO called on every ordinary (non-generic)
1888
+ /// generics resolved) — this is ALSO called on every ordinary
1974
- /// class's fields, not just specialized ones, since a plain class can
1889
+ /// (non-generic) enum's fields, not just specialized ones, since a plain
1975
- /// perfectly well have a field like `items: List[Int]`.
1890
+ /// record-shaped enum can perfectly well have a field like `items:
1891
+ /// List[Int]`.
1976
1892
  fn resolveFieldType(&mut self, ty: &ast::Type) -> ast::Type {
1977
1893
  if ty.generics.is_empty() {
1978
1894
  return ty.clone();
@@ -1992,45 +1908,6 @@ impl<'a> Monomorphizer<'a> {
1992
1908
  }
1993
1909
  let type_args: Vec<PlumType> = resolved_args.iter().map(crate::plumTypeFromAst).collect();
1994
1910
 
1995
- if let Some(class) = self.classes_generic.get(ty.name.as_str()).copied() {
1996
- let params = classGenericParams(class);
1997
- if params.len() == type_args.len() {
1998
- let bindings: BTreeMap<String, PlumType> = params.into_iter().zip(type_args.iter().cloned()).collect();
1999
- let mangled = mangle(&ty.name, &type_args);
2000
- self.class_specialization_info.entry(mangled.clone()).or_insert_with(|| (ty.name.clone(), bindings.clone()));
2001
- if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
2002
- self.enqueued.insert(mangled.clone());
2003
- self.worklist.push(PendingSpecialization::Class { base: class, subst: Substitution(bindings.clone()), mangled: mangled.clone() });
2004
- }
2005
- // Register this specialization's field types right now, even
2006
- // though its actual `Item::Class`/method output is only pushed
2007
- // to `m.produced` once the worklist entry above is popped —
2008
- // another method being rewritten in THIS SAME worklist round
2009
- // (e.g. a sibling method on the class currently being
2010
- // specialized) may need to resolve a field access against it
2011
- // immediately, well before that later worklist entry runs.
2012
- if !self.classes.contains_key(&mangled) {
2013
- // Insert a placeholder BEFORE recursing into the fields below —
2014
- // a self-referential class (`Node[T]`'s own `prev`/`next` fields
2015
- // point back to `Option[Node[T]]`) would otherwise recurse into
2016
- // resolving its own not-yet-registered specialization forever.
2017
- // The recursive re-entry only needs this specialization's
2018
- // MANGLED NAME to build its own field's type, not its fields —
2019
- // those get filled in for real below once the recursion unwinds.
2020
- self.classes.insert(mangled.clone(), vec![]);
2021
- let mut spec_class = specializeClass(class, &Substitution(bindings.clone()), &mangled);
2022
- for f in &mut spec_class.fields {
2023
- f.ty = self.resolveFieldType(&f.ty);
2024
- }
2025
- self.classes.insert(
2026
- mangled.clone(),
2027
- spec_class.fields.iter().map(|f| (f.name.clone(), crate::plumTypeFromAst(&f.ty))).collect(),
2028
- );
2029
- self.registerClassMethodSignatures(class, &mangled, &bindings);
2030
- }
2031
- return ast::Type { name: mangled, generics: vec![] };
2032
- }
2033
- }
2034
1911
  if let Some(e) = self.enums_generic_by_name.get(ty.name.as_str()).copied() {
2035
1912
  let params = enumGenericParams(e);
2036
1913
  if params.len() == type_args.len() {
@@ -2150,16 +2027,14 @@ impl<'a> Monomorphizer<'a> {
2150
2027
  // "union" enum field (`Wrapper(value: 5)` where `value:
2151
2028
  // Number`) or a named-payload enum variant field (`Circle
2152
2029
  // (radius: 5)`, though there `radius: Int` already matches so
2153
- // this is a no-op) — neither an ordinary class's nor a named
2030
+ // this is a no-op) — a named variant's OWN field types don't
2154
- // variant's OWN field types depend on generic specialization,
2031
+ // depend on generic specialization, so this can run
2155
- // so this can run unconditionally here regardless of whether
2032
+ // unconditionally here regardless of whether `call.type_name`
2156
- // `call.type_name` turns out to be generic or not below.
2033
+ // turns out to be generic or not below.
2157
2034
  for fa in &mut call.fields {
2158
- let expected_ty = self.classes.get(call.type_name.as_str())
2159
- .and_then(|fields| fields.iter().find(|(n, _)| n == &fa.name).map(|(_, t)| t.clone()))
2160
- .or_else(|| self.enum_variants.get(call.type_name.as_str()).and_then(|info| {
2035
+ let expected_ty = self.enum_variants.get(call.type_name.as_str()).and_then(|info| {
2161
- info.field_names.iter().position(|n| n == &fa.name).map(|i| info.field_types[i].clone())
2036
+ info.field_names.iter().position(|n| n == &fa.name).map(|i| info.field_types[i].clone())
2162
- }));
2037
+ });
2163
2038
  if let Some(expected) = expected_ty {
2164
2039
  self.wrapPrimitiveAgainstExpected(&mut fa.value, &expected, env);
2165
2040
  }
@@ -2167,7 +2042,7 @@ impl<'a> Monomorphizer<'a> {
2167
2042
  // Runs next (before the generic per-field rewrite below): a
2168
2043
  // bare payload-free variant field value (`Node(..., next:
2169
2044
  // None)`) carries no type of its own to infer a generic
2170
- // param from, and needs the class's OWN (about-to-be-
2045
+ // param from, and needs the enum's OWN (about-to-be-
2171
2046
  // specialized) field type to resolve which specialization it
2172
2047
  // actually means — `resolveClassInstantiation` handles that
2173
2048
  // internally once it knows the full binding set.
@@ -2388,20 +2263,17 @@ impl<'a> Monomorphizer<'a> {
2388
2263
  /// result has no generic syntax left in it — `checkSource`/`compileSource` run
2389
2264
  /// on it completely unmodified.
2390
2265
  pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2391
- let (global_env, classes, methods, enum_variants, enum_params) = crate::buildGlobalTables(source);
2266
+ let (global_env, methods, enum_variants, enum_params) = crate::buildGlobalTables(source);
2392
2267
  let min_required = crate::buildMinRequiredArgs(source);
2393
2268
 
2394
2269
  let mut m = Monomorphizer {
2395
- classes_generic: BTreeMap::new(),
2396
2270
  fns_generic: BTreeMap::new(),
2397
- methods_generic_on: BTreeMap::new(),
2398
2271
  methods_generic_on_enum: BTreeMap::new(),
2399
2272
  enums_generic_by_variant: BTreeMap::new(),
2400
2273
  enums_generic_by_name: BTreeMap::new(),
2401
2274
  enum_variant_mangling: BTreeMap::new(),
2402
2275
  fns_bare_generic: BTreeMap::new(),
2403
2276
  global_env,
2404
- classes,
2405
2277
  methods,
2406
2278
  enum_variants,
2407
2279
  enum_params,
@@ -2418,38 +2290,33 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2418
2290
  };
2419
2291
 
2420
2292
  for item in &source.items {
2421
- match item {
2293
+ if let ast::Item::Enum(e) = item {
2422
- ast::Item::Class(c) if !c.generics.is_empty() => { m.classes_generic.insert(c.name.clone(), c); }
2423
- ast::Item::Enum(e) if !enumGenericParams(e).is_empty() => {
2294
+ if !enumGenericParams(e).is_empty() {
2424
2295
  m.enums_generic_by_name.insert(e.name.clone(), e);
2425
2296
  for v in &e.variants {
2426
2297
  m.enums_generic_by_variant.insert(v.name.clone(), e);
2427
2298
  }
2428
2299
  }
2429
- _ => {}
2430
2300
  }
2431
2301
  }
2432
- // Classifies every method by its receiver (generic class/enum, or neither),
2302
+ // Classifies every method by its receiver (generic enum, or neither),
2433
- // populating `methods_generic_on`/`methods_generic_on_enum` — MUST run
2303
+ // populating `methods_generic_on_enum` — MUST run before the
2434
- // before the `fn_return_generic_enum` loop below, which calls
2304
+ // `fn_return_generic_enum` loop below, which calls `resolveFieldType` and
2435
- // `resolveFieldType` and can therefore trigger `registerClassMethodSignatures`
2305
+ // can therefore trigger `registerEnumMethodSignatures` for a generic enum
2436
- // for a generic class as a SIDE EFFECT (e.g. resolving a nested `List[Str]`
2306
+ // as a SIDE EFFECT (e.g. resolving a nested `List[Str]` inside some other
2437
- // inside some other function's `-> Result[List[Str], Str]` return type).
2307
+ // function's `-> Result[List[Str], Str]` return type).
2438
- // `registerClassMethodSignatures` looks up `methods_generic_on` and, finding
2308
+ // `registerEnumMethodSignatures` looks up `methods_generic_on_enum` and,
2439
- // it not yet populated, silently registers ZERO methods for that
2309
+ // finding it not yet populated, silently registers ZERO methods for that
2440
2310
  // specialization (`Some(methods) = ... else { return }`) — and because the
2441
- // specialization is marked done at that point (`self.classes` already has
2311
+ // specialization is marked done at that point (`self.enum_variant_mangling`
2442
- // the mangled name), no later call ever retries it, permanently leaving
2312
+ // already has the mangled name), no later call ever retries it, permanently
2443
- // e.g. `List$Str.reject` unresolvable for the rest of this pass (surfacing
2313
+ // leaving e.g. `List$Str.reject` unresolvable for the rest of this pass
2444
- // much later, and confusingly, as a chained method call inferring to `TVar`
2314
+ // (surfacing much later, and confusingly, as a chained method call
2445
- // deep inside some unrelated enum-construction site).
2315
+ // inferring to `TVar` deep inside some unrelated enum-construction site).
2446
2316
  for item in &source.items {
2447
2317
  if let ast::Item::Fn(f) = item {
2448
- let receiver_is_generic_class = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
2449
2318
  let receiver_is_generic_enum = f.type_param.as_deref().map(|r| m.enums_generic_by_name.contains_key(r)).unwrap_or(false);
2450
- if receiver_is_generic_class {
2319
+ if receiver_is_generic_enum {
2451
- m.methods_generic_on.entry(f.type_param.clone().unwrap()).or_default().push(f);
2452
- } else if receiver_is_generic_enum {
2453
2320
  m.methods_generic_on_enum.entry(f.type_param.clone().unwrap()).or_default().push(f);
2454
2321
  } else if f.type_param.is_none() && !fnGenericParams(f).is_empty() {
2455
2322
  m.fns_generic.insert(f.name.clone(), f);
@@ -2516,7 +2383,7 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2516
2383
  for item in &source.items {
2517
2384
  if let ast::Item::Fn(f) = item {
2518
2385
  let receiver_is_generic = f.type_param.as_deref()
2519
- .map(|r| m.classes_generic.contains_key(r) || m.enums_generic_by_name.contains_key(r))
2386
+ .map(|r| m.enums_generic_by_name.contains_key(r))
2520
2387
  .unwrap_or(false);
2521
2388
  let is_generic_fn = f.type_param.is_none() && !fnGenericParams(f).is_empty();
2522
2389
  let is_bare_generic_fn = f.type_param.is_none() && m.fns_bare_generic.contains_key(f.name.as_str());
@@ -2548,29 +2415,6 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2548
2415
 
2549
2416
  for item in &source.items {
2550
2417
  match item {
2551
- ast::Item::Class(c) if c.generics.is_empty() => {
2552
- let mut c2 = c.clone();
2553
- for f in &mut c2.fields {
2554
- f.ty = m.resolveFieldType(&f.ty);
2555
- }
2556
- // `self.classes` was seeded from the ORIGINAL, pre-monomorphization
2557
- // source (see `buildGlobalTables`, which — like `plumTypeFromAst`
2558
- // generally — doesn't resolve a field's own nested generics), so a
2559
- // field like `items: List[Int]` is still recorded there as bare
2560
- // `List`, not `List$Int`. Refresh it now that `resolveFieldType`
2561
- // has produced the real, resolved (and, for a generic field,
2562
- // mangled) type — otherwise any LATER inference of a `ClassCall`
2563
- // against this class (e.g. `self.infer` on a nested constructor
2564
- // call, during another item's own monomorphization) unifies the
2565
- // stale `List` against an actual `List$Int` value and fails, and
2566
- // that failure then gets silently swallowed by `infer`'s
2567
- // `unwrap_or(TVar("_"))` fallback further up the call chain.
2568
- m.classes.insert(
2569
- c2.name.clone(),
2570
- c2.fields.iter().map(|f| (f.name.clone(), crate::plumTypeFromAst(&f.ty))).collect(),
2571
- );
2572
- m.produced.push(ast::Item::Class(c2));
2573
- }
2574
2418
  ast::Item::Enum(e) if enumGenericParams(e).is_empty() => {
2575
2419
  let mut e2 = e.clone();
2576
2420
  for v in &mut e2.variants {
@@ -2579,20 +2423,33 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2579
2423
  }
2580
2424
  }
2581
2425
  // Refresh `self.enum_variants` with the resolved field types —
2582
- // same staleness reason as the `Class` arm above: a variant field
2426
+ // `self.enum_variants` was seeded from the ORIGINAL,
2583
- // like `List[Json]` was recorded there (from `buildGlobalTables`)
2427
+ // pre-monomorphization source (see `buildGlobalTables`, which —
2584
- // as bare `List`, not `List$Json`, and a later `self.infer` on a
2428
+ // like `plumTypeFromAst` generally doesn't resolve a field's
2585
- // construction site for this variant would otherwise unify that
2429
+ // own nested generics), so a variant field like `List[Json]` is
2430
+ // still recorded there as bare `List`, not `List$Json`.
2431
+ // Otherwise any LATER inference of a construction site for this
2432
+ // variant (e.g. `self.infer` during another item's own
2433
+ // monomorphization) unifies that stale name against the real,
2586
- // stale name against the real, specialized value's type and fail.
2434
+ // specialized value's type and fails, and that failure then
2435
+ // gets silently swallowed by `infer`'s `unwrap_or(TVar("_"))`
2436
+ // fallback further up the call chain.
2587
2437
  for v in &e2.variants {
2438
+ let existing = m.enum_variants.get(v.name.as_str());
2588
- let tag = m.enum_variants.get(v.name.as_str()).map(|info| info.tag).unwrap_or(0);
2439
+ let tag = existing.map(|info| info.tag).unwrap_or(0);
2589
2440
  // Bare-type variant sugar (see `buildGlobalTables`): `v.fields`
2590
- // is empty in the AST for `| FantasyBook`, but its resolved
2441
+ // is empty in the AST for `| Cat` or `| Int`, but its resolved
2591
- // field type was already recorded there as a single
2442
+ // field type was already recorded there (by the initial
2443
+ // `buildGlobalTables` seeding, before this loop runs) as a
2592
- // self-wrap `TNamed(v.name)` recompute that same shape here
2444
+ // single self-wrap `TNamed(v.name)` (a record-shaped enum) or
2445
+ // primitive bare-wrap (`Int`/`Float`) — reuse that same shape
2593
- // instead of clobbering it back to an empty vec.
2446
+ // here instead of clobbering it back to an empty vec.
2594
- let (field_types, field_names) = if v.fields.is_empty() && m.classes.contains_key(v.name.as_str()) {
2447
+ let existing_field_types = existing.map(|info| info.field_types.clone()).unwrap_or_default();
2448
+ let is_bare_wrap = v.fields.is_empty()
2595
- (vec![PlumType::TNamed(v.name.clone())], Vec::new())
2449
+ && (existing_field_types == [PlumType::TNamed(v.name.clone())]
2450
+ || existing_field_types == [crate::plumTypeFromName(&v.name)]);
2451
+ let (field_types, field_names) = if is_bare_wrap {
2452
+ (existing_field_types, Vec::new())
2596
2453
  } else {
2597
2454
  (v.fields.iter().map(crate::plumTypeFromAst).collect(), v.field_names.clone())
2598
2455
  };
@@ -2618,7 +2475,7 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2618
2475
  }
2619
2476
  ast::Item::Fn(f) => {
2620
2477
  let receiver_is_generic = f.type_param.as_deref()
2621
- .map(|r| m.classes_generic.contains_key(r) || m.enums_generic_by_name.contains_key(r))
2478
+ .map(|r| m.enums_generic_by_name.contains_key(r))
2622
2479
  .unwrap_or(false);
2623
2480
  let is_generic_fn = f.type_param.is_none() && !fnGenericParams(f).is_empty();
2624
2481
  let is_bare_generic_fn = f.type_param.is_none() && m.fns_bare_generic.contains_key(f.name.as_str());
@@ -2630,7 +2487,7 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2630
2487
  m.produced.push(ast::Item::Fn(f2));
2631
2488
  }
2632
2489
  }
2633
- _ => {} // generic Class/Enum declarations dropped here — templates only
2490
+ _ => {} // generic Enum declarations dropped here — templates only
2634
2491
  }
2635
2492
  }
2636
2493
 
@@ -2641,83 +2498,6 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2641
2498
  return Err("monomorphize: exceeded specialization limit (possible unbounded generic recursion)".to_string());
2642
2499
  }
2643
2500
  match pending {
2644
- PendingSpecialization::Class { base, subst, mangled } => {
2645
- if !m.specialized.insert(mangled.clone()) { continue; }
2646
- let mut spec_class = specializeClass(base, &subst, &mangled);
2647
- // `specializeClass` only substitutes a field's bare generic-param
2648
- // NAME (`T` -> `Int`) — a field like `Option[Node[T]]` becomes
2649
- // `Option[Node[Int]]`, still a generic instantiation, not yet a
2650
- // real (mangled) type. Resolve those the rest of the way now.
2651
- for f in &mut spec_class.fields {
2652
- f.ty = m.resolveFieldType(&f.ty);
2653
- }
2654
- // Register the specialized class's fields so inference inside its
2655
- // own (and other items') bodies can resolve `receiver.field` on the
2656
- // mangled type — `self.classes` was built from the ORIGINAL source
2657
- // and would otherwise not know this freshly-minted class.
2658
- m.classes.insert(
2659
- mangled.clone(),
2660
- spec_class.fields.iter().map(|f| (f.name.clone(), crate::plumTypeFromAst(&f.ty))).collect(),
2661
- );
2662
- m.produced.push(ast::Item::Class(spec_class));
2663
- // Same self-nesting landmine as `registerMethodSignatures` —
2664
- // if THIS specialization is itself already nested
2665
- // (`isSelfNested`), skips (only) a method whose return type
2666
- // would construct yet another nested specialization
2667
- // (`methodMentionsTemplate`; see `isSelfNested`'s doc
2668
- // comment). Without this, `List.chunk() -> List[List[T]]`
2669
- // would recurse into producing an unbounded chain of
2670
- // ever-more-nested `List` specializations via THIS SAME
2671
- // loop — and since this loop runs once per separate
2672
- // WORKLIST pop rather than recursively, a guard scoped to a
2673
- // single call stack isn't enough on its own.
2674
- {
2675
- let already_nested = m.isSelfNested(&base.name, &subst.0);
2676
- if let Some(methods) = m.methods_generic_on.get(base.name.as_str()).cloned() {
2677
- let owner_params = classGenericParams(base);
2678
- for method in methods {
2679
- // A method with generic param(s) of its OWN (e.g. `map`'s
2680
- // `U`, beyond `List[T]`'s own `T`) can't be produced HERE —
2681
- // `U` is only known at a particular CALL SITE, not at class-
2682
- // specialization time. `resolveMethodOwnGenerics` queues a
2683
- // `PendingSpecialization::Fn` for each concrete `U` it
2684
- // actually sees used, instead.
2685
- if !methodOwnGenericParams(method, &owner_params).is_empty() {
2686
- continue;
2687
- }
2688
- if already_nested && Monomorphizer::methodMentionsTemplate(method, &base.name) {
2689
- continue;
2690
- }
2691
- let mut specialized_method = specializeFn(method, &subst, &method.name, Some(mangled.clone()));
2692
- // Params (not returns — see `resolveFnParamTypes`'s doc
2693
- // comment) must run before `rewriteFnBody`/registration
2694
- // below: both read the signature's types directly off
2695
- // this AST, and the real checker later rebuilds its own
2696
- // tables from this exact (post-monomorphize) AST too.
2697
- m.resolveFnParamTypes(&mut specialized_method);
2698
- m.rewriteFnBody(&mut specialized_method, true)?;
2699
- m.resolveFnReturnType(&mut specialized_method);
2700
- // Register the specialized method's signature under its
2701
- // (mangled receiver, method name) key so any later body that
2702
- // dispatches to it can resolve its concrete return type.
2703
- let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
2704
- ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
2705
- ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
2706
- ast::ParamType::Fn(params, ret) => {
2707
- let param_types = params.iter().map(crate::plumTypeFromAst).collect();
2708
- let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
2709
- PlumType::TFun(param_types, Box::new(ret_ty))
2710
- }
2711
- }).collect();
2712
- let ret = specialized_method.returns.as_ref()
2713
- .map(crate::plumTypeFromAst)
2714
- .unwrap_or(PlumType::TUnit);
2715
- m.methods.insert((mangled.clone(), specialized_method.name.clone()), PlumType::TFun(param_types, Box::new(ret)));
2716
- m.produced.push(ast::Item::Fn(specialized_method));
2717
- }
2718
- }
2719
- }
2720
- }
2721
2501
  PendingSpecialization::Fn { base, subst, mangled, new_receiver } => {
2722
2502
  if !m.specialized.insert(mangled.clone()) { continue; }
2723
2503
  let mut specialized_fn = specializeFn(base, &subst, &mangled, new_receiver);
@@ -2747,21 +2527,32 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2747
2527
  // `specializeEnum` only substitutes a bare generic-param NAME —
2748
2528
  // a field like `List[T]` becomes `List[Int]`, still a generic
2749
2529
  // instantiation, not yet a real (mangled) type. Resolve those
2750
- // the rest of the way now, same as the `Class` arm above.
2530
+ // the rest of the way now.
2751
2531
  for v in &mut spec_enum.variants {
2752
2532
  for f in &mut v.fields {
2753
2533
  *f = m.resolveFieldType(f);
2754
2534
  }
2755
2535
  }
2756
2536
  m.produced.push(ast::Item::Enum(spec_enum));
2757
- // Guarded against the same self-nesting landmine as the
2537
+ // Guarded against the self-nesting landmine documented on
2758
- // `Class` arm above see `isSelfNested`'s doc comment.
2538
+ // `isSelfNested` — without this, `List.chunk() -> List[List[T]]`
2539
+ // would recurse into producing an unbounded chain of
2540
+ // ever-more-nested `List` specializations via THIS SAME loop —
2541
+ // and since this loop runs once per separate WORKLIST pop
2542
+ // rather than recursively, a guard scoped to a single call
2543
+ // stack isn't enough on its own.
2759
2544
  {
2760
2545
  let already_nested = m.isSelfNested(&base.name, &subst.0);
2761
2546
  if let Some(methods) = m.methods_generic_on_enum.get(base.name.as_str()).cloned() {
2762
2547
  let owner_params = enumGenericParams(base);
2763
2548
  for method in methods {
2764
- // See the matching comment in the `Class` arm above.
2549
+ // A method with generic param(s) of its OWN (e.g.
2550
+ // `map`'s `U`, beyond `List[T]`'s own `T`) can't be
2551
+ // produced HERE — `U` is only known at a particular
2552
+ // CALL SITE, not at enum-specialization time.
2553
+ // `resolveMethodOwnGenerics` queues a
2554
+ // `PendingSpecialization::Fn` for each concrete `U`
2555
+ // it actually sees used, instead.
2765
2556
  if !methodOwnGenericParams(method, &owner_params).is_empty() {
2766
2557
  continue;
2767
2558
  }
plum-checker/tests/checker_tests.rs CHANGED
@@ -6,13 +6,11 @@ use plum_core::{ast::*, AstParser};
6
6
 
7
7
  fn emptyCtx() -> CheckCtx<'static> {
8
8
  use std::sync::OnceLock;
9
- static CLASSES: OnceLock<plum_checker::ClassEnv> = OnceLock::new();
10
9
  static METHODS: OnceLock<plum_checker::MethodEnv> = OnceLock::new();
11
10
  static ENUM_VARIANTS: OnceLock<plum_checker::EnumVariants> = OnceLock::new();
12
11
  static ENUM_PARAMS: OnceLock<plum_checker::EnumParams> = OnceLock::new();
13
12
  static MIN_REQUIRED: OnceLock<plum_checker::MinRequiredArgs> = OnceLock::new();
14
13
  CheckCtx {
15
- classes: CLASSES.get_or_init(Default::default),
16
14
  methods: METHODS.get_or_init(Default::default),
17
15
  enum_variants: ENUM_VARIANTS.get_or_init(Default::default),
18
16
  enum_params: ENUM_PARAMS.get_or_init(Default::default),
@@ -147,7 +145,7 @@ fn boolLiteralWrongReturnTypeIsError() {
147
145
 
148
146
  #[test]
149
147
  fn methodSelfFieldAccessPasses() {
150
- let src = "type Cat =\n name: Str\n age: Int\n\n fun getName() -> Str =\n self.name\n";
148
+ let src = "enum Cat =\n | Cat(name: Str, age: Int)\n\n fun getName() -> Str =\n self.name\n";
151
149
  let source = parse(src);
152
150
  let result = checkSource(&source);
153
151
  assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
@@ -155,7 +153,7 @@ fn methodSelfFieldAccessPasses() {
155
153
 
156
154
  #[test]
157
155
  fn methodSelfUnknownFieldIsError() {
158
- let src = "type Cat =\n name: Str\n\n fun getAge() -> Int =\n self.age\n";
156
+ let src = "enum Cat =\n | Cat(name: Str)\n\n fun getAge() -> Int =\n self.age\n";
159
157
  let source = parse(src);
160
158
  let result = checkSource(&source);
161
159
  assert!(result.is_err());
@@ -163,14 +161,14 @@ fn methodSelfUnknownFieldIsError() {
163
161
 
164
162
  #[test]
165
163
  fn nestedMethodTypeChecks() {
166
- let src = "type Cat =\n name: Str\n age: Int\n\n fun getName(self) -> Str =\n self.name\n";
164
+ let src = "enum Cat =\n | Cat(name: Str, age: Int)\n\n fun getName(self) -> Str =\n self.name\n";
167
165
  let result = checkSource(&parse(src));
168
166
  assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
169
167
  }
170
168
 
171
169
  #[test]
172
170
  fn nestedMethodUnknownFieldIsError() {
173
- let src = "type Cat =\n name: Str\n\n fun getAge(self) -> Int =\n self.age\n";
171
+ let src = "enum Cat =\n | Cat(name: Str)\n\n fun getAge(self) -> Int =\n self.age\n";
174
172
  assert!(checkSource(&parse(src)).is_err());
175
173
  }
176
174
 
@@ -184,7 +182,7 @@ fn selfOutsideMethodIsError() {
184
182
 
185
183
  #[test]
186
184
  fn classCallChecksFieldTypes() {
187
- let src = "type Cat =\n name: Str\n age: Int\n\nfun makeCat() -> Cat =\n Cat(name: \"x\", age: 1)\n";
185
+ let src = "enum Cat =\n | Cat(name: Str, age: Int)\n\nfun makeCat() -> Cat =\n Cat(name: \"x\", age: 1)\n";
188
186
  let source = parse(src);
189
187
  let result = checkSource(&source);
190
188
  assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
@@ -192,7 +190,7 @@ fn classCallChecksFieldTypes() {
192
190
 
193
191
  #[test]
194
192
  fn classCallWrongFieldTypeIsError() {
195
- let src = "type Cat =\n name: Str\n age: Int\n\nfun makeCat() -> Cat =\n Cat(name: \"x\", age: \"y\")\n";
193
+ let src = "enum Cat =\n | Cat(name: Str, age: Int)\n\nfun makeCat() -> Cat =\n Cat(name: \"x\", age: \"y\")\n";
196
194
  let source = parse(src);
197
195
  let result = checkSource(&source);
198
196
  assert!(result.is_err());
@@ -200,7 +198,7 @@ fn classCallWrongFieldTypeIsError() {
200
198
 
201
199
  #[test]
202
200
  fn classCallUnknownFieldIsError() {
203
- let src = "type Cat =\n name: Str\n\nfun makeCat() -> Cat =\n Cat(name: \"x\", age: 1)\n";
201
+ let src = "enum Cat =\n | Cat(name: Str)\n\nfun makeCat() -> Cat =\n Cat(name: \"x\", age: 1)\n";
204
202
  let source = parse(src);
205
203
  let result = checkSource(&source);
206
204
  assert!(result.is_err());
@@ -208,7 +206,7 @@ fn classCallUnknownFieldIsError() {
208
206
 
209
207
  #[test]
210
208
  fn methodCallViaAttributeTypeChecksArgs() {
211
- let src = "type Cat =\n name: Str\n\n fun rename(n: Str) -> Str =\n n\n\nfun use(c: Cat) -> Str =\n c.rename(\"x\")\n";
209
+ let src = "enum Cat =\n | Cat(name: Str)\n\n fun rename(n: Str) -> Str =\n n\n\nfun use(c: Cat) -> Str =\n c.rename(\"x\")\n";
212
210
  let source = parse(src);
213
211
  let result = checkSource(&source);
214
212
  assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
@@ -435,42 +433,16 @@ fun bad(s: Shape) -> Float =
435
433
  }
436
434
 
437
435
  #[test]
438
- fn classNameCollidingWithNonSelfWrapEnumVariantIsAClearError() {
439
- // `Cat(...)` is ambiguous when `Cat` is both a class and an enum variant
440
- // whose payload ISN'T that same class (`Cat[Int]`, not bare `Cat`):
441
- // downstream code consults `enum_variants` first, so the class
442
- // constructor would otherwise be silently shadowed with no diagnostic.
443
- // Compare `bareTypeEnumVariantSugarWrapsTheSameNamedClass` below, where
436
+ fn bareTypeEnumVariantSugarWrapsTheSameNamedRecordShapedEnum() {
444
- // `Cat` bare (no `[...]`) is deliberately allowed.
437
+ // `| Cat` (bare, no `[...]`) naming an already-declared record-shaped enum
438
+ // (the `type Cat = ...` replacement) is sugar for wrapping that whole
439
+ // record as the variant's single payload field — shorthand for `| Cat[Cat]`
440
+ // under a separate tag name. This is the DOP-friendly form: a sealed union
441
+ // of plain records, matching Java's "sealed interface implemented directly
442
+ // by records" shape.
445
443
  let src = "\
446
- type Cat =
444
+ enum Cat =
447
- name: Str
445
+ | Cat(name: Str)
448
-
449
- enum Animal =
450
- | Cat(Int)
451
- | Dog
452
- ";
453
- let source = parse(src);
454
- let result = checkSource(&source);
455
- assert!(result.is_err(), "expected Err");
456
- let errs = result.unwrap_err();
457
- assert!(
458
- errs.iter().any(|e| e.message.contains("is declared as both a class and an enum variant")),
459
- "got: {:?}",
460
- errs
461
- );
462
- }
463
-
464
- #[test]
465
- fn bareTypeEnumVariantSugarWrapsTheSameNamedClass() {
466
- // `| Cat` (bare, no `[...]`) naming an already-declared `type Cat` is
467
- // sugar for wrapping that whole class as the variant's single payload
468
- // field — shorthand for `| Cat[Cat]` under a separate tag name. This is
469
- // the DOP-friendly form: a sealed union of plain records, matching
470
- // Java's "sealed interface implemented directly by records" shape.
471
- let src = "\
472
- type Cat =
473
- name: Str
474
446
 
475
447
  enum Animal =
476
448
  | Cat
@@ -491,8 +463,8 @@ fun catName(a: Animal) -> Str =
491
463
  #[test]
492
464
  fn genericClassInstantiatedAtTwoConcreteTypesTypeChecks() {
493
465
  let src = "\
494
- type Box[T] =
466
+ enum Box[T] =
495
- value: T
467
+ | Box(value: T)
496
468
 
497
469
  fun makeIntBox() -> Box =
498
470
  Box(value: 5)
@@ -653,8 +625,8 @@ fun useStr() -> Str =
653
625
  #[test]
654
626
  fn genericMethodOnGenericClassTypeChecks() {
655
627
  let src = "\
656
- type Box[T] =
628
+ enum Box[T] =
657
- value: T
629
+ | Box(value: T)
658
630
 
659
631
  fun getValue() -> T =
660
632
  self.value
@@ -679,8 +651,8 @@ fn unboundedRecursiveGenericInstantiationIsAClearError() {
679
651
  // declaration, which is never itself a call site and so never reaches the
680
652
  // worklist at all) and must fail with a clear, bounded error rather than hang.
681
653
  let src = "\
682
- type Box[T] =
654
+ enum Box[T] =
683
- value: T
655
+ | Box(value: T)
684
656
 
685
657
  fun recurse(v: T) -> Int =
686
658
  b = Box(value: v)
@@ -736,8 +708,8 @@ fn ordinaryFunctionWithBareGenericClassParamTypeChecks() {
736
708
  // now, but the identical root cause: `Box` is dropped from the monomorphized
737
709
  // output, so a bare `Box`-typed param would otherwise reference nothing.
738
710
  let src = "\
739
- type Box[T] =
711
+ enum Box[T] =
740
- value: T
712
+ | Box(value: T)
741
713
 
742
714
  fun getBoxValue() -> T =
743
715
  self.value
@@ -784,9 +756,8 @@ fun each(cb: fn(Int) -> Bool) -> Bool =
784
756
  #[test]
785
757
  fn fieldAssignmentTargetWithMatchingTypePasses() {
786
758
  let src = "\
787
- type Cat =
759
+ enum Cat =
788
- name: Str
760
+ | Cat(name: Str, age: Int)
789
- age: Int
790
761
 
791
762
  fun haveBirthday() =
792
763
  self.age = self.age + 1
@@ -798,9 +769,8 @@ type Cat =
798
769
  #[test]
799
770
  fn fieldAssignmentTargetWithMismatchedTypeIsError() {
800
771
  let src = "\
801
- type Cat =
772
+ enum Cat =
802
- name: Str
773
+ | Cat(name: Str, age: Int)
803
- age: Int
804
774
 
805
775
  fun breakCat() =
806
776
  self.age = \"oops\"
@@ -813,9 +783,8 @@ type Cat =
813
783
  #[test]
814
784
  fn fieldAssignmentTargetUnknownFieldIsError() {
815
785
  let src = "\
816
- type Cat =
786
+ enum Cat =
817
- name: Str
787
+ | Cat(name: Str, age: Int)
818
- age: Int
819
788
 
820
789
  fun breakCat() =
821
790
  self.nope = 1
@@ -981,8 +950,8 @@ fn classImplementingTraitWithMatchingMethodPasses() {
981
950
  trait Greeter =
982
951
  greet(name: Str) -> Str
983
952
 
984
- type Robot(Greeter) =
953
+ enum Robot(Greeter) =
985
- id: Int
954
+ | Robot(id: Int)
986
955
 
987
956
  fun greet(self, name: Str) -> Str =
988
957
  \"beep {name}\"
@@ -997,8 +966,8 @@ fn classClaimingTraitButMissingMethodIsError() {
997
966
  trait Greeter =
998
967
  greet(name: Str) -> Str
999
968
 
1000
- type Robot(Greeter) =
969
+ enum Robot(Greeter) =
1001
- id: Int
970
+ | Robot(id: Int)
1002
971
  ";
1003
972
  let source = parse(src);
1004
973
  let result = checkSource(&source);
@@ -1013,8 +982,8 @@ fn classClaimingTraitWithWrongParamCountIsError() {
1013
982
  trait Greeter =
1014
983
  greet(name: Str) -> Str
1015
984
 
1016
- type Robot(Greeter) =
985
+ enum Robot(Greeter) =
1017
- id: Int
986
+ | Robot(id: Int)
1018
987
 
1019
988
  fun greet(self) -> Str =
1020
989
  \"beep\"
@@ -1032,8 +1001,8 @@ fn classClaimingTraitWithWrongParamTypeIsError() {
1032
1001
  trait Greeter =
1033
1002
  greet(name: Str) -> Str
1034
1003
 
1035
- type Robot(Greeter) =
1004
+ enum Robot(Greeter) =
1036
- id: Int
1005
+ | Robot(id: Int)
1037
1006
 
1038
1007
  fun greet(self, name: Int) -> Str =
1039
1008
  \"beep\"
@@ -1051,8 +1020,8 @@ fn classClaimingTraitWithWrongReturnTypeIsError() {
1051
1020
  trait Greeter =
1052
1021
  greet(name: Str) -> Str
1053
1022
 
1054
- type Robot(Greeter) =
1023
+ enum Robot(Greeter) =
1055
- id: Int
1024
+ | Robot(id: Int)
1056
1025
 
1057
1026
  fun greet(self, name: Str) -> Int =
1058
1027
  0
@@ -1070,8 +1039,8 @@ fn classClaimingUndeclaredTraitNameIsNotEnforced() {
1070
1039
  // never declared as a real `trait` anywhere — silently unenforced, not an
1071
1040
  // error, since there is nothing real to check it against.
1072
1041
  let src = "\
1073
- type Robot(NotARealTrait) =
1042
+ enum Robot(NotARealTrait) =
1074
- id: Int
1043
+ | Robot(id: Int)
1075
1044
  ";
1076
1045
  let source = parse(src);
1077
1046
  assert!(checkSource(&source).is_ok(), "expected Ok, got: {:?}", checkSource(&source));
@@ -1091,8 +1060,8 @@ enum Option[T] =
1091
1060
  trait Err =
1092
1061
  cause() -> Option
1093
1062
 
1094
- type MyErr(Err) =
1063
+ enum MyErr(Err) =
1095
- pos: Int
1064
+ | MyErr(pos: Int)
1096
1065
 
1097
1066
  fun cause(self) -> Option[MyErr] =
1098
1067
  None
@@ -1104,9 +1073,8 @@ type MyErr(Err) =
1104
1073
  #[test]
1105
1074
  fn matchDestructuresPlainClassAndBindsFieldTypes() {
1106
1075
  let src = "\
1107
- type Point =
1076
+ enum Point =
1108
- x: Int
1077
+ | Point(x: Int, y: Int)
1109
- y: Int
1110
1078
 
1111
1079
  fun sumPoint(p: Point) -> Int =
1112
1080
  match p
@@ -1119,9 +1087,8 @@ fun sumPoint(p: Point) -> Int =
1119
1087
  #[test]
1120
1088
  fn matchClassPatternWrongFieldCountIsError() {
1121
1089
  let src = "\
1122
- type Point =
1090
+ enum Point =
1123
- x: Int
1091
+ | Point(x: Int, y: Int)
1124
- y: Int
1125
1092
 
1126
1093
  fun sumPoint(p: Point) -> Int =
1127
1094
  match p
plum-checker/tests/monomorphize_tests.rs CHANGED
@@ -13,17 +13,6 @@ fn isGenericParamNameAcceptsSingleUppercaseLettersOnly() {
13
13
  assert!(!isGenericParamName(""));
14
14
  }
15
15
 
16
- #[test]
17
- fn classGenericParamsReadsDeclaredGenericsList() {
18
- let c = ast::Class {
19
- name: "Box".to_string(),
20
- implements: vec![],
21
- generics: vec![ast::GenericParam { name: "T".to_string(), bounds: vec![] }],
22
- fields: vec![ast::Field { name: "value".to_string(), ty: ast::Type { name: "T".to_string(), generics: vec![] } }],
23
- };
24
- assert_eq!(classGenericParams(&c), vec!["T".to_string()]);
25
- }
26
-
27
16
  #[test]
28
17
  fn fnGenericParamsDetectsImplicitUppercaseLetterTypesInOrder() {
29
18
  let f = ast::Fn {
@@ -45,6 +34,7 @@ fn enumGenericParamsDetectsImplicitUppercaseLetterVariantFields() {
45
34
  let e = ast::Enum {
46
35
  name: "Option".to_string(),
47
36
  generics: vec![],
37
+ implements: vec![],
48
38
  params: vec![],
49
39
  variants: vec![
50
40
  ast::EnumVariant { name: "Some".to_string(), fields: vec![ast::Type { name: "T".to_string(), generics: vec![] }], field_names: vec![], values: vec![] },
@@ -61,22 +51,6 @@ fn mangleJoinsBaseNameAndTypeArgs() {
61
51
  assert_eq!(mangle("Green", &[]), "Green");
62
52
  }
63
53
 
64
- #[test]
65
- fn specializeClassSubstitutesGenericFieldTypesAndClearsGenericsList() {
66
- let c = ast::Class {
67
- name: "Box".to_string(),
68
- implements: vec![],
69
- generics: vec![ast::GenericParam { name: "T".to_string(), bounds: vec![] }],
70
- fields: vec![ast::Field { name: "value".to_string(), ty: ast::Type { name: "T".to_string(), generics: vec![] } }],
71
- };
72
- let mut bindings = std::collections::BTreeMap::new();
73
- bindings.insert("T".to_string(), PlumType::TInt);
74
- let specialized = specializeClass(&c, &Substitution(bindings), "Box$Int");
75
- assert_eq!(specialized.name, "Box$Int");
76
- assert!(specialized.generics.is_empty());
77
- assert_eq!(specialized.fields[0].ty.name, "Int");
78
- }
79
-
80
54
  #[test]
81
55
  fn specializeFnSubstitutesGenericParamAndReturnTypes() {
82
56
  let f = ast::Fn {
@@ -121,6 +95,7 @@ fn specializeEnumSubstitutesGenericVariantFieldNames() {
121
95
  let e = ast::Enum {
122
96
  name: "Option".to_string(),
123
97
  generics: vec![],
98
+ implements: vec![],
124
99
  params: vec![],
125
100
  variants: vec![
126
101
  ast::EnumVariant { name: "Some".to_string(), fields: vec![ast::Type { name: "T".to_string(), generics: vec![] }], field_names: vec![], values: vec![] },
plum-core/src/ast.rs CHANGED
@@ -65,7 +65,6 @@ pub struct Import {
65
65
 
66
66
  #[derive(Debug, Clone, PartialEq)]
67
67
  pub enum Item {
68
- Class(Class),
69
68
  Trait(Trait),
70
69
  Enum(Enum),
71
70
  Fn(Fn),
@@ -75,26 +74,12 @@ pub enum Item {
75
74
 
76
75
  // ---------- Type definitions ----------
77
76
 
78
- #[derive(Debug, Clone, PartialEq)]
79
- pub struct Class {
80
- pub name: String,
81
- pub implements: Vec<String>,
82
- pub generics: Vec<GenericParam>,
83
- pub fields: Vec<Field>,
84
- }
85
-
86
77
  #[derive(Debug, Clone, PartialEq)]
87
78
  pub struct GenericParam {
88
79
  pub name: String,
89
80
  pub bounds: Vec<String>,
90
81
  }
91
82
 
92
- #[derive(Debug, Clone, PartialEq)]
93
- pub struct Field {
94
- pub name: String,
95
- pub ty: Type,
96
- }
97
-
98
83
  #[derive(Debug, Clone, PartialEq)]
99
84
  pub struct Trait {
100
85
  pub name: String,
@@ -113,8 +98,13 @@ pub struct TraitMethod {
113
98
  pub struct Enum {
114
99
  pub name: String,
115
100
  /// The enum's OWN declared generic type parameters (`enum Option[T] =
116
- /// ...`) — mirrors `Class::generics`. Empty for a non-generic enum.
101
+ /// ...`). Empty for a non-generic enum.
117
102
  pub generics: Vec<GenericParam>,
103
+ /// Trait names this enum claims to implement (`enum Cat(ToStr) = ...`).
104
+ /// Mutually exclusive with `params` at the grammar level (see
105
+ /// `grammar.js`'s `enum` rule) — a discriminant enum's shared per-variant
106
+ /// values and a plain enum's trait conformance never coexist.
107
+ pub implements: Vec<String>,
118
108
  pub params: Vec<EnumParam>,
119
109
  pub variants: Vec<EnumVariant>,
120
110
  }
plum-core/src/builtin_usage.rs CHANGED
@@ -24,11 +24,6 @@ pub fn usedBuiltinTypeNames(source: &Source) -> HashSet<String> {
24
24
  let mut names = HashSet::new();
25
25
  for item in &source.items {
26
26
  match item {
27
- Item::Class(c) => {
28
- for f in &c.fields {
29
- walkType(&f.ty, &mut names);
30
- }
31
- }
32
27
  Item::Trait(t) => {
33
28
  for m in &t.methods {
34
29
  for p in &m.params {
plum-core/src/loader.rs CHANGED
@@ -85,15 +85,16 @@ fn checkBuiltinImports(path: &Path, source: &Source) -> Result<(), String> {
85
85
  if !used.contains(name) {
86
86
  continue;
87
87
  }
88
- // A file can also "declare itself" by giving `Int`/`Float` as a bare,
88
+ // A file can also "declare itself" either as an ordinary enum sharing
89
- // payload-free variant name of one of its OWN enums (a "union" enum's
90
- // primitive bare-wrap sugar, e.g. `enum Number = | Int | Float` — see
89
+ // this exact name (e.g. `enum Str = | Str(data: Buffer) ...`, the
90
+ // record-shaped form `type Str = ...` used to be) or by giving
91
+ // `Int`/`Float` as a bare, payload-free variant name of one of its OWN
92
+ // enums (a "union" enum's primitive bare-wrap sugar, e.g. `enum Number
91
- // `plum-checker`'s `buildGlobalTables`) instead of the older `type
93
+ // = | Int | Float` — see `plum-checker`'s `buildGlobalTables`)
92
- // Int = ...` class form — either way, the file is the one DECLARING
94
+ // either way, the file is the one DECLARING the type, not merely
93
- // the type, not merely using it.
95
+ // using it.
94
96
  let declares_self = source.items.iter().any(|i| match i {
95
- Item::Class(c) => c.name == name,
96
- Item::Enum(e) => e.variants.iter().any(|v| v.name == name && v.fields.is_empty()),
97
+ Item::Enum(e) => e.name == name || e.variants.iter().any(|v| v.name == name && v.fields.is_empty()),
97
98
  _ => false,
98
99
  });
99
100
  if declares_self {
@@ -205,11 +206,10 @@ fn mergeItems(
205
206
  /// A collision key that mirrors `plum-checker`'s own separation of
206
207
  /// namespaces: methods are keyed by `(receiver, name)` (so `length<Cat>` and
207
208
  /// `length<Box>` never collide, exactly like `plum_checker::MethodEnv`),
208
- /// while classes/traits/enums/consts/free-functions are each their own
209
+ /// while traits/enums/consts/free-functions are each their own flat,
209
- /// flat, kind-qualified namespace.
210
+ /// kind-qualified namespace.
210
211
  fn itemNameKey(item: &Item) -> String {
211
212
  match item {
212
- Item::Class(c) => format!("class::{}", c.name),
213
213
  Item::Trait(t) => format!("trait::{}", t.name),
214
214
  Item::Enum(e) => format!("enum::{}", e.name),
215
215
  Item::Const(c) => format!("const::{}", c.name),
plum-core/src/parser.rs CHANGED
@@ -65,16 +65,6 @@ impl<'a> AstParser<'a> {
65
65
  imports.push(self.parseImport(child));
66
66
  topLevelOrder.push(TopLevelKind::Import);
67
67
  }
68
- "class" => {
69
- let c = self.parseClass(child);
70
- let nested = self.collectNestedFns(child, &c.name);
71
- items.push(Item::Class(c));
72
- topLevelOrder.push(TopLevelKind::Other);
73
- for f in nested {
74
- items.push(Item::Fn(f));
75
- topLevelOrder.push(TopLevelKind::Other);
76
- }
77
- }
78
68
  "trait" => {
79
69
  items.push(Item::Trait(self.parseTrait(child)));
80
70
  topLevelOrder.push(TopLevelKind::Other);
@@ -139,34 +129,6 @@ impl<'a> AstParser<'a> {
139
129
  .collect()
140
130
  }
141
131
 
142
- fn parseClass(&self, node: Node) -> Class {
143
- // class: "type" type_identifier generics? ("(" type_identifier,* ")")? "=" body
144
- // Named children in order: type_identifier (name), generics? (declaration), type_identifier* (implements), field*
145
- let named: Vec<Node> = self.namedChildren(node);
146
-
147
- let name = named.first().map(|n| self.text(*n)).unwrap_or_default();
148
-
149
- // Skip the optional `generics` declaration node before looking for implements.
150
- let after_generics = if named.get(1).map(|n| n.kind()) == Some("generics") { 2 } else { 1 };
151
-
152
- // implements = type_identifiers that appear before any `field` node
153
- let implements: Vec<String> = named[after_generics..]
154
- .iter()
155
- .take_while(|n| n.kind() == "type_identifier")
156
- .map(|n| self.text(*n))
157
- .collect();
158
-
159
- let generics = self.parseGenericsField(node);
160
-
161
- let fields: Vec<Field> = named
162
- .iter()
163
- .filter(|n| n.kind() == "field")
164
- .map(|n| self.parseField(*n))
165
- .collect();
166
-
167
- Class { name, implements, generics, fields }
168
- }
169
-
170
132
  fn parseGenericsField(&self, node: Node) -> Vec<GenericParam> {
171
133
  // generics: "[" generic_type,* "]" where generic_type: generic (":" sep1(type_identifier, "+"))?
172
134
  //
@@ -197,16 +159,6 @@ impl<'a> AstParser<'a> {
197
159
  params
198
160
  }
199
161
 
200
- fn parseField(&self, node: Node) -> Field {
201
- // class_field (aliased to field): var_identifier ":" type
202
- let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
203
- let ty = node
204
- .named_child(1)
205
- .map(|n| self.parseType(n))
206
- .unwrap_or(Type { name: String::new(), generics: vec![] });
207
- Field { name, ty }
208
- }
209
-
210
162
  fn parseTrait(&self, node: Node) -> Trait {
211
163
  // trait: "trait" type_identifier generics? "=" body
212
164
  let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
@@ -238,7 +190,20 @@ impl<'a> AstParser<'a> {
238
190
  }
239
191
 
240
192
  fn parseEnum(&self, node: Node) -> Enum {
193
+ // enum: "enum" type_identifier generics? (implements | params)? "=" body
194
+ // `implements` (bare `type_identifier`,*) and `params` (`enum_param`,*) are
195
+ // mutually exclusive at the grammar level — same comment-safe positional
196
+ // technique `parseClass` used to use for its own `implements` list, since
197
+ // `implements`'s items are bare `type_identifier` nodes, the same kind as
198
+ // the enum's own name at position 0.
199
+ let named: Vec<Node> = self.namedChildren(node);
241
- let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
200
+ let name = named.first().map(|n| self.text(*n)).unwrap_or_default();
201
+ let after_generics = if named.get(1).map(|n| n.kind()) == Some("generics") { 2 } else { 1 };
202
+ let implements: Vec<String> = named[after_generics..]
203
+ .iter()
204
+ .take_while(|n| n.kind() == "type_identifier")
205
+ .map(|n| self.text(*n))
206
+ .collect();
242
207
  let generics = self.parseGenericsField(node);
243
208
  let params = self.childrenOfKind(node, "enum_param")
244
209
  .into_iter()
@@ -248,7 +213,7 @@ impl<'a> AstParser<'a> {
248
213
  .into_iter()
249
214
  .map(|f| self.parseEnumVariant(f))
250
215
  .collect();
251
- Enum { name, generics, params, variants }
216
+ Enum { name, generics, implements, params, variants }
252
217
  }
253
218
 
254
219
  fn parseEnumParam(&self, node: Node) -> EnumParam {
plum-core/tests/formatter_test.rs CHANGED
@@ -41,9 +41,8 @@ fn keywordsGetSpacedCorrectly() {
41
41
  module test
42
42
  import std/os
43
43
 
44
- type Cat(IAnimal) =
44
+ enum Cat(IAnimal) =
45
- name: Str
45
+ | Cat(name: Str, age: Int)
46
- age: Int
47
46
 
48
47
  fun speak(self) -> Str =
49
48
  \"meow\"
@@ -69,7 +68,7 @@ fun main() =
69
68
  let once = formatSource(input).expect("first pass");
70
69
  assert!(once.contains("module test"), "got: {once}");
71
70
  assert!(once.contains("import std/os"), "got: {once}");
72
- assert!(once.contains("type Cat"), "got: {once}");
71
+ assert!(once.contains("enum Cat"), "got: {once}");
73
72
  assert!(once.contains("fun speak"), "got: {once}");
74
73
  assert!(once.contains("if x < 0"), "got: {once}");
75
74
  assert!(once.contains("else if x == 0"), "got: {once}");
plum-core/tests/loader_test.rs CHANGED
@@ -114,8 +114,8 @@ fn sameMethodNameOnDifferentReceiversAcrossFilesIsNotACollision() {
114
114
  writeLibFile(&lib_path, "cat", "\
115
115
  module fixtures
116
116
 
117
- type Cat =
117
+ enum Cat =
118
- age: Unit
118
+ | Cat(age: Unit)
119
119
 
120
120
  fun length(self) -> Unit =
121
121
  self.age
@@ -125,8 +125,8 @@ module fixtures
125
125
 
126
126
  import cat
127
127
 
128
- type Box =
128
+ enum Box =
129
- items: Unit
129
+ | Box(items: Unit)
130
130
 
131
131
  fun length(self) -> Unit =
132
132
  self.items
plum-core/tests/parser_test.rs CHANGED
@@ -26,8 +26,8 @@ fn onlyEnum(source: &Source) -> &Enum {
26
26
  #[test]
27
27
  fn nestedClassMethodsBecomeTopLevelFnItemsWithTypeParamSet() {
28
28
  let src = "\
29
- type Cat =
29
+ enum Cat =
30
- name: Str
30
+ | Cat(name: Str)
31
31
 
32
32
  fun getName(self) -> Str =
33
33
  self.name
@@ -36,8 +36,8 @@ type Cat =
36
36
  todo
37
37
  ";
38
38
  let source = parse(src);
39
- assert_eq!(source.items.len(), 3, "class + 2 nested methods, in order");
39
+ assert_eq!(source.items.len(), 3, "class-shaped enum + 2 nested methods, in order");
40
- let Item::Class(class) = &source.items[0] else { panic!("expected a Class item first") };
40
+ let Item::Enum(class) = &source.items[0] else { panic!("expected an Enum item first") };
41
41
  assert_eq!(class.name, "Cat");
42
42
  let Item::Fn(get_name) = &source.items[1] else { panic!("expected getName immediately after the class") };
43
43
  assert_eq!(get_name.name, "getName");
@@ -87,8 +87,8 @@ enum Step(n: Int) =
87
87
  #[test]
88
88
  fn classAndEnumWithNoNestedMethodsProduceNoExtraFnItems() {
89
89
  let src = "\
90
- type Dog =
90
+ enum Dog =
91
- name: Str
91
+ | Dog(name: Str)
92
92
 
93
93
  enum Bool =
94
94
  | True
@@ -96,7 +96,7 @@ enum Bool =
96
96
  ";
97
97
  let source = parse(src);
98
98
  assert_eq!(source.items.len(), 2, "no nested methods means no extra Item::Fn entries");
99
- assert!(matches!(source.items[0], Item::Class(_)));
99
+ assert!(matches!(source.items[0], Item::Enum(_)));
100
100
  assert!(matches!(source.items[1], Item::Enum(_)));
101
101
  }
102
102
 
@@ -144,7 +144,7 @@ fn fnWithoutReceiverButWithReturnTypeHasNoTypeParam() {
144
144
 
145
145
  #[test]
146
146
  fn nestedMethodWithReturnTypeHasCorrectTypeParam() {
147
- let src = "type Cat =\n name: Str\n\n fun toStr() -> Str =\n \"x\"\n";
147
+ let src = "enum Cat =\n | Cat(name: Str)\n\n fun toStr() -> Str =\n \"x\"\n";
148
148
  let source = parse(src);
149
149
  let f = onlyFn(&source);
150
150
  assert_eq!(f.type_param, Some("Cat".to_string()));
plum-examples/closures.plum CHANGED
@@ -30,8 +30,8 @@ fun useFloatClosure() -> Float =
30
30
  offset + v
31
31
  eachF(floatCb)
32
32
 
33
- type ClosureCat =
33
+ enum ClosureCat =
34
- age: Int
34
+ | ClosureCat(age: Int)
35
35
 
36
36
  fun eachCat(cb: fn(ClosureCat) -> Int) -> Int =
37
37
  cb(ClosureCat(age: 7))
plum-examples/functions.plum CHANGED
@@ -43,8 +43,8 @@ fun factorial(x: Int) -> Int =
43
43
  fun double(n: Int) -> Int =
44
44
  n * 2
45
45
 
46
- type GenericBox[T] =
46
+ enum GenericBox[T] =
47
- value: T
47
+ | GenericBox(value: T)
48
48
 
49
49
  fun getIntValue() -> Int =
50
50
  self.value
@@ -56,8 +56,8 @@ fun useGenericBox() -> Int =
56
56
  fun identity(value: T) -> T =
57
57
  value
58
58
 
59
- type GenericBox2[T] =
59
+ enum GenericBox2[T] =
60
- value: T
60
+ | GenericBox2(value: T)
61
61
 
62
62
  fun getValue() -> Int =
63
63
  self.value
@@ -87,8 +87,8 @@ fun unwrapFnOptionStrOr(o: FnOption, default: Int) -> Int =
87
87
  FnSome(v) => 4
88
88
  FnNone => default
89
89
 
90
- type GenericBox3[T] =
90
+ enum GenericBox3[T] =
91
- value: T
91
+ | GenericBox3(value: T)
92
92
 
93
93
  fun getBoxValue() -> Int =
94
94
  self.value
plum-examples/match.plum CHANGED
@@ -138,9 +138,8 @@ fun bothOptions(a: Option, b: Option) -> Int =
138
138
  Some(x), Some(y) => x + y
139
139
  _, _ => 0
140
140
 
141
- type Point =
141
+ enum Point =
142
- x: Int
142
+ | Point(x: Int, y: Int)
143
- y: Int
144
143
 
145
144
  fun sum(self) -> Int =
146
145
  match self
plum-examples/methods.plum CHANGED
@@ -2,9 +2,8 @@ import std/Str
2
2
  import std/Bool
3
3
  import std/Number
4
4
 
5
- type Cat =
5
+ enum Cat =
6
- name: Str
6
+ | Cat(name: Str, age: Int)
7
- age: Int
8
7
 
9
8
  fun getAge(self) -> Int =
10
9
  self.age
@@ -12,9 +11,8 @@ type Cat =
12
11
  fun birthday(self) -> Int =
13
12
  self.age + 1
14
13
 
15
- type Wrapper =
14
+ enum Wrapper =
16
- inner: Cat
15
+ | Wrapper(inner: Cat, tag: Int)
17
- tag: Int
18
16
 
19
17
  fun innerAge(self) -> Int =
20
18
  self.inner.age
plum-examples/oop_visitor.plum CHANGED
@@ -41,10 +41,8 @@ enum Rating =
41
41
  trait Book =
42
42
  collectInterestingInfo() -> Str
43
43
 
44
- type FantasyBook(Book) =
44
+ enum FantasyBook(Book) =
45
- title: Str
46
- pages: Int
47
- hasMythicalCreatures: Bool
45
+ | FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)
48
46
 
49
47
  fun collectInterestingInfo(self) -> Str =
50
48
  if self.hasMythicalCreatures
@@ -52,10 +50,8 @@ type FantasyBook(Book) =
52
50
  else
53
51
  "Fantasy book \"{self.title}\" has no mythical creatures"
54
52
 
55
- type ScifiBook(Book) =
53
+ enum ScifiBook(Book) =
56
- title: Str
54
+ | ScifiBook(title: Str, pages: Int, theme: Str)
57
- pages: Int
58
- theme: Str
59
55
 
60
56
  fun collectInterestingInfo(self) -> Str =
61
57
  if self.theme == "space exploration"
@@ -63,10 +59,8 @@ type ScifiBook(Book) =
63
59
  else
64
60
  "Scifi book \"{self.title}\" has theme {self.theme}"
65
61
 
66
- type ChildrensTaleBook(Book) =
62
+ enum ChildrensTaleBook(Book) =
67
- title: Str
68
- pages: Int
69
- moralLesson: Str
63
+ | ChildrensTaleBook(title: Str, pages: Int, moralLesson: Str)
70
64
 
71
65
  fun collectInterestingInfo(self) -> Str =
72
66
  if self.pages == 0
@@ -74,11 +68,8 @@ type ChildrensTaleBook(Book) =
74
68
  else
75
69
  "Childrens book \"{self.title}\" teaches: {self.moralLesson}"
76
70
 
77
- type NonFictionBook(Book) =
71
+ enum NonFictionBook(Book) =
78
- title: Str
79
- pages: Int
80
- rating1: Rating
72
+ | NonFictionBook(title: Str, pages: Int, rating1: Rating, rating2: Rating)
81
- rating2: Rating
82
73
 
83
74
  fun collectInterestingInfo(self) -> Str =
84
75
  match self.rating1, self.rating2
plum-examples/types.plum CHANGED
@@ -3,18 +3,17 @@ import std/Bool
3
3
  import std/Str
4
4
  import std/Number
5
5
 
6
- type Point =
6
+ enum Point =
7
- x: Int
7
+ | Point(x: Int, y: Int)
8
- y: Int
9
8
 
10
- type Named(ToStr) =
9
+ enum Named(ToStr) =
11
- name: Str
10
+ | Named(name: Str)
12
11
 
13
12
  fun toStr(self) -> Str =
14
13
  self.name
15
14
 
16
- type Box[T] =
15
+ enum Box[T] =
17
- value: T
16
+ | Box(value: T)
18
17
 
19
18
  trait Shape =
20
19
  area() -> Float
@@ -47,32 +46,28 @@ fun makeIntBox() -> Box =
47
46
  fun makeStrBox() -> Box =
48
47
  Box(value: "x")
49
48
 
50
- # ---- class and enum regression tests ----
49
+ # ---- record-shaped and sum-type enum regression tests ----
51
50
 
52
- type Cat =
51
+ enum Cat =
53
- name: Str
52
+ | Cat(name: Str, age: Int)
54
- age: Int
55
53
 
56
54
  fun getAge() -> Int =
57
55
  self.age
58
56
 
59
- type Dog =
57
+ enum Dog =
60
- name: Str
58
+ | Dog(name: Str, age: Int)
61
- age: Int
62
59
 
63
60
  fun getAge(self) -> Int =
64
61
  self.age
65
62
 
66
- type Pair =
63
+ enum Pair =
67
- a: Int
64
+ | Pair(a: Int, b: Int)
68
- b: Int
69
65
 
70
- type Wrapper =
66
+ enum Wrapper =
71
- inner: Pair
67
+ | Wrapper(inner: Pair, tag: Int)
72
- tag: Int
73
68
 
74
- type LoopBox =
69
+ enum LoopBox =
75
- v: Int
70
+ | LoopBox(v: Int)
76
71
 
77
72
  fun sumLoopBoxes() -> Int =
78
73
  total := 0
@@ -121,8 +116,8 @@ enum ShapeWithFields =
121
116
  fun numberKind(n: Number) -> Str =
122
117
  n.kind()
123
118
 
124
- type OptionBox =
119
+ enum OptionBox =
125
- value: Option[Int]
120
+ | OptionBox(value: Option[Int])
126
121
 
127
122
  fun unwrap(default: Int) -> Int =
128
123
  match self.value
plum-std/Array.plum CHANGED
@@ -33,7 +33,9 @@ import std/Number
33
33
  # hood — so `get` on an index that was never `set` traps rather than
34
34
  # returning some default `T` value (there is no way to conjure a default `T`
35
35
  # generically).
36
- type Array[T] =
36
+ enum Array[T] =
37
+ | Array
38
+
37
39
  # A new, empty array.
38
40
  fun init() -> Array[T] =
39
41
  todo
plum-std/Buffer.plum CHANGED
@@ -14,9 +14,8 @@ import std/Number
14
14
  # `bytes.Buffer`'s asymptotic behavior — unlike the previous `Str`-backed
15
15
  # version of this type, which recopied everything written so far on every
16
16
  # write.
17
- type Buffer(ToStr) =
17
+ enum Buffer(ToStr) =
18
- data: []Byte
18
+ | Buffer(data: []Byte, len: Int)
19
- len: Int
20
19
 
21
20
  # A no-param `init` nested in the type is `Buffer()`'s constructor — a
22
21
  # zero-arg `Buffer()` call desugars to `Buffer.init()` (see `plum-checker`'s
plum-std/Byte.plum CHANGED
@@ -9,7 +9,9 @@ import std/Str
9
9
  # recognized conversion, not an ordinary function call, exactly like
10
10
  # `Int(x)`/`Float(x)` (see `plum-checker`/`plum-wasm-codegen`'s special-cased
11
11
  # handling of these three names).
12
- type Byte =
12
+ enum Byte =
13
+ | Byte
14
+
13
15
  fun toInt(self) -> Int =
14
16
  Int(self)
15
17
 
plum-std/ByteSlice.plum CHANGED
@@ -14,7 +14,9 @@ import std/Number
14
14
  # — there's no way to express raw array length/indexing/construction/copying
15
15
  # in Plum source itself; `copyStrToBytes`/`bytesToStr` build on top of them
16
16
  # instead of needing intrinsics of their own.
17
- type ByteSlice =
17
+ enum ByteSlice =
18
+ | ByteSlice
19
+
18
20
  # Number of bytes in the slice.
19
21
  fun length(self) -> Int =
20
22
  todo
plum-std/Http.plum CHANGED
@@ -12,10 +12,8 @@ import std/Number
12
12
 
13
13
  # An HTTP response: status code, response headers, and the raw response
14
14
  # body as a `Str` (a byte array, so binary bodies round-trip intact).
15
- type Response =
15
+ enum Response =
16
- status: Int
17
- headers: Map[Str, Str]
16
+ | Response(status: Int, headers: Map[Str, Str], body: Str)
18
- body: Str
19
17
 
20
18
  # 2xx status codes are success; everything else (redirects, 4xx, 5xx) is
21
19
  # surfaced here rather than as an `Err` from `request` — a non-2xx
plum-std/Json.plum CHANGED
@@ -84,9 +84,8 @@ enum Json =
84
84
  # message — implements the shared `Err` trait (`libs/std/err.plum`) rather
85
85
  # than being a plain `Str`, so a caller that wants the position can get it
86
86
  # without re-parsing the message.
87
- type JsonParseError(Err) =
87
+ enum JsonParseError(Err) =
88
- pos: Int
89
- text: Str
88
+ | JsonParseError(pos: Int, text: Str)
90
89
 
91
90
  fun code(self) -> Int =
92
91
  1
@@ -107,9 +106,8 @@ type JsonParseError(Err) =
107
106
  # but `Readable` is an undefined trait with no methods anywhere in `libs/std`
108
107
  # — there is nothing to call on it — so this reads directly from a `Str`
109
108
  # instead, the same way every other stdlib parser, e.g. `Int.fromStr`, does.)
110
- type JsonParser =
109
+ enum JsonParser =
111
- src: Str
110
+ | JsonParser(src: Str, pos: Int)
112
- pos: Int
113
111
 
114
112
  fun fail(self, text: Str) -> JsonParseError =
115
113
  JsonParseError(pos: self.pos, text: text)
plum-std/List.plum CHANGED
@@ -7,17 +7,13 @@ import std/Bool
7
7
  import std/Str
8
8
 
9
9
  # A node stores the data in a list and contains pointers to the previous and next sibling nodes
10
- type Node[T] =
10
+ enum Node[T] =
11
- value: T
12
- prev: Option[Node[T]]
11
+ | Node(value: T, prev: Option[Node[T]], next: Option[Node[T]])
13
- next: Option[Node[T]]
14
12
 
15
13
  # A list is a data structure describing a contiguous section of an array stored separately from the slice variable itself.
16
14
  # It contains the pointers to the start and end nodes (head, tail) and maintains the size as well
17
- type List[T: ToStr](ToStr) =
15
+ enum List[T: ToStr](ToStr) =
18
- head: Option[Node[T]]
16
+ | List(head: Option[Node[T]], tail: Option[Node[T]], size: Int)
19
- tail: Option[Node[T]]
20
- size: Int
21
17
 
22
18
  # `List[T](1, 2, 3)` (or, for an empty list, `List[T]()` — explicit
23
19
  # generics required either way, since nothing else in a call this shape
plum-std/Map.plum CHANGED
@@ -29,9 +29,8 @@ trait Hashable =
29
29
  # so a `Pair` used as a `List` element always has `K`/`V: ToStr` too,
30
30
  # and `List`'s own eagerly-compiled `join`/`toStr` (which call `.toStr()` on
31
31
  # every element) need this to exist for every `Map` specialization anyway.
32
- type Pair[K, V](ToStr) =
32
+ enum Pair[K, V](ToStr) =
33
- key: K
33
+ | Pair(key: K, val: V)
34
- val: V
35
34
 
36
35
  fun toStr(self) -> Str =
37
36
  return "{self.key.toStr()}: {self.val.toStr()}"
@@ -48,9 +47,8 @@ BUCKET_COUNT = 16
48
47
  # `get`/`set`/`remove` (versus the single, whole-map linear scan a plain
49
48
  # association list costs), degrading to O(n) only if many keys collide into
50
49
  # the same bucket.
51
- type Map[K: Hashable, V] =
50
+ enum Map[K: Hashable, V] =
52
- buckets: Array[List[Pair[K, V]]]
51
+ | Map(buckets: Array[List[Pair[K, V]]], size: Int)
53
- size: Int
54
52
 
55
53
  # `Map[Str, Int]()` — explicit generics required (nothing in a no-arg call
56
54
  # pins down `K`/`V`), builds `BUCKET_COUNT` empty bucket lists up front via
plum-std/Str.plum CHANGED
@@ -26,8 +26,8 @@ trait ToStr =
26
26
  # means constructing a brand new `Str`/`Buffer` pair (`Buffer.write`'s own
27
27
  # callers are always building up a FRESH buffer before wrapping it, never
28
28
  # mutating an already-published `Str`'s backing storage).
29
- type Str(Comparable, ToStr, Readable, Writable) =
29
+ enum Str(Comparable, ToStr, Readable, Writable) =
30
- data: Buffer
30
+ | Str(data: Buffer)
31
31
 
32
32
  # Number of bytes in the string — `self.data` (a `Buffer`) already tracks
33
33
  # this, so unlike `byteAt`/`byteToStr` this needs no compiler intrinsic of
plum-std/Time.plum CHANGED
@@ -7,8 +7,8 @@ import std/Bool
7
7
  extern fun rawNowMillis() -> Int
8
8
 
9
9
  # A single point in time, stored as milliseconds since the Unix epoch.
10
- type Time(ToStr) =
10
+ enum Time(ToStr) =
11
- value: Int
11
+ | Time(value: Int)
12
12
 
13
13
  fun toMillis(self) -> Int =
14
14
  return self.value
@@ -78,10 +78,8 @@ fun now() -> Time =
78
78
  return Time(value: rawNowMillis())
79
79
 
80
80
  # A Gregorian calendar date, decomposed from a day count by `civilFromDays`.
81
- type Civil =
81
+ enum Civil =
82
- year: Int
83
- month: Int
82
+ | Civil(year: Int, month: Int, day: Int)
84
- day: Int
85
83
 
86
84
  MILLIS_PER_DAY = 86400000
87
85
 
@@ -116,8 +114,8 @@ fun floorMod(a: Int, b: Int) -> Int =
116
114
 
117
115
  # A span of time, stored as milliseconds. Negative values are meaningful
118
116
  # (e.g. the result of `Time.since` when `self` is earlier than `other`).
119
- type Duration =
117
+ enum Duration =
120
- value: Int
118
+ | Duration(value: Int)
121
119
 
122
120
  fun millis(n: Int) -> Duration =
123
121
  return Duration(value: n)
plum-std/Uuid.plum CHANGED
@@ -6,8 +6,8 @@ import std/Bool
6
6
 
7
7
  # A UUID, stored as its canonical 36-character
8
8
  # `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` text form.
9
- type Uuid(ToStr) =
9
+ enum Uuid(ToStr) =
10
- value: Str
10
+ | Uuid(value: Str)
11
11
 
12
12
  fun toStr(self) -> Str =
13
13
  return self.value
plum-tooling/tree-sitter-plum/grammar.js CHANGED
@@ -78,7 +78,7 @@ module.exports = grammar({
78
78
  seq(
79
79
  optional($.module),
80
80
  repeat($.import),
81
- repeat(choice($.class, $.trait, $.enum, $.fn, $.const, $.test)),
81
+ repeat(choice($.trait, $.enum, $.fn, $.const, $.test)),
82
82
  ),
83
83
 
84
84
  module: ($) => seq("module", $.mod_identifier),
@@ -119,20 +119,6 @@ module.exports = grammar({
119
119
  ),
120
120
  variadic_type: ($) => seq("...", $.type),
121
121
 
122
- class: ($) =>
123
- seq(
124
- "type",
125
- field("name", $.type_identifier),
126
- field("generics", optional($.generics)),
127
- field("implements", optional(seq("(", commaSep1($.type_identifier), ")"))),
128
- "=",
129
- $._indent,
130
- field("fields", optional(repeat(alias($.class_field, $.field)))),
131
- field("methods", optional(repeat($.fn))),
132
- $._dedent,
133
- ),
134
- class_field: ($) => seq(field("name", $.var_identifier), ":", field("type", $.type)),
135
-
136
122
  trait: ($) =>
137
123
  seq(
138
124
  "trait",
@@ -173,7 +159,16 @@ module.exports = grammar({
173
159
  "enum",
174
160
  field("name", $.type_identifier),
175
161
  field("generics", optional($.generics)),
162
+ // Mutually exclusive with each other (an enum is either a plain sum
163
+ // type possibly claiming trait conformance, OR a discriminant enum
164
+ // with shared per-variant values — never both) — disambiguated by
165
+ // content, not just position: `implements` holds bare type names
166
+ // (`ToStr`), `params` holds `name: Type` pairs (`n: Int`), and
167
+ // `var_identifier`/`type_identifier` are distinct tokens.
168
+ optional(choice(
169
+ field("implements", seq("(", commaSep1($.type_identifier), ")")),
176
- field("params", optional(seq("(", commaSep1($.enum_param), ")"))),
170
+ field("params", seq("(", commaSep1($.enum_param), ")")),
171
+ )),
177
172
  "=",
178
173
  $._indent,
179
174
  optional(repeat(alias($.enum_field, $.field))),
plum-tooling/tree-sitter-plum/queries/plum/format.scm CHANGED
@@ -9,9 +9,6 @@
9
9
  (source
10
10
  (fn) @allow_blank_line_before)
11
11
 
12
- (source
13
- (class) @allow_blank_line_before)
14
-
15
12
  (source
16
13
  (enum) @allow_blank_line_before)
17
14
 
@@ -96,8 +93,6 @@
96
93
 
97
94
  (import "import" @append_space)
98
95
 
99
- (class "type" @append_space)
100
-
101
96
  (trait "trait" @append_space)
102
97
 
103
98
  (enum "enum" @append_space)
@@ -158,35 +153,13 @@
158
153
  ":" @append_space
159
154
 
160
155
  ; ============================================================
161
- ; class/trait/enum bodies — unlike a `fn`'s body, these have no
156
+ ; trait/enum bodies — unlike a `fn`'s body, these have no
162
157
  ; wrapping `(body ...)` node of their own (the grammar puts their
163
- ; `field`/`fn` children directly under `class`/`trait`/`enum`), so
158
+ ; `field`/`fn` children directly under `trait`/`enum`), so
164
159
  ; they need their own hardline + indent rules instead of reusing
165
160
  ; the `(body)` rule below.
166
161
  ; ============================================================
167
162
 
168
- (class
169
- (field) @prepend_hardline)
170
-
171
- (class
172
- (fn) @prepend_hardline)
173
-
174
- ; A blank line between two fields/methods (or a field and a method) inside a
175
- ; `type` body is preserved if the source already had one — same as top-level
176
- ; items — rather than always being collapsed to a single hardline.
177
- (class
178
- (field) @allow_blank_line_before)
179
-
180
- (class
181
- (fn) @allow_blank_line_before)
182
-
183
- (class
184
- "=" @append_indent_start)
185
-
186
- (class
187
- (_) @append_indent_end
188
- .)
189
-
190
163
  (trait
191
164
  (field) @prepend_hardline)
192
165
 
@@ -203,7 +176,9 @@
203
176
  (enum
204
177
  (fn) @prepend_hardline)
205
178
 
179
+ ; A blank line between two fields/methods (or a field and a method) inside an
180
+ ; `enum` body is preserved if the source already had one — same as top-level
206
- ; Same blank-line preservation as `class` above.
181
+ ; items rather than always being collapsed to a single hardline.
207
182
  (enum
208
183
  (field) @allow_blank_line_before)
209
184
 
@@ -218,13 +193,13 @@
218
193
  .)
219
194
 
220
195
  ; `enum_field` (a variant like `| Some[T]`) is ALIASED to the same visible
221
- ; node kind ("field") as class_field/trait_field, but only it has a literal
196
+ ; node kind ("field") as trait_field, but only it has a literal
222
197
  ; "|" — safe to target unscoped by parent.
223
198
  (field
224
199
  "|" @append_space)
225
200
 
226
201
  ; ============================================================
227
- ; match — same situation as class/trait/enum: `case`/`guard_case`
202
+ ; match — same situation as trait/enum: `case`/`guard_case`
228
203
  ; children sit directly under `match`, no wrapping `(body ...)`.
229
204
  ; ============================================================
230
205
 
plum-tooling/tree-sitter-plum/queries/plum/highlights.scm CHANGED
@@ -59,6 +59,7 @@
59
59
  "||"
60
60
  "&&"
61
61
  "..."
62
+ ".."
62
63
  ] @operator
63
64
 
64
65
  [
@@ -71,7 +72,6 @@
71
72
  [
72
73
  "import"
73
74
  "module"
74
- "type"
75
75
  "enum"
76
76
  "trait"
77
77
  "fun"
plum-tooling/tree-sitter-plum/queries/plum/indents.scm CHANGED
@@ -1,7 +1,6 @@
1
1
  [
2
2
  (enum)
3
3
  (trait)
4
- (class)
5
4
  (fn)
6
5
  (for)
7
6
  (while)
@@ -16,7 +15,6 @@
16
15
  (for)
17
16
  (while)
18
17
  (fn)
19
- (class)
20
18
  (enum)
21
19
  (trait)
22
20
  ] @extend
plum-tooling/tree-sitter-plum/queries/plum/tags.scm CHANGED
@@ -1,6 +1,3 @@
1
- (class
2
- name: (type_identifier) @name) @definition.class
3
-
4
1
  (enum
5
2
  name: (type_identifier) @name) @definition.class
6
3
 
plum-tooling/tree-sitter-plum/queries/plum/textobjects.scm CHANGED
@@ -1,9 +1,6 @@
1
1
  (fn
2
2
  body: (_) @function.inside) @function.around
3
3
 
4
- (class
5
- fields: (field) @class.inside) @class.around
6
-
7
4
  (trait
8
5
  fields: (field) @class.inside) @class.around
9
6
 
plum-tooling/tree-sitter-plum/src/grammar.json CHANGED
Binary file
plum-tooling/tree-sitter-plum/src/node-types.json CHANGED
Binary file
plum-tooling/tree-sitter-plum/src/parser.c CHANGED
Binary file
plum-tooling/tree-sitter-plum/test/corpus/type.txt CHANGED
@@ -2,13 +2,11 @@
2
2
  type
3
3
  ================================================================================
4
4
 
5
- type Dog =
5
+ enum Dog =
6
- name: Str
6
+ | Dog(name: Str, age: B)
7
- age: B
8
7
 
9
- type Cat(ToStr) =
8
+ enum Cat(ToStr) =
10
- name: Str
9
+ | Cat(name: Str, age: Int)
11
- age: Int
12
10
 
13
11
  fun init(name: Str) -> Cat =
14
12
  Cat(name: name, age: 0)
@@ -25,27 +23,31 @@ type Cat(ToStr) =
25
23
  --------------------------------------------------------------------------------
26
24
 
27
25
  (source
28
- (class
26
+ (enum
29
27
  (type_identifier)
30
28
  (field
29
+ (type_identifier)
30
+ (enum_named_field
31
- (var_identifier)
31
+ (var_identifier)
32
- (type
32
+ (type
33
- (type_identifier)))
33
+ (type_identifier)))
34
- (field
34
+ (enum_named_field
35
- (var_identifier)
35
+ (var_identifier)
36
- (type
36
+ (type
37
- (generic))))
37
+ (generic)))))
38
- (class
38
+ (enum
39
39
  (type_identifier)
40
40
  (type_identifier)
41
41
  (field
42
+ (type_identifier)
43
+ (enum_named_field
42
- (var_identifier)
44
+ (var_identifier)
43
- (type
45
+ (type
44
- (type_identifier)))
46
+ (type_identifier)))
45
- (field
47
+ (enum_named_field
46
- (var_identifier)
48
+ (var_identifier)
47
- (type
49
+ (type
48
- (type_identifier)))
50
+ (type_identifier))))
49
51
  (fn
50
52
  (fn_identifier)
51
53
  (param
@@ -60,14 +62,16 @@ type Cat(ToStr) =
60
62
  (class_call
61
63
  (type_identifier)
62
64
  (class_argument_list
65
+ (field_argument
63
- (var_identifier)
66
+ (var_identifier)
64
- (expression
67
+ (expression
65
- (primary_expression
68
+ (primary_expression
66
- (var_identifier)))
69
+ (var_identifier))))
70
+ (field_argument
67
- (var_identifier)
71
+ (var_identifier)
68
- (expression
72
+ (expression
69
- (primary_expression
73
+ (primary_expression
70
- (integer)))))))))
74
+ (integer))))))))))
71
75
  (fn
72
76
  (fn_identifier)
73
77
  (param
@@ -82,14 +86,16 @@ type Cat(ToStr) =
82
86
  (class_call
83
87
  (type_identifier)
84
88
  (class_argument_list
89
+ (field_argument
85
- (var_identifier)
90
+ (var_identifier)
86
- (expression
91
+ (expression
87
- (primary_expression
92
+ (primary_expression
88
- (var_identifier)))
93
+ (var_identifier))))
94
+ (field_argument
89
- (var_identifier)
95
+ (var_identifier)
90
- (expression
96
+ (expression
91
- (primary_expression
97
+ (primary_expression
92
- (integer)))))))))
98
+ (integer))))))))))
93
99
  (fn
94
100
  (fn_identifier)
95
101
  (param
@@ -104,16 +110,18 @@ type Cat(ToStr) =
104
110
  (class_call
105
111
  (type_identifier)
106
112
  (class_argument_list
113
+ (field_argument
107
- (var_identifier)
114
+ (var_identifier)
108
- (expression
115
+ (expression
109
- (primary_expression
116
+ (primary_expression
110
- (string
117
+ (string
111
- (string_start)
118
+ (string_start)
112
- (string_end))))
119
+ (string_end)))))
120
+ (field_argument
113
- (var_identifier)
121
+ (var_identifier)
114
- (expression
122
+ (expression
115
- (primary_expression
123
+ (primary_expression
116
- (var_identifier)))))))))
124
+ (var_identifier))))))))))
117
125
  (fn
118
126
  (fn_identifier)
119
127
  (type
@@ -146,8 +154,8 @@ type Cat(ToStr) =
146
154
  type - nested method declaration
147
155
  ================================================================================
148
156
 
149
- type Cat =
157
+ enum Cat =
150
- name: Str
158
+ | Cat(name: Str)
151
159
 
152
160
  fun getName(self) -> Str =
153
161
  self.name
@@ -155,12 +163,14 @@ type Cat =
155
163
  --------------------------------------------------------------------------------
156
164
 
157
165
  (source
158
- (class
166
+ (enum
159
167
  (type_identifier)
160
168
  (field
169
+ (type_identifier)
170
+ (enum_named_field
161
- (var_identifier)
171
+ (var_identifier)
162
- (type
172
+ (type
163
- (type_identifier)))
173
+ (type_identifier))))
164
174
  (fn
165
175
  (fn_identifier)
166
176
  (self)
plum-tooling/tree-sitter-plum/test/highlight/sample.plum CHANGED
@@ -1,8 +1,7 @@
1
1
  module std
2
2
 
3
- type Cat(ToStr) =
3
+ enum Cat(ToStr) =
4
- name: Str
4
+ | Cat(name: Str, age: Int)
5
- age: Int
6
5
 
7
6
  fun withName(name: Str) -> Cat =
8
7
  Cat(name: name, age: self.age)
plum-wasm-codegen/src/lib.rs CHANGED
@@ -7,7 +7,7 @@ use std::cell::{Cell, RefCell};
7
7
  use std::collections::{HashMap, HashSet};
8
8
  use plum_core::ast;
9
9
  use plum_checker::types::{PlumType, TypeEnv, TypeScheme};
10
- use plum_checker::{ClassEnv, MethodEnv, EnumVariants, EnumVariantInfo, EnumParams};
10
+ use plum_checker::{MethodEnv, EnumVariants, EnumVariantInfo, EnumParams};
11
11
 
12
12
  /// One entry in the module's type section. Wasm's type section is a SINGLE shared
13
13
  /// index space for function types AND (once wasm-gc is in play) composite
@@ -328,7 +328,6 @@ type ClosureSigKey = (Vec<ValType>, Option<ValType>);
328
328
  pub struct CompileCtx<'a> {
329
329
  pub func_ids: HashMap<String, u32>,
330
330
  pub func_sigs: HashMap<String, FuncSig>,
331
- pub classes: ClassEnv,
332
331
  pub methods: MethodEnv,
333
332
  pub enum_variants: EnumVariants,
334
333
  pub enum_params: EnumParams,
@@ -358,7 +357,7 @@ pub struct CompileCtx<'a> {
358
357
  /// Shared runtime helper `(n: i64) -> ref Str`: allocates a new `array<i8>`
359
358
  /// holding `n`'s decimal representation, for interpolating an `Int`.
360
359
  pub int_to_string_func: u32,
361
- /// wasm-gc type-section indices for this program's classes/enums/Str/closures —
360
+ /// wasm-gc type-section indices for this program's enums/Str/closures —
362
361
  /// every value's real representation (see `docs/superpowers/plans/2026-07-25-wasm-gc-migration.md`).
363
362
  pub gc_types: GcTypeRegistry,
364
363
  /// Payload-free variant name (True/False/None/...) -> the global index holding
@@ -401,7 +400,6 @@ struct LocalCtx<'a> {
401
400
  string_concat_func: u32,
402
401
  string_eq_func: u32,
403
402
  int_to_string_func: u32,
404
- classes: &'a ClassEnv,
405
403
  methods: &'a MethodEnv,
406
404
  enum_variants: &'a EnumVariants,
407
405
  enum_params: &'a EnumParams,
@@ -506,13 +504,14 @@ fn withGcTypes<R>(f: impl FnOnce(&GcTypeRegistry) -> R) -> R {
506
504
  }
507
505
 
508
506
  /// 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
507
+ /// other spot a pattern's own concrete GC type index is needed) is always an enum
510
- /// variant or a plain class — see `compileClassDestructureArm`. Checks both
508
+ /// variant falls back to `class_type_idx` only for the record-shaped alias case
509
+ /// (see `buildGcTypeRegistry`'s own alias comment), which `variant_type_idx` should
511
- /// registries so a scratch local narrows correctly either way.
510
+ /// already cover directly; kept as a defensive second lookup.
512
511
  fn classOrVariantTypeIdx(r: &GcTypeRegistry, name: &str) -> u32 {
513
512
  *r.variant_type_idx.get(name)
514
513
  .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))
514
+ .unwrap_or_else(|| panic!("internal codegen error: '{}' missing from the GC type registry", name))
516
515
  }
517
516
 
518
517
  /// Resolves an `ast::Type`/`ast::ParamType`'s bare name (e.g. from a function
@@ -677,8 +676,8 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
677
676
  PlumType::TByteSlice => ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(registry.byte_array_type_idx) }),
678
677
  PlumType::TNamed(name) => match registry.class_type_idx.get(name).or_else(|| registry.enum_super_type_idx.get(name)) {
679
678
  Some(idx) => ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(*idx) }),
680
- // A field typed as a generic enum/class (e.g. `Node.next: Option[Node]`)
679
+ // A field typed as a generic enum (e.g. `Node.next: Option[Node]`)
681
- // resolves here to the BARE generic name — `ClassEnv`'s `plumTypeFromAst`
680
+ // resolves here to the BARE generic name — `plumTypeFromAst`
682
681
  // has no representation for type arguments, so it can't know this means
683
682
  // `Option$Int` once monomorphization specializes (and removes the
684
683
  // unspecialized) `Option` — same permissive `anyref` fallback as
@@ -696,24 +695,22 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
696
695
  }
697
696
  }
698
697
 
699
- /// Builds the wasm-gc type registry for every concrete class/enum actually declared
698
+ /// Builds the wasm-gc type registry for every concrete enum actually declared in
700
- /// in `source` (`Bool`/`Str`/etc included, but only if imported — no builtin is
699
+ /// `source` (`Bool`/`Str`/etc included, but only if imported — no builtin is
701
700
  /// hardcoded here), plus the shared raw byte-array type, and declares them all as
702
701
  /// ONE `rec` group via `module.addGcTypes` — a single group sidesteps every ordering
703
- /// question about mutual/self-references (a class field of another class's type, an
702
+ /// question about mutual/self-references (a record-shaped enum's field naming
704
- /// enum variant field referencing its own enum, `Node.next: Option[Node]`, etc.),
703
+ /// another such enum's type, an enum variant field referencing its own enum,
705
- /// since within one `rec` group members may reference each other regardless of
704
+ /// `Node.next: Option[Node]`, etc.), since within one `rec` group members may
706
- /// declaration order.
705
+ /// reference each other regardless of declaration order.
707
706
  fn buildGcTypeRegistry(
708
707
  module: &mut WasmModule,
709
708
  source: &ast::Source,
710
- classes: &ClassEnv,
711
709
  enum_variants: &EnumVariants,
712
710
  enum_params: &EnumParams,
713
711
  ) -> GcTypeRegistry {
714
712
  enum Slot {
715
713
  Str,
716
- Class(String),
717
714
  EnumSuper(String),
718
715
  Variant(String),
719
716
  /// The single shared `array<anyref>` type every `Array[T]`
@@ -729,22 +726,6 @@ fn buildGcTypeRegistry(
729
726
  let mut variant_type_idx: HashMap<String, u32> = HashMap::new();
730
727
  let mut array_ref_slot: Option<u32> = None;
731
728
 
732
- for item in &source.items {
733
- if let ast::Item::Class(c) = item {
734
- if isArraySpecialization(&c.name) {
735
- let idx = *array_ref_slot.get_or_insert_with(|| {
736
- let idx = slots.len() as u32;
737
- slots.push(Slot::ArrayRef);
738
- idx
739
- });
740
- class_type_idx.insert(c.name.clone(), idx);
741
- } else {
742
- class_type_idx.insert(c.name.clone(), slots.len() as u32);
743
- slots.push(Slot::Class(c.name.clone()));
744
- }
745
- }
746
- }
747
-
748
729
  // `Bool` is an ordinary enum now (`type Bool = | True | False` in
749
730
  // `libs/std/Bool.plum`) — registered exactly like any other enum
750
731
  // declared in `source.items`, present only if actually imported.
@@ -755,12 +736,42 @@ fn buildGcTypeRegistry(
755
736
  }
756
737
  }
757
738
  for (enum_name, variant_names) in &enum_decls {
739
+ // `Array`/`Array$T` is a RECORD-shaped enum (single variant sharing the
740
+ // enum's own name — the `type Array[T] = ...` replacement) whose values
741
+ // are ALL represented by the one shared `array<anyref>` type regardless
742
+ // of `T` (see `isArraySpecialization`'s own doc comment) — alias its
743
+ // enum/variant/class names onto that single existing slot instead of
744
+ // giving it (and every OTHER `Array$...` specialization) its own real
745
+ // struct type.
746
+ if isArraySpecialization(enum_name) {
747
+ let idx = *array_ref_slot.get_or_insert_with(|| {
748
+ let idx = slots.len() as u32;
749
+ slots.push(Slot::ArrayRef);
750
+ idx
751
+ });
752
+ enum_super_type_idx.insert(enum_name.clone(), idx);
753
+ class_type_idx.insert(enum_name.clone(), idx);
754
+ for vname in variant_names {
755
+ variant_type_idx.insert(vname.clone(), idx);
756
+ }
757
+ continue;
758
+ }
758
759
  enum_super_type_idx.insert(enum_name.clone(), slots.len() as u32);
759
760
  slots.push(Slot::EnumSuper(enum_name.clone()));
760
761
  for vname in variant_names {
761
762
  variant_type_idx.insert(vname.clone(), slots.len() as u32);
762
763
  slots.push(Slot::Variant(vname.clone()));
763
764
  }
765
+ // A RECORD-shaped enum (the `type X = ...` replacement, e.g. `enum Cat =
766
+ // | Cat(name: Str)`) also gets a `class_type_idx` alias onto its one
767
+ // variant's real struct slot — `plumTypeToValtype`'s `TNamed(name)` arm
768
+ // resolves a value of this type via `class_type_idx` (checked first,
769
+ // falling back to `enum_super_type_idx`), and every OTHER codegen site
770
+ // that already looks up a "class" by this exact name (field access,
771
+ // construction, pattern destructuring, ...) keeps working unchanged.
772
+ if variant_names.len() == 1 && variant_names[0] == *enum_name {
773
+ class_type_idx.insert(enum_name.clone(), variant_type_idx[&variant_names[0]]);
774
+ }
764
775
  }
765
776
 
766
777
  // The registry is fully index-complete after pass 1 (every name has an assigned
@@ -787,18 +798,6 @@ fn buildGcTypeRegistry(
787
798
  shared: false,
788
799
  },
789
800
  },
790
- Slot::Class(name) => {
791
- let fields = classes.get(name).cloned().unwrap_or_default();
792
- let field_types: Vec<FieldType> = fields.iter().map(|(_, ty)| FieldType {
793
- element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
794
- mutable: true,
795
- }).collect();
796
- SubType {
797
- is_final: true,
798
- supertype_idx: None,
799
- composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: field_types.into() }), shared: false },
800
- }
801
- }
802
801
  // An ORDINARY enum's supertype declares zero fields (each variant adds its
803
802
  // own distinct payload fields below it). A DISCRIMINANT enum (`enum
804
803
  // Foo(n: Int) = ...`) is different: every variant shares the EXACT SAME
@@ -903,14 +902,13 @@ fn initCallableWithNoArgs(params: &[PlumType]) -> bool {
903
902
  }
904
903
 
905
904
  fn checkCtxOf<'a>(
906
- ctx_classes: &'a ClassEnv,
907
905
  ctx_methods: &'a MethodEnv,
908
906
  ctx_enum_variants: &'a EnumVariants,
909
907
  ctx_enum_params: &'a EnumParams,
910
908
  ctx_min_required: &'a plum_checker::MinRequiredArgs,
911
909
  ) -> plum_checker::CheckCtx<'a> {
912
910
  plum_checker::CheckCtx {
913
- classes: ctx_classes, methods: ctx_methods, enum_variants: ctx_enum_variants,
911
+ methods: ctx_methods, enum_variants: ctx_enum_variants,
914
912
  enum_params: ctx_enum_params, min_required: ctx_min_required,
915
913
  }
916
914
  }
@@ -921,7 +919,7 @@ fn checkCtxOf<'a>(
921
919
  /// codegen is being driven directly on unchecked input (as some tests do).
922
920
  fn inferLocalType(expr: &ast::Expr, ctx: &LocalCtx) -> PlumType {
923
921
  let env = ctx.type_env.borrow();
924
- let cctx = checkCtxOf(ctx.classes, ctx.methods, ctx.enum_variants, ctx.enum_params, ctx.min_required);
922
+ let cctx = checkCtxOf(ctx.methods, ctx.enum_variants, ctx.enum_params, ctx.min_required);
925
923
  plum_checker::inferExpr(expr, &env, &cctx).unwrap_or(PlumType::TInt)
926
924
  }
927
925
 
@@ -1080,12 +1078,12 @@ fn desugarAssertStmt(stmt: &ast::Stmt) -> ast::Stmt {
1080
1078
 
1081
1079
  fn compileSourceInner(source: &ast::Source, extra_exports: &[(String, String)]) -> Result<Vec<u8>, String> {
1082
1080
  let source = &plum_checker::monomorphize::monomorphizeSource(source)?;
1083
- let (global_env, classes, methods, enum_variants, enum_params) = plum_checker::buildGlobalTables(source);
1081
+ let (global_env, methods, enum_variants, enum_params) = plum_checker::buildGlobalTables(source);
1084
1082
  let min_required = plum_checker::buildMinRequiredArgs(source);
1085
1083
 
1086
1084
  let mut module = WasmModule::new();
1087
1085
 
1088
- let mut gc_types = buildGcTypeRegistry(&mut module, source, &classes, &enum_variants, &enum_params);
1086
+ let mut gc_types = buildGcTypeRegistry(&mut module, source, &enum_variants, &enum_params);
1089
1087
  CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
1090
1088
 
1091
1089
  CURRENT_CONSTS.with(|c| {
@@ -1169,6 +1167,15 @@ fn compileSourceInner(source: &ast::Source, extra_exports: &[(String, String)])
1169
1167
  if !info.field_types.is_empty() {
1170
1168
  continue;
1171
1169
  }
1170
+ // `Array`/`Array$T`'s bare payload-free variant is aliased onto the
1171
+ // ONE shared `array<anyref>` type every specialization reuses (see
1172
+ // `buildGcTypeRegistry`'s `isArraySpecialization` handling), not a
1173
+ // real struct type — it's never actually instantiated this way
1174
+ // (construction goes through the `init`/`push` compiler intrinsics
1175
+ // instead), so it must not get a singleton global here.
1176
+ if isArraySpecialization(name) {
1177
+ continue;
1178
+ }
1172
1179
  let variant_idx = *gc_types.variant_type_idx.get(name)
1173
1180
  .unwrap_or_else(|| panic!("internal codegen error: payload-free variant '{}' missing from GC type registry", name));
1174
1181
  let mut init = Vec::new();
@@ -1258,7 +1265,7 @@ fn compileSourceInner(source: &ast::Source, extra_exports: &[(String, String)])
1258
1265
  }
1259
1266
  let mut walker = ClosureWalker {
1260
1267
  env,
1261
- cctx: checkCtxOf(&classes, &methods, &enum_variants, &enum_params, &min_required),
1268
+ cctx: checkCtxOf(&methods, &enum_variants, &enum_params, &min_required),
1262
1269
  fn_decls: &fn_decls,
1263
1270
  found: Vec::new(),
1264
1271
  locals,
@@ -1400,7 +1407,7 @@ fn compileSourceInner(source: &ast::Source, extra_exports: &[(String, String)])
1400
1407
  let int_to_string_func = registerIntToStringHelper(&mut module, gc_types.byte_array_type_idx);
1401
1408
 
1402
1409
  let ctx = CompileCtx {
1403
- func_ids, func_sigs, classes, methods, enum_variants, enum_params, min_required, global_env,
1410
+ func_ids, func_sigs, methods, enum_variants, enum_params, min_required, global_env,
1404
1411
  closures, closure_asts, closure_call_types, named_fn_values,
1405
1412
  string_concat_func, string_eq_func, int_to_string_func, gc_types, singleton_globals,
1406
1413
  };
@@ -2442,14 +2449,15 @@ fn scanExprForParamTypes(
2442
2449
  }
2443
2450
  ast::Expr::Attribute(a) => {
2444
2451
  // `c.field` on a bare, unresolved param implies `c`'s type is whichever
2445
- // class declares that field name — ambiguous if more than one class has
2452
+ // enum declares a variant with that field name — ambiguous if more than
2446
- // a field by that name, but resolvable in the common case.
2453
+ // one variant has a field by that name, but resolvable in the common case.
2447
2454
  if let ast::AttrKind::Field(field_name) = &a.attr {
2448
2455
  if let ast::Expr::Var(n) = &a.object {
2449
2456
  if params.contains(n) && !resolved.contains_key(n) {
2457
+ let mut matches = cctx.enum_variants.values()
2450
- let mut matches = cctx.classes.iter().filter(|(_, fields)| fields.iter().any(|(fname, _)| fname == field_name));
2458
+ .filter(|info| info.field_names.iter().any(|fname| fname == field_name));
2451
- if let (Some((class_name, _)), None) = (matches.next(), matches.next()) {
2459
+ if let (Some(info), None) = (matches.next(), matches.next()) {
2452
- resolved.insert(n.clone(), PlumType::TNamed(class_name.clone()));
2460
+ resolved.insert(n.clone(), PlumType::TNamed(info.enum_name.clone()));
2453
2461
  }
2454
2462
  }
2455
2463
  }
@@ -2763,11 +2771,6 @@ impl<'a> Collector<'a> {
2763
2771
  for (f, fty) in fields.iter().zip(field_types.iter()) {
2764
2772
  self.collectPattern(f, fty);
2765
2773
  }
2766
- } else if let Some(class_fields) = self.cctx.classes.get(name) {
2767
- let field_types: Vec<PlumType> = class_fields.iter().map(|(_, t)| t.clone()).collect();
2768
- for (f, fty) in fields.iter().zip(field_types.iter()) {
2769
- self.collectPattern(f, fty);
2770
- }
2771
2774
  }
2772
2775
  }
2773
2776
  _ => {}
@@ -2952,7 +2955,7 @@ fn compileFnBody(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Resu
2952
2955
 
2953
2956
  let mut collector = Collector {
2954
2957
  env: base_env.clone(),
2955
- cctx: checkCtxOf(&ctx.classes, &ctx.methods, &ctx.enum_variants, &ctx.enum_params, &ctx.min_required),
2958
+ cctx: checkCtxOf(&ctx.methods, &ctx.enum_variants, &ctx.enum_params, &ctx.min_required),
2956
2959
  named: Vec::new(),
2957
2960
  named_set: Default::default(),
2958
2961
  match_scratch: HashMap::new(),
@@ -3044,7 +3047,6 @@ fn compileFnBody(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Resu
3044
3047
  string_concat_func: ctx.string_concat_func,
3045
3048
  string_eq_func: ctx.string_eq_func,
3046
3049
  int_to_string_func: ctx.int_to_string_func,
3047
- classes: &ctx.classes,
3048
3050
  methods: &ctx.methods,
3049
3051
  enum_variants: &ctx.enum_variants,
3050
3052
  enum_params: &ctx.enum_params,
@@ -3307,47 +3309,36 @@ fn compileStmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
3307
3309
  // struct.set expects [(ref null $t) value] on the stack (ref
3308
3310
  // pushed first/deeper, value second/on top) — same push order
3309
3311
  // this already used for the old memory store. Mirrors the read
3310
- // side's class / discriminant-enum-param / single-owning-variant
3312
+ // side's discriminant-enum-param / single-owning-variant
3311
- // fallback chain (`AttrKind::Field` below) so a named-payload
3313
+ // fallback chain (`AttrKind::Field` below) this also covers a
3312
- // enum variant's own field can be mutated too, not just a class's.
3314
+ // record-shaped enum's own field (the `type X = ...`
3315
+ // replacement), whose one variant is trivially its own unique
3316
+ // owner of every field it declares.
3313
- match ctx.classes.get(&class_name) {
3317
+ match ctx.enum_params.get(&class_name) {
3314
- Some(fields) => {
3318
+ Some(params) => {
3315
- let field_idx = fields
3319
+ let field_idx = params
3316
3320
  .iter()
3317
3321
  .position(|(n, _)| n == field_name)
3318
- .ok_or_else(|| format!("codegen: no field '{}' on class '{}'", field_name, class_name))?;
3322
+ .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
3319
- let class_type_idx = *ctx.gc_types.class_type_idx.get(&class_name)
3323
+ let super_type_idx = *ctx.gc_types.enum_super_type_idx.get(&class_name)
3320
- .ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", class_name))?;
3324
+ .ok_or_else(|| format!("codegen: enum '{}' missing from the GC type registry", class_name))?;
3321
3325
  compileExpr(object, body, ctx, state)?;
3322
3326
  compileExpr(value, body, ctx, state)?;
3323
- Instruction::StructSet { struct_type_index: class_type_idx, field_index: field_idx as u32 }.encode(body);
3327
+ Instruction::StructSet { struct_type_index: super_type_idx, field_index: field_idx as u32 }.encode(body);
3328
+ }
3329
+ None => {
3330
+ let (variant_name, info) = ctx.enum_variants.iter()
3331
+ .find(|(_, info)| info.enum_name == class_name && info.field_names.iter().any(|n| n == field_name))
3332
+ .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
3333
+ let field_idx = info.field_names.iter().position(|n| n == field_name)
3334
+ .expect("just found by this field name");
3335
+ let variant_type_idx = *ctx.gc_types.variant_type_idx.get(variant_name)
3336
+ .ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", variant_name))?;
3337
+ compileExpr(object, body, ctx, state)?;
3338
+ Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx)).encode(body);
3339
+ compileExpr(value, body, ctx, state)?;
3340
+ Instruction::StructSet { struct_type_index: variant_type_idx, field_index: field_idx as u32 }.encode(body);
3324
3341
  }
3325
- None => match ctx.enum_params.get(&class_name) {
3326
- Some(params) => {
3327
- let field_idx = params
3328
- .iter()
3329
- .position(|(n, _)| n == field_name)
3330
- .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
3331
- let super_type_idx = *ctx.gc_types.enum_super_type_idx.get(&class_name)
3332
- .ok_or_else(|| format!("codegen: enum '{}' missing from the GC type registry", class_name))?;
3333
- compileExpr(object, body, ctx, state)?;
3334
- compileExpr(value, body, ctx, state)?;
3335
- Instruction::StructSet { struct_type_index: super_type_idx, field_index: field_idx as u32 }.encode(body);
3336
- }
3337
- None => {
3338
- let (variant_name, info) = ctx.enum_variants.iter()
3339
- .find(|(_, info)| info.enum_name == class_name && info.field_names.iter().any(|n| n == field_name))
3340
- .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
3341
- let field_idx = info.field_names.iter().position(|n| n == field_name)
3342
- .expect("just found by this field name");
3343
- let variant_type_idx = *ctx.gc_types.variant_type_idx.get(variant_name)
3344
- .ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", variant_name))?;
3345
- compileExpr(object, body, ctx, state)?;
3346
- Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx)).encode(body);
3347
- compileExpr(value, body, ctx, state)?;
3348
- Instruction::StructSet { struct_type_index: variant_type_idx, field_index: field_idx as u32 }.encode(body);
3349
- }
3350
- },
3351
3342
  }
3352
3343
  }
3353
3344
  }
@@ -3780,11 +3771,7 @@ fn compileCasePositions(
3780
3771
  ast::CasePattern::String(_) => Err("codegen: string match patterns are not yet supported".to_string()),
3781
3772
  ast::CasePattern::Float(_) => Err("codegen: float match patterns are not yet supported".to_string()),
3782
3773
  ast::CasePattern::Class { name, fields } => {
3783
- if ctx.enum_variants.contains_key(name) {
3784
- compileVariantConstructorArm(pat, name, fields, subject_vt, scratch_local, case, pos, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
3774
+ compileVariantConstructorArm(pat, name, fields, subject_vt, scratch_local, case, pos, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
3785
- } else {
3786
- compileClassDestructureArm(name, fields, subject_vt, scratch_local, case, pos, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
3787
- }
3788
3775
  }
3789
3776
  }
3790
3777
  }
@@ -3891,51 +3878,6 @@ fn compileVariantConstructorArm(
3891
3878
  Ok(())
3892
3879
  }
3893
3880
 
3894
- /// Destructures a PLAIN CLASS constructor pattern (`Point(x, y)` where `Point` is a
3895
- /// `type`, not an enum variant) — the counterpart to `compileVariantConstructorArm`.
3896
- /// A class subject has only "the one shape": there's no discriminant to test, and
3897
- /// the subject's own static type already IS this exact concrete GC struct type (the
3898
- /// checker already verified `name` matches it), so this always matches — just
3899
- /// destructure fields straight off `scratch_local` itself, no `ref.test`/`ref.cast`
3900
- /// narrowing or separate scratch local needed at all.
3901
- #[allow(clippy::too_many_arguments)]
3902
- fn compileClassDestructureArm(
3903
- name: &str,
3904
- fields: &[ast::CasePattern],
3905
- subject_vt: ValType,
3906
- scratch_local: u32,
3907
- case: &ast::Case,
3908
- pos: usize,
3909
- all_subjects: &[(ValType, u32)],
3910
- rest: &[ast::Case],
3911
- result_vt: Option<ValType>,
3912
- exhaustive_fallback: bool,
3913
- body: &mut Vec<u8>,
3914
- ctx: &LocalCtx,
3915
- state: &mut ModuleState,
3916
- ) -> Result<(), String> {
3917
- let class_fields = ctx
3918
- .classes
3919
- .get(name)
3920
- .ok_or_else(|| format!("codegen: unknown constructor '{}' (neither an enum variant nor a class)", name))?;
3921
- if !matches!(subject_vt, ValType::Ref(_)) {
3922
- return Err(format!("codegen: constructor pattern '{}' against a non-class subject", name));
3923
- }
3924
- if fields.len() != class_fields.len() {
3925
- return Err(format!(
3926
- "codegen: constructor pattern '{}' expects {} field(s), got {}",
3927
- name, class_fields.len(), fields.len()
3928
- ));
3929
- }
3930
- let class_type_idx = *ctx.gc_types.class_type_idx.get(name)
3931
- .ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", name))?;
3932
- let field_types: Vec<PlumType> = class_fields.iter().map(|(_, t)| t.clone()).collect();
3933
- compileFieldPatterns(
3934
- fields, &field_types, 0, scratch_local, class_type_idx, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state,
3935
- &mut |body, state| compileCasePositions(case, pos + 1, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state),
3936
- )
3937
- }
3938
-
3939
3881
  /// Checks `fields[fpos..]` (a constructor pattern's own sub-patterns, e.g. the `v` in
3940
3882
  /// `Some(v)`, or — recursively — the `Some(v)` in `Wrap(Some(v))`) against the
3941
3883
  /// already-loaded value in `container_local`, one field at a time. Once every field
@@ -4021,8 +3963,9 @@ fn compileFieldPatterns(
4021
3963
  }
4022
3964
  ast::CasePattern::String(_) => Err("codegen: string match patterns are not yet supported".to_string()),
4023
3965
  ast::CasePattern::Float(_) => Err("codegen: float match patterns are not yet supported".to_string()),
4024
- ast::CasePattern::Class { name, fields: inner_fields } if ctx.enum_variants.contains_key(name) => {
3966
+ ast::CasePattern::Class { name, fields: inner_fields } => {
4025
- let info = ctx.enum_variants.get(name).expect("just checked contains_key");
3967
+ let info = ctx.enum_variants.get(name)
3968
+ .ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?;
4026
3969
  if !matches!(plumTypeToValtype(field_ty), ValType::Ref(_)) {
4027
3970
  return Err(format!("codegen: constructor pattern '{}' against a non-enum field", name));
4028
3971
  }
@@ -4059,42 +4002,6 @@ fn compileFieldPatterns(
4059
4002
  Instruction::End.encode(body);
4060
4003
  Ok(())
4061
4004
  }
4062
- // A plain class nested inside another constructor pattern's fields (see
4063
- // `compileClassDestructureArm`'s doc comment) — there's only ever "the one
4064
- // shape" to match, so no `ref.test`/`ref.cast` narrowing is needed; just
4065
- // load the field into its own local (`compileFieldPatterns` needs a LOCAL
4066
- // to repeatedly `struct.get` against, not a bare stack value) and recurse.
4067
- ast::CasePattern::Class { name, fields: inner_fields } => {
4068
- let class_fields = ctx
4069
- .classes
4070
- .get(name)
4071
- .ok_or_else(|| format!("codegen: unknown constructor '{}' (neither an enum variant nor a class)", name))?;
4072
- if !matches!(plumTypeToValtype(field_ty), ValType::Ref(_)) {
4073
- return Err(format!("codegen: constructor pattern '{}' against a non-class field", name));
4074
- }
4075
- if inner_fields.len() != class_fields.len() {
4076
- return Err(format!(
4077
- "codegen: constructor pattern '{}' expects {} field(s), got {}",
4078
- name, class_fields.len(), inner_fields.len()
4079
- ));
4080
- }
4081
- let inner_class_type_idx = *ctx.gc_types.class_type_idx.get(name)
4082
- .ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", name))?;
4083
- let key = pat as *const ast::CasePattern as usize;
4084
- let slot = *ctx
4085
- .nested_class_scratch
4086
- .get(&key)
4087
- .ok_or_else(|| "internal codegen error: missing nested constructor pattern scratch slot".to_string())?;
4088
- let nested_local = ctx.nested_class_scratch_base + slot;
4089
- let inner_field_types: Vec<PlumType> = class_fields.iter().map(|(_, t)| t.clone()).collect();
4090
-
4091
- Instruction::LocalGet(container_local).encode(body);
4092
- Instruction::StructGet { struct_type_index: container_type_idx, field_index: fpos as u32 }.encode(body);
4093
- Instruction::LocalSet(nested_local).encode(body);
4094
- 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| {
4095
- compileFieldPatterns(fields, field_types, fpos + 1, container_local, container_type_idx, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, on_match)
4096
- })
4097
- }
4098
4005
  }
4099
4006
  }
4100
4007
 
@@ -4307,18 +4214,22 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
4307
4214
  }
4308
4215
  } else if is_closure_call {
4309
4216
  compileClosureCall(call, body, ctx, state)?;
4310
- } else if let Some(info) = ctx.enum_variants.get(&call.name) {
4311
- compileVariantConstruction(info, call, expr, body, ctx, state)?;
4312
- } else if ctx.classes.contains_key(&call.name) && ctx.methods.contains_key(&(call.name.clone(), "init".to_string())) {
4313
- // `Type(1, 2, 3)` — mirrors `plum-checker`'s own `inferExpr` handling
4217
+ // `Type(1, 2, 3)` — mirrors `plum-checker`'s own `inferExpr` handling of
4314
- // of the same case: sugar for `Type.init(1, 2, 3)`, desugared to that
4218
+ // the same case: sugar for `Type.init(1, 2, 3)`, desugared to that
4315
- // equivalent static method-call expression so the existing
4219
+ // equivalent static method-call expression so the existing
4316
- // `AttrKind::Method` codegen (below) handles it without duplication.
4220
+ // `AttrKind::Method` codegen (below) handles it without duplication.
4221
+ // Checked BEFORE the ordinary positional-variant-construction case just
4222
+ // below — see the matching comment in `plum-checker`'s `inferExpr` for
4223
+ // why a record-shaped enum's own name needs this precedence.
4224
+ } else if ctx.enum_variants.get(&call.name).is_some_and(|info| info.enum_name == call.name)
4225
+ && ctx.methods.contains_key(&(call.name.clone(), "init".to_string())) {
4317
4226
  let synthetic = ast::Expr::Attribute(Box::new(ast::AttributeExpr {
4318
4227
  object: ast::Expr::TypeName(call.name.clone()),
4319
4228
  attr: ast::AttrKind::Method(ast::FnCall { name: "init".to_string(), args: call.args.clone() }),
4320
4229
  }));
4321
4230
  compileExpr(&synthetic, body, ctx, state)?;
4231
+ } else if let Some(info) = ctx.enum_variants.get(&call.name) {
4232
+ compileVariantConstruction(info, call, expr, body, ctx, state)?;
4322
4233
  } else {
4323
4234
  fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
4324
4235
  match arg {
@@ -4423,13 +4334,13 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
4423
4334
  // A NAMED-payload enum variant construction (`Circle(radius: 5)`, as
4424
4335
  // opposed to the positional/generic-payload form `Some[T]` — constructed
4425
4336
  // via `Expr::FnCall` instead, elsewhere) — mirrors `plum-checker`'s own
4426
- // `inferClassCallRaw` handling of the same call shape. Same field-pushing
4337
+ // `inferClassCallRaw` handling of the same call shape. Against the
4427
- // shape as the plain-class arm just below, but against the variant's own
4428
- // GC struct type (`gc_types.variant_type_idx`), matched by NAME (recorded
4338
+ // variant's own GC struct type (`gc_types.variant_type_idx`), matched by
4429
- // only for this named-payload form — see `EnumVariantInfo::field_names`)
4339
+ // NAME (recorded only for this named-payload form — see
4430
- // instead of position.
4340
+ // `EnumVariantInfo::field_names`) instead of position. This ALSO covers a
4431
- ast::Expr::ClassCall(call) if !ctx.classes.contains_key(&call.type_name)
4341
+ // record-shaped enum's own construction (the `type X = ...` replacement,
4342
+ // e.g. `Cat(name: "x")`) — its one variant always has named fields too.
4432
- && ctx.enum_variants.get(&call.type_name).is_some_and(|info| !info.field_names.is_empty()) =>
4343
+ ast::Expr::ClassCall(call) if ctx.enum_variants.get(&call.type_name).is_some_and(|info| !info.field_names.is_empty()) =>
4433
4344
  {
4434
4345
  let info = ctx.enum_variants.get(&call.type_name).expect("just checked is_some");
4435
4346
  let variant_idx = *ctx.gc_types.variant_type_idx.get(&call.type_name)
@@ -4458,32 +4369,12 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
4458
4369
  }
4459
4370
  Instruction::StructNew(variant_idx).encode(body);
4460
4371
  }
4372
+ // Every named-payload construction now goes through the variant-construction
4373
+ // arm above (record-shaped or not) — reaching here means `call.type_name`
4374
+ // is genuinely unmodeled, which the checker already permits (`inferExpr`'s
4375
+ // "unmodeled type: allow, codegen will catch" case) — a real compile error.
4461
4376
  ast::Expr::ClassCall(call) => {
4462
- // struct.new needs every field value pushed in DECLARATION order (not
4463
- // `call.fields`'s written order) immediately before the single
4464
- // construction instruction — no intermediate scratch pointer needed at
4465
- // all, unlike the old bump-pointer-then-store approach.
4466
- let fields = ctx
4467
- .classes
4468
- .get(&call.type_name)
4469
- .ok_or_else(|| format!("codegen: unknown class '{}'", call.type_name))?
4377
+ return Err(format!("codegen: unknown class or enum variant '{}'", call.type_name));
4470
- .clone();
4471
- let class_type_idx = *ctx.gc_types.class_type_idx.get(&call.type_name)
4472
- .ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", call.type_name))?;
4473
-
4474
- for (field_idx, (field_name, _)) in fields.iter().enumerate() {
4475
- match call.fields.iter().find(|fa| &fa.name == field_name) {
4476
- Some(fa) => compileExpr(&fa.value, body, ctx, state)?,
4477
- // See the matching comment in the variant-construction arm above.
4478
- None => {
4479
- let spread = call.spread.as_ref()
4480
- .ok_or_else(|| format!("codegen: class '{}' missing field '{}'", call.type_name, field_name))?;
4481
- compileExpr(spread, body, ctx, state)?;
4482
- Instruction::StructGet { struct_type_index: class_type_idx, field_index: field_idx as u32 }.encode(body);
4483
- }
4484
- }
4485
- }
4486
- Instruction::StructNew(class_type_idx).encode(body);
4487
4378
  }
4488
4379
  ast::Expr::Attribute(attr) => {
4489
4380
  let obj_ty = inferLocalType(&attr.object, ctx);
@@ -4497,57 +4388,48 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
4497
4388
  PlumType::TStr => "Str".to_string(),
4498
4389
  other => return Err(format!("codegen: cannot access field '{}' on non-class type {}", field_name, other)),
4499
4390
  };
4391
+ // Fall back to a discriminant enum's shared params, declared
4392
+ // directly on the enum's SUPERTYPE (see `buildGcTypeRegistry`'s
4393
+ // `EnumSuper` arm) — no `ref.cast` to any particular variant
4394
+ // needed, since every variant has the exact same field list as
4395
+ // the supertype itself. This ALSO covers a record-shaped enum's
4396
+ // own field (the `type X = ...` replacement) via the unique-
4397
+ // owning-variant fallback below, since its one variant is
4398
+ // trivially its own unique owner of every field it declares.
4500
- match ctx.classes.get(&class_name) {
4399
+ match ctx.enum_params.get(&class_name) {
4501
- Some(fields) => {
4400
+ Some(params) => {
4502
- let field_idx = fields
4401
+ let field_idx = params
4503
4402
  .iter()
4504
4403
  .position(|(n, _)| n == field_name)
4505
- .ok_or_else(|| format!("codegen: no field '{}' on class '{}'", field_name, class_name))?;
4404
+ .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
4506
- let class_type_idx = *ctx.gc_types.class_type_idx.get(&class_name)
4405
+ let super_type_idx = *ctx.gc_types.enum_super_type_idx.get(&class_name)
4507
- .ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", class_name))?;
4406
+ .ok_or_else(|| format!("codegen: enum '{}' missing from the GC type registry", class_name))?;
4508
4407
  compileExpr(&attr.object, body, ctx, state)?;
4509
- Instruction::StructGet { struct_type_index: class_type_idx, field_index: field_idx as u32 }.encode(body);
4408
+ Instruction::StructGet { struct_type_index: super_type_idx, field_index: field_idx as u32 }.encode(body);
4409
+ }
4410
+ // Not a discriminant enum either: `class_name` is an
4411
+ // ordinary enum, and `plum-checker`'s `inferExpr` already
4412
+ // verified exactly ONE of its variants declares a field
4413
+ // with this name (ambiguity across sibling variants is a
4414
+ // checker error, never reaches codegen). Compile `.field`
4415
+ // as a checked downcast to that one variant's own
4416
+ // concrete struct type (same `ref.cast` idiom `match`
4417
+ // pattern-matching already uses, e.g. `compileCasePositions`),
4418
+ // then read the field off it — traps at runtime if the
4419
+ // value turns out to be a different variant, so no static
4420
+ // proof the value IS that variant is required here.
4421
+ None => {
4422
+ let (variant_name, info) = ctx.enum_variants.iter()
4423
+ .find(|(_, info)| info.enum_name == class_name && info.field_names.iter().any(|n| n == field_name))
4424
+ .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
4425
+ let field_idx = info.field_names.iter().position(|n| n == field_name)
4426
+ .expect("just found by this field name");
4427
+ let variant_type_idx = *ctx.gc_types.variant_type_idx.get(variant_name)
4428
+ .ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", variant_name))?;
4429
+ compileExpr(&attr.object, body, ctx, state)?;
4430
+ Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx)).encode(body);
4431
+ Instruction::StructGet { struct_type_index: variant_type_idx, field_index: field_idx as u32 }.encode(body);
4510
4432
  }
4511
- // Not a class: fall back to a discriminant enum's shared params,
4512
- // declared directly on the enum's SUPERTYPE (see
4513
- // `buildGcTypeRegistry`'s `EnumSuper` arm) — no `ref.cast` to any
4514
- // particular variant needed, since every variant has the exact
4515
- // same field list as the supertype itself.
4516
- None => match ctx.enum_params.get(&class_name) {
4517
- Some(params) => {
4518
- let field_idx = params
4519
- .iter()
4520
- .position(|(n, _)| n == field_name)
4521
- .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
4522
- let super_type_idx = *ctx.gc_types.enum_super_type_idx.get(&class_name)
4523
- .ok_or_else(|| format!("codegen: enum '{}' missing from the GC type registry", class_name))?;
4524
- compileExpr(&attr.object, body, ctx, state)?;
4525
- Instruction::StructGet { struct_type_index: super_type_idx, field_index: field_idx as u32 }.encode(body);
4526
- }
4527
- // Not a discriminant enum either: `class_name` is an
4528
- // ordinary enum, and `plum-checker`'s `inferExpr` already
4529
- // verified exactly ONE of its variants declares a field
4530
- // with this name (ambiguity across sibling variants is a
4531
- // checker error, never reaches codegen). Compile `.field`
4532
- // as a checked downcast to that one variant's own
4533
- // concrete struct type (same `ref.cast` idiom `match`
4534
- // pattern-matching already uses, e.g. `compileCasePositions`),
4535
- // then read the field off it — traps at runtime if the
4536
- // value turns out to be a different variant, so no static
4537
- // proof the value IS that variant is required here.
4538
- None => {
4539
- let (variant_name, info) = ctx.enum_variants.iter()
4540
- .find(|(_, info)| info.enum_name == class_name && info.field_names.iter().any(|n| n == field_name))
4541
- .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
4542
- let field_idx = info.field_names.iter().position(|n| n == field_name)
4543
- .expect("just found by this field name");
4544
- let variant_type_idx = *ctx.gc_types.variant_type_idx.get(variant_name)
4545
- .ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", variant_name))?;
4546
- compileExpr(&attr.object, body, ctx, state)?;
4547
- Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx)).encode(body);
4548
- Instruction::StructGet { struct_type_index: variant_type_idx, field_index: field_idx as u32 }.encode(body);
4549
- }
4550
- },
4551
4433
  }
4552
4434
  }
4553
4435
  ast::AttrKind::Method(call) => {
@@ -4972,7 +4854,7 @@ fn compileClosureBody(
4972
4854
 
4973
4855
  let mut collector = Collector {
4974
4856
  env: base_env.clone(),
4975
- cctx: checkCtxOf(&ctx.classes, &ctx.methods, &ctx.enum_variants, &ctx.enum_params, &ctx.min_required),
4857
+ cctx: checkCtxOf(&ctx.methods, &ctx.enum_variants, &ctx.enum_params, &ctx.min_required),
4976
4858
  named: Vec::new(),
4977
4859
  named_set: Default::default(),
4978
4860
  match_scratch: HashMap::new(),
@@ -5074,7 +4956,6 @@ fn compileClosureBody(
5074
4956
  string_concat_func: ctx.string_concat_func,
5075
4957
  string_eq_func: ctx.string_eq_func,
5076
4958
  int_to_string_func: ctx.int_to_string_func,
5077
- classes: &ctx.classes,
5078
4959
  methods: &ctx.methods,
5079
4960
  enum_variants: &ctx.enum_variants,
5080
4961
  enum_params: &ctx.enum_params,
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -208,9 +208,8 @@ fn floatArithmeticAndNegationCompile() {
208
208
  #[test]
209
209
  fn classFieldAndMethodCompile() {
210
210
  let src = "\
211
- type Cat =
211
+ enum Cat =
212
- name: Str
212
+ | Cat(name: Str, age: Int)
213
- age: Int
214
213
 
215
214
  fun getAge() -> Int =
216
215
  self.age
@@ -225,13 +224,11 @@ fun makeCat() -> Int =
225
224
  #[test]
226
225
  fn nestedClassCallCompiles() {
227
226
  let src = "\
228
- type Pair =
227
+ enum Pair =
229
- a: Int
228
+ | Pair(a: Int, b: Int)
230
- b: Int
231
229
 
232
- type Wrapper =
230
+ enum Wrapper =
233
- inner: Pair
231
+ | Wrapper(inner: Pair, tag: Int)
234
- tag: Int
235
232
 
236
233
  fun make() -> Int =
237
234
  w = Wrapper(inner: Pair(a: 1, b: 2), tag: 9)