plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
2216237
— Peter John
2026-07-20T20:11:29+05:30
feat(plum-checker): support multiple concrete instantiations of the same generic enum
- README.md +1 -1
- plum-checker/src/monomorphize.rs +181 -51
- plum-checker/tests/checker_tests.rs +78 -16
- plum-wasm-codegen/tests/codegen_tests.rs +56 -0
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
|
|
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
|
|
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
|
-
///
|
|
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
|
-
///
|
|
199
|
+
/// type arguments become known) rather than waiting for the worklist to actually
|
|
189
|
-
///
|
|
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
|
-
///
|
|
201
|
+
/// `match` on the same specialization can rewrite variant names consistently,
|
|
192
|
-
///
|
|
202
|
+
/// regardless of processing order.
|
|
193
|
-
|
|
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(
|
|
371
|
+
if let Some(pat) = case.patterns.first_mut() {
|
|
372
|
+
match pat {
|
|
373
|
+
ast::CasePattern::Name(n) => {
|
|
340
|
-
|
|
374
|
+
let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
|
|
341
|
-
|
|
375
|
+
&& self.enum_variants.contains_key(n.as_str());
|
|
342
|
-
|
|
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
|
-
|
|
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 ¶m.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`)
|
|
527
|
+
/// `enum Option = | Some(a) | None`), rewriting `call.name` from the bare
|
|
417
|
-
///
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
724
|
+
// name, or bare-name a generic class/enum — those shapes are out of scope
|
|
576
|
-
//
|
|
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
|
|
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.
|
|
412
|
+
// (not the generic `a`), and must NOT retain the generic `Option` template.
|
|
413
|
-
// variant name
|
|
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
|
|
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
|
|
434
|
+
// program must now type-check correctly for BOTH instantiations — this is the
|
|
434
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
464
|
+
.expect("monomorphize should succeed");
|
|
465
|
+
let has_enum_with_variant = |enum_name: &str, variant_name: &str| {
|
|
460
|
-
|
|
466
|
+
mono.items.iter().any(|it| matches!(it, Item::Enum(e) if e.name == enum_name
|
|
461
|
-
&& e.message.contains("Option")
|
|
462
|
-
&& e.
|
|
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
|
+
}
|