plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
22140cf
— Peter 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 +2 -1
- plum-checker/src/monomorphize.rs +410 -0
- plum-checker/tests/checker_tests.rs +66 -0
- plum-core/src/parser.rs +28 -22
- plum-wasm-codegen/src/lib.rs +1 -0
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 ¶ms {
|
|
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 ¶m.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
|
-
//
|
|
102
|
+
// Both `generic_type` and `generic` are `inline`d in the grammar, so the
|
|
101
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
110
|
+
return Vec::new();
|
|
105
|
-
self.children_of_kind(g, "generic_type")
|
|
106
|
-
|
|
111
|
+
};
|
|
107
|
-
|
|
112
|
+
let mut cursor = generics_node.walk();
|
|
108
|
-
|
|
113
|
+
let mut params: Vec<GenericParam> = Vec::new();
|
|
114
|
+
for child in generics_node.named_children(&mut cursor) {
|
|
109
|
-
|
|
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
|
-
|
|
119
|
+
"type_identifier" => {
|
|
114
|
-
|
|
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
|
-
|
|
121
|
+
last.bounds.push(self.text(child));
|
|
118
|
-
.filter_map(|i| node.named_child(i as u32))
|
|
119
|
-
|
|
122
|
+
}
|
|
123
|
+
}
|
|
120
|
-
|
|
124
|
+
_ => {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
121
|
-
|
|
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();
|