plum

#treesitter#compiler#wasm

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(&param.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(&lt, &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, &lt).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(&lt, &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 = &param_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 = &param_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
}