plum

#treesitter#compiler#wasm

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

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


2216237Peter John 2026-07-20T20:11:29+05:30
feat(plum-checker): support multiple concrete instantiations of the same generic enum
README.md CHANGED
@@ -240,7 +240,7 @@ wrap(value: a) -> Bool = # generic param type
240
240
  True
241
241
  ```
242
242
 
243
- Generic **arguments** (instantiating a generic type) accept either bracket or paren syntax: `List[Int]` and `List(Int)` both parse. User-defined generics (classes, their methods, free functions, and enums) are monomorphized: each concrete-type-argument combination actually used in the program gets its own specialized, fully-concrete copy, which then type-checks and compiles to wasm through the normal, unmodified pipeline. See `useWrap`/`usePair` in [`examples/functions.plum`](examples/functions.plum) and `makeIntBox`/`makeStrBox` in [`examples/types.plum`](examples/types.plum) for real instantiation sites. One documented limitation: a generic *enum* may only be instantiated at one concrete type per program (instantiating the same generic enum at two different concrete types produces a clear `monomorphize:`-prefixed error, since the runtime's enum-variant table is keyed by bare variant name).
243
+ Generic **arguments** (instantiating a generic type) accept either bracket or paren syntax: `List[Int]` and `List(Int)` both parse. User-defined generics (classes, their methods, free functions, and enums) are monomorphized: each concrete-type-argument combination actually used in the program gets its own specialized, fully-concrete copy, which then type-checks and compiles to wasm through the normal, unmodified pipeline. See `useWrap`/`usePair` in [`examples/functions.plum`](examples/functions.plum) and `makeIntBox`/`makeStrBox` in [`examples/types.plum`](examples/types.plum) for real instantiation sites. Generic enums support any number of concrete instantiations coexisting in one program (variant names are mangled per instantiation, e.g. `Some` -> `Some$Int`/`Some$Str`, internally — invisible to user code). One narrower residual limitation: a payload-free variant (e.g. `None`) used as a bare value *outside* of a `match` pattern can't be disambiguated between multiple concrete instantiations of its enum from that expression alone; constructing via a payload-carrying sibling (`Some(5)`) and matching (`Some(v) => ...`, `None => ...`) is fully supported and is the overwhelmingly common usage pattern.
244
244
 
245
245
  Full example: [`examples/types.plum`](examples/types.plum), [`examples/functions.plum`](examples/functions.plum).
246
246
 
plum-checker/src/monomorphize.rs CHANGED
@@ -154,11 +154,20 @@ pub fn specialize_fn(f: &ast::Fn, subst: &Substitution, mangled_name: &str, new_
154
154
  /// Produces a concrete, specialized copy of a generic enum under `mangled_name`,
155
155
  /// substituting every variant field type name that matches one of the enum's
156
156
  /// generic parameters with its resolved concrete type's name.
157
+ ///
158
+ /// Variant names are ALSO mangled here, with the same suffix as the enum's own
159
+ /// name (e.g. `Some` -> `Some$Int`) — even a payload-free variant like `None`.
160
+ /// This is necessary because the runtime `EnumVariants` table (built by
161
+ /// `build_global_tables`) is keyed by bare variant name globally: without this,
162
+ /// two specializations of the same generic enum would both register a variant
163
+ /// literally named `Some`, colliding in that flat table.
157
164
  pub fn specialize_enum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -> ast::Enum {
165
+ let params = enum_generic_params(e);
166
+ let type_args: Vec<PlumType> = params.iter().filter_map(|p| subst.get(p).cloned()).collect();
158
167
  ast::Enum {
159
168
  name: mangled_name.to_string(),
160
169
  variants: e.variants.iter().map(|v| ast::EnumVariant {
161
- name: v.name.clone(),
170
+ name: mangle(&v.name, &type_args),
162
171
  fields: v.fields.iter().map(|f| {
163
172
  subst.get(f).map(|t| t.to_string()).unwrap_or_else(|| f.clone())
164
173
  }).collect(),
@@ -168,7 +177,7 @@ pub fn specialize_enum(e: &ast::Enum, subst: &Substitution, mangled_name: &str)
168
177
 
169
178
  use std::collections::BTreeSet;
170
179
  use crate::types::{TypeEnv, TypeScheme};
171
- use crate::{ClassEnv, MethodEnv, EnumVariants, CheckCtx};
180
+ use crate::{ClassEnv, MethodEnv, EnumVariants, EnumVariantInfo, CheckCtx};
172
181
 
173
182
  enum PendingSpecialization<'a> {
174
183
  Class { base: &'a ast::Class, subst: Substitution, mangled: String },
@@ -184,13 +193,26 @@ struct Monomorphizer<'a> {
184
193
  /// by variant name because a construction site (`Some(5)`) parses as a `FnCall`
185
194
  /// whose `name` is the VARIANT, not the enum's own name.
186
195
  enums_generic_by_variant: BTreeMap<String, &'a ast::Enum>,
187
- /// Bare variant name -> the mangled enum name that has currently "claimed" it.
196
+ /// Mangled enum name -> {original variant name -> mangled variant name}, e.g.
197
+ /// `"Option$Int" -> {"Some": "Some$Int", "None": "None$Int"}`. Populated eagerly
198
+ /// (in `resolve_enum_instantiation`, at the moment an instantiation's concrete
188
- /// Because the runtime `EnumVariants` table is keyed by BARE variant name
199
+ /// type arguments become known) rather than waiting for the worklist to actually
189
- /// globally, two specializations of the same generic enum would both try to
200
+ /// produce that specialization so both a construction call site and a later
190
- /// register `"Some"`, silently colliding. We detect that here and error rather
191
- /// than corrupt one specialization (single-instantiation-per-generic-enum is a
201
+ /// `match` on the same specialization can rewrite variant names consistently,
192
- /// documented limitation of this pass).
202
+ /// regardless of processing order.
193
- enum_variant_owner: BTreeMap<String, String>,
203
+ enum_variant_mangling: BTreeMap<String, BTreeMap<String, String>>,
204
+ /// The enum's own bare name -> the generic `Enum` — used to detect a bare
205
+ /// generic-enum-typed function param (e.g. `o: Option`), distinct from
206
+ /// `enums_generic_by_variant` (keyed by VARIANT name, used for construction
207
+ /// sites like `Some(5)`).
208
+ enums_generic_by_name: BTreeMap<String, &'a ast::Enum>,
209
+ /// Free functions that are NOT generic by `fn_generic_params`'s lowercase-letter
210
+ /// convention, but whose param type(s) bare-name a generic class or enum (e.g.
211
+ /// `unwrapOr(o: Option, ...)`) — such a function still needs its own
212
+ /// per-call-site specialization, since its receiver generic class/enum is
213
+ /// dropped from the monomorphized output and the bare name would otherwise
214
+ /// resolve to nothing.
215
+ fns_bare_generic: BTreeMap<String, &'a ast::Fn>,
194
216
  global_env: TypeEnv,
195
217
  classes: ClassEnv,
196
218
  methods: MethodEnv,
@@ -279,6 +301,7 @@ impl<'a> Monomorphizer<'a> {
279
301
  Some(rt) => {
280
302
  is_generic_param_name(&rt.name)
281
303
  || self.classes_generic.contains_key(&rt.name)
304
+ || self.enums_generic_by_name.contains_key(&rt.name)
282
305
  }
283
306
  };
284
307
  if needs {
@@ -334,13 +357,40 @@ impl<'a> Monomorphizer<'a> {
334
357
  self.rewrite_expr(s, env)?;
335
358
  }
336
359
  let subject_ty = m.subjects.first().map(|s| self.infer(s, env)).unwrap_or(PlumType::TInt);
360
+ // If the subject's concrete type is a specialized generic enum, its
361
+ // variant-name mangling table lets us rewrite this match's patterns
362
+ // (`Some`/`None` -> `Some$Int`/`None$Int`) to reference the correct
363
+ // specialization, so the checker/codegen's unmodified, bare-name-keyed
364
+ // `EnumVariants` lookup still resolves each pattern correctly.
365
+ let variant_mangling: Option<BTreeMap<String, String>> = match &subject_ty {
366
+ PlumType::TNamed(n) => self.enum_variant_mangling.get(n).cloned(),
367
+ _ => None,
368
+ };
337
369
  for case in &mut m.cases {
338
370
  let mut case_env = env.clone();
339
- if let Some(ast::CasePattern::Name(n)) = case.patterns.first() {
371
+ if let Some(pat) = case.patterns.first_mut() {
372
+ match pat {
373
+ ast::CasePattern::Name(n) => {
340
- let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
374
+ let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
341
- && self.enum_variants.contains_key(n);
375
+ && self.enum_variants.contains_key(n.as_str());
342
- if !is_variant {
376
+ if is_variant {
377
+ if let Some(table) = &variant_mangling {
378
+ if let Some(mangled_variant) = table.get(n) {
379
+ *n = mangled_variant.clone();
380
+ }
381
+ }
382
+ } else {
343
- case_env.insert(n.clone(), TypeScheme::mono(subject_ty.clone()));
383
+ case_env.insert(n.clone(), TypeScheme::mono(subject_ty.clone()));
384
+ }
385
+ }
386
+ ast::CasePattern::Class { name, .. } => {
387
+ if let Some(table) = &variant_mangling {
388
+ if let Some(mangled_variant) = table.get(name) {
389
+ *name = mangled_variant.clone();
390
+ }
391
+ }
392
+ }
393
+ _ => {}
344
394
  }
345
395
  }
346
396
  self.rewrite_block(&mut case.body, &mut case_env)?;
@@ -412,20 +462,81 @@ impl<'a> Monomorphizer<'a> {
412
462
  Ok(())
413
463
  }
414
464
 
465
+ /// The bare names of any generic class or enum referenced directly (not via a
466
+ /// lowercase-letter generic parameter) in `f`'s param types — e.g. `"Option"` for
467
+ /// `unwrapOr(o: Option, default: Int) -> Int`. See `fns_bare_generic`'s doc
468
+ /// comment for why such a function needs its own specialization.
469
+ fn fn_bare_generic_refs(&self, f: &ast::Fn) -> Vec<String> {
470
+ let mut names: Vec<String> = Vec::new();
471
+ for p in &f.params {
472
+ let n = match &p.ty {
473
+ ast::ParamType::Type(t) => &t.name,
474
+ ast::ParamType::Variadic(t) => &t.name,
475
+ };
476
+ if (self.classes_generic.contains_key(n.as_str()) || self.enums_generic_by_name.contains_key(n.as_str()))
477
+ && !names.iter().any(|x| x == n)
478
+ {
479
+ names.push(n.clone());
480
+ }
481
+ }
482
+ names
483
+ }
484
+
485
+ /// Resolves a call to an otherwise-ordinary function whose param type(s)
486
+ /// bare-name a generic class/enum, specializing it per call site exactly like a
487
+ /// truly-generic function — reusing the same `PendingSpecialization::Fn`
488
+ /// worklist entry and the unmodified `specialize_fn`, whose substitution
489
+ /// mechanism already replaces any type whose bare name matches a substitution
490
+ /// key (it doesn't care whether that key came from a lowercase-letter generic
491
+ /// parameter or a bare generic class/enum reference).
492
+ fn resolve_bare_generic_fn_instantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
493
+ let Some(f) = self.fns_bare_generic.get(call.name.as_str()).copied() else { return Ok(()) };
494
+ let refs = self.fn_bare_generic_refs(f);
495
+ let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
496
+ for (param, arg) in f.params.iter().zip(call.args.iter()) {
497
+ let n = match &param.ty {
498
+ ast::ParamType::Type(t) => t.name.clone(),
499
+ ast::ParamType::Variadic(t) => t.name.clone(),
500
+ };
501
+ if refs.contains(&n) {
502
+ let arg_expr = match arg {
503
+ ast::Arg::Positional(e) => e,
504
+ ast::Arg::Keyword { value, .. } => value,
505
+ ast::Arg::Pair { value, .. } => value,
506
+ };
507
+ bindings.entry(n).or_insert_with(|| self.infer(arg_expr, env));
508
+ }
509
+ }
510
+ if bindings.len() != refs.len() {
511
+ return Err(format!(
512
+ "monomorphize: could not resolve all generic parameters for '{}' at this call site",
513
+ call.name
514
+ ));
515
+ }
516
+ let type_args: Vec<PlumType> = refs.iter().map(|p| bindings[p].clone()).collect();
517
+ let mangled = mangle(&call.name, &type_args);
518
+ if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
519
+ self.enqueued.insert(mangled.clone());
520
+ self.worklist.push(PendingSpecialization::Fn { base: f, subst: Substitution(bindings), mangled: mangled.clone(), new_receiver: None });
521
+ }
522
+ call.name = mangled;
523
+ Ok(())
524
+ }
525
+
415
526
  /// Resolves a construction of a generic enum's variant (e.g. `Some(5)` for
416
- /// `enum Option = | Some(a) | None`). Unlike class/function resolution, this
527
+ /// `enum Option = | Some(a) | None`), rewriting `call.name` from the bare
417
- /// does NOT rewrite `call.name`: the variant name (`Some`) must stay exactly as
528
+ /// variant name (`Some`) to its mangled form (`Some$Int`) once the enum's own
418
- /// declared — only the ENUM's own name is mangled (`Option$Int`), and the
419
- /// specialized `ast::Enum` keeps its variants named `Some`/`None`. We only need
420
- /// to enqueue the enum's specialization; the checker/codegen's `EnumVariants`
421
- /// lookup (keyed by bare variant name) resolves `Some` correctly once the
422
- /// concrete `Option$Int` is the only thing left in the output.
529
+ /// concrete instantiation is known. Mangling is eager and deterministic — it
530
+ /// doesn't wait for the worklist to actually produce the specialized `ast::Enum`
531
+ /// (see `enum_variant_mangling`'s doc comment).
423
532
  ///
424
533
  /// A variant that carries no generic fields (e.g. `None`) can't pin down the
425
534
  /// enum's type parameters on its own, so such a construction site is left alone
426
535
  /// here — some other construction site (e.g. `Some(5)`) is what drives the
427
- /// specialization, and the bare `None` needs no rewriting either way.
536
+ /// specialization. (A bare `None` used as a *value*, not a call, is
537
+ /// `ast::Expr::TypeName` and doesn't go through this function at all — see the
538
+ /// plan's Global Constraints for that narrower, documented residual limitation.)
428
- fn resolve_enum_instantiation(&mut self, call: &ast::FnCall, env: &TypeEnv) -> Result<(), String> {
539
+ fn resolve_enum_instantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
429
540
  let Some(e) = self.enums_generic_by_variant.get(call.name.as_str()).copied() else { return Ok(()) };
430
541
  let params = enum_generic_params(e);
431
542
  let Some(variant) = e.variants.iter().find(|v| v.name == call.name) else { return Ok(()) };
@@ -448,11 +559,41 @@ impl<'a> Monomorphizer<'a> {
448
559
  }
449
560
  let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
450
561
  let mangled = mangle(&e.name, &type_args);
562
+
563
+ // Populate the mangling table (and, crucially, teach `self.enum_variants`
564
+ // about the new mangled variant names too) the first time this exact
565
+ // specialization is seen. `self.enum_variants` was built once, up front,
566
+ // from the ORIGINAL source and only knows bare variant names — without
567
+ // this, `self.infer` on a rewritten construction site (which now names
568
+ // the MANGLED variant) would fail to resolve it via `enum_variants` and
569
+ // fall back to an uninformative `TVar`, which would in turn make a later
570
+ // `match` on that same value unable to tell which specialization it's
571
+ // matching and leave its patterns un-rewritten (a real bug: the produced
572
+ // enum's variants are mangled but its match patterns wouldn't be).
573
+ if !self.enum_variant_mangling.contains_key(&mangled) {
574
+ let mut table = BTreeMap::new();
575
+ for (tag, v) in e.variants.iter().enumerate() {
576
+ let mangled_variant = mangle(&v.name, &type_args);
577
+ table.insert(v.name.clone(), mangled_variant.clone());
578
+ let field_types: Vec<PlumType> = v.fields.iter().map(|f| {
579
+ bindings.get(f).cloned().unwrap_or_else(|| {
580
+ crate::plum_type_from_ast(&ast::Type { name: f.clone(), generics: vec![] })
581
+ })
582
+ }).collect();
583
+ self.enum_variants.insert(mangled_variant, EnumVariantInfo {
584
+ enum_name: mangled.clone(),
585
+ tag: tag as i32,
586
+ field_types,
587
+ });
588
+ }
589
+ self.enum_variant_mangling.insert(mangled.clone(), table);
590
+ }
591
+
451
592
  if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
452
593
  self.enqueued.insert(mangled.clone());
453
- self.worklist.push(PendingSpecialization::Enum { base: e, subst: Substitution(bindings), mangled });
594
+ self.worklist.push(PendingSpecialization::Enum { base: e, subst: Substitution(bindings), mangled: mangled.clone() });
454
595
  }
455
- // Intentionally NOT rewriting `call.name` see doc comment.
596
+ call.name = self.enum_variant_mangling[&mangled][&variant.name].clone();
456
597
  Ok(())
457
598
  }
458
599
 
@@ -475,10 +616,13 @@ impl<'a> Monomorphizer<'a> {
475
616
  }
476
617
  // A `FnCall` may name either a generic free function or a generic
477
618
  // enum's variant; the two name spaces don't overlap (variants are
478
- // capitalized), so checking both is safe. Enum resolution never
619
+ // capitalized). Enum resolution runs first and rewrites `call.name`
620
+ // to its mangled form when it resolves — `fns_generic` is keyed by
621
+ // the ORIGINAL unmangled free-function names, so a rewritten variant
479
- // rewrites `call.name`, so order doesn't matter.
622
+ // name can never accidentally match it afterward.
480
623
  self.resolve_enum_instantiation(call, env)?;
481
624
  self.resolve_fn_instantiation(call, env)?;
625
+ self.resolve_bare_generic_fn_instantiation(call, env)?;
482
626
  }
483
627
  ast::Expr::Attribute(attr) => {
484
628
  self.rewrite_expr(&mut attr.object, env)?;
@@ -540,7 +684,9 @@ pub fn monomorphize_source(source: &ast::Source) -> Result<ast::Source, String>
540
684
  fns_generic: BTreeMap::new(),
541
685
  methods_generic_on: BTreeMap::new(),
542
686
  enums_generic_by_variant: BTreeMap::new(),
687
+ enums_generic_by_name: BTreeMap::new(),
543
- enum_variant_owner: BTreeMap::new(),
688
+ enum_variant_mangling: BTreeMap::new(),
689
+ fns_bare_generic: BTreeMap::new(),
544
690
  global_env,
545
691
  classes,
546
692
  methods,
@@ -555,6 +701,7 @@ pub fn monomorphize_source(source: &ast::Source) -> Result<ast::Source, String>
555
701
  match item {
556
702
  ast::Item::Class(c) if !c.generics.is_empty() => { m.classes_generic.insert(c.name.clone(), c); }
557
703
  ast::Item::Enum(e) if !enum_generic_params(e).is_empty() => {
704
+ m.enums_generic_by_name.insert(e.name.clone(), e);
558
705
  for v in &e.variants {
559
706
  m.enums_generic_by_variant.insert(v.name.clone(), e);
560
707
  }
@@ -569,11 +716,13 @@ pub fn monomorphize_source(source: &ast::Source) -> Result<ast::Source, String>
569
716
  m.methods_generic_on.entry(f.type_param.clone().unwrap()).or_default().push(f);
570
717
  } else if f.type_param.is_none() && !fn_generic_params(f).is_empty() {
571
718
  m.fns_generic.insert(f.name.clone(), f);
719
+ } else if f.type_param.is_none() && !m.fn_bare_generic_refs(f).is_empty() {
720
+ m.fns_bare_generic.insert(f.name.clone(), f);
572
721
  }
573
722
  // A method whose receiver is NOT generic is left as a regular method below,
574
723
  // even if its own params/return happen to use a bare lowercase-letter type
575
- // name that shape (a method introducing its own extra generic parameter)
724
+ // name, or bare-name a generic class/enum those shapes are out of scope
576
- // is out of scope for this pass; see the plan's Global Constraints.
725
+ // for this pass; see the plan's Global Constraints.
577
726
  }
578
727
  }
579
728
 
@@ -586,7 +735,8 @@ pub fn monomorphize_source(source: &ast::Source) -> Result<ast::Source, String>
586
735
  ast::Item::Fn(f) => {
587
736
  let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
588
737
  let is_generic_fn = f.type_param.is_none() && !fn_generic_params(f).is_empty();
738
+ let is_bare_generic_fn = f.type_param.is_none() && m.fns_bare_generic.contains_key(f.name.as_str());
589
- if !receiver_is_generic && !is_generic_fn {
739
+ if !receiver_is_generic && !is_generic_fn && !is_bare_generic_fn {
590
740
  let mut f2 = f.clone();
591
741
  m.rewrite_fn_body(&mut f2, false)?;
592
742
  m.produced.push(ast::Item::Fn(f2));
@@ -653,26 +803,6 @@ pub fn monomorphize_source(source: &ast::Source) -> Result<ast::Source, String>
653
803
  PendingSpecialization::Enum { base, subst, mangled } => {
654
804
  if !m.specialized.insert(mangled.clone()) { continue; }
655
805
  let spec_enum = specialize_enum(base, &subst, &mangled);
656
- // Claim each bare variant name for this mangled enum. If a DIFFERENT
657
- // mangled enum already owns it, this generic enum is being
658
- // instantiated at more than one concrete type in the same program —
659
- // which the flat, bare-variant-name-keyed `EnumVariants` runtime
660
- // table can't represent (both would register under `"Some"`). Rather
661
- // than silently let the second specialization corrupt the first, we
662
- // fail with a clear, specific error. (Re-claiming by the SAME mangled
663
- // enum can't reach here — worklist dedup + the `specialized` guard
664
- // above ensure each mangled enum is produced exactly once.)
665
- for v in &spec_enum.variants {
666
- if let Some(owner) = m.enum_variant_owner.get(&v.name) {
667
- if owner != &mangled {
668
- return Err(format!(
669
- "monomorphize: generic enum '{}' is instantiated at more than one concrete type in the same program ('{}' and '{}'), which is not yet supported. Only a single concrete instantiation per generic enum is allowed (variant '{}' would collide in the global variant table). This is a known, documented limitation, not a bug.",
670
- base.name, owner, mangled, v.name
671
- ));
672
- }
673
- }
674
- m.enum_variant_owner.insert(v.name.clone(), mangled.clone());
675
- }
676
806
  m.produced.push(ast::Item::Enum(spec_enum));
677
807
  }
678
808
  }
plum-checker/tests/checker_tests.rs CHANGED
@@ -408,9 +408,10 @@ get() -> Int =
408
408
  assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
409
409
 
410
410
  // Directly prove resolution happened: the monomorphized output must contain a
411
- // concrete `Option$Int` enum whose `Some` variant carries an `Int` field (not
411
+ // concrete `Option$Int` enum whose `Some$Int` variant carries an `Int` field
412
- // the generic `a`), and must NOT retain the generic `Option` template. The
412
+ // (not the generic `a`), and must NOT retain the generic `Option` template.
413
- // variant name itself stays `Some` (only the enum's own name is mangled).
413
+ // The variant name is ALSO mangled (`Some` -> `Some$Int`), the same suffix as
414
+ // the enum's own name.
414
415
  let mono = plum_checker::monomorphize::monomorphize_source(&source)
415
416
  .expect("monomorphize should succeed");
416
417
  let opt = mono.items.iter().find_map(|it| match it {
@@ -418,8 +419,8 @@ get() -> Int =
418
419
  _ => None,
419
420
  });
420
421
  let opt = opt.expect("expected a specialized `Option$Int` enum in the output");
421
- let some = opt.variants.iter().find(|v| v.name == "Some")
422
+ let some = opt.variants.iter().find(|v| v.name == "Some$Int")
422
- .expect("expected `Some` variant on `Option$Int`");
423
+ .expect("expected `Some$Int` (mangled) variant on `Option$Int`");
423
424
  assert_eq!(some.fields, vec!["Int".to_string()], "Some's field should be concrete Int");
424
425
  assert!(
425
426
  !mono.items.iter().any(|it| matches!(it, Item::Enum(e) if e.name == "Option")),
@@ -428,10 +429,10 @@ get() -> Int =
428
429
  }
429
430
 
430
431
  #[test]
431
- fn generic_enum_multi_instantiation_is_a_clear_error() {
432
+ fn generic_enum_multiple_instantiations_coexist_and_type_check() {
432
433
  // The SAME generic enum instantiated at two different concrete types in one
433
- // program is a known, documented limitation it must fail with a clear
434
+ // program must now type-check correctly for BOTH instantiations this is the
434
- // monomorphize error rather than silently corrupting one specialization.
435
+ // behavior this task adds (previously this was a documented, rejected limitation).
435
436
  let src = "\
436
437
  enum Option =
437
438
  | Some(a)
@@ -455,14 +456,18 @@ useStr() -> Str =
455
456
  ";
456
457
  let source = parse(src);
457
458
  let result = check_source(&source);
458
- let errs = result.err().expect("expected a monomorphize collision error, got Ok");
459
+ assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
460
+
461
+ // Directly prove both specializations exist independently, with distinct
462
+ // mangled variant names, so neither collides with the other.
463
+ let mono = plum_checker::monomorphize::monomorphize_source(&source)
459
- assert!(
464
+ .expect("monomorphize should succeed");
465
+ let has_enum_with_variant = |enum_name: &str, variant_name: &str| {
460
- errs.iter().any(|e| e.message.contains("monomorphize")
466
+ mono.items.iter().any(|it| matches!(it, Item::Enum(e) if e.name == enum_name
461
- && e.message.contains("Option")
462
- && e.message.contains("more than one concrete type")),
467
+ && e.variants.iter().any(|v| v.name == variant_name)))
463
- "expected a clear monomorphize multi-instantiation error, got {:?}",
464
- errs
465
- );
468
+ };
469
+ assert!(has_enum_with_variant("Option$Int", "Some$Int"), "expected Option$Int with Some$Int");
470
+ assert!(has_enum_with_variant("Option$Str", "Some$Str"), "expected Option$Str with Some$Str");
466
471
  }
467
472
 
468
473
  #[test]
@@ -510,3 +515,60 @@ use() -> Int =
510
515
  let errs = result.unwrap_err();
511
516
  assert!(errs[0].message.contains("monomorphize"), "got: {:?}", errs);
512
517
  }
518
+
519
+
520
+
521
+
522
+ #[test]
523
+ fn ordinary_function_with_bare_generic_enum_param_type_checks() {
524
+ // The shape that broke the pre-existing codegen test: an otherwise-ordinary
525
+ // function taking a bare generic-enum-typed parameter.
526
+ let src = "\
527
+ enum Option =
528
+ | Some(a)
529
+ | None
530
+
531
+ unwrapOr(o: Option, default: Int) -> Int =
532
+ match o
533
+ Some(v) =>
534
+ v
535
+ None =>
536
+ default
537
+
538
+ use() -> Int =
539
+ unwrapOr(Some(5), 0)
540
+ ";
541
+ let source = parse(src);
542
+ let result = check_source(&source);
543
+ assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
544
+
545
+ // Directly prove `unwrapOr` itself got specialized (not left bare/unresolved).
546
+ let mono = plum_checker::monomorphize::monomorphize_source(&source)
547
+ .expect("monomorphize should succeed");
548
+ let has_specialized_unwrap_or = mono.items.iter().any(|it| matches!(it, Item::Fn(f)
549
+ if f.name.starts_with("unwrapOr$") && f.type_param.is_none()));
550
+ assert!(has_specialized_unwrap_or, "expected a specialized `unwrapOr$...` function in the output");
551
+ }
552
+
553
+ #[test]
554
+ fn ordinary_function_with_bare_generic_class_param_type_checks() {
555
+ // The same shape, for a generic CLASS param instead of an enum — untested until
556
+ // now, but the identical root cause: `Box` is dropped from the monomorphized
557
+ // output, so a bare `Box`-typed param would otherwise reference nothing.
558
+ let src = "\
559
+ type Box(a) =
560
+ value: a
561
+
562
+ getBoxValue<Box>() -> a =
563
+ self.value
564
+
565
+ sumBox(b: Box) -> Int =
566
+ b.getBoxValue()
567
+
568
+ use() -> Int =
569
+ sumBox(Box(value: 5))
570
+ ";
571
+ let source = parse(src);
572
+ let result = check_source(&source);
573
+ assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
574
+ }
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -776,3 +776,59 @@ main() -> Int =
776
776
  let bytes = compile_source(&source).expect("compile failed");
777
777
  assert_eq!(run_main(&bytes), 13);
778
778
  }
779
+
780
+ #[test]
781
+ fn generic_enum_multiple_instantiations_coexist_and_run_correctly() {
782
+ // `Str.length()` is not a real, working method in this codebase (no built-in
783
+ // Str methods exist in codegen, and string-literal match patterns are an
784
+ // explicit, documented "not yet supported" error — see
785
+ // `match_string_pattern_is_a_clear_error` above). So the `Some(v) => ...` arm
786
+ // for the Str instantiation returns a fixed literal instead of deriving
787
+ // anything from `v`'s content; the point of this test is that `Option$Str`
788
+ // coexists with `Option$Int` and both run correctly, not string processing.
789
+ let src = "\
790
+ enum Option =
791
+ | Some(a)
792
+ | None
793
+
794
+ unwrapIntOr(o: Option, default: Int) -> Int =
795
+ match o
796
+ Some(v) =>
797
+ v
798
+ None =>
799
+ default
800
+
801
+ unwrapStrOr(o: Option, default: Int) -> Int =
802
+ match o
803
+ Some(v) =>
804
+ 4
805
+ None =>
806
+ default
807
+
808
+ main() -> Int =
809
+ unwrapIntOr(Some(13), 0) + unwrapStrOr(Some(\"abcd\"), 0)
810
+ ";
811
+ let source = parse(src);
812
+ let bytes = compile_source(&source).expect("compile failed");
813
+ assert_eq!(run_main(&bytes), 17);
814
+ }
815
+
816
+ #[test]
817
+ fn ordinary_function_with_bare_generic_class_param_runs_correctly() {
818
+ let src = "\
819
+ type Box(a) =
820
+ value: a
821
+
822
+ getBoxValue<Box>() -> Int =
823
+ self.value
824
+
825
+ sumBox(b: Box) -> Int =
826
+ b.getBoxValue()
827
+
828
+ main() -> Int =
829
+ sumBox(Box(value: 11))
830
+ ";
831
+ let source = parse(src);
832
+ let bytes = compile_source(&source).expect("compile failed");
833
+ assert_eq!(run_main(&bytes), 11);
834
+ }