plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-checker/src/monomorphize.rs
use std::collections::BTreeMap;
use plum_core::ast;
use crate::types::PlumType;
/// A single uppercase letter (`T`, `U`, `K`, ...) is the grammar's only legal
/// spelling for a generic type parameter — this is how we recognize one, since
/// `ast::Fn` and `ast::Enum` (unlike `ast::Class`/`ast::Trait`) carry no explicit
/// generics declaration list.
pub fn isGenericParamName(name: &str) -> bool {
let mut chars = name.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => c.is_ascii_uppercase(),
_ => false,
}
}
/// The generic parameter names introduced by a `Class`, in declaration order.
pub fn classGenericParams(c: &ast::Class) -> Vec<String> {
c.generics.iter().map(|g| g.name.clone()).collect()
}
/// The generic parameter names implicitly introduced by a `Fn` — every distinct
/// single-uppercase-letter type name appearing in its params or return type, in
/// first-appearance order.
pub fn fnGenericParams(f: &ast::Fn) -> Vec<String> {
let mut names: Vec<String> = Vec::new();
let mut consider = |n: &str| {
if isGenericParamName(n) && !names.iter().any(|x| x == n) {
names.push(n.to_string());
}
};
for p in &f.params {
match &p.ty {
ast::ParamType::Type(t) => consider(&t.name),
ast::ParamType::Variadic(t) => consider(&t.name),
// TODO: closures/fn-value params don't yet participate in generic
// parameter inference.
ast::ParamType::Fn(_, _) => {}
}
}
if let Some(r) = &f.returns {
consider(&r.name);
}
names
}
/// The generic parameter names implicitly introduced by an `Enum` — every distinct
/// single-uppercase-letter variant field type name, in first-appearance order.
pub fn enumGenericParams(e: &ast::Enum) -> Vec<String> {
let mut names: Vec<String> = Vec::new();
for v in &e.variants {
for field_ty in &v.fields {
if isGenericParamName(field_ty) && !names.iter().any(|x| x == field_ty) {
names.push(field_ty.clone());
}
}
}
names
}
/// A resolved binding from a generic item's parameter names to concrete types for
/// one instantiation site, e.g. `{"a": Int}` for `Box(value: 5)`.
#[derive(Debug, Clone)]
pub struct Substitution(pub BTreeMap<String, PlumType>);
impl Substitution {
fn get(&self, name: &str) -> Option<&PlumType> {
self.0.get(name)
}
}
/// Converts a resolved concrete `PlumType` back into the `ast::Type` shape needed
/// to substitute into a declared field/param/return type position. Only ever
/// called with types resolved from a real call-site argument's inferred type, so
/// `TVar`/`TFun` (which never arise from a concrete argument) are an internal-error
/// case rather than something this needs to model.
fn plumTypeToAstType(t: &PlumType) -> ast::Type {
let name = match t {
PlumType::TInt => "Int".to_string(),
PlumType::TFloat => "Float".to_string(),
PlumType::TBool => "Bool".to_string(),
PlumType::TStr => "Str".to_string(),
PlumType::TByte => "Byte".to_string(),
PlumType::TByteSlice => "[]Byte".to_string(),
PlumType::TUnit => "Unit".to_string(),
PlumType::TNamed(n) => n.clone(),
PlumType::TVar(_) | PlumType::TFun(_, _) | PlumType::TVariadic(_) => t.to_string(),
};
ast::Type { name, generics: vec![] }
}
fn substituteType(ty: &ast::Type, subst: &Substitution) -> ast::Type {
if ty.generics.is_empty() {
if let Some(concrete) = subst.get(&ty.name) {
return plumTypeToAstType(concrete);
}
}
ast::Type {
name: ty.name.clone(),
generics: ty.generics.iter().map(|g| substituteType(g, subst)).collect(),
}
}
/// Mangles a generic item's base name and its resolved concrete type arguments
/// (in the item's own generic-parameter declaration order) into the internal name
/// used for its specialized copy, e.g. `Box` + `[Int]` -> `"Box$Int"`.
pub fn mangle(base: &str, type_args: &[PlumType]) -> String {
let mut out = base.to_string();
for t in type_args {
out.push('$');
out.push_str(&t.to_string());
}
out
}
/// Produces a concrete, specialized copy of a generic class under `mangled_name`,
/// substituting every field whose declared type names one of the class's generic
/// parameters with its resolved concrete type. The class's own `generics` list is
/// cleared on the copy (it is now fully concrete).
pub fn specializeClass(c: &ast::Class, subst: &Substitution, mangled_name: &str) -> ast::Class {
ast::Class {
name: mangled_name.to_string(),
implements: c.implements.clone(),
generics: vec![],
fields: c.fields.iter().map(|f| ast::Field {
name: f.name.clone(),
ty: substituteType(&f.ty, subst),
}).collect(),
}
}
/// Produces a concrete, specialized copy of a generic function (or method) under
/// `mangled_name`. `new_type_param` overrides the receiver-type name for a method
/// whose receiver class was itself specialized (e.g. a method declared on `Box`
/// becomes a method on `Box$Int`); pass the original `f.type_param.clone()`
/// unchanged for a plain free function. The body is left structurally identical
/// here — its own call sites are rewritten separately (Task 2), since expressions
/// don't carry declared-type annotations the way fields/params/return types do.
pub fn specializeFn(f: &ast::Fn, subst: &Substitution, mangled_name: &str, new_type_param: Option<String>) -> ast::Fn {
ast::Fn {
name: mangled_name.to_string(),
type_param: new_type_param,
is_extern: f.is_extern,
params: f.params.iter().map(|p| ast::Param {
name: p.name.clone(),
ty: match &p.ty {
ast::ParamType::Type(t) => ast::ParamType::Type(substituteType(t, subst)),
ast::ParamType::Variadic(t) => ast::ParamType::Variadic(substituteType(t, subst)),
ast::ParamType::Fn(params, ret) => ast::ParamType::Fn(
params.iter().map(|t| substituteType(t, subst)).collect(),
ret.as_ref().map(|r| Box::new(substituteType(r, subst))),
),
},
default: p.default.clone(),
}).collect(),
returns: f.returns.as_ref().map(|r| substituteType(r, subst)),
body: f.body.clone(),
}
}
/// Produces a concrete, specialized copy of a generic enum under `mangled_name`,
/// substituting every variant field type name that matches one of the enum's
/// generic parameters with its resolved concrete type's name.
///
/// Variant names are ALSO mangled here, with the same suffix as the enum's own
/// name (e.g. `Some` -> `Some$Int`) — even a payload-free variant like `None`.
/// This is necessary because the runtime `EnumVariants` table (built by
/// `buildGlobalTables`) is keyed by bare variant name globally: without this,
/// two specializations of the same generic enum would both register a variant
/// literally named `Some`, colliding in that flat table.
pub fn specializeEnum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -> ast::Enum {
let params = enumGenericParams(e);
let type_args: Vec<PlumType> = params.iter().filter_map(|p| subst.get(p).cloned()).collect();
ast::Enum {
name: mangled_name.to_string(),
params: e.params.clone(),
variants: e.variants.iter().map(|v| ast::EnumVariant {
name: mangle(&v.name, &type_args),
fields: v.fields.iter().map(|f| {
subst.get(f).map(|t| t.to_string()).unwrap_or_else(|| f.clone())
}).collect(),
values: v.values.clone(),
}).collect(),
}
}
use std::collections::BTreeSet;
use crate::types::{TypeEnv, TypeScheme};
use crate::{ClassEnv, MethodEnv, EnumVariants, EnumVariantInfo, EnumParams, CheckCtx};
enum PendingSpecialization<'a> {
Class { base: &'a ast::Class, subst: Substitution, mangled: String },
Fn { base: &'a ast::Fn, subst: Substitution, mangled: String, new_receiver: Option<String> },
Enum { base: &'a ast::Enum, subst: Substitution, mangled: String },
}
/// True if `pat` binds `name` anywhere within it (a `Name` sub-pattern, at any
/// nesting depth inside a `Class` constructor pattern) — used by
/// `renameVarInStmt`'s `Match` case to recognize when a NESTED case pattern
/// re-shadows the name currently being renamed, in which case that nested
/// case's body refers to a different (shadowing) binding and must be left alone.
fn caseBindsName(pat: &ast::CasePattern, name: &str) -> bool {
match pat {
ast::CasePattern::Name(n) => n == name,
ast::CasePattern::Class { fields, .. } => fields.iter().any(|f| caseBindsName(f, name)),
_ => false,
}
}
/// Renames every `Expr::Var(old)` to `Expr::Var(new)` within `block`, used by
/// `Monomorphizer::dedupLocalName` to rename a match-case/for-loop binding
/// (plus every reference to it) once its usage is known to be confined to that
/// one block — see `local_types_by_name`'s doc comment for why this is needed
/// at all. Purely syntactic (no type information needed): stops descending into
/// any NESTED scope that re-binds `old` itself (a nested `for` over the same
/// name, or a nested `match` case whose pattern binds it again), since that
/// inner scope's occurrences of `old` are a different, shadowing variable, not
/// the one being renamed.
fn renameVarInBlock(block: &mut ast::Block, old: &str, new: &str) {
for stmt in &mut block.stmts {
renameVarInStmt(stmt, old, new);
}
}
fn renameVarInStmt(stmt: &mut ast::Stmt, old: &str, new: &str) {
match stmt {
ast::Stmt::Assign(a) => {
for v in &mut a.values {
renameVarInExpr(v, old, new);
}
for t in &mut a.targets {
match t {
ast::AssignTarget::Var(n) => {
if n == old {
*n = new.to_string();
}
}
ast::AssignTarget::Field(obj, _) => renameVarInExpr(obj, old, new),
}
}
}
ast::Stmt::Return(Some(e)) => renameVarInExpr(e, old, new),
ast::Stmt::Return(None) | ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
ast::Stmt::Assert(e) => renameVarInExpr(e, old, new),
ast::Stmt::Expr(e) => renameVarInExpr(e, old, new),
ast::Stmt::If(if_) => {
renameVarInExpr(&mut if_.condition, old, new);
renameVarInBlock(&mut if_.body, old, new);
for ei in &mut if_.else_ifs {
renameVarInExpr(&mut ei.condition, old, new);
renameVarInBlock(&mut ei.body, old, new);
}
if let Some(else_block) = &mut if_.else_ {
renameVarInBlock(else_block, old, new);
}
}
ast::Stmt::While(w) => {
renameVarInExpr(&mut w.condition, old, new);
renameVarInBlock(&mut w.body, old, new);
}
ast::Stmt::For(f) => {
renameVarInExpr(&mut f.iter, old, new);
if !f.vars.iter().any(|v| v == old) {
renameVarInBlock(&mut f.body, old, new);
}
}
ast::Stmt::Match(m) => {
for s in &mut m.subjects {
renameVarInExpr(s, old, new);
}
for case in &mut m.cases {
if !case.patterns.iter().any(|p| caseBindsName(p, old)) {
renameVarInBlock(&mut case.body, old, new);
}
}
}
}
}
fn renameVarInExpr(expr: &mut ast::Expr, old: &str, new: &str) {
match expr {
ast::Expr::Var(n) => {
if n == old {
*n = new.to_string();
}
}
ast::Expr::ClassCall(call) => {
for fa in &mut call.fields {
renameVarInExpr(&mut fa.value, old, new);
}
}
ast::Expr::FnCall(call) => {
for arg in &mut call.args {
renameVarInArg(arg, old, new);
}
}
ast::Expr::Attribute(attr) => {
renameVarInExpr(&mut attr.object, old, new);
if let ast::AttrKind::Method(call) = &mut attr.attr {
for arg in &mut call.args {
renameVarInArg(arg, old, new);
}
}
}
ast::Expr::Binary(b) => { renameVarInExpr(&mut b.left, old, new); renameVarInExpr(&mut b.right, old, new); }
ast::Expr::Bool(b) => { renameVarInExpr(&mut b.left, old, new); renameVarInExpr(&mut b.right, old, new); }
ast::Expr::Compare(c) => { renameVarInExpr(&mut c.left, old, new); renameVarInExpr(&mut c.right, old, new); }
ast::Expr::Not(inner) => renameVarInExpr(inner, old, new),
ast::Expr::Unary(u) => renameVarInExpr(&mut u.operand, old, new),
ast::Expr::Paren(inner) => renameVarInExpr(inner, old, new),
ast::Expr::Ternary(t) => {
renameVarInExpr(&mut t.condition, old, new);
renameVarInExpr(&mut t.then, old, new);
renameVarInExpr(&mut t.else_, old, new);
}
ast::Expr::String(s) => {
for part in &mut s.parts {
if let ast::StringPart::Interp(e) = part {
renameVarInExpr(e, old, new);
}
}
}
ast::Expr::Int(_) | ast::Expr::Float(_) | ast::Expr::Self_ | ast::Expr::TypeName(_) => {}
// Not recursed into — matches `rewriteExpr`'s identical `Closure` case
// (closure bodies are compiled/free-variable-captured separately and
// aren't otherwise touched by this pass either). A closure capturing a
// variable that gets renamed here is a known, narrow residual gap.
ast::Expr::Closure(_) => {}
}
}
fn renameVarInArg(arg: &mut ast::Arg, old: &str, new: &str) {
match arg {
ast::Arg::Positional(e) => renameVarInExpr(e, old, new),
ast::Arg::Keyword { value, .. } => renameVarInExpr(value, old, new),
ast::Arg::Pair { value, .. } => renameVarInExpr(value, old, new),
}
}
struct Monomorphizer<'a> {
classes_generic: BTreeMap<String, &'a ast::Class>,
fns_generic: BTreeMap<String, &'a ast::Fn>,
methods_generic_on: BTreeMap<String, Vec<&'a ast::Fn>>,
/// Same as `methods_generic_on`, but for a method declared on a generic ENUM
/// (e.g. `Result`'s `isOk`/`isErr`) rather than a generic class — a separate map
/// because the two need separate lookups keyed by their own base-name maps
/// (`enums_generic_by_name` vs `classes_generic`) at both classification and
/// specialization time.
methods_generic_on_enum: BTreeMap<String, Vec<&'a ast::Fn>>,
/// Bare variant name (e.g. `"Some"`) -> the generic `Enum` it belongs to. Keyed
/// by variant name because a construction site (`Some(5)`) parses as a `FnCall`
/// whose `name` is the VARIANT, not the enum's own name.
enums_generic_by_variant: BTreeMap<String, &'a ast::Enum>,
/// Mangled enum name -> {original variant name -> mangled variant name}, e.g.
/// `"Option$Int" -> {"Some": "Some$Int", "None": "None$Int"}`. Populated eagerly
/// (in `resolveEnumInstantiation`, at the moment an instantiation's concrete
/// type arguments become known) rather than waiting for the worklist to actually
/// produce that specialization — so both a construction call site and a later
/// `match` on the same specialization can rewrite variant names consistently,
/// regardless of processing order.
enum_variant_mangling: BTreeMap<String, BTreeMap<String, String>>,
/// The enum's own bare name -> the generic `Enum` — used to detect a bare
/// generic-enum-typed function param (e.g. `o: Option`), distinct from
/// `enums_generic_by_variant` (keyed by VARIANT name, used for construction
/// sites like `Some(5)`).
enums_generic_by_name: BTreeMap<String, &'a ast::Enum>,
/// Free functions that are NOT generic by `fnGenericParams`'s lowercase-letter
/// convention, but whose param type(s) bare-name a generic class or enum (e.g.
/// `unwrapOr(o: Option, ...)`) — such a function still needs its own
/// per-call-site specialization, since its receiver generic class/enum is
/// dropped from the monomorphized output and the bare name would otherwise
/// resolve to nothing.
fns_bare_generic: BTreeMap<String, &'a ast::Fn>,
global_env: TypeEnv,
classes: ClassEnv,
methods: MethodEnv,
enum_variants: EnumVariants,
enum_params: EnumParams,
specialized: BTreeSet<String>,
enqueued: BTreeSet<String>,
worklist: Vec<PendingSpecialization<'a>>,
produced: Vec<ast::Item>,
/// The declared return type of the function/method currently being rewritten
/// by `rewriteFnBody` — consulted by `resolveEnumInstantiation` as a fallback
/// when a single variant construction site (e.g. `Ok(5)`) can't pin down every
/// one of the enum's generic params by itself (see its doc comment). Reset at
/// the top of every `rewriteFnBody` call; never needs saving/restoring since
/// closures aren't recursed into by this pass (see `rewriteExpr`'s `Closure` arm).
current_return_type: Option<ast::Type>,
/// (receiver name or `None` for a free function, function/method name) ->
/// (the generic enum its declared return type names, the concrete type args
/// it names them with) — for every function/method whose OWN declared return
/// type is a fully-general instantiation of a known generic enum (e.g.
/// `-> Result[Int, Str]`). Populated once up front (in `monomorphizeSource`,
/// alongside `enums_generic_by_name`) from the ORIGINAL, unmodified signatures
/// — unlike everywhere else in this file, this doesn't need the specialization
/// to have actually run yet, since the declared signature already says
/// everything needed. Consulted by `resolveCallReturnType`.
fn_return_generic_enum: BTreeMap<(Option<String>, String), (&'a ast::Enum, Vec<PlumType>)>,
/// Every local name's type as first seen in the function currently being
/// rewritten (reset per `rewriteFnBody` call, like `current_return_type`).
/// Wasm local slots are allocated once per NAME for the whole function (see
/// `plum-wasm-codegen`'s `Collector`/`compileFnBody`), not per lexical scope —
/// so two unrelated bindings that happen to share a name (e.g. `Ok(v)` in two
/// separate, non-overlapping `match` statements) would silently collide on
/// one slot if their types ever differ. `dedupLocalName` consults this to
/// catch that and rename the second, conflicting binding instead.
local_types_by_name: BTreeMap<String, PlumType>,
/// Bumped each time `dedupLocalName` needs a fresh name; part of the fresh
/// name itself, so collisions between two different renames are impossible.
rename_counter: usize,
}
impl<'a> Monomorphizer<'a> {
/// Like `infer`, but tries `resolveCallReturnType` first — needed anywhere the
/// resulting `PlumType` will be used to look up `enum_variant_mangling` (i.e.
/// wherever a value might need its constructor-pattern names rewritten later:
/// an assignment's recorded local type, or a `match` subject's type).
fn inferConcrete(&mut self, e: &ast::Expr, env: &TypeEnv) -> PlumType {
self.resolveCallReturnType(e, env).unwrap_or_else(|| self.infer(e, env))
}
fn infer(&self, e: &ast::Expr, env: &TypeEnv) -> PlumType {
let ctx = CheckCtx { classes: &self.classes, methods: &self.methods, enum_variants: &self.enum_variants, enum_params: &self.enum_params };
crate::inferExpr(e, env, &ctx).unwrap_or(PlumType::TVar("_".to_string()))
}
/// Records `name`'s first-seen type in `local_types_by_name` without
/// renaming anything — for a binding whose usage isn't cleanly bounded to a
/// single `&mut ast::Block` this pass has in hand at the binding site (e.g.
/// a plain `Assign` — its "scope" is however much of the flat function body
/// follows it, not a nested block). This still lets a LATER, cleanly-bounded
/// binding (`dedupLocalName`, from a `match` arm or `for` loop) detect a
/// conflict against it and rename itself accordingly; it just means a
/// conflict in the other direction (an `Assign` conflicting with an
/// EARLIER match-bound name) isn't caught. Real but narrower residual gap —
/// see the `local_types_by_name` doc comment.
fn seedLocalType(&mut self, name: &str, ty: &PlumType) {
self.local_types_by_name.entry(name.to_string()).or_insert_with(|| ty.clone());
}
/// Ensures `name` can be bound to `ty` here without colliding with a
/// DIFFERENT type already recorded for that same name elsewhere in the
/// current function (see `local_types_by_name`'s doc comment for why that's
/// otherwise unsafe). If there's no conflict, returns `name` unchanged. If
/// there IS one, mints a fresh name, renames every reference to `name`
/// within `scope` (a match case body / for-loop body — the full extent this
/// particular binding's usage can ever reach) to that fresh name, and
/// returns it for the caller to use as the actual binding name instead.
fn dedupLocalName(&mut self, name: &str, ty: &PlumType, scope: &mut ast::Block) -> String {
match self.local_types_by_name.get(name) {
None => {
self.local_types_by_name.insert(name.to_string(), ty.clone());
name.to_string()
}
Some(existing) if existing == ty => name.to_string(),
Some(_) => {
self.rename_counter += 1;
let fresh = format!("{}$dup{}", name, self.rename_counter);
self.local_types_by_name.insert(fresh.clone(), ty.clone());
renameVarInBlock(scope, name, &fresh);
fresh
}
}
}
/// Rewrites a function/method body's generic call sites. When `resolve_return`
/// is set (for freshly-generated specializations, whose declared return may be
/// a generic parameter like `a` — which the current grammar can't even parse,
/// leaving `returns: None` — or a generic class), the declared return type is
/// re-derived from the concrete inferred type of the body's tail expression.
/// For an ordinary (non-generic) top-level function we only rewrite the return
/// annotation when it names a generic class used bare (e.g. `-> Box`), which
/// the body's construction site has just been specialized to a mangled name.
fn rewriteFnBody(&mut self, f: &mut ast::Fn, resolve_return: bool) -> Result<(), String> {
self.current_return_type = f.returns.clone();
self.local_types_by_name.clear();
let mut env = self.global_env.clone();
if let Some(recv) = &f.type_param {
env.insert("self".to_string(), TypeScheme::mono(PlumType::TNamed(recv.clone())));
}
for p in &f.params {
let ty = match &p.ty {
ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
ast::ParamType::Fn(params, ret) => {
let param_types = params.iter().map(crate::plumTypeFromAst).collect();
let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
PlumType::TFun(param_types, Box::new(ret_ty))
}
};
self.local_types_by_name.insert(p.name.clone(), ty.clone());
env.insert(p.name.clone(), TypeScheme::mono(ty));
}
// The function's own return type, resolved for THIS specialization — used
// below to resolve a bare payload-free variant (`None`) that's the
// BODY'S TAIL EXPRESSION (implicit return), the same way `Stmt::Return`
// already does. A tail `Stmt::Expr` never goes through `Stmt::Return`'s
// own handling, so without this a trailing bare `None` is left
// unmangled and later fails the real checker's return-type unification.
let expected_ret = self.current_return_type.clone()
.map(|rt| self.resolveFieldType(&rt))
.map(|rt| crate::plumTypeFromAst(&rt));
let tail: Option<PlumType> = match &mut f.body {
ast::FnBody::Expr(e) => {
if let Some(expected) = &expected_ret {
self.resolveBareVariantAgainstExpected(e, expected);
}
self.rewriteExpr(e, &env)?;
Some(self.infer(e, &env))
}
ast::FnBody::Block(block) => {
if let Some(expected) = &expected_ret {
if let Some(ast::Stmt::Expr(e)) = block.stmts.last_mut() {
self.resolveBareVariantAgainstExpected(e, expected);
}
}
self.rewriteBlock(block, &mut env)?;
match block.stmts.last() {
Some(ast::Stmt::Expr(e)) => Some(self.infer(e, &env)),
Some(ast::Stmt::Return(Some(e))) => Some(self.infer(e, &env)),
_ => None,
}
}
// No body to rewrite or infer a tail type from.
ast::FnBody::Extern => None,
};
if let Some(t) = tail {
self.maybeRewriteReturn(f, &t, resolve_return);
}
Ok(())
}
/// Overwrites `f.returns` with a concrete type derived from the body's tail
/// type `t` when the currently-declared return type is genuinely generic or
/// unresolved. Never clobbers a real, concrete declared return type — even for
/// a specialization (`resolve_return: true`) — so a generic function whose body
/// is internally inconsistent with its concrete declared return (e.g.
/// `wrong(x: a) -> Int = "hello"`) is left for the checker's normal
/// return-type-mismatch logic to REJECT rather than silently rewritten (and
/// thereby masked). The overwrite fires only when:
/// - `f.returns` is `None` — the unparseable `-> a` generic-parameter-return
/// case, where the grammar dropped the annotation entirely (this only ever
/// happens for a specialization, which is the only path that can supply a
/// concrete tail type to fill it in); or
/// - the declared return names something still-generic: a generic-parameter
/// letter (e.g. `-> a`) or a generic class used bare (e.g. `-> Box`).
/// For the `Some(rt)` arm this condition is identical whether `resolve_return`
/// is `true` or `false`; the specialization path differs only in that its tail
/// is inferred against a resolved substitution, so a generic-parameter-letter
/// return resolves to the specialization's concrete bound type (which the
/// ordinary path cannot do). The unparseable-`None` fill-in is gated on
/// `resolve_return` so an ordinary void function (`returns: None` meaning "no
/// declared return", not "a generic return the grammar dropped") is never given
/// a fabricated return type. Never fabricates a return from an un-inferrable
/// (`TVar`) tail.
fn maybeRewriteReturn(&self, f: &mut ast::Fn, t: &PlumType, resolve_return: bool) {
if matches!(t, PlumType::TVar(_) | PlumType::TFun(_, _)) {
return;
}
let needs = match &f.returns {
None => resolve_return,
Some(rt) => {
isGenericParamName(&rt.name)
|| self.classes_generic.contains_key(&rt.name)
|| self.enums_generic_by_name.contains_key(&rt.name)
}
};
if needs {
f.returns = Some(ast::Type { name: t.to_string(), generics: vec![] });
}
}
fn rewriteBlock(&mut self, block: &mut ast::Block, env: &mut TypeEnv) -> Result<(), String> {
for stmt in &mut block.stmts {
self.rewriteStmt(stmt, env)?;
}
Ok(())
}
/// Rewrites `pat` (bare-name/constructor pattern) in place: an uppercase variant
/// name gets mangled to its specialized form (`Some` -> `Some$Int`) if `mangling`
/// says this position's subject is a specialized generic enum; a plain binding
/// name is inserted into `case_env` at `ty`. Recurses into a constructor
/// pattern's own fields (`Some(Some(v))`), looking up *that* field's own
/// mangling table from `self.enum_variant_mangling` — a nested sub-pattern can
/// be a specialization independent of its enclosing pattern's.
fn manglePattern(
&mut self,
pat: &mut ast::CasePattern,
ty: &PlumType,
mangling: Option<&BTreeMap<String, String>>,
case_env: &mut TypeEnv,
body: &mut ast::Block,
) {
match pat {
ast::CasePattern::Name(n) => {
let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
&& self.enum_variants.contains_key(n.as_str());
if is_variant {
if let Some(table) = mangling {
if let Some(mangled_variant) = table.get(n) {
*n = mangled_variant.clone();
}
}
} else {
let bound = self.dedupLocalName(n, ty, body);
if bound != *n {
*n = bound.clone();
}
case_env.insert(bound, TypeScheme::mono(ty.clone()));
}
}
ast::CasePattern::Class { name, fields } => {
if let Some(table) = mangling {
if let Some(mangled_variant) = table.get(name) {
*name = mangled_variant.clone();
}
}
if let Some(info) = self.enum_variants.get(name.as_str()) {
let field_types = info.field_types.clone();
for (f, fty) in fields.iter_mut().zip(field_types.iter()) {
let field_mangling: Option<BTreeMap<String, String>> = match fty {
PlumType::TNamed(n) => self.enum_variant_mangling.get(n).cloned(),
_ => None,
};
self.manglePattern(f, fty, field_mangling.as_ref(), case_env, body);
}
}
}
_ => {}
}
}
fn rewriteStmt(&mut self, stmt: &mut ast::Stmt, env: &mut TypeEnv) -> Result<(), String> {
match stmt {
ast::Stmt::Assign(a) => {
for (target, value) in a.targets.iter_mut().zip(a.values.iter_mut()) {
// For a field target (`self.head = None`), resolve the
// value against the field's OWN declared type BEFORE the
// generic rewrite/inference below — same reasoning as
// `ClassCall`'s field values: a bare payload-free variant
// has no type of its own, but the field it's being
// written into does.
if let ast::AssignTarget::Field(object, field_name) = target {
if let PlumType::TNamed(class_name) = self.infer(object, env) {
if let Some(field_ty) = self.classes.get(&class_name)
.and_then(|fields| fields.iter().find(|(n, _)| n == field_name).map(|(_, t)| t.clone()))
{
self.resolveBareVariantAgainstExpected(value, &field_ty);
}
}
}
self.rewriteExpr(value, env)?;
let ty = self.inferConcrete(value, env);
match target {
ast::AssignTarget::Var(name) => {
self.seedLocalType(name, &ty);
env.insert(name.clone(), TypeScheme::mono(ty));
}
ast::AssignTarget::Field(object, _) => {
self.rewriteExpr(object, env)?;
}
}
}
}
ast::Stmt::Return(Some(e)) => {
// Same idea as the field-target case above, but against the
// enclosing function's own declared return type (`return
// None` inside a method returning `Option[Int]`).
if let Some(rt) = self.current_return_type.clone() {
let resolved = self.resolveFieldType(&rt);
let expected = crate::plumTypeFromAst(&resolved);
self.resolveBareVariantAgainstExpected(e, &expected);
}
self.rewriteExpr(e, env)?;
}
ast::Stmt::Return(None) => {}
ast::Stmt::If(if_) => {
self.rewriteExpr(&mut if_.condition, env)?;
self.rewriteBlock(&mut if_.body, &mut env.clone())?;
for ei in &mut if_.else_ifs {
self.rewriteExpr(&mut ei.condition, env)?;
self.rewriteBlock(&mut ei.body, &mut env.clone())?;
}
if let Some(else_block) = &mut if_.else_ {
self.rewriteBlock(else_block, &mut env.clone())?;
}
}
ast::Stmt::While(w) => {
self.rewriteExpr(&mut w.condition, env)?;
self.rewriteBlock(&mut w.body, &mut env.clone())?;
}
ast::Stmt::For(f) => {
self.rewriteExpr(&mut f.iter, env)?;
let mut inner = env.clone();
let ast::For { vars, body, .. } = f;
for v in vars.iter_mut() {
let bound = self.dedupLocalName(v, &PlumType::TInt, body);
inner.insert(bound.clone(), TypeScheme::mono(PlumType::TInt));
*v = bound;
}
self.rewriteBlock(body, &mut inner)?;
}
ast::Stmt::Expr(e) => self.rewriteExpr(e, env)?,
ast::Stmt::Assert(e) => self.rewriteExpr(e, env)?,
ast::Stmt::Match(m) => {
for s in &mut m.subjects {
self.rewriteExpr(s, env)?;
}
// One (type, variant-mangling table) pair per subject — `match a, b`
// needs each position's own generic-enum specialization handled
// independently, not just the first subject's.
let subject_types: Vec<PlumType> = m.subjects.iter().map(|s| self.inferConcrete(s, env)).collect();
// If a subject's concrete type is a specialized generic enum, its
// variant-name mangling table lets us rewrite that position's patterns
// (`Some`/`None` -> `Some$Int`/`None$Int`) to reference the correct
// specialization, so the checker/codegen's unmodified, bare-name-keyed
// `EnumVariants` lookup still resolves each pattern correctly.
let variant_manglings: Vec<Option<BTreeMap<String, String>>> = subject_types
.iter()
.map(|ty| match ty {
PlumType::TNamed(n) => self.enum_variant_mangling.get(n).cloned(),
_ => None,
})
.collect();
for case in &mut m.cases {
let mut case_env = env.clone();
let ast::Case { patterns, body } = case;
for (pat, (subject_ty, variant_mangling)) in
patterns.iter_mut().zip(subject_types.iter().zip(variant_manglings.iter()))
{
self.manglePattern(pat, subject_ty, variant_mangling.as_ref(), &mut case_env, body);
}
// A case body's own LAST statement, when it's a bare
// `Stmt::Expr`, is itself an implicit-return position
// whenever this whole `match` is the function's tail
// expression (`first`/`last`'s `None =>` arm ending in a
// bare `None`, for instance) — same reasoning as
// `rewriteFnBody`'s own tail-expression handling, just one
// level down through the match. Resolving against the
// function's OWN declared return type is a no-op unless
// the expression is actually a bare payload-free variant
// of that same enum family, so this is harmless even for
// a match that ISN'T in tail position.
if let Some(rt) = self.current_return_type.clone() {
let resolved = self.resolveFieldType(&rt);
let expected = crate::plumTypeFromAst(&resolved);
if let Some(ast::Stmt::Expr(e)) = body.stmts.last_mut() {
self.resolveBareVariantAgainstExpected(e, &expected);
}
}
self.rewriteBlock(body, &mut case_env)?;
}
}
ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
}
Ok(())
}
fn resolveClassInstantiation(&mut self, call: &mut ast::ClassCall, env: &TypeEnv) -> Result<(), String> {
let Some(class) = self.classes_generic.get(call.type_name.as_str()).copied() else { return Ok(()) };
let params = classGenericParams(class);
let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
for gp in ¶ms {
if let Some(field) = class.fields.iter().find(|f| f.ty.name == *gp) {
if let Some(fa) = call.fields.iter().find(|fa| fa.name == field.name) {
// A bare payload-free variant reference (`None`) carries
// no type of its own to bind a generic param FROM — skip
// it here; it gets resolved AGAINST the binding (once
// known) below instead, same as every other field.
let is_bare_variant = matches!(&fa.value, ast::Expr::TypeName(n) if self.enums_generic_by_variant.contains_key(n));
if !is_bare_variant {
bindings.insert(gp.clone(), self.infer(&fa.value, env));
}
}
}
}
// No field is EVER directly typed as a bare generic param for a class
// like `List[T]` (its fields are `Option[Node[T]]`/`Int`, never a bare
// `T`) — and even where one exists, constructing with a payload-free
// value (`List(head: None, ...)`) gives no VALUE to infer a type from
// regardless. Fall back to an explicit `List[Int](...)` annotation at
// the call site when field-value inference alone isn't enough.
if bindings.len() != params.len() && call.generics.len() == params.len() {
for (p, gt) in params.iter().zip(call.generics.iter()) {
bindings.entry(p.clone()).or_insert_with(|| crate::plumTypeFromAst(gt));
}
}
if bindings.len() != params.len() {
return Err(format!(
"monomorphize: could not resolve all generic parameters for '{}' at this call site — pass them explicitly, e.g. '{}[Int](...)'",
call.type_name, call.type_name
));
}
let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
let mangled = mangle(&call.type_name, &type_args);
// Now that every generic param is bound, resolve any bare
// payload-free-variant field values (`None`) against THIS class's own
// (about-to-be-specialized) field types — codegen only ever sees the
// mangled specializations (the generic template is dropped), so a
// still-bare `None` would be an unresolvable reference by the time it
// gets there. `specializeClass` is a pure function; calling it here
// ahead of the worklist actually processing this specialization is
// fine — the worklist dedups on `mangled` regardless of how many
// times it's computed.
let spec_class = specializeClass(class, &Substitution(bindings.clone()), &mangled);
for fa in &mut call.fields {
if let Some(field) = spec_class.fields.iter().find(|f| f.name == fa.name) {
let field_ty = self.resolveFieldType(&field.ty);
let expected = crate::plumTypeFromAst(&field_ty);
self.resolveBareVariantAgainstExpected(&mut fa.value, &expected);
}
}
if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
self.enqueued.insert(mangled.clone());
self.worklist.push(PendingSpecialization::Class { base: class, subst: Substitution(bindings.clone()), mangled: mangled.clone() });
}
// Register this specialization's own field types and its methods'
// signatures right now — a construction site like this one can appear
// inside an ORDINARY (non-generic) function, which gets rewritten in
// the pass BEFORE the worklist above ever runs. Any later statement in
// that SAME function body (e.g. `l.get(1)` followed by a `match` on
// its result) needs `self.methods`/`self.classes` to already know
// about "List$Int" right now, not once the worklist eventually
// catches up.
if !self.classes.contains_key(&mangled) {
let field_types: Vec<(String, PlumType)> = spec_class.fields.iter()
.map(|f| (f.name.clone(), crate::plumTypeFromAst(&self.resolveFieldType(&f.ty))))
.collect();
self.classes.insert(mangled.clone(), field_types);
}
self.registerClassMethodSignatures(class, &mangled, &bindings);
call.type_name = mangled;
Ok(())
}
/// Eagerly computes and registers (into `self.methods`) the `(mangled,
/// method_name) -> TFun` signature of every method declared on `class`,
/// for the specialization named `mangled` under `bindings` — without
/// producing the actual `ast::Fn` items (that still only happens once the
/// worklist entry for this specialization is popped, avoiding duplicate
/// emission). Needed so a call site that appears in a function processed
/// BEFORE the worklist runs (see callers) can still resolve a method call
/// against this specialization immediately.
fn registerClassMethodSignatures(&mut self, class: &'a ast::Class, mangled: &str, bindings: &BTreeMap<String, PlumType>) {
let Some(methods) = self.methods_generic_on.get(class.name.as_str()).cloned() else { return };
for method in methods {
let key = (mangled.to_string(), method.name.clone());
if self.methods.contains_key(&key) {
continue;
}
let mut specialized_method = specializeFn(method, &Substitution(bindings.clone()), &method.name, Some(mangled.to_string()));
self.resolveFnSignature(&mut specialized_method);
let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
ast::ParamType::Fn(params, ret) => {
let param_types = params.iter().map(crate::plumTypeFromAst).collect();
let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
PlumType::TFun(param_types, Box::new(ret_ty))
}
}).collect();
let ret = specialized_method.returns.as_ref()
.map(crate::plumTypeFromAst)
.unwrap_or(PlumType::TUnit);
self.methods.insert(key, PlumType::TFun(param_types, Box::new(ret)));
}
}
/// If `expr` is a bare reference to a payload-free variant of a GENERIC
/// enum (`None`), and `expected` names a SPECIFIC specialization of that
/// same enum (`Option$Node$Int`), rewrites `expr`'s name to that
/// specialization's own mangled variant name (`None$Node$Int`) and
/// ensures that specialization is registered — codegen only ever knows
/// about specializations (the generic template enum is dropped entirely),
/// so an un-rewritten bare reference would be unresolvable by the time it
/// gets there. No-op if `expr` isn't a bare generic-enum variant, or
/// `expected` doesn't name a specialization of the SAME enum.
fn resolveBareVariantAgainstExpected(&mut self, expr: &mut ast::Expr, expected: &PlumType) {
let ast::Expr::TypeName(n) = expr else { return };
let Some(e) = self.enums_generic_by_variant.get(n.as_str()).copied() else { return };
let PlumType::TNamed(mangled) = expected else { return };
if !mangled.starts_with(&format!("{}$", e.name)) {
return;
}
if let Some(table) = self.enum_variant_mangling.get(mangled) {
if let Some(mangled_variant) = table.get(n) {
*n = mangled_variant.clone();
}
}
}
fn resolveFnInstantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
let Some(f) = self.fns_generic.get(call.name.as_str()).copied() else { return Ok(()) };
let params = fnGenericParams(f);
let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
for (param, arg) in f.params.iter().zip(call.args.iter()) {
let gp = match ¶m.ty {
ast::ParamType::Type(t) => t.name.clone(),
ast::ParamType::Variadic(t) => t.name.clone(),
// TODO: fn-value params don't yet resolve to a generic parameter.
ast::ParamType::Fn(_, _) => String::new(),
};
if params.contains(&gp) {
let arg_expr = match arg {
ast::Arg::Positional(e) => e,
ast::Arg::Keyword { value, .. } => value,
ast::Arg::Pair { value, .. } => value,
};
bindings.entry(gp).or_insert_with(|| self.infer(arg_expr, env));
}
}
if bindings.len() != params.len() {
return Err(format!(
"monomorphize: could not resolve all generic parameters for '{}' at this call site",
call.name
));
}
let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
let mangled = mangle(&call.name, &type_args);
if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
self.enqueued.insert(mangled.clone());
self.worklist.push(PendingSpecialization::Fn { base: f, subst: Substitution(bindings), mangled: mangled.clone(), new_receiver: None });
}
call.name = mangled;
Ok(())
}
/// The bare names of any generic class or enum referenced directly (not via a
/// lowercase-letter generic parameter) in `f`'s param types — e.g. `"Option"` for
/// `unwrapOr(o: Option, default: Int) -> Int`. See `fns_bare_generic`'s doc
/// comment for why such a function needs its own specialization.
fn fnBareGenericRefs(&self, f: &ast::Fn) -> Vec<String> {
let mut names: Vec<String> = Vec::new();
for p in &f.params {
let n = match &p.ty {
ast::ParamType::Type(t) => &t.name,
ast::ParamType::Variadic(t) => &t.name,
// TODO: fn-value params don't yet participate in bare-generic resolution.
ast::ParamType::Fn(_, _) => continue,
};
if (self.classes_generic.contains_key(n.as_str()) || self.enums_generic_by_name.contains_key(n.as_str()))
&& !names.iter().any(|x| x == n)
{
names.push(n.clone());
}
}
names
}
/// Resolves a call to an otherwise-ordinary function whose param type(s)
/// bare-name a generic class/enum, specializing it per call site exactly like a
/// truly-generic function — reusing the same `PendingSpecialization::Fn`
/// worklist entry and the unmodified `specializeFn`, whose substitution
/// mechanism already replaces any type whose bare name matches a substitution
/// key (it doesn't care whether that key came from a lowercase-letter generic
/// parameter or a bare generic class/enum reference).
fn resolveBareGenericFnInstantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
let Some(f) = self.fns_bare_generic.get(call.name.as_str()).copied() else { return Ok(()) };
let refs = self.fnBareGenericRefs(f);
let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
for (param, arg) in f.params.iter().zip(call.args.iter()) {
let n = match ¶m.ty {
ast::ParamType::Type(t) => t.name.clone(),
ast::ParamType::Variadic(t) => t.name.clone(),
// TODO: fn-value params don't yet resolve to a bare generic reference.
ast::ParamType::Fn(_, _) => String::new(),
};
if refs.contains(&n) {
let arg_expr = match arg {
ast::Arg::Positional(e) => e,
ast::Arg::Keyword { value, .. } => value,
ast::Arg::Pair { value, .. } => value,
};
bindings.entry(n).or_insert_with(|| self.infer(arg_expr, env));
}
}
if bindings.len() != refs.len() {
return Err(format!(
"monomorphize: could not resolve all generic parameters for '{}' at this call site",
call.name
));
}
let type_args: Vec<PlumType> = refs.iter().map(|p| bindings[p].clone()).collect();
let mangled = mangle(&call.name, &type_args);
if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
self.enqueued.insert(mangled.clone());
self.worklist.push(PendingSpecialization::Fn { base: f, subst: Substitution(bindings), mangled: mangled.clone(), new_receiver: None });
}
call.name = mangled;
Ok(())
}
/// Resolves a construction of a generic enum's variant (e.g. `Some(5)` for
/// `enum Option = | Some(a) | None`), rewriting `call.name` from the bare
/// variant name (`Some`) to its mangled form (`Some$Int`) once the enum's own
/// concrete instantiation is known. Mangling is eager and deterministic — it
/// doesn't wait for the worklist to actually produce the specialized `ast::Enum`
/// (see `enum_variant_mangling`'s doc comment).
///
/// A variant that carries no generic fields (e.g. `None`) can't pin down the
/// enum's type parameters on its own, so such a construction site is left alone
/// here — some other construction site (e.g. `Some(5)`) is what drives the
/// specialization. (A bare `None` used as a *value*, not a call, is
/// `ast::Expr::TypeName` and doesn't go through this function at all — see the
/// plan's Global Constraints for that narrower, documented residual limitation.)
fn resolveEnumInstantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
let Some(e) = self.enums_generic_by_variant.get(call.name.as_str()).copied() else { return Ok(()) };
let params = enumGenericParams(e);
let Some(variant) = e.variants.iter().find(|v| v.name == call.name) else { return Ok(()) };
let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
for (field_ty_name, arg) in variant.fields.iter().zip(call.args.iter()) {
if params.contains(field_ty_name) {
let arg_expr = match arg {
ast::Arg::Positional(e) => e,
ast::Arg::Keyword { value, .. } => value,
ast::Arg::Pair { value, .. } => value,
};
bindings.entry(field_ty_name.clone()).or_insert_with(|| self.infer(arg_expr, env));
}
}
// This single construction site couldn't pin down every generic parameter
// by itself — e.g. `Ok(5)` only ever supplies `Result`'s `T`, never its `E`
// (no `Ok` call site can, since `Err`'s payload is a disjoint field). Fall
// back to the enclosing function's declared return type, if it names this
// same enum with an explicit, fully-general `[...]` instantiation (e.g.
// `-> Result[Int, Str]`) — that's the one other place a type this
// construction site can't see on its own is written down on purpose.
if bindings.len() != params.len() {
if let Some(rt) = &self.current_return_type {
if rt.name == e.name && rt.generics.len() == params.len() {
for (p, gt) in params.iter().zip(rt.generics.iter()) {
bindings.entry(p.clone()).or_insert_with(|| crate::plumTypeFromAst(gt));
}
}
}
}
// Still couldn't pin down every generic parameter (e.g. a payload-free
// `None`, or no informative return-type annotation either). Leave it for
// another site to drive.
if bindings.len() != params.len() {
return Ok(());
}
let mangled = self.ensureEnumSpecialized(e, ¶ms, bindings);
call.name = self.enum_variant_mangling[&mangled][&variant.name].clone();
Ok(())
}
/// Registers (if not already registered) the specialization of generic enum
/// `e` at `bindings` — mangling every variant name, teaching `self.enum_variants`
/// about each mangled variant (so `self.infer` on an already-rewritten
/// construction site resolves correctly instead of falling back to an
/// uninformative `TVar`), and enqueueing the specialization to actually be
/// produced. Returns the mangled enum name. Shared by `resolveEnumInstantiation`
/// (bindings inferred from a construction site's own args, falling back to the
/// enclosing return type) and `resolveCallReturnType` (bindings taken directly
/// from a callee's *own* declared return type, with no construction site at all
/// — see its doc comment).
fn ensureEnumSpecialized(&mut self, e: &'a ast::Enum, params: &[String], bindings: BTreeMap<String, PlumType>) -> String {
let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
let mangled = mangle(&e.name, &type_args);
if !self.enum_variant_mangling.contains_key(&mangled) {
let mut table = BTreeMap::new();
for (tag, v) in e.variants.iter().enumerate() {
let mangled_variant = mangle(&v.name, &type_args);
table.insert(v.name.clone(), mangled_variant.clone());
let field_types: Vec<PlumType> = v.fields.iter().map(|f| {
bindings.get(f).cloned().unwrap_or_else(|| {
crate::plumTypeFromAst(&ast::Type { name: f.clone(), generics: vec![] })
})
}).collect();
self.enum_variants.insert(mangled_variant, EnumVariantInfo {
enum_name: mangled.clone(),
tag: tag as i32,
field_types,
values: v.values.clone(),
});
}
self.enum_variant_mangling.insert(mangled.clone(), table);
}
if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
self.enqueued.insert(mangled.clone());
self.worklist.push(PendingSpecialization::Enum { base: e, subst: Substitution(bindings), mangled: mangled.clone() });
}
mangled
}
/// Fully resolves a FIELD's declared type into something that will actually
/// exist after monomorphization. `specializeClass`'s own field substitution
/// only replaces a bare generic-param NAME (`T` -> `Int`) — a field declared
/// `Option[Node[T]]` becomes `Option[Node[Int]]` this way, which is now
/// fully CONCRETE but still names the generic TEMPLATES `Option`/`Node`
/// directly, both of which monomorphization deletes from the output (only
/// their mangled specializations, e.g. `Node$Int`, survive). Recursively
/// resolves any nested generic arguments first (so `Node[Int]` inside
/// `Option[Node[Int]]` becomes `Node$Int` before `Option[...]` itself is
/// resolved), then — if the type names a known generic class/enum applied
/// to arguments — mangles it to that specialization's real name and
/// enqueues the specialization if it hasn't been already (via the same
/// `ensureEnumSpecialized` used for enum construction sites, for enums; classes
/// don't have an equivalent shared helper, so that half is inlined here).
/// A field that's already concrete (no generics), or whose name isn't a
/// known generic template, is returned unchanged (or with just its nested
/// generics resolved) — this is ALSO called on every ordinary (non-generic)
/// class's fields, not just specialized ones, since a plain class can
/// perfectly well have a field like `items: List[Int]`.
fn resolveFieldType(&mut self, ty: &ast::Type) -> ast::Type {
if ty.generics.is_empty() {
return ty.clone();
}
let resolved_args: Vec<ast::Type> = ty.generics.iter().map(|g| self.resolveFieldType(g)).collect();
// A type argument that's STILL a bare single-uppercase-letter name after
// resolving means it's a truly free type variable at this point in the
// pipeline — e.g. `List[U]` inside `List[T]`'s own `map` method, where
// `U` is `map`'s OWN generic param, not `List`'s `T` (already substituted
// to a concrete type by the time this runs). Mangling/specializing
// against a placeholder name would silently manufacture a bogus
// `List$U` "specialization" baked from the letter U itself, so leave the
// whole type unresolved instead — see the `map`/method-level-generics
// gap noted in `libs/std/list.plum`.
if resolved_args.iter().any(|a| isGenericParamName(&a.name)) {
return ast::Type { name: ty.name.clone(), generics: resolved_args };
}
let type_args: Vec<PlumType> = resolved_args.iter().map(crate::plumTypeFromAst).collect();
if let Some(class) = self.classes_generic.get(ty.name.as_str()).copied() {
let params = classGenericParams(class);
if params.len() == type_args.len() {
let bindings: BTreeMap<String, PlumType> = params.into_iter().zip(type_args.iter().cloned()).collect();
let mangled = mangle(&ty.name, &type_args);
if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
self.enqueued.insert(mangled.clone());
self.worklist.push(PendingSpecialization::Class { base: class, subst: Substitution(bindings.clone()), mangled: mangled.clone() });
}
// Register this specialization's field types right now, even
// though its actual `Item::Class`/method output is only pushed
// to `m.produced` once the worklist entry above is popped —
// another method being rewritten in THIS SAME worklist round
// (e.g. a sibling method on the class currently being
// specialized) may need to resolve a field access against it
// immediately, well before that later worklist entry runs.
if !self.classes.contains_key(&mangled) {
// Insert a placeholder BEFORE recursing into the fields below —
// a self-referential class (`Node[T]`'s own `prev`/`next` fields
// point back to `Option[Node[T]]`) would otherwise recurse into
// resolving its own not-yet-registered specialization forever.
// The recursive re-entry only needs this specialization's
// MANGLED NAME to build its own field's type, not its fields —
// those get filled in for real below once the recursion unwinds.
self.classes.insert(mangled.clone(), vec![]);
let mut spec_class = specializeClass(class, &Substitution(bindings.clone()), &mangled);
for f in &mut spec_class.fields {
f.ty = self.resolveFieldType(&f.ty);
}
self.classes.insert(
mangled.clone(),
spec_class.fields.iter().map(|f| (f.name.clone(), crate::plumTypeFromAst(&f.ty))).collect(),
);
self.registerClassMethodSignatures(class, &mangled, &bindings);
}
return ast::Type { name: mangled, generics: vec![] };
}
}
if let Some(e) = self.enums_generic_by_name.get(ty.name.as_str()).copied() {
let params = enumGenericParams(e);
if params.len() == type_args.len() {
let bindings: BTreeMap<String, PlumType> = params.iter().cloned().zip(type_args.iter().cloned()).collect();
let mangled = self.ensureEnumSpecialized(e, ¶ms, bindings);
return ast::Type { name: mangled, generics: vec![] };
}
}
ast::Type { name: ty.name.clone(), generics: resolved_args }
}
/// Resolves every generic-with-args type in `f`'s own signature (params,
/// return) via `resolveFieldType`, mutating `f` in place. Must run before
/// `f` is pushed to `m.produced` — the real checker rebuilds its tables
/// FRESH from that final AST (via `buildGlobalTables`, using the plain
/// generics-dropping `plumTypeFromAst`), so whatever a signature still
/// says at that point is what the checker sees; a bare unresolved `Node`
/// or `List[Int]` left in a param would either dangle or get its generics
/// silently dropped again.
fn resolveFnSignature(&mut self, f: &mut ast::Fn) {
self.resolveFnParamTypes(f);
self.resolveFnReturnType(f);
}
/// Just the params half of `resolveFnSignature` — deliberately split out
/// so callers can run this BEFORE `rewriteFnBody` (env seeding needs
/// param types already flattened, e.g. `Node[T]` -> `Node$Int`, or a
/// field access on a param would fail to resolve) while leaving
/// `f.returns` untouched until AFTER the body's been rewritten. The body
/// rewrite reads `self.current_return_type` (seeded from `f.returns` as
/// originally declared, generics and all) to drive
/// `resolveEnumInstantiation`'s fallback for construction sites that
/// can't infer every generic param from their own arguments alone (e.g.
/// `Err("...")` needs `Result`'s OTHER param, `T`, from the function's
/// own `-> Result[Int, Str]` declaration) — flattening the return type
/// up front would replace `rt.name` with an already-mangled name that
/// fallback's own bare-template-name comparison can never match again.
fn resolveFnParamTypes(&mut self, f: &mut ast::Fn) {
for p in &mut f.params {
match &mut p.ty {
ast::ParamType::Type(t) => *t = self.resolveFieldType(t),
ast::ParamType::Variadic(t) => *t = self.resolveFieldType(t),
ast::ParamType::Fn(_, _) => {}
}
}
}
/// The returns half of `resolveFnSignature` — see `resolveFnParamTypes`'s
/// doc comment for why this must run AFTER `rewriteFnBody`, not before.
fn resolveFnReturnType(&mut self, f: &mut ast::Fn) {
if let Some(r) = &mut f.returns {
*r = self.resolveFieldType(r);
}
}
/// A construction site (`Ok(5)`) only ever tells us about the TYPE that's being
/// PRODUCED — it says nothing about code further downstream that CONSUMES an
/// already-specialized generic-enum value returned from calling some other
/// (already fully concrete, non-generic) function or method, e.g.
/// `match parseIt() { Ok(v) => ... }` where `fun parseIt() -> Result[Int, Str]`.
/// `self.infer` can't help there either: it's built from `buildGlobalTables`,
/// which (like every other `PlumType` site) drops a declared type's generic
/// args entirely (`plumTypeFromAst` maps `Result[Int, Str]` to the bare
/// `TNamed("Result")`) — so it has no way to know this call's result is the
/// SPECIALIZED `Result$Int$Str`, not the generic template.
///
/// This resolves that one specific, common shape directly from the callee's own
/// declaration (recorded in `fn_return_generic_enum` during classification) —
/// bypassing `self.infer` entirely, since the answer is already fully known
/// from the signature and doesn't depend on this call site's arguments at all.
/// Returns `None` for anything else (an ordinary call, a call to a function
/// whose return isn't a generic-enum instantiation, ...), meaning "fall back to
/// `self.infer` as before."
fn resolveCallReturnType(&mut self, e: &ast::Expr, env: &TypeEnv) -> Option<PlumType> {
let (recv, name) = match e {
ast::Expr::FnCall(call) => (None, call.name.clone()),
ast::Expr::Attribute(attr) => match &attr.attr {
ast::AttrKind::Method(call) => {
let recv = match crate::methodReceiverName(&self.infer(&attr.object, env)) {
Some(r) => r,
None => return None,
};
(Some(recv), call.name.clone())
}
ast::AttrKind::Field(_) => return None,
},
_ => return None,
};
let (enum_ref, type_args) = self.fn_return_generic_enum.get(&(recv, name))?.clone();
let params = enumGenericParams(enum_ref);
let bindings: BTreeMap<String, PlumType> = params.iter().cloned().zip(type_args).collect();
Some(PlumType::TNamed(self.ensureEnumSpecialized(enum_ref, ¶ms, bindings)))
}
fn rewriteExpr(&mut self, expr: &mut ast::Expr, env: &TypeEnv) -> Result<(), String> {
match expr {
ast::Expr::ClassCall(call) => {
// Runs FIRST (before the generic per-field rewrite below): a
// bare payload-free variant field value (`Node(..., next:
// None)`) carries no type of its own to infer a generic
// param from, and needs the class's OWN (about-to-be-
// specialized) field type to resolve which specialization it
// actually means — `resolveClassInstantiation` handles that
// internally once it knows the full binding set.
self.resolveClassInstantiation(call, env)?;
for fa in &mut call.fields {
self.rewriteExpr(&mut fa.value, env)?;
}
}
ast::Expr::FnCall(call) => {
for arg in &mut call.args {
let e = match arg {
ast::Arg::Positional(e) => e,
ast::Arg::Keyword { value, .. } => value,
ast::Arg::Pair { value, .. } => value,
};
self.rewriteExpr(e, env)?;
}
// A `FnCall` may name either a generic free function or a generic
// enum's variant; the two name spaces don't overlap (variants are
// capitalized). Enum resolution runs first and rewrites `call.name`
// to its mangled form when it resolves — `fns_generic` is keyed by
// the ORIGINAL unmangled free-function names, so a rewritten variant
// name can never accidentally match it afterward.
self.resolveEnumInstantiation(call, env)?;
self.resolveFnInstantiation(call, env)?;
self.resolveBareGenericFnInstantiation(call, env)?;
}
ast::Expr::Attribute(attr) => {
self.rewriteExpr(&mut attr.object, env)?;
if let ast::AttrKind::Method(call) = &mut attr.attr {
for arg in &mut call.args {
let e = match arg {
ast::Arg::Positional(e) => e,
ast::Arg::Keyword { value, .. } => value,
ast::Arg::Pair { value, .. } => value,
};
self.rewriteExpr(e, env)?;
}
// Method dispatch on a specialized receiver needs no rewrite here:
// once the receiver's construction site is rewritten to its mangled
// class name, the receiver's inferred static type IS that mangled
// name, and the specialized methods were registered under exactly
// that (mangled receiver, method name) key when their class was
// specialized (see the `PendingSpecialization::Class` arm below).
}
}
ast::Expr::Binary(b) => { self.rewriteExpr(&mut b.left, env)?; self.rewriteExpr(&mut b.right, env)?; }
ast::Expr::Bool(b) => { self.rewriteExpr(&mut b.left, env)?; self.rewriteExpr(&mut b.right, env)?; }
ast::Expr::Compare(c) => {
self.rewriteExpr(&mut c.left, env)?;
self.rewriteExpr(&mut c.right, env)?;
// `current != None`: `current`'s side may already be a SPECIFIC
// enum specialization (e.g. `Option$Node$Int`) while `None`
// itself is still bare (it carries no type of its own) —
// resolve each side against the OTHER's type; a no-op unless
// that side is actually a bare generic-enum variant.
let lt = self.infer(&c.left, env);
let rt = self.infer(&c.right, env);
self.resolveBareVariantAgainstExpected(&mut c.left, &rt);
self.resolveBareVariantAgainstExpected(&mut c.right, <);
}
ast::Expr::Not(inner) => self.rewriteExpr(inner, env)?,
ast::Expr::Unary(u) => self.rewriteExpr(&mut u.operand, env)?,
ast::Expr::Paren(inner) => self.rewriteExpr(inner, env)?,
ast::Expr::Ternary(t) => {
self.rewriteExpr(&mut t.condition, env)?;
self.rewriteExpr(&mut t.then, env)?;
self.rewriteExpr(&mut t.else_, env)?;
}
// String interpolation can embed arbitrary expressions (including generic
// call sites), so recurse into its interpolated parts.
ast::Expr::String(s) => {
for part in &mut s.parts {
if let ast::StringPart::Interp(e) = part {
self.rewriteExpr(e, env)?;
}
}
}
ast::Expr::Int(_) | ast::Expr::Float(_)
| ast::Expr::Self_ | ast::Expr::Var(_) | ast::Expr::TypeName(_) => {}
// TODO: closure bodies don't yet get rewritten for generic call sites.
ast::Expr::Closure(_) => {}
}
Ok(())
}
}
/// Runs the whole generics-monomorphization pass over `source`, producing a plain,
/// fully-concrete `ast::Source` with every generic `Class`/`Fn`/`Enum` template
/// replaced by zero or more mangled concrete specializations, and every remaining
/// item's body rewritten so its call sites reference those mangled names. The
/// result has no generic syntax left in it — `checkSource`/`compileSource` run
/// on it completely unmodified.
pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
let (global_env, classes, methods, enum_variants, enum_params) = crate::buildGlobalTables(source);
let mut m = Monomorphizer {
classes_generic: BTreeMap::new(),
fns_generic: BTreeMap::new(),
methods_generic_on: BTreeMap::new(),
methods_generic_on_enum: BTreeMap::new(),
enums_generic_by_variant: BTreeMap::new(),
enums_generic_by_name: BTreeMap::new(),
enum_variant_mangling: BTreeMap::new(),
fns_bare_generic: BTreeMap::new(),
global_env,
classes,
methods,
enum_variants,
enum_params,
specialized: BTreeSet::new(),
enqueued: BTreeSet::new(),
worklist: Vec::new(),
produced: Vec::new(),
current_return_type: None,
fn_return_generic_enum: BTreeMap::new(),
local_types_by_name: BTreeMap::new(),
rename_counter: 0,
};
for item in &source.items {
match item {
ast::Item::Class(c) if !c.generics.is_empty() => { m.classes_generic.insert(c.name.clone(), c); }
ast::Item::Enum(e) if !enumGenericParams(e).is_empty() => {
m.enums_generic_by_name.insert(e.name.clone(), e);
for v in &e.variants {
m.enums_generic_by_variant.insert(v.name.clone(), e);
}
}
_ => {}
}
}
// Any function/method (generic or not — this doesn't care either way) whose
// OWN declared return type fully instantiates a known generic enum. Must run
// after the loop above (needs `enums_generic_by_name` filled) but is otherwise
// independent of every other classification pass here.
for item in &source.items {
if let ast::Item::Fn(f) = item {
if let Some(rt) = &f.returns {
if let Some(e) = m.enums_generic_by_name.get(rt.name.as_str()).copied() {
let params = enumGenericParams(e);
if !rt.generics.is_empty() && rt.generics.len() == params.len() {
let type_args: Vec<PlumType> = rt.generics.iter().map(crate::plumTypeFromAst).collect();
m.fn_return_generic_enum.insert((f.type_param.clone(), f.name.clone()), (e, type_args));
}
}
}
}
}
for item in &source.items {
if let ast::Item::Fn(f) = item {
let receiver_is_generic_class = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
let receiver_is_generic_enum = f.type_param.as_deref().map(|r| m.enums_generic_by_name.contains_key(r)).unwrap_or(false);
if receiver_is_generic_class {
m.methods_generic_on.entry(f.type_param.clone().unwrap()).or_default().push(f);
} else if receiver_is_generic_enum {
m.methods_generic_on_enum.entry(f.type_param.clone().unwrap()).or_default().push(f);
} else if f.type_param.is_none() && !fnGenericParams(f).is_empty() {
m.fns_generic.insert(f.name.clone(), f);
} else if f.type_param.is_none() && !m.fnBareGenericRefs(f).is_empty() {
m.fns_bare_generic.insert(f.name.clone(), f);
}
// A method whose receiver is NOT generic is left as a regular method below,
// even if its own params/return happen to use a bare lowercase-letter type
// name, or bare-name a generic class/enum — those shapes are out of scope
// for this pass; see the plan's Global Constraints.
}
}
for item in &source.items {
match item {
ast::Item::Class(c) if c.generics.is_empty() => {
let mut c2 = c.clone();
for f in &mut c2.fields {
f.ty = m.resolveFieldType(&f.ty);
}
m.produced.push(ast::Item::Class(c2));
}
ast::Item::Enum(e) if enumGenericParams(e).is_empty() => m.produced.push(ast::Item::Enum(e.clone())),
ast::Item::Const(c) => m.produced.push(ast::Item::Const(c.clone())),
ast::Item::Trait(t) => m.produced.push(ast::Item::Trait(t.clone())),
ast::Item::Fn(f) => {
let receiver_is_generic = f.type_param.as_deref()
.map(|r| m.classes_generic.contains_key(r) || m.enums_generic_by_name.contains_key(r))
.unwrap_or(false);
let is_generic_fn = f.type_param.is_none() && !fnGenericParams(f).is_empty();
let is_bare_generic_fn = f.type_param.is_none() && m.fns_bare_generic.contains_key(f.name.as_str());
if !receiver_is_generic && !is_generic_fn && !is_bare_generic_fn {
let mut f2 = f.clone();
m.resolveFnParamTypes(&mut f2);
m.rewriteFnBody(&mut f2, false)?;
m.resolveFnReturnType(&mut f2);
m.produced.push(ast::Item::Fn(f2));
}
}
_ => {} // generic Class/Enum declarations dropped here — templates only
}
}
let mut guard = 0usize;
while let Some(pending) = m.worklist.pop() {
guard += 1;
if guard > 10_000 {
return Err("monomorphize: exceeded specialization limit (possible unbounded generic recursion)".to_string());
}
match pending {
PendingSpecialization::Class { base, subst, mangled } => {
if !m.specialized.insert(mangled.clone()) { continue; }
let mut spec_class = specializeClass(base, &subst, &mangled);
// `specializeClass` only substitutes a field's bare generic-param
// NAME (`T` -> `Int`) — a field like `Option[Node[T]]` becomes
// `Option[Node[Int]]`, still a generic instantiation, not yet a
// real (mangled) type. Resolve those the rest of the way now.
for f in &mut spec_class.fields {
f.ty = m.resolveFieldType(&f.ty);
}
// Register the specialized class's fields so inference inside its
// own (and other items') bodies can resolve `receiver.field` on the
// mangled type — `self.classes` was built from the ORIGINAL source
// and would otherwise not know this freshly-minted class.
m.classes.insert(
mangled.clone(),
spec_class.fields.iter().map(|f| (f.name.clone(), crate::plumTypeFromAst(&f.ty))).collect(),
);
m.produced.push(ast::Item::Class(spec_class));
if let Some(methods) = m.methods_generic_on.get(base.name.as_str()).cloned() {
for method in methods {
let mut specialized_method = specializeFn(method, &subst, &method.name, Some(mangled.clone()));
// Params (not returns — see `resolveFnParamTypes`'s doc
// comment) must run before `rewriteFnBody`/registration
// below: both read the signature's types directly off
// this AST, and the real checker later rebuilds its own
// tables from this exact (post-monomorphize) AST too.
m.resolveFnParamTypes(&mut specialized_method);
m.rewriteFnBody(&mut specialized_method, true)?;
m.resolveFnReturnType(&mut specialized_method);
// Register the specialized method's signature under its
// (mangled receiver, method name) key so any later body that
// dispatches to it can resolve its concrete return type.
let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
ast::ParamType::Fn(params, ret) => {
let param_types = params.iter().map(crate::plumTypeFromAst).collect();
let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
PlumType::TFun(param_types, Box::new(ret_ty))
}
}).collect();
let ret = specialized_method.returns.as_ref()
.map(crate::plumTypeFromAst)
.unwrap_or(PlumType::TUnit);
m.methods.insert((mangled.clone(), specialized_method.name.clone()), PlumType::TFun(param_types, Box::new(ret)));
m.produced.push(ast::Item::Fn(specialized_method));
}
}
}
PendingSpecialization::Fn { base, subst, mangled, new_receiver } => {
if !m.specialized.insert(mangled.clone()) { continue; }
let mut specialized_fn = specializeFn(base, &subst, &mangled, new_receiver);
m.resolveFnParamTypes(&mut specialized_fn);
m.rewriteFnBody(&mut specialized_fn, true)?;
m.resolveFnReturnType(&mut specialized_fn);
// Register the specialized free function's signature so later bodies
// can resolve calls to it during inference.
let param_types: Vec<PlumType> = specialized_fn.params.iter().map(|p| match &p.ty {
ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
ast::ParamType::Fn(params, ret) => {
let param_types = params.iter().map(crate::plumTypeFromAst).collect();
let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
PlumType::TFun(param_types, Box::new(ret_ty))
}
}).collect();
let ret = specialized_fn.returns.as_ref()
.map(crate::plumTypeFromAst)
.unwrap_or(PlumType::TUnit);
m.global_env.insert(specialized_fn.name.clone(), TypeScheme::mono(PlumType::TFun(param_types, Box::new(ret))));
m.produced.push(ast::Item::Fn(specialized_fn));
}
PendingSpecialization::Enum { base, subst, mangled } => {
if !m.specialized.insert(mangled.clone()) { continue; }
let spec_enum = specializeEnum(base, &subst, &mangled);
m.produced.push(ast::Item::Enum(spec_enum));
if let Some(methods) = m.methods_generic_on_enum.get(base.name.as_str()).cloned() {
for method in methods {
let mut specialized_method = specializeFn(method, &subst, &method.name, Some(mangled.clone()));
m.resolveFnParamTypes(&mut specialized_method);
m.rewriteFnBody(&mut specialized_method, true)?;
m.resolveFnReturnType(&mut specialized_method);
let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
ast::ParamType::Fn(params, ret) => {
let param_types = params.iter().map(crate::plumTypeFromAst).collect();
let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
PlumType::TFun(param_types, Box::new(ret_ty))
}
}).collect();
let ret = specialized_method.returns.as_ref()
.map(crate::plumTypeFromAst)
.unwrap_or(PlumType::TUnit);
m.methods.insert((mangled.clone(), specialized_method.name.clone()), PlumType::TFun(param_types, Box::new(ret)));
m.produced.push(ast::Item::Fn(specialized_method));
}
}
}
}
}
Ok(ast::Source { module: source.module.clone(), imports: source.imports.clone(), items: m.produced })
}