plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-checker/src/lib.rs
| 3d6f280 | 1 | // Functions/methods are named camelCase across this project (matching plum's own |
| 3d6f280 | 2 | // naming convention), not Rust's idiomatic snake_case — silence the resulting lint. |
| 3d6f280 | 3 | #![allow(non_snake_case)] |
| 3d6f280 | 4 | |
| 1daab8d | 5 | pub mod types; |
| 1b220d2 | 6 | pub mod monomorphize; |
| 30f1008 | 7 | |
| d1a4183 | 8 | use std::collections::BTreeMap; |
| 30f1008 | 9 | use types::{PlumType, TypeEnv, TypeScheme, CheckError, CheckResult}; |
| 30f1008 | 10 | use plum_core::ast; |
| 30f1008 | 11 | |
| 0000000 | 12 | /// Maps a bare type name (as written in source: a param/return annotation, or |
| 0000000 | 13 | /// a method's receiver name) to its `PlumType`. Builtin primitives get their |
| 0000000 | 14 | /// dedicated variant; anything else is an unmodeled `TNamed`. |
| 0000000 | 15 | pub fn plumTypeFromName(name: &str) -> PlumType { |
| 0000000 | 16 | match name { |
| 30f1008 | 17 | "Int" => PlumType::TInt, |
| 30f1008 | 18 | "Float" => PlumType::TFloat, |
| 30f1008 | 19 | "Bool" => PlumType::TBool, |
| 30f1008 | 20 | "Str" => PlumType::TStr, |
| 0000000 | 21 | "Byte" => PlumType::TByte, |
| 0000000 | 22 | "[]Byte" => PlumType::TByteSlice, |
| 30f1008 | 23 | "Unit" => PlumType::TUnit, |
| 30f1008 | 24 | other => PlumType::TNamed(other.to_string()), |
| 30f1008 | 25 | } |
| 30f1008 | 26 | } |
| 30f1008 | 27 | |
| 0000000 | 28 | pub fn plumTypeFromAst(ty: &ast::Type) -> PlumType { |
| 0000000 | 29 | plumTypeFromName(&ty.name) |
| 0000000 | 30 | } |
| 0000000 | 31 | |
| 0000000 | 32 | /// The `ctx.methods`/`ctx.classes` receiver name for a value of type `ty`, or |
| 0000000 | 33 | /// `None` if `ty` has no methods (e.g. an unresolved `TVar` or a bare |
| 0000000 | 34 | /// function type). Builtin primitive types (`Int`/`Float`/`Bool`/`Str`) are |
| 0000000 | 35 | /// declared as `type Int = fun ...` etc in `libs/std`, exactly like a class — |
| 0000000 | 36 | /// they're just never `TNamed` at the type level, so this maps them back to |
| 0000000 | 37 | /// the same receiver name string `ctx.methods` is keyed by. |
| 0000000 | 38 | pub fn methodReceiverName(ty: &PlumType) -> Option<String> { |
| 0000000 | 39 | match ty { |
| 0000000 | 40 | PlumType::TNamed(name) => Some(name.clone()), |
| 0000000 | 41 | PlumType::TInt => Some("Int".to_string()), |
| 0000000 | 42 | PlumType::TFloat => Some("Float".to_string()), |
| 0000000 | 43 | PlumType::TBool => Some("Bool".to_string()), |
| 0000000 | 44 | PlumType::TStr => Some("Str".to_string()), |
| 0000000 | 45 | PlumType::TByte => Some("Byte".to_string()), |
| 0000000 | 46 | PlumType::TByteSlice => Some("ByteSlice".to_string()), |
| 0000000 | 47 | _ => None, |
| 0000000 | 48 | } |
| 0000000 | 49 | } |
| 0000000 | 50 | |
| 30f1008 | 51 | pub fn unify(t1: &PlumType, t2: &PlumType) -> Result<(), String> { |
| 30f1008 | 52 | match (t1, t2) { |
| 30f1008 | 53 | (PlumType::TVar(_), _) | (_, PlumType::TVar(_)) => Ok(()), |
| 30f1008 | 54 | (PlumType::TInt, PlumType::TInt) => Ok(()), |
| 30f1008 | 55 | (PlumType::TFloat, PlumType::TFloat) => Ok(()), |
| 30f1008 | 56 | (PlumType::TBool, PlumType::TBool) => Ok(()), |
| 30f1008 | 57 | (PlumType::TStr, PlumType::TStr) => Ok(()), |
| 0000000 | 58 | (PlumType::TByte, PlumType::TByte) => Ok(()), |
| 0000000 | 59 | (PlumType::TByteSlice, PlumType::TByteSlice) => Ok(()), |
| 30f1008 | 60 | (PlumType::TUnit, PlumType::TUnit) => Ok(()), |
| 30f1008 | 61 | (PlumType::TNamed(a), PlumType::TNamed(b)) if a == b => Ok(()), |
| 30f1008 | 62 | (PlumType::TFun(ps1, r1), PlumType::TFun(ps2, r2)) if ps1.len() == ps2.len() => { |
| 30f1008 | 63 | for (p1, p2) in ps1.iter().zip(ps2.iter()) { |
| 30f1008 | 64 | unify(p1, p2)?; |
| 30f1008 | 65 | } |
| 30f1008 | 66 | unify(r1, r2) |
| 30f1008 | 67 | } |
| 30f1008 | 68 | _ => Err(format!("type mismatch: expected {}, found {}", t1, t2)), |
| 30f1008 | 69 | } |
| 30f1008 | 70 | } |
| 30f1008 | 71 | |
| d1a4183 | 72 | pub fn lookup(env: &TypeEnv, name: &str) -> Result<PlumType, String> { |
| 30f1008 | 73 | env.get(name) |
| 30f1008 | 74 | .map(|s| *s.body.clone()) |
| 30f1008 | 75 | .ok_or_else(|| format!("undefined name '{}'", name)) |
| 30f1008 | 76 | } |
| 30f1008 | 77 | |
| d1a4183 | 78 | /// Field names and types for every `type ClassName = ...` declaration in the source. |
| d1a4183 | 79 | pub type ClassEnv = BTreeMap<String, Vec<(String, PlumType)>>; |
| d1a4183 | 80 | /// `(receiver type, method name) -> TFun` for every `name<Receiver>(...)` method. |
| d1a4183 | 81 | pub type MethodEnv = BTreeMap<(String, String), PlumType>; |
| 7ac1d37 | 82 | /// Info about one `enum` variant: which enum it belongs to, its 0-based runtime tag |
| 7ac1d37 | 83 | /// (numbering is shared across all of that enum's variants), and its payload field |
| 7ac1d37 | 84 | /// types (empty for a payload-free variant like `Red` or `None`). |
| 7ac1d37 | 85 | #[derive(Debug, Clone, PartialEq)] |
| 7ac1d37 | 86 | pub struct EnumVariantInfo { |
| 7ac1d37 | 87 | pub enum_name: String, |
| 7ac1d37 | 88 | pub tag: i32, |
| 7ac1d37 | 89 | pub field_types: Vec<PlumType>, |
| 4fda634 | 90 | pub values: Vec<ast::Expr>, |
| 7ac1d37 | 91 | } |
| 7ac1d37 | 92 | /// Enum variant name -> its info, e.g. `"True" -> { enum_name: "Bool", tag: 1, field_types: [] }`. |
| 7ac1d37 | 93 | pub type EnumVariants = BTreeMap<String, EnumVariantInfo>; |
| d1a4183 | 94 | |
| 4fda634 | 95 | /// Field names and types for every discriminant enum's shared params (`enum Foo(n: Int) = ...`), |
| 4fda634 | 96 | /// keyed by the ENUM's name (not a variant name) — e.g. `"Step" -> [("n", TInt)]`. Field |
| 4fda634 | 97 | /// access on a value of this type must load/store at offset `(field_idx + 1) * 8`, NOT |
| 4fda634 | 98 | /// `field_idx * 8` like a class — slot 0 is always the variant's tag. |
| 4fda634 | 99 | pub type EnumParams = BTreeMap<String, Vec<(String, PlumType)>>; |
| 4fda634 | 100 | |
| d1a4183 | 101 | /// Shared, read-only lookup tables built once from the whole source, threaded through |
| d1a4183 | 102 | /// every check/infer call alongside the (mutable, scope-local) `TypeEnv`. |
| d1a4183 | 103 | pub struct CheckCtx<'a> { |
| d1a4183 | 104 | pub classes: &'a ClassEnv, |
| d1a4183 | 105 | pub methods: &'a MethodEnv, |
| d1a4183 | 106 | pub enum_variants: &'a EnumVariants, |
| 4fda634 | 107 | pub enum_params: &'a EnumParams, |
| d1a4183 | 108 | } |
| d1a4183 | 109 | |
| d1a4183 | 110 | /// Builds the global lookup tables (function/const signatures, class fields, method |
| 3d6f280 | 111 | /// signatures, enum variants) from a whole source. Shared by `checkSource` and by |
| d1a4183 | 112 | /// `plum-wasm-codegen`, which needs the same tables to resolve `self`, field access, |
| d1a4183 | 113 | /// and method dispatch during code generation. |
| 3d6f280 | 114 | pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, ClassEnv, MethodEnv, EnumVariants, EnumParams) { |
| 30f1008 | 115 | let mut global_env: TypeEnv = TypeEnv::new(); |
| d1a4183 | 116 | let mut classes: ClassEnv = BTreeMap::new(); |
| d1a4183 | 117 | let mut methods: MethodEnv = BTreeMap::new(); |
| d1a4183 | 118 | let mut enum_variants: EnumVariants = BTreeMap::new(); |
| 4fda634 | 119 | let mut enum_params: EnumParams = BTreeMap::new(); |
| 3d6f280 | 120 | // `Bool`'s variants are built in (see `inferExpr`'s TypeName handling) rather |
| d1a4183 | 121 | // than requiring every source file to redeclare `enum Bool = | True | False`. |
| 4fda634 | 122 | enum_variants.insert("True".to_string(), EnumVariantInfo { enum_name: "Bool".to_string(), tag: 1, field_types: vec![], values: vec![] }); |
| 4fda634 | 123 | enum_variants.insert("False".to_string(), EnumVariantInfo { enum_name: "Bool".to_string(), tag: 0, field_types: vec![], values: vec![] }); |
| d1a4183 | 124 | |
| d1a4183 | 125 | // First pass: register class fields and enum variants so later passes can |
| d1a4183 | 126 | // resolve `self.field`, `ClassName(...)`, and bare enum-tag patterns. |
| d1a4183 | 127 | for item in &source.items { |
| d1a4183 | 128 | match item { |
| d1a4183 | 129 | ast::Item::Class(c) => { |
| d1a4183 | 130 | let fields = c.fields.iter() |
| 3d6f280 | 131 | .map(|f| (f.name.clone(), plumTypeFromAst(&f.ty))) |
| d1a4183 | 132 | .collect(); |
| d1a4183 | 133 | classes.insert(c.name.clone(), fields); |
| d1a4183 | 134 | } |
| 0000000 | 135 | // `Bool` may be re-"declared" (`enum Bool = | True | False`) purely to |
| 0000000 | 136 | // give it a nesting site for methods (`and`/`or`/`parse`/...) — the |
| 0000000 | 137 | // language currently has no other way to attach a method to a builtin |
| 0000000 | 138 | // type (see `libs/std/int.plum`/`float.plum`'s own `type Int =`/`type |
| 0000000 | 139 | // Float =` for the same pattern). Re-registering its variants here |
| 0000000 | 140 | // would silently overwrite the hardcoded tags above with whatever |
| 0000000 | 141 | // order this declaration happens to list them in, flipping every |
| 0000000 | 142 | // `True`/`False` tag used throughout the rest of the codegen. Skip. |
| 0000000 | 143 | ast::Item::Enum(e) if e.name == "Bool" => {} |
| d1a4183 | 144 | ast::Item::Enum(e) => { |
| 4fda634 | 145 | let shared_field_types: Vec<PlumType> = e.params.iter() |
| 3d6f280 | 146 | .map(|p| plumTypeFromAst(&p.ty)) |
| 4fda634 | 147 | .collect(); |
| 4fda634 | 148 | if !e.params.is_empty() { |
| 4fda634 | 149 | let params = e.params.iter() |
| 3d6f280 | 150 | .map(|p| (p.name.clone(), plumTypeFromAst(&p.ty))) |
| 7ac1d37 | 151 | .collect(); |
| 4fda634 | 152 | enum_params.insert(e.name.clone(), params); |
| 4fda634 | 153 | } |
| 4fda634 | 154 | for (tag, v) in e.variants.iter().enumerate() { |
| 4fda634 | 155 | let field_types = if !e.params.is_empty() { |
| 4fda634 | 156 | shared_field_types.clone() |
| 4fda634 | 157 | } else { |
| 4fda634 | 158 | v.fields.iter() |
| 3d6f280 | 159 | .map(|f| plumTypeFromAst(&ast::Type { name: f.clone(), generics: vec![] })) |
| 4fda634 | 160 | .collect() |
| 4fda634 | 161 | }; |
| 7ac1d37 | 162 | enum_variants.insert(v.name.clone(), EnumVariantInfo { |
| 7ac1d37 | 163 | enum_name: e.name.clone(), |
| 7ac1d37 | 164 | tag: tag as i32, |
| 7ac1d37 | 165 | field_types, |
| 4fda634 | 166 | values: v.values.clone(), |
| 7ac1d37 | 167 | }); |
| d1a4183 | 168 | } |
| d1a4183 | 169 | } |
| d1a4183 | 170 | _ => {} |
| d1a4183 | 171 | } |
| d1a4183 | 172 | } |
| 30f1008 | 173 | |
| d1a4183 | 174 | // Second pass: register top-level function/method signatures and consts. |
| d1a4183 | 175 | // Methods (`name<Receiver>(...)`) live in `methods`, keyed by receiver type, |
| d1a4183 | 176 | // so a bare call can't accidentally resolve to some other type's method. |
| 30f1008 | 177 | for item in &source.items { |
| 30f1008 | 178 | match item { |
| 30f1008 | 179 | ast::Item::Fn(f) => { |
| 30f1008 | 180 | let param_types: Vec<PlumType> = f.params.iter().map(|p| { |
| 30f1008 | 181 | match &p.ty { |
| 3d6f280 | 182 | ast::ParamType::Type(t) => plumTypeFromAst(t), |
| 3d6f280 | 183 | ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plumTypeFromAst(t))), |
| d7e5ff4 | 184 | ast::ParamType::Fn(params, ret) => { |
| 3d6f280 | 185 | let param_types = params.iter().map(plumTypeFromAst).collect(); |
| 3d6f280 | 186 | let ret_ty = ret.as_ref().map(|r| plumTypeFromAst(r)).unwrap_or(PlumType::TUnit); |
| d7e5ff4 | 187 | PlumType::TFun(param_types, Box::new(ret_ty)) |
| d7e5ff4 | 188 | } |
| 30f1008 | 189 | } |
| 30f1008 | 190 | }).collect(); |
| 30f1008 | 191 | let ret = f.returns.as_ref() |
| 3d6f280 | 192 | .map(plumTypeFromAst) |
| 30f1008 | 193 | .unwrap_or(PlumType::TUnit); |
| d1a4183 | 194 | let fn_ty = PlumType::TFun(param_types, Box::new(ret)); |
| d1a4183 | 195 | if let Some(recv) = &f.type_param { |
| d1a4183 | 196 | methods.insert((recv.clone(), f.name.clone()), fn_ty); |
| d1a4183 | 197 | } else { |
| d1a4183 | 198 | global_env.insert(f.name.clone(), TypeScheme::mono(fn_ty)); |
| d1a4183 | 199 | } |
| 30f1008 | 200 | } |
| 30f1008 | 201 | ast::Item::Const(c) => { |
| 0000000 | 202 | // Top-level consts are always simple literals in practice; infer |
| 0000000 | 203 | // directly from the literal kind rather than pulling in the full |
| 0000000 | 204 | // `inferExpr` (which needs a `CheckCtx` this pass hasn't built yet). |
| 0000000 | 205 | let ty = match &c.value { |
| 0000000 | 206 | ast::Expr::Int(_) => PlumType::TInt, |
| 0000000 | 207 | ast::Expr::Float(_) => PlumType::TFloat, |
| 0000000 | 208 | ast::Expr::String(_) => PlumType::TStr, |
| 0000000 | 209 | _ => PlumType::TVar("_".to_string()), |
| 0000000 | 210 | }; |
| 0000000 | 211 | global_env.insert(c.name.clone(), TypeScheme::mono(ty)); |
| 30f1008 | 212 | } |
| 30f1008 | 213 | _ => {} |
| 30f1008 | 214 | } |
| 30f1008 | 215 | } |
| 30f1008 | 216 | |
| 4fda634 | 217 | (global_env, classes, methods, enum_variants, enum_params) |
| d1a4183 | 218 | } |
| d1a4183 | 219 | |
| 3d6f280 | 220 | pub fn checkSource(source: &ast::Source) -> CheckResult<()> { |
| 3d6f280 | 221 | let source = monomorphize::monomorphizeSource(source).map_err(|e| vec![CheckError { message: e }])?; |
| d1a4183 | 222 | let mut errors: Vec<CheckError> = Vec::new(); |
| 3d6f280 | 223 | let (global_env, classes, methods, enum_variants, enum_params) = buildGlobalTables(&source); |
| 4fda634 | 224 | let ctx = CheckCtx { classes: &classes, methods: &methods, enum_variants: &enum_variants, enum_params: &enum_params }; |
| d1a4183 | 225 | |
| 42d88a3 | 226 | // A name that is both a class and an enum variant is ambiguous: `Name(...)` |
| 42d88a3 | 227 | // could mean either construction, and downstream code (both the checker's |
| 3d6f280 | 228 | // `inferExpr` and codegen) consults `enum_variants` first, so the class |
| 42d88a3 | 229 | // constructor would be silently shadowed with no diagnostic. Reject it. |
| 42d88a3 | 230 | for name in classes.keys() { |
| 42d88a3 | 231 | if enum_variants.contains_key(name) { |
| 42d88a3 | 232 | errors.push(CheckError { |
| 42d88a3 | 233 | message: format!("'{}' is declared as both a class and an enum variant", name), |
| 42d88a3 | 234 | }); |
| 42d88a3 | 235 | } |
| 42d88a3 | 236 | } |
| 42d88a3 | 237 | |
| 4fda634 | 238 | // A discriminant enum (`enum Foo(n: Int) = ...`) requires every variant to supply |
| 4fda634 | 239 | // exactly one value per declared param, unified against that param's type. An |
| 4fda634 | 240 | // ordinary (param-less) enum must NOT have variants with values — most likely |
| 4fda634 | 241 | // caused by writing `Some(5)` where the generic-payload form `Some[Int]` was meant. |
| 4fda634 | 242 | for item in &source.items { |
| 4fda634 | 243 | if let ast::Item::Enum(e) = item { |
| 4fda634 | 244 | for v in &e.variants { |
| 4fda634 | 245 | if e.params.is_empty() { |
| 4fda634 | 246 | if !v.values.is_empty() { |
| 4fda634 | 247 | errors.push(CheckError { |
| 4fda634 | 248 | message: format!("enum '{}' variant '{}': has discriminant values but '{}' declares no params", e.name, v.name, e.name), |
| 4fda634 | 249 | }); |
| 4fda634 | 250 | } |
| 4fda634 | 251 | continue; |
| 4fda634 | 252 | } |
| 4fda634 | 253 | if v.values.len() != e.params.len() { |
| 4fda634 | 254 | errors.push(CheckError { |
| 4fda634 | 255 | message: format!( |
| 4fda634 | 256 | "enum '{}' variant '{}': expected {} discriminant value(s), got {}", |
| 4fda634 | 257 | e.name, v.name, e.params.len(), v.values.len() |
| 4fda634 | 258 | ), |
| 4fda634 | 259 | }); |
| 4fda634 | 260 | continue; |
| 4fda634 | 261 | } |
| 4fda634 | 262 | for (value, param) in v.values.iter().zip(e.params.iter()) { |
| 3d6f280 | 263 | match inferExpr(value, &global_env, &ctx) { |
| 4fda634 | 264 | Ok(actual) => { |
| 3d6f280 | 265 | let expected = plumTypeFromAst(¶m.ty); |
| 4fda634 | 266 | if let Err(msg) = unify(&expected, &actual) { |
| 4fda634 | 267 | errors.push(CheckError { |
| 4fda634 | 268 | message: format!("enum '{}' variant '{}': param '{}': {}", e.name, v.name, param.name, msg), |
| 4fda634 | 269 | }); |
| 4fda634 | 270 | } |
| 4fda634 | 271 | } |
| 4fda634 | 272 | Err(msg) => errors.push(CheckError { |
| 4fda634 | 273 | message: format!("enum '{}' variant '{}': param '{}': {}", e.name, v.name, param.name, msg), |
| 4fda634 | 274 | }), |
| 4fda634 | 275 | } |
| 4fda634 | 276 | } |
| 4fda634 | 277 | } |
| 4fda634 | 278 | } |
| 4fda634 | 279 | } |
| 4fda634 | 280 | |
| 30f1008 | 281 | for item in &source.items { |
| 30f1008 | 282 | if let ast::Item::Fn(f) = item { |
| 3d6f280 | 283 | let mut local_errors = checkFn(f, &global_env, &ctx); |
| 30f1008 | 284 | errors.append(&mut local_errors); |
| 30f1008 | 285 | } |
| 30f1008 | 286 | } |
| 30f1008 | 287 | |
| 30f1008 | 288 | if errors.is_empty() { Ok(()) } else { Err(errors) } |
| 30f1008 | 289 | } |
| 30f1008 | 290 | |
| 3d6f280 | 291 | fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError> { |
| 30f1008 | 292 | let mut errors = Vec::new(); |
| 0000000 | 293 | |
| 0000000 | 294 | // `is_extern` and `body == FnBody::Extern` must agree — either both (a genuine |
| 0000000 | 295 | // host-backed declaration) or neither (a normal Plum function). A mismatch here |
| 0000000 | 296 | // is always a parser artifact of a malformed declaration, not user intent, so |
| 0000000 | 297 | // report it plainly rather than trying to guess which side was "right". |
| 0000000 | 298 | match (f.is_extern, &f.body) { |
| 0000000 | 299 | (true, ast::FnBody::Extern) => { |
| 0000000 | 300 | if f.type_param.is_some() { |
| 0000000 | 301 | errors.push(CheckError { message: format!("fn '{}': extern functions can't be methods (no receiver)", f.name) }); |
| 0000000 | 302 | } |
| 0000000 | 303 | return errors; |
| 0000000 | 304 | } |
| 0000000 | 305 | (true, _) => { |
| 0000000 | 306 | errors.push(CheckError { message: format!("fn '{}': extern fn must not have a body", f.name) }); |
| 0000000 | 307 | return errors; |
| 0000000 | 308 | } |
| 0000000 | 309 | (false, ast::FnBody::Extern) => { |
| 0000000 | 310 | errors.push(CheckError { message: format!("fn '{}': missing a body (or mark it `extern`)", f.name) }); |
| 0000000 | 311 | return errors; |
| 0000000 | 312 | } |
| 0000000 | 313 | (false, _) => {} |
| 0000000 | 314 | } |
| 0000000 | 315 | |
| 30f1008 | 316 | let mut env = global_env.clone(); |
| 30f1008 | 317 | |
| 4a494a8 | 318 | let variadic_positions: Vec<usize> = f.params.iter().enumerate() |
| 4a494a8 | 319 | .filter(|(_, p)| matches!(p.ty, ast::ParamType::Variadic(_))) |
| 4a494a8 | 320 | .map(|(i, _)| i) |
| 4a494a8 | 321 | .collect(); |
| 4a494a8 | 322 | if variadic_positions.len() > 1 { |
| 4a494a8 | 323 | errors.push(CheckError { message: format!("fn '{}': at most one variadic parameter is allowed", f.name) }); |
| 4a494a8 | 324 | } else if let Some(&pos) = variadic_positions.first() { |
| 4a494a8 | 325 | if pos != f.params.len() - 1 { |
| 4a494a8 | 326 | errors.push(CheckError { message: format!("fn '{}': a variadic parameter must be last", f.name) }); |
| 4a494a8 | 327 | } |
| 4a494a8 | 328 | } |
| 4a494a8 | 329 | |
| d1a4183 | 330 | // Methods (`name<Receiver>(...)`) get an implicit `self: Receiver` binding. |
| d1a4183 | 331 | if let Some(recv) = &f.type_param { |
| 141de54 | 332 | let recv_ty = ast::Type { name: recv.clone(), generics: vec![] }; |
| 3d6f280 | 333 | env.insert("self".to_string(), TypeScheme::mono(plumTypeFromAst(&recv_ty))); |
| d1a4183 | 334 | } |
| d1a4183 | 335 | |
| 30f1008 | 336 | // Add params to env |
| 30f1008 | 337 | for p in &f.params { |
| 30f1008 | 338 | let ty = match &p.ty { |
| 3d6f280 | 339 | ast::ParamType::Type(t) => plumTypeFromAst(t), |
| 3d6f280 | 340 | ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plumTypeFromAst(t))), |
| d7e5ff4 | 341 | ast::ParamType::Fn(params, ret) => { |
| 3d6f280 | 342 | let param_types = params.iter().map(plumTypeFromAst).collect(); |
| 3d6f280 | 343 | let ret_ty = ret.as_ref().map(|r| plumTypeFromAst(r)).unwrap_or(PlumType::TUnit); |
| d7e5ff4 | 344 | PlumType::TFun(param_types, Box::new(ret_ty)) |
| d7e5ff4 | 345 | } |
| 30f1008 | 346 | }; |
| 30f1008 | 347 | env.insert(p.name.clone(), TypeScheme::mono(ty)); |
| 30f1008 | 348 | } |
| 30f1008 | 349 | |
| 30f1008 | 350 | let declared_ret = f.returns.as_ref() |
| 3d6f280 | 351 | .map(plumTypeFromAst) |
| 30f1008 | 352 | .unwrap_or(PlumType::TUnit); |
| 30f1008 | 353 | |
| 30f1008 | 354 | match &f.body { |
| 30f1008 | 355 | ast::FnBody::Expr(e) => { |
| 3d6f280 | 356 | match inferExpr(e, &env, ctx) { |
| 30f1008 | 357 | Ok(t) => { |
| 30f1008 | 358 | if let Err(msg) = unify(&declared_ret, &t) { |
| 30f1008 | 359 | errors.push(CheckError { message: format!("fn '{}': return type mismatch: {}", f.name, msg) }); |
| 30f1008 | 360 | } |
| 30f1008 | 361 | } |
| 30f1008 | 362 | Err(msg) => errors.push(CheckError { message: format!("fn '{}': {}", f.name, msg) }), |
| 30f1008 | 363 | } |
| 30f1008 | 364 | } |
| 30f1008 | 365 | ast::FnBody::Block(block) => { |
| 3d6f280 | 366 | let mut block_errors = checkBlock(block, &mut env, &declared_ret, &f.name, ctx); |
| 30f1008 | 367 | errors.append(&mut block_errors); |
| 729d7cb | 368 | // Check the type of the last expression statement against the declared return type |
| 729d7cb | 369 | if let Some(ast::Stmt::Expr(last_expr)) = block.stmts.last() { |
| 3d6f280 | 370 | match inferExpr(last_expr, &env, ctx) { |
| 729d7cb | 371 | Ok(t) => { |
| 729d7cb | 372 | if let Err(msg) = unify(&declared_ret, &t) { |
| 729d7cb | 373 | errors.push(CheckError { message: format!("fn '{}': return type mismatch: {}", f.name, msg) }); |
| 729d7cb | 374 | } |
| 729d7cb | 375 | } |
| 3d6f280 | 376 | Err(_) => {} // already reported by checkBlock |
| 729d7cb | 377 | } |
| 729d7cb | 378 | } |
| 30f1008 | 379 | } |
| 0000000 | 380 | // Ruled out (and returned early) by the `is_extern`/body coherence check above. |
| 0000000 | 381 | ast::FnBody::Extern => unreachable!("extern fn bodies are handled earlier in checkFn"), |
| 30f1008 | 382 | } |
| 30f1008 | 383 | errors |
| 30f1008 | 384 | } |
| 30f1008 | 385 | |
| 3d6f280 | 386 | fn checkBlock(block: &ast::Block, env: &mut TypeEnv, declared_ret: &PlumType, fn_name: &str, ctx: &CheckCtx) -> Vec<CheckError> { |
| 30f1008 | 387 | let mut errors = Vec::new(); |
| 30f1008 | 388 | for stmt in &block.stmts { |
| 3d6f280 | 389 | let mut stmt_errors = checkStmt(stmt, env, declared_ret, fn_name, ctx); |
| 30f1008 | 390 | errors.append(&mut stmt_errors); |
| 30f1008 | 391 | } |
| 30f1008 | 392 | errors |
| 30f1008 | 393 | } |
| 30f1008 | 394 | |
| 3d6f280 | 395 | fn describeTargetObject(expr: &ast::Expr) -> String { |
| 47abc49 | 396 | match expr { |
| 47abc49 | 397 | ast::Expr::Self_ => "self".to_string(), |
| 47abc49 | 398 | ast::Expr::Var(n) => n.clone(), |
| 47abc49 | 399 | ast::Expr::Attribute(a) => { |
| 47abc49 | 400 | if let ast::AttrKind::Field(f) = &a.attr { |
| 3d6f280 | 401 | format!("{}.{}", describeTargetObject(&a.object), f) |
| 47abc49 | 402 | } else { |
| 47abc49 | 403 | "<expr>".to_string() |
| 47abc49 | 404 | } |
| 47abc49 | 405 | } |
| 47abc49 | 406 | _ => "<expr>".to_string(), |
| 47abc49 | 407 | } |
| 47abc49 | 408 | } |
| 47abc49 | 409 | |
| 3d6f280 | 410 | fn checkStmt(stmt: &ast::Stmt, env: &mut TypeEnv, declared_ret: &PlumType, fn_name: &str, ctx: &CheckCtx) -> Vec<CheckError> { |
| 30f1008 | 411 | let mut errors = Vec::new(); |
| 30f1008 | 412 | match stmt { |
| 30f1008 | 413 | ast::Stmt::Assign(a) => { |
| 30f1008 | 414 | for (target, value) in a.targets.iter().zip(a.values.iter()) { |
| 47abc49 | 415 | match target { |
| 47abc49 | 416 | ast::AssignTarget::Var(name) => { |
| 0000000 | 417 | let already_declared = lookup(env, name).is_ok(); |
| 0000000 | 418 | if a.declare && already_declared { |
| 0000000 | 419 | errors.push(CheckError { |
| 0000000 | 420 | message: format!("fn '{}': assign '{}': already declared — use '=' to reassign, not ':='", fn_name, name), |
| 0000000 | 421 | }); |
| 0000000 | 422 | } else if !a.declare && already_declared { |
| 0000000 | 423 | // Reassignment: the new value's type must match what |
| 0000000 | 424 | // `name` was already bound to — this is the actual |
| 0000000 | 425 | // point of `:=` existing at all (catches `x := 5` |
| 0000000 | 426 | // then later `x = 3.14` at compile time instead of |
| 0000000 | 427 | // silently miscompiling or silently changing type). |
| 0000000 | 428 | match inferExpr(value, env, ctx) { |
| 0000000 | 429 | Ok(t) => { |
| 0000000 | 430 | let existing = lookup(env, name).expect("already_declared just confirmed this succeeds"); |
| 0000000 | 431 | if let Err(msg) = unify(&existing, &t) { |
| 0000000 | 432 | errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, name, msg) }); |
| 0000000 | 433 | } else if matches!(existing, PlumType::TVar(_)) { |
| 0000000 | 434 | // `unify` never narrows a `TVar` itself — |
| 0000000 | 435 | // once a concrete type is available, record |
| 0000000 | 436 | // it so later uses of `name` see it too. |
| 0000000 | 437 | env.insert(name.clone(), TypeScheme::mono(t)); |
| 0000000 | 438 | } |
| 0000000 | 439 | } |
| 0000000 | 440 | Err(msg) => errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, name, msg) }), |
| 0000000 | 441 | } |
| 0000000 | 442 | } else { |
| 0000000 | 443 | // First declaration — via `:=`, or (for backward |
| 0000000 | 444 | // compatibility with every existing bare `x = ...` |
| 0000000 | 445 | // first-use) via a plain `=` on a name not yet bound. |
| 0000000 | 446 | match inferExpr(value, env, ctx) { |
| 0000000 | 447 | Ok(t) => { env.insert(name.clone(), TypeScheme::mono(t)); } |
| 0000000 | 448 | Err(msg) => errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, name, msg) }), |
| 0000000 | 449 | } |
| 47abc49 | 450 | } |
| 47abc49 | 451 | } |
| 47abc49 | 452 | ast::AssignTarget::Field(object, field_name) => { |
| 3d6f280 | 453 | let label = format!("{}.{}", describeTargetObject(object), field_name); |
| 3d6f280 | 454 | match (inferExpr(object, env, ctx), inferExpr(value, env, ctx)) { |
| 47abc49 | 455 | (Ok(PlumType::TNamed(class_name)), Ok(value_ty)) => { |
| 47abc49 | 456 | match ctx.classes.get(&class_name).and_then(|fields| { |
| 47abc49 | 457 | fields.iter().find(|(n, _)| n == field_name).map(|(_, ty)| ty.clone()) |
| 47abc49 | 458 | }) { |
| 47abc49 | 459 | Some(field_ty) => { |
| 47abc49 | 460 | if let Err(msg) = unify(&field_ty, &value_ty) { |
| 47abc49 | 461 | errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, label, msg) }); |
| 47abc49 | 462 | } |
| 47abc49 | 463 | } |
| 47abc49 | 464 | None => errors.push(CheckError { message: format!("fn '{}': assign '{}': no field '{}' on class '{}'", fn_name, label, field_name, class_name) }), |
| 47abc49 | 465 | } |
| 47abc49 | 466 | } |
| 47abc49 | 467 | (Ok(other), Ok(_)) => errors.push(CheckError { message: format!("fn '{}': assign '{}': cannot access field on non-class type {}", fn_name, label, other) }), |
| 47abc49 | 468 | (Err(msg), _) => errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, label, msg) }), |
| 47abc49 | 469 | (_, Err(msg)) => errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, label, msg) }), |
| 47abc49 | 470 | } |
| 47abc49 | 471 | } |
| 30f1008 | 472 | } |
| 30f1008 | 473 | } |
| 30f1008 | 474 | } |
| 30f1008 | 475 | ast::Stmt::Return(Some(e)) => { |
| 3d6f280 | 476 | match inferExpr(e, env, ctx) { |
| 30f1008 | 477 | Ok(t) => { |
| 30f1008 | 478 | if let Err(msg) = unify(declared_ret, &t) { |
| 30f1008 | 479 | errors.push(CheckError { message: format!("fn '{}': return type mismatch: {}", fn_name, msg) }); |
| 30f1008 | 480 | } |
| 30f1008 | 481 | } |
| 30f1008 | 482 | Err(msg) => errors.push(CheckError { message: format!("fn '{}': return: {}", fn_name, msg) }), |
| 30f1008 | 483 | } |
| 30f1008 | 484 | } |
| 30f1008 | 485 | ast::Stmt::Return(None) => { |
| 30f1008 | 486 | if let Err(msg) = unify(declared_ret, &PlumType::TUnit) { |
| 30f1008 | 487 | errors.push(CheckError { message: format!("fn '{}': bare return in non-Unit function: {}", fn_name, msg) }); |
| 30f1008 | 488 | } |
| 30f1008 | 489 | } |
| 30f1008 | 490 | ast::Stmt::If(if_) => { |
| 3d6f280 | 491 | match inferExpr(&if_.condition, env, ctx) { |
| 30f1008 | 492 | Ok(t) => { |
| 30f1008 | 493 | if let Err(msg) = unify(&PlumType::TBool, &t) { |
| 30f1008 | 494 | errors.push(CheckError { message: format!("fn '{}': if condition must be Bool: {}", fn_name, msg) }); |
| 30f1008 | 495 | } |
| 30f1008 | 496 | } |
| 30f1008 | 497 | Err(msg) => errors.push(CheckError { message: format!("fn '{}': if condition: {}", fn_name, msg) }), |
| 30f1008 | 498 | } |
| 3d6f280 | 499 | errors.append(&mut checkBlock(&if_.body, env, declared_ret, fn_name, ctx)); |
| 30f1008 | 500 | for ei in &if_.else_ifs { |
| 3d6f280 | 501 | match inferExpr(&ei.condition, env, ctx) { |
| 30f1008 | 502 | Ok(t) => { |
| 30f1008 | 503 | if let Err(msg) = unify(&PlumType::TBool, &t) { |
| 30f1008 | 504 | errors.push(CheckError { message: format!("fn '{}': else if condition must be Bool: {}", fn_name, msg) }); |
| 30f1008 | 505 | } |
| 30f1008 | 506 | } |
| 30f1008 | 507 | Err(msg) => errors.push(CheckError { message: format!("fn '{}': else if condition: {}", fn_name, msg) }), |
| 30f1008 | 508 | } |
| 3d6f280 | 509 | errors.append(&mut checkBlock(&ei.body, env, declared_ret, fn_name, ctx)); |
| 30f1008 | 510 | } |
| 30f1008 | 511 | if let Some(else_block) = &if_.else_ { |
| 3d6f280 | 512 | errors.append(&mut checkBlock(else_block, env, declared_ret, fn_name, ctx)); |
| 30f1008 | 513 | } |
| 30f1008 | 514 | } |
| 30f1008 | 515 | ast::Stmt::While(w) => { |
| 3d6f280 | 516 | match inferExpr(&w.condition, env, ctx) { |
| 30f1008 | 517 | Ok(t) => { |
| 30f1008 | 518 | if let Err(msg) = unify(&PlumType::TBool, &t) { |
| 30f1008 | 519 | errors.push(CheckError { message: format!("fn '{}': while condition must be Bool: {}", fn_name, msg) }); |
| 30f1008 | 520 | } |
| 30f1008 | 521 | } |
| 30f1008 | 522 | Err(msg) => errors.push(CheckError { message: format!("fn '{}': while condition: {}", fn_name, msg) }), |
| 30f1008 | 523 | } |
| 3d6f280 | 524 | errors.append(&mut checkBlock(&w.body, env, declared_ret, fn_name, ctx)); |
| 30f1008 | 525 | } |
| 30f1008 | 526 | ast::Stmt::For(f_stmt) => { |
| 3d6f280 | 527 | let iter_ty = inferExpr(&f_stmt.iter, env, ctx); |
| 30f1008 | 528 | let mut inner_env = env.clone(); |
| 4a494a8 | 529 | match &iter_ty { |
| 4a494a8 | 530 | Ok(PlumType::TVariadic(elem)) => { |
| 4a494a8 | 531 | if f_stmt.vars.len() != 1 { |
| 4a494a8 | 532 | errors.push(CheckError { message: format!("fn '{}': for-loop over a variadic param must bind exactly one variable", fn_name) }); |
| 4a494a8 | 533 | } |
| 4a494a8 | 534 | for var in &f_stmt.vars { |
| 4a494a8 | 535 | inner_env.insert(var.clone(), TypeScheme::mono((**elem).clone())); |
| 4a494a8 | 536 | } |
| 4a494a8 | 537 | } |
| 4a494a8 | 538 | Ok(_) => { |
| 4a494a8 | 539 | for var in &f_stmt.vars { |
| 4a494a8 | 540 | inner_env.insert(var.clone(), TypeScheme::mono(PlumType::TInt)); |
| 4a494a8 | 541 | } |
| 4a494a8 | 542 | } |
| 4a494a8 | 543 | Err(msg) => { |
| 4a494a8 | 544 | errors.push(CheckError { message: format!("fn '{}': for iter: {}", fn_name, msg) }); |
| 4a494a8 | 545 | for var in &f_stmt.vars { |
| 4a494a8 | 546 | inner_env.insert(var.clone(), TypeScheme::mono(PlumType::TInt)); |
| 4a494a8 | 547 | } |
| 4a494a8 | 548 | } |
| 30f1008 | 549 | } |
| 3d6f280 | 550 | errors.append(&mut checkBlock(&f_stmt.body, &mut inner_env, declared_ret, fn_name, ctx)); |
| 30f1008 | 551 | } |
| 30f1008 | 552 | ast::Stmt::Expr(e) => { |
| 3d6f280 | 553 | if let Err(msg) = inferExpr(e, env, ctx) { |
| 30f1008 | 554 | errors.push(CheckError { message: format!("fn '{}': {}", fn_name, msg) }); |
| 30f1008 | 555 | } |
| 30f1008 | 556 | } |
| 30f1008 | 557 | ast::Stmt::Assert(e) => { |
| 3d6f280 | 558 | match inferExpr(e, env, ctx) { |
| 30f1008 | 559 | Ok(t) => { |
| 30f1008 | 560 | if let Err(msg) = unify(&PlumType::TBool, &t) { |
| 30f1008 | 561 | errors.push(CheckError { message: format!("fn '{}': assert must be Bool: {}", fn_name, msg) }); |
| 30f1008 | 562 | } |
| 30f1008 | 563 | } |
| 30f1008 | 564 | Err(msg) => errors.push(CheckError { message: format!("fn '{}': assert: {}", fn_name, msg) }), |
| 30f1008 | 565 | } |
| 30f1008 | 566 | } |
| d1a4183 | 567 | ast::Stmt::Match(m) => { |
| 3d6f280 | 568 | errors.append(&mut checkMatch(m, env, declared_ret, fn_name, ctx)); |
| d1a4183 | 569 | } |
| 30f1008 | 570 | ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {} |
| 30f1008 | 571 | } |
| 30f1008 | 572 | errors |
| 30f1008 | 573 | } |
| 30f1008 | 574 | |
| 3d6f280 | 575 | fn checkMatch(m: &ast::Match, env: &TypeEnv, declared_ret: &PlumType, fn_name: &str, ctx: &CheckCtx) -> Vec<CheckError> { |
| d1a4183 | 576 | let mut errors = Vec::new(); |
| d1a4183 | 577 | |
| d1a4183 | 578 | let mut subject_types: Vec<PlumType> = Vec::new(); |
| d1a4183 | 579 | for s in &m.subjects { |
| 3d6f280 | 580 | match inferExpr(s, env, ctx) { |
| d1a4183 | 581 | Ok(t) => subject_types.push(t), |
| d1a4183 | 582 | Err(msg) => errors.push(CheckError { message: format!("fn '{}': match subject: {}", fn_name, msg) }), |
| d1a4183 | 583 | } |
| d1a4183 | 584 | } |
| d1a4183 | 585 | if subject_types.len() != m.subjects.len() { |
| d1a4183 | 586 | return errors; // a subject failed to type — cases can't be checked meaningfully |
| d1a4183 | 587 | } |
| d1a4183 | 588 | |
| d1a4183 | 589 | for case in &m.cases { |
| d1a4183 | 590 | let mut case_env = env.clone(); |
| d1a4183 | 591 | if case.patterns.len() == subject_types.len() { |
| d1a4183 | 592 | for (pat, sty) in case.patterns.iter().zip(subject_types.iter()) { |
| 3d6f280 | 593 | if let Err(msg) = checkPattern(pat, sty, &mut case_env, ctx) { |
| d1a4183 | 594 | errors.push(CheckError { message: format!("fn '{}': match case: {}", fn_name, msg) }); |
| d1a4183 | 595 | } |
| d1a4183 | 596 | } |
| d1a4183 | 597 | } else { |
| d1a4183 | 598 | errors.push(CheckError { |
| d1a4183 | 599 | message: format!( |
| d1a4183 | 600 | "fn '{}': match case has {} pattern(s), expected {}", |
| d1a4183 | 601 | fn_name, case.patterns.len(), subject_types.len() |
| d1a4183 | 602 | ), |
| d1a4183 | 603 | }); |
| d1a4183 | 604 | } |
| 3d6f280 | 605 | errors.append(&mut checkBlock(&case.body, &mut case_env, declared_ret, fn_name, ctx)); |
| d1a4183 | 606 | } |
| d1a4183 | 607 | errors |
| d1a4183 | 608 | } |
| d1a4183 | 609 | |
| d1a4183 | 610 | /// Checks a single case pattern against the type of the subject it matches, binding any |
| d1a4183 | 611 | /// new names it introduces into `env`. Constructor-payload sub-patterns (`Some(x)`) bind |
| 7ac1d37 | 612 | /// against that variant's declared field types (see `EnumVariantInfo::field_types`). |
| 3d6f280 | 613 | fn checkPattern(pat: &ast::CasePattern, subject_ty: &PlumType, env: &mut TypeEnv, ctx: &CheckCtx) -> Result<(), String> { |
| d1a4183 | 614 | match pat { |
| d1a4183 | 615 | ast::CasePattern::Wildcard => Ok(()), |
| d1a4183 | 616 | ast::CasePattern::Int(_) => unify(subject_ty, &PlumType::TInt), |
| d1a4183 | 617 | ast::CasePattern::Float(_) => unify(subject_ty, &PlumType::TFloat), |
| d1a4183 | 618 | ast::CasePattern::String(_) => unify(subject_ty, &PlumType::TStr), |
| d1a4183 | 619 | ast::CasePattern::Name(n) => { |
| d1a4183 | 620 | let is_known_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) |
| d1a4183 | 621 | && ctx.enum_variants.contains_key(n); |
| d1a4183 | 622 | if is_known_variant { |
| d1a4183 | 623 | Ok(()) // equality check against a known enum tag, e.g. `True` |
| d1a4183 | 624 | } else { |
| d1a4183 | 625 | env.insert(n.clone(), TypeScheme::mono(subject_ty.clone())); |
| d1a4183 | 626 | Ok(()) |
| d1a4183 | 627 | } |
| d1a4183 | 628 | } |
| 7ac1d37 | 629 | ast::CasePattern::Class { name, fields } => match ctx.enum_variants.get(name) { |
| 7ac1d37 | 630 | Some(info) => { |
| 7ac1d37 | 631 | if fields.len() != info.field_types.len() { |
| 7ac1d37 | 632 | return Err(format!( |
| 7ac1d37 | 633 | "constructor pattern '{}' expects {} field(s), got {}", |
| 7ac1d37 | 634 | name, info.field_types.len(), fields.len() |
| 7ac1d37 | 635 | )); |
| 7ac1d37 | 636 | } |
| 7ac1d37 | 637 | for (f, fty) in fields.iter().zip(info.field_types.iter()) { |
| 3d6f280 | 638 | checkPattern(f, fty, env, ctx)?; |
| 7ac1d37 | 639 | } |
| 7ac1d37 | 640 | Ok(()) |
| d1a4183 | 641 | } |
| 7ac1d37 | 642 | // Unmodeled/builtin variant: allow, codegen will catch. |
| 7ac1d37 | 643 | None => { |
| 7ac1d37 | 644 | for f in fields { |
| 3d6f280 | 645 | checkPattern(f, &PlumType::TVar("_".to_string()), env, ctx)?; |
| 7ac1d37 | 646 | } |
| 7ac1d37 | 647 | Ok(()) |
| 7ac1d37 | 648 | } |
| 7ac1d37 | 649 | }, |
| d1a4183 | 650 | } |
| d1a4183 | 651 | } |
| d1a4183 | 652 | |
| 3d6f280 | 653 | pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<PlumType, String> { |
| 30f1008 | 654 | match expr { |
| 30f1008 | 655 | ast::Expr::Int(_) => Ok(PlumType::TInt), |
| 30f1008 | 656 | ast::Expr::Float(_) => Ok(PlumType::TFloat), |
| 30f1008 | 657 | ast::Expr::String(_) => Ok(PlumType::TStr), |
| 30f1008 | 658 | ast::Expr::Var(name) => lookup(env, name), |
| d1a4183 | 659 | ast::Expr::Self_ => lookup(env, "self"), |
| 0000000 | 660 | // A bare `NAME` lexes as a type_identifier whenever it starts with an |
| 0000000 | 661 | // uppercase letter — which includes every SCREAMING_CASE top-level |
| 0000000 | 662 | // const (`PI`, `MAX_VALUE`, ...), since there's no separate "constant" |
| 0000000 | 663 | // token. Prefer a matching const's real type over the enum-variant/ |
| 0000000 | 664 | // unmodeled-type-name fallbacks below. |
| d1a4183 | 665 | ast::Expr::TypeName(n) => match n.as_str() { |
| d1a4183 | 666 | "True" | "False" => Ok(PlumType::TBool), |
| 0000000 | 667 | _ => match lookup(env, n) { |
| 0000000 | 668 | Ok(ty) if !matches!(ty, PlumType::TVar(_)) => Ok(ty), |
| 0000000 | 669 | _ => match ctx.enum_variants.get(n) { |
| 0000000 | 670 | Some(info) => Ok(PlumType::TNamed(info.enum_name.clone())), |
| 0000000 | 671 | // Unmodeled/builtin type name: allow, codegen will catch. |
| 0000000 | 672 | None => Ok(PlumType::TNamed(n.to_string())), |
| 0000000 | 673 | }, |
| 7ac1d37 | 674 | }, |
| d1a4183 | 675 | }, |
| 3d6f280 | 676 | ast::Expr::Paren(inner) => inferExpr(inner, env, ctx), |
| d1ea2ff | 677 | ast::Expr::Closure(cl) => { |
| d1ea2ff | 678 | let mut closure_env = env.clone(); |
| d1ea2ff | 679 | let param_types: Vec<PlumType> = cl.params.iter().map(|p| { |
| d1ea2ff | 680 | let t = PlumType::TVar(format!("_closure_{}", p)); |
| d1ea2ff | 681 | closure_env.insert(p.clone(), TypeScheme::mono(t.clone())); |
| d1ea2ff | 682 | t |
| d1ea2ff | 683 | }).collect(); |
| d1ea2ff | 684 | let body_ty = match &cl.body.stmts.last() { |
| 3d6f280 | 685 | Some(ast::Stmt::Expr(e)) => inferExpr(e, &closure_env, ctx)?, |
| 3d6f280 | 686 | Some(ast::Stmt::Return(Some(e))) => inferExpr(e, &closure_env, ctx)?, |
| d1ea2ff | 687 | _ => PlumType::TUnit, |
| d1ea2ff | 688 | }; |
| d1ea2ff | 689 | Ok(PlumType::TFun(param_types, Box::new(body_ty))) |
| d1ea2ff | 690 | } |
| 30f1008 | 691 | ast::Expr::Not(inner) => { |
| 3d6f280 | 692 | let t = inferExpr(inner, env, ctx)?; |
| 30f1008 | 693 | unify(&PlumType::TBool, &t)?; |
| 30f1008 | 694 | Ok(PlumType::TBool) |
| 30f1008 | 695 | } |
| 3d6f280 | 696 | ast::Expr::Unary(u) => inferExpr(&u.operand, env, ctx), |
| 30f1008 | 697 | ast::Expr::Binary(b) => { |
| 3d6f280 | 698 | let lt = inferExpr(&b.left, env, ctx)?; |
| 3d6f280 | 699 | let rt = inferExpr(&b.right, env, ctx)?; |
| 30f1008 | 700 | unify(<, &rt).map_err(|e| format!("binary op: {}", e))?; |
| 0000000 | 701 | Ok(lt) |
| 30f1008 | 702 | } |
| 30f1008 | 703 | ast::Expr::Bool(b) => { |
| 3d6f280 | 704 | let lt = inferExpr(&b.left, env, ctx)?; |
| 3d6f280 | 705 | let rt = inferExpr(&b.right, env, ctx)?; |
| 30f1008 | 706 | unify(&PlumType::TBool, <).map_err(|e| format!("bool op left: {}", e))?; |
| 30f1008 | 707 | unify(&PlumType::TBool, &rt).map_err(|e| format!("bool op right: {}", e))?; |
| 30f1008 | 708 | Ok(PlumType::TBool) |
| 30f1008 | 709 | } |
| 30f1008 | 710 | ast::Expr::Compare(c) => { |
| 3d6f280 | 711 | let lt = inferExpr(&c.left, env, ctx)?; |
| 3d6f280 | 712 | let rt = inferExpr(&c.right, env, ctx)?; |
| 30f1008 | 713 | unify(<, &rt).map_err(|e| format!("compare op: {}", e))?; |
| 30f1008 | 714 | Ok(PlumType::TBool) |
| 30f1008 | 715 | } |
| 30f1008 | 716 | ast::Expr::Ternary(t) => { |
| 3d6f280 | 717 | let ct = inferExpr(&t.condition, env, ctx)?; |
| 30f1008 | 718 | unify(&PlumType::TBool, &ct).map_err(|e| format!("ternary condition: {}", e))?; |
| 3d6f280 | 719 | let tt = inferExpr(&t.then, env, ctx)?; |
| 3d6f280 | 720 | let et = inferExpr(&t.else_, env, ctx)?; |
| 30f1008 | 721 | unify(&tt, &et).map_err(|e| format!("ternary branches: {}", e))?; |
| 30f1008 | 722 | Ok(tt) |
| 30f1008 | 723 | } |
| 30f1008 | 724 | ast::Expr::FnCall(call) => { |
| 0000000 | 725 | // `Int(x)`/`Float(x)`/`Byte(x)` are builtin numeric conversions, not |
| 0000000 | 726 | // ordinary calls — handled here so `y = Float(x)` unifies against |
| 0000000 | 727 | // `TFloat` rather than falling through to the permissive |
| 0000000 | 728 | // "unknown fn" case. |
| 0000000 | 729 | if (call.name == "Int" || call.name == "Float" || call.name == "Byte") && call.args.len() == 1 { |
| 0000000 | 730 | let arg_expr = match &call.args[0] { |
| 0000000 | 731 | ast::Arg::Positional(e) => e, |
| 0000000 | 732 | ast::Arg::Keyword { value, .. } => value, |
| 0000000 | 733 | ast::Arg::Pair { value, .. } => value, |
| 0000000 | 734 | }; |
| 0000000 | 735 | let actual = inferExpr(arg_expr, env, ctx)?; |
| 0000000 | 736 | return match (call.name.as_str(), &actual) { |
| 0000000 | 737 | ("Float", PlumType::TInt) => Ok(PlumType::TFloat), |
| 0000000 | 738 | ("Int", PlumType::TFloat) => Ok(PlumType::TInt), |
| 0000000 | 739 | ("Float", PlumType::TFloat) | ("Int", PlumType::TInt) => Ok(actual), |
| 0000000 | 740 | ("Byte", PlumType::TInt) | ("Byte", PlumType::TByte) => Ok(PlumType::TByte), |
| 0000000 | 741 | ("Int", PlumType::TByte) => Ok(PlumType::TInt), |
| 0000000 | 742 | (name, other) => Err(format!("call '{}': cannot convert {} to {}", name, other, name)), |
| 0000000 | 743 | }; |
| 0000000 | 744 | } |
| 7ac1d37 | 745 | if let Some(info) = ctx.enum_variants.get(&call.name) { |
| 7ac1d37 | 746 | if call.args.len() != info.field_types.len() { |
| 7ac1d37 | 747 | return Err(format!( |
| 7ac1d37 | 748 | "variant '{}': expected {} arg(s), got {}", |
| 7ac1d37 | 749 | call.name, info.field_types.len(), call.args.len() |
| 7ac1d37 | 750 | )); |
| 7ac1d37 | 751 | } |
| 7ac1d37 | 752 | for (i, (arg, expected)) in call.args.iter().zip(info.field_types.iter()).enumerate() { |
| 7ac1d37 | 753 | let arg_expr = match arg { |
| 7ac1d37 | 754 | ast::Arg::Positional(e) => e, |
| 7ac1d37 | 755 | ast::Arg::Keyword { value, .. } => value, |
| 7ac1d37 | 756 | ast::Arg::Pair { value, .. } => value, |
| 7ac1d37 | 757 | }; |
| 3d6f280 | 758 | let actual = inferExpr(arg_expr, env, ctx)?; |
| 7ac1d37 | 759 | unify(expected, &actual).map_err(|e| format!("variant '{}' arg {}: {}", call.name, i, e))?; |
| 7ac1d37 | 760 | } |
| 7ac1d37 | 761 | return Ok(PlumType::TNamed(info.enum_name.clone())); |
| 7ac1d37 | 762 | } |
| 30f1008 | 763 | match lookup(env, &call.name) { |
| 30f1008 | 764 | Ok(PlumType::TFun(param_types, ret)) => { |
| 4a494a8 | 765 | match param_types.last() { |
| 4a494a8 | 766 | Some(PlumType::TVariadic(elem)) => { |
| 4a494a8 | 767 | let fixed = ¶m_types[..param_types.len() - 1]; |
| 4a494a8 | 768 | if call.args.len() < fixed.len() { |
| 4a494a8 | 769 | return Err(format!("call '{}': expected at least {} arg(s), got {}", call.name, fixed.len(), call.args.len())); |
| 4a494a8 | 770 | } |
| 4a494a8 | 771 | for (i, (arg, expected)) in call.args.iter().zip(fixed.iter()).enumerate() { |
| 4a494a8 | 772 | let arg_expr = match arg { |
| 4a494a8 | 773 | ast::Arg::Positional(e) => e, |
| 4a494a8 | 774 | ast::Arg::Keyword { value, .. } => value, |
| 4a494a8 | 775 | ast::Arg::Pair { value, .. } => value, |
| 4a494a8 | 776 | }; |
| 3d6f280 | 777 | let actual = inferExpr(arg_expr, env, ctx)?; |
| 4a494a8 | 778 | unify(expected, &actual).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?; |
| 4a494a8 | 779 | } |
| 4a494a8 | 780 | for (i, arg) in call.args.iter().enumerate().skip(fixed.len()) { |
| 4a494a8 | 781 | let arg_expr = match arg { |
| 4a494a8 | 782 | ast::Arg::Positional(e) => e, |
| 4a494a8 | 783 | ast::Arg::Keyword { value, .. } => value, |
| 4a494a8 | 784 | ast::Arg::Pair { value, .. } => value, |
| 4a494a8 | 785 | }; |
| 3d6f280 | 786 | let actual = inferExpr(arg_expr, env, ctx)?; |
| 4a494a8 | 787 | unify(elem, &actual).map_err(|e| format!("call '{}' variadic arg {}: {}", call.name, i, e))?; |
| 4a494a8 | 788 | } |
| 4a494a8 | 789 | Ok(*ret) |
| 4a494a8 | 790 | } |
| 4a494a8 | 791 | _ => { |
| 4a494a8 | 792 | if call.args.len() != param_types.len() { |
| 4a494a8 | 793 | return Err(format!("call '{}': expected {} args, got {}", call.name, param_types.len(), call.args.len())); |
| 4a494a8 | 794 | } |
| 4a494a8 | 795 | for (i, (arg, expected)) in call.args.iter().zip(param_types.iter()).enumerate() { |
| 4a494a8 | 796 | let arg_expr = match arg { |
| 4a494a8 | 797 | ast::Arg::Positional(e) => e, |
| 4a494a8 | 798 | ast::Arg::Keyword { value, .. } => value, |
| 4a494a8 | 799 | ast::Arg::Pair { value, .. } => value, |
| 4a494a8 | 800 | }; |
| 3d6f280 | 801 | let actual = inferExpr(arg_expr, env, ctx)?; |
| 4a494a8 | 802 | unify(expected, &actual).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?; |
| 4a494a8 | 803 | } |
| 4a494a8 | 804 | Ok(*ret) |
| 4a494a8 | 805 | } |
| 30f1008 | 806 | } |
| 30f1008 | 807 | } |
| 30f1008 | 808 | Ok(_) => Err(format!("'{}' is not a function", call.name)), |
| 30f1008 | 809 | Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch |
| 30f1008 | 810 | } |
| 30f1008 | 811 | } |
| d1a4183 | 812 | ast::Expr::ClassCall(call) => { |
| d1a4183 | 813 | match ctx.classes.get(&call.type_name) { |
| d1a4183 | 814 | Some(fields) => { |
| d1a4183 | 815 | for fa in &call.fields { |
| d1a4183 | 816 | match fields.iter().find(|(n, _)| n == &fa.name) { |
| d1a4183 | 817 | Some((_, expected)) => { |
| 3d6f280 | 818 | let actual = inferExpr(&fa.value, env, ctx)?; |
| d1a4183 | 819 | unify(expected, &actual) |
| d1a4183 | 820 | .map_err(|e| format!("class '{}' field '{}': {}", call.type_name, fa.name, e))?; |
| d1a4183 | 821 | } |
| d1a4183 | 822 | None => return Err(format!("unknown field '{}' on class '{}'", fa.name, call.type_name)), |
| d1a4183 | 823 | } |
| d1a4183 | 824 | } |
| d1a4183 | 825 | Ok(PlumType::TNamed(call.type_name.clone())) |
| d1a4183 | 826 | } |
| d1a4183 | 827 | // Unmodeled (e.g. builtin/std) type: allow, codegen will catch. |
| d1a4183 | 828 | None => Ok(PlumType::TNamed(call.type_name.clone())), |
| d1a4183 | 829 | } |
| d1a4183 | 830 | } |
| d1a4183 | 831 | ast::Expr::Attribute(attr) => { |
| 3d6f280 | 832 | let obj_ty = inferExpr(&attr.object, env, ctx)?; |
| d1a4183 | 833 | match &attr.attr { |
| d1a4183 | 834 | ast::AttrKind::Field(field_name) => match &obj_ty { |
| d1a4183 | 835 | PlumType::TNamed(class_name) => match ctx.classes.get(class_name) { |
| d1a4183 | 836 | Some(fields) => fields.iter() |
| d1a4183 | 837 | .find(|(n, _)| n == field_name) |
| d1a4183 | 838 | .map(|(_, t)| t.clone()) |
| d1a4183 | 839 | .ok_or_else(|| format!("no field '{}' on type '{}'", field_name, class_name)), |
| 4fda634 | 840 | None => match ctx.enum_params.get(class_name) { |
| 4fda634 | 841 | Some(params) => params.iter() |
| 4fda634 | 842 | .find(|(n, _)| n == field_name) |
| 4fda634 | 843 | .map(|(_, t)| t.clone()) |
| 4fda634 | 844 | .ok_or_else(|| format!("no field '{}' on type '{}'", field_name, class_name)), |
| 4fda634 | 845 | // Unmodeled type: allow, codegen will catch. |
| 4fda634 | 846 | None => Ok(PlumType::TVar("_".to_string())), |
| 4fda634 | 847 | }, |
| d1a4183 | 848 | }, |
| d1a4183 | 849 | _ => Err(format!("cannot access field '{}' on non-class type {}", field_name, obj_ty)), |
| d1a4183 | 850 | }, |
| 0000000 | 851 | ast::AttrKind::Method(call) => match methodReceiverName(&obj_ty) { |
| 0000000 | 852 | Some(class_name) => match ctx.methods.get(&(class_name.clone(), call.name.clone())) { |
| d1a4183 | 853 | Some(PlumType::TFun(param_types, ret)) => { |
| 0000000 | 854 | match param_types.last() { |
| 0000000 | 855 | Some(PlumType::TVariadic(elem)) => { |
| 0000000 | 856 | let fixed = ¶m_types[..param_types.len() - 1]; |
| 0000000 | 857 | if call.args.len() < fixed.len() { |
| 0000000 | 858 | return Err(format!( |
| 0000000 | 859 | "method '{}.{}': expected at least {} arg(s), got {}", |
| 0000000 | 860 | class_name, call.name, fixed.len(), call.args.len() |
| 0000000 | 861 | )); |
| 0000000 | 862 | } |
| 0000000 | 863 | for (i, (arg, expected)) in call.args.iter().zip(fixed.iter()).enumerate() { |
| 0000000 | 864 | let arg_expr = match arg { |
| 0000000 | 865 | ast::Arg::Positional(e) => e, |
| 0000000 | 866 | ast::Arg::Keyword { value, .. } => value, |
| 0000000 | 867 | ast::Arg::Pair { value, .. } => value, |
| 0000000 | 868 | }; |
| 0000000 | 869 | let actual = inferExpr(arg_expr, env, ctx)?; |
| 0000000 | 870 | unify(expected, &actual) |
| 0000000 | 871 | .map_err(|e| format!("method '{}.{}' arg {}: {}", class_name, call.name, i, e))?; |
| 0000000 | 872 | } |
| 0000000 | 873 | for (i, arg) in call.args.iter().enumerate().skip(fixed.len()) { |
| 0000000 | 874 | let arg_expr = match arg { |
| 0000000 | 875 | ast::Arg::Positional(e) => e, |
| 0000000 | 876 | ast::Arg::Keyword { value, .. } => value, |
| 0000000 | 877 | ast::Arg::Pair { value, .. } => value, |
| 0000000 | 878 | }; |
| 0000000 | 879 | let actual = inferExpr(arg_expr, env, ctx)?; |
| 0000000 | 880 | unify(elem, &actual) |
| 0000000 | 881 | .map_err(|e| format!("method '{}.{}' variadic arg {}: {}", class_name, call.name, i, e))?; |
| 0000000 | 882 | } |
| 0000000 | 883 | Ok(*ret.clone()) |
| 0000000 | 884 | } |
| 0000000 | 885 | _ => { |
| 0000000 | 886 | if call.args.len() != param_types.len() { |
| 0000000 | 887 | return Err(format!( |
| 0000000 | 888 | "method '{}.{}': expected {} args, got {}", |
| 0000000 | 889 | class_name, call.name, param_types.len(), call.args.len() |
| 0000000 | 890 | )); |
| 0000000 | 891 | } |
| 0000000 | 892 | for (i, (arg, expected)) in call.args.iter().zip(param_types.iter()).enumerate() { |
| 0000000 | 893 | let arg_expr = match arg { |
| 0000000 | 894 | ast::Arg::Positional(e) => e, |
| 0000000 | 895 | ast::Arg::Keyword { value, .. } => value, |
| 0000000 | 896 | ast::Arg::Pair { value, .. } => value, |
| 0000000 | 897 | }; |
| 0000000 | 898 | let actual = inferExpr(arg_expr, env, ctx)?; |
| 0000000 | 899 | unify(expected, &actual) |
| 0000000 | 900 | .map_err(|e| format!("method '{}.{}' arg {}: {}", class_name, call.name, i, e))?; |
| 0000000 | 901 | } |
| 0000000 | 902 | Ok(*ret.clone()) |
| 0000000 | 903 | } |
| d1a4183 | 904 | } |
| d1a4183 | 905 | } |
| d1a4183 | 906 | // Unmodeled method (e.g. builtin/std): allow, codegen will catch. |
| d1a4183 | 907 | _ => Ok(PlumType::TVar("_".to_string())), |
| d1a4183 | 908 | }, |
| 0000000 | 909 | None => Ok(PlumType::TVar("_".to_string())), |
| d1a4183 | 910 | }, |
| d1a4183 | 911 | } |
| d1a4183 | 912 | } |
| 30f1008 | 913 | } |
| 30f1008 | 914 | } |