plum

#treesitter#compiler#wasm

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

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


eaf49cb — Peter John 2026-10-25T19:10:46+05:30
feat(checker): check generics structurally on the original source; add typedSource
plum-checker/src/lib.rs CHANGED
@@ -6,9 +6,264 @@ pub mod monomorphize;
6
6
  pub mod types;
7
7
 
8
8
  use plum_core::ast;
9
+ use std::cell::{Cell, RefCell};
9
- use std::collections::BTreeMap;
10
+ use std::collections::{BTreeMap, HashMap};
10
11
  use types::{CheckError, CheckResult, PlumType, TypeEnv, TypeScheme};
11
12
 
13
+ thread_local! {
14
+ /// Counter for the fresh `_t<N>` inference variables minted per generic call site.
15
+ static FRESH: Cell<u32> = const { Cell::new(0) };
16
+ /// When set, `inferExprWith` records every expression's type here (see `typedSource`).
17
+ static RECORD: RefCell<Option<HashMap<usize, PlumType>>> = const { RefCell::new(None) };
18
+ }
19
+
20
+ /// Per-expression instantiated types of a checked source, for codegen.
21
+ #[derive(Debug, Clone, Default)]
22
+ pub struct TypedInfo {
23
+ /// Expression address (`&ast::Expr as *const _ as usize`) -> its type.
24
+ pub expr_types: HashMap<usize, PlumType>,
25
+ }
26
+
27
+ fn freshVar() -> PlumType {
28
+ FRESH.with(|c| {
29
+ let n = c.get();
30
+ c.set(n + 1);
31
+ PlumType::TVar(format!("_t{}", n))
32
+ })
33
+ }
34
+
35
+ /// Binding of `_t<N>` inference variables to types, local to one call site.
36
+ type Binds = BTreeMap<String, PlumType>;
37
+
38
+ fn isInferVar(name: &str) -> bool {
39
+ name.starts_with("_t")
40
+ }
41
+
42
+ /// Replaces bound `_t<N>` variables (to a fixpoint, so chains resolve).
43
+ fn substVars(ty: &PlumType, b: &Binds) -> PlumType {
44
+ if b.is_empty() {
45
+ return ty.clone();
46
+ }
47
+ fn go(ty: &PlumType, b: &Binds, depth: u32) -> PlumType {
48
+ use PlumType::*;
49
+ match ty {
50
+ TVar(n) if depth < 8 => match b.get(n) {
51
+ Some(t) => go(t, b, depth + 1),
52
+ None => ty.clone(),
53
+ },
54
+ TApp(n, args) => TApp(n.clone(), args.iter().map(|a| go(a, b, depth)).collect()),
55
+ TFun(ps, r) => TFun(
56
+ ps.iter().map(|p| go(p, b, depth)).collect(),
57
+ Box::new(go(r, b, depth)),
58
+ ),
59
+ TVariadic(i) => TVariadic(Box::new(go(i, b, depth))),
60
+ TTags(tags) => TTags(
61
+ tags.iter()
62
+ .map(|(k, v)| (k.clone(), v.as_ref().map(|t| Box::new(go(t, b, depth)))))
63
+ .collect(),
64
+ ),
65
+ _ => ty.clone(),
66
+ }
67
+ }
68
+ go(ty, b, 0)
69
+ }
70
+
71
+ /// Free generic parameter names in `ty`, first-appearance order.
72
+ fn freeTParams(ty: &PlumType, out: &mut Vec<String>) {
73
+ use PlumType::*;
74
+ match ty {
75
+ TParam(n) => {
76
+ if !out.contains(n) {
77
+ out.push(n.clone());
78
+ }
79
+ }
80
+ TApp(_, args) => args.iter().for_each(|a| freeTParams(a, out)),
81
+ TFun(ps, r) => {
82
+ ps.iter().for_each(|p| freeTParams(p, out));
83
+ freeTParams(r, out);
84
+ }
85
+ TVariadic(i) => freeTParams(i, out),
86
+ TTags(tags) => tags
87
+ .values()
88
+ .flatten()
89
+ .for_each(|t| freeTParams(t, out)),
90
+ _ => {}
91
+ }
92
+ }
93
+
94
+ /// Instantiates every free `TParam` in `sig`: those named in `owner` take the
95
+ /// owner's (receiver's) type, all others get a fresh inference variable.
96
+ fn instSig(sig: &PlumType, owner: &BTreeMap<String, PlumType>) -> PlumType {
97
+ let mut names = Vec::new();
98
+ freeTParams(sig, &mut names);
99
+ let map: BTreeMap<String, PlumType> = names
100
+ .into_iter()
101
+ .map(|n| {
102
+ let t = owner.get(&n).cloned().unwrap_or_else(freshVar);
103
+ (n, t)
104
+ })
105
+ .collect();
106
+ types::substitute(sig, &map)
107
+ }
108
+
109
+ /// One-way matching: binds the inference variables of `pattern` from `actual`.
110
+ fn bindMatch(pattern: &PlumType, actual: &PlumType, b: &mut Binds) {
111
+ use PlumType::*;
112
+ match (pattern, actual) {
113
+ (TVar(n), a) if isInferVar(n) => {
114
+ if matches!(a, TVar(_)) {
115
+ return;
116
+ }
117
+ match b.get(n).cloned() {
118
+ None => {
119
+ b.insert(n.clone(), a.clone());
120
+ }
121
+ // A variable standing for a whole tag union widens to cover all
122
+ // the unions seen for it (E is covariant).
123
+ Some(TTags(old)) if matches!(a, TTags(_)) => {
124
+ let TTags(new) = a else { unreachable!() };
125
+ let mut merged = old;
126
+ for (k, v) in new {
127
+ merged.entry(k.clone()).or_insert_with(|| v.clone());
128
+ }
129
+ b.insert(n.clone(), TTags(merged));
130
+ }
131
+ Some(bound) => bindMatch(&bound, a, b),
132
+ }
133
+ }
134
+ (TApp(x, xs), TApp(y, ys)) if x == y && xs.len() == ys.len() => {
135
+ for (p, a) in xs.iter().zip(ys) {
136
+ bindMatch(p, a, b);
137
+ }
138
+ }
139
+ (TFun(ps, r), TFun(qs, s)) if ps.len() == qs.len() => {
140
+ for (p, a) in ps.iter().zip(qs) {
141
+ bindMatch(p, a, b);
142
+ }
143
+ bindMatch(r, s, b);
144
+ }
145
+ (TVariadic(p), TVariadic(a)) => bindMatch(p, a, b),
146
+ (TTags(p), TTags(a)) => {
147
+ for (tag, pp) in p {
148
+ if let (Some(pp), Some(Some(ap))) = (pp, a.get(tag)) {
149
+ bindMatch(pp, ap, b);
150
+ }
151
+ }
152
+ }
153
+ _ => {}
154
+ }
155
+ }
156
+
157
+ fn isGenericName(n: &str) -> bool {
158
+ monomorphize::isGenericParamName(n)
159
+ }
160
+
161
+ /// Structural type conversion for UN-monomorphized source: `List[Int]` ->
162
+ /// `TApp`, a single-uppercase-letter name (the grammar's generic parameter
163
+ /// spelling) -> `TParam`.
164
+ pub fn plumTypeFromAstG(ty: &ast::Type) -> PlumType {
165
+ if !ty.generics.is_empty() {
166
+ return PlumType::TApp(
167
+ ty.name.clone(),
168
+ ty.generics.iter().map(plumTypeFromAstG).collect(),
169
+ );
170
+ }
171
+ if isGenericName(&ty.name) {
172
+ return PlumType::TParam(ty.name.clone());
173
+ }
174
+ plumTypeFromName(&ty.name)
175
+ }
176
+
177
+ fn paramTypeG(pt: &ast::ParamType) -> PlumType {
178
+ match pt {
179
+ ast::ParamType::Type(t) => plumTypeFromAstG(t),
180
+ ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plumTypeFromAstG(t))),
181
+ ast::ParamType::Fn(params, ret) => PlumType::TFun(
182
+ params.iter().map(plumTypeFromAstG).collect(),
183
+ Box::new(
184
+ ret.as_ref()
185
+ .map(|r| plumTypeFromAstG(r))
186
+ .unwrap_or(PlumType::TUnit),
187
+ ),
188
+ ),
189
+ }
190
+ }
191
+
192
+ /// The generic parameter names of enum `name` (empty if non-generic/unknown).
193
+ fn enumGenericsOf<'a>(name: &str, ctx: &'a CheckCtx) -> &'a [String] {
194
+ ctx.enum_variants
195
+ .values()
196
+ .find(|i| i.enum_name == name)
197
+ .map(|i| i.generics.as_slice())
198
+ .unwrap_or(&[])
199
+ }
200
+
201
+ fn appSubst(generics: &[String], args: &[PlumType]) -> BTreeMap<String, PlumType> {
202
+ generics.iter().cloned().zip(args.iter().cloned()).collect()
203
+ }
204
+
205
+ /// A variant's payload field types with the enum's generics replaced by the
206
+ /// type arguments of `ty` (when it is a `TApp`).
207
+ fn variantFields(info: &EnumVariantInfo, ty: &PlumType) -> Vec<PlumType> {
208
+ match ty {
209
+ PlumType::TApp(_, args) if args.len() == info.generics.len() => {
210
+ let s = appSubst(&info.generics, args);
211
+ info.field_types
212
+ .iter()
213
+ .map(|f| types::substitute(f, &s))
214
+ .collect()
215
+ }
216
+ _ if info.generics.is_empty() => info.field_types.clone(),
217
+ // A generic variant seen through a non-parameterized type: unknown arguments.
218
+ _ => {
219
+ let s: BTreeMap<String, PlumType> = info
220
+ .generics
221
+ .iter()
222
+ .map(|g| (g.clone(), PlumType::TVar("_".to_string())))
223
+ .collect();
224
+ info.field_types
225
+ .iter()
226
+ .map(|f| types::substitute(f, &s))
227
+ .collect()
228
+ }
229
+ }
230
+ }
231
+
232
+ /// `Ok(3)`-style result type of a variant of enum `info.enum_name`.
233
+ fn enumTypeWith(info: &EnumVariantInfo, owner: &BTreeMap<String, PlumType>) -> PlumType {
234
+ if info.generics.is_empty() {
235
+ PlumType::TNamed(info.enum_name.clone())
236
+ } else {
237
+ PlumType::TApp(
238
+ info.enum_name.clone(),
239
+ info.generics
240
+ .iter()
241
+ .map(|g| owner.get(g).cloned().unwrap_or(PlumType::TVar("_".into())))
242
+ .collect(),
243
+ )
244
+ }
245
+ }
246
+
247
+ fn freshOwner(generics: &[String]) -> BTreeMap<String, PlumType> {
248
+ generics.iter().map(|g| (g.clone(), freshVar())).collect()
249
+ }
250
+
251
+ fn recordType(expr: &ast::Expr, ty: &PlumType) {
252
+ RECORD.with(|r| {
253
+ if let Some(m) = r.borrow_mut().as_mut() {
254
+ m.insert(expr as *const ast::Expr as usize, ty.clone());
255
+ }
256
+ });
257
+ }
258
+
259
+ /// Base name of a nominal type (`TNamed`/`TApp`).
260
+ fn nominalName(t: &PlumType) -> Option<&str> {
261
+ match t {
262
+ PlumType::TNamed(n) | PlumType::TApp(n, _) => Some(n),
263
+ _ => None,
264
+ }
265
+ }
266
+
12
267
  /// Maps a bare type name (as written in source: a param/return annotation, or
13
268
  /// a method's receiver name) to its `PlumType`. Builtin primitives get their
14
269
  /// dedicated variant; anything else is an unmodeled `TNamed`.
@@ -139,6 +394,13 @@ pub fn tagUnionSegmentsCompatible(expected: &str, actual: &str, ctx: &CheckCtx)
139
394
 
140
395
  /// Reserved `TypeEnv` key holding the enclosing function's declared return type.
141
396
  const RETURN_KEY: &str = "$return";
397
+ /// `TypeEnv` key prefix marking a name as a top-level function (a local of the
398
+ /// same name shadows it instead of being a redeclaration).
399
+ const GLOBAL_KEY: &str = "$global:";
400
+ /// `TypeEnv` key prefix for a trait method's signature (`$trait:Hashable:hash`).
401
+ const TRAIT_KEY: &str = "$trait:";
402
+ /// `TypeEnv` key prefix for a generic parameter's trait bound (`$bound:K:Hashable`).
403
+ const BOUND_KEY: &str = "$bound:";
142
404
 
143
405
  pub fn plumTypeFromAst(ty: &ast::Type) -> PlumType {
144
406
  plumTypeFromName(&ty.name)
@@ -152,7 +414,7 @@ pub fn plumTypeFromAst(ty: &ast::Type) -> PlumType {
152
414
  /// the same receiver name string `ctx.methods` is keyed by.
153
415
  pub fn methodReceiverName(ty: &PlumType) -> Option<String> {
154
416
  match ty {
155
- PlumType::TNamed(name) => Some(name.clone()),
417
+ PlumType::TNamed(name) | PlumType::TApp(name, _) => Some(name.clone()),
156
418
  PlumType::TInt => Some("Int".to_string()),
157
419
  PlumType::TFloat => Some("Float".to_string()),
158
420
  PlumType::TBool => Some("Bool".to_string()),
@@ -197,6 +459,33 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
197
459
  (PlumType::TByteSlice, PlumType::TByteSlice) => Ok(()),
198
460
  (PlumType::TUnit, PlumType::TUnit) => Ok(()),
199
461
  (PlumType::TNamed(a), PlumType::TNamed(b)) if a == b => Ok(()),
462
+ // Structural generics: same base, arguments unify pairwise (tag unions
463
+ // inside the arguments stay covariant through the TTags arm).
464
+ (PlumType::TApp(a, xs), PlumType::TApp(b, ys)) if a == b && xs.len() == ys.len() => {
465
+ for (x, y) in xs.iter().zip(ys.iter()) {
466
+ unify(x, y, ctx).map_err(|e| {
467
+ format!("type mismatch: expected {}, found {} ({})", t1, t2, e)
468
+ })?;
469
+ }
470
+ Ok(())
471
+ }
472
+ (PlumType::TParam(a), PlumType::TParam(b)) if a == b => Ok(()),
473
+ // A generic type used bare (`o: Link`) accepts any instantiation.
474
+ (PlumType::TApp(a, _), PlumType::TNamed(b))
475
+ | (PlumType::TNamed(a), PlumType::TApp(b, _))
476
+ if a == b =>
477
+ {
478
+ Ok(())
479
+ }
480
+ // A bare variant name as a type against its generic enum (either direction).
481
+ (PlumType::TApp(a, _), PlumType::TNamed(b)) | (PlumType::TNamed(b), PlumType::TApp(a, _))
482
+ if ctx
483
+ .enum_variants
484
+ .get(b)
485
+ .is_some_and(|info| &info.enum_name == a) =>
486
+ {
487
+ Ok(())
488
+ }
200
489
  // Tag-union subtyping: every tag of `actual` must exist in `expected`
201
490
  // with a unifying payload.
202
491
  (PlumType::TTags(exp), PlumType::TTags(act)) => {
@@ -274,6 +563,22 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
274
563
  }
275
564
  }
276
565
 
566
+ /// Return-position check: a value still typed by a bare generic parameter is
567
+ /// accepted for a concrete declared return (`fun get() -> Int = self.value` in
568
+ /// a `Box[T]` is only ever instantiated at `Int` by existing code), everything
569
+ /// else goes through `unifyArg`.
570
+ fn unifyReturn(
571
+ declared: &PlumType,
572
+ actual: &PlumType,
573
+ actual_expr: &ast::Expr,
574
+ ctx: &CheckCtx,
575
+ ) -> Result<(), String> {
576
+ if matches!(actual, PlumType::TParam(_)) && !matches!(declared, PlumType::TParam(_)) {
577
+ return Ok(());
578
+ }
579
+ unifyArg(declared, actual, actual_expr, ctx)
580
+ }
581
+
277
582
  /// True if `expr` (looking through parens) is a direct construction/reference
278
583
  /// of exactly the enum variant named `variant_name` — a bare payload-free
279
584
  /// reference (`Red`), a positional-payload construction (`Circle(1)`), or a
@@ -344,6 +649,9 @@ pub struct EnumVariantInfo {
344
649
  /// form (`Next(T)`). See `ast::EnumVariant::field_names`.
345
650
  pub field_names: Vec<String>,
346
651
  pub values: Vec<ast::Expr>,
652
+ /// The owning enum's generic parameter names (`[T, E]` for `Try`) —
653
+ /// empty for a non-generic enum and for monomorphized specializations.
654
+ pub generics: Vec<String>,
347
655
  }
348
656
  /// Enum variant name -> its info, e.g. `"True" -> { enum_name: "Bool", tag: 1, field_types: [] }`.
349
657
  pub type EnumVariants = BTreeMap<String, EnumVariantInfo>;
@@ -396,6 +704,23 @@ pub struct CheckCtx<'a> {
396
704
  /// `plum-wasm-codegen`, which needs the same tables to resolve `self`, field access,
397
705
  /// and method dispatch during code generation.
398
706
  pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVariants, EnumParams) {
707
+ buildTables(source, false)
708
+ }
709
+
710
+ /// `generic == true` builds the structural tables for UN-monomorphized source
711
+ /// (`TApp`/`TParam`, generic schemes, trait signatures); `false` is the legacy
712
+ /// shape codegen uses on monomorphized source.
713
+ pub fn buildTables(
714
+ source: &ast::Source,
715
+ generic: bool,
716
+ ) -> (TypeEnv, MethodEnv, EnumVariants, EnumParams) {
717
+ let conv = |t: &ast::Type| {
718
+ if generic {
719
+ plumTypeFromAstG(t)
720
+ } else {
721
+ plumTypeFromAst(t)
722
+ }
723
+ };
399
724
  let mut global_env: TypeEnv = TypeEnv::new();
400
725
  let mut methods: MethodEnv = BTreeMap::new();
401
726
  let mut enum_variants: EnumVariants = BTreeMap::new();
@@ -420,12 +745,12 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
420
745
  match item {
421
746
  ast::Item::Enum(e) => {
422
747
  let shared_field_types: Vec<PlumType> =
423
- e.params.iter().map(|p| plumTypeFromAst(&p.ty)).collect();
748
+ e.params.iter().map(|p| conv(&p.ty)).collect();
424
749
  if !e.params.is_empty() {
425
750
  let params = e
426
751
  .params
427
752
  .iter()
428
- .map(|p| (p.name.clone(), plumTypeFromAst(&p.ty)))
753
+ .map(|p| (p.name.clone(), conv(&p.ty)))
429
754
  .collect();
430
755
  enum_params.insert(e.name.clone(), params);
431
756
  }
@@ -466,7 +791,7 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
466
791
  (vec![PlumType::TNamed(v.name.clone())], Vec::new())
467
792
  } else {
468
793
  (
469
- v.fields.iter().map(plumTypeFromAst).collect(),
794
+ v.fields.iter().map(conv).collect(),
470
795
  v.field_names.clone(),
471
796
  )
472
797
  };
@@ -478,6 +803,11 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
478
803
  field_types,
479
804
  field_names,
480
805
  values: v.values.clone(),
806
+ generics: if generic {
807
+ monomorphize::enumGenericParams(e)
808
+ } else {
809
+ Vec::new()
810
+ },
481
811
  },
482
812
  );
483
813
  }
@@ -496,28 +826,36 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
496
826
  .params
497
827
  .iter()
498
828
  .map(|p| match &p.ty {
499
- ast::ParamType::Type(t) => plumTypeFromAst(t),
829
+ ast::ParamType::Type(t) => conv(t),
500
- ast::ParamType::Variadic(t) => {
501
- PlumType::TVariadic(Box::new(plumTypeFromAst(t)))
830
+ ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(conv(t))),
502
- }
503
831
  ast::ParamType::Fn(params, ret) => {
504
- let param_types = params.iter().map(plumTypeFromAst).collect();
832
+ let param_types = params.iter().map(&conv).collect();
505
833
  let ret_ty = ret
506
834
  .as_ref()
507
- .map(|r| plumTypeFromAst(r))
835
+ .map(|r| conv(r))
508
836
  .unwrap_or(PlumType::TUnit);
509
837
  PlumType::TFun(param_types, Box::new(ret_ty))
510
838
  }
511
839
  })
512
840
  .collect();
513
- let ret = f
514
- .returns
515
- .as_ref()
516
- .map(plumTypeFromAst)
517
- .unwrap_or(PlumType::TUnit);
841
+ let ret = f.returns.as_ref().map(&conv).unwrap_or(PlumType::TUnit);
518
842
  let fn_ty = PlumType::TFun(param_types, Box::new(ret));
519
843
  if let Some(recv) = &f.type_param {
520
844
  methods.insert((recv.clone(), f.name.clone()), fn_ty);
845
+ } else if generic {
846
+ global_env.insert(
847
+ format!("{}{}", GLOBAL_KEY, f.name),
848
+ TypeScheme::mono(PlumType::TUnit),
849
+ );
850
+ let mut vars = Vec::new();
851
+ freeTParams(&fn_ty, &mut vars);
852
+ global_env.insert(
853
+ f.name.clone(),
854
+ TypeScheme {
855
+ vars,
856
+ body: Box::new(fn_ty),
857
+ },
858
+ );
521
859
  } else {
522
860
  global_env.insert(f.name.clone(), TypeScheme::mono(fn_ty));
523
861
  }
@@ -534,6 +872,17 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
534
872
  };
535
873
  global_env.insert(c.name.clone(), TypeScheme::mono(ty));
536
874
  }
875
+ ast::Item::Trait(t) if generic => {
876
+ // Trait method signatures, looked up for calls on a bounded `TParam`.
877
+ for m in &t.methods {
878
+ let params: Vec<PlumType> = m.params.iter().map(|p| paramTypeG(&p.ty)).collect();
879
+ let ret = m.returns.as_ref().map(&conv).unwrap_or(PlumType::TUnit);
880
+ global_env.insert(
881
+ format!("{}{}:{}", TRAIT_KEY, t.name, m.name),
882
+ TypeScheme::mono(PlumType::TFun(params, Box::new(ret))),
883
+ );
884
+ }
885
+ }
537
886
  _ => {}
538
887
  }
539
888
  }
@@ -652,12 +1001,26 @@ fn checkTraitConformance(source: &ast::Source) -> Vec<CheckError> {
652
1001
  }
653
1002
 
654
1003
  pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
1004
+ checkSourceInner(source)
1005
+ }
1006
+
655
- // Runs against the ORIGINAL (pre-monomorphization) source, deliberately —
1007
+ /// Type-checks the original source like `checkSource` and also returns the
1008
+ /// instantiated type of every expression (keyed by expression address) for codegen.
1009
+ pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
1010
+ RECORD.with(|r| *r.borrow_mut() = Some(HashMap::new()));
1011
+ let result = checkSourceInner(source);
1012
+ let recorded = RECORD.with(|r| r.borrow_mut().take()).unwrap_or_default();
1013
+ result.map(|()| TypedInfo {
1014
+ expr_types: recorded,
1015
+ })
1016
+ }
1017
+
1018
+ fn checkSourceInner(source: &ast::Source) -> CheckResult<()> {
656
- // see `checkTraitConformance`'s own doc comment for why.
1019
+ // Generic items are checked once, structurally (`TApp`/`TParam`), on the
1020
+ // ORIGINAL source — no monomorphization.
657
1021
  let mut errors: Vec<CheckError> = checkTraitConformance(source);
658
- let source = monomorphize::monomorphizeSource(source).map_err(|e| vec![CheckError::new(e)])?;
659
- let (global_env, methods, enum_variants, enum_params) = buildGlobalTables(&source);
1022
+ let (global_env, methods, enum_variants, enum_params) = buildTables(source, true);
660
- let min_required = buildMinRequiredArgs(&source);
1023
+ let min_required = buildMinRequiredArgs(source);
661
1024
  let ctx = CheckCtx {
662
1025
  methods: &methods,
663
1026
  enum_variants: &enum_variants,
@@ -721,9 +1084,17 @@ pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
721
1084
  }
722
1085
  }
723
1086
 
1087
+ let enums_by_name: BTreeMap<&str, &ast::Enum> = source
1088
+ .items
1089
+ .iter()
1090
+ .filter_map(|i| match i {
1091
+ ast::Item::Enum(e) => Some((e.name.as_str(), e)),
1092
+ _ => None,
1093
+ })
1094
+ .collect();
724
1095
  for item in &source.items {
725
1096
  if let ast::Item::Fn(f) = item {
726
- let mut local_errors = checkFn(f, &global_env, &ctx);
1097
+ let mut local_errors = checkFn(f, &global_env, &ctx, &enums_by_name);
727
1098
  errors.append(&mut local_errors);
728
1099
  }
729
1100
  }
@@ -750,7 +1121,12 @@ pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
750
1121
  }
751
1122
  }
752
1123
 
1124
+ fn checkFn(
1125
+ f: &ast::Fn,
1126
+ global_env: &TypeEnv,
1127
+ ctx: &CheckCtx,
753
- fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError> {
1128
+ enums: &BTreeMap<&str, &ast::Enum>,
1129
+ ) -> Vec<CheckError> {
754
1130
  let mut errors = Vec::new();
755
1131
 
756
1132
  // `is_extern` and `body == FnBody::Extern` must agree — either both (a genuine
@@ -817,45 +1193,46 @@ fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError>
817
1193
 
818
1194
  // Methods (`name<Receiver>(...)`) get an implicit `self: Receiver` binding.
819
1195
  if let Some(recv) = &f.type_param {
1196
+ let generics = enumGenericsOf(recv, ctx);
820
- let recv_ty = ast::Type {
1197
+ let self_ty = if generics.is_empty() {
1198
+ plumTypeFromName(recv)
1199
+ } else {
1200
+ PlumType::TApp(
821
- name: recv.clone(),
1201
+ recv.clone(),
822
- generics: vec![],
1202
+ generics.iter().map(|g| PlumType::TParam(g.clone())).collect(),
1203
+ )
823
1204
  };
1205
+ env.insert("self".to_string(), TypeScheme::mono(self_ty));
1206
+ // The owner's declared trait bounds (`K: Hashable`) apply in its methods.
1207
+ if let Some(e) = enums.get(recv.as_str()) {
1208
+ for g in &e.generics {
1209
+ for b in &g.bounds {
824
- env.insert(
1210
+ env.insert(
825
- "self".to_string(),
1211
+ format!("{}{}:{}", BOUND_KEY, g.name, b),
826
- TypeScheme::mono(plumTypeFromAst(&recv_ty)),
1212
+ TypeScheme::mono(PlumType::TUnit),
827
- );
1213
+ );
1214
+ }
1215
+ }
1216
+ }
828
1217
  }
829
1218
 
830
1219
  // Add params to env
831
1220
  for p in &f.params {
832
- let ty = match &p.ty {
833
- ast::ParamType::Type(t) => plumTypeFromAst(t),
834
- ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plumTypeFromAst(t))),
835
- ast::ParamType::Fn(params, ret) => {
836
- let param_types = params.iter().map(plumTypeFromAst).collect();
837
- let ret_ty = ret
838
- .as_ref()
839
- .map(|r| plumTypeFromAst(r))
840
- .unwrap_or(PlumType::TUnit);
841
- PlumType::TFun(param_types, Box::new(ret_ty))
842
- }
843
- };
844
- env.insert(p.name.clone(), TypeScheme::mono(ty));
1221
+ env.insert(p.name.clone(), TypeScheme::mono(paramTypeG(&p.ty)));
845
1222
  }
846
1223
 
847
1224
  let declared_ret = f
848
1225
  .returns
849
1226
  .as_ref()
850
- .map(plumTypeFromAst)
1227
+ .map(plumTypeFromAstG)
851
1228
  .unwrap_or(PlumType::TUnit);
852
1229
  // Reserved env key read by `?` (see `inferExpr`'s `Try` arm); closures remove it.
853
1230
  env.insert(RETURN_KEY.to_string(), TypeScheme::mono(declared_ret.clone()));
854
1231
 
855
1232
  match &f.body {
856
- ast::FnBody::Expr(e) => match inferExpr(e, &env, ctx) {
1233
+ ast::FnBody::Expr(e) => match inferExprWith(e, &env, ctx, Some(&declared_ret)) {
857
1234
  Ok(t) => {
858
- if let Err(msg) = unifyArg(&declared_ret, &t, e, ctx) {
1235
+ if let Err(msg) = unifyReturn(&declared_ret, &t, e, ctx) {
859
1236
  errors.push(CheckError::at(
860
1237
  f.line,
861
1238
  f.column,
@@ -870,28 +1247,39 @@ fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError>
870
1247
  )),
871
1248
  },
872
1249
  ast::FnBody::Block(block) => {
1250
+ // The tail expression is checked against the declared return type
1251
+ // (which also pins down its generic arguments); everything before
1252
+ // it is an ordinary statement.
1253
+ let (tail, init) = match block.stmts.split_last() {
1254
+ Some((ast::Stmt::Expr(e, line, column), init)) => (Some((e, *line, *column)), init),
1255
+ _ => (None, &block.stmts[..]),
1256
+ };
1257
+ for stmt in init {
873
- let mut block_errors = checkBlock(
1258
+ errors.append(&mut checkStmt(
874
- block,
1259
+ stmt,
875
- &mut env,
1260
+ &mut env,
876
- &declared_ret,
1261
+ &declared_ret,
877
- &f.name,
1262
+ &f.name,
878
- (f.line, f.column),
1263
+ (f.line, f.column),
879
- ctx,
1264
+ ctx,
880
- );
1265
+ ));
881
- errors.append(&mut block_errors);
1266
+ }
882
- // Check the type of the last expression statement against the declared return type
883
- if let Some(ast::Stmt::Expr(last_expr, line, column)) = block.stmts.last() {
1267
+ if let Some((last_expr, line, column)) = tail {
884
- match inferExpr(last_expr, &env, ctx) {
1268
+ match inferExprWith(last_expr, &env, ctx, Some(&declared_ret)) {
885
1269
  Ok(t) => {
886
- if let Err(msg) = unifyArg(&declared_ret, &t, last_expr, ctx) {
1270
+ if let Err(msg) = unifyReturn(&declared_ret, &t, last_expr, ctx) {
887
1271
  errors.push(CheckError::at(
888
- *line,
1272
+ line,
889
- *column,
1273
+ column,
890
1274
  format!("fn '{}': return type mismatch: {}", f.name, msg),
891
1275
  ));
892
1276
  }
893
1277
  }
1278
+ Err(msg) => errors.push(CheckError::at(
1279
+ line,
1280
+ column,
894
- Err(_) => {} // already reported by checkBlock
1281
+ format!("fn '{}': {}", f.name, msg),
1282
+ )),
895
1283
  }
896
1284
  }
897
1285
  }
@@ -946,7 +1334,8 @@ fn checkStmt(
946
1334
  for (target, value) in a.targets.iter().zip(a.values.iter()) {
947
1335
  match target {
948
1336
  ast::AssignTarget::Var(name) => {
949
- let already_declared = lookup(env, name).is_ok();
1337
+ let already_declared = lookup(env, name).is_ok()
1338
+ && !env.contains_key(&format!("{}{}", GLOBAL_KEY, name));
950
1339
  if a.declare && already_declared {
951
1340
  errors.push(CheckError::at(fn_pos.0, fn_pos.1, format!("fn '{}': assign '{}': already declared — use '=' to reassign, not ':='", fn_name, name)));
952
1341
  } else if !a.declare && already_declared {
@@ -997,8 +1386,8 @@ fn checkStmt(
997
1386
  ast::AssignTarget::Field(object, field_name) => {
998
1387
  let label = format!("{}.{}", describeTargetObject(object), field_name);
999
1388
  match (inferExpr(object, env, ctx), inferExpr(value, env, ctx)) {
1000
- (Ok(PlumType::TNamed(class_name)), Ok(value_ty)) => {
1389
+ (Ok(obj_ty @ (PlumType::TNamed(_) | PlumType::TApp(..))), Ok(value_ty)) => {
1001
- match lookupFieldType(&class_name, field_name, ctx) {
1390
+ match fieldTypeOf(&obj_ty, field_name, ctx) {
1002
1391
  Ok(field_ty) => {
1003
1392
  if let Err(msg) = unifyArg(&field_ty, &value_ty, value, ctx) {
1004
1393
  errors.push(CheckError::at(fn_pos.0, fn_pos.1, format!("fn '{}': assign '{}': {}", fn_name, label, msg)));
@@ -1015,9 +1404,9 @@ fn checkStmt(
1015
1404
  }
1016
1405
  }
1017
1406
  }
1018
- ast::Stmt::Return(Some(e)) => match inferExpr(e, env, ctx) {
1407
+ ast::Stmt::Return(Some(e)) => match inferExprWith(e, env, ctx, Some(declared_ret)) {
1019
1408
  Ok(t) => {
1020
- if let Err(msg) = unifyArg(declared_ret, &t, e, ctx) {
1409
+ if let Err(msg) = unifyReturn(declared_ret, &t, e, ctx) {
1021
1410
  errors.push(CheckError::at(
1022
1411
  fn_pos.0,
1023
1412
  fn_pos.1,
@@ -1277,7 +1666,7 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
1277
1666
  })
1278
1667
  .collect(),
1279
1668
  )),
1280
- PlumType::TNamed(enum_name) => {
1669
+ PlumType::TNamed(enum_name) | PlumType::TApp(enum_name, _) => {
1281
1670
  let mut vs: Vec<(&String, &EnumVariantInfo)> = ctx
1282
1671
  .enum_variants
1283
1672
  .iter()
@@ -1297,7 +1686,7 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
1297
1686
  fields: if ctx.enum_params.contains_key(enum_name) {
1298
1687
  Vec::new()
1299
1688
  } else {
1300
- i.field_types.clone()
1689
+ variantFields(i, ty)
1301
1690
  },
1302
1691
  })
1303
1692
  .collect(),
@@ -1541,7 +1930,9 @@ fn inferClassCallRaw(
1541
1930
  call: &ast::ClassCall,
1542
1931
  env: &TypeEnv,
1543
1932
  ctx: &CheckCtx,
1933
+ expected: Option<&PlumType>,
1544
1934
  ) -> Result<PlumType, String> {
1935
+ let explicit: Vec<PlumType> = call.generics.iter().map(plumTypeFromAstG).collect();
1545
1936
  // A zero-arg `Type()` construction with a nested, no-param `fun init() ->
1546
1937
  // Type = ...` method declared is sugar for calling that method — the
1547
1938
  // constructor pattern used by e.g. `Buffer` — rather than raw field
@@ -1549,12 +1940,25 @@ fn inferClassCallRaw(
1549
1940
  // and go through ordinary field-by-field construction below instead,
1550
1941
  // which requires every field to be given.
1551
1942
  if call.fields.is_empty() {
1552
- if let Some(PlumType::TFun(init_params, ret)) = ctx
1943
+ if let Some(sig @ PlumType::TFun(init_params, _)) = ctx
1553
1944
  .methods
1554
1945
  .get(&(call.type_name.clone(), "init".to_string()))
1555
1946
  {
1556
1947
  if initCallableWithNoArgs(init_params) {
1948
+ let gens = enumGenericsOf(&call.type_name, ctx);
1949
+ let owner = if !explicit.is_empty() && explicit.len() == gens.len() {
1950
+ appSubst(gens, &explicit)
1951
+ } else {
1952
+ BTreeMap::new()
1953
+ };
1954
+ let PlumType::TFun(_, ret) = instSig(sig, &owner) else {
1955
+ unreachable!("instSig preserves the TFun shape")
1956
+ };
1957
+ let mut binds = Binds::new();
1958
+ if let Some(exp) = expected {
1959
+ bindMatch(&ret, exp, &mut binds);
1960
+ }
1557
- return Ok(*ret.clone());
1961
+ return Ok(substVars(&ret, &binds));
1558
1962
  }
1559
1963
  }
1560
1964
  }
@@ -1566,12 +1970,19 @@ fn inferClassCallRaw(
1566
1970
  // replacement, e.g. `enum Cat = | Cat(name: Str)`) constructs, with no
1567
1971
  // separate class-construction path needed.
1568
1972
  Some(info) if !info.field_names.is_empty() => {
1973
+ let (owner, result, mut binds) = startVariant(info, &explicit, expected);
1569
1974
  for fa in &call.fields {
1570
1975
  match info.field_names.iter().position(|n| n == &fa.name) {
1571
1976
  Some(i) => {
1572
- let expected = &info.field_types[i];
1977
+ let pat = types::substitute(&info.field_types[i], &owner);
1573
- let actual = inferExpr(&fa.value, env, ctx)?;
1978
+ let actual = inferExprWith(
1979
+ &fa.value,
1980
+ env,
1981
+ ctx,
1982
+ Some(&substVars(&pat, &binds)),
1983
+ )?;
1984
+ bindMatch(&pat, &actual, &mut binds);
1574
- unifyArg(expected, &actual, &fa.value, ctx).map_err(|e| {
1985
+ unifyArg(&substVars(&pat, &binds), &actual, &fa.value, ctx).map_err(|e| {
1575
1986
  format!("variant '{}' field '{}': {}", call.type_name, fa.name, e)
1576
1987
  })?;
1577
1988
  }
@@ -1598,7 +2009,7 @@ fn inferClassCallRaw(
1598
2009
  }
1599
2010
  }
1600
2011
  }
1601
- Ok(PlumType::TNamed(info.enum_name.clone()))
2012
+ Ok(substVars(&result, &binds))
1602
2013
  }
1603
2014
  // Positional/generic-payload variant (`Next(T)`, constructed via `Expr::FnCall`
1604
2015
  // elsewhere instead), a payload-free variant, or an unmodeled (e.g.
@@ -1682,7 +2093,12 @@ fn checkPattern(
1682
2093
  fields.len()
1683
2094
  ));
1684
2095
  }
2096
+ let field_tys = if nominalName(subject_ty) == Some(info.enum_name.as_str()) {
2097
+ variantFields(info, subject_ty)
2098
+ } else {
2099
+ variantFields(info, &PlumType::TNamed(info.enum_name.clone()))
2100
+ };
1685
- for (f, fty) in fields.iter().zip(info.field_types.iter()) {
2101
+ for (f, fty) in fields.iter().zip(field_tys.iter()) {
1686
2102
  checkPattern(f, fty, env, ctx)?;
1687
2103
  }
1688
2104
  Ok(())
@@ -1936,12 +2352,298 @@ fn isTagLiteralName(n: &str, ctx: &CheckCtx) -> bool {
1936
2352
  && !ctx.methods.keys().any(|(recv, _)| recv == n)
1937
2353
  }
1938
2354
 
2355
+ fn argExpr(arg: &ast::Arg) -> &ast::Expr {
2356
+ match arg {
2357
+ ast::Arg::Positional(e) => e,
2358
+ ast::Arg::Keyword { value, .. } => value,
2359
+ ast::Arg::Pair { value, .. } => value,
2360
+ }
2361
+ }
2362
+
2363
+ /// Checks call `args` against the (already instantiated) `params`, binding the
2364
+ /// inference variables in `binds` as it goes. Non-closure arguments are checked
2365
+ /// first so a closure sees the types its parameters were already pinned to.
2366
+ fn checkCallArgs(
2367
+ label: &str,
2368
+ params: &[PlumType],
2369
+ args: &[ast::Arg],
2370
+ required: usize,
2371
+ env: &TypeEnv,
2372
+ ctx: &CheckCtx,
2373
+ binds: &mut Binds,
2374
+ ) -> Result<(), String> {
2375
+ let (fixed, variadic) = match params.last() {
2376
+ Some(PlumType::TVariadic(elem)) => (&params[..params.len() - 1], Some(&**elem)),
2377
+ _ => (params, None),
2378
+ };
2379
+ match variadic {
2380
+ Some(_) => {
2381
+ if args.len() < fixed.len() {
2382
+ return Err(format!(
2383
+ "{}: expected at least {} arg(s), got {}",
2384
+ label,
2385
+ fixed.len(),
2386
+ args.len()
2387
+ ));
2388
+ }
2389
+ }
2390
+ None => {
2391
+ if args.len() < required || args.len() > params.len() {
2392
+ return Err(format!(
2393
+ "{}: expected {} args, got {}",
2394
+ label,
2395
+ params.len(),
2396
+ args.len()
2397
+ ));
2398
+ }
2399
+ }
2400
+ }
2401
+ let slots: Vec<(usize, &ast::Expr, &PlumType, bool)> = args
2402
+ .iter()
2403
+ .enumerate()
2404
+ .map(|(i, a)| match fixed.get(i) {
2405
+ Some(p) => (i, argExpr(a), p, false),
2406
+ None => (i, argExpr(a), variadic.expect("variadic slot"), true),
2407
+ })
2408
+ .collect();
2409
+ for pass in 0..2 {
2410
+ for (i, e, pat, is_var) in &slots {
2411
+ let is_closure = matches!(e, ast::Expr::Closure(_));
2412
+ if (pass == 1) != is_closure {
2413
+ continue;
2414
+ }
2415
+ let want = substVars(pat, binds);
2416
+ let actual = inferExprWith(e, env, ctx, Some(&want))?;
2417
+ bindMatch(pat, &actual, binds);
2418
+ let want = substVars(pat, binds);
2419
+ unifyArg(&want, &actual, e, ctx).map_err(|err| {
2420
+ if *is_var {
2421
+ format!("{} variadic arg {}: {}", label, i, err)
2422
+ } else {
2423
+ format!("{} arg {}: {}", label, i, err)
2424
+ }
2425
+ })?;
2426
+ }
2427
+ }
2428
+ Ok(())
2429
+ }
2430
+
2431
+ /// Type of `obj.field`, substituting a generic receiver's type arguments.
2432
+ fn fieldTypeOf(obj_ty: &PlumType, field: &str, ctx: &CheckCtx) -> Result<PlumType, String> {
2433
+ match obj_ty {
2434
+ PlumType::TNamed(n) => lookupFieldType(n, field, ctx),
2435
+ PlumType::TStr => lookupFieldType("Str", field, ctx),
2436
+ PlumType::TApp(n, args) => {
2437
+ let raw = lookupFieldType(n, field, ctx)?;
2438
+ let gens = enumGenericsOf(n, ctx);
2439
+ if gens.len() == args.len() {
2440
+ Ok(types::substitute(&raw, &appSubst(gens, args)))
2441
+ } else {
2442
+ Ok(raw)
2443
+ }
2444
+ }
2445
+ other => Err(format!(
2446
+ "cannot access field '{}' on non-class type {}",
2447
+ field, other
2448
+ )),
2449
+ }
2450
+ }
2451
+
2452
+ /// The success (`Ok`) payload and failure (`Err`) payload types of a `Try`-shaped
2453
+ /// value of type `ty`, with its type arguments substituted.
2454
+ fn tryParts(ty: &PlumType, ctx: &CheckCtx) -> Option<(PlumType, Option<PlumType>)> {
2455
+ let name = nominalName(ty)?;
2456
+ let (_, success, _, failure) = tryOperatorVariants(name, ctx)?;
2457
+ let ok = variantFields(success, ty)
2458
+ .first()
2459
+ .cloned()
2460
+ .unwrap_or(PlumType::TUnit);
2461
+ let err = variantFields(failure, ty).first().cloned();
2462
+ Some((ok, err))
2463
+ }
2464
+
2465
+ fn inferMethodCall(
2466
+ obj_ty: &PlumType,
2467
+ call: &ast::FnCall,
2468
+ env: &TypeEnv,
2469
+ ctx: &CheckCtx,
2470
+ expected: Option<&PlumType>,
2471
+ ) -> Result<PlumType, String> {
2472
+ // A method on a generic parameter resolves through its trait bounds only.
2473
+ if let PlumType::TParam(p) = obj_ty {
2474
+ let prefix = format!("{}{}:", BOUND_KEY, p);
2475
+ for key in env.keys() {
2476
+ let Some(tr) = key.strip_prefix(&prefix) else {
2477
+ continue;
2478
+ };
2479
+ let Some(sch) = env.get(&format!("{}{}:{}", TRAIT_KEY, tr, call.name)) else {
2480
+ continue;
2481
+ };
2482
+ if let PlumType::TFun(params, ret) = &*sch.body {
2483
+ let mut binds = Binds::new();
2484
+ let label = format!("method '{}.{}'", p, call.name);
2485
+ checkCallArgs(&label, params, &call.args, params.len(), env, ctx, &mut binds)?;
2486
+ return Ok(substVars(ret, &binds));
2487
+ }
2488
+ }
2489
+ return Err(format!(
2490
+ "'{}' has no method '{}' (add a bound)",
2491
+ p, call.name
2492
+ ));
2493
+ }
2494
+ let Some(class_name) = methodReceiverName(obj_ty) else {
2495
+ return Ok(PlumType::TVar("_".to_string()));
2496
+ };
2497
+ // Fallback: a primitive/class receiver's method might be defined once on
2498
+ // the "union" enum that bare-wraps it instead (`enum Number = | Int | Float`).
2499
+ let class_name = if ctx
2500
+ .methods
2501
+ .contains_key(&(class_name.clone(), call.name.clone()))
2502
+ {
2503
+ class_name
2504
+ } else {
2505
+ ctx.enum_variants
2506
+ .get(class_name.as_str())
2507
+ .map(|info| info.enum_name.clone())
2508
+ .filter(|enum_name| {
2509
+ ctx.methods
2510
+ .contains_key(&(enum_name.clone(), call.name.clone()))
2511
+ })
2512
+ .unwrap_or(class_name)
2513
+ };
2514
+ let Some(sig) = ctx.methods.get(&(class_name.clone(), call.name.clone())) else {
2515
+ // Unmodeled method (e.g. builtin/std): allow, codegen will catch.
2516
+ return Ok(PlumType::TVar("_".to_string()));
2517
+ };
2518
+ let PlumType::TFun(..) = sig else {
2519
+ return Ok(PlumType::TVar("_".to_string()));
2520
+ };
2521
+ let owner = match obj_ty {
2522
+ PlumType::TApp(n, args) if *n == class_name => {
2523
+ appSubst(enumGenericsOf(n, ctx), args)
2524
+ }
2525
+ _ => BTreeMap::new(),
2526
+ };
2527
+ let PlumType::TFun(params, ret) = instSig(sig, &owner) else {
2528
+ unreachable!("instSig preserves the TFun shape")
2529
+ };
2530
+ let mut binds = Binds::new();
2531
+ if let Some(exp) = expected {
2532
+ bindMatch(&ret, exp, &mut binds);
2533
+ }
2534
+ let required = ctx
2535
+ .min_required
2536
+ .get(&(Some(class_name.clone()), call.name.clone()))
2537
+ .copied()
2538
+ .unwrap_or(params.len());
2539
+ let label = format!("method '{}.{}'", class_name, call.name);
2540
+ checkCallArgs(&label, &params, &call.args, required, env, ctx, &mut binds)?;
2541
+ Ok(substVars(&ret, &binds))
2542
+ }
2543
+
2544
+ /// Starts constructing a value of one of `info`'s enum: fresh owner variables
2545
+ /// (or the explicit `generics`), the result type pattern, and bindings seeded
2546
+ /// from the `expected` type.
2547
+ fn startVariant(
2548
+ info: &EnumVariantInfo,
2549
+ explicit: &[PlumType],
2550
+ expected: Option<&PlumType>,
2551
+ ) -> (BTreeMap<String, PlumType>, PlumType, Binds) {
2552
+ let owner = if !explicit.is_empty() && explicit.len() == info.generics.len() {
2553
+ appSubst(&info.generics, explicit)
2554
+ } else {
2555
+ freshOwner(&info.generics)
2556
+ };
2557
+ let result = enumTypeWith(info, &owner);
2558
+ let mut binds = Binds::new();
2559
+ if let Some(exp) = expected {
2560
+ bindMatch(&result, exp, &mut binds);
2561
+ }
2562
+ (owner, result, binds)
2563
+ }
2564
+
2565
+ /// A tag literal (`Bad` / `Bad(x)`) written where the expected type is a tag
2566
+ /// union naming it — it is the TAG even if a declared variant shares the name.
2567
+ fn expectedTagLiteral(
2568
+ expr: &ast::Expr,
2569
+ env: &TypeEnv,
2570
+ ctx: &CheckCtx,
2571
+ expected: Option<&PlumType>,
2572
+ ) -> Option<Result<PlumType, String>> {
2573
+ let Some(PlumType::TTags(tags)) = expected else {
2574
+ return None;
2575
+ };
2576
+ match expr {
2577
+ ast::Expr::TypeName(n)
2578
+ if matches!(tags.get(n.as_str()), Some(None)) && ctx.enum_variants.contains_key(n) =>
2579
+ {
2580
+ Some(Ok(PlumType::TTags(BTreeMap::from([(n.clone(), None)]))))
2581
+ }
2582
+ ast::Expr::FnCall(call)
2583
+ if call.args.len() == 1
2584
+ && matches!(tags.get(call.name.as_str()), Some(Some(_)))
2585
+ && ctx.enum_variants.contains_key(&call.name) =>
2586
+ {
2587
+ let payload_expected = tags.get(call.name.as_str()).and_then(|p| p.as_deref());
2588
+ Some(
2589
+ inferExprWith(argExpr(&call.args[0]), env, ctx, payload_expected).map(|p| {
2590
+ PlumType::TTags(BTreeMap::from([(
2591
+ call.name.clone(),
2592
+ Some(Box::new(p)),
2593
+ )]))
2594
+ }),
2595
+ )
2596
+ }
2597
+ _ => None,
2598
+ }
2599
+ }
2600
+
1939
2601
  pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<PlumType, String> {
2602
+ inferExprWith(expr, env, ctx, None)
2603
+ }
2604
+
2605
+ /// Like `inferExpr`, with the type the context expects (a declared return, a
2606
+ /// parameter, a field) — used to pin down generic arguments nothing else
2607
+ /// determines (`Err(Bad)` in a `Try[Int, [Bad]]` position).
2608
+ pub fn inferExprWith(
2609
+ expr: &ast::Expr,
2610
+ env: &TypeEnv,
2611
+ ctx: &CheckCtx,
2612
+ expected: Option<&PlumType>,
2613
+ ) -> Result<PlumType, String> {
2614
+ let r = inferExprInner(expr, env, ctx, expected);
2615
+ if let Ok(t) = &r {
2616
+ recordType(expr, t);
2617
+ }
2618
+ r
2619
+ }
2620
+
2621
+ fn inferExprInner(
2622
+ expr: &ast::Expr,
2623
+ env: &TypeEnv,
2624
+ ctx: &CheckCtx,
2625
+ expected: Option<&PlumType>,
2626
+ ) -> Result<PlumType, String> {
2627
+ if let Some(r) = expectedTagLiteral(expr, env, ctx, expected) {
2628
+ return r;
2629
+ }
1940
2630
  match expr {
1941
2631
  ast::Expr::Int(_) => Ok(PlumType::TInt),
1942
2632
  ast::Expr::Float(_) => Ok(PlumType::TFloat),
1943
- ast::Expr::String(_) => Ok(PlumType::TStr),
2633
+ ast::Expr::String(s) => {
2634
+ // Interpolated parts are typed for the record only; errors in them
2635
+ // were never part of this check.
2636
+ for part in &s.parts {
2637
+ if let ast::StringPart::Interp(e) = part {
2638
+ let _ = inferExprWith(e, env, ctx, None);
2639
+ }
2640
+ }
2641
+ Ok(PlumType::TStr)
2642
+ }
1944
- ast::Expr::Var(name) => lookup(env, name),
2643
+ ast::Expr::Var(name) => match env.get(name) {
2644
+ Some(s) if !s.vars.is_empty() => Ok(instSig(&s.body, &BTreeMap::new())),
2645
+ _ => lookup(env, name),
2646
+ },
1945
2647
  ast::Expr::Self_ => lookup(env, "self"),
1946
2648
  // A bare `NAME` lexes as a type_identifier whenever it starts with an
1947
2649
  // uppercase letter — which includes every SCREAMING_CASE top-level
@@ -1952,15 +2654,12 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
1952
2654
  Ok(ty) if !matches!(ty, PlumType::TVar(_)) => Ok(ty),
1953
2655
  _ => match ctx.enum_variants.get(n) {
1954
2656
  // `Bool` normalizes to the dedicated `TBool` variant (not
1955
- // `TNamed("Bool")`) — `True`/`False` only resolve here at all
2657
+ // `TNamed("Bool")`) — see the note on `unify`'s Bool arm.
1956
- // once `enum Bool = | True | False` (`libs/std/Bool.plum`) is
1957
- // actually imported, but every OTHER part of the checker AND
1958
- // `plum-wasm-codegen` (which reuses `inferExpr` via
1959
- // `inferLocalType`) still branches on the literal `TBool`
1960
- // variant, not a generic `TNamed` name — normalizing here
1961
- // keeps that working without patching every such site.
1962
2658
  Some(info) if info.enum_name == "Bool" => Ok(PlumType::TBool),
2659
+ Some(info) => {
1963
- Some(info) => Ok(PlumType::TNamed(info.enum_name.clone())),
2660
+ let (_, result, binds) = startVariant(info, &[], expected);
2661
+ Ok(substVars(&result, &binds))
2662
+ }
1964
2663
  None if isTagLiteralName(n, ctx) => Ok(PlumType::TTags(BTreeMap::from([(
1965
2664
  tagLiteralName(n).to_string(),
1966
2665
  None,
@@ -1969,35 +2668,27 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
1969
2668
  None => Ok(PlumType::TNamed(n.to_string())),
1970
2669
  },
1971
2670
  },
1972
- ast::Expr::Paren(inner) => inferExpr(inner, env, ctx),
2671
+ ast::Expr::Paren(inner) => inferExprWith(inner, env, ctx, expected),
1973
2672
  // `expr?` — deliberately does NOT check that the enclosing function's
1974
- // return type actually accepts the `Err` value this can exit
2673
+ // return type actually accepts the `Err` value beyond its error type.
1975
- // with early; that's left permissive here (codegen will catch a
1976
- // genuine mismatch as a wasm type error), consistent with this
1977
- // checker's existing "unmodeled/unvalidated, codegen catches it"
1978
- // fallbacks elsewhere (e.g. `TypeName`'s unmodeled-type-name arm
1979
- // above) — checking it properly would need the enclosing function's
1980
- // declared return type threaded through every `inferExpr` call site,
1981
- // for a case a wrong program still fails on, just less precisely.
1982
2674
  ast::Expr::Try(inner) => {
1983
2675
  let inner_ty = inferExpr(inner, env, ctx)?;
1984
- let name = match &inner_ty {
2676
+ if nominalName(&inner_ty).is_none() {
1985
- PlumType::TNamed(name) => name,
1986
- other => return Err(format!("'?' requires a Try value, found {}", other)),
2677
+ return Err(format!("'?' requires a Try value, found {}", inner_ty));
1987
- };
2678
+ }
1988
- let Some((_, success_info, _, failure_info)) = tryOperatorVariants(name, ctx) else {
2679
+ let Some((success_ty, inner_err)) = tryParts(&inner_ty, ctx) else {
2680
+ return Err(format!(
1989
- return Err(format!("'?' requires a Try value, found '{}'", name));
2681
+ "'?' requires a Try value, found '{}'",
2682
+ nominalName(&inner_ty).unwrap_or("?")
2683
+ ));
1990
2684
  };
1991
2685
  // The failure branch returns early from the enclosing fn, so the
1992
2686
  // inner error type must fit the fn's declared error type. Closures
1993
2687
  // (and test blocks) have no `$return` and aren't checked.
1994
- if let Some(PlumType::TNamed(outer)) = env.get(RETURN_KEY).map(|s| (*s.body).clone()) {
2688
+ if let Some(outer_ty) = env.get(RETURN_KEY).map(|s| (*s.body).clone()) {
1995
- if let Some((_, _, _, outer_failure)) = tryOperatorVariants(&outer, ctx) {
2689
+ if let Some((_, Some(outer_err))) = tryParts(&outer_ty, ctx) {
1996
- if let (Some(inner_err), Some(outer_err)) = (
2690
+ if let Some(inner_err) = &inner_err {
1997
- failure_info.field_types.first(),
1998
- outer_failure.field_types.first(),
1999
- ) {
2000
- if unify(outer_err, inner_err, ctx).is_err() {
2691
+ if unify(&outer_err, inner_err, ctx).is_err() {
2001
2692
  return Err(format!(
2002
2693
  "'?' error type {} is not part of the function's error type {}",
2003
2694
  inner_err, outer_err
@@ -2006,11 +2697,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
2006
2697
  }
2007
2698
  }
2008
2699
  }
2009
- Ok(success_info
2700
+ Ok(success_ty)
2010
- .field_types
2011
- .first()
2012
- .cloned()
2013
- .unwrap_or(PlumType::TUnit))
2014
2701
  }
2015
2702
  ast::Expr::Closure(cl) => {
2016
2703
  let mut closure_env = env.clone();
@@ -2027,18 +2714,32 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
2027
2714
  &mut resolved_from_usage,
2028
2715
  );
2029
2716
  }
2717
+ let (exp_params, exp_ret): (Option<&Vec<PlumType>>, Option<&PlumType>) = match expected
2718
+ {
2719
+ Some(PlumType::TFun(ps, r)) if ps.len() == cl.params.len() => {
2720
+ (Some(ps), Some(&**r))
2721
+ }
2722
+ _ => (None, None),
2723
+ };
2030
2724
  let param_types: Vec<PlumType> = cl
2031
2725
  .params
2032
2726
  .iter()
2727
+ .enumerate()
2033
- .map(|p| {
2728
+ .map(|(i, p)| {
2034
2729
  // A fresh TVar for anything not resolved below — `unify` treats an
2035
2730
  // unbound TVar as a wildcard, so this is only a real problem for
2036
2731
  // field access (`p.field`), which needs a concrete class name to
2037
2732
  // look the field up at all; see `resolveClosureParamFromFieldUsage`.
2733
+ let from_expected = exp_params
2734
+ .and_then(|ps| ps.get(i))
2735
+ .filter(|t| !matches!(t, PlumType::TVar(_)))
2736
+ .cloned();
2737
+ let t = from_expected.unwrap_or_else(|| {
2038
- let t = resolved_from_usage
2738
+ resolved_from_usage
2039
- .get(p)
2739
+ .get(p)
2040
- .cloned()
2740
+ .cloned()
2041
- .unwrap_or_else(|| PlumType::TVar(format!("_closure_{}", p)));
2741
+ .unwrap_or_else(|| PlumType::TVar(format!("_closure_{}", p)))
2742
+ });
2042
2743
  closure_env.insert(p.clone(), TypeScheme::mono(t.clone()));
2043
2744
  t
2044
2745
  })
@@ -2060,8 +2761,8 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
2060
2761
  }
2061
2762
  }
2062
2763
  let body_ty = match &cl.body.stmts.last() {
2063
- Some(ast::Stmt::Expr(e, ..)) => inferExpr(e, &closure_env, ctx)?,
2764
+ Some(ast::Stmt::Expr(e, ..)) => inferExprWith(e, &closure_env, ctx, exp_ret)?,
2064
- Some(ast::Stmt::Return(Some(e))) => inferExpr(e, &closure_env, ctx)?,
2765
+ Some(ast::Stmt::Return(Some(e))) => inferExprWith(e, &closure_env, ctx, exp_ret)?,
2065
2766
  _ => PlumType::TUnit,
2066
2767
  };
2067
2768
  Ok(PlumType::TFun(param_types, Box::new(body_ty)))
@@ -2076,6 +2777,9 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
2076
2777
  let lt = inferExpr(&b.left, env, ctx)?;
2077
2778
  let rt = inferExpr(&b.right, env, ctx)?;
2078
2779
  unify(&lt, &rt, ctx).map_err(|e| format!("binary op: {}", e))?;
2780
+ if matches!(lt, PlumType::TVar(_)) && !matches!(rt, PlumType::TVar(_)) {
2781
+ return Ok(rt);
2782
+ }
2079
2783
  Ok(lt)
2080
2784
  }
2081
2785
  ast::Expr::Bool(b) => {
@@ -2104,8 +2808,8 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
2104
2808
  ast::Expr::Ternary(t) => {
2105
2809
  let ct = inferExpr(&t.condition, env, ctx)?;
2106
2810
  unify(&PlumType::TBool, &ct, ctx).map_err(|e| format!("ternary condition: {}", e))?;
2107
- let tt = inferExpr(&t.then, env, ctx)?;
2811
+ let tt = inferExprWith(&t.then, env, ctx, expected)?;
2108
- let et = inferExpr(&t.else_, env, ctx)?;
2812
+ let et = inferExprWith(&t.else_, env, ctx, expected)?;
2109
2813
  unify(&tt, &et, ctx).map_err(|e| format!("ternary branches: {}", e))?;
2110
2814
  Ok(tt)
2111
2815
  }
@@ -2114,28 +2818,13 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
2114
2818
  // unify with `left`'s success (`Ok`) field type.
2115
2819
  ast::Expr::Elvis(e) => {
2116
2820
  let left_ty = inferExpr(&e.left, env, ctx)?;
2117
- let success_ty = match &left_ty {
2821
+ let Some(name) = nominalName(&left_ty) else {
2822
+ return Err(format!("'??' requires a Try value, found {}", left_ty));
2823
+ };
2118
- PlumType::TNamed(name) => match tryOperatorVariants(name, ctx) {
2824
+ let Some((success_ty, _)) = tryParts(&left_ty, ctx) else {
2119
- Some((_, success_info, _, _)) => success_info
2120
- .field_types
2121
- .first()
2122
- .cloned()
2123
- .unwrap_or(PlumType::TUnit),
2124
- None => {
2125
- return Err(format!(
2126
- "'??' requires a Try value, found '{}'",
2825
+ return Err(format!("'??' requires a Try value, found '{}'", name));
2127
- name
2128
- ))
2129
- }
2130
- },
2131
- other => {
2132
- return Err(format!(
2133
- "'??' requires a Try value, found {}",
2134
- other
2135
- ))
2136
- }
2137
2826
  };
2138
- let right_ty = inferExpr(&e.right, env, ctx)?;
2827
+ let right_ty = inferExprWith(&e.right, env, ctx, Some(&success_ty))?;
2139
2828
  unify(&success_ty, &right_ty, ctx).map_err(|e| format!("'??' branches: {}", e))?;
2140
2829
  Ok(success_ty)
2141
2830
  }
@@ -2147,21 +2836,12 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
2147
2836
  // registered enum variant (a "union" enum's bare-primitive-wrap
2148
2837
  // variant, e.g. `enum Number = | Int | Float`) whose single
2149
2838
  // field type exactly matches the arg's own type — a same-type
2150
- // call (`Int(intExpr)`) then means constructing that variant
2839
+ // call (`Int(intExpr)`) then means constructing that variant,
2151
- // (`monomorphize::wrapPrimitiveAgainstExpected` synthesizes
2152
- // exactly this call shape), not a no-op cast; falls through to
2153
- // the ordinary enum-variant-construction handling just below
2154
- // instead. A genuinely cross-type call (`Int(floatExpr)`) is
2155
- // never ambiguous this way, so every existing numeric-cast call
2156
- // site keeps its normal meaning.
2840
+ // not a no-op cast.
2157
2841
  if (call.name == "Int" || call.name == "Float" || call.name == "Byte")
2158
2842
  && call.args.len() == 1
2159
2843
  {
2160
- let arg_expr = match &call.args[0] {
2844
+ let arg_expr = argExpr(&call.args[0]);
2161
- ast::Arg::Positional(e) => e,
2162
- ast::Arg::Keyword { value, .. } => value,
2163
- ast::Arg::Pair { value, .. } => value,
2164
- };
2165
2845
  let actual = inferExpr(arg_expr, env, ctx)?;
2166
2846
  let is_same_type_variant_wrap = ctx
2167
2847
  .enum_variants
@@ -2180,26 +2860,11 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
2180
2860
  )),
2181
2861
  };
2182
2862
  }
2183
- // Else: fall through to the ordinary enum-variant-construction
2184
- // handling just below, exactly as if this special case didn't
2185
- // exist for this call.
2186
2863
  }
2187
2864
  // `Type(1, 2, 3)` — a call with bare POSITIONAL args on a record-shaped
2188
- // type's name (as opposed to `Type(field: value, ...)`, which parses as a
2189
- // `ClassCall` instead — see `class_argument_list` vs `fn_argument_list` in
2190
- // the grammar) — is sugar for `Type.init(1, 2, 3)`, the same constructor
2865
+ // type's name is sugar for `Type.init(1, 2, 3)` (see the `ClassCall`
2191
- // pattern as `Type()`'s own `ClassCall` handling above, letting `init`
2192
- // take real params (e.g. `List`'s own variadic `init(values: ...T)`)
2193
- // instead of being limited to zero args. Checked BEFORE the ordinary
2866
+ // zero-arg `Type()` handling); checked BEFORE the ordinary
2194
- // positional-variant-construction case just below: a record-shaped
2867
+ // positional-variant-construction case just below.
2195
- // enum's own name is ALSO its one variant's name (see
2196
- // `buildGlobalTables`), so without this ordering a call meant as
2197
- // `List.init(1, 2, 3)` could instead misfire as "construct the List
2198
- // variant positionally" whenever the arg count happens to match its
2199
- // real field count — a record-shaped type never supported positional
2200
- // field construction via bare `Type(...)` even before enums/classes
2201
- // merged into one representation, only through `init` or the named
2202
- // `Type(field: value, ...)` form.
2203
2868
  if ctx
2204
2869
  .enum_variants
2205
2870
  .get(&call.name)
@@ -2215,7 +2880,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
2215
2880
  args: call.args.clone(),
2216
2881
  }),
2217
2882
  }));
2218
- return inferExpr(&synthetic, env, ctx);
2883
+ return inferExprWith(&synthetic, env, ctx, expected);
2219
2884
  }
2220
2885
  if let Some(info) = ctx.enum_variants.get(&call.name) {
2221
2886
  if call.args.len() != info.field_types.len() {
@@ -2226,121 +2891,51 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
2226
2891
  call.args.len()
2227
2892
  ));
2228
2893
  }
2229
- for (i, (arg, expected)) in
2894
+ let (owner, result, mut binds) = startVariant(info, &[], expected);
2230
- call.args.iter().zip(info.field_types.iter()).enumerate()
2895
+ for (i, (arg, field)) in call.args.iter().zip(info.field_types.iter()).enumerate() {
2231
- {
2232
- let arg_expr = match arg {
2896
+ let arg_expr = argExpr(arg);
2233
- ast::Arg::Positional(e) => e,
2234
- ast::Arg::Keyword { value, .. } => value,
2897
+ let pat = types::substitute(field, &owner);
2235
- ast::Arg::Pair { value, .. } => value,
2236
- };
2237
- // A bare-type variant's own payload (`FantasyBook(FantasyBook(...))`,
2898
+ // A bare-type variant's own payload (`FantasyBook(FantasyBook(...))`)
2238
- // whether hand-written or produced by desugaring `FantasyBook(...)`
2239
- // sugar) infers via the RAW class-field rules — not the
2899
+ // infers via the RAW class-field rules — see `inferClassCallRaw`.
2240
- // enum-wrapping `inferExpr` would otherwise apply to a
2241
- // `ClassCall` of that same name — so it unifies against
2242
- // `expected` (the bare class type), not the enum itself.
2243
- let actual = match (expected, arg_expr) {
2900
+ let actual = match (&pat, arg_expr) {
2244
2901
  (PlumType::TNamed(n), ast::Expr::ClassCall(c))
2245
2902
  if n == &call.name && c.type_name == call.name =>
2246
2903
  {
2247
- inferClassCallRaw(c, env, ctx)?
2904
+ inferClassCallRaw(c, env, ctx, None)?
2248
2905
  }
2249
- _ => inferExpr(arg_expr, env, ctx)?,
2906
+ _ => inferExprWith(arg_expr, env, ctx, Some(&substVars(&pat, &binds)))?,
2250
2907
  };
2908
+ bindMatch(&pat, &actual, &mut binds);
2251
- unifyArg(expected, &actual, arg_expr, ctx)
2909
+ unifyArg(&substVars(&pat, &binds), &actual, arg_expr, ctx)
2252
2910
  .map_err(|e| format!("variant '{}' arg {}: {}", call.name, i, e))?;
2253
2911
  }
2254
- return Ok(PlumType::TNamed(info.enum_name.clone()));
2912
+ return Ok(substVars(&result, &binds));
2255
2913
  }
2914
+ let callee = match env.get(&call.name) {
2915
+ Some(s) if !s.vars.is_empty() => Ok(instSig(&s.body, &BTreeMap::new())),
2256
- match lookup(env, &call.name) {
2916
+ _ => lookup(env, &call.name),
2917
+ };
2918
+ match callee {
2257
2919
  Ok(PlumType::TFun(param_types, ret)) => {
2258
- match param_types.last() {
2920
+ let mut binds = Binds::new();
2259
- Some(PlumType::TVariadic(elem)) => {
2260
- let fixed = &param_types[..param_types.len() - 1];
2261
- if call.args.len() < fixed.len() {
2262
- return Err(format!(
2263
- "call '{}': expected at least {} arg(s), got {}",
2264
- call.name,
2265
- fixed.len(),
2266
- call.args.len()
2267
- ));
2268
- }
2269
- for (i, (arg, expected)) in
2270
- call.args.iter().zip(fixed.iter()).enumerate()
2271
- {
2272
- let arg_expr = match arg {
2921
+ if let Some(exp) = expected {
2273
- ast::Arg::Positional(e) => e,
2274
- ast::Arg::Keyword { value, .. } => value,
2275
- ast::Arg::Pair { value, .. } => value,
2276
- };
2277
- let actual = inferExpr(arg_expr, env, ctx)?;
2278
- unifyArg(expected, &actual, arg_expr, ctx).map_err(|e| {
2279
- format!("call '{}' arg {}: {}", call.name, i, e)
2280
- })?;
2281
- }
2282
- for (i, arg) in call.args.iter().enumerate().skip(fixed.len()) {
2283
- let arg_expr = match arg {
2284
- ast::Arg::Positional(e) => e,
2285
- ast::Arg::Keyword { value, .. } => value,
2286
- ast::Arg::Pair { value, .. } => value,
2287
- };
2288
- let actual = inferExpr(arg_expr, env, ctx)?;
2289
- unifyArg(elem, &actual, arg_expr, ctx).map_err(|e| {
2290
- format!("call '{}' variadic arg {}: {}", call.name, i, e)
2291
- })?;
2292
- }
2293
- Ok(*ret)
2294
- }
2295
- _ => {
2296
- // Fewer args than params is fine as long as every
2297
- // OMITTED one (always the trailing ones — see
2298
- // `MinRequiredArgs`'s doc comment) has a declared
2299
- // default; codegen fills those in from their own
2300
- // default expression at the call site.
2301
- let required = ctx
2302
- .min_required
2303
- .get(&(None, call.name.clone()))
2922
+ bindMatch(&ret, exp, &mut binds);
2304
- .copied()
2305
- .unwrap_or(param_types.len());
2306
- if call.args.len() < required || call.args.len() > param_types.len() {
2307
- return Err(format!(
2308
- "call '{}': expected {} args, got {}",
2309
- call.name,
2310
- param_types.len(),
2311
- call.args.len()
2312
- ));
2313
- }
2314
- for (i, (arg, expected)) in
2315
- call.args.iter().zip(param_types.iter()).enumerate()
2316
- {
2317
- let arg_expr = match arg {
2318
- ast::Arg::Positional(e) => e,
2319
- ast::Arg::Keyword { value, .. } => value,
2320
- ast::Arg::Pair { value, .. } => value,
2321
- };
2322
- let actual = inferExpr(arg_expr, env, ctx)?;
2323
- unifyArg(expected, &actual, arg_expr, ctx).map_err(|e| {
2324
- format!("call '{}' arg {}: {}", call.name, i, e)
2325
- })?;
2326
- }
2327
- Ok(*ret)
2328
- }
2329
2923
  }
2924
+ let required = ctx
2925
+ .min_required
2926
+ .get(&(None, call.name.clone()))
2927
+ .copied()
2928
+ .unwrap_or(param_types.len());
2929
+ let label = format!("call '{}'", call.name);
2930
+ checkCallArgs(&label, &param_types, &call.args, required, env, ctx, &mut binds)?;
2931
+ Ok(substVars(&ret, &binds))
2330
2932
  }
2331
2933
  Ok(_) => Err(format!("'{}' is not a function", call.name)),
2332
2934
  Err(_) if isTagLiteralName(&call.name, ctx) && call.args.len() <= 1 => {
2333
2935
  // `Bad(x)` — an undeclared uppercase name applied to one arg is
2334
2936
  // a tag literal with a payload.
2335
2937
  let payload = match call.args.first() {
2336
- Some(arg) => {
2337
- let arg_expr = match arg {
2338
- ast::Arg::Positional(e) => e,
2339
- ast::Arg::Keyword { value, .. } => value,
2340
- ast::Arg::Pair { value, .. } => value,
2341
- };
2342
- Some(Box::new(inferExpr(arg_expr, env, ctx)?))
2938
+ Some(arg) => Some(Box::new(inferExpr(argExpr(arg), env, ctx)?)),
2343
- }
2344
2939
  None => None,
2345
2940
  };
2346
2941
  Ok(PlumType::TTags(BTreeMap::from([(
@@ -2358,7 +2953,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
2358
2953
  }
2359
2954
  }
2360
2955
  ast::Expr::ClassCall(call) => {
2361
- let bare = inferClassCallRaw(call, env, ctx)?;
2956
+ let bare = inferClassCallRaw(call, env, ctx, expected)?;
2362
2957
  Ok(match bare {
2363
2958
  PlumType::TNamed(n) => bareVariantWrapType(&n, ctx),
2364
2959
  other => other,
@@ -2368,157 +2963,20 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
2368
2963
  let obj_ty = inferExpr(&attr.object, env, ctx)?;
2369
2964
  match &attr.attr {
2370
2965
  ast::AttrKind::Field(field_name) => match &obj_ty {
2371
- // `Str` is an ordinary class (`type Str(...) = data: Buffer
2966
+ // `Str` is an ordinary class with a real field; a generic
2372
- // ...` in `str.plum`) with a real field, unlike historically
2373
- // (see the `other` hard-error arm's comment below) — resolve
2967
+ // class substitutes its receiver's type arguments.
2374
- // it the same way `TNamed` does, just keyed by the literal
2375
- // name "Str" since `TStr` itself carries no name.
2376
- PlumType::TNamed(_) | PlumType::TStr => {
2968
+ PlumType::TNamed(_) | PlumType::TStr | PlumType::TApp(..) => {
2377
- let class_name: &str = match &obj_ty {
2378
- PlumType::TNamed(n) => n.as_str(),
2379
- _ => "Str",
2380
- };
2381
- lookupFieldType(class_name, field_name, ctx)
2969
+ fieldTypeOf(&obj_ty, field_name, ctx)
2382
2970
  }
2383
- // An unresolved generic method return (e.g. `Try[T, E].unwrap()`'s
2971
+ // An unresolved generic return reaches here as a plain `TVar`;
2384
- // bare `T`, which this checker's `PlumType` erases entirely — see
2385
- // `types.rs`'s `TNamed` having no type-argument slot) reaches here as
2386
- // a plain `TVar`, not a real "wrong type". Field access on it is
2387
- // "unmodeled" the same way an unknown enum's fields already are just
2388
- // above (`ctx.enum_params.get` returning `None`) — allow it and let
2389
- // codegen (which DOES track concrete monomorphized types) catch a
2972
+ // field access on it is unmodeled — allow, codegen will catch.
2390
- // genuine mismatch. Only a definitely-wrong concrete type (`Int`,
2391
- // `Str`, ...) still hard-errors here.
2392
2973
  PlumType::TVar(_) => Ok(PlumType::TVar("_".to_string())),
2393
2974
  other => Err(format!(
2394
2975
  "cannot access field '{}' on non-class type {}",
2395
2976
  field_name, other
2396
2977
  )),
2397
2978
  },
2398
- ast::AttrKind::Method(call) => {
2979
+ ast::AttrKind::Method(call) => inferMethodCall(&obj_ty, call, env, ctx, expected),
2399
- match methodReceiverName(&obj_ty) {
2400
- Some(class_name) => {
2401
- // Fallback: a primitive/class receiver's method might be
2402
- // defined once on the "union" enum that bare-wraps it
2403
- // instead (`enum Number = | Int | Float`) — `class_name`
2404
- // (e.g. "Int") is itself a registered enum-variant name
2405
- // in that case (see `buildGlobalTables`'s bare-wrap
2406
- // conditions); retry there before falling through to the
2407
- // permissive "unmodeled method" case below. Mirrors
2408
- // `plum-wasm-codegen`'s equivalent dispatch fallback,
2409
- // which also boxes `self` into the wrap-enum's variant.
2410
- let class_name = if ctx
2411
- .methods
2412
- .contains_key(&(class_name.clone(), call.name.clone()))
2413
- {
2414
- class_name
2415
- } else {
2416
- ctx.enum_variants
2417
- .get(class_name.as_str())
2418
- .map(|info| info.enum_name.clone())
2419
- .filter(|enum_name| {
2420
- ctx.methods
2421
- .contains_key(&(enum_name.clone(), call.name.clone()))
2422
- })
2423
- .unwrap_or(class_name)
2424
- };
2425
- match ctx.methods.get(&(class_name.clone(), call.name.clone())) {
2426
- Some(PlumType::TFun(param_types, ret)) => {
2427
- match param_types.last() {
2428
- Some(PlumType::TVariadic(elem)) => {
2429
- let fixed = &param_types[..param_types.len() - 1];
2430
- if call.args.len() < fixed.len() {
2431
- return Err(format!(
2432
- "method '{}.{}': expected at least {} arg(s), got {}",
2433
- class_name, call.name, fixed.len(), call.args.len()
2434
- ));
2435
- }
2436
- for (i, (arg, expected)) in
2437
- call.args.iter().zip(fixed.iter()).enumerate()
2438
- {
2439
- let arg_expr = match arg {
2440
- ast::Arg::Positional(e) => e,
2441
- ast::Arg::Keyword { value, .. } => value,
2442
- ast::Arg::Pair { value, .. } => value,
2443
- };
2444
- let actual = inferExpr(arg_expr, env, ctx)?;
2445
- unifyArg(expected, &actual, arg_expr, ctx)
2446
- .map_err(|e| {
2447
- format!(
2448
- "method '{}.{}' arg {}: {}",
2449
- class_name, call.name, i, e
2450
- )
2451
- })?;
2452
- }
2453
- for (i, arg) in
2454
- call.args.iter().enumerate().skip(fixed.len())
2455
- {
2456
- let arg_expr = match arg {
2457
- ast::Arg::Positional(e) => e,
2458
- ast::Arg::Keyword { value, .. } => value,
2459
- ast::Arg::Pair { value, .. } => value,
2460
- };
2461
- let actual = inferExpr(arg_expr, env, ctx)?;
2462
- unifyArg(elem, &actual, arg_expr, ctx).map_err(
2463
- |e| {
2464
- format!(
2465
- "method '{}.{}' variadic arg {}: {}",
2466
- class_name, call.name, i, e
2467
- )
2468
- },
2469
- )?;
2470
- }
2471
- Ok(*ret.clone())
2472
- }
2473
- _ => {
2474
- // See the matching comment on the free-function
2475
- // arm above — fewer args than params is fine as
2476
- // long as every omitted (trailing) one has a
2477
- // declared default.
2478
- let required = ctx
2479
- .min_required
2480
- .get(&(Some(class_name.clone()), call.name.clone()))
2481
- .copied()
2482
- .unwrap_or(param_types.len());
2483
- if call.args.len() < required
2484
- || call.args.len() > param_types.len()
2485
- {
2486
- return Err(format!(
2487
- "method '{}.{}': expected {} args, got {}",
2488
- class_name,
2489
- call.name,
2490
- param_types.len(),
2491
- call.args.len()
2492
- ));
2493
- }
2494
- for (i, (arg, expected)) in
2495
- call.args.iter().zip(param_types.iter()).enumerate()
2496
- {
2497
- let arg_expr = match arg {
2498
- ast::Arg::Positional(e) => e,
2499
- ast::Arg::Keyword { value, .. } => value,
2500
- ast::Arg::Pair { value, .. } => value,
2501
- };
2502
- let actual = inferExpr(arg_expr, env, ctx)?;
2503
- unifyArg(expected, &actual, arg_expr, ctx)
2504
- .map_err(|e| {
2505
- format!(
2506
- "method '{}.{}' arg {}: {}",
2507
- class_name, call.name, i, e
2508
- )
2509
- })?;
2510
- }
2511
- Ok(*ret.clone())
2512
- }
2513
- }
2514
- }
2515
- // Unmodeled method (e.g. builtin/std): allow, codegen will catch.
2516
- _ => Ok(PlumType::TVar("_".to_string())),
2517
- }
2518
- }
2519
- None => Ok(PlumType::TVar("_".to_string())),
2520
- }
2521
- }
2522
2980
  }
2523
2981
  }
2524
2982
  }
plum-checker/src/monomorphize.rs CHANGED
@@ -2354,6 +2354,7 @@ impl<'a> Monomorphizer<'a> {
2354
2354
  field_types,
2355
2355
  field_names: v.field_names.clone(),
2356
2356
  values: v.values.clone(),
2357
+ generics: Vec::new(),
2357
2358
  },
2358
2359
  );
2359
2360
  }
@@ -3100,6 +3101,7 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
3100
3101
  field_types,
3101
3102
  field_names,
3102
3103
  values: v.values.clone(),
3104
+ generics: Vec::new(),
3103
3105
  },
3104
3106
  );
3105
3107
  }
plum-checker/tests/checker_tests.rs CHANGED
@@ -2,7 +2,7 @@
2
2
  use plum_checker::types::*;
3
3
  use plum_checker::{
4
4
  checkSource, erasedEnumName, plumTypeFromAst, plumTypeFromName, tagUnionSegmentsCompatible,
5
- unify, CheckCtx,
5
+ typedSource, unify, CheckCtx,
6
6
  };
7
7
  use plum_core::ast::Type as AstType;
8
8
  use plum_core::{ast::*, AstParser};
@@ -1749,12 +1749,12 @@ fun main() -> Int =
1749
1749
 
1750
1750
  #[test]
1751
1751
  fn unpinnedOkIsNotATagLiteral() {
1752
+ // Structural generics: `Ok(3)` with nothing pinning `E` is a `Try[Int, ?]`
1752
- // (needs some concrete `Try` specialization to exist, as in any real
1753
+ // (the unresolved `E` stays open), never a tag literal named `Ok`.
1753
- // program importing std/Try; with none, the checker can't tell `Ok` from a tag)
1754
1754
  let r = checkTry("fun one() -> Try[Int, [Nope]] =\n Ok(1)\n\nfun main() =\n x := Ok(3)\n");
1755
- let msg = errText(r);
1756
- assert!(msg.contains("cannot infer the type parameters of 'Ok'"), "{}", msg);
1757
- assert!(!msg.contains("[Ok("), "{}", msg);
1755
+ assert!(r.is_ok(), "{:?}", r.err());
1756
+ let bad = checkTry("fun f() -> Try[Str, [Nope]] =\n Ok(1)\n");
1757
+ assert!(errText(bad).contains("variant 'Ok' arg 0"), "pinned T still checked");
1758
1758
  }
1759
1759
 
1760
1760
  #[test]
@@ -1959,3 +1959,208 @@ fun f(s: Step) -> Int =
1959
1959
  let r = errText(enumMatch(def, " ReadMin => 1\n"));
1960
1960
  assert!(r.contains("non-exhaustive match on Step: missing ReadMax"), "got: {}", r);
1961
1961
  }
1962
+
1963
+ fn repoRoot() -> std::path::PathBuf {
1964
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
1965
+ }
1966
+
1967
+ /// Acceptance: every std and example file checks with zero errors on the
1968
+ /// ORIGINAL (un-monomorphized) source.
1969
+ #[test]
1970
+ fn everyStdAndExampleChecksStructurally() {
1971
+ let mut failures: Vec<String> = Vec::new();
1972
+ for dir in ["plum-std", "plum-examples"] {
1973
+ let mut files: Vec<_> = std::fs::read_dir(repoRoot().join(dir))
1974
+ .unwrap()
1975
+ .filter_map(|e| e.ok())
1976
+ .map(|e| e.path())
1977
+ .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("plum"))
1978
+ .collect();
1979
+ files.sort();
1980
+ for path in files {
1981
+ let source = plum_core::loadAndMerge(&path, &repoRoot())
1982
+ .unwrap_or_else(|e| panic!("failed to load {}: {}", path.display(), e));
1983
+ if let Err(errs) = checkSource(&source) {
1984
+ for e in errs {
1985
+ failures.push(format!(
1986
+ "{}: {}",
1987
+ path.file_name().unwrap().to_string_lossy(),
1988
+ e.message
1989
+ ));
1990
+ }
1991
+ }
1992
+ }
1993
+ }
1994
+ assert!(failures.is_empty(), "structural check errors:\n{}", failures.join("\n"));
1995
+ }
1996
+
1997
+
1998
+ // ---- structural generics (T2) ----
1999
+
2000
+ const GENERIC_DEFS: &str = "\
2001
+ enum Try[T, E] =
2002
+ | Ok(T)
2003
+ | Err(E)
2004
+
2005
+ enum List[T] =
2006
+ | List(items: T)
2007
+
2008
+ fun map(self, cb: fn(T) -> U) -> List[U] =
2009
+ List(items: cb(self.items))
2010
+
2011
+ enum Box[T] =
2012
+ | Box(value: T)
2013
+
2014
+ ";
2015
+
2016
+ fn tapp(name: &str, args: Vec<PlumType>) -> PlumType {
2017
+ PlumType::TApp(name.to_string(), args)
2018
+ }
2019
+
2020
+ fn tagsA() -> PlumType {
2021
+ PlumType::TTags(std::collections::BTreeMap::from([("A".to_string(), None)]))
2022
+ }
2023
+
2024
+ /// The instantiated type of the body expression of the LAST fn in `src`.
2025
+ fn lastFnBodyType(src: &str) -> PlumType {
2026
+ let source = parseClean(&format!("{}{}", GENERIC_DEFS, src));
2027
+ let info = typedSource(&source).unwrap_or_else(|e| {
2028
+ panic!(
2029
+ "unexpected errors: {:?}",
2030
+ e.iter().map(|x| x.message.clone()).collect::<Vec<_>>()
2031
+ )
2032
+ });
2033
+ let f = source
2034
+ .items
2035
+ .iter()
2036
+ .rev()
2037
+ .find_map(|i| match i {
2038
+ Item::Fn(f) => Some(f),
2039
+ _ => None,
2040
+ })
2041
+ .unwrap();
2042
+ let e = match &f.body {
2043
+ FnBody::Expr(e) => e,
2044
+ FnBody::Block(b) => match b.stmts.last() {
2045
+ Some(Stmt::Expr(e, ..)) => e,
2046
+ _ => panic!("no tail expression"),
2047
+ },
2048
+ _ => panic!("no body"),
2049
+ };
2050
+ info.expr_types
2051
+ .get(&(e as *const Expr as usize))
2052
+ .cloned()
2053
+ .expect("body expression has a recorded type")
2054
+ }
2055
+
2056
+ #[test]
2057
+ fn method_own_generic_instantiated_per_call() {
2058
+ assert_eq!(
2059
+ lastFnBodyType("fun f(xs: List[Int]) -> List[Int] =\n xs.map(|n| n * 2)\n"),
2060
+ tapp("List", vec![PlumType::TInt])
2061
+ );
2062
+ assert_eq!(
2063
+ lastFnBodyType("fun f(xs: List[Int]) -> List[Str] =\n xs.map(|n| \"{n}\")\n"),
2064
+ tapp("List", vec![PlumType::TStr])
2065
+ );
2066
+ // The two instantiations are independent: Str result where Int is declared is an error.
2067
+ let bad = checkSource(&parseClean(&format!(
2068
+ "{}fun f(xs: List[Int]) -> List[Int] =\n xs.map(|n| \"{{n}}\")\n",
2069
+ GENERIC_DEFS
2070
+ )));
2071
+ assert!(errText(bad).contains("type mismatch"));
2072
+ }
2073
+
2074
+ #[test]
2075
+ fn nested_generic_field() {
2076
+ let tryIntA = tapp("Try", vec![PlumType::TInt, tagsA()]);
2077
+ assert_eq!(
2078
+ lastFnBodyType("fun f(b: Box[Try[Int, [A]]]) -> Try[Int, [A]] =\n b.value\n"),
2079
+ tryIntA
2080
+ );
2081
+ }
2082
+
2083
+ #[test]
2084
+ fn unbounded_tparam_method_error() {
2085
+ let r = checkSource(&parseClean("fun f(x: T) -> Int =\n x.hash()\n"));
2086
+ assert!(
2087
+ errText(r).contains("'T' has no method 'hash' (add a bound)"),
2088
+ "unbounded T must not resolve methods"
2089
+ );
2090
+ }
2091
+
2092
+ #[test]
2093
+ fn bounded_ok() {
2094
+ let src = "\
2095
+ trait Hashable =
2096
+ hash() -> Int
2097
+
2098
+ enum Keyed[T: Hashable] =
2099
+ | Keyed(value: T)
2100
+
2101
+ fun h(self) -> Int =
2102
+ self.value.hash()
2103
+ ";
2104
+ let r = checkSource(&parseClean(src));
2105
+ assert!(r.is_ok(), "got {:?}", r.err());
2106
+ }
2107
+
2108
+ #[test]
2109
+ fn tags_covariant_in_tapp() {
2110
+ let defs = "\
2111
+ fun wide(t: Try[Int, [A, B]]) -> Int =
2112
+ 1
2113
+
2114
+ fun narrow() -> Try[Int, [A]] =
2115
+ Err(A)
2116
+
2117
+ fun ok() -> Int =
2118
+ wide(narrow())
2119
+ ";
2120
+ let r = checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, defs)));
2121
+ assert!(r.is_ok(), "got {:?}", r.err());
2122
+ // ...but not the other way around.
2123
+ let rev = "\
2124
+ fun narrow(t: Try[Int, [A]]) -> Int =
2125
+ 1
2126
+
2127
+ fun wide() -> Try[Int, [A, B]] =
2128
+ Err(A)
2129
+
2130
+ fun bad() -> Int =
2131
+ narrow(wide())
2132
+ ";
2133
+ let r = checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, rev)));
2134
+ assert!(r.is_err());
2135
+ }
2136
+
2137
+ #[test]
2138
+ fn typed_source_records_every_instantiated_expr() {
2139
+ let source = parseClean(&format!(
2140
+ "{}fun f() -> Try[Int, [A]] =\n Ok(1)\n",
2141
+ GENERIC_DEFS
2142
+ ));
2143
+ let info = typedSource(&source).unwrap();
2144
+ assert!(!info.expr_types.is_empty());
2145
+ assert_eq!(
2146
+ lastFnBodyType("fun f() -> Try[Int, [A]] =\n Ok(1)\n"),
2147
+ tapp("Try", vec![PlumType::TInt, tagsA()])
2148
+ );
2149
+ }
2150
+
2151
+ #[test]
2152
+ fn generic_free_fn_instantiated_per_call() {
2153
+ let defs = "\
2154
+ fun identity(value: T) -> T =
2155
+ value
2156
+
2157
+ fun a() -> Int =
2158
+ identity(5)
2159
+
2160
+ fun b() -> Str =
2161
+ identity(\"x\")
2162
+ ";
2163
+ assert!(checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, defs))).is_ok());
2164
+ let bad = "fun identity(value: T) -> T =\n value\n\nfun a() -> Str =\n identity(5)\n";
2165
+ assert!(checkSource(&parseClean(bad)).is_err());
2166
+ }