plum

#treesitter#compiler#wasm

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

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


22140cfPeter John 2026-07-20T15:43:06+05:30
feat(plum-checker): monomorphize generic classes/functions/methods/enums before checking and codegen
plum-checker/src/lib.rs CHANGED
@@ -138,8 +138,9 @@ pub fn build_global_tables(source: &ast::Source) -> (TypeEnv, ClassEnv, MethodEn
138
138
  }
139
139
 
140
140
  pub fn check_source(source: &ast::Source) -> CheckResult<()> {
141
+ let source = monomorphize::monomorphize_source(source).map_err(|e| vec![CheckError { message: e }])?;
141
142
  let mut errors: Vec<CheckError> = Vec::new();
142
- let (global_env, classes, methods, enum_variants) = build_global_tables(source);
143
+ let (global_env, classes, methods, enum_variants) = build_global_tables(&source);
143
144
  let ctx = CheckCtx { classes: &classes, methods: &methods, enum_variants: &enum_variants };
144
145
 
145
146
  // A name that is both a class and an enum variant is ambiguous: `Name(...)`
plum-checker/src/monomorphize.rs CHANGED
@@ -165,3 +165,413 @@ pub fn specialize_enum(e: &ast::Enum, subst: &Substitution, mangled_name: &str)
165
165
  }).collect(),
166
166
  }
167
167
  }
168
+
169
+ use std::collections::BTreeSet;
170
+ use crate::types::{TypeEnv, TypeScheme};
171
+ use crate::{ClassEnv, MethodEnv, EnumVariants, CheckCtx};
172
+
173
+ enum PendingSpecialization<'a> {
174
+ Class { base: &'a ast::Class, subst: Substitution, mangled: String },
175
+ Fn { base: &'a ast::Fn, subst: Substitution, mangled: String, new_receiver: Option<String> },
176
+ #[allow(dead_code)]
177
+ Enum { base: &'a ast::Enum, subst: Substitution, mangled: String },
178
+ }
179
+
180
+ struct Monomorphizer<'a> {
181
+ classes_generic: BTreeMap<String, &'a ast::Class>,
182
+ fns_generic: BTreeMap<String, &'a ast::Fn>,
183
+ methods_generic_on: BTreeMap<String, Vec<&'a ast::Fn>>,
184
+ global_env: TypeEnv,
185
+ classes: ClassEnv,
186
+ methods: MethodEnv,
187
+ enum_variants: EnumVariants,
188
+ specialized: BTreeSet<String>,
189
+ enqueued: BTreeSet<String>,
190
+ worklist: Vec<PendingSpecialization<'a>>,
191
+ produced: Vec<ast::Item>,
192
+ }
193
+
194
+ impl<'a> Monomorphizer<'a> {
195
+ fn infer(&self, e: &ast::Expr, env: &TypeEnv) -> PlumType {
196
+ let ctx = CheckCtx { classes: &self.classes, methods: &self.methods, enum_variants: &self.enum_variants };
197
+ crate::infer_expr(e, env, &ctx).unwrap_or(PlumType::TVar("_".to_string()))
198
+ }
199
+
200
+ /// Rewrites a function/method body's generic call sites. When `resolve_return`
201
+ /// is set (for freshly-generated specializations, whose declared return may be
202
+ /// a generic parameter like `a` — which the current grammar can't even parse,
203
+ /// leaving `returns: None` — or a generic class), the declared return type is
204
+ /// re-derived from the concrete inferred type of the body's tail expression.
205
+ /// For an ordinary (non-generic) top-level function we only rewrite the return
206
+ /// annotation when it names a generic class used bare (e.g. `-> Box`), which
207
+ /// the body's construction site has just been specialized to a mangled name.
208
+ fn rewrite_fn_body(&mut self, f: &mut ast::Fn, resolve_return: bool) -> Result<(), String> {
209
+ let mut env = self.global_env.clone();
210
+ if let Some(recv) = &f.type_param {
211
+ env.insert("self".to_string(), TypeScheme::mono(PlumType::TNamed(recv.clone())));
212
+ }
213
+ for p in &f.params {
214
+ let ty = match &p.ty {
215
+ ast::ParamType::Type(t) => crate::plum_type_from_ast(t),
216
+ ast::ParamType::Variadic(t) => crate::plum_type_from_ast(t),
217
+ };
218
+ env.insert(p.name.clone(), TypeScheme::mono(ty));
219
+ }
220
+ let tail: Option<PlumType> = match &mut f.body {
221
+ ast::FnBody::Expr(e) => {
222
+ self.rewrite_expr(e, &env)?;
223
+ Some(self.infer(e, &env))
224
+ }
225
+ ast::FnBody::Block(block) => {
226
+ self.rewrite_block(block, &mut env)?;
227
+ match block.stmts.last() {
228
+ Some(ast::Stmt::Expr(e)) => Some(self.infer(e, &env)),
229
+ Some(ast::Stmt::Return(Some(e))) => Some(self.infer(e, &env)),
230
+ _ => None,
231
+ }
232
+ }
233
+ };
234
+ if let Some(t) = tail {
235
+ self.maybe_rewrite_return(f, &t, resolve_return);
236
+ }
237
+ Ok(())
238
+ }
239
+
240
+ /// Overwrites `f.returns` with a concrete type derived from the body's tail
241
+ /// type `t` when the currently-declared return type is generic/unresolved.
242
+ /// Never clobbers a genuine concrete annotation on an ordinary function, and
243
+ /// never fabricates a return type from an un-inferrable (`TVar`) tail.
244
+ fn maybe_rewrite_return(&self, f: &mut ast::Fn, t: &PlumType, resolve_return: bool) {
245
+ if matches!(t, PlumType::TVar(_) | PlumType::TFun(_, _)) {
246
+ return;
247
+ }
248
+ let needs = match &f.returns {
249
+ None => resolve_return,
250
+ Some(rt) => {
251
+ resolve_return
252
+ || is_generic_param_name(&rt.name)
253
+ || self.classes_generic.contains_key(&rt.name)
254
+ }
255
+ };
256
+ if needs {
257
+ f.returns = Some(ast::ReturnType { name: t.to_string(), generics: vec![] });
258
+ }
259
+ }
260
+
261
+ fn rewrite_block(&mut self, block: &mut ast::Block, env: &mut TypeEnv) -> Result<(), String> {
262
+ for stmt in &mut block.stmts {
263
+ self.rewrite_stmt(stmt, env)?;
264
+ }
265
+ Ok(())
266
+ }
267
+
268
+ fn rewrite_stmt(&mut self, stmt: &mut ast::Stmt, env: &mut TypeEnv) -> Result<(), String> {
269
+ match stmt {
270
+ ast::Stmt::Assign(a) => {
271
+ for (target, value) in a.targets.iter().zip(a.values.iter_mut()) {
272
+ self.rewrite_expr(value, env)?;
273
+ let ty = self.infer(value, env);
274
+ env.insert(target.clone(), TypeScheme::mono(ty));
275
+ }
276
+ }
277
+ ast::Stmt::Return(Some(e)) => self.rewrite_expr(e, env)?,
278
+ ast::Stmt::Return(None) => {}
279
+ ast::Stmt::If(if_) => {
280
+ self.rewrite_expr(&mut if_.condition, env)?;
281
+ self.rewrite_block(&mut if_.body, &mut env.clone())?;
282
+ for ei in &mut if_.else_ifs {
283
+ self.rewrite_expr(&mut ei.condition, env)?;
284
+ self.rewrite_block(&mut ei.body, &mut env.clone())?;
285
+ }
286
+ if let Some(else_block) = &mut if_.else_ {
287
+ self.rewrite_block(else_block, &mut env.clone())?;
288
+ }
289
+ }
290
+ ast::Stmt::While(w) => {
291
+ self.rewrite_expr(&mut w.condition, env)?;
292
+ self.rewrite_block(&mut w.body, &mut env.clone())?;
293
+ }
294
+ ast::Stmt::For(f) => {
295
+ self.rewrite_expr(&mut f.iter, env)?;
296
+ let mut inner = env.clone();
297
+ for v in &f.vars {
298
+ inner.insert(v.clone(), TypeScheme::mono(PlumType::TInt));
299
+ }
300
+ self.rewrite_block(&mut f.body, &mut inner)?;
301
+ }
302
+ ast::Stmt::Expr(e) => self.rewrite_expr(e, env)?,
303
+ ast::Stmt::Assert(e) => self.rewrite_expr(e, env)?,
304
+ ast::Stmt::Match(m) => {
305
+ for s in &mut m.subjects {
306
+ self.rewrite_expr(s, env)?;
307
+ }
308
+ let subject_ty = m.subjects.first().map(|s| self.infer(s, env)).unwrap_or(PlumType::TInt);
309
+ for case in &mut m.cases {
310
+ let mut case_env = env.clone();
311
+ if let Some(ast::CasePattern::Name(n)) = case.patterns.first() {
312
+ let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
313
+ && self.enum_variants.contains_key(n);
314
+ if !is_variant {
315
+ case_env.insert(n.clone(), TypeScheme::mono(subject_ty.clone()));
316
+ }
317
+ }
318
+ self.rewrite_block(&mut case.body, &mut case_env)?;
319
+ }
320
+ }
321
+ ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
322
+ }
323
+ Ok(())
324
+ }
325
+
326
+ fn resolve_class_instantiation(&mut self, call: &mut ast::ClassCall, env: &TypeEnv) -> Result<(), String> {
327
+ let Some(class) = self.classes_generic.get(call.type_name.as_str()).copied() else { return Ok(()) };
328
+ let params = class_generic_params(class);
329
+ let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
330
+ for gp in &params {
331
+ if let Some(field) = class.fields.iter().find(|f| f.ty.name == *gp) {
332
+ if let Some(fa) = call.fields.iter().find(|fa| fa.name == field.name) {
333
+ bindings.insert(gp.clone(), self.infer(&fa.value, env));
334
+ }
335
+ }
336
+ }
337
+ if bindings.len() != params.len() {
338
+ return Err(format!(
339
+ "monomorphize: could not resolve all generic parameters for '{}' at this call site",
340
+ call.type_name
341
+ ));
342
+ }
343
+ let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
344
+ let mangled = mangle(&call.type_name, &type_args);
345
+ if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
346
+ self.enqueued.insert(mangled.clone());
347
+ self.worklist.push(PendingSpecialization::Class { base: class, subst: Substitution(bindings), mangled: mangled.clone() });
348
+ }
349
+ call.type_name = mangled;
350
+ Ok(())
351
+ }
352
+
353
+ fn resolve_fn_instantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
354
+ let Some(f) = self.fns_generic.get(call.name.as_str()).copied() else { return Ok(()) };
355
+ let params = fn_generic_params(f);
356
+ let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
357
+ for (param, arg) in f.params.iter().zip(call.args.iter()) {
358
+ let gp = match &param.ty {
359
+ ast::ParamType::Type(t) => t.name.clone(),
360
+ ast::ParamType::Variadic(t) => t.name.clone(),
361
+ };
362
+ if params.contains(&gp) {
363
+ let arg_expr = match arg {
364
+ ast::Arg::Positional(e) => e,
365
+ ast::Arg::Keyword { value, .. } => value,
366
+ ast::Arg::Pair { value, .. } => value,
367
+ };
368
+ bindings.entry(gp).or_insert_with(|| self.infer(arg_expr, env));
369
+ }
370
+ }
371
+ if bindings.len() != params.len() {
372
+ return Err(format!(
373
+ "monomorphize: could not resolve all generic parameters for '{}' at this call site",
374
+ call.name
375
+ ));
376
+ }
377
+ let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
378
+ let mangled = mangle(&call.name, &type_args);
379
+ if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
380
+ self.enqueued.insert(mangled.clone());
381
+ self.worklist.push(PendingSpecialization::Fn { base: f, subst: Substitution(bindings), mangled: mangled.clone(), new_receiver: None });
382
+ }
383
+ call.name = mangled;
384
+ Ok(())
385
+ }
386
+
387
+ fn rewrite_expr(&mut self, expr: &mut ast::Expr, env: &TypeEnv) -> Result<(), String> {
388
+ match expr {
389
+ ast::Expr::ClassCall(call) => {
390
+ for fa in &mut call.fields {
391
+ self.rewrite_expr(&mut fa.value, env)?;
392
+ }
393
+ self.resolve_class_instantiation(call, env)?;
394
+ }
395
+ ast::Expr::FnCall(call) => {
396
+ for arg in &mut call.args {
397
+ let e = match arg {
398
+ ast::Arg::Positional(e) => e,
399
+ ast::Arg::Keyword { value, .. } => value,
400
+ ast::Arg::Pair { value, .. } => value,
401
+ };
402
+ self.rewrite_expr(e, env)?;
403
+ }
404
+ self.resolve_fn_instantiation(call, env)?;
405
+ }
406
+ ast::Expr::Attribute(attr) => {
407
+ self.rewrite_expr(&mut attr.object, env)?;
408
+ if let ast::AttrKind::Method(call) = &mut attr.attr {
409
+ for arg in &mut call.args {
410
+ let e = match arg {
411
+ ast::Arg::Positional(e) => e,
412
+ ast::Arg::Keyword { value, .. } => value,
413
+ ast::Arg::Pair { value, .. } => value,
414
+ };
415
+ self.rewrite_expr(e, env)?;
416
+ }
417
+ // Method dispatch on a specialized receiver needs no rewrite here:
418
+ // once the receiver's construction site is rewritten to its mangled
419
+ // class name, the receiver's inferred static type IS that mangled
420
+ // name, and the specialized methods were registered under exactly
421
+ // that (mangled receiver, method name) key when their class was
422
+ // specialized (see the `PendingSpecialization::Class` arm below).
423
+ }
424
+ }
425
+ ast::Expr::Binary(b) => { self.rewrite_expr(&mut b.left, env)?; self.rewrite_expr(&mut b.right, env)?; }
426
+ ast::Expr::Bool(b) => { self.rewrite_expr(&mut b.left, env)?; self.rewrite_expr(&mut b.right, env)?; }
427
+ ast::Expr::Compare(c) => { self.rewrite_expr(&mut c.left, env)?; self.rewrite_expr(&mut c.right, env)?; }
428
+ ast::Expr::Not(inner) => self.rewrite_expr(inner, env)?,
429
+ ast::Expr::Unary(u) => self.rewrite_expr(&mut u.operand, env)?,
430
+ ast::Expr::Paren(inner) => self.rewrite_expr(inner, env)?,
431
+ ast::Expr::Ternary(t) => {
432
+ self.rewrite_expr(&mut t.condition, env)?;
433
+ self.rewrite_expr(&mut t.then, env)?;
434
+ self.rewrite_expr(&mut t.else_, env)?;
435
+ }
436
+ // String interpolation can embed arbitrary expressions (including generic
437
+ // call sites), so recurse into its interpolated parts.
438
+ ast::Expr::String(s) => {
439
+ for part in &mut s.parts {
440
+ if let ast::StringPart::Interp(e) = part {
441
+ self.rewrite_expr(e, env)?;
442
+ }
443
+ }
444
+ }
445
+ ast::Expr::Int(_) | ast::Expr::Float(_)
446
+ | ast::Expr::Self_ | ast::Expr::Var(_) | ast::Expr::TypeName(_) => {}
447
+ }
448
+ Ok(())
449
+ }
450
+ }
451
+
452
+ /// Runs the whole generics-monomorphization pass over `source`, producing a plain,
453
+ /// fully-concrete `ast::Source` with every generic `Class`/`Fn`/`Enum` template
454
+ /// replaced by zero or more mangled concrete specializations, and every remaining
455
+ /// item's body rewritten so its call sites reference those mangled names. The
456
+ /// result has no generic syntax left in it — `check_source`/`compile_source` run
457
+ /// on it completely unmodified.
458
+ pub fn monomorphize_source(source: &ast::Source) -> Result<ast::Source, String> {
459
+ let (global_env, classes, methods, enum_variants) = crate::build_global_tables(source);
460
+
461
+ let mut m = Monomorphizer {
462
+ classes_generic: BTreeMap::new(),
463
+ fns_generic: BTreeMap::new(),
464
+ methods_generic_on: BTreeMap::new(),
465
+ global_env,
466
+ classes,
467
+ methods,
468
+ enum_variants,
469
+ specialized: BTreeSet::new(),
470
+ enqueued: BTreeSet::new(),
471
+ worklist: Vec::new(),
472
+ produced: Vec::new(),
473
+ };
474
+
475
+ for item in &source.items {
476
+ match item {
477
+ ast::Item::Class(c) if !c.generics.is_empty() => { m.classes_generic.insert(c.name.clone(), c); }
478
+ _ => {}
479
+ }
480
+ }
481
+ for item in &source.items {
482
+ if let ast::Item::Fn(f) = item {
483
+ let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
484
+ if receiver_is_generic {
485
+ m.methods_generic_on.entry(f.type_param.clone().unwrap()).or_default().push(f);
486
+ } else if f.type_param.is_none() && !fn_generic_params(f).is_empty() {
487
+ m.fns_generic.insert(f.name.clone(), f);
488
+ }
489
+ // A method whose receiver is NOT generic is left as a regular method below,
490
+ // even if its own params/return happen to use a bare lowercase-letter type
491
+ // name — that shape (a method introducing its own extra generic parameter)
492
+ // is out of scope for this pass; see the plan's Global Constraints.
493
+ }
494
+ }
495
+
496
+ for item in &source.items {
497
+ match item {
498
+ ast::Item::Class(c) if c.generics.is_empty() => m.produced.push(ast::Item::Class(c.clone())),
499
+ ast::Item::Enum(e) if enum_generic_params(e).is_empty() => m.produced.push(ast::Item::Enum(e.clone())),
500
+ ast::Item::Const(c) => m.produced.push(ast::Item::Const(c.clone())),
501
+ ast::Item::Trait(t) => m.produced.push(ast::Item::Trait(t.clone())),
502
+ ast::Item::Fn(f) => {
503
+ let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
504
+ let is_generic_fn = f.type_param.is_none() && !fn_generic_params(f).is_empty();
505
+ if !receiver_is_generic && !is_generic_fn {
506
+ let mut f2 = f.clone();
507
+ m.rewrite_fn_body(&mut f2, false)?;
508
+ m.produced.push(ast::Item::Fn(f2));
509
+ }
510
+ }
511
+ _ => {} // generic Class/Enum declarations dropped here — templates only
512
+ }
513
+ }
514
+
515
+ let mut guard = 0usize;
516
+ while let Some(pending) = m.worklist.pop() {
517
+ guard += 1;
518
+ if guard > 10_000 {
519
+ return Err("monomorphize: exceeded specialization limit (possible unbounded generic recursion)".to_string());
520
+ }
521
+ match pending {
522
+ PendingSpecialization::Class { base, subst, mangled } => {
523
+ if !m.specialized.insert(mangled.clone()) { continue; }
524
+ let spec_class = specialize_class(base, &subst, &mangled);
525
+ // Register the specialized class's fields so inference inside its
526
+ // own (and other items') bodies can resolve `receiver.field` on the
527
+ // mangled type — `self.classes` was built from the ORIGINAL source
528
+ // and would otherwise not know this freshly-minted class.
529
+ m.classes.insert(
530
+ mangled.clone(),
531
+ spec_class.fields.iter().map(|f| (f.name.clone(), crate::plum_type_from_ast(&f.ty))).collect(),
532
+ );
533
+ m.produced.push(ast::Item::Class(spec_class));
534
+ if let Some(methods) = m.methods_generic_on.get(base.name.as_str()).cloned() {
535
+ for method in methods {
536
+ let mut specialized_method = specialize_fn(method, &subst, &method.name, Some(mangled.clone()));
537
+ m.rewrite_fn_body(&mut specialized_method, true)?;
538
+ // Register the specialized method's signature under its
539
+ // (mangled receiver, method name) key so any later body that
540
+ // dispatches to it can resolve its concrete return type.
541
+ let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
542
+ ast::ParamType::Type(t) => crate::plum_type_from_ast(t),
543
+ ast::ParamType::Variadic(t) => crate::plum_type_from_ast(t),
544
+ }).collect();
545
+ let ret = specialized_method.returns.as_ref()
546
+ .map(|r| crate::plum_type_from_ast(&ast::Type { name: r.name.clone(), generics: vec![] }))
547
+ .unwrap_or(PlumType::TUnit);
548
+ m.methods.insert((mangled.clone(), specialized_method.name.clone()), PlumType::TFun(param_types, Box::new(ret)));
549
+ m.produced.push(ast::Item::Fn(specialized_method));
550
+ }
551
+ }
552
+ }
553
+ PendingSpecialization::Fn { base, subst, mangled, new_receiver } => {
554
+ if !m.specialized.insert(mangled.clone()) { continue; }
555
+ let mut specialized_fn = specialize_fn(base, &subst, &mangled, new_receiver);
556
+ m.rewrite_fn_body(&mut specialized_fn, true)?;
557
+ // Register the specialized free function's signature so later bodies
558
+ // can resolve calls to it during inference.
559
+ let param_types: Vec<PlumType> = specialized_fn.params.iter().map(|p| match &p.ty {
560
+ ast::ParamType::Type(t) => crate::plum_type_from_ast(t),
561
+ ast::ParamType::Variadic(t) => crate::plum_type_from_ast(t),
562
+ }).collect();
563
+ let ret = specialized_fn.returns.as_ref()
564
+ .map(|r| crate::plum_type_from_ast(&ast::Type { name: r.name.clone(), generics: vec![] }))
565
+ .unwrap_or(PlumType::TUnit);
566
+ m.global_env.insert(specialized_fn.name.clone(), TypeScheme::mono(PlumType::TFun(param_types, Box::new(ret))));
567
+ m.produced.push(ast::Item::Fn(specialized_fn));
568
+ }
569
+ PendingSpecialization::Enum { base, subst, mangled } => {
570
+ if !m.specialized.insert(mangled.clone()) { continue; }
571
+ m.produced.push(ast::Item::Enum(specialize_enum(base, &subst, &mangled)));
572
+ }
573
+ }
574
+ }
575
+
576
+ Ok(ast::Source { module: source.module.clone(), imports: source.imports.clone(), items: m.produced })
577
+ }
plum-checker/tests/checker_tests.rs CHANGED
@@ -315,3 +315,69 @@ enum Animal =
315
315
  errs
316
316
  );
317
317
  }
318
+
319
+ #[test]
320
+ fn generic_class_instantiated_at_two_concrete_types_type_checks() {
321
+ let src = "\
322
+ type Box(a) =
323
+ value: a
324
+
325
+ makeIntBox() -> Box =
326
+ Box(value: 5)
327
+
328
+ makeStrBox() -> Box =
329
+ Box(value: \"x\")
330
+ ";
331
+ let source = parse(src);
332
+ let result = check_source(&source);
333
+ assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
334
+ }
335
+
336
+ #[test]
337
+ fn generic_function_called_with_different_concrete_types_per_site_type_checks() {
338
+ let src = "\
339
+ wrap(value: a) -> Bool =
340
+ True
341
+
342
+ useInt() -> Bool =
343
+ wrap(5)
344
+
345
+ useStr() -> Bool =
346
+ wrap(\"x\")
347
+ ";
348
+ let source = parse(src);
349
+ let result = check_source(&source);
350
+ assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
351
+ }
352
+
353
+ #[test]
354
+ fn generic_function_with_two_independent_type_params_type_checks() {
355
+ let src = "\
356
+ pair(first: a, second: b) -> Bool =
357
+ True
358
+
359
+ use() -> Bool =
360
+ pair(1, \"x\")
361
+ ";
362
+ let source = parse(src);
363
+ let result = check_source(&source);
364
+ assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
365
+ }
366
+
367
+ #[test]
368
+ fn generic_method_on_generic_class_type_checks() {
369
+ let src = "\
370
+ type Box(a) =
371
+ value: a
372
+
373
+ getValue<Box>() -> a =
374
+ self.value
375
+
376
+ use() -> Int =
377
+ b = Box(value: 5)
378
+ b.getValue()
379
+ ";
380
+ let source = parse(src);
381
+ let result = check_source(&source);
382
+ assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
383
+ }
plum-core/src/parser.rs CHANGED
@@ -97,28 +97,34 @@ impl<'a> AstParser<'a> {
97
97
  }
98
98
 
99
99
  fn parse_generics_field(&self, node: Node) -> Vec<GenericParam> {
100
+ // generics: "(" generic_type,* ")" where generic_type: generic (":" sep1(type_identifier, "+"))?
101
+ //
100
- // generics: "(" generic_type,* ")"
102
+ // Both `generic_type` and `generic` are `inline`d in the grammar, so the
101
- // The generics node is not field-named in a straightforward way; look
103
+ // `generics` node has NO `generic_type` children its named children are
104
+ // the single-letter generic nodes (`a`/`b`/`c`/`d`) each optionally
105
+ // followed by their bound `type_identifier` nodes, all flattened together.
106
+ // Reconstruct each `GenericParam` by starting a new one at every generic
107
+ // letter and attaching any following `type_identifier`s as its bounds
102
- // for a child whose kind is "generics".
108
+ // until the next generic letter.
103
- let generics_node = self.children_of_kind(node, "generics").into_iter().next();
109
+ let Some(generics_node) = self.children_of_kind(node, "generics").into_iter().next() else {
104
- generics_node.map(|g| {
110
+ return Vec::new();
105
- self.children_of_kind(g, "generic_type")
106
- .into_iter()
111
+ };
107
- .map(|n| self.parse_generic_type(n))
112
+ let mut cursor = generics_node.walk();
108
- .collect()
113
+ let mut params: Vec<GenericParam> = Vec::new();
114
+ for child in generics_node.named_children(&mut cursor) {
109
- }).unwrap_or_default()
115
+ match child.kind() {
116
+ "a" | "b" | "c" | "d" => {
117
+ params.push(GenericParam { name: self.text(child), bounds: Vec::new() });
110
- }
118
+ }
111
-
112
- fn parse_generic_type(&self, node: Node) -> GenericParam {
113
- // generic_type: generic (":" sep1(type_identifier, "+"))?
119
+ "type_identifier" => {
114
- // named_child(0) = generic (single letter)
120
+ if let Some(last) = params.last_mut() {
115
- // remaining named children = bound type_identifiers
116
- let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
117
- let bounds = (1..node.named_child_count())
121
+ last.bounds.push(self.text(child));
118
- .filter_map(|i| node.named_child(i as u32))
119
- .map(|n| self.text(n))
122
+ }
123
+ }
120
- .collect();
124
+ _ => {}
125
+ }
126
+ }
121
- GenericParam { name, bounds }
127
+ params
122
128
  }
123
129
 
124
130
  fn parse_field(&self, node: Node) -> Field {
plum-wasm-codegen/src/lib.rs CHANGED
@@ -294,6 +294,7 @@ fn infer_local_type(expr: &ast::Expr, ctx: &LocalCtx) -> PlumType {
294
294
  }
295
295
 
296
296
  pub fn compile_source(source: &ast::Source) -> Result<Vec<u8>, String> {
297
+ let source = &plum_checker::monomorphize::monomorphize_source(source)?;
297
298
  let (global_env, classes, methods, enum_variants) = plum_checker::build_global_tables(source);
298
299
 
299
300
  let mut module = WasmModule::new();