plum

#treesitter#compiler#wasm

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

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


7d25f15 — Peter John 2026-10-26T10:03:07+05:30
refactor!: delete the monomorphizer; List on Array; reject bare generic types
plum-checker/src/lib.rs CHANGED
@@ -2,7 +2,6 @@
2
2
  // naming convention), not Rust's idiomatic snake_case — silence the resulting lint.
3
3
  #![allow(non_snake_case)]
4
4
 
5
- pub mod monomorphize;
6
5
  pub mod types;
7
6
 
8
7
  use plum_core::ast;
@@ -183,8 +182,32 @@ fn bindMatch(pattern: &PlumType, actual: &PlumType, b: &mut Binds) {
183
182
  }
184
183
  }
185
184
 
185
+ /// A single uppercase letter (`T`, `U`, `K`, ...) is the grammar's only legal
186
+ /// spelling for a generic type parameter.
186
- fn isGenericName(n: &str) -> bool {
187
+ pub fn isGenericName(n: &str) -> bool {
188
+ let mut chars = n.chars();
187
- monomorphize::isGenericParamName(n)
189
+ match (chars.next(), chars.next()) {
190
+ (Some(c), None) => c.is_ascii_uppercase(),
191
+ _ => false,
192
+ }
193
+ }
194
+
195
+ /// The generic parameter names an `Enum` introduces: its own declared
196
+ /// generics (`enum Link[T] = ...`), else every distinct single-uppercase-letter
197
+ /// variant field type name in first-appearance order.
198
+ pub fn enumGenericParams(e: &ast::Enum) -> Vec<String> {
199
+ if !e.generics.is_empty() {
200
+ return e.generics.iter().map(|g| g.name.clone()).collect();
201
+ }
202
+ let mut names: Vec<String> = Vec::new();
203
+ for v in &e.variants {
204
+ for field_ty in &v.fields {
205
+ if isGenericName(&field_ty.name) && !names.iter().any(|x| x == &field_ty.name) {
206
+ names.push(field_ty.name.clone());
207
+ }
208
+ }
209
+ }
210
+ names
188
211
  }
189
212
 
190
213
  /// Structural type conversion for UN-monomorphized source: `List[Int]` ->
@@ -371,66 +394,6 @@ fn parseTagUnion(s: &str) -> Option<PlumType> {
371
394
  Some(PlumType::TTags(tags))
372
395
  }
373
396
 
374
- /// Replaces every tag-union segment (`[...]`, but not the `[]Byte` slice
375
- /// spelling) of a mangled name with `Tags`, so specializations that differ
376
- /// only in their tag unions share one runtime type:
377
- /// `erasedEnumName("Try$Int$[A,B(Str)]") == "Try$Int$Tags"`.
378
- pub fn erasedEnumName(name: &str) -> String {
379
- let chars: Vec<char> = name.chars().collect();
380
- let mut out = String::new();
381
- let mut i = 0;
382
- while i < chars.len() {
383
- if chars[i] == '[' && chars.get(i + 1) != Some(&']') {
384
- let mut depth = 0;
385
- while i < chars.len() {
386
- match chars[i] {
387
- '[' => depth += 1,
388
- ']' => depth -= 1,
389
- _ => {}
390
- }
391
- i += 1;
392
- if depth == 0 {
393
- break;
394
- }
395
- }
396
- out.push_str("Tags");
397
- } else {
398
- out.push(chars[i]);
399
- i += 1;
400
- }
401
- }
402
- out
403
- }
404
-
405
- /// True when two mangled specialization names (`Try$Int$[A,B]`) are identical
406
- /// except for their tag-union segments and every `actual` union is a subset of
407
- /// the matching `expected` one (E is covariant).
408
- pub fn tagUnionSegmentsCompatible(expected: &str, actual: &str, ctx: &CheckCtx) -> bool {
409
- let e_segs = splitTopLevel(expected, '$');
410
- let a_segs = splitTopLevel(actual, '$');
411
- if e_segs.len() != a_segs.len() || e_segs.len() < 2 {
412
- return false;
413
- }
414
- let mut saw_union = false;
415
- for (e, a) in e_segs.iter().zip(a_segs.iter()) {
416
- if e == a {
417
- continue;
418
- }
419
- let both_unions = e.starts_with('[')
420
- && !e.starts_with("[]")
421
- && a.starts_with('[')
422
- && !a.starts_with("[]");
423
- if !both_unions {
424
- return false;
425
- }
426
- saw_union = true;
427
- if unify(&plumTypeFromName(e), &plumTypeFromName(a), ctx).is_err() {
428
- return false;
429
- }
430
- }
431
- saw_union
432
- }
433
-
434
397
  /// Reserved `TypeEnv` key holding the enclosing function's declared return type.
435
398
  const RETURN_KEY: &str = "$return";
436
399
  /// `TypeEnv` key prefix marking a name as a top-level function (a local of the
@@ -585,12 +548,6 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
585
548
  }
586
549
  Ok(())
587
550
  }
588
- // Two specializations differing only in their tag-union segments.
589
- (PlumType::TNamed(a), PlumType::TNamed(b))
590
- if a != b && tagUnionSegmentsCompatible(a, b, ctx) =>
591
- {
592
- Ok(())
593
- }
594
551
  // A bare enum variant name (`Red`) can be used directly as a type — e.g.
595
552
  // a fn param `v: Red` — meaning "a `Color` value that is specifically
596
553
  // the `Red` variant". A variant's OWN constructions/references always
@@ -778,23 +735,7 @@ pub struct CheckCtx<'a> {
778
735
  /// `plum-wasm-codegen`, which needs the same tables to resolve `self`, field access,
779
736
  /// and method dispatch during code generation.
780
737
  pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVariants, EnumParams) {
781
- buildTables(source, false)
782
- }
783
-
784
- /// `generic == true` builds the structural tables for UN-monomorphized source
785
- /// (`TApp`/`TParam`, generic schemes, trait signatures); `false` is the legacy
786
- /// shape codegen uses on monomorphized source.
787
- pub fn buildTables(
788
- source: &ast::Source,
789
- generic: bool,
790
- ) -> (TypeEnv, MethodEnv, EnumVariants, EnumParams) {
791
- let conv = |t: &ast::Type| {
792
- if generic {
793
- plumTypeFromAstG(t)
738
+ let conv = plumTypeFromAstG;
794
- } else {
795
- plumTypeFromAst(t)
796
- }
797
- };
798
739
  let mut global_env: TypeEnv = TypeEnv::new();
799
740
  let mut methods: MethodEnv = BTreeMap::new();
800
741
  let mut enum_variants: EnumVariants = BTreeMap::new();
@@ -877,11 +818,7 @@ pub fn buildTables(
877
818
  field_types,
878
819
  field_names,
879
820
  values: v.values.clone(),
880
- generics: if generic {
881
- monomorphize::enumGenericParams(e)
821
+ generics: enumGenericParams(e),
882
- } else {
883
- Vec::new()
884
- },
885
822
  },
886
823
  );
887
824
  }
@@ -916,7 +853,7 @@ pub fn buildTables(
916
853
  let fn_ty = PlumType::TFun(param_types, Box::new(ret));
917
854
  if let Some(recv) = &f.type_param {
918
855
  methods.insert((recv.clone(), f.name.clone()), fn_ty);
919
- } else if generic {
856
+ } else {
920
857
  global_env.insert(
921
858
  format!("{}{}", GLOBAL_KEY, f.name),
922
859
  TypeScheme::mono(PlumType::TUnit),
@@ -930,8 +867,6 @@ pub fn buildTables(
930
867
  body: Box::new(fn_ty),
931
868
  },
932
869
  );
933
- } else {
934
- global_env.insert(f.name.clone(), TypeScheme::mono(fn_ty));
935
870
  }
936
871
  }
937
872
  ast::Item::Const(c) => {
@@ -973,13 +908,11 @@ pub fn buildTables(
973
908
  let ret = m.returns.as_ref().map(&conv).unwrap_or(PlumType::TUnit);
974
909
  let sig = PlumType::TFun(params, Box::new(ret));
975
910
  methods.insert((t.name.clone(), m.name.clone()), sig.clone());
976
- if generic {
977
- // Looked up for calls on a bounded `TParam`.
911
+ // Looked up for calls on a bounded `TParam`.
978
- global_env.insert(
912
+ global_env.insert(
979
- format!("{}{}:{}", TRAIT_KEY, t.name, m.name),
913
+ format!("{}{}:{}", TRAIT_KEY, t.name, m.name),
980
- TypeScheme::mono(sig),
914
+ TypeScheme::mono(sig),
981
- );
915
+ );
982
- }
983
916
  }
984
917
  }
985
918
  _ => {}
@@ -1122,11 +1055,89 @@ pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
1122
1055
  })
1123
1056
  }
1124
1057
 
1058
+ /// A generic type named without type arguments (`p: Parcel` for `enum
1059
+ /// Parcel[T]`) has no known payload representation at runtime, so it is
1060
+ /// rejected in signatures and field types. Inside the generic's own methods the
1061
+ /// bare name is allowed (it means the receiver's own type).
1062
+ fn checkBareGenericTypes(source: &ast::Source) -> Vec<CheckError> {
1063
+ let generic_enums: BTreeMap<&str, usize> = source
1064
+ .items
1065
+ .iter()
1066
+ .filter_map(|i| match i {
1067
+ ast::Item::Enum(e) if !e.generics.is_empty() => Some((e.name.as_str(), e.generics.len())),
1068
+ _ => None,
1069
+ })
1070
+ .collect();
1071
+ let mut errors = Vec::new();
1072
+ if generic_enums.is_empty() {
1073
+ return errors;
1074
+ }
1075
+ fn walk(
1076
+ t: &ast::Type,
1077
+ scope: Option<&str>,
1078
+ line: usize,
1079
+ column: usize,
1080
+ generic_enums: &BTreeMap<&str, usize>,
1081
+ errors: &mut Vec<CheckError>,
1082
+ ) {
1083
+ if t.generics.is_empty() && scope != Some(t.name.as_str()) {
1084
+ if generic_enums.contains_key(t.name.as_str()) {
1085
+ errors.push(CheckError::at(
1086
+ line,
1087
+ column,
1088
+ format!(
1089
+ "generic type '{0}' needs type arguments, e.g. {0}[T]",
1090
+ t.name
1091
+ ),
1092
+ ));
1093
+ }
1094
+ }
1095
+ for g in &t.generics {
1096
+ walk(g, scope, line, column, generic_enums, errors);
1097
+ }
1098
+ }
1099
+ for item in &source.items {
1100
+ match item {
1101
+ ast::Item::Fn(f) => {
1102
+ let scope = f.type_param.as_deref();
1103
+ let mut w = |t: &ast::Type| walk(t, scope, f.line, f.column, &generic_enums, &mut errors);
1104
+ for p in &f.params {
1105
+ match &p.ty {
1106
+ ast::ParamType::Type(t) | ast::ParamType::Variadic(t) => w(t),
1107
+ ast::ParamType::Fn(ps, ret) => {
1108
+ ps.iter().for_each(&mut w);
1109
+ if let Some(r) = ret {
1110
+ w(r);
1111
+ }
1112
+ }
1113
+ }
1114
+ }
1115
+ if let Some(r) = &f.returns {
1116
+ w(r);
1117
+ }
1118
+ }
1119
+ ast::Item::Enum(e) => {
1120
+ for ty in e
1121
+ .variants
1122
+ .iter()
1123
+ .flat_map(|v| v.fields.iter())
1124
+ .chain(e.params.iter().map(|p| &p.ty))
1125
+ {
1126
+ walk(ty, Some(e.name.as_str()), e.line, e.column, &generic_enums, &mut errors);
1127
+ }
1128
+ }
1129
+ _ => {}
1130
+ }
1131
+ }
1132
+ errors
1133
+ }
1134
+
1125
1135
  fn checkSourceInner(source: &ast::Source) -> CheckResult<()> {
1126
1136
  // Generic items are checked once, structurally (`TApp`/`TParam`), on the
1127
1137
  // ORIGINAL source — no monomorphization.
1128
1138
  let mut errors: Vec<CheckError> = checkTraitConformance(source);
1139
+ errors.extend(checkBareGenericTypes(source));
1129
- let (global_env, methods, enum_variants, enum_params) = buildTables(source, true);
1140
+ let (global_env, methods, enum_variants, enum_params) = buildGlobalTables(source);
1130
1141
  let min_required = buildMinRequiredArgs(source);
1131
1142
  let ctx = CheckCtx {
1132
1143
  methods: &methods,
@@ -1784,10 +1795,10 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
1784
1795
  }
1785
1796
  vs.sort_by_key(|(_, i)| i.tag);
1786
1797
  Some((
1787
- variantBaseName(enum_name).to_string(),
1798
+ enum_name.to_string(),
1788
1799
  vs.into_iter()
1789
1800
  .map(|(k, i)| CoverCtor {
1790
- name: variantBaseName(k).to_string(),
1801
+ name: k.to_string(),
1791
1802
  // A discriminant enum's field types are its shared
1792
1803
  // params, which patterns never destructure.
1793
1804
  fields: if ctx.enum_params.contains_key(enum_name) {
@@ -1843,7 +1854,7 @@ fn coverRows<'a>(
1843
1854
  if patCatchAll(r[0]) {
1844
1855
  row.extend(std::iter::repeat(&COVER_WILDCARD).take(c.fields.len()));
1845
1856
  } else if let Some((n, fs)) = patCtor(r[0]) {
1846
- if tagLiteralName(variantBaseName(n)) != c.name {
1857
+ if n != c.name {
1847
1858
  continue;
1848
1859
  }
1849
1860
  if let ast::CasePattern::Name(_) = r[0] {
@@ -2383,14 +2394,6 @@ pub(crate) fn lookupFieldType(
2383
2394
  }
2384
2395
  }
2385
2396
 
2386
- /// A mangled variant name's own unmangled base (`"Ok$Int$Str"` -> `"Ok"`,
2387
- /// `"End"` -> `"End"`) — `monomorphize::mangle` always joins the original
2388
- /// variant name and its resolved type args with `$`, so the base is simply
2389
- /// everything before the first `$`.
2390
- fn variantBaseName(name: &str) -> &str {
2391
- name.split('$').next().unwrap_or(name)
2392
- }
2393
-
2394
2397
  /// Finds the "success" (`Ok`) and "failure" (`Err`) variants
2395
2398
  /// belonging to the same enum as `enum_name`, for the `?` operator (`Expr::Try`)
2396
2399
  /// — used identically by the checker (`inferExpr`) and codegen (`inferLocalType`
@@ -2407,7 +2410,7 @@ pub fn tryOperatorVariants<'a>(
2407
2410
  if info.enum_name != enum_name {
2408
2411
  continue;
2409
2412
  }
2410
- match variantBaseName(vname) {
2413
+ match vname.as_str() {
2411
2414
  "Ok" => success = Some((vname.as_str(), info)),
2412
2415
  "Err" => failure = Some((vname.as_str(), info)),
2413
2416
  _ => {}
@@ -2419,37 +2422,10 @@ pub fn tryOperatorVariants<'a>(
2419
2422
  }
2420
2423
  }
2421
2424
 
2422
- /// Prefix the monomorphizer puts on a tag literal whose name a declared variant
2423
- /// also uses, at a site whose expected type is a tag union naming it
2424
- /// (`Err(Bad)` in `-> Try[Int, [Bad]]` next to `enum Rating = | Bad(...)`).
2425
- /// Not a legal identifier character, so it can't clash with source names.
2426
- pub const TAG_MARKER: char = '#';
2427
-
2428
- /// The tag name a tag-literal expression name stands for (drops `TAG_MARKER`).
2429
- pub fn tagLiteralName(n: &str) -> &str {
2430
- n.strip_prefix(TAG_MARKER).unwrap_or(n)
2431
- }
2432
-
2433
- /// True if `n` is the base name of a generic enum, or of one of its variants,
2434
- /// that only survives monomorphization under mangled names (`Ok` ->
2435
- /// `Ok$Int$[X]`, `Try` -> `Try$Int$[X]`).
2436
- fn isGenericEnumBaseName(n: &str, ctx: &CheckCtx) -> bool {
2437
- let base = |k: &str| k.split_once('$').map(|(b, _)| b == n).unwrap_or(false);
2438
- !ctx.enum_variants.contains_key(n)
2439
- && ctx
2440
- .enum_variants
2441
- .iter()
2442
- .any(|(k, info)| base(k) || base(&info.enum_name))
2443
- }
2444
-
2445
2425
  /// True if `n` is an uppercase name that is not a declared type, variant, method
2446
2426
  /// receiver, builtin type or const — i.e. a structural tag literal.
2447
2427
  fn isTagLiteralName(n: &str, ctx: &CheckCtx) -> bool {
2448
- if n.starts_with(TAG_MARKER) {
2449
- return true;
2450
- }
2451
2428
  n.chars().next().is_some_and(|c| c.is_uppercase())
2452
- && !isGenericEnumBaseName(n, ctx)
2453
2429
  && !matches!(
2454
2430
  n,
2455
2431
  "Int" | "Float" | "Bool" | "Str" | "Byte" | "Unit" | "Self"
@@ -2776,7 +2752,7 @@ fn inferExprInner(
2776
2752
  Ok(substVars(&result, &binds))
2777
2753
  }
2778
2754
  None if isTagLiteralName(n, ctx) => Ok(PlumType::TTags(BTreeMap::from([(
2779
- tagLiteralName(n).to_string(),
2755
+ n.to_string(),
2780
2756
  None,
2781
2757
  )]))),
2782
2758
  // Unmodeled/builtin type name: allow, codegen will catch.
@@ -3077,16 +3053,10 @@ fn inferExprInner(
3077
3053
  None => None,
3078
3054
  };
3079
3055
  Ok(PlumType::TTags(BTreeMap::from([(
3080
- tagLiteralName(&call.name).to_string(),
3056
+ call.name.to_string(),
3081
3057
  payload,
3082
3058
  )])))
3083
3059
  }
3084
- // `Ok(3)` that nothing pinned to a concrete specialization (no
3085
- // typed param/field/return to take `E` from) — it is never a tag.
3086
- Err(_) if isGenericEnumBaseName(&call.name, ctx) => Err(format!(
3087
- "cannot infer the type parameters of '{}' here: use it where its full type is known (a typed parameter, field or return)",
3088
- call.name
3089
- )),
3090
3060
  Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch
3091
3061
  }
3092
3062
  }
plum-checker/src/monomorphize.rs DELETED
@@ -1,3287 +0,0 @@
1
- use crate::types::PlumType;
2
- use plum_core::ast;
3
- use std::collections::BTreeMap;
4
-
5
- /// A single uppercase letter (`T`, `U`, `K`, ...) is the grammar's only legal
6
- /// spelling for a generic type parameter — this is how we recognize one, since
7
- /// `ast::Fn` (unlike `ast::Enum`/`ast::Trait`) carries no explicit generics
8
- /// declaration list.
9
- pub fn isGenericParamName(name: &str) -> bool {
10
- let mut chars = name.chars();
11
- match (chars.next(), chars.next()) {
12
- (Some(c), None) => c.is_ascii_uppercase(),
13
- _ => false,
14
- }
15
- }
16
-
17
- /// Collects every distinct single-uppercase-letter type name appearing
18
- /// anywhere in `t`, RECURSING into `t.generics` — so `List[T]` (a container
19
- /// type applied to the bare letter `T`) is recognized as introducing `T`,
20
- /// not just a bare `T` on its own. Order is first-appearance, depth-first.
21
- fn collectGenericParamNames(t: &ast::Type, names: &mut Vec<String>) {
22
- if isGenericParamName(&t.name) && !names.iter().any(|x| x == &t.name) {
23
- names.push(t.name.clone());
24
- }
25
- for g in &t.generics {
26
- collectGenericParamNames(g, names);
27
- }
28
- }
29
-
30
- /// The generic parameter names implicitly introduced by a `Fn` — every distinct
31
- /// single-uppercase-letter type name appearing in its params or return type
32
- /// (including nested inside a container type like `List[T]`, not just a bare
33
- /// `T` on its own — see `collectGenericParamNames`), in first-appearance order.
34
- pub fn fnGenericParams(f: &ast::Fn) -> Vec<String> {
35
- let mut names: Vec<String> = Vec::new();
36
- for p in &f.params {
37
- match &p.ty {
38
- ast::ParamType::Type(t) => collectGenericParamNames(t, &mut names),
39
- ast::ParamType::Variadic(t) => collectGenericParamNames(t, &mut names),
40
- // TODO: closures/fn-value params don't yet participate in generic
41
- // parameter inference.
42
- ast::ParamType::Fn(_, _) => {}
43
- }
44
- }
45
- if let Some(r) = &f.returns {
46
- collectGenericParamNames(r, &mut names);
47
- }
48
- names
49
- }
50
-
51
- /// The generic parameter names an `Enum` introduces — its OWN declared
52
- /// generics (`enum Link[T] = ...`) if it declares any, else (for an
53
- /// enum that doesn't) every distinct
54
- /// single-uppercase-letter variant field type name, in first-appearance
55
- /// order — the older, purely-implicit inference this replaces.
56
- pub fn enumGenericParams(e: &ast::Enum) -> Vec<String> {
57
- if !e.generics.is_empty() {
58
- return e.generics.iter().map(|g| g.name.clone()).collect();
59
- }
60
- let mut names: Vec<String> = Vec::new();
61
- for v in &e.variants {
62
- for field_ty in &v.fields {
63
- if isGenericParamName(&field_ty.name) && !names.iter().any(|x| x == &field_ty.name) {
64
- names.push(field_ty.name.clone());
65
- }
66
- }
67
- }
68
- names
69
- }
70
-
71
- /// Where to infer a method's OWN generic param's concrete binding from, at one
72
- /// call site — see `methodOwnGenericParams`.
73
- #[derive(Debug, Clone, Copy)]
74
- enum ExtraGenericSource {
75
- /// `f.params[i]`'s declared type IS the bare letter itself (e.g. `List.reduce`'s
76
- /// `acc: U`) — infer directly from that argument's own value.
77
- Param(usize),
78
- /// `f.params[i]` is `fn(...) -> <letter>` (e.g. `List.map`'s `cb: fn(T) -> U`) —
79
- /// the argument must be a closure literal; infer by evaluating its body with
80
- /// its own params seeded from the closure type's OTHER (already class-level-
81
- /// concrete) param types.
82
- ClosureReturn(usize),
83
- }
84
-
85
- /// The generic letters `f`'s signature introduces BEYOND `owner_params` (its
86
- /// enclosing class/enum's own declared generic params) — e.g. `List[T].map`'s
87
- /// `U`, distinct from `List[T]`'s own `T`. Unlike `fnGenericParams`, this DOES
88
- /// look inside closure-typed (`fn(...) -> ...`) param types, since that's
89
- /// exactly where a callback-taking method's own generic tends to live (a
90
- /// plain bare-letter param like `reduce`'s `acc: U` is also covered, via the
91
- /// same detection `fnGenericParams` already does for non-closure params). A
92
- /// closure's return type can ALSO nest the extra letter inside a container
93
- /// (`Link.andThen`'s `cb: fn(T) -> Link[U]`), not just be the bare
94
- /// letter itself (`List.map`'s `cb: fn(T) -> U`) — `collectGenericParamNames`
95
- /// recurses to find either shape; `resolveMethodOwnGenerics`'s
96
- /// `ClosureReturn` handling knows how to bind a nested one back out.
97
- fn methodOwnGenericParams(
98
- f: &ast::Fn,
99
- owner_params: &[String],
100
- ) -> Vec<(String, ExtraGenericSource)> {
101
- let mut result: Vec<(String, ExtraGenericSource)> = Vec::new();
102
- for (i, p) in f.params.iter().enumerate() {
103
- match &p.ty {
104
- ast::ParamType::Type(t)
105
- if isGenericParamName(&t.name) && !owner_params.iter().any(|o| o == &t.name) =>
106
- {
107
- if !result.iter().any(|(n, _)| n == &t.name) {
108
- result.push((t.name.clone(), ExtraGenericSource::Param(i)));
109
- }
110
- }
111
- ast::ParamType::Fn(_, Some(ret)) => {
112
- let mut ret_names: Vec<String> = Vec::new();
113
- collectGenericParamNames(ret, &mut ret_names);
114
- for name in ret_names {
115
- if !owner_params.iter().any(|o| o == &name)
116
- && !result.iter().any(|(n, _)| n == &name)
117
- {
118
- result.push((name, ExtraGenericSource::ClosureReturn(i)));
119
- }
120
- }
121
- }
122
- _ => {}
123
- }
124
- }
125
- result
126
- }
127
-
128
- /// A resolved binding from a generic item's parameter names to concrete types for
129
- /// one instantiation site, e.g. `{"a": Int}` for `Box(value: 5)`.
130
- #[derive(Debug, Clone)]
131
- pub struct Substitution(pub BTreeMap<String, PlumType>);
132
-
133
- impl Substitution {
134
- fn get(&self, name: &str) -> Option<&PlumType> {
135
- self.0.get(name)
136
- }
137
- }
138
-
139
- /// Converts a resolved concrete `PlumType` back into the `ast::Type` shape needed
140
- /// to substitute into a declared field/param/return type position. Only ever
141
- /// called with types resolved from a real call-site argument's inferred type, so
142
- /// `TVar`/`TFun` (which never arise from a concrete argument) are an internal-error
143
- /// case rather than something this needs to model.
144
- fn plumTypeToAstType(t: &PlumType) -> ast::Type {
145
- let name = match t {
146
- PlumType::TInt => "Int".to_string(),
147
- PlumType::TFloat => "Float".to_string(),
148
- PlumType::TBool => "Bool".to_string(),
149
- PlumType::TStr => "Str".to_string(),
150
- PlumType::TByte => "Byte".to_string(),
151
- PlumType::TByteSlice => "[]Byte".to_string(),
152
- PlumType::TUnit => "Unit".to_string(),
153
- PlumType::TNamed(n) | PlumType::TApp(n, _) => n.clone(),
154
- PlumType::TParam(n) => n.clone(),
155
- PlumType::TVar(_)
156
- | PlumType::TFun(_, _)
157
- | PlumType::TVariadic(_)
158
- | PlumType::TTags(_) => t.to_string(),
159
- };
160
- ast::Type {
161
- name,
162
- generics: vec![],
163
- }
164
- }
165
-
166
- fn substituteType(ty: &ast::Type, subst: &Substitution) -> ast::Type {
167
- if ty.generics.is_empty() {
168
- if let Some(concrete) = subst.get(&ty.name) {
169
- return plumTypeToAstType(concrete);
170
- }
171
- }
172
- ast::Type {
173
- name: ty.name.clone(),
174
- generics: ty
175
- .generics
176
- .iter()
177
- .map(|g| substituteType(g, subst))
178
- .collect(),
179
- }
180
- }
181
-
182
- /// Mangles a generic item's base name and its resolved concrete type arguments
183
- /// (in the item's own generic-parameter declaration order) into the internal name
184
- /// used for its specialized copy, e.g. `Box` + `[Int]` -> `"Box$Int"`.
185
- pub fn mangle(base: &str, type_args: &[PlumType]) -> String {
186
- let mut out = base.to_string();
187
- for t in type_args {
188
- out.push('$');
189
- out.push_str(&t.to_string());
190
- }
191
- out
192
- }
193
-
194
- /// Produces a concrete, specialized copy of a generic function (or method) under
195
- /// `mangled_name`. `new_type_param` overrides the receiver-type name for a method
196
- /// whose receiver class was itself specialized (e.g. a method declared on `Box`
197
- /// becomes a method on `Box$Int`); pass the original `f.type_param.clone()`
198
- /// unchanged for a plain free function. The body is left structurally identical
199
- /// here — its own call sites are rewritten separately (Task 2), since expressions
200
- /// don't carry declared-type annotations the way fields/params/return types do.
201
- pub fn specializeFn(
202
- f: &ast::Fn,
203
- subst: &Substitution,
204
- mangled_name: &str,
205
- new_type_param: Option<String>,
206
- ) -> ast::Fn {
207
- ast::Fn {
208
- name: mangled_name.to_string(),
209
- line: f.line,
210
- column: f.column,
211
- type_param: new_type_param,
212
- is_extern: f.is_extern,
213
- params: f
214
- .params
215
- .iter()
216
- .map(|p| ast::Param {
217
- name: p.name.clone(),
218
- ty: match &p.ty {
219
- ast::ParamType::Type(t) => ast::ParamType::Type(substituteType(t, subst)),
220
- ast::ParamType::Variadic(t) => {
221
- ast::ParamType::Variadic(substituteType(t, subst))
222
- }
223
- ast::ParamType::Fn(params, ret) => ast::ParamType::Fn(
224
- params.iter().map(|t| substituteType(t, subst)).collect(),
225
- ret.as_ref().map(|r| Box::new(substituteType(r, subst))),
226
- ),
227
- },
228
- default: p.default.clone(),
229
- })
230
- .collect(),
231
- returns: f.returns.as_ref().map(|r| substituteType(r, subst)),
232
- body: {
233
- let mut body = f.body.clone();
234
- match &mut body {
235
- ast::FnBody::Block(b) => substituteTypesInBlock(b, subst),
236
- ast::FnBody::Expr(e) => substituteTypesInExpr(e, subst),
237
- ast::FnBody::Extern => {}
238
- }
239
- body
240
- },
241
- }
242
- }
243
-
244
- /// Rewrites every explicit generic-type-argument annotation reachable inside
245
- /// `block` (currently only `ClassCall.generics`, e.g. the `[X, Y]` in
246
- /// `Map[X, Y](...)`) through `subst` — the SAME substitution `specializeFn`
247
- /// already applies to a method's own params/return, just extended to its
248
- /// BODY. Without this, a method with its own extra generic param(s) (beyond
249
- /// its receiver's) that writes an explicit bracketed construction using one
250
- /// of those letters keeps the literal, un-substituted letter ("X") after
251
- /// specialization: `resolveClassInstantiation` then treats "X" as a real
252
- /// (nonexistent) class name, silently minting bogus `List$X`/`Pair$X`
253
- /// "specializations" that corrupt unrelated template compilation. See the
254
- /// `plum_map_map_gaps_2026_09` memory (gap 1) for the discovery writeup.
255
- fn substituteTypesInBlock(block: &mut ast::Block, subst: &Substitution) {
256
- for stmt in &mut block.stmts {
257
- substituteTypesInStmt(stmt, subst);
258
- }
259
- }
260
-
261
- fn substituteTypesInStmt(stmt: &mut ast::Stmt, subst: &Substitution) {
262
- match stmt {
263
- ast::Stmt::Assign(a) => {
264
- for v in &mut a.values {
265
- substituteTypesInExpr(v, subst);
266
- }
267
- for t in &mut a.targets {
268
- if let ast::AssignTarget::Field(obj, _) = t {
269
- substituteTypesInExpr(obj, subst);
270
- }
271
- }
272
- }
273
- ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
274
- ast::Stmt::Crash(e) => substituteTypesInExpr(e, subst),
275
- ast::Stmt::Assert(c) => substituteTypesInExpr(&mut c.cond, subst),
276
- ast::Stmt::For(f) => {
277
- substituteTypesInExpr(&mut f.iter, subst);
278
- substituteTypesInBlock(&mut f.body, subst);
279
- }
280
- ast::Stmt::While(w) => {
281
- substituteTypesInExpr(&mut w.condition, subst);
282
- substituteTypesInBlock(&mut w.body, subst);
283
- }
284
- ast::Stmt::If(i) => {
285
- substituteTypesInExpr(&mut i.condition, subst);
286
- substituteTypesInBlock(&mut i.body, subst);
287
- for ei in &mut i.else_ifs {
288
- substituteTypesInExpr(&mut ei.condition, subst);
289
- substituteTypesInBlock(&mut ei.body, subst);
290
- }
291
- if let Some(e) = &mut i.else_ {
292
- substituteTypesInBlock(e, subst);
293
- }
294
- }
295
- ast::Stmt::Match(m) => {
296
- for s in &mut m.subjects {
297
- substituteTypesInExpr(s, subst);
298
- }
299
- for c in &mut m.cases {
300
- substituteTypesInBlock(&mut c.body, subst);
301
- }
302
- }
303
- ast::Stmt::Return(Some(e)) => substituteTypesInExpr(e, subst),
304
- ast::Stmt::Return(None) => {}
305
- ast::Stmt::Expr(e, ..) => substituteTypesInExpr(e, subst),
306
- }
307
- }
308
-
309
- fn substituteTypesInArg(arg: &mut ast::Arg, subst: &Substitution) {
310
- match arg {
311
- ast::Arg::Positional(e) => substituteTypesInExpr(e, subst),
312
- ast::Arg::Keyword { value, .. } => substituteTypesInExpr(value, subst),
313
- ast::Arg::Pair { value, .. } => substituteTypesInExpr(value, subst),
314
- }
315
- }
316
-
317
- fn substituteTypesInExpr(expr: &mut ast::Expr, subst: &Substitution) {
318
- match expr {
319
- ast::Expr::Binary(b) => {
320
- substituteTypesInExpr(&mut b.left, subst);
321
- substituteTypesInExpr(&mut b.right, subst);
322
- }
323
- ast::Expr::Unary(u) => substituteTypesInExpr(&mut u.operand, subst),
324
- ast::Expr::Bool(b) => {
325
- substituteTypesInExpr(&mut b.left, subst);
326
- substituteTypesInExpr(&mut b.right, subst);
327
- }
328
- ast::Expr::Not(inner) => substituteTypesInExpr(inner, subst),
329
- ast::Expr::Try(inner) => substituteTypesInExpr(inner, subst),
330
- ast::Expr::Elvis(e) => {
331
- substituteTypesInExpr(&mut e.left, subst);
332
- substituteTypesInExpr(&mut e.right, subst);
333
- }
334
- ast::Expr::Compare(c) => {
335
- substituteTypesInExpr(&mut c.left, subst);
336
- substituteTypesInExpr(&mut c.right, subst);
337
- }
338
- ast::Expr::Ternary(t) => {
339
- substituteTypesInExpr(&mut t.condition, subst);
340
- substituteTypesInExpr(&mut t.then, subst);
341
- substituteTypesInExpr(&mut t.else_, subst);
342
- }
343
- ast::Expr::FnCall(call) => {
344
- for arg in &mut call.args {
345
- substituteTypesInArg(arg, subst);
346
- }
347
- }
348
- ast::Expr::ClassCall(call) => {
349
- for g in &mut call.generics {
350
- *g = substituteType(g, subst);
351
- }
352
- for fa in &mut call.fields {
353
- substituteTypesInExpr(&mut fa.value, subst);
354
- }
355
- }
356
- ast::Expr::Attribute(attr) => {
357
- substituteTypesInExpr(&mut attr.object, subst);
358
- if let ast::AttrKind::Method(call) = &mut attr.attr {
359
- for arg in &mut call.args {
360
- substituteTypesInArg(arg, subst);
361
- }
362
- }
363
- }
364
- ast::Expr::Paren(inner) => substituteTypesInExpr(inner, subst),
365
- ast::Expr::String(s) => {
366
- for part in &mut s.parts {
367
- if let ast::StringPart::Interp(e) = part {
368
- substituteTypesInExpr(e, subst);
369
- }
370
- }
371
- }
372
- ast::Expr::Closure(cl) => substituteTypesInBlock(&mut cl.body, subst),
373
- ast::Expr::Int(_)
374
- | ast::Expr::Float(_)
375
- | ast::Expr::Self_
376
- | ast::Expr::Var(_)
377
- | ast::Expr::TypeName(_) => {}
378
- }
379
- }
380
-
381
- /// Produces a concrete, specialized copy of a generic enum under `mangled_name`,
382
- /// substituting every variant field type name that matches one of the enum's
383
- /// generic parameters with its resolved concrete type's name.
384
- ///
385
- /// Variant names are ALSO mangled here, with the same suffix as the enum's own
386
- /// name (e.g. `Next` -> `Next$Int`) — even a payload-free variant like `End`.
387
- /// This is necessary because the runtime `EnumVariants` table (built by
388
- /// `buildGlobalTables`) is keyed by bare variant name globally: without this,
389
- /// two specializations of the same generic enum would both register a variant
390
- /// literally named `Next`, colliding in that flat table.
391
- pub fn specializeEnum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -> ast::Enum {
392
- let params = enumGenericParams(e);
393
- let type_args: Vec<PlumType> = params
394
- .iter()
395
- .filter_map(|p| subst.get(p).cloned())
396
- .collect();
397
- ast::Enum {
398
- name: mangled_name.to_string(),
399
- line: e.line,
400
- column: e.column,
401
- generics: vec![],
402
- implements: e.implements.clone(),
403
- params: e.params.clone(),
404
- variants: e
405
- .variants
406
- .iter()
407
- .map(|v| ast::EnumVariant {
408
- name: mangle(&v.name, &type_args),
409
- fields: v.fields.iter().map(|f| substituteType(f, subst)).collect(),
410
- field_names: v.field_names.clone(),
411
- values: v.values.clone(),
412
- })
413
- .collect(),
414
- }
415
- }
416
-
417
- use crate::types::{TypeEnv, TypeScheme};
418
- use crate::{CheckCtx, EnumParams, EnumVariantInfo, EnumVariants, MethodEnv};
419
- use std::collections::BTreeSet;
420
-
421
- enum PendingSpecialization<'a> {
422
- Fn {
423
- base: &'a ast::Fn,
424
- subst: Substitution,
425
- mangled: String,
426
- new_receiver: Option<String>,
427
- },
428
- Enum {
429
- base: &'a ast::Enum,
430
- subst: Substitution,
431
- mangled: String,
432
- },
433
- }
434
-
435
- /// True if `pat` binds `name` anywhere within it (a `Name` sub-pattern, at any
436
- /// nesting depth inside a `Class` constructor pattern) — used by
437
- /// `renameVarInStmt`'s `Match` case to recognize when a NESTED case pattern
438
- /// re-shadows the name currently being renamed, in which case that nested
439
- /// case's body refers to a different (shadowing) binding and must be left alone.
440
- fn caseBindsName(pat: &ast::CasePattern, name: &str) -> bool {
441
- match pat {
442
- ast::CasePattern::Name(n) => n == name,
443
- ast::CasePattern::Class { fields, .. } => fields.iter().any(|f| caseBindsName(f, name)),
444
- _ => false,
445
- }
446
- }
447
-
448
- /// Renames every `Expr::Var(old)` to `Expr::Var(new)` within `block`, used by
449
- /// `Monomorphizer::dedupLocalName` to rename a match-case/for-loop binding
450
- /// (plus every reference to it) once its usage is known to be confined to that
451
- /// one block — see `local_types_by_name`'s doc comment for why this is needed
452
- /// at all. Purely syntactic (no type information needed): stops descending into
453
- /// any NESTED scope that re-binds `old` itself (a nested `for` over the same
454
- /// name, or a nested `match` case whose pattern binds it again), since that
455
- /// inner scope's occurrences of `old` are a different, shadowing variable, not
456
- /// the one being renamed.
457
- fn renameVarInBlock(block: &mut ast::Block, old: &str, new: &str) {
458
- for stmt in &mut block.stmts {
459
- renameVarInStmt(stmt, old, new);
460
- }
461
- }
462
-
463
- fn renameVarInStmt(stmt: &mut ast::Stmt, old: &str, new: &str) {
464
- match stmt {
465
- ast::Stmt::Assign(a) => {
466
- for v in &mut a.values {
467
- renameVarInExpr(v, old, new);
468
- }
469
- for t in &mut a.targets {
470
- match t {
471
- ast::AssignTarget::Var(n) => {
472
- if n == old {
473
- *n = new.to_string();
474
- }
475
- }
476
- ast::AssignTarget::Field(obj, _) => renameVarInExpr(obj, old, new),
477
- }
478
- }
479
- }
480
- ast::Stmt::Return(Some(e)) => renameVarInExpr(e, old, new),
481
- ast::Stmt::Return(None) | ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
482
- ast::Stmt::Crash(e) => renameVarInExpr(e, old, new),
483
- ast::Stmt::Assert(c) => renameVarInExpr(&mut c.cond, old, new),
484
- ast::Stmt::Expr(e, ..) => renameVarInExpr(e, old, new),
485
- ast::Stmt::If(if_) => {
486
- renameVarInExpr(&mut if_.condition, old, new);
487
- renameVarInBlock(&mut if_.body, old, new);
488
- for ei in &mut if_.else_ifs {
489
- renameVarInExpr(&mut ei.condition, old, new);
490
- renameVarInBlock(&mut ei.body, old, new);
491
- }
492
- if let Some(else_block) = &mut if_.else_ {
493
- renameVarInBlock(else_block, old, new);
494
- }
495
- }
496
- ast::Stmt::While(w) => {
497
- renameVarInExpr(&mut w.condition, old, new);
498
- renameVarInBlock(&mut w.body, old, new);
499
- }
500
- ast::Stmt::For(f) => {
501
- renameVarInExpr(&mut f.iter, old, new);
502
- if !f.vars.iter().any(|v| v == old) {
503
- renameVarInBlock(&mut f.body, old, new);
504
- }
505
- }
506
- ast::Stmt::Match(m) => {
507
- for s in &mut m.subjects {
508
- renameVarInExpr(s, old, new);
509
- }
510
- for case in &mut m.cases {
511
- if !case.patterns.iter().any(|p| caseBindsName(p, old)) {
512
- renameVarInBlock(&mut case.body, old, new);
513
- }
514
- }
515
- }
516
- }
517
- }
518
-
519
- fn renameVarInExpr(expr: &mut ast::Expr, old: &str, new: &str) {
520
- match expr {
521
- ast::Expr::Var(n) => {
522
- if n == old {
523
- *n = new.to_string();
524
- }
525
- }
526
- ast::Expr::ClassCall(call) => {
527
- for fa in &mut call.fields {
528
- renameVarInExpr(&mut fa.value, old, new);
529
- }
530
- }
531
- ast::Expr::FnCall(call) => {
532
- for arg in &mut call.args {
533
- renameVarInArg(arg, old, new);
534
- }
535
- }
536
- ast::Expr::Attribute(attr) => {
537
- renameVarInExpr(&mut attr.object, old, new);
538
- if let ast::AttrKind::Method(call) = &mut attr.attr {
539
- for arg in &mut call.args {
540
- renameVarInArg(arg, old, new);
541
- }
542
- }
543
- }
544
- ast::Expr::Binary(b) => {
545
- renameVarInExpr(&mut b.left, old, new);
546
- renameVarInExpr(&mut b.right, old, new);
547
- }
548
- ast::Expr::Bool(b) => {
549
- renameVarInExpr(&mut b.left, old, new);
550
- renameVarInExpr(&mut b.right, old, new);
551
- }
552
- ast::Expr::Compare(c) => {
553
- renameVarInExpr(&mut c.left, old, new);
554
- renameVarInExpr(&mut c.right, old, new);
555
- }
556
- ast::Expr::Not(inner) => renameVarInExpr(inner, old, new),
557
- ast::Expr::Try(inner) => renameVarInExpr(inner, old, new),
558
- ast::Expr::Elvis(e) => {
559
- renameVarInExpr(&mut e.left, old, new);
560
- renameVarInExpr(&mut e.right, old, new);
561
- }
562
- ast::Expr::Unary(u) => renameVarInExpr(&mut u.operand, old, new),
563
- ast::Expr::Paren(inner) => renameVarInExpr(inner, old, new),
564
- ast::Expr::Ternary(t) => {
565
- renameVarInExpr(&mut t.condition, old, new);
566
- renameVarInExpr(&mut t.then, old, new);
567
- renameVarInExpr(&mut t.else_, old, new);
568
- }
569
- ast::Expr::String(s) => {
570
- for part in &mut s.parts {
571
- if let ast::StringPart::Interp(e) = part {
572
- renameVarInExpr(e, old, new);
573
- }
574
- }
575
- }
576
- ast::Expr::Int(_) | ast::Expr::Float(_) | ast::Expr::Self_ | ast::Expr::TypeName(_) => {}
577
- // Not recursed into — matches `rewriteExpr`'s identical `Closure` case
578
- // (closure bodies are compiled/free-variable-captured separately and
579
- // aren't otherwise touched by this pass either). A closure capturing a
580
- // variable that gets renamed here is a known, narrow residual gap.
581
- ast::Expr::Closure(_) => {}
582
- }
583
- }
584
-
585
- fn renameVarInArg(arg: &mut ast::Arg, old: &str, new: &str) {
586
- match arg {
587
- ast::Arg::Positional(e) => renameVarInExpr(e, old, new),
588
- ast::Arg::Keyword { value, .. } => renameVarInExpr(value, old, new),
589
- ast::Arg::Pair { value, .. } => renameVarInExpr(value, old, new),
590
- }
591
- }
592
-
593
- struct Monomorphizer<'a> {
594
- fns_generic: BTreeMap<String, &'a ast::Fn>,
595
- /// Every method declared on a generic enum (e.g. `Try`'s `isOk`/`isErr`,
596
- /// or a record-shaped `List`/`Map`'s own methods), keyed by the enum's own
597
- /// bare name.
598
- methods_generic_on_enum: BTreeMap<String, Vec<&'a ast::Fn>>,
599
- /// Bare variant name (e.g. `"Next"`) -> the generic `Enum` it belongs to. Keyed
600
- /// by variant name because a construction site (`Next(5)`) parses as a `FnCall`
601
- /// whose `name` is the VARIANT, not the enum's own name.
602
- enums_generic_by_variant: BTreeMap<String, &'a ast::Enum>,
603
- /// Mangled enum name -> {original variant name -> mangled variant name}, e.g.
604
- /// `"Link$Int" -> {"Next": "Next$Int", "End": "End$Int"}`. Populated eagerly
605
- /// (in `resolveEnumInstantiation`, at the moment an instantiation's concrete
606
- /// type arguments become known) rather than waiting for the worklist to actually
607
- /// produce that specialization — so both a construction call site and a later
608
- /// `match` on the same specialization can rewrite variant names consistently,
609
- /// regardless of processing order.
610
- enum_variant_mangling: BTreeMap<String, BTreeMap<String, String>>,
611
- /// The enum's own bare name -> the generic `Enum` — used to detect a bare
612
- /// generic-enum-typed function param (e.g. `o: Link`), distinct from
613
- /// `enums_generic_by_variant` (keyed by VARIANT name, used for construction
614
- /// sites like `Next(5)`).
615
- enums_generic_by_name: BTreeMap<String, &'a ast::Enum>,
616
- /// Free functions that are NOT generic by `fnGenericParams`'s lowercase-letter
617
- /// convention, but whose param type(s) bare-name a generic enum (e.g.
618
- /// `unwrapOr(o: Link, ...)`) — such a function still needs its own
619
- /// per-call-site specialization, since its receiver generic enum is
620
- /// dropped from the monomorphized output and the bare name would otherwise
621
- /// resolve to nothing.
622
- fns_bare_generic: BTreeMap<String, &'a ast::Fn>,
623
- /// Mangled enum name -> (its own TEMPLATE name, the generic-param
624
- /// bindings it was specialized with), e.g. `"List$Int" -> ("List", {"T":
625
- /// TInt})`. Populated wherever an enum specialization's `mangled`
626
- /// name is first computed. Used by `resolveMethodOwnGenerics` to look up
627
- /// a method's OWNER template (to find its original, doubly-generic
628
- /// `ast::Fn`) and the receiver's own already-known bindings (e.g. `T`),
629
- /// given only the receiver's mangled type name at a method call site.
630
- class_specialization_info: BTreeMap<String, (String, BTreeMap<String, PlumType>)>,
631
- global_env: TypeEnv,
632
- methods: MethodEnv,
633
- enum_variants: EnumVariants,
634
- enum_params: EnumParams,
635
- min_required: crate::MinRequiredArgs,
636
- specialized: BTreeSet<String>,
637
- enqueued: BTreeSet<String>,
638
- worklist: Vec<PendingSpecialization<'a>>,
639
- produced: Vec<ast::Item>,
640
- /// The declared return type of the function/method currently being rewritten
641
- /// by `rewriteFnBody` — consulted by `resolveEnumInstantiation` as a fallback
642
- /// when a single variant construction site (e.g. `Ok(5)`) can't pin down every
643
- /// one of the enum's generic params by itself (see its doc comment). Reset at
644
- /// the top of every `rewriteFnBody` call; never needs saving/restoring since
645
- /// closures aren't recursed into by this pass (see `rewriteExpr`'s `Closure` arm).
646
- current_return_type: Option<ast::Type>,
647
- /// True while `current_return_type` holds a closure argument's declared
648
- /// return (see `rewriteMethodClosureArgs`), which may still contain the
649
- /// method's own unresolved generic letters.
650
- return_type_is_closure_expected: bool,
651
- /// The enclosing real function's return type while
652
- /// `return_type_is_closure_expected` is set (fallback for letters the
653
- /// closure's expected return leaves unresolved).
654
- closure_outer_return_type: Option<ast::Type>,
655
- /// (receiver name or `None` for a free function, function/method name) ->
656
- /// (the generic enum its declared return type names, the concrete type args
657
- /// it names them with) — for every function/method whose OWN declared return
658
- /// type is a fully-general instantiation of a known generic enum (e.g.
659
- /// `-> Try[Int, Str]`). Populated once up front (in `monomorphizeSource`,
660
- /// alongside `enums_generic_by_name`) from the ORIGINAL, unmodified signatures
661
- /// — unlike everywhere else in this file, this doesn't need the specialization
662
- /// to have actually run yet, since the declared signature already says
663
- /// everything needed. Consulted by `resolveCallReturnType`.
664
- fn_return_generic_enum: BTreeMap<(Option<String>, String), (&'a ast::Enum, Vec<PlumType>)>,
665
- /// Every local name's type as first seen in the function currently being
666
- /// rewritten (reset per `rewriteFnBody` call, like `current_return_type`).
667
- /// Wasm local slots are allocated once per NAME for the whole function (see
668
- /// `plum-wasm-codegen`'s `Collector`/`compileFnBody`), not per lexical scope —
669
- /// so two unrelated bindings that happen to share a name (e.g. `Ok(v)` in two
670
- /// separate, non-overlapping `match` statements) would silently collide on
671
- /// one slot if their types ever differ. `dedupLocalName` consults this to
672
- /// catch that and rename the second, conflicting binding instead.
673
- local_types_by_name: BTreeMap<String, PlumType>,
674
- /// Bumped each time `dedupLocalName` needs a fresh name; part of the fresh
675
- /// name itself, so collisions between two different renames are impossible.
676
- rename_counter: usize,
677
- }
678
-
679
- impl<'a> Monomorphizer<'a> {
680
- /// Like `infer`, but tries `resolveCallReturnType` first — needed anywhere the
681
- /// resulting `PlumType` will be used to look up `enum_variant_mangling` (i.e.
682
- /// wherever a value might need its constructor-pattern names rewritten later:
683
- /// an assignment's recorded local type, or a `match` subject's type).
684
- fn inferConcrete(&mut self, e: &ast::Expr, env: &TypeEnv) -> PlumType {
685
- self.resolveCallReturnType(e, env)
686
- .unwrap_or_else(|| self.infer(e, env))
687
- }
688
-
689
- fn infer(&self, e: &ast::Expr, env: &TypeEnv) -> PlumType {
690
- // `env` is a SNAPSHOT of `self.global_env`, cloned once at the top of
691
- // whichever `rewriteFnBody` call is currently processing this
692
- // statement — any free function specialized (and eagerly registered
693
- // into `self.global_env`, e.g. by `resolveFnInstantiation`) SINCE that
694
- // clone was taken is invisible to a plain `env.get(...)` lookup, even
695
- // though it's already a real, fully-resolved entry. Overlay the LIVE
696
- // `self.global_env` underneath `env` (local bindings still shadow it,
697
- // same precedence a real nested scope would have) so a call rewritten
698
- // to its mangled name earlier in THIS SAME statement (e.g. `l :=
699
- // makeList(1, 2, 3)`, immediately needing `makeList$Int`'s freshly
700
- // resolved return type) resolves correctly instead of falling back to
701
- // `TVar` — which would otherwise cascade into `resolveMethodOwnGenerics`
702
- // silently no-op'ing on the very next statement (`l.map(...)`).
703
- let mut merged_env = self.global_env.clone();
704
- merged_env.extend(env.iter().map(|(k, v)| (k.clone(), v.clone())));
705
- let ctx = CheckCtx {
706
- methods: &self.methods,
707
- enum_variants: &self.enum_variants,
708
- enum_params: &self.enum_params,
709
- min_required: &self.min_required,
710
- };
711
- crate::inferExpr(e, &merged_env, &ctx).unwrap_or(PlumType::TVar("_".to_string()))
712
- }
713
-
714
- /// Records `name`'s first-seen type in `local_types_by_name` without
715
- /// renaming anything — for a binding whose usage isn't cleanly bounded to a
716
- /// single `&mut ast::Block` this pass has in hand at the binding site (e.g.
717
- /// a plain `Assign` — its "scope" is however much of the flat function body
718
- /// follows it, not a nested block). This still lets a LATER, cleanly-bounded
719
- /// binding (`dedupLocalName`, from a `match` arm or `for` loop) detect a
720
- /// conflict against it and rename itself accordingly; it just means a
721
- /// conflict in the other direction (an `Assign` conflicting with an
722
- /// EARLIER match-bound name) isn't caught. Real but narrower residual gap —
723
- /// see the `local_types_by_name` doc comment.
724
- fn seedLocalType(&mut self, name: &str, ty: &PlumType) {
725
- self.local_types_by_name
726
- .entry(name.to_string())
727
- .or_insert_with(|| ty.clone());
728
- }
729
-
730
- /// Ensures `name` can be bound to `ty` here without colliding with a
731
- /// DIFFERENT type already recorded for that same name elsewhere in the
732
- /// current function (see `local_types_by_name`'s doc comment for why that's
733
- /// otherwise unsafe). If there's no conflict, returns `name` unchanged. If
734
- /// there IS one, mints a fresh name, renames every reference to `name`
735
- /// within `scope` (a match case body / for-loop body — the full extent this
736
- /// particular binding's usage can ever reach) to that fresh name, and
737
- /// returns it for the caller to use as the actual binding name instead.
738
- fn dedupLocalName(&mut self, name: &str, ty: &PlumType, scope: &mut ast::Block) -> String {
739
- self.dedupLocalNameWithGuard(name, ty, scope, None)
740
- }
741
-
742
- /// Same as `dedupLocalName`, but when a match case has its own `when` guard,
743
- /// that guard can reference the SAME pattern-bound name the rename below
744
- /// patches inside the case body — patch it there too, or the guard is left
745
- /// referring to a name `case_env` no longer has (see the caller in
746
- /// `manglePattern`).
747
- fn dedupLocalNameWithGuard(
748
- &mut self,
749
- name: &str,
750
- ty: &PlumType,
751
- scope: &mut ast::Block,
752
- guard: Option<&mut ast::Expr>,
753
- ) -> String {
754
- match self.local_types_by_name.get(name) {
755
- None => {
756
- self.local_types_by_name
757
- .insert(name.to_string(), ty.clone());
758
- name.to_string()
759
- }
760
- Some(existing) if existing == ty => name.to_string(),
761
- Some(_) => {
762
- self.rename_counter += 1;
763
- let fresh = format!("{}$dup{}", name, self.rename_counter);
764
- self.local_types_by_name.insert(fresh.clone(), ty.clone());
765
- renameVarInBlock(scope, name, &fresh);
766
- if let Some(g) = guard {
767
- renameVarInExpr(g, name, &fresh);
768
- }
769
- fresh
770
- }
771
- }
772
- }
773
-
774
- /// Rewrites a function/method body's generic call sites. When `resolve_return`
775
- /// is set (for freshly-generated specializations, whose declared return may be
776
- /// a generic parameter like `a` — which the current grammar can't even parse,
777
- /// leaving `returns: None` — or a generic class), the declared return type is
778
- /// re-derived from the concrete inferred type of the body's tail expression.
779
- /// For an ordinary (non-generic) top-level function we only rewrite the return
780
- /// annotation when it names a generic class used bare (e.g. `-> Box`), which
781
- /// the body's construction site has just been specialized to a mangled name.
782
- fn rewriteFnBody(&mut self, f: &mut ast::Fn, resolve_return: bool) -> Result<(), String> {
783
- self.current_return_type = f.returns.clone();
784
- self.return_type_is_closure_expected = false;
785
- self.local_types_by_name.clear();
786
- let mut env = self.global_env.clone();
787
- if let Some(recv) = &f.type_param {
788
- env.insert(
789
- "self".to_string(),
790
- TypeScheme::mono(PlumType::TNamed(recv.clone())),
791
- );
792
- }
793
- for p in &f.params {
794
- let ty = match &p.ty {
795
- ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
796
- ast::ParamType::Variadic(t) => {
797
- PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t)))
798
- }
799
- ast::ParamType::Fn(params, ret) => {
800
- let param_types = params.iter().map(crate::plumTypeFromAst).collect();
801
- let ret_ty = ret
802
- .as_ref()
803
- .map(|r| crate::plumTypeFromAst(r))
804
- .unwrap_or(PlumType::TUnit);
805
- PlumType::TFun(param_types, Box::new(ret_ty))
806
- }
807
- };
808
- self.local_types_by_name.insert(p.name.clone(), ty.clone());
809
- env.insert(p.name.clone(), TypeScheme::mono(ty));
810
- }
811
- // The function's own return type, resolved for THIS specialization — used
812
- // below to resolve a bare payload-free variant (`End`) that's the
813
- // BODY'S TAIL EXPRESSION (implicit return), the same way `Stmt::Return`
814
- // already does. A tail `Stmt::Expr` never goes through `Stmt::Return`'s
815
- // own handling, so without this a trailing bare `End` is left
816
- // unmangled and later fails the real checker's return-type unification.
817
- let expected_ret = self
818
- .current_return_type
819
- .clone()
820
- .map(|rt| self.resolveFieldType(&rt))
821
- .map(|rt| crate::plumTypeFromAst(&rt));
822
- let tail: Option<PlumType> = match &mut f.body {
823
- ast::FnBody::Expr(e) => {
824
- if let Some(expected) = &expected_ret {
825
- self.resolveBareVariantAgainstExpected(e, expected);
826
- self.wrapPrimitiveAgainstExpected(e, expected, &env);
827
- }
828
- self.rewriteExpr(e, &env)?;
829
- Some(self.inferConcrete(e, &env))
830
- }
831
- ast::FnBody::Block(block) => {
832
- if let Some(expected) = &expected_ret {
833
- if let Some(ast::Stmt::Expr(e, ..)) = block.stmts.last_mut() {
834
- self.resolveBareVariantAgainstExpected(e, expected);
835
- self.wrapPrimitiveAgainstExpected(e, expected, &env);
836
- }
837
- }
838
- self.rewriteBlock(block, &mut env)?;
839
- // `inferConcrete` (not plain `infer`) — the tail's type feeds
840
- // `maybeRewriteReturn` below, which needs the call's REAL
841
- // specialized return type (e.g. `Link$Str`) when the tail is
842
- // a call to another function whose OWN declared return type
843
- // instantiates a generic enum. Plain `infer` only consults
844
- // `self.global_env`, built once from the ORIGINAL source
845
- // before any mangling — so a tail like `return otherFn()`
846
- // where `otherFn() -> Link[Str]` would infer the bare,
847
- // unmangled `Link` here, and `maybeRewriteReturn` would then
848
- // permanently freeze this function's OWN return annotation at
849
- // that same bare, ungeneric name instead of the concrete
850
- // `Link$Str` the body actually produces.
851
- match block.stmts.last() {
852
- Some(ast::Stmt::Expr(e, ..)) => Some(self.inferConcrete(e, &env)),
853
- Some(ast::Stmt::Return(Some(e))) => Some(self.inferConcrete(e, &env)),
854
- _ => None,
855
- }
856
- }
857
- // No body to rewrite or infer a tail type from.
858
- ast::FnBody::Extern => None,
859
- };
860
- if let Some(t) = tail {
861
- self.maybeRewriteReturn(f, &t, resolve_return);
862
- }
863
- Ok(())
864
- }
865
-
866
- /// Overwrites `f.returns` with a concrete type derived from the body's tail
867
- /// type `t` when the currently-declared return type is genuinely generic or
868
- /// unresolved. Never clobbers a real, concrete declared return type — even for
869
- /// a specialization (`resolve_return: true`) — so a generic function whose body
870
- /// is internally inconsistent with its concrete declared return (e.g.
871
- /// `wrong(x: a) -> Int = "hello"`) is left for the checker's normal
872
- /// return-type-mismatch logic to REJECT rather than silently rewritten (and
873
- /// thereby masked). The overwrite fires only when:
874
- /// - `f.returns` is `None` — the unparseable `-> a` generic-parameter-return
875
- /// case, where the grammar dropped the annotation entirely (this only ever
876
- /// happens for a specialization, which is the only path that can supply a
877
- /// concrete tail type to fill it in); or
878
- /// - the declared return names something still-generic: a generic-parameter
879
- /// letter (e.g. `-> a`) or a generic class used bare (e.g. `-> Box`).
880
- /// For the `Some(rt)` arm this condition is identical whether `resolve_return`
881
- /// is `true` or `false`; the specialization path differs only in that its tail
882
- /// is inferred against a resolved substitution, so a generic-parameter-letter
883
- /// return resolves to the specialization's concrete bound type (which the
884
- /// ordinary path cannot do). The unparseable-`End` fill-in is gated on
885
- /// `resolve_return` so an ordinary void function (`returns: None` meaning "no
886
- /// declared return", not "a generic return the grammar dropped") is never given
887
- /// a fabricated return type. Never fabricates a return from an un-inferrable
888
- /// (`TVar`) tail.
889
- fn maybeRewriteReturn(&self, f: &mut ast::Fn, t: &PlumType, resolve_return: bool) {
890
- if matches!(t, PlumType::TVar(_) | PlumType::TFun(_, _)) {
891
- return;
892
- }
893
- let needs = match &f.returns {
894
- None => resolve_return,
895
- Some(rt) => {
896
- // A generic enum named WITH concrete type args already
897
- // (`-> List[Str]`, `-> Try[Int, Str]`) is already fully
898
- // resolved — only a genuinely BARE reference (`-> List`,
899
- // `-> Box`, no `[...]` at all) is the "unresolved" shape this
900
- // is meant to catch. Without the `rt.generics.is_empty()`
901
- // guard, a plain function's ALREADY-concrete `List[Str]`
902
- // return annotation got blown away and replaced by whatever
903
- // (possibly stale/unmangled) type the body's tail inferred
904
- // to, purely because "List" happens to be a known generic
905
- // class name — corrupting an otherwise-correct declaration.
906
- rt.generics.is_empty()
907
- && (isGenericParamName(&rt.name)
908
- || self.enums_generic_by_name.contains_key(&rt.name))
909
- }
910
- };
911
- if needs {
912
- f.returns = Some(ast::Type {
913
- name: t.to_string(),
914
- generics: vec![],
915
- });
916
- }
917
- }
918
-
919
- fn rewriteBlock(&mut self, block: &mut ast::Block, env: &mut TypeEnv) -> Result<(), String> {
920
- for stmt in &mut block.stmts {
921
- self.rewriteStmt(stmt, env)?;
922
- }
923
- Ok(())
924
- }
925
-
926
- /// Rewrites `pat` (bare-name/constructor pattern) in place: an uppercase variant
927
- /// name gets mangled to its specialized form (`Next` -> `Next$Int`) if `mangling`
928
- /// says this position's subject is a specialized generic enum; a plain binding
929
- /// name is inserted into `case_env` at `ty`. Recurses into a constructor
930
- /// pattern's own fields (`Next(Next(v))`), looking up *that* field's own
931
- /// mangling table from `self.enum_variant_mangling` — a nested sub-pattern can
932
- /// be a specialization independent of its enclosing pattern's.
933
- fn manglePattern(
934
- &mut self,
935
- pat: &mut ast::CasePattern,
936
- ty: &PlumType,
937
- mangling: Option<&BTreeMap<String, String>>,
938
- case_env: &mut TypeEnv,
939
- body: &mut ast::Block,
940
- mut guard: Option<&mut ast::Expr>,
941
- ) {
942
- match pat {
943
- ast::CasePattern::Name(n) => {
944
- let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
945
- && self.enum_variants.contains_key(n.as_str());
946
- if is_variant {
947
- if let Some(table) = mangling {
948
- if let Some(mangled_variant) = table.get(n) {
949
- *n = mangled_variant.clone();
950
- }
951
- }
952
- } else {
953
- let bound = self.dedupLocalNameWithGuard(n, ty, body, guard.as_deref_mut());
954
- if bound != *n {
955
- *n = bound.clone();
956
- }
957
- case_env.insert(bound, TypeScheme::mono(ty.clone()));
958
- }
959
- }
960
- ast::CasePattern::Class { name, fields } => {
961
- if let Some(table) = mangling {
962
- if let Some(mangled_variant) = table.get(name) {
963
- *name = mangled_variant.clone();
964
- }
965
- }
966
- if let Some(info) = self.enum_variants.get(name.as_str()) {
967
- let field_types = info.field_types.clone();
968
- for (f, fty) in fields.iter_mut().zip(field_types.iter()) {
969
- let field_mangling: Option<BTreeMap<String, String>> = match fty {
970
- PlumType::TNamed(n) => self.enum_variant_mangling.get(n).cloned(),
971
- _ => None,
972
- };
973
- self.manglePattern(
974
- f,
975
- fty,
976
- field_mangling.as_ref(),
977
- case_env,
978
- body,
979
- guard.as_deref_mut(),
980
- );
981
- }
982
- }
983
- }
984
- _ => {}
985
- }
986
- }
987
-
988
- fn rewriteStmt(&mut self, stmt: &mut ast::Stmt, env: &mut TypeEnv) -> Result<(), String> {
989
- match stmt {
990
- ast::Stmt::Assign(a) => {
991
- for (target, value) in a.targets.iter_mut().zip(a.values.iter_mut()) {
992
- // For a field target (`self.head = End`), resolve the
993
- // value against the field's OWN declared type BEFORE the
994
- // generic rewrite/inference below — same reasoning as
995
- // `ClassCall`'s field values: a bare payload-free variant
996
- // has no type of its own, but the field it's being
997
- // written into does.
998
- if let ast::AssignTarget::Field(object, field_name) = target {
999
- if let PlumType::TNamed(class_name) = self.infer(object, env) {
1000
- let ctx = CheckCtx {
1001
- methods: &self.methods,
1002
- enum_variants: &self.enum_variants,
1003
- enum_params: &self.enum_params,
1004
- min_required: &self.min_required,
1005
- };
1006
- if let Ok(field_ty) =
1007
- crate::lookupFieldType(&class_name, field_name, &ctx)
1008
- {
1009
- self.resolveBareVariantAgainstExpected(value, &field_ty);
1010
- self.wrapPrimitiveAgainstExpected(value, &field_ty, env);
1011
- }
1012
- }
1013
- }
1014
- self.rewriteExpr(value, env)?;
1015
- let ty = self.inferConcrete(value, env);
1016
- match target {
1017
- ast::AssignTarget::Var(name) => {
1018
- self.seedLocalType(name, &ty);
1019
- env.insert(name.clone(), TypeScheme::mono(ty));
1020
- }
1021
- ast::AssignTarget::Field(object, _) => {
1022
- self.rewriteExpr(object, env)?;
1023
- }
1024
- }
1025
- }
1026
- }
1027
- ast::Stmt::Return(Some(e)) => {
1028
- // Same idea as the field-target case above, but against the
1029
- // enclosing function's own declared return type (`return
1030
- // End` inside a method returning `Link[Int]`).
1031
- if let Some(rt) = self.current_return_type.clone() {
1032
- let resolved = self.resolveFieldType(&rt);
1033
- let expected = crate::plumTypeFromAst(&resolved);
1034
- self.resolveBareVariantAgainstExpected(e, &expected);
1035
- self.wrapPrimitiveAgainstExpected(e, &expected, env);
1036
- }
1037
- self.rewriteExpr(e, env)?;
1038
- }
1039
- ast::Stmt::Return(None) => {}
1040
- ast::Stmt::If(if_) => {
1041
- self.rewriteExpr(&mut if_.condition, env)?;
1042
- self.rewriteBlock(&mut if_.body, &mut env.clone())?;
1043
- for ei in &mut if_.else_ifs {
1044
- self.rewriteExpr(&mut ei.condition, env)?;
1045
- self.rewriteBlock(&mut ei.body, &mut env.clone())?;
1046
- }
1047
- if let Some(else_block) = &mut if_.else_ {
1048
- self.rewriteBlock(else_block, &mut env.clone())?;
1049
- }
1050
- }
1051
- ast::Stmt::While(w) => {
1052
- self.rewriteExpr(&mut w.condition, env)?;
1053
- self.rewriteBlock(&mut w.body, &mut env.clone())?;
1054
- }
1055
- ast::Stmt::For(f) => {
1056
- self.rewriteExpr(&mut f.iter, env)?;
1057
- // `for i := range n` binds `i: Int` — but `for v := range
1058
- // values` over a `...T` variadic param binds `v: T` (whatever
1059
- // `T` was specialized to), NOT `Int`. Without this check, a
1060
- // variadic loop var was always (wrongly) recorded as `Int` in
1061
- // this pass's own type env — harmless whenever `T` really was
1062
- // `Int` (masking the bug in e.g. `makeList`/`List.add`'s own
1063
- // `for v := range values` for every `List[Int]` test), but
1064
- // silently wrong for any other element type (e.g.
1065
- // `List[Pair[Str, Int]]`), producing bogus inferred types for
1066
- // anything built from `v` later in the body (like the `T` a
1067
- // `Node(value: v, ...)` construction infers).
1068
- let elem_ty = match self.infer(&f.iter, env) {
1069
- PlumType::TVariadic(elem) => *elem,
1070
- _ => PlumType::TInt,
1071
- };
1072
- let mut inner = env.clone();
1073
- let ast::For { vars, body, .. } = f;
1074
- for v in vars.iter_mut() {
1075
- let bound = self.dedupLocalName(v, &elem_ty, body);
1076
- inner.insert(bound.clone(), TypeScheme::mono(elem_ty.clone()));
1077
- *v = bound;
1078
- }
1079
- self.rewriteBlock(body, &mut inner)?;
1080
- }
1081
- ast::Stmt::Expr(e, ..) => self.rewriteExpr(e, env)?,
1082
- ast::Stmt::Crash(e) => self.rewriteExpr(e, env)?,
1083
- ast::Stmt::Assert(c) => self.rewriteExpr(&mut c.cond, env)?,
1084
- ast::Stmt::Match(m) => {
1085
- for s in &mut m.subjects {
1086
- self.rewriteExpr(s, env)?;
1087
- }
1088
- // One (type, variant-mangling table) pair per subject — `match a, b`
1089
- // needs each position's own generic-enum specialization handled
1090
- // independently, not just the first subject's.
1091
- let subject_types: Vec<PlumType> = m
1092
- .subjects
1093
- .iter()
1094
- .map(|s| self.inferConcrete(s, env))
1095
- .collect();
1096
- // If a subject's concrete type is a specialized generic enum, its
1097
- // variant-name mangling table lets us rewrite that position's patterns
1098
- // (`Next`/`End` -> `Next$Int`/`End$Int`) to reference the correct
1099
- // specialization, so the checker/codegen's unmodified, bare-name-keyed
1100
- // `EnumVariants` lookup still resolves each pattern correctly.
1101
- let variant_manglings: Vec<Option<BTreeMap<String, String>>> = subject_types
1102
- .iter()
1103
- .map(|ty| match ty {
1104
- PlumType::TNamed(n) => self.enum_variant_mangling.get(n).cloned(),
1105
- _ => None,
1106
- })
1107
- .collect();
1108
- for case in &mut m.cases {
1109
- let mut case_env = env.clone();
1110
- let ast::Case {
1111
- patterns,
1112
- guard,
1113
- body,
1114
- } = case;
1115
- for (pat, (subject_ty, variant_mangling)) in patterns
1116
- .iter_mut()
1117
- .zip(subject_types.iter().zip(variant_manglings.iter()))
1118
- {
1119
- self.manglePattern(
1120
- pat,
1121
- subject_ty,
1122
- variant_mangling.as_ref(),
1123
- &mut case_env,
1124
- body,
1125
- guard.as_mut(),
1126
- );
1127
- }
1128
- if let Some(g) = guard {
1129
- self.rewriteExpr(g, &case_env)?;
1130
- }
1131
- // A case body's own LAST statement, when it's a bare
1132
- // `Stmt::Expr`, is itself an implicit-return position
1133
- // whenever this whole `match` is the function's tail
1134
- // expression (`first`/`last`'s `End =>` arm ending in a
1135
- // bare `End`, for instance) — same reasoning as
1136
- // `rewriteFnBody`'s own tail-expression handling, just one
1137
- // level down through the match. Resolving against the
1138
- // function's OWN declared return type is a no-op unless
1139
- // the expression is actually a bare payload-free variant
1140
- // of that same enum family, so this is harmless even for
1141
- // a match that ISN'T in tail position.
1142
- if let Some(rt) = self.current_return_type.clone() {
1143
- let resolved = self.resolveFieldType(&rt);
1144
- let expected = crate::plumTypeFromAst(&resolved);
1145
- if let Some(ast::Stmt::Expr(e, ..)) = body.stmts.last_mut() {
1146
- self.resolveBareVariantAgainstExpected(e, &expected);
1147
- self.wrapPrimitiveAgainstExpected(e, &expected, &case_env);
1148
- }
1149
- }
1150
- self.rewriteBlock(body, &mut case_env)?;
1151
- }
1152
- }
1153
- ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
1154
- }
1155
- Ok(())
1156
- }
1157
-
1158
- /// Resolves a `Type(field: value, ...)` construction of a generic,
1159
- /// record-shaped (single-variant, named-payload) enum — the `type X[T] =
1160
- /// ...` replacement shape, e.g. `List[T]`/`Map[K, V]`. `ClassCall` is the
1161
- /// shared AST node this call shape ALSO uses for a non-generic or already-
1162
- /// mangled enum variant's own named-payload construction (see
1163
- /// `plum-checker`'s `inferClassCallRaw`) — this only fires for one that's
1164
- /// still a bare, generic TEMPLATE name.
1165
- fn resolveClassInstantiation(
1166
- &mut self,
1167
- call: &mut ast::ClassCall,
1168
- env: &TypeEnv,
1169
- ) -> Result<(), String> {
1170
- let Some(e) = self
1171
- .enums_generic_by_name
1172
- .get(call.type_name.as_str())
1173
- .copied()
1174
- else {
1175
- return Ok(());
1176
- };
1177
- let params = enumGenericParams(e);
1178
- let variant = &e.variants[0];
1179
- let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
1180
- for gp in &params {
1181
- if let Some(idx) = variant.fields.iter().position(|t| t.name == *gp) {
1182
- let field_name = &variant.field_names[idx];
1183
- if let Some(fa) = call.fields.iter().find(|fa| &fa.name == field_name) {
1184
- // A bare payload-free variant reference (`End`) carries
1185
- // no type of its own to bind a generic param FROM — skip
1186
- // it here; it gets resolved AGAINST the binding (once
1187
- // known) below instead, same as every other field.
1188
- let is_bare_variant = matches!(&fa.value, ast::Expr::TypeName(n) if self.enums_generic_by_variant.contains_key(n));
1189
- if !is_bare_variant {
1190
- bindings.insert(gp.clone(), self.infer(&fa.value, env));
1191
- }
1192
- }
1193
- }
1194
- }
1195
- // No field is EVER directly typed as a bare generic param for a type
1196
- // like `List[T]` (its fields are `Link[Node[T]]`/`Int`, never a bare
1197
- // `T`) — and even where one exists, constructing with a payload-free
1198
- // value (`List(head: End, ...)`) gives no VALUE to infer a type from
1199
- // regardless. Fall back to an explicit `List[Int](...)` annotation at
1200
- // the call site when field-value inference alone isn't enough.
1201
- if bindings.len() != params.len() && call.generics.len() == params.len() {
1202
- let explicit_generics = call.generics.clone();
1203
- for (p, gt) in params.iter().zip(explicit_generics.iter()) {
1204
- if !bindings.contains_key(p) {
1205
- // `resolveFieldType` first (not a bare `plumTypeFromAst`) —
1206
- // `gt` may itself be a generic-with-args type (e.g. the
1207
- // `Pair[Str, Int]` in `List[Pair[Str, Int]](...)`), which
1208
- // `plumTypeFromAst` alone would silently flatten to the
1209
- // bare, unmangled `TNamed("Pair")` (it drops `.generics`
1210
- // entirely) — exactly the same recursive resolution
1211
- // `resolveFieldType` already does for a field's own
1212
- // declared type, just applied here to an explicit
1213
- // call-site type argument instead.
1214
- let resolved = self.resolveFieldType(gt);
1215
- bindings.insert(p.clone(), crate::plumTypeFromAst(&resolved));
1216
- }
1217
- }
1218
- }
1219
- // Still not fully resolved — fall back to the enclosing function's own
1220
- // declared return type, exactly like `resolveEnumInstantiation` already
1221
- // does for a generic-enum construction site that can't see every
1222
- // parameter from its own arguments. This is what makes `fun
1223
- // makeList(values: ...T) -> List[T] = List(head: End, ...).add(values)`
1224
- // work: the bare `List(...)` construction gives no type witness at all
1225
- // (every field value is a payload-free `End`/`0`), but by the time
1226
- // THIS function is specialized for a concrete `T`, `current_return_type`
1227
- // has already been substituted to e.g. `List[Int]` by `specializeFn`.
1228
- if bindings.len() != params.len() {
1229
- if let Some(rt) = self.current_return_type.clone() {
1230
- if rt.name == call.type_name && rt.generics.len() == params.len() {
1231
- for (p, gt) in params.iter().zip(rt.generics.iter()) {
1232
- if !bindings.contains_key(p) {
1233
- // See the matching comment on the `call.generics`
1234
- // fallback above — `gt` needs the same recursive
1235
- // `resolveFieldType` resolution before dropping
1236
- // into `plumTypeFromAst`.
1237
- let resolved = self.resolveFieldType(gt);
1238
- bindings.insert(p.clone(), crate::plumTypeFromAst(&resolved));
1239
- }
1240
- }
1241
- }
1242
- }
1243
- }
1244
- if bindings.len() != params.len() {
1245
- return Err(format!(
1246
- "monomorphize: could not resolve all generic parameters for '{}' at this call site — pass them explicitly, e.g. '{}[Int](...)'",
1247
- call.type_name, call.type_name
1248
- ));
1249
- }
1250
- let mangled = self.ensureEnumSpecialized(e, &params, bindings);
1251
-
1252
- // Now that every generic param is bound, resolve any bare
1253
- // payload-free-variant field values (`End`) against THIS
1254
- // specialization's own field types — codegen only ever sees the
1255
- // mangled specialization (the generic template is dropped), so a
1256
- // still-bare `End` would be an unresolvable reference by the time it
1257
- // gets there.
1258
- if let Some(info) = self.enum_variants.get(&mangled).cloned() {
1259
- for fa in &mut call.fields {
1260
- if let Some(idx) = info.field_names.iter().position(|n| n == &fa.name) {
1261
- let expected = info.field_types[idx].clone();
1262
- self.resolveBareVariantAgainstExpected(&mut fa.value, &expected);
1263
- self.resolveGenericVariantAgainstExpected(&mut fa.value, &expected);
1264
- self.wrapPrimitiveAgainstExpected(&mut fa.value, &expected, env);
1265
- }
1266
- }
1267
- }
1268
- call.type_name = mangled;
1269
- Ok(())
1270
- }
1271
-
1272
- /// The `Arg`'s underlying expression, regardless of whether it was passed
1273
- /// positionally or by keyword — the same three-way match repeated at every
1274
- /// call site in this file, pulled out once for `resolveMethodOwnGenerics`.
1275
- fn argExprAt(call: &ast::FnCall, i: usize) -> Option<&ast::Expr> {
1276
- call.args.get(i).map(|arg| match arg {
1277
- ast::Arg::Positional(e) => e,
1278
- ast::Arg::Keyword { value, .. } => value,
1279
- ast::Arg::Pair { value, .. } => value,
1280
- })
1281
- }
1282
-
1283
- /// Rewrites every closure-literal argument of a method call's OWN generic
1284
- /// construction sites BEFORE anything else touches that argument. Must run
1285
- /// ahead of the generic "walk every arg" rewrite in `rewriteExpr`'s
1286
- /// `Attribute` arm — that generic walk seeds a closure's params as
1287
- /// unconstrained `TVar`s (it has no way to know a method's declared
1288
- /// closure-param types), and once a construction inside a closure body
1289
- /// (e.g. `andThen`'s `|n| Next(n / 2)`) gets mangled against a WRONG
1290
- /// (`TVar`-derived) binding, nothing downstream can fix it —
1291
- /// `resolveEnumInstantiation` only fires for a bare, still-unmangled
1292
- /// variant name, so a premature wrong rewrite sticks permanently. Called
1293
- /// unconditionally for every method call (regardless of whether it turns
1294
- /// out to have its own extra generics — an ORDINARY method's closure arg,
1295
- /// e.g. `List.any(|x| ...)`, needs its body rewritten too, just seeded
1296
- /// from `method`'s own declared closure-param types with no extra
1297
- /// generic involved). Falls back to `env` as-is (the same behavior the
1298
- /// generic walk used to provide) for a receiver that isn't a resolvable
1299
- /// generic specialization, or a method not found on it.
1300
- fn rewriteMethodClosureArgs(
1301
- &mut self,
1302
- object: &ast::Expr,
1303
- call: &mut ast::FnCall,
1304
- env: &TypeEnv,
1305
- ) -> Result<(), String> {
1306
- let mut method_and_bindings: Option<(&'a ast::Fn, BTreeMap<String, PlumType>)> = None;
1307
- if let PlumType::TNamed(mangled_receiver) = self.inferConcrete(object, env) {
1308
- if let Some((template_name, class_bindings)) = self
1309
- .class_specialization_info
1310
- .get(&mangled_receiver)
1311
- .cloned()
1312
- {
1313
- let method = self
1314
- .methods_generic_on_enum
1315
- .get(template_name.as_str())
1316
- .and_then(|methods| methods.iter().find(|m| m.name == call.name).copied());
1317
- if let Some(method) = method {
1318
- method_and_bindings = Some((method, class_bindings));
1319
- }
1320
- }
1321
- }
1322
-
1323
- for (i, arg) in call.args.iter_mut().enumerate() {
1324
- let arg_expr = match arg {
1325
- ast::Arg::Positional(e) => e,
1326
- ast::Arg::Keyword { value, .. } => value,
1327
- ast::Arg::Pair { value, .. } => value,
1328
- };
1329
- let ast::Expr::Closure(cl) = arg_expr else {
1330
- continue;
1331
- };
1332
- let closure_param_tys: Vec<PlumType> = match &method_and_bindings {
1333
- Some((method, class_bindings)) => match method.params.get(i).map(|p| &p.ty) {
1334
- Some(ast::ParamType::Fn(params, _)) => params
1335
- .iter()
1336
- .map(|t| substituteType(t, &Substitution(class_bindings.clone())))
1337
- .map(|t| crate::plumTypeFromAst(&t))
1338
- .collect(),
1339
- _ => Vec::new(),
1340
- },
1341
- None => Vec::new(),
1342
- };
1343
- // The closure's own declared return, with the receiver's bindings
1344
- // substituted (`andThen`'s `cb: fn(T) -> Try[U, E]` on a
1345
- // `Try[Int, [Bad]]` receiver -> `Try[U, [Bad]]`). A bare `Ok(n * 2)`
1346
- // in the closure body can only pin `T`, never `E`; without this it
1347
- // fell back to the ENCLOSING function's return type (wrong scope)
1348
- // and stayed unmangled, so `resolveMethodOwnGenerics` couldn't
1349
- // recover `U`. Only a non-bare-letter return is used (a bare
1350
- // `fn(T) -> U` says nothing more than the outer fallback did).
1351
- let closure_expected_ret: Option<ast::Type> = match &method_and_bindings {
1352
- Some((method, class_bindings)) => match method.params.get(i).map(|p| &p.ty) {
1353
- Some(ast::ParamType::Fn(_, Some(ret))) if !ret.generics.is_empty() => {
1354
- Some(substituteType(ret, &Substitution(class_bindings.clone())))
1355
- }
1356
- _ => None,
1357
- },
1358
- None => None,
1359
- };
1360
- let mut closure_env = env.clone();
1361
- for (p, ty) in cl.params.iter().zip(closure_param_tys.iter()) {
1362
- closure_env.insert(p.clone(), TypeScheme::mono(ty.clone()));
1363
- }
1364
- for p in &cl.params {
1365
- closure_env
1366
- .entry(p.clone())
1367
- .or_insert_with(|| TypeScheme::mono(PlumType::TVar("_".to_string())));
1368
- }
1369
- if let Some(expected) = closure_expected_ret {
1370
- let saved_ret = self.current_return_type.replace(expected);
1371
- let saved_flag = std::mem::replace(&mut self.return_type_is_closure_expected, true);
1372
- // Nested closures keep the outermost real function's return.
1373
- let saved_outer = if saved_flag {
1374
- self.closure_outer_return_type.clone()
1375
- } else {
1376
- std::mem::replace(&mut self.closure_outer_return_type, saved_ret.clone())
1377
- };
1378
- let result = self.rewriteBlock(&mut cl.body, &mut closure_env);
1379
- self.current_return_type = saved_ret;
1380
- self.return_type_is_closure_expected = saved_flag;
1381
- self.closure_outer_return_type = saved_outer;
1382
- result?;
1383
- } else {
1384
- self.rewriteBlock(&mut cl.body, &mut closure_env)?;
1385
- }
1386
- }
1387
- Ok(())
1388
- }
1389
-
1390
- /// If `call` invokes a method that has generic param(s) of its OWN, beyond
1391
- /// its receiver enum's own (e.g. `List[T].map`'s `U`) — a shape
1392
- /// `registerEnumMethodSignatures`/the `PendingSpecialization::Enum`
1393
- /// worklist arm deliberately skip producing anything for — infers those
1394
- /// extra param(s) from `call`'s own arguments (a plain arg's own type for a
1395
- /// bare-letter param, or a closure argument's INFERRED BODY TYPE for a
1396
- /// closure-typed param whose return is the letter), then specializes+
1397
- /// registers a new per-CALL-SITE mangled method (e.g. `"map$Str"`) — the
1398
- /// same idea as a truly generic free function's own `fns_generic`/
1399
- /// `resolveFnInstantiation`, just combined with the receiver's own already-
1400
- /// known enum-level bindings (e.g. `T = Int`) — and rewrites `call.name`
1401
- /// to it. No-op if `object`'s type isn't a specialization this pass has
1402
- /// recorded `class_specialization_info` for, or the method has no extra
1403
- /// generics of its own (the ordinary, already-handled case).
1404
- fn resolveMethodOwnGenerics(
1405
- &mut self,
1406
- object: &ast::Expr,
1407
- call: &mut ast::FnCall,
1408
- env: &TypeEnv,
1409
- ) -> Result<(), String> {
1410
- let PlumType::TNamed(mangled_receiver) = self.inferConcrete(object, env) else {
1411
- return Ok(());
1412
- };
1413
- let Some((template_name, class_bindings)) = self
1414
- .class_specialization_info
1415
- .get(&mangled_receiver)
1416
- .cloned()
1417
- else {
1418
- return Ok(());
1419
- };
1420
- let method = self
1421
- .methods_generic_on_enum
1422
- .get(template_name.as_str())
1423
- .and_then(|methods| methods.iter().find(|m| m.name == call.name).copied());
1424
- let Some(method) = method else { return Ok(()) };
1425
- let owner_params: Vec<String> = class_bindings.keys().cloned().collect();
1426
- let extra = methodOwnGenericParams(method, &owner_params);
1427
- if extra.is_empty() {
1428
- return Ok(());
1429
- }
1430
-
1431
- let mut full_bindings = class_bindings.clone();
1432
- for (letter, source) in &extra {
1433
- match *source {
1434
- ExtraGenericSource::Param(i) => {
1435
- let arg_expr = Self::argExprAt(call, i)
1436
- .ok_or_else(|| format!("monomorphize: '{}.{}' call is missing arg {} needed to infer its own generic parameter '{}'", template_name, call.name, i, letter))?;
1437
- full_bindings.insert(letter.clone(), self.infer(arg_expr, env));
1438
- }
1439
- ExtraGenericSource::ClosureReturn(i) => {
1440
- // The closure's body was already rewritten (any generic
1441
- // construction inside it, e.g. `andThen`'s `Next(n / 2)`,
1442
- // already mangled to its real specialization) by
1443
- // `rewriteMethodClosureArgs`, called before this — just
1444
- // re-seed the same closure-param env to infer its
1445
- // (already-rewritten) tail type, not rewrite it again.
1446
- let (closure_param_tys, declared_ret): (Vec<PlumType>, Option<ast::Type>) =
1447
- match &method.params[i].ty {
1448
- ast::ParamType::Fn(params, ret) => (
1449
- params
1450
- .iter()
1451
- .map(|t| {
1452
- substituteType(t, &Substitution(class_bindings.clone()))
1453
- })
1454
- .map(|t| crate::plumTypeFromAst(&t))
1455
- .collect(),
1456
- ret.as_deref().cloned(),
1457
- ),
1458
- _ => (Vec::new(), None),
1459
- };
1460
- let arg_expr = Self::argExprAt(call, i)
1461
- .ok_or_else(|| format!("monomorphize: '{}.{}' call is missing arg {} needed to infer its own generic parameter '{}'", template_name, call.name, i, letter))?;
1462
- let ast::Expr::Closure(cl) = arg_expr else {
1463
- return Err(format!(
1464
- "monomorphize: '{}.{}' needs a closure literal argument (not a stored value) to infer its own generic parameter '{}'",
1465
- template_name, call.name, letter
1466
- ));
1467
- };
1468
- let mut closure_env = env.clone();
1469
- for (p, ty) in cl.params.iter().zip(closure_param_tys.iter()) {
1470
- closure_env.insert(p.clone(), TypeScheme::mono(ty.clone()));
1471
- }
1472
- let bound = match cl.body.stmts.last() {
1473
- Some(ast::Stmt::Expr(e, ..)) => self.infer(e, &closure_env),
1474
- Some(ast::Stmt::Return(Some(e))) => self.infer(e, &closure_env),
1475
- _ => PlumType::TUnit,
1476
- };
1477
- // A bare closure return (`cb: fn(T) -> U`, e.g. `List.map`)
1478
- // binds `letter` directly to the closure's whole inferred
1479
- // type. A NESTED one (`cb: fn(T) -> Link[U]`, e.g.
1480
- // `Link.andThen`) instead needs `bound` (the closure's
1481
- // ACTUAL concrete return, e.g. `Link$Int`) matched back
1482
- // against the DECLARED shape `Link[U]` to recover what
1483
- // `U` itself was — `bindGenericArg` (shared with
1484
- // `resolveFnInstantiation`'s free-function argument
1485
- // binding) does exactly that structural extraction.
1486
- match &declared_ret {
1487
- Some(declared) if !declared.generics.is_empty() => {
1488
- self.bindGenericArg(declared, &bound, &mut full_bindings);
1489
- }
1490
- _ => {
1491
- full_bindings.insert(letter.clone(), bound);
1492
- }
1493
- }
1494
- }
1495
- }
1496
- }
1497
- for (letter, _) in &extra {
1498
- if !full_bindings.contains_key(letter) {
1499
- return Err(format!(
1500
- "monomorphize: '{}.{}' could not resolve its own generic parameter '{}' from this call site",
1501
- template_name, call.name, letter
1502
- ));
1503
- }
1504
- }
1505
-
1506
- let extra_type_args: Vec<PlumType> = extra
1507
- .iter()
1508
- .map(|(l, _)| full_bindings[l].clone())
1509
- .collect();
1510
- // Receiver-qualified (not just `mangle(&call.name, &extra_type_args)`)
1511
- // because `self.specialized`/`self.enqueued` (below, inherited from the
1512
- // generic-FREE-function machinery this reuses) are FLAT sets keyed by
1513
- // this bare string alone, globally across the whole program — without
1514
- // the receiver folded in, two unrelated receivers whose OWN method
1515
- // happens to mangle to the same suffix (e.g. `Link[Int].map` and
1516
- // `List[Int].map` both producing `"map$Str"`) would collide: the
1517
- // second one popped would find the first's bare name already in
1518
- // `specialized` and silently skip producing its own body.
1519
- let mangled_method = format!(
1520
- "{}${}",
1521
- mangled_receiver,
1522
- mangle(&call.name, &extra_type_args)
1523
- );
1524
- let key = (mangled_receiver.clone(), mangled_method.clone());
1525
- if !self.methods.contains_key(&key) {
1526
- // Eagerly resolve a full signature right now (mirroring
1527
- // `registerClassMethodSignatures`) so THIS SAME statement (e.g.
1528
- // `list.map(f).toStr()`) can resolve the call's result type
1529
- // immediately — separately from the fresh, UNRESOLVED copy queued
1530
- // below for the worklist to turn into a real body (its own
1531
- // `resolveFnParamTypes`/`resolveFnReturnType` need to run around
1532
- // `rewriteFnBody`, not before it, per their own ordering rules).
1533
- let mut sig_method = specializeFn(
1534
- method,
1535
- &Substitution(full_bindings.clone()),
1536
- &mangled_method,
1537
- Some(mangled_receiver.clone()),
1538
- );
1539
- self.resolveFnSignature(&mut sig_method);
1540
- let param_types: Vec<PlumType> = sig_method
1541
- .params
1542
- .iter()
1543
- .map(|p| match &p.ty {
1544
- ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
1545
- ast::ParamType::Variadic(t) => {
1546
- PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t)))
1547
- }
1548
- ast::ParamType::Fn(params, ret) => {
1549
- let param_types = params.iter().map(crate::plumTypeFromAst).collect();
1550
- let ret_ty = ret
1551
- .as_ref()
1552
- .map(|r| crate::plumTypeFromAst(r))
1553
- .unwrap_or(PlumType::TUnit);
1554
- PlumType::TFun(param_types, Box::new(ret_ty))
1555
- }
1556
- })
1557
- .collect();
1558
- let ret = sig_method
1559
- .returns
1560
- .as_ref()
1561
- .map(crate::plumTypeFromAst)
1562
- .unwrap_or(PlumType::TUnit);
1563
- self.methods
1564
- .insert(key, PlumType::TFun(param_types, Box::new(ret)));
1565
-
1566
- self.enqueued.insert(mangled_method.clone());
1567
- self.worklist.push(PendingSpecialization::Fn {
1568
- base: method,
1569
- subst: Substitution(full_bindings),
1570
- mangled: mangled_method.clone(),
1571
- new_receiver: Some(mangled_receiver),
1572
- });
1573
- }
1574
- call.name = mangled_method;
1575
- Ok(())
1576
- }
1577
-
1578
- /// Without this, a method call chained directly onto a FRESH
1579
- /// enum construction in the same statement (`Next(4).filter(...)`, or
1580
- /// any `SomeVariant(...).method(...)` one-liner for a generic enum) could
1581
- /// never resolve: the receiver's specialization is only ENQUEUED here,
1582
- /// not fully produced until the worklist drains (well after the current
1583
- /// forward pass over top-level items finishes), so `self.methods` had no
1584
- /// entry yet for `(mangled, method_name)` when `inferExpr` needed one —
1585
- /// silently falling back to an unresolved type, which then couldn't tell
1586
- /// a `match` on the result which mangled variant names to rewrite its
1587
- /// patterns to, surfacing many statements later and confusingly as a
1588
- /// codegen-time "variant missing from the GC type registry" panic.
1589
- fn registerEnumMethodSignatures(
1590
- &mut self,
1591
- e: &'a ast::Enum,
1592
- mangled: &str,
1593
- bindings: &BTreeMap<String, PlumType>,
1594
- ) {
1595
- let Some(methods) = self.methods_generic_on_enum.get(e.name.as_str()).cloned() else {
1596
- return;
1597
- };
1598
- let owner_params = enumGenericParams(e);
1599
- self.registerMethodSignatures(&e.name, mangled, methods, &owner_params, bindings);
1600
- }
1601
-
1602
- /// Shared body of `registerClassMethodSignatures`/`registerEnumMethodSignatures`
1603
- /// — eagerly resolves and registers (into `self.methods`) the `(mangled,
1604
- /// method_name) -> TFun` signature of every ORDINARY (no generic params
1605
- /// of its own — see the skip below) method in `methods`, for the
1606
- /// specialization named `mangled` under `bindings`, without producing the
1607
- /// actual `ast::Fn` bodies (that still only happens once the worklist
1608
- /// entry for this specialization is popped). If this specialization is
1609
- /// itself already nested (`isSelfNested`), skips (only) a method whose
1610
- /// return type would construct yet another nested specialization
1611
- /// (`methodMentionsTemplate`) — the self-nesting landmine. Deliberately
1612
- /// NOT a blanket skip for every method whenever `mangled` is
1613
- /// self-nested: `List$List$Int`'s own `add`/`get`/etc are perfectly
1614
- /// finite regardless (their return types don't mention `List` at all)
1615
- /// and are needed for `chunk`'s own body (`result.add(piece)`) to even
1616
- /// compile — only `chunk`/`partition` THEMSELVES, called again on an
1617
- /// already-nested `List$List$Int`, would recurse into a third level and
1618
- /// must be skipped.
1619
- fn registerMethodSignatures(
1620
- &mut self,
1621
- template_name: &str,
1622
- mangled: &str,
1623
- methods: Vec<&'a ast::Fn>,
1624
- owner_params: &[String],
1625
- bindings: &BTreeMap<String, PlumType>,
1626
- ) {
1627
- let already_nested = self.isSelfNested(template_name, bindings);
1628
- for method in methods {
1629
- // See the matching comment in the `PendingSpecialization::Class`/
1630
- // `PendingSpecialization::Enum` worklist arms — a method with its
1631
- // OWN extra generic param(s) is resolved per-call-site by
1632
- // `resolveMethodOwnGenerics` instead.
1633
- if !methodOwnGenericParams(method, owner_params).is_empty() {
1634
- continue;
1635
- }
1636
- if already_nested && Self::methodMentionsTemplate(method, template_name) {
1637
- continue;
1638
- }
1639
- let key = (mangled.to_string(), method.name.clone());
1640
- if self.methods.contains_key(&key) {
1641
- continue;
1642
- }
1643
- let mut specialized_method = specializeFn(
1644
- method,
1645
- &Substitution(bindings.clone()),
1646
- &method.name,
1647
- Some(mangled.to_string()),
1648
- );
1649
- self.resolveFnSignature(&mut specialized_method);
1650
- let param_types: Vec<PlumType> = specialized_method
1651
- .params
1652
- .iter()
1653
- .map(|p| match &p.ty {
1654
- ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
1655
- ast::ParamType::Variadic(t) => {
1656
- PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t)))
1657
- }
1658
- ast::ParamType::Fn(params, ret) => {
1659
- let param_types = params.iter().map(crate::plumTypeFromAst).collect();
1660
- let ret_ty = ret
1661
- .as_ref()
1662
- .map(|r| crate::plumTypeFromAst(r))
1663
- .unwrap_or(PlumType::TUnit);
1664
- PlumType::TFun(param_types, Box::new(ret_ty))
1665
- }
1666
- })
1667
- .collect();
1668
- let ret = specialized_method
1669
- .returns
1670
- .as_ref()
1671
- .map(crate::plumTypeFromAst)
1672
- .unwrap_or(PlumType::TUnit);
1673
- self.methods
1674
- .insert(key, PlumType::TFun(param_types, Box::new(ret)));
1675
- }
1676
- }
1677
-
1678
- /// If `expr` is a bare reference to a payload-free variant of a GENERIC
1679
- /// enum (`End`), and `expected` names a SPECIFIC specialization of that
1680
- /// same enum (`Link$Node$Int`), rewrites `expr`'s name to that
1681
- /// specialization's own mangled variant name (`End$Node$Int`) and
1682
- /// ensures that specialization is registered — codegen only ever knows
1683
- /// about specializations (the generic template enum is dropped entirely),
1684
- /// so an un-rewritten bare reference would be unresolvable by the time it
1685
- /// gets there. No-op if `expr` isn't a bare generic-enum variant, or
1686
- /// `expected` doesn't name a specialization of the SAME enum.
1687
- fn resolveBareVariantAgainstExpected(&mut self, expr: &mut ast::Expr, expected: &PlumType) {
1688
- self.markTagsAgainstExpected(expr, expected);
1689
- let ast::Expr::TypeName(n) = expr else { return };
1690
- let Some(e) = self.enums_generic_by_variant.get(n.as_str()).copied() else {
1691
- return;
1692
- };
1693
- let PlumType::TNamed(mangled) = expected else {
1694
- return;
1695
- };
1696
- if !mangled.starts_with(&format!("{}$", e.name)) {
1697
- return;
1698
- }
1699
- if let Some(table) = self.enum_variant_mangling.get(mangled) {
1700
- if let Some(mangled_variant) = table.get(n) {
1701
- *n = mangled_variant.clone();
1702
- }
1703
- }
1704
- }
1705
-
1706
- /// A tag literal (`Bad` / `Bad(x)`) whose expected type is a tag union
1707
- /// naming it, but whose name a declared variant also uses (`enum Rating =
1708
- /// | Bad(...)`): prefixes it with `crate::TAG_MARKER` so the checker and
1709
- /// codegen treat it as the tag, not the variant. Recurses into an
1710
- /// already-specialized variant construction's payloads (`Err$Int$[Bad](Bad)`)
1711
- /// against that variant's own field types.
1712
- fn markTagsAgainstExpected(&self, expr: &mut ast::Expr, expected: &PlumType) {
1713
- let collides = |n: &str| {
1714
- self.enum_variants.contains_key(n) || self.enums_generic_by_variant.contains_key(n)
1715
- };
1716
- match expr {
1717
- ast::Expr::Paren(inner) => self.markTagsAgainstExpected(inner, expected),
1718
- ast::Expr::TypeName(n) => {
1719
- if let PlumType::TTags(tags) = expected {
1720
- if matches!(tags.get(n.as_str()), Some(None)) && collides(n) {
1721
- *n = format!("{}{}", crate::TAG_MARKER, n);
1722
- }
1723
- }
1724
- }
1725
- ast::Expr::FnCall(call) => {
1726
- if let PlumType::TTags(tags) = expected {
1727
- if call.args.len() == 1
1728
- && matches!(tags.get(call.name.as_str()), Some(Some(_)))
1729
- && collides(&call.name)
1730
- {
1731
- call.name = format!("{}{}", crate::TAG_MARKER, call.name);
1732
- }
1733
- return;
1734
- }
1735
- if self.enums_generic_by_variant.contains_key(call.name.as_str()) {
1736
- return; // not specialized yet: no concrete field types
1737
- }
1738
- if let Some(info) = self.enum_variants.get(call.name.as_str()) {
1739
- for (arg, ft) in call.args.iter_mut().zip(info.field_types.iter()) {
1740
- let e = match arg {
1741
- ast::Arg::Positional(e) => e,
1742
- ast::Arg::Keyword { value, .. } => value,
1743
- ast::Arg::Pair { value, .. } => value,
1744
- };
1745
- self.markTagsAgainstExpected(e, ft);
1746
- }
1747
- }
1748
- }
1749
- _ => {}
1750
- }
1751
- }
1752
-
1753
- /// `Ok(3)` passed where a concrete `Try$Int$[X]` is expected (a typed
1754
- /// param or field): a generic variant whose own payload can't pin every
1755
- /// enum param takes its specialization from that expected type instead —
1756
- /// the arg/field analogue of `resolveEnumInstantiation`'s return-type
1757
- /// fallback. Also marks colliding tag payloads (`Err(Bad)`).
1758
- fn resolveGenericVariantAgainstExpected(&mut self, expr: &mut ast::Expr, expected: &PlumType) {
1759
- if let ast::Expr::FnCall(call) = expr {
1760
- if let Some(e) = self.enums_generic_by_variant.get(call.name.as_str()).copied() {
1761
- if let PlumType::TNamed(mangled) = expected {
1762
- if mangled.starts_with(&format!("{}$", e.name)) {
1763
- if let Some(mv) = self
1764
- .enum_variant_mangling
1765
- .get(mangled)
1766
- .and_then(|table| table.get(&call.name))
1767
- {
1768
- call.name = mv.clone();
1769
- }
1770
- }
1771
- }
1772
- }
1773
- }
1774
- self.markTagsAgainstExpected(expr, expected);
1775
- }
1776
-
1777
- /// If `expr`'s own inferred type is a builtin primitive (`Int`/`Float`)
1778
- /// and `expected` names a "union" enum with a bare-primitive-wrap variant
1779
- /// matching that exact primitive (`enum Number = | Int | Float` — see
1780
- /// `buildGlobalTables`'s primitive bare-wrap condition), rewrites `expr`
1781
- /// in place into that variant's constructor call (`5` -> `Int(5)`),
1782
- /// reusing the ordinary positional-variant-construction codegen path
1783
- /// (`compileVariantConstruction`) unchanged. This is the primitive
1784
- /// analogue of the class bare-wrap sugar's `ClassCall`->`FnCall` rewrite
1785
- /// just below — unlike that one (which fires unconditionally, keyed only
1786
- /// on the constructed name), this MUST be expected-type-directed: `Int`/
1787
- /// `Float` are used as bare primitives everywhere in the language and
1788
- /// can't be globally reinterpreted as "the enum-wrapped value". No-op if
1789
- /// `expected` isn't such an enum, or `expr` isn't already exactly that
1790
- /// primitive type (a genuine mismatch is left for `unify`/`unifyArg` to
1791
- /// catch elsewhere).
1792
- fn wrapPrimitiveAgainstExpected(
1793
- &mut self,
1794
- expr: &mut ast::Expr,
1795
- expected: &PlumType,
1796
- env: &TypeEnv,
1797
- ) {
1798
- let PlumType::TNamed(enum_name) = expected else {
1799
- return;
1800
- };
1801
- let actual = self.infer(expr, env);
1802
- if !matches!(actual, PlumType::TInt | PlumType::TFloat) {
1803
- return;
1804
- }
1805
- let owner = self
1806
- .enum_variants
1807
- .iter()
1808
- .find(|(_, info)| &info.enum_name == enum_name && info.field_types == [actual.clone()]);
1809
- if let Some((variant_name, _)) = owner {
1810
- let inner = expr.clone();
1811
- *expr = ast::Expr::FnCall(ast::FnCall {
1812
- name: variant_name.clone(),
1813
- args: vec![ast::Arg::Positional(inner)],
1814
- });
1815
- }
1816
- }
1817
-
1818
- /// True if the specialization about to be processed is ITSELF already a
1819
- /// nested instance of `template_name` — one of `bindings`'s VALUES is
1820
- /// either `template_name` itself or a recorded specialization OF it
1821
- /// (via `class_specialization_info`), e.g. `List$Int`'s `T` bound to
1822
- /// `Int` is NOT self-nested, but `List$List$Int`'s `T` bound to
1823
- /// `List$Int` IS. Guards the self-nesting landmine (a method returning
1824
- /// its OWN owning class/enum nested in itself, e.g. `List[T].chunk() ->
1825
- /// List[List[T]]`): resolving `chunk`'s return type for a specialization
1826
- /// that's ALREADY nested would create yet another, deeper nested
1827
- /// specialization, forever — and because this eager registration
1828
- /// happens once per WORKLIST entry (processed iteratively, not
1829
- /// recursively), a plain call-stack-scoped recursion guard tried first
1830
- /// was NOT enough to stop it (confirmed: hung 20+s with only that).
1831
- /// Combined with `methodMentionsTemplate` (only reachable, and only
1832
- /// meaningful, when this is true) — a specialization's OWN `add`/`get`/
1833
- /// etc are perfectly finite regardless of nesting and must NOT be
1834
- /// skipped, only a method whose return type would construct yet ANOTHER
1835
- /// nested specialization.
1836
- fn isSelfNested(&self, template_name: &str, bindings: &BTreeMap<String, PlumType>) -> bool {
1837
- bindings.values().any(|t| match t {
1838
- PlumType::TNamed(n) => {
1839
- n == template_name
1840
- || self
1841
- .class_specialization_info
1842
- .get(n.as_str())
1843
- .map(|(base, _)| base == template_name)
1844
- .unwrap_or(false)
1845
- }
1846
- _ => false,
1847
- })
1848
- }
1849
-
1850
- /// True if `method`'s OWN declared return type mentions `template_name`
1851
- /// anywhere (at any nesting depth, INCLUDING the top level — unlike a
1852
- /// naive structural check, depth doesn't matter here: this is only ever
1853
- /// consulted once `isSelfNested` has already confirmed the CURRENT
1854
- /// specialization is itself nested, at which point even a top-level
1855
- /// `List[...]` return needs a new, deeper nested specialization). See
1856
- /// `isSelfNested`'s doc comment for the full picture.
1857
- fn methodMentionsTemplate(method: &ast::Fn, template_name: &str) -> bool {
1858
- fn mentions(ty: &ast::Type, template_name: &str) -> bool {
1859
- ty.name == template_name || ty.generics.iter().any(|g| mentions(g, template_name))
1860
- }
1861
- method
1862
- .returns
1863
- .as_ref()
1864
- .map(|ret| mentions(ret, template_name))
1865
- .unwrap_or(false)
1866
- }
1867
-
1868
- /// Fills `out` with a binding for every bare generic-letter type occurring
1869
- /// in `declared` (possibly nested inside a container type, e.g. `T` inside
1870
- /// `List[T]`), given `actual` — the ACTUAL concrete `PlumType` an argument
1871
- /// declared as `declared` was inferred to have. The bare case (`declared`
1872
- /// IS the letter, e.g. `a: T`) binds it directly to `actual`. The
1873
- /// container case (`declared` is `List[T]`/`Link[T]`/`Try[T, E]`,
1874
- /// `actual` is that container's own mangled specialization like
1875
- /// `List$Int`) looks up `actual`'s recorded `class_specialization_info`
1876
- /// (the SAME table `resolveMethodOwnGenerics` already trusts for a
1877
- /// method's own receiver) to recover the concrete type EACH of the
1878
- /// container's own generic params was bound to, then recurses — so
1879
- /// `List[T]` against `List$Int` binds `T -> Int`, and (one level deeper)
1880
- /// `List[Link[T]]` against `List$Link$Int` binds `T -> Int` too. A
1881
- /// mismatch (wrong container, or `actual` isn't even a named/specialized
1882
- /// type) silently binds nothing for that param, same as before this
1883
- /// existed — `resolveFnInstantiation`'s caller already rejects an
1884
- /// incomplete `bindings` set with a clear error.
1885
- fn bindGenericArg(
1886
- &self,
1887
- declared: &ast::Type,
1888
- actual: &PlumType,
1889
- out: &mut BTreeMap<String, PlumType>,
1890
- ) {
1891
- if declared.generics.is_empty() {
1892
- if isGenericParamName(&declared.name) {
1893
- out.entry(declared.name.clone())
1894
- .or_insert_with(|| actual.clone());
1895
- }
1896
- return;
1897
- }
1898
- let PlumType::TNamed(mangled) = actual else {
1899
- return;
1900
- };
1901
- let Some((template_name, class_bindings)) =
1902
- self.class_specialization_info.get(mangled.as_str())
1903
- else {
1904
- return;
1905
- };
1906
- if template_name != &declared.name {
1907
- return;
1908
- }
1909
- let owner_params: Vec<String> = self
1910
- .enums_generic_by_name
1911
- .get(template_name.as_str())
1912
- .map(|e| enumGenericParams(e))
1913
- .unwrap_or_default();
1914
- for (declared_arg, owner_param) in declared.generics.iter().zip(owner_params.iter()) {
1915
- if let Some(bound) = class_bindings.get(owner_param) {
1916
- self.bindGenericArg(declared_arg, bound, out);
1917
- }
1918
- }
1919
- }
1920
-
1921
- fn resolveFnInstantiation(
1922
- &mut self,
1923
- call: &mut ast::FnCall,
1924
- env: &TypeEnv,
1925
- ) -> Result<(), String> {
1926
- let Some(f) = self.fns_generic.get(call.name.as_str()).copied() else {
1927
- return Ok(());
1928
- };
1929
- let params = fnGenericParams(f);
1930
- let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
1931
- for (param, arg) in f.params.iter().zip(call.args.iter()) {
1932
- let declared = match &param.ty {
1933
- ast::ParamType::Type(t) => Some(t),
1934
- ast::ParamType::Variadic(t) => Some(t),
1935
- // TODO: fn-value params don't yet resolve to a generic parameter.
1936
- ast::ParamType::Fn(_, _) => None,
1937
- };
1938
- if let Some(declared) = declared {
1939
- let arg_expr = match arg {
1940
- ast::Arg::Positional(e) => e,
1941
- ast::Arg::Keyword { value, .. } => value,
1942
- ast::Arg::Pair { value, .. } => value,
1943
- };
1944
- let actual = self.infer(arg_expr, env);
1945
- self.bindGenericArg(declared, &actual, &mut bindings);
1946
- }
1947
- }
1948
- if bindings.len() != params.len() {
1949
- return Err(format!(
1950
- "monomorphize: could not resolve all generic parameters for '{}' at this call site",
1951
- call.name
1952
- ));
1953
- }
1954
- let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
1955
- let mangled = mangle(&call.name, &type_args);
1956
- if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
1957
- self.enqueued.insert(mangled.clone());
1958
- // Eagerly resolve and register this specialization's full signature
1959
- // into `global_env` right now (mirroring `registerClassMethodSignatures`
1960
- // for classes) — otherwise a statement in the SAME pass over the
1961
- // caller's body that needs this call's concrete return type (e.g.
1962
- // `l := makeList(1, 2, 3)` immediately followed by `l.map(...)`,
1963
- // where `resolveMethodOwnGenerics` needs `l`'s type to already be the
1964
- // mangled `"List$Int"`, not a fallback `TVar`) would see nothing —
1965
- // `global_env` was otherwise only updated once the worklist entry
1966
- // below is popped, which happens well after this whole top-level
1967
- // item has already been rewritten.
1968
- let mut sig_fn = specializeFn(f, &Substitution(bindings.clone()), &mangled, None);
1969
- self.resolveFnSignature(&mut sig_fn);
1970
- let param_types: Vec<PlumType> = sig_fn
1971
- .params
1972
- .iter()
1973
- .map(|p| match &p.ty {
1974
- ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
1975
- ast::ParamType::Variadic(t) => {
1976
- PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t)))
1977
- }
1978
- ast::ParamType::Fn(params, ret) => {
1979
- let param_types = params.iter().map(crate::plumTypeFromAst).collect();
1980
- let ret_ty = ret
1981
- .as_ref()
1982
- .map(|r| crate::plumTypeFromAst(r))
1983
- .unwrap_or(PlumType::TUnit);
1984
- PlumType::TFun(param_types, Box::new(ret_ty))
1985
- }
1986
- })
1987
- .collect();
1988
- let ret = sig_fn
1989
- .returns
1990
- .as_ref()
1991
- .map(crate::plumTypeFromAst)
1992
- .unwrap_or(PlumType::TUnit);
1993
- self.global_env.insert(
1994
- mangled.clone(),
1995
- TypeScheme::mono(PlumType::TFun(param_types, Box::new(ret))),
1996
- );
1997
-
1998
- self.worklist.push(PendingSpecialization::Fn {
1999
- base: f,
2000
- subst: Substitution(bindings),
2001
- mangled: mangled.clone(),
2002
- new_receiver: None,
2003
- });
2004
- }
2005
- call.name = mangled;
2006
- Ok(())
2007
- }
2008
-
2009
- /// The bare names of any generic class or enum referenced directly (not via a
2010
- /// lowercase-letter generic parameter) in `f`'s param types — e.g. `"Link"` for
2011
- /// `unwrapOr(o: Link, default: Int) -> Int`. See `fns_bare_generic`'s doc
2012
- /// comment for why such a function needs its own specialization.
2013
- fn fnBareGenericRefs(&self, f: &ast::Fn) -> Vec<String> {
2014
- let mut names: Vec<String> = Vec::new();
2015
- for p in &f.params {
2016
- let t = match &p.ty {
2017
- ast::ParamType::Type(t) => t,
2018
- ast::ParamType::Variadic(t) => t,
2019
- // TODO: fn-value params don't yet participate in bare-generic resolution.
2020
- ast::ParamType::Fn(_, _) => continue,
2021
- };
2022
- // A param already fully instantiated with concrete type arguments
2023
- // (`headers: Map[Str, Str]`) is NOT a bare reference to the generic
2024
- // template — only the truly bare spelling (`headers: Map`, no `[...]`
2025
- // at all) needs this function's own per-call-site specialization.
2026
- // Without this check, ANY function taking an already-concrete
2027
- // generic-typed param (however unrelated to its own logic) was
2028
- // wrongly routed through `fns_bare_generic`'s worklist instead of
2029
- // the ordinary per-item pass, silently dropping it from `m.produced`
2030
- // under its original name — the caller's own inferred return type
2031
- // (via `fn_return_generic_enum`, which doesn't consult this
2032
- // classification at all) still looked correct, masking the missing
2033
- // function until codegen.
2034
- if !t.generics.is_empty() {
2035
- continue;
2036
- }
2037
- let n = &t.name;
2038
- if self.enums_generic_by_name.contains_key(n.as_str()) && !names.iter().any(|x| x == n)
2039
- {
2040
- names.push(n.clone());
2041
- }
2042
- }
2043
- names
2044
- }
2045
-
2046
- /// Resolves a call to an otherwise-ordinary function whose param type(s)
2047
- /// bare-name a generic class/enum, specializing it per call site exactly like a
2048
- /// truly-generic function — reusing the same `PendingSpecialization::Fn`
2049
- /// worklist entry and the unmodified `specializeFn`, whose substitution
2050
- /// mechanism already replaces any type whose bare name matches a substitution
2051
- /// key (it doesn't care whether that key came from a lowercase-letter generic
2052
- /// parameter or a bare generic class/enum reference).
2053
- fn resolveBareGenericFnInstantiation(
2054
- &mut self,
2055
- call: &mut ast::FnCall,
2056
- env: &TypeEnv,
2057
- ) -> Result<(), String> {
2058
- let Some(f) = self.fns_bare_generic.get(call.name.as_str()).copied() else {
2059
- return Ok(());
2060
- };
2061
- let refs = self.fnBareGenericRefs(f);
2062
- let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
2063
- for (param, arg) in f.params.iter().zip(call.args.iter()) {
2064
- let n = match &param.ty {
2065
- ast::ParamType::Type(t) => t.name.clone(),
2066
- ast::ParamType::Variadic(t) => t.name.clone(),
2067
- // TODO: fn-value params don't yet resolve to a bare generic reference.
2068
- ast::ParamType::Fn(_, _) => String::new(),
2069
- };
2070
- if refs.contains(&n) {
2071
- let arg_expr = match arg {
2072
- ast::Arg::Positional(e) => e,
2073
- ast::Arg::Keyword { value, .. } => value,
2074
- ast::Arg::Pair { value, .. } => value,
2075
- };
2076
- bindings
2077
- .entry(n)
2078
- .or_insert_with(|| self.infer(arg_expr, env));
2079
- }
2080
- }
2081
- if bindings.len() != refs.len() {
2082
- return Err(format!(
2083
- "monomorphize: could not resolve all generic parameters for '{}' at this call site",
2084
- call.name
2085
- ));
2086
- }
2087
- let type_args: Vec<PlumType> = refs.iter().map(|p| bindings[p].clone()).collect();
2088
- let mangled = mangle(&call.name, &type_args);
2089
- if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
2090
- self.enqueued.insert(mangled.clone());
2091
- self.worklist.push(PendingSpecialization::Fn {
2092
- base: f,
2093
- subst: Substitution(bindings),
2094
- mangled: mangled.clone(),
2095
- new_receiver: None,
2096
- });
2097
- }
2098
- call.name = mangled;
2099
- Ok(())
2100
- }
2101
-
2102
- /// Resolves a construction of a generic enum's variant (e.g. `Next(5)` for
2103
- /// `enum Link = | Next(a) | End`), rewriting `call.name` from the bare
2104
- /// variant name (`Next`) to its mangled form (`Next$Int`) once the enum's own
2105
- /// concrete instantiation is known. Mangling is eager and deterministic — it
2106
- /// doesn't wait for the worklist to actually produce the specialized `ast::Enum`
2107
- /// (see `enum_variant_mangling`'s doc comment).
2108
- ///
2109
- /// A variant that carries no generic fields (e.g. `End`) can't pin down the
2110
- /// enum's type parameters on its own, so such a construction site is left alone
2111
- /// here — some other construction site (e.g. `Next(5)`) is what drives the
2112
- /// specialization. (A bare `End` used as a *value*, not a call, is
2113
- /// `ast::Expr::TypeName` and doesn't go through this function at all — see the
2114
- /// plan's Global Constraints for that narrower, documented residual limitation.)
2115
- fn resolveEnumInstantiation(
2116
- &mut self,
2117
- call: &mut ast::FnCall,
2118
- env: &TypeEnv,
2119
- ) -> Result<(), String> {
2120
- let Some(e) = self
2121
- .enums_generic_by_variant
2122
- .get(call.name.as_str())
2123
- .copied()
2124
- else {
2125
- return Ok(());
2126
- };
2127
- let params = enumGenericParams(e);
2128
- let Some(variant) = e.variants.iter().find(|v| v.name == call.name) else {
2129
- return Ok(());
2130
- };
2131
- // A payload whose param the enclosing return type pins to a tag union
2132
- // (`return Err(Bad)` in `-> Try[Int, [Bad]]`) is that tag, even when a
2133
- // declared variant shares its name — mark it before inferring from it.
2134
- if let Some(rt) = self.current_return_type.clone() {
2135
- if rt.name == e.name && rt.generics.len() == params.len() {
2136
- for (field_ty, arg) in variant.fields.iter().zip(call.args.iter_mut()) {
2137
- let Some(pi) = params.iter().position(|p| p == &field_ty.name) else {
2138
- continue;
2139
- };
2140
- let gt = &rt.generics[pi];
2141
- if gt.generics.is_empty() && isGenericParamName(&gt.name) {
2142
- continue;
2143
- }
2144
- let resolved = self.resolveFieldType(gt);
2145
- let expected = crate::plumTypeFromAst(&resolved);
2146
- if matches!(expected, PlumType::TTags(_)) {
2147
- let arg_expr = match arg {
2148
- ast::Arg::Positional(e) => e,
2149
- ast::Arg::Keyword { value, .. } => value,
2150
- ast::Arg::Pair { value, .. } => value,
2151
- };
2152
- self.markTagsAgainstExpected(arg_expr, &expected);
2153
- }
2154
- }
2155
- }
2156
- }
2157
- let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
2158
- for (field_ty, arg) in variant.fields.iter().zip(call.args.iter()) {
2159
- if params.contains(&field_ty.name) {
2160
- let arg_expr = match arg {
2161
- ast::Arg::Positional(e) => e,
2162
- ast::Arg::Keyword { value, .. } => value,
2163
- ast::Arg::Pair { value, .. } => value,
2164
- };
2165
- bindings
2166
- .entry(field_ty.name.clone())
2167
- .or_insert_with(|| self.infer(arg_expr, env));
2168
- }
2169
- }
2170
- // This single construction site couldn't pin down every generic parameter
2171
- // by itself — e.g. `Ok(5)` only ever supplies `Try`'s `T`, never its `E`
2172
- // (no `Ok` call site can, since `Err`'s payload is a disjoint field). Fall
2173
- // back to the enclosing function's declared return type, if it names this
2174
- // same enum with an explicit, fully-general `[...]` instantiation (e.g.
2175
- // `-> Try[Int, Str]`) — that's the one other place a type this
2176
- // construction site can't see on its own is written down on purpose.
2177
- if bindings.len() != params.len() {
2178
- if let Some(rt) = self.current_return_type.clone() {
2179
- if rt.name == e.name && rt.generics.len() == params.len() {
2180
- for (p, gt) in params.iter().zip(rt.generics.iter()) {
2181
- // A closure's expected return can still name the
2182
- // method's own not-yet-inferred letter (`U` in
2183
- // `Try[U, [Bad]]`) — that's what this site is about
2184
- // to determine, not a binding to copy.
2185
- // Fall back to the enclosing function's return (the
2186
- // pre-closure-expected behavior), so `return
2187
- // x.andThen(|n| Err(Bad))` in a `-> Try[Int, [Bad]]`
2188
- // function still pins `T`.
2189
- if self.return_type_is_closure_expected
2190
- && gt.generics.is_empty()
2191
- && isGenericParamName(&gt.name)
2192
- {
2193
- if bindings.contains_key(p) {
2194
- continue;
2195
- }
2196
- if let Some(outer) = self.closure_outer_return_type.clone() {
2197
- if outer.name == e.name && outer.generics.len() == params.len() {
2198
- let og = &outer.generics[params.iter().position(|q| q == p).unwrap()];
2199
- let resolved = self.resolveFieldType(og);
2200
- bindings.insert(p.clone(), crate::plumTypeFromAst(&resolved));
2201
- }
2202
- }
2203
- continue;
2204
- }
2205
- if !bindings.contains_key(p) {
2206
- // See the matching comment in
2207
- // `resolveClassInstantiation` — `gt` needs
2208
- // `resolveFieldType`'s recursive resolution
2209
- // before `plumTypeFromAst`, or a generic-with-args
2210
- // type argument (e.g. `Try[Link[Int], Str]`)
2211
- // would have its own nested generics silently
2212
- // dropped.
2213
- let resolved = self.resolveFieldType(gt);
2214
- bindings.insert(p.clone(), crate::plumTypeFromAst(&resolved));
2215
- }
2216
- }
2217
- }
2218
- }
2219
- }
2220
- // Still couldn't pin down every generic parameter (e.g. a payload-free
2221
- // `None`, or no informative return-type annotation either). Leave it for
2222
- // another site to drive.
2223
- if bindings.len() != params.len() {
2224
- return Ok(());
2225
- }
2226
- let mangled = self.ensureEnumSpecialized(e, &params, bindings);
2227
- call.name = self.enum_variant_mangling[&mangled][&variant.name].clone();
2228
- Ok(())
2229
- }
2230
-
2231
- /// `Type(1, 2, 3)` — a bare-positional-args call on a GENERIC record-shaped
2232
- /// enum name that declares an `init` method (e.g. `List`'s own
2233
- /// `init(values: ...T)`) — is sugar for `Type.init(1, 2, 3)` (see
2234
- /// `plum-checker`'s `inferExpr` / `plum-wasm-codegen`'s matching `FnCall`
2235
- /// handling, which run AFTER monomorphization and only ever see concrete,
2236
- /// mangled names). Infers `init`'s own generic bindings from `call.args`'
2237
- /// types (mirroring `resolveEnumInstantiation`'s args-based inference for
2238
- /// a variant's fields), specializes the enum for them, and rewrites
2239
- /// `call.name` to the mangled name — same rewrite target
2240
- /// `resolveClassInstantiation` uses for the named `Type(field: value, ...)`
2241
- /// call shape, just reached from a differently-shaped call site.
2242
- fn resolveClassInitInstantiation(
2243
- &mut self,
2244
- call: &mut ast::FnCall,
2245
- env: &TypeEnv,
2246
- ) -> Result<(), String> {
2247
- let Some(e) = self.enums_generic_by_name.get(call.name.as_str()).copied() else {
2248
- return Ok(());
2249
- };
2250
- let Some(init_fn) = self
2251
- .methods_generic_on_enum
2252
- .get(call.name.as_str())
2253
- .and_then(|methods| methods.iter().find(|f| f.name == "init").copied())
2254
- else {
2255
- return Ok(());
2256
- };
2257
- let params = enumGenericParams(e);
2258
- let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
2259
- for (i, arg) in call.args.iter().enumerate() {
2260
- let Some(param) = init_fn.params.get(i).or_else(|| init_fn.params.last()) else {
2261
- break;
2262
- };
2263
- let param_ty = match &param.ty {
2264
- ast::ParamType::Type(t) => Some(t),
2265
- ast::ParamType::Variadic(t) => Some(t),
2266
- ast::ParamType::Fn(_, _) => None,
2267
- };
2268
- let Some(param_ty) = param_ty else { continue };
2269
- if params.contains(&param_ty.name) && !bindings.contains_key(&param_ty.name) {
2270
- let arg_expr = match arg {
2271
- ast::Arg::Positional(e) => e,
2272
- ast::Arg::Keyword { value, .. } => value,
2273
- ast::Arg::Pair { value, .. } => value,
2274
- };
2275
- bindings.insert(param_ty.name.clone(), self.infer(arg_expr, env));
2276
- }
2277
- }
2278
- // Same fallback as `resolveEnumInstantiation`/`resolveClassInstantiation` —
2279
- // an `init` whose args alone can't pin down every generic param (e.g. one
2280
- // with a fixed, non-generic-typed param alongside the generic one) falls
2281
- // back to the enclosing function's own declared return type.
2282
- if bindings.len() != params.len() {
2283
- if let Some(rt) = self.current_return_type.clone() {
2284
- if rt.name == call.name && rt.generics.len() == params.len() {
2285
- for (p, gt) in params.iter().zip(rt.generics.iter()) {
2286
- if !bindings.contains_key(p) {
2287
- let resolved = self.resolveFieldType(gt);
2288
- bindings.insert(p.clone(), crate::plumTypeFromAst(&resolved));
2289
- }
2290
- }
2291
- }
2292
- }
2293
- }
2294
- if bindings.len() != params.len() {
2295
- return Ok(());
2296
- }
2297
- let mangled = self.ensureEnumSpecialized(e, &params, bindings);
2298
- call.name = mangled;
2299
- Ok(())
2300
- }
2301
-
2302
- /// Registers (if not already registered) the specialization of generic enum
2303
- /// `e` at `bindings` — mangling every variant name, teaching `self.enum_variants`
2304
- /// about each mangled variant (so `self.infer` on an already-rewritten
2305
- /// construction site resolves correctly instead of falling back to an
2306
- /// uninformative `TVar`), and enqueueing the specialization to actually be
2307
- /// produced. Returns the mangled enum name. Shared by `resolveEnumInstantiation`
2308
- /// (bindings inferred from a construction site's own args, falling back to the
2309
- /// enclosing return type) and `resolveCallReturnType` (bindings taken directly
2310
- /// from a callee's *own* declared return type, with no construction site at all
2311
- /// — see its doc comment).
2312
- fn ensureEnumSpecialized(
2313
- &mut self,
2314
- e: &'a ast::Enum,
2315
- params: &[String],
2316
- bindings: BTreeMap<String, PlumType>,
2317
- ) -> String {
2318
- let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
2319
- let mangled = mangle(&e.name, &type_args);
2320
- self.class_specialization_info
2321
- .entry(mangled.clone())
2322
- .or_insert_with(|| (e.name.clone(), bindings.clone()));
2323
- if !self.enum_variant_mangling.contains_key(&mangled) {
2324
- // Insert a placeholder table BEFORE recursively resolving any variant's
2325
- // own field types below — a self-referential generic enum (e.g. a
2326
- // linked-list `Node[T]`'s own `next: Link[Node[T]]` field) would
2327
- // otherwise recurse right back into specializing itself here before
2328
- // this specialization is even marked as in-progress, infinitely. The
2329
- // recursive re-entry only ever needs this specialization's MANGLED
2330
- // NAME back (see `resolveFieldType`'s `ast::Type { name: mangled, ...
2331
- // }` return), not its fully-resolved variant table — that gets
2332
- // overwritten with the real one below once the recursion unwinds.
2333
- self.enum_variant_mangling
2334
- .insert(mangled.clone(), BTreeMap::new());
2335
- let mut table = BTreeMap::new();
2336
- for (tag, v) in e.variants.iter().enumerate() {
2337
- let mangled_variant = mangle(&v.name, &type_args);
2338
- table.insert(v.name.clone(), mangled_variant.clone());
2339
- let field_subst = Substitution(bindings.clone());
2340
- let field_types: Vec<PlumType> = v
2341
- .fields
2342
- .iter()
2343
- .map(|f| {
2344
- let substituted = substituteType(f, &field_subst);
2345
- let resolved = self.resolveFieldType(&substituted);
2346
- crate::plumTypeFromAst(&resolved)
2347
- })
2348
- .collect();
2349
- self.enum_variants.insert(
2350
- mangled_variant,
2351
- EnumVariantInfo {
2352
- enum_name: mangled.clone(),
2353
- tag: tag as i32,
2354
- field_types,
2355
- field_names: v.field_names.clone(),
2356
- values: v.values.clone(),
2357
- generics: Vec::new(),
2358
- },
2359
- );
2360
- }
2361
- self.enum_variant_mangling.insert(mangled.clone(), table);
2362
- // Eagerly registers this specialization's ORDINARY methods'
2363
- // signatures right now (see `registerEnumMethodSignatures`'s doc
2364
- // comment) — a method called on this value in the SAME statement
2365
- // that constructs it (`Next(4).filter(...)`) needs
2366
- // `self.methods` to already know about it, well before the
2367
- // worklist entry below is ever popped. MUST run only inside this
2368
- // `if` (guarded by the `enum_variant_mangling` insert just
2369
- // above, which marks this exact specialization as already being
2370
- // set up) — a method whose own return type re-mentions this SAME
2371
- // specialization (`Link[T]`'s `filter` returning `Link[T]`)
2372
- // recurses right back into `ensureEnumSpecialized("Link$Int",
2373
- // ...)`; running this unconditionally on every call (rather than
2374
- // only the first) recurses infinitely and stack-overflows.
2375
- self.registerEnumMethodSignatures(e, &mangled, &bindings);
2376
- }
2377
- if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
2378
- self.enqueued.insert(mangled.clone());
2379
- self.worklist.push(PendingSpecialization::Enum {
2380
- base: e,
2381
- subst: Substitution(bindings),
2382
- mangled: mangled.clone(),
2383
- });
2384
- }
2385
- mangled
2386
- }
2387
-
2388
- /// Fully resolves a FIELD's declared type into something that will actually
2389
- /// exist after monomorphization. `specializeEnum`'s own field substitution
2390
- /// only replaces a bare generic-param NAME (`T` -> `Int`) — a field declared
2391
- /// `Link[Node[T]]` becomes `Link[Node[Int]]` this way, which is now
2392
- /// fully CONCRETE but still names the generic TEMPLATES `Link`/`Node`
2393
- /// directly, both of which monomorphization deletes from the output (only
2394
- /// their mangled specializations, e.g. `Node$Int`, survive). Recursively
2395
- /// resolves any nested generic arguments first (so `Node[Int]` inside
2396
- /// `Link[Node[Int]]` becomes `Node$Int` before `Link[...]` itself is
2397
- /// resolved), then — if the type names a known generic enum applied to
2398
- /// arguments — mangles it to that specialization's real name and enqueues
2399
- /// the specialization if it hasn't been already, via the same
2400
- /// `ensureEnumSpecialized` used for enum construction sites. A field
2401
- /// that's already concrete (no generics), or whose name isn't a known
2402
- /// generic template, is returned unchanged (or with just its nested
2403
- /// generics resolved) — this is ALSO called on every ordinary
2404
- /// (non-generic) enum's fields, not just specialized ones, since a plain
2405
- /// record-shaped enum can perfectly well have a field like `items:
2406
- /// List[Int]`.
2407
- fn resolveFieldType(&mut self, ty: &ast::Type) -> ast::Type {
2408
- if ty.generics.is_empty() {
2409
- return ty.clone();
2410
- }
2411
- let resolved_args: Vec<ast::Type> = ty
2412
- .generics
2413
- .iter()
2414
- .map(|g| self.resolveFieldType(g))
2415
- .collect();
2416
- // A type argument that's STILL a bare single-uppercase-letter name after
2417
- // resolving means it's a truly free type variable at this point in the
2418
- // pipeline — e.g. `List[U]` inside `List[T]`'s own `map` method, where
2419
- // `U` is `map`'s OWN generic param, not `List`'s `T` (already substituted
2420
- // to a concrete type by the time this runs). Mangling/specializing
2421
- // against a placeholder name would silently manufacture a bogus
2422
- // `List$U` "specialization" baked from the letter U itself, so leave the
2423
- // whole type unresolved instead — see the `map`/method-level-generics
2424
- // gap noted in `libs/std/list.plum`.
2425
- if resolved_args.iter().any(|a| isGenericParamName(&a.name)) {
2426
- return ast::Type {
2427
- name: ty.name.clone(),
2428
- generics: resolved_args,
2429
- };
2430
- }
2431
- let type_args: Vec<PlumType> = resolved_args.iter().map(crate::plumTypeFromAst).collect();
2432
-
2433
- if let Some(e) = self.enums_generic_by_name.get(ty.name.as_str()).copied() {
2434
- let params = enumGenericParams(e);
2435
- if params.len() == type_args.len() {
2436
- let bindings: BTreeMap<String, PlumType> = params
2437
- .iter()
2438
- .cloned()
2439
- .zip(type_args.iter().cloned())
2440
- .collect();
2441
- let mangled = self.ensureEnumSpecialized(e, &params, bindings);
2442
- return ast::Type {
2443
- name: mangled,
2444
- generics: vec![],
2445
- };
2446
- }
2447
- }
2448
- ast::Type {
2449
- name: ty.name.clone(),
2450
- generics: resolved_args,
2451
- }
2452
- }
2453
-
2454
- /// Resolves every generic-with-args type in `f`'s own signature (params,
2455
- /// return) via `resolveFieldType`, mutating `f` in place. Must run before
2456
- /// `f` is pushed to `m.produced` — the real checker rebuilds its tables
2457
- /// FRESH from that final AST (via `buildGlobalTables`, using the plain
2458
- /// generics-dropping `plumTypeFromAst`), so whatever a signature still
2459
- /// says at that point is what the checker sees; a bare unresolved `Node`
2460
- /// or `List[Int]` left in a param would either dangle or get its generics
2461
- /// silently dropped again.
2462
- fn resolveFnSignature(&mut self, f: &mut ast::Fn) {
2463
- self.resolveFnParamTypes(f);
2464
- self.resolveFnReturnType(f);
2465
- }
2466
-
2467
- /// Just the params half of `resolveFnSignature` — deliberately split out
2468
- /// so callers can run this BEFORE `rewriteFnBody` (env seeding needs
2469
- /// param types already flattened, e.g. `Node[T]` -> `Node$Int`, or a
2470
- /// field access on a param would fail to resolve) while leaving
2471
- /// `f.returns` untouched until AFTER the body's been rewritten. The body
2472
- /// rewrite reads `self.current_return_type` (seeded from `f.returns` as
2473
- /// originally declared, generics and all) to drive
2474
- /// `resolveEnumInstantiation`'s fallback for construction sites that
2475
- /// can't infer every generic param from their own arguments alone (e.g.
2476
- /// `Err("...")` needs `Try`'s OTHER param, `T`, from the function's
2477
- /// own `-> Try[Int, Str]` declaration) — flattening the return type
2478
- /// up front would replace `rt.name` with an already-mangled name that
2479
- /// fallback's own bare-template-name comparison can never match again.
2480
- fn resolveFnParamTypes(&mut self, f: &mut ast::Fn) {
2481
- for p in &mut f.params {
2482
- match &mut p.ty {
2483
- ast::ParamType::Type(t) => *t = self.resolveFieldType(t),
2484
- ast::ParamType::Variadic(t) => *t = self.resolveFieldType(t),
2485
- // A closure-typed param's own param/return types can equally
2486
- // name a concrete generic instantiation after substitution
2487
- // (e.g. `andThen`'s `cb: fn(T) -> Link[U]` becomes `fn(Int)
2488
- // -> Link[Int]` once `T`/`U` are bound) — leaving them
2489
- // unresolved meant this param's type, once turned into a
2490
- // `PlumType` via `plumTypeFromAst` for the body's own type
2491
- // env, silently flattened to a bare, unmangled `Link`
2492
- // (`plumTypeFromAst` drops every type's `.generics`
2493
- // regardless of nesting depth), so a call through the
2494
- // closure param (`cb(v)`) inferred the wrong, un-mangled
2495
- // return type.
2496
- ast::ParamType::Fn(params, ret) => {
2497
- for t in params.iter_mut() {
2498
- *t = self.resolveFieldType(t);
2499
- }
2500
- if let Some(r) = ret {
2501
- **r = self.resolveFieldType(r);
2502
- }
2503
- }
2504
- }
2505
- }
2506
- }
2507
-
2508
- /// The returns half of `resolveFnSignature` — see `resolveFnParamTypes`'s
2509
- /// doc comment for why this must run AFTER `rewriteFnBody`, not before.
2510
- fn resolveFnReturnType(&mut self, f: &mut ast::Fn) {
2511
- if let Some(r) = &mut f.returns {
2512
- *r = self.resolveFieldType(r);
2513
- }
2514
- }
2515
-
2516
- /// A construction site (`Ok(5)`) only ever tells us about the TYPE that's being
2517
- /// PRODUCED — it says nothing about code further downstream that CONSUMES an
2518
- /// already-specialized generic-enum value returned from calling some other
2519
- /// (already fully concrete, non-generic) function or method, e.g.
2520
- /// `match parseIt() { Ok(v) => ... }` where `fun parseIt() -> Try[Int, Str]`.
2521
- /// `self.infer` can't help there either: it's built from `buildGlobalTables`,
2522
- /// which (like every other `PlumType` site) drops a declared type's generic
2523
- /// args entirely (`plumTypeFromAst` maps `Try[Int, Str]` to the bare
2524
- /// `TNamed("Try")`) — so it has no way to know this call's result is the
2525
- /// SPECIALIZED `Try$Int$Str`, not the generic template.
2526
- ///
2527
- /// This resolves that one specific, common shape directly from the callee's own
2528
- /// declaration (recorded in `fn_return_generic_enum` during classification) —
2529
- /// bypassing `self.infer` entirely, since the answer is already fully known
2530
- /// from the signature and doesn't depend on this call site's arguments at all.
2531
- /// Returns `None` for anything else (an ordinary call, a call to a function
2532
- /// whose return isn't a generic-enum instantiation, ...), meaning "fall back to
2533
- /// `self.infer` as before."
2534
- fn resolveCallReturnType(&mut self, e: &ast::Expr, env: &TypeEnv) -> Option<PlumType> {
2535
- let (recv, name) = match e {
2536
- ast::Expr::FnCall(call) => (None, call.name.clone()),
2537
- ast::Expr::Attribute(attr) => match &attr.attr {
2538
- ast::AttrKind::Method(call) => {
2539
- let recv = match crate::methodReceiverName(&self.infer(&attr.object, env)) {
2540
- Some(r) => r,
2541
- None => return None,
2542
- };
2543
- (Some(recv), call.name.clone())
2544
- }
2545
- ast::AttrKind::Field(_) => return None,
2546
- },
2547
- _ => return None,
2548
- };
2549
- let (enum_ref, type_args) = self.fn_return_generic_enum.get(&(recv, name))?.clone();
2550
- let params = enumGenericParams(enum_ref);
2551
- let bindings: BTreeMap<String, PlumType> = params.iter().cloned().zip(type_args).collect();
2552
- Some(PlumType::TNamed(
2553
- self.ensureEnumSpecialized(enum_ref, &params, bindings),
2554
- ))
2555
- }
2556
-
2557
- fn rewriteExpr(&mut self, expr: &mut ast::Expr, env: &TypeEnv) -> Result<(), String> {
2558
- match expr {
2559
- ast::Expr::ClassCall(call) => {
2560
- // A bare `Int`/`Float` field value being constructed into a
2561
- // "union" enum field (`Wrapper(value: 5)` where `value:
2562
- // Number`) or a named-payload enum variant field (`Circle
2563
- // (radius: 5)`, though there `radius: Int` already matches so
2564
- // this is a no-op) — a named variant's OWN field types don't
2565
- // depend on generic specialization, so this can run
2566
- // unconditionally here regardless of whether `call.type_name`
2567
- // turns out to be generic or not below.
2568
- for fa in &mut call.fields {
2569
- let expected_ty =
2570
- self.enum_variants
2571
- .get(call.type_name.as_str())
2572
- .and_then(|info| {
2573
- info.field_names
2574
- .iter()
2575
- .position(|n| n == &fa.name)
2576
- .map(|i| info.field_types[i].clone())
2577
- });
2578
- if let Some(expected) = expected_ty {
2579
- self.wrapPrimitiveAgainstExpected(&mut fa.value, &expected, env);
2580
- self.resolveGenericVariantAgainstExpected(&mut fa.value, &expected);
2581
- }
2582
- }
2583
- // Runs next (before the generic per-field rewrite below): a
2584
- // bare payload-free variant field value (`Node(..., next:
2585
- // End)`) carries no type of its own to infer a generic
2586
- // param from, and needs the enum's OWN (about-to-be-
2587
- // specialized) field type to resolve which specialization it
2588
- // actually means — `resolveClassInstantiation` handles that
2589
- // internally once it knows the full binding set.
2590
- self.resolveClassInstantiation(call, env)?;
2591
- for fa in &mut call.fields {
2592
- self.rewriteExpr(&mut fa.value, env)?;
2593
- }
2594
- // Bare-type variant sugar (`enum Book = | FantasyBook | ...`,
2595
- // see `buildGlobalTables`): a class used as its own enum
2596
- // variant is recorded there with a single self-wrap field type
2597
- // `TNamed(<same name>)`. Calling it with the class's own
2598
- // named-field constructor (`FantasyBook(title: ..., ...)`)
2599
- // should build `Book`, not a bare `FantasyBook` — desugar to
2600
- // exactly what writing `FantasyBook(FantasyBook(...))` by hand
2601
- // would produce, reusing the existing variant-construction
2602
- // (`FnCall`) codegen path unchanged.
2603
- let is_bare_variant_wrap = self
2604
- .enum_variants
2605
- .get(call.type_name.as_str())
2606
- .map(|info| info.field_types == [PlumType::TNamed(call.type_name.clone())])
2607
- .unwrap_or(false);
2608
- if is_bare_variant_wrap {
2609
- let inner = call.clone();
2610
- let name = inner.type_name.clone();
2611
- *expr = ast::Expr::FnCall(ast::FnCall {
2612
- name,
2613
- args: vec![ast::Arg::Positional(ast::Expr::ClassCall(inner))],
2614
- });
2615
- }
2616
- }
2617
- ast::Expr::FnCall(call) => {
2618
- // Coerce a bare `Int`/`Float` ARGUMENT into a "union" enum
2619
- // param (`fun useNumber(n: Number)`) — the one call site
2620
- // `wrapPrimitiveAgainstExpected` didn't already have (return
2621
- // position and field construction are handled elsewhere).
2622
- // Looked up from `env` by the call's CURRENT name, which may
2623
- // still be an unspecialized generic template at this point —
2624
- // harmless, since a param typed by one of the function's own
2625
- // generic params is a bare `TVar`/`TNamed(<param>)`, never a
2626
- // real enum, so `expected` only resolves to an actual
2627
- // `Number`-like enum for a param that's genuinely concretely
2628
- // typed that way (generic function or not). Positional zip
2629
- // against `param_types` mirrors the same simplification
2630
- // `resolveFnInstantiation` already makes elsewhere in this
2631
- // file — a keyword arg out of declared order would zip
2632
- // against the wrong param, but that's a pre-existing
2633
- // limitation, not one this feature introduces.
2634
- if let Some(PlumType::TFun(param_types, _)) =
2635
- env.get(call.name.as_str()).map(|s| *s.body.clone())
2636
- {
2637
- for (arg, expected) in call.args.iter_mut().zip(param_types.iter()) {
2638
- let e = match arg {
2639
- ast::Arg::Positional(e) => e,
2640
- ast::Arg::Keyword { value, .. } => value,
2641
- ast::Arg::Pair { value, .. } => value,
2642
- };
2643
- self.wrapPrimitiveAgainstExpected(e, expected, env);
2644
- self.resolveGenericVariantAgainstExpected(e, expected);
2645
- }
2646
- }
2647
- for arg in &mut call.args {
2648
- let e = match arg {
2649
- ast::Arg::Positional(e) => e,
2650
- ast::Arg::Keyword { value, .. } => value,
2651
- ast::Arg::Pair { value, .. } => value,
2652
- };
2653
- self.rewriteExpr(e, env)?;
2654
- }
2655
- // A `FnCall` may name a generic free function, a generic enum's
2656
- // variant, or (bare positional args on a class name, e.g. `List(1,
2657
- // 2, 3)` — sugar for `List.init(1, 2, 3)`, see `plum-checker`'s own
2658
- // `inferExpr`/`plum-wasm-codegen`'s matching `ClassCall`/`FnCall`
2659
- // handling) a generic class with an `init` method; the three name
2660
- // spaces don't overlap (variants/classes are capitalized, free
2661
- // functions aren't). Enum and class resolution both run first and
2662
- // rewrite `call.name` to its mangled form when they resolve —
2663
- // `fns_generic` is keyed by the ORIGINAL unmangled free-function
2664
- // names, so a rewritten variant/class name can never accidentally
2665
- // match it afterward.
2666
- self.resolveEnumInstantiation(call, env)?;
2667
- self.resolveClassInitInstantiation(call, env)?;
2668
- self.resolveFnInstantiation(call, env)?;
2669
- self.resolveBareGenericFnInstantiation(call, env)?;
2670
- }
2671
- ast::Expr::Attribute(attr) => {
2672
- self.rewriteExpr(&mut attr.object, env)?;
2673
- if let ast::AttrKind::Method(call) = &mut attr.attr {
2674
- // Closure-literal args need their OWN, properly-seeded pass
2675
- // (see `rewriteMethodClosureArgs`'s doc comment) BEFORE the
2676
- // generic per-arg walk below, which only knows `env` as-is
2677
- // and would otherwise seed a closure's params as
2678
- // unconstrained `TVar`s — permanently baking in a WRONG
2679
- // mangled name for any generic construction inside a
2680
- // closure body. The generic walk below skips closures
2681
- // entirely as a result; every other arg shape still goes
2682
- // through it exactly as before.
2683
- self.rewriteMethodClosureArgs(&attr.object, call, env)?;
2684
- // Same "union" enum argument coercion as the free-function
2685
- // `FnCall` arm above, keyed by (receiver type, method name)
2686
- // in `self.methods` instead of a bare function name. Also
2687
- // follows the SAME dispatch fallback `plum-wasm-codegen`'s
2688
- // `AttrKind::Method` codegen uses (a method not found
2689
- // under the receiver's own name might be defined on the
2690
- // enum that bare-wraps it, e.g. `Number.min(self, other:
2691
- // Number)` dispatched from an `Int` receiver) — without
2692
- // this, an OTHER argument like `{3}.min(5)`'s `5` would
2693
- // never get boxed into `Number`, even though codegen
2694
- // correctly boxes `self` for the very same call, causing
2695
- // a wasm validation failure (raw `i64` where the callee
2696
- // expects a boxed `Number` struct ref).
2697
- let receiver_ty = self.infer(&attr.object, env);
2698
- let direct_receiver_name = crate::methodReceiverName(&receiver_ty);
2699
- let dispatch_receiver_name = direct_receiver_name.clone().and_then(|name| {
2700
- if self
2701
- .methods
2702
- .contains_key(&(name.clone(), call.name.clone()))
2703
- {
2704
- Some(name)
2705
- } else {
2706
- self.enum_variants
2707
- .get(name.as_str())
2708
- .map(|info| info.enum_name.clone())
2709
- }
2710
- });
2711
- if let Some(receiver_name) = dispatch_receiver_name {
2712
- if let Some(PlumType::TFun(param_types, _)) = self
2713
- .methods
2714
- .get(&(receiver_name, call.name.clone()))
2715
- .cloned()
2716
- {
2717
- for (arg, expected) in call.args.iter_mut().zip(param_types.iter()) {
2718
- let e = match arg {
2719
- ast::Arg::Positional(e) => e,
2720
- ast::Arg::Keyword { value, .. } => value,
2721
- ast::Arg::Pair { value, .. } => value,
2722
- };
2723
- if !matches!(e, ast::Expr::Closure(_)) {
2724
- self.wrapPrimitiveAgainstExpected(e, expected, env);
2725
- self.resolveGenericVariantAgainstExpected(e, expected);
2726
- }
2727
- }
2728
- }
2729
- }
2730
- for arg in &mut call.args {
2731
- let e = match arg {
2732
- ast::Arg::Positional(e) => e,
2733
- ast::Arg::Keyword { value, .. } => value,
2734
- ast::Arg::Pair { value, .. } => value,
2735
- };
2736
- if matches!(e, ast::Expr::Closure(_)) {
2737
- continue;
2738
- }
2739
- self.rewriteExpr(e, env)?;
2740
- }
2741
- // Method dispatch on a specialized receiver needs no rewrite here
2742
- // in the ORDINARY case: once the receiver's construction site is
2743
- // rewritten to its mangled class name, the receiver's inferred
2744
- // static type IS that mangled name, and the specialized methods
2745
- // were registered under exactly that (mangled receiver, method
2746
- // name) key when their class was specialized (see the
2747
- // `PendingSpecialization::Class` arm below). A method with generic
2748
- // param(s) of its OWN (`List.map`'s `U`) is the exception — that
2749
- // one DOES need a rewrite, since no such registration happened for
2750
- // it above (see `resolveMethodOwnGenerics`'s own doc comment).
2751
- self.resolveMethodOwnGenerics(&attr.object, call, env)?;
2752
- }
2753
- }
2754
- ast::Expr::Binary(b) => {
2755
- self.rewriteExpr(&mut b.left, env)?;
2756
- self.rewriteExpr(&mut b.right, env)?;
2757
- }
2758
- ast::Expr::Bool(b) => {
2759
- self.rewriteExpr(&mut b.left, env)?;
2760
- self.rewriteExpr(&mut b.right, env)?;
2761
- }
2762
- ast::Expr::Compare(c) => {
2763
- self.rewriteExpr(&mut c.left, env)?;
2764
- self.rewriteExpr(&mut c.right, env)?;
2765
- // `current != End`: `current`'s side may already be a SPECIFIC
2766
- // enum specialization (e.g. `Link$Node$Int`) while `End`
2767
- // itself is still bare (it carries no type of its own) —
2768
- // resolve each side against the OTHER's type; a no-op unless
2769
- // that side is actually a bare generic-enum variant.
2770
- let lt = self.infer(&c.left, env);
2771
- let rt = self.infer(&c.right, env);
2772
- self.resolveBareVariantAgainstExpected(&mut c.left, &rt);
2773
- self.resolveBareVariantAgainstExpected(&mut c.right, &lt);
2774
- }
2775
- ast::Expr::Not(inner) => self.rewriteExpr(inner, env)?,
2776
- ast::Expr::Try(inner) => self.rewriteExpr(inner, env)?,
2777
- ast::Expr::Unary(u) => self.rewriteExpr(&mut u.operand, env)?,
2778
- ast::Expr::Paren(inner) => self.rewriteExpr(inner, env)?,
2779
- ast::Expr::Ternary(t) => {
2780
- self.rewriteExpr(&mut t.condition, env)?;
2781
- self.rewriteExpr(&mut t.then, env)?;
2782
- self.rewriteExpr(&mut t.else_, env)?;
2783
- }
2784
- ast::Expr::Elvis(e) => {
2785
- self.rewriteExpr(&mut e.left, env)?;
2786
- self.rewriteExpr(&mut e.right, env)?;
2787
- }
2788
- // String interpolation can embed arbitrary expressions (including generic
2789
- // call sites), so recurse into its interpolated parts.
2790
- ast::Expr::String(s) => {
2791
- for part in &mut s.parts {
2792
- if let ast::StringPart::Interp(e) = part {
2793
- self.rewriteExpr(e, env)?;
2794
- }
2795
- }
2796
- }
2797
- ast::Expr::Int(_)
2798
- | ast::Expr::Float(_)
2799
- | ast::Expr::Self_
2800
- | ast::Expr::Var(_)
2801
- | ast::Expr::TypeName(_) => {}
2802
- ast::Expr::Closure(cl) => {
2803
- // A closure literal's body can itself contain a generic
2804
- // construction site (`|v| Next(v)`) that needs rewriting to
2805
- // its mangled variant/class name, same as anywhere else —
2806
- // without this, codegen later traps with "variant 'Next'
2807
- // missing from the GC type registry" the first time such a
2808
- // closure is actually compiled. The closure's OWN params
2809
- // aren't bound to any concrete type YET at this point in the
2810
- // pass (that only happens per-CALL-SITE, in
2811
- // `resolveMethodOwnGenerics`'s `ClosureReturn` handling) —
2812
- // seed them as unresolved `TVar`s in a scoped copy of `env`
2813
- // so a construction that depends on a captured OUTER
2814
- // variable (not a closure param) still resolves correctly;
2815
- // one that depends on a closure param itself is a known,
2816
- // narrower residual gap (see `fnBareGenericRefs`'s sibling
2817
- // `ParamType::Fn` TODOs).
2818
- let mut closure_env = env.clone();
2819
- for p in &cl.params {
2820
- closure_env
2821
- .entry(p.clone())
2822
- .or_insert_with(|| TypeScheme::mono(PlumType::TVar("_".to_string())));
2823
- }
2824
- self.rewriteBlock(&mut cl.body, &mut closure_env)?;
2825
- }
2826
- }
2827
- Ok(())
2828
- }
2829
- }
2830
-
2831
- /// Runs the whole generics-monomorphization pass over `source`, producing a plain,
2832
- /// fully-concrete `ast::Source` with every generic `Class`/`Fn`/`Enum` template
2833
- /// replaced by zero or more mangled concrete specializations, and every remaining
2834
- /// item's body rewritten so its call sites reference those mangled names. The
2835
- /// result has no generic syntax left in it — `checkSource`/`compileSource` run
2836
- /// on it completely unmodified.
2837
- pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2838
- let (global_env, methods, enum_variants, enum_params) = crate::buildGlobalTables(source);
2839
- let min_required = crate::buildMinRequiredArgs(source);
2840
-
2841
- let mut m = Monomorphizer {
2842
- fns_generic: BTreeMap::new(),
2843
- methods_generic_on_enum: BTreeMap::new(),
2844
- enums_generic_by_variant: BTreeMap::new(),
2845
- enums_generic_by_name: BTreeMap::new(),
2846
- enum_variant_mangling: BTreeMap::new(),
2847
- fns_bare_generic: BTreeMap::new(),
2848
- global_env,
2849
- methods,
2850
- enum_variants,
2851
- enum_params,
2852
- min_required,
2853
- class_specialization_info: BTreeMap::new(),
2854
- specialized: BTreeSet::new(),
2855
- enqueued: BTreeSet::new(),
2856
- worklist: Vec::new(),
2857
- produced: Vec::new(),
2858
- current_return_type: None,
2859
- return_type_is_closure_expected: false,
2860
- closure_outer_return_type: None,
2861
- fn_return_generic_enum: BTreeMap::new(),
2862
- local_types_by_name: BTreeMap::new(),
2863
- rename_counter: 0,
2864
- };
2865
-
2866
- for item in &source.items {
2867
- if let ast::Item::Enum(e) = item {
2868
- if !enumGenericParams(e).is_empty() {
2869
- m.enums_generic_by_name.insert(e.name.clone(), e);
2870
- for v in &e.variants {
2871
- m.enums_generic_by_variant.insert(v.name.clone(), e);
2872
- }
2873
- }
2874
- }
2875
- }
2876
- // Classifies every method by its receiver (generic enum, or neither),
2877
- // populating `methods_generic_on_enum` — MUST run before the
2878
- // `fn_return_generic_enum` loop below, which calls `resolveFieldType` and
2879
- // can therefore trigger `registerEnumMethodSignatures` for a generic enum
2880
- // as a SIDE EFFECT (e.g. resolving a nested `List[Str]` inside some other
2881
- // function's `-> Try[List[Str], Str]` return type).
2882
- // `registerEnumMethodSignatures` looks up `methods_generic_on_enum` and,
2883
- // finding it not yet populated, silently registers ZERO methods for that
2884
- // specialization (`Some(methods) = ... else { return }`) — and because the
2885
- // specialization is marked done at that point (`self.enum_variant_mangling`
2886
- // already has the mangled name), no later call ever retries it, permanently
2887
- // leaving e.g. `List$Str.reject` unresolvable for the rest of this pass
2888
- // (surfacing much later, and confusingly, as a chained method call
2889
- // inferring to `TVar` deep inside some unrelated enum-construction site).
2890
- for item in &source.items {
2891
- if let ast::Item::Fn(f) = item {
2892
- let receiver_is_generic_enum = f
2893
- .type_param
2894
- .as_deref()
2895
- .map(|r| m.enums_generic_by_name.contains_key(r))
2896
- .unwrap_or(false);
2897
- if receiver_is_generic_enum {
2898
- m.methods_generic_on_enum
2899
- .entry(f.type_param.clone().unwrap())
2900
- .or_default()
2901
- .push(f);
2902
- } else if f.type_param.is_none() && !fnGenericParams(f).is_empty() {
2903
- m.fns_generic.insert(f.name.clone(), f);
2904
- } else if f.type_param.is_none() && !m.fnBareGenericRefs(f).is_empty() {
2905
- m.fns_bare_generic.insert(f.name.clone(), f);
2906
- }
2907
- // A method whose receiver is NOT generic is left as a regular method below,
2908
- // even if its own params/return happen to use a bare lowercase-letter type
2909
- // name, or bare-name a generic class/enum — those shapes are out of scope
2910
- // for this pass; see the plan's Global Constraints.
2911
- }
2912
- }
2913
-
2914
- // Any function/method (generic or not — this doesn't care either way) whose
2915
- // OWN declared return type fully instantiates a known generic enum. Must run
2916
- // after the loop above (needs `enums_generic_by_name` AND `methods_generic_on`
2917
- // filled — see that loop's doc comment) but is otherwise independent of every
2918
- // other classification pass here.
2919
- for item in &source.items {
2920
- if let ast::Item::Fn(f) = item {
2921
- if let Some(rt) = &f.returns {
2922
- if let Some(e) = m.enums_generic_by_name.get(rt.name.as_str()).copied() {
2923
- let params = enumGenericParams(e);
2924
- if !rt.generics.is_empty() && rt.generics.len() == params.len() {
2925
- // `gt` needs `resolveFieldType`'s recursive resolution
2926
- // before `plumTypeFromAst`, same as every other site
2927
- // that turns a declared `ast::Type` generic argument
2928
- // into a `PlumType` — otherwise a nested generic type
2929
- // argument (e.g. this method's own `Link[List[Json]]`
2930
- // return type) has its `List[Json]` silently flattened
2931
- // to bare `List`, and every match on this method's
2932
- // return value ends up destructuring against the
2933
- // unspecialized (method-less) `List` template instead
2934
- // of `List$Json`.
2935
- let type_args: Vec<PlumType> = rt
2936
- .generics
2937
- .iter()
2938
- .map(|gt| {
2939
- let resolved = m.resolveFieldType(gt);
2940
- crate::plumTypeFromAst(&resolved)
2941
- })
2942
- .collect();
2943
- m.fn_return_generic_enum
2944
- .insert((f.type_param.clone(), f.name.clone()), (e, type_args));
2945
- }
2946
- }
2947
- }
2948
- }
2949
- }
2950
-
2951
- // Pre-resolve every ORDINARY (non-generic, non-generic-receiver) fn/method's
2952
- // OWN signature into `self.methods`/`self.global_env` before the main
2953
- // per-item loop below does any body rewriting. Without this, a call to
2954
- // e.g. `Str.split` (an ordinary method whose return type is the concrete
2955
- // generic instantiation `List[Str]`) made from another item processed
2956
- // EARLIER in `source.items`' order — imports are merged in AFTER the
2957
- // entry file's own items (see `loadAndMerge`), so a stdlib method
2958
- // commonly sits LATER in this list than the entry-file code that calls
2959
- // it — would have its return type inferred through the stale, un-mangled
2960
- // tables `buildGlobalTables` built at the very top of this function
2961
- // (which drops every declared type's generic args entirely). That
2962
- // silently bound a generic enum/class construction's type parameter to
2963
- // the wrong, bare template name instead of the real mangled one (e.g.
2964
- // `Ok(joined.split(...))`'s `T` resolving to bare `List` instead of
2965
- // `List$Str`, later rejected by the checker as `expected List, found
2966
- // List$Str` even though the program was correct). `resolveFnSignature`'s
2967
- // own side effects (enqueueing a class/enum specialization) are
2968
- // idempotent — the main loop below re-resolving the same signature again
2969
- // is harmless.
2970
- for item in &source.items {
2971
- if let ast::Item::Fn(f) = item {
2972
- let receiver_is_generic = f
2973
- .type_param
2974
- .as_deref()
2975
- .map(|r| m.enums_generic_by_name.contains_key(r))
2976
- .unwrap_or(false);
2977
- let is_generic_fn = f.type_param.is_none() && !fnGenericParams(f).is_empty();
2978
- let is_bare_generic_fn =
2979
- f.type_param.is_none() && m.fns_bare_generic.contains_key(f.name.as_str());
2980
- if receiver_is_generic || is_generic_fn || is_bare_generic_fn {
2981
- continue;
2982
- }
2983
- let mut sig = f.clone();
2984
- m.resolveFnSignature(&mut sig);
2985
- let param_types: Vec<PlumType> = sig
2986
- .params
2987
- .iter()
2988
- .map(|p| match &p.ty {
2989
- ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
2990
- ast::ParamType::Variadic(t) => {
2991
- PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t)))
2992
- }
2993
- ast::ParamType::Fn(params, ret) => {
2994
- let param_types = params.iter().map(crate::plumTypeFromAst).collect();
2995
- let ret_ty = ret
2996
- .as_ref()
2997
- .map(|r| crate::plumTypeFromAst(r))
2998
- .unwrap_or(PlumType::TUnit);
2999
- PlumType::TFun(param_types, Box::new(ret_ty))
3000
- }
3001
- })
3002
- .collect();
3003
- let ret = sig
3004
- .returns
3005
- .as_ref()
3006
- .map(crate::plumTypeFromAst)
3007
- .unwrap_or(PlumType::TUnit);
3008
- match &sig.type_param {
3009
- Some(recv) => {
3010
- m.methods.insert(
3011
- (recv.clone(), sig.name.clone()),
3012
- PlumType::TFun(param_types, Box::new(ret)),
3013
- );
3014
- }
3015
- None => {
3016
- m.global_env.insert(
3017
- sig.name.clone(),
3018
- TypeScheme::mono(PlumType::TFun(param_types, Box::new(ret))),
3019
- );
3020
- }
3021
- }
3022
- }
3023
- }
3024
-
3025
- // Same reasoning for a non-generic enum's variant field types: a
3026
- // construction (`Holder(t: Ok(5))`) in an item EARLIER than the enum's own
3027
- // declaration must already see `Try$Int$[X]`, not the stale bare `Try`,
3028
- // to resolve its field values against (the main loop below refreshes
3029
- // them again, idempotently, when it reaches the enum).
3030
- for item in &source.items {
3031
- if let ast::Item::Enum(e) = item {
3032
- if !enumGenericParams(e).is_empty() {
3033
- continue;
3034
- }
3035
- for v in &e.variants {
3036
- if v.fields.is_empty() || !m.enum_variants.contains_key(v.name.as_str()) {
3037
- continue;
3038
- }
3039
- let field_types: Vec<PlumType> = v
3040
- .fields
3041
- .iter()
3042
- .map(|f| crate::plumTypeFromAst(&m.resolveFieldType(f)))
3043
- .collect();
3044
- if let Some(info) = m.enum_variants.get_mut(v.name.as_str()) {
3045
- info.field_types = field_types;
3046
- }
3047
- }
3048
- }
3049
- }
3050
-
3051
- for item in &source.items {
3052
- match item {
3053
- ast::Item::Enum(e) if enumGenericParams(e).is_empty() => {
3054
- let mut e2 = e.clone();
3055
- for v in &mut e2.variants {
3056
- for f in &mut v.fields {
3057
- *f = m.resolveFieldType(f);
3058
- }
3059
- }
3060
- // Refresh `self.enum_variants` with the resolved field types —
3061
- // `self.enum_variants` was seeded from the ORIGINAL,
3062
- // pre-monomorphization source (see `buildGlobalTables`, which —
3063
- // like `plumTypeFromAst` generally — doesn't resolve a field's
3064
- // own nested generics), so a variant field like `List[Json]` is
3065
- // still recorded there as bare `List`, not `List$Json`.
3066
- // Otherwise any LATER inference of a construction site for this
3067
- // variant (e.g. `self.infer` during another item's own
3068
- // monomorphization) unifies that stale name against the real,
3069
- // specialized value's type and fails, and that failure then
3070
- // gets silently swallowed by `infer`'s `unwrap_or(TVar("_"))`
3071
- // fallback further up the call chain.
3072
- for v in &e2.variants {
3073
- let existing = m.enum_variants.get(v.name.as_str());
3074
- let tag = existing.map(|info| info.tag).unwrap_or(0);
3075
- // Bare-type variant sugar (see `buildGlobalTables`): `v.fields`
3076
- // is empty in the AST for `| Cat` or `| Int`, but its resolved
3077
- // field type was already recorded there (by the initial
3078
- // `buildGlobalTables` seeding, before this loop runs) as a
3079
- // single self-wrap `TNamed(v.name)` (a record-shaped enum) or
3080
- // primitive bare-wrap (`Int`/`Float`) — reuse that same shape
3081
- // here instead of clobbering it back to an empty vec.
3082
- let existing_field_types = existing
3083
- .map(|info| info.field_types.clone())
3084
- .unwrap_or_default();
3085
- let is_bare_wrap = v.fields.is_empty()
3086
- && (existing_field_types == [PlumType::TNamed(v.name.clone())]
3087
- || existing_field_types == [crate::plumTypeFromName(&v.name)]);
3088
- let (field_types, field_names) = if is_bare_wrap {
3089
- (existing_field_types, Vec::new())
3090
- } else {
3091
- (
3092
- v.fields.iter().map(crate::plumTypeFromAst).collect(),
3093
- v.field_names.clone(),
3094
- )
3095
- };
3096
- m.enum_variants.insert(
3097
- v.name.clone(),
3098
- EnumVariantInfo {
3099
- enum_name: e2.name.clone(),
3100
- tag,
3101
- field_types,
3102
- field_names,
3103
- values: v.values.clone(),
3104
- generics: Vec::new(),
3105
- },
3106
- );
3107
- }
3108
- m.produced.push(ast::Item::Enum(e2));
3109
- }
3110
- ast::Item::Const(c) => m.produced.push(ast::Item::Const(c.clone())),
3111
- ast::Item::Trait(t) => m.produced.push(ast::Item::Trait(t.clone())),
3112
- ast::Item::Test(t) => {
3113
- let mut t2 = t.clone();
3114
- let mut env = m.global_env.clone();
3115
- m.current_return_type = None;
3116
- m.local_types_by_name.clear();
3117
- m.rewriteBlock(&mut t2.body, &mut env)?;
3118
- m.produced.push(ast::Item::Test(t2));
3119
- }
3120
- ast::Item::Fn(f) => {
3121
- let receiver_is_generic = f
3122
- .type_param
3123
- .as_deref()
3124
- .map(|r| m.enums_generic_by_name.contains_key(r))
3125
- .unwrap_or(false);
3126
- let is_generic_fn = f.type_param.is_none() && !fnGenericParams(f).is_empty();
3127
- let is_bare_generic_fn =
3128
- f.type_param.is_none() && m.fns_bare_generic.contains_key(f.name.as_str());
3129
- if !receiver_is_generic && !is_generic_fn && !is_bare_generic_fn {
3130
- let mut f2 = f.clone();
3131
- m.resolveFnParamTypes(&mut f2);
3132
- m.rewriteFnBody(&mut f2, false)?;
3133
- m.resolveFnReturnType(&mut f2);
3134
- m.produced.push(ast::Item::Fn(f2));
3135
- }
3136
- }
3137
- _ => {} // generic Enum declarations dropped here — templates only
3138
- }
3139
- }
3140
-
3141
- let mut guard = 0usize;
3142
- while let Some(pending) = m.worklist.pop() {
3143
- guard += 1;
3144
- if guard > 10_000 {
3145
- return Err("monomorphize: exceeded specialization limit (possible unbounded generic recursion)".to_string());
3146
- }
3147
- match pending {
3148
- PendingSpecialization::Fn {
3149
- base,
3150
- subst,
3151
- mangled,
3152
- new_receiver,
3153
- } => {
3154
- if !m.specialized.insert(mangled.clone()) {
3155
- continue;
3156
- }
3157
- let mut specialized_fn = specializeFn(base, &subst, &mangled, new_receiver);
3158
- m.resolveFnParamTypes(&mut specialized_fn);
3159
- m.rewriteFnBody(&mut specialized_fn, true)?;
3160
- m.resolveFnReturnType(&mut specialized_fn);
3161
- // Register the specialized free function's signature so later bodies
3162
- // can resolve calls to it during inference.
3163
- let param_types: Vec<PlumType> = specialized_fn
3164
- .params
3165
- .iter()
3166
- .map(|p| match &p.ty {
3167
- ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
3168
- ast::ParamType::Variadic(t) => {
3169
- PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t)))
3170
- }
3171
- ast::ParamType::Fn(params, ret) => {
3172
- let param_types = params.iter().map(crate::plumTypeFromAst).collect();
3173
- let ret_ty = ret
3174
- .as_ref()
3175
- .map(|r| crate::plumTypeFromAst(r))
3176
- .unwrap_or(PlumType::TUnit);
3177
- PlumType::TFun(param_types, Box::new(ret_ty))
3178
- }
3179
- })
3180
- .collect();
3181
- let ret = specialized_fn
3182
- .returns
3183
- .as_ref()
3184
- .map(crate::plumTypeFromAst)
3185
- .unwrap_or(PlumType::TUnit);
3186
- m.global_env.insert(
3187
- specialized_fn.name.clone(),
3188
- TypeScheme::mono(PlumType::TFun(param_types, Box::new(ret))),
3189
- );
3190
- m.produced.push(ast::Item::Fn(specialized_fn));
3191
- }
3192
- PendingSpecialization::Enum {
3193
- base,
3194
- subst,
3195
- mangled,
3196
- } => {
3197
- if !m.specialized.insert(mangled.clone()) {
3198
- continue;
3199
- }
3200
- let mut spec_enum = specializeEnum(base, &subst, &mangled);
3201
- // `specializeEnum` only substitutes a bare generic-param NAME —
3202
- // a field like `List[T]` becomes `List[Int]`, still a generic
3203
- // instantiation, not yet a real (mangled) type. Resolve those
3204
- // the rest of the way now.
3205
- for v in &mut spec_enum.variants {
3206
- for f in &mut v.fields {
3207
- *f = m.resolveFieldType(f);
3208
- }
3209
- }
3210
- m.produced.push(ast::Item::Enum(spec_enum));
3211
- // Guarded against the self-nesting landmine documented on
3212
- // `isSelfNested` — without this, `List.chunk() -> List[List[T]]`
3213
- // would recurse into producing an unbounded chain of
3214
- // ever-more-nested `List` specializations via THIS SAME loop —
3215
- // and since this loop runs once per separate WORKLIST pop
3216
- // rather than recursively, a guard scoped to a single call
3217
- // stack isn't enough on its own.
3218
- {
3219
- let already_nested = m.isSelfNested(&base.name, &subst.0);
3220
- if let Some(methods) =
3221
- m.methods_generic_on_enum.get(base.name.as_str()).cloned()
3222
- {
3223
- let owner_params = enumGenericParams(base);
3224
- for method in methods {
3225
- // A method with generic param(s) of its OWN (e.g.
3226
- // `map`'s `U`, beyond `List[T]`'s own `T`) can't be
3227
- // produced HERE — `U` is only known at a particular
3228
- // CALL SITE, not at enum-specialization time.
3229
- // `resolveMethodOwnGenerics` queues a
3230
- // `PendingSpecialization::Fn` for each concrete `U`
3231
- // it actually sees used, instead.
3232
- if !methodOwnGenericParams(method, &owner_params).is_empty() {
3233
- continue;
3234
- }
3235
- if already_nested
3236
- && Monomorphizer::methodMentionsTemplate(method, &base.name)
3237
- {
3238
- continue;
3239
- }
3240
- let mut specialized_method =
3241
- specializeFn(method, &subst, &method.name, Some(mangled.clone()));
3242
- m.resolveFnParamTypes(&mut specialized_method);
3243
- m.rewriteFnBody(&mut specialized_method, true)?;
3244
- m.resolveFnReturnType(&mut specialized_method);
3245
- let param_types: Vec<PlumType> = specialized_method
3246
- .params
3247
- .iter()
3248
- .map(|p| match &p.ty {
3249
- ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
3250
- ast::ParamType::Variadic(t) => {
3251
- PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t)))
3252
- }
3253
- ast::ParamType::Fn(params, ret) => {
3254
- let param_types =
3255
- params.iter().map(crate::plumTypeFromAst).collect();
3256
- let ret_ty = ret
3257
- .as_ref()
3258
- .map(|r| crate::plumTypeFromAst(r))
3259
- .unwrap_or(PlumType::TUnit);
3260
- PlumType::TFun(param_types, Box::new(ret_ty))
3261
- }
3262
- })
3263
- .collect();
3264
- let ret = specialized_method
3265
- .returns
3266
- .as_ref()
3267
- .map(crate::plumTypeFromAst)
3268
- .unwrap_or(PlumType::TUnit);
3269
- m.methods.insert(
3270
- (mangled.clone(), specialized_method.name.clone()),
3271
- PlumType::TFun(param_types, Box::new(ret)),
3272
- );
3273
- m.produced.push(ast::Item::Fn(specialized_method));
3274
- }
3275
- }
3276
- }
3277
- }
3278
- }
3279
- }
3280
-
3281
- Ok(ast::Source {
3282
- module: source.module.clone(),
3283
- imports: source.imports.clone(),
3284
- items: m.produced,
3285
- topLevelOrder: Vec::new(),
3286
- })
3287
- }
plum-checker/tests/checker_tests.rs CHANGED
@@ -1,7 +1,7 @@
1
1
  #![allow(non_snake_case)]
2
2
  use plum_checker::types::*;
3
3
  use plum_checker::{
4
- checkSource, erasedEnumName, plumTypeFromAst, plumTypeFromName, tagUnionSegmentsCompatible,
4
+ checkSource, plumTypeFromAst, plumTypeFromName,
5
5
  typedSource, unify, CheckCtx,
6
6
  };
7
7
  use plum_core::ast::Type as AstType;
@@ -496,10 +496,10 @@ fn genericClassInstantiatedAtTwoConcreteTypesTypeChecks() {
496
496
  enum Box[T] =
497
497
  | Box(value: T)
498
498
 
499
- fun makeIntBox() -> Box =
499
+ fun makeIntBox() -> Box[Int] =
500
500
  Box(value: 5)
501
501
 
502
- fun makeStrBox() -> Box =
502
+ fun makeStrBox() -> Box[Str] =
503
503
  Box(value: \"x\")
504
504
  ";
505
505
  let source = parse(src);
@@ -589,38 +589,6 @@ fun get() -> Int =
589
589
  let result = checkSource(&source);
590
590
  assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
591
591
 
592
- // Directly prove resolution happened: the monomorphized output must contain a
593
- // concrete `Maybe$Int` enum whose `Just$Int` variant carries an `Int` field
594
- // (not the generic `a`), and must NOT retain the generic `Maybe` template.
595
- // The variant name is ALSO mangled (`Just` -> `Just$Int`), the same suffix as
596
- // the enum's own name.
597
- let mono = plum_checker::monomorphize::monomorphizeSource(&source)
598
- .expect("monomorphize should succeed");
599
- let opt = mono.items.iter().find_map(|it| match it {
600
- Item::Enum(e) if e.name == "Maybe$Int" => Some(e),
601
- _ => None,
602
- });
603
- let opt = opt.expect("expected a specialized `Maybe$Int` enum in the output");
604
- let some = opt
605
- .variants
606
- .iter()
607
- .find(|v| v.name == "Just$Int")
608
- .expect("expected `Just$Int` (mangled) variant on `Maybe$Int`");
609
- assert_eq!(
610
- some.fields,
611
- vec![Type {
612
- name: "Int".to_string(),
613
- generics: vec![]
614
- }],
615
- "Just's field should be concrete Int"
616
- );
617
- assert!(
618
- !mono
619
- .items
620
- .iter()
621
- .any(|it| matches!(it, Item::Enum(e) if e.name == "Maybe")),
622
- "the generic `Maybe` template must be dropped from the output"
623
- );
624
592
  }
625
593
 
626
594
  #[test]
@@ -653,24 +621,6 @@ fun useStr() -> Str =
653
621
  let result = checkSource(&source);
654
622
  assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
655
623
 
656
- // Directly prove both specializations exist independently, with distinct
657
- // mangled variant names, so neither collides with the other.
658
- let mono = plum_checker::monomorphize::monomorphizeSource(&source)
659
- .expect("monomorphize should succeed");
660
- let has_enum_with_variant = |enum_name: &str, variant_name: &str| {
661
- mono.items.iter().any(|it| {
662
- matches!(it, Item::Enum(e) if e.name == enum_name
663
- && e.variants.iter().any(|v| v.name == variant_name))
664
- })
665
- };
666
- assert!(
667
- has_enum_with_variant("Maybe$Int", "Just$Int"),
668
- "expected Maybe$Int with Just$Int"
669
- );
670
- assert!(
671
- has_enum_with_variant("Maybe$Str", "Just$Str"),
672
- "expected Maybe$Str with Just$Str"
673
- );
674
624
  }
675
625
 
676
626
  #[test]
@@ -691,34 +641,6 @@ fun use() -> Int =
691
641
  assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
692
642
  }
693
643
 
694
- #[test]
695
- #[ignore = "slow (~100s) — exercises the 10,000-specialization runaway-recursion guard; run explicitly with --ignored when touching monomorphize.rs's guard logic"]
696
- fn unboundedRecursiveGenericInstantiationIsAClearError() {
697
- // `recurse` re-wraps its argument in a `Box` on every recursive call, so each
698
- // specialization's own body demands a specialization of `recurse` at a STRICTLY
699
- // bigger type (`recurse$Int`, then `recurse$Box$Int`, then `recurse$Box$Box$Int`,
700
- // ...), forever. This genuinely grows the worklist without bound (unlike a
701
- // generic class field merely NAMING a recursive generic type in its own
702
- // declaration, which is never itself a call site and so never reaches the
703
- // worklist at all) and must fail with a clear, bounded error rather than hang.
704
- let src = "\
705
- enum Box[T] =
706
- | Box(value: T)
707
-
708
- fun recurse(v: T) -> Int =
709
- b = Box(value: v)
710
- recurse(b)
711
-
712
- fun use() -> Int =
713
- recurse(5)
714
- ";
715
- let source = parse(src);
716
- let result = checkSource(&source);
717
- assert!(result.is_err());
718
- let errs = result.unwrap_err();
719
- assert!(errs[0].message.contains("monomorphize"), "got: {:?}", errs);
720
- }
721
-
722
644
  #[test]
723
645
  fn ordinaryFunctionWithBareGenericEnumParamTypeChecks() {
724
646
  // The shape that broke the pre-existing codegen test: an otherwise-ordinary
@@ -728,7 +650,7 @@ enum Maybe[T] =
728
650
  | Just(T)
729
651
  | Nothing
730
652
 
731
- fun unwrapOr(o: Maybe, default: Int) -> Int =
653
+ fun unwrapOr(o: Maybe[Int], default: Int) -> Int =
732
654
  match o
733
655
  Just(v) =>
734
656
  v
@@ -742,24 +664,13 @@ fun use() -> Int =
742
664
  let result = checkSource(&source);
743
665
  assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
744
666
 
745
- // Directly prove `unwrapOr` itself got specialized (not left bare/unresolved).
746
- let mono = plum_checker::monomorphize::monomorphizeSource(&source)
747
- .expect("monomorphize should succeed");
748
- let has_specialized_unwrap_or = mono.items.iter().any(|it| {
749
- matches!(it, Item::Fn(f)
750
- if f.name.starts_with("unwrapOr$") && f.type_param.is_none())
751
- });
752
- assert!(
753
- has_specialized_unwrap_or,
754
- "expected a specialized `unwrapOr$...` function in the output"
755
- );
756
667
  }
757
668
 
758
669
  #[test]
759
670
  fn ordinaryFunctionWithBareGenericClassParamTypeChecks() {
760
671
  // The same shape, for a generic CLASS param instead of an enum — untested until
761
- // now, but the identical root cause: `Box` is dropped from the monomorphized
672
+ // now, but the identical root cause: a generic class param must carry its type
762
- // output, so a bare `Box`-typed param would otherwise reference nothing.
673
+ // arguments.
763
674
  let src = "\
764
675
  enum Box[T] =
765
676
  | Box(value: T)
@@ -767,7 +678,7 @@ enum Box[T] =
767
678
  fun getBoxValue() -> T =
768
679
  self.value
769
680
 
770
- fun sumBox(b: Box) -> Int =
681
+ fun sumBox(b: Box[Int]) -> Int =
771
682
  b.getBoxValue()
772
683
 
773
684
  fun use() -> Int =
@@ -1376,28 +1287,19 @@ fn tagsSubsetUnify() {
1376
1287
  #[test]
1377
1288
  fn nested_union_in_payload() {
1378
1289
  let ctx = emptyCtx();
1290
+ use PlumType::{TApp, TInt, TStr};
1291
+ let try_ = |tags: &str| TApp("Try".into(), vec![TInt, plumTypeFromName(tags)]);
1379
1292
  // Brackets inside a payload must not split the outer segment list.
1293
+ assert!(unify(&try_("[A,B]"), &try_("[A]"), &ctx).is_ok());
1294
+ assert!(unify(&try_("[A]"), &try_("[A,B]"), &ctx).is_err());
1295
+ assert!(unify(&try_("[A(Try)]"), &try_("[A(Try)]"), &ctx).is_ok());
1380
- assert!(tagUnionSegmentsCompatible(
1296
+ // Non-tag args must match exactly.
1381
- "Try$Int$[A(Try$Int$[X,Y])]",
1297
+ assert!(unify(
1382
- "Try$Int$[A(Try$Int$[X])]",
1298
+ &try_("[A]"),
1299
+ &TApp("Try".into(), vec![TStr, plumTypeFromName("[A]")]),
1383
1300
  &ctx
1384
- ));
1385
- assert!(!tagUnionSegmentsCompatible(
1386
- "Try$Int$[A(Try$Int$[X])]",
1387
- "Try$Int$[A(Try$Int$[Z])]",
1388
- &ctx
1389
- ));
1390
- assert!(tagUnionSegmentsCompatible("Try$Int$[A,B]", "Try$Int$[A]", &ctx));
1391
- assert!(!tagUnionSegmentsCompatible("Try$Int$[A]", "Try$Int$[A,B]", &ctx));
1392
- // Non-tag segments must match exactly; arity must match.
1393
- assert!(!tagUnionSegmentsCompatible("Try$Int$[A]", "Try$Str$[A]", &ctx));
1394
- assert!(!tagUnionSegmentsCompatible("Try$Int$[A]", "Try$Int", &ctx));
1395
- assert_eq!(
1396
- erasedEnumName("Try$Int$[A,B(Try$Int$[X])]"),
1397
- "Try$Int$Tags"
1398
- );
1301
+ )
1399
- assert_eq!(erasedEnumName("Try$[A]$[B,C]"), "Try$Tags$Tags");
1400
- assert_eq!(erasedEnumName("List$[]Byte"), "List$[]Byte");
1302
+ .is_err());
1401
1303
  }
1402
1304
 
1403
1305
  #[test]
@@ -1716,8 +1618,8 @@ fun main() =
1716
1618
  r := five().andThen(|n| Ok(n * 2))
1717
1619
  ";
1718
1620
  let source = parseClean(src);
1719
- let mono = plum_checker::monomorphize::monomorphizeSource(&source);
1621
+ let result = checkSource(&source);
1720
- assert!(mono.is_ok(), "expected Ok, got {:?}", mono.err());
1622
+ assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1721
1623
  }
1722
1624
 
1723
1625
  #[test]
@@ -2302,3 +2204,38 @@ fun go() -> Str =
2302
2204
  let r = checkSource(&parseClean(src));
2303
2205
  assert!(r.is_ok(), "got {:?}", r.err());
2304
2206
  }
2207
+
2208
+ #[test]
2209
+ fn bare_generic_type_in_signature_is_rejected() {
2210
+ let src = "\
2211
+ enum Parcel[T] =
2212
+ | Delivered(T)
2213
+ | Lost
2214
+
2215
+ fun contentsOr(p: Parcel, default: Int) -> Int =
2216
+ match p
2217
+ Delivered(v) => v
2218
+ Lost => default
2219
+ ";
2220
+ let errs = checkSource(&parse(src)).unwrap_err();
2221
+ assert!(
2222
+ errs.iter().any(|e| e
2223
+ .message
2224
+ .contains("generic type 'Parcel' needs type arguments, e.g. Parcel[T]")),
2225
+ "got: {:?}",
2226
+ errs
2227
+ );
2228
+ }
2229
+
2230
+ #[test]
2231
+ fn bare_generic_type_allowed_inside_its_own_methods() {
2232
+ let src = "\
2233
+ enum Box[T] =
2234
+ | Box(value: T)
2235
+
2236
+ fun same(self) -> Box =
2237
+ self
2238
+ ";
2239
+ let result = checkSource(&parse(src));
2240
+ assert!(result.is_ok(), "got {:?}", result.err());
2241
+ }
plum-checker/tests/monomorphize_tests.rs DELETED
@@ -1,191 +0,0 @@
1
- #![allow(non_snake_case)]
2
- use plum_checker::monomorphize::*;
3
- use plum_checker::types::PlumType;
4
- use plum_core::ast;
5
-
6
- #[test]
7
- fn isGenericParamNameAcceptsSingleUppercaseLettersOnly() {
8
- assert!(isGenericParamName("T"));
9
- assert!(isGenericParamName("W"));
10
- assert!(!isGenericParamName("Int"));
11
- assert!(!isGenericParamName("TU"));
12
- assert!(!isGenericParamName("a"));
13
- assert!(!isGenericParamName(""));
14
- }
15
-
16
- #[test]
17
- fn fnGenericParamsDetectsImplicitUppercaseLetterTypesInOrder() {
18
- let f = ast::Fn {
19
- is_extern: false,
20
- line: 0,
21
- column: 0,
22
- name: "pair".to_string(),
23
- type_param: None,
24
- params: vec![
25
- ast::Param {
26
- name: "first".to_string(),
27
- ty: ast::ParamType::Type(ast::Type {
28
- name: "T".to_string(),
29
- generics: vec![],
30
- }),
31
- default: None,
32
- },
33
- ast::Param {
34
- name: "second".to_string(),
35
- ty: ast::ParamType::Type(ast::Type {
36
- name: "U".to_string(),
37
- generics: vec![],
38
- }),
39
- default: None,
40
- },
41
- ],
42
- returns: Some(ast::Type {
43
- name: "Bool".to_string(),
44
- generics: vec![],
45
- }),
46
- body: ast::FnBody::Block(ast::Block { stmts: vec![] }),
47
- };
48
- assert_eq!(fnGenericParams(&f), vec!["T".to_string(), "U".to_string()]);
49
- }
50
-
51
- #[test]
52
- fn enumGenericParamsDetectsImplicitUppercaseLetterVariantFields() {
53
- let e = ast::Enum {
54
- line: 0,
55
- column: 0,
56
- name: "Maybe".to_string(),
57
- generics: vec![],
58
- implements: vec![],
59
- params: vec![],
60
- variants: vec![
61
- ast::EnumVariant {
62
- name: "Just".to_string(),
63
- fields: vec![ast::Type {
64
- name: "T".to_string(),
65
- generics: vec![],
66
- }],
67
- field_names: vec![],
68
- values: vec![],
69
- },
70
- ast::EnumVariant {
71
- name: "Nothing".to_string(),
72
- fields: vec![],
73
- field_names: vec![],
74
- values: vec![],
75
- },
76
- ],
77
- };
78
- assert_eq!(enumGenericParams(&e), vec!["T".to_string()]);
79
- }
80
-
81
- #[test]
82
- fn mangleJoinsBaseNameAndTypeArgs() {
83
- assert_eq!(mangle("Box", &[PlumType::TInt]), "Box$Int");
84
- assert_eq!(
85
- mangle("Pair", &[PlumType::TInt, PlumType::TStr]),
86
- "Pair$Int$Str"
87
- );
88
- assert_eq!(mangle("Green", &[]), "Green");
89
- }
90
-
91
- #[test]
92
- fn specializeFnSubstitutesGenericParamAndReturnTypes() {
93
- let f = ast::Fn {
94
- is_extern: false,
95
- line: 0,
96
- column: 0,
97
- name: "wrap".to_string(),
98
- type_param: None,
99
- params: vec![ast::Param {
100
- name: "value".to_string(),
101
- ty: ast::ParamType::Type(ast::Type {
102
- name: "T".to_string(),
103
- generics: vec![],
104
- }),
105
- default: None,
106
- }],
107
- returns: Some(ast::Type {
108
- name: "T".to_string(),
109
- generics: vec![],
110
- }),
111
- body: ast::FnBody::Expr(ast::Expr::Var("value".to_string())),
112
- };
113
- let mut bindings = std::collections::BTreeMap::new();
114
- bindings.insert("T".to_string(), PlumType::TInt);
115
- let specialized = specializeFn(&f, &Substitution(bindings), "wrap$Int", None);
116
- assert_eq!(specialized.name, "wrap$Int");
117
- match &specialized.params[0].ty {
118
- ast::ParamType::Type(t) => assert_eq!(t.name, "Int"),
119
- _ => panic!("expected ParamType::Type"),
120
- }
121
- assert_eq!(specialized.returns.unwrap().name, "Int");
122
- }
123
-
124
- #[test]
125
- fn specializeFnSetsNewReceiverForAMethod() {
126
- let f = ast::Fn {
127
- is_extern: false,
128
- line: 0,
129
- column: 0,
130
- name: "getValue".to_string(),
131
- type_param: Some("Box".to_string()),
132
- params: vec![],
133
- returns: Some(ast::Type {
134
- name: "T".to_string(),
135
- generics: vec![],
136
- }),
137
- body: ast::FnBody::Expr(ast::Expr::Self_),
138
- };
139
- let mut bindings = std::collections::BTreeMap::new();
140
- bindings.insert("T".to_string(), PlumType::TStr);
141
- let specialized = specializeFn(
142
- &f,
143
- &Substitution(bindings),
144
- "getValue",
145
- Some("Box$Str".to_string()),
146
- );
147
- assert_eq!(specialized.name, "getValue");
148
- assert_eq!(specialized.type_param, Some("Box$Str".to_string()));
149
- assert_eq!(specialized.returns.unwrap().name, "Str");
150
- }
151
-
152
- #[test]
153
- fn specializeEnumSubstitutesGenericVariantFieldNames() {
154
- let e = ast::Enum {
155
- line: 0,
156
- column: 0,
157
- name: "Maybe".to_string(),
158
- generics: vec![],
159
- implements: vec![],
160
- params: vec![],
161
- variants: vec![
162
- ast::EnumVariant {
163
- name: "Just".to_string(),
164
- fields: vec![ast::Type {
165
- name: "T".to_string(),
166
- generics: vec![],
167
- }],
168
- field_names: vec![],
169
- values: vec![],
170
- },
171
- ast::EnumVariant {
172
- name: "Nothing".to_string(),
173
- fields: vec![],
174
- field_names: vec![],
175
- values: vec![],
176
- },
177
- ],
178
- };
179
- let mut bindings = std::collections::BTreeMap::new();
180
- bindings.insert("T".to_string(), PlumType::TInt);
181
- let specialized = specializeEnum(&e, &Substitution(bindings), "Maybe$Int");
182
- assert_eq!(specialized.name, "Maybe$Int");
183
- assert_eq!(
184
- specialized.variants[0].fields,
185
- vec![ast::Type {
186
- name: "Int".to_string(),
187
- generics: vec![]
188
- }]
189
- );
190
- assert!(specialized.variants[1].fields.is_empty());
191
- }
plum-cli/src/main.rs CHANGED
@@ -149,16 +149,17 @@ fn cmdFormat(file: Option<std::path::PathBuf>, check: bool, use_stdin: bool) ->
149
149
  fn compileToWasm(file: &std::path::Path, lib_path: &std::path::Path) -> Result<Vec<u8>> {
150
150
  let ast = plum_core::loadAndMerge(file, lib_path).map_err(|e| anyhow::anyhow!("{e}"))?;
151
151
 
152
- // Type check
152
+ // Type check once; codegen reuses the result.
153
- if let Err(errors) = plum_checker::checkSource(&ast) {
153
+ let typed = match plum_wasm_codegen::typeCheck(&ast) {
154
+ Ok(t) => t,
154
- for e in &errors {
155
+ Err(messages) => {
155
- eprintln!("type error: {}", e.message);
156
+ eprintln!("{messages}");
157
+ process::exit(1);
156
158
  }
157
- process::exit(1);
158
- }
159
+ };
159
160
 
160
- // Codegen
161
+ plum_wasm_codegen::compileTyped(&ast, typed)
161
- plum_wasm_codegen::compileSource(&ast).map_err(|e| anyhow::anyhow!("codegen error: {e}"))
162
+ .map_err(|e| anyhow::anyhow!("codegen error: {e}"))
162
163
  }
163
164
 
164
165
  fn compileToTestWasm(
@@ -167,21 +168,16 @@ fn compileToTestWasm(
167
168
  ) -> Result<(Vec<u8>, Vec<String>)> {
168
169
  let ast = plum_core::loadAndMerge(file, lib_path).map_err(|e| anyhow::anyhow!("{e}"))?;
169
170
 
170
- if let Err(errors) = plum_checker::checkSource(&ast) {
171
- // Returns an error (rather than exiting the process directly, as
171
+ // Returns an error (rather than exiting the process directly, as
172
- // `compileToWasm`'s equivalent check still does for the single-file
172
+ // `compileToWasm`'s equivalent check does for the single-file
173
- // `compile`/`run`/`build` commands) so a recursive `plum test <dir>`
173
+ // `compile`/`run`/`build` commands) so a recursive `plum test <dir>`
174
- // sweep can catch it per-file, in `cmdTest`'s directory branch, and
174
+ // sweep can catch it per-file, in `cmdTest`'s directory branch, and
175
- // continue to the rest of the directory instead of the whole run
175
+ // continue to the rest of the directory instead of the whole run
176
- // dying on the first file with a type error.
176
+ // dying on the first file with a type error. Checked once, on the
177
- let messages: Vec<String> = errors
177
+ // test-desugared source codegen then compiles.
178
- .iter()
179
- .map(|e| format!("type error: {}", e.message))
178
+ let prog = plum_wasm_codegen::desugarTests(&ast);
180
- .collect();
181
- anyhow::bail!(messages.join("\n"));
179
+ let typed = plum_wasm_codegen::typeCheck(&prog.source).map_err(|m| anyhow::anyhow!(m))?;
182
- }
183
-
184
- let compiled = plum_wasm_codegen::compileTestSource(&ast)
180
+ let compiled = plum_wasm_codegen::compileTestTyped(&prog, typed)
185
181
  .map_err(|e| anyhow::anyhow!("codegen error: {e}"))?;
186
182
  // Debug aid: `PLUM_DUMP_WASM=out.wasm plum test file.plum` keeps the module.
187
183
  if let Some(path) = std::env::var_os("PLUM_DUMP_WASM") {
plum-examples/functions.plum CHANGED
@@ -69,20 +69,20 @@ enum Parcel[T] =
69
69
  | Delivered(T)
70
70
  | Lost
71
71
 
72
- fun contentsOr(p: Parcel, default: Int) -> Int =
72
+ fun contentsOr(p: Parcel[Int], default: Int) -> Int =
73
73
  match p
74
74
  Delivered(v) => v
75
75
  Lost => default
76
76
 
77
77
  # `Str.length()` is not a real, working method — the point of this test is
78
- # that `Parcel$Str` coexists with `Parcel$Int` and both run correctly, not
78
+ # that `Parcel[Str]` coexists with `Parcel[Int]` and both run correctly, not
79
79
  # string processing, so the Str arm returns a fixed literal.
80
- fun intContentsOr(p: Parcel, default: Int) -> Int =
80
+ fun intContentsOr(p: Parcel[Int], default: Int) -> Int =
81
81
  match p
82
82
  Delivered(v) => v
83
83
  Lost => default
84
84
 
85
- fun strContentsOr(p: Parcel, default: Int) -> Int =
85
+ fun strContentsOr(p: Parcel[Str], default: Int) -> Int =
86
86
  match p
87
87
  Delivered(v) => 4
88
88
  Lost => default
plum-examples/match.plum CHANGED
@@ -63,7 +63,7 @@ enum GenericBox[T] =
63
63
  | GFull(T)
64
64
  | GEmpty
65
65
 
66
- fun unwrapGenericBox(b: GenericBox) -> Int =
66
+ fun unwrapGenericBox(b: GenericBox[Reading[Int]]) -> Int =
67
67
  match b
68
68
  GFull(Sample(v)) => v
69
69
  GFull(NoSignal) => -1
plum-examples/types.plum CHANGED
@@ -40,10 +40,10 @@ enum Color =
40
40
  fun stringifyColor(v: Red) -> Str =
41
41
  "Red"
42
42
 
43
- fun makeIntBox() -> Box =
43
+ fun makeIntBox() -> Box[Int] =
44
44
  Box(value: 5)
45
45
 
46
- fun makeStrBox() -> Box =
46
+ fun makeStrBox() -> Box[Str] =
47
47
  Box(value: "x")
48
48
 
49
49
  # ---- record-shaped and sum-type enum regression tests ----
plum-std/Array.plum CHANGED
@@ -3,20 +3,16 @@ import std/Bool
3
3
  import std/Number
4
4
 
5
5
  # A fixed-length, O(1)-indexable array of `T`, backed directly by a wasm-gc
6
- # `array<anyref>` — every `Array[T]` specialization (`Array$Int`, `Array$Str`,
6
+ # `array<anyref>` — `Array[T]` is erased, so every `T` shares the SAME
7
- # ...) shares the SAME underlying wasm array type (see
7
+ # underlying wasm array type (see
8
8
  # `plum-wasm-codegen::GcTypeRegistry::array_type_idx`), exactly like `[]Byte`
9
- # reuses `Str`'s own array type. `init`/`get`/`set`/`length` are compiler
9
+ # reuses `Str`'s own array type. `init`/`get`/`set`/`length`/`push`/`resize` are compiler
10
10
  # intrinsics (see `compileIntrinsicFnBody`) — there's no way to express raw
11
11
  # array allocation/indexing in Plum source itself.
12
12
  #
13
- # LIMITATION: `T` must be a reference type (a `type`/`enum` value, `Str`,
13
+ # Elements are stored as `anyref`; `T` is erased, so a primitive `T` (`Int`,
14
- # `List`, etc.) — every element is stored as a boxed `anyref` and `get`
14
+ # `Float`, ...) is boxed on `set`/`push` and unboxed on `get` at the call
15
- # narrows it back to `T` via `ref.cast`. A primitive `T` (`Int`/`Float`/`Bool`/
16
- # `Byte`) is NOT supported (no boxing/unboxing exists yet) and will not
17
- # codegen correctly. Nothing in `libs/std` instantiates `Array` with a
18
- # primitive `T` today — the only user is `Map[K, V]`'s bucket array,
15
+ # site, like any other generic edge. `List[T]` is built on this.
19
- # `Array[List[Pair[K, V]]]`.
20
16
  #
21
17
  # `init()` is deliberately zero-arg (no `Array[T](n)` constructor exists) —
22
18
  # `class_call`'s grammar (`Type[Generics](...)`) only ever accepts `name:
@@ -58,3 +54,9 @@ enum Array[T] =
58
54
  # arr.push(v)`.
59
55
  fun push(self, v: T) -> Array[T] =
60
56
  todo
57
+
58
+ # Returns a NEW array of length `n` holding `self`'s first `min(length, n)`
59
+ # elements (the remaining slots null-shaped, like `init`/`push`). `List`
60
+ # grows by doubling through this.
61
+ fun resize(self, n: Int) -> Array[T] =
62
+ todo
plum-std/Json.plum CHANGED
@@ -289,7 +289,7 @@ enum JsonParser =
289
289
  fun parseArray(self) -> Try[Json, JsonParseError] =
290
290
  self.advance()
291
291
  self.skipSpace()
292
- items := List[Json](head: End, tail: End, size: 0)
292
+ items := List[Json]()
293
293
  if self.peek() == 93
294
294
  self.advance()
295
295
  return Ok(JsonList(items))
plum-std/List.plum CHANGED
@@ -1,24 +1,20 @@
1
1
  module std
2
2
 
3
3
  import std/Try
4
+ import std/Array
4
5
  import std/Buffer
5
6
  import std/Number
6
7
  import std/Bool
7
8
  import std/Str
8
9
 
9
- # A node stores the data in a list and contains pointers to the previous and next sibling nodes
10
+ # A growable list: `data` is a backing `Array[T]` whose length is the list's
11
+ # capacity, `size` the number of slots in use. Capacity doubles when `push`
12
+ # runs out of room (via `Array.resize`), so `push` is O(1) amortized and
13
+ # `get`/`set` are O(1). The core operations (`init`, `length`, `get`, `set`,
14
+ # `push`, `pop`, `clear`) sit directly on the `Array` intrinsics; everything
10
- # Private link between nodes (not an error tag, so a domain enum rather than Try).
15
+ # else (`map`, `retain`, `reduce`, `chunk`, ...) is ordinary Plum on top of them.
11
- enum Link[N] =
12
- | Next(N)
13
- | End
14
-
15
- enum Node[T] =
16
- | Node(value: T, prev: Link[Node[T]], next: Link[Node[T]])
17
-
18
- # A list is a data structure describing a contiguous section of an array stored separately from the slice variable itself.
19
- # It contains the pointers to the start and end nodes (head, tail) and maintains the size as well
20
16
  enum List[T: ToStr](ToStr) =
21
- | List(head: Link[Node[T]], tail: Link[Node[T]], size: Int)
17
+ | List(data: Array[T], size: Int)
22
18
 
23
19
  # `List[T](1, 2, 3)` (or, for an empty list, `List[T]()` — explicit
24
20
  # generics required either way, since nothing else in a call this shape
@@ -26,42 +22,24 @@ enum List[T: ToStr](ToStr) =
26
22
  # `plum-checker`/`plum-wasm-codegen`'s `ClassCall`/`FnCall` handling, which
27
23
  # desugars both call shapes to `List.init(...)`.
28
24
  fun init(values: ...T) -> List[T] =
29
- l := List(head: End, tail: End, size: 0)
25
+ l := List(data: Array[T](), size: 0)
30
26
  for v := range values
31
- l.add(v)
27
+ l.push(v)
32
28
  return l
33
29
 
34
30
  # gets the element at i'th index of the list
35
31
  fun get(self, i: Int) -> Try[T, [OutOfBounds]] =
36
- current := self.head
32
+ if i < 0 || i >= self.size
37
- index := 0
38
- while current != End
39
- match current
40
- Next(Node(value, _, next)) =>
41
- if index == i
42
- return Ok(value)
43
- current = next
44
- index = index + 1
45
- End =>
46
- break
47
- Err(OutOfBounds)
33
+ return Err(OutOfBounds)
34
+ Ok(self.data.get(i))
48
35
 
49
36
  # sets the element at i'th index of the list, returning the old value (or Err(OutOfBounds) if i is out of bounds)
50
37
  fun set(self, i: Int, v: T) -> Try[T, [OutOfBounds]] =
51
- current := self.head
38
+ if i < 0 || i >= self.size
52
- index := 0
53
- while current != End
54
- match current
55
- Next(node) =>
56
- if index == i
57
- old := node.value
58
- node.value = v
59
- return Ok(old)
60
- current = node.next
61
- index = index + 1
62
- End =>
63
- break
64
- Err(OutOfBounds)
39
+ return Err(OutOfBounds)
40
+ old := self.data.get(i)
41
+ self.data.set(i, v)
42
+ Ok(old)
65
43
 
66
44
  # returns the no of elements in the list
67
45
  fun length(self) -> Int =
@@ -74,143 +52,86 @@ enum List[T: ToStr](ToStr) =
74
52
  if self.length() != other.length()
75
53
  return False
76
54
  for i := range self.length()
77
- if self.get(i).unwrap() != other.get(i).unwrap()
55
+ if self.data.get(i) != other.data.get(i)
78
56
  return False
79
57
  return True
80
58
 
59
+ # appends `v` to the end of the list, doubling the capacity when full
60
+ fun push(self, v: T) -> Unit =
61
+ if self.size == self.data.length()
62
+ cap := self.size == 0 ? 16 : self.size * 2
63
+ self.data = self.data.resize(cap)
64
+ self.data.set(self.size, v)
65
+ self.size = self.size + 1
66
+
81
67
  # adds the specified elements to the end of the list
82
68
  fun add(self, values: ...T) =
83
69
  for v := range values
84
- node := Node(value: v, prev: self.tail, next: End)
85
- match self.tail
86
- Next(t) =>
87
- t.next = Next(node)
88
- End =>
89
- self.head = Next(node)
70
+ self.push(v)
71
+
72
+ # removes and returns the last element, or `Err(ListWasEmpty)` if empty
90
- self.tail = Next(node)
73
+ fun pop(self) -> Try[T, [ListWasEmpty]] =
91
- self.size = self.size + 1
74
+ if self.size == 0
92
-
93
- # unlinks node from the list, patching its neighbors (or head/tail) to close the gap
94
- fun unlink(self, node: Node[T]) =
95
- match node.prev
96
- Next(p) =>
97
- p.next = node.next
98
- End =>
99
- self.head = node.next
75
+ return Err(ListWasEmpty)
100
- match node.next
101
- Next(n) =>
102
- n.prev = node.prev
103
- End =>
104
- self.tail = node.prev
105
76
  self.size = self.size - 1
77
+ Ok(self.data.get(self.size))
106
78
 
107
- # removes the element at i'th index of the list
79
+ # removes the element at i'th index of the list (out of range is a no-op)
108
80
  fun removeAt(self, i: Int) =
109
- current := self.head
81
+ if i < 0 || i >= self.size
110
- index := 0
111
- while current != End
112
- match current
113
- Next(node) =>
114
- if index == i
115
- self.unlink(node)
116
- return
82
+ return
83
+ j := i
117
- current = node.next
84
+ while j < self.size - 1
85
+ self.data.set(j, self.data.get(j + 1))
118
- index = index + 1
86
+ j = j + 1
119
- End =>
87
+ self.size = self.size - 1
120
- break
121
88
 
122
89
  # removes the first element equal to v from list
123
90
  fun remove(self, v: T) =
124
- current := self.head
91
+ for i := range self.size
125
- while current != End
126
- match current
127
- Next(node) =>
128
- if node.value == v
92
+ if self.data.get(i) == v
129
- self.unlink(node)
93
+ self.removeAt(i)
130
- return
94
+ return
131
- current = node.next
132
- End =>
133
- break
134
95
 
135
96
  # removes all objects from this list
136
97
  fun clear(self) =
137
- self.head = End
138
- self.tail = End
139
98
  self.size = 0
140
99
 
141
- # returns the list with the elements relinked in reverse order.
100
+ # reverses the list in place, returning it.
142
- fun reverse(self) -> List =
101
+ fun reverse(self) -> List[T] =
102
+ i := 0
143
- current := self.head
103
+ j := self.size - 1
144
- while current != End
104
+ while i < j
145
- match current
146
- Next(node) =>
147
- next := node.next
148
- node.next = node.prev
149
- node.prev = next
150
- current = next
151
- End =>
152
- break
153
- old_head := self.head
105
+ tmp := self.data.get(i)
106
+ self.data.set(i, self.data.get(j))
154
- self.head = self.tail
107
+ self.data.set(j, tmp)
155
- self.tail = old_head
108
+ i = i + 1
109
+ j = j - 1
156
110
  self
157
111
 
158
- # Inserts `value` immediately before `node` (which must belong to `self`),
159
- # patching neighbors (or `head`) to make room — the insertion-point half of
160
- # `sort`'s pointer surgery, split out since `unlink`'s counterpart already
161
- # gets its own method.
162
- fun insertBefore(self, node: Node[T], value: T) -> Unit =
163
- new_node := Node(value: value, prev: node.prev, next: Next(node))
164
- match node.prev
165
- Next(p) =>
166
- p.next = Next(new_node)
167
- End =>
168
- self.head = Next(new_node)
169
- node.prev = Next(new_node)
170
- self.size = self.size + 1
171
-
172
- # Inserts `value` into `self` (assumed already sorted by `before`) at the
173
- # position that keeps it sorted — the first position whose existing element
174
- # `before(value, existing)` holds for, or the end if none does.
175
- fun insertSorted(self, value: T, before: fn(T, T) -> Bool) -> Unit =
176
- current := self.head
177
- while current != End
178
- match current
179
- Next(node) =>
180
- if before(value, node.value)
181
- self.insertBefore(node, value)
182
- return
183
- current = node.next
184
- End =>
185
- break
186
- self.add(value)
187
-
188
112
  # Returns a NEW list with `self`'s elements sorted, using `before(a, b)` as
189
- # the "does a belong before b" comparator. Insertion sort (O(n^2)) — simple
113
+ # the "does a belong before b" comparator. Insertion sort (O(n^2)).
190
- # and adequate for the list sizes a linked list is already suited to.
191
114
  fun sort(self, before: fn(T, T) -> Bool) -> List[T] =
192
- result := List(head: End, tail: End, size: 0)
115
+ result := self.sublist(0, self.size)
116
+ i := 1
193
- current := self.head
117
+ while i < result.size
118
+ x := result.data.get(i)
119
+ j := i - 1
194
- while current != End
120
+ while j >= 0
195
- match current
196
- Next(Node(value, _, next)) =>
121
+ if !before(x, result.data.get(j))
197
- result.insertSorted(value, before)
198
- current = next
199
- End =>
200
122
  break
123
+ result.data.set(j + 1, result.data.get(j))
124
+ j = j - 1
125
+ result.data.set(j + 1, x)
126
+ i = i + 1
201
127
  return result
202
128
 
203
129
  # Returns the first element equal to `search`, or `Err(NotFound)`.
204
130
  fun find(self, search: T) -> Try[T, [NotFound]] =
131
+ for i := range self.size
205
- current := self.head
132
+ value := self.data.get(i)
206
- while current != End
207
- match current
208
- Next(Node(value, _, next)) =>
209
- if value == search
133
+ if value == search
210
- return Ok(value)
134
+ return Ok(value)
211
- current = next
212
- End =>
213
- break
214
135
  return Err(NotFound)
215
136
 
216
137
  fun contains(self, v: T) -> Bool =
@@ -218,31 +139,15 @@ enum List[T: ToStr](ToStr) =
218
139
 
219
140
  # calls f for each elem in the list
220
141
  fun each(self, cb: fn(T)) -> Unit =
221
- current := self.head
142
+ for i := range self.size
222
- while current != End
143
+ cb(self.data.get(i))
223
- match current
224
- Next(Node(value, _, next)) =>
225
- cb(value)
226
- current = next
227
- End =>
228
- break
229
144
 
230
- # Returns a new list with each element transformed by `cb`.
145
+ # Returns a new list with each element transformed by `cb`. `U` is a
231
- #
232
- # `U` here is a generic param of `map` ITSELF, separate from `List[T]`'s own
146
+ # generic param of `map` itself, separate from `List[T]`'s own `T`.
233
- # `T` — resolved per CALL SITE (from `cb`'s own inferred return type), not
234
- # when `List[T]` itself is specialized, via `resolveMethodOwnGenerics` in
235
- # `plum-checker/src/monomorphize.rs`.
236
147
  fun map(self, cb: fn(T) -> U) -> List[U] =
237
- result := List(head: End, tail: End, size: 0)
148
+ result := List[U]()
238
- current := self.head
149
+ for i := range self.size
239
- while current != End
240
- match current
241
- Next(Node(value, _, next)) =>
242
- result.add(cb(value))
150
+ result.push(cb(self.data.get(i)))
243
- current = next
244
- End =>
245
- break
246
151
  return result
247
152
 
248
153
  # returns a new list with each element flat-mapped
@@ -251,102 +156,65 @@ enum List[T: ToStr](ToStr) =
251
156
 
252
157
  # Returns a new list with only the elements `predicate` holds for.
253
158
  fun retain(self, predicate: fn(T) -> Bool) -> List[T] =
254
- result := List(head: End, tail: End, size: 0)
159
+ result := List[T]()
160
+ for i := range self.size
255
- current := self.head
161
+ value := self.data.get(i)
256
- while current != End
257
- match current
258
- Next(Node(value, _, next)) =>
259
- if predicate(value)
162
+ if predicate(value)
260
- result.add(value)
163
+ result.push(value)
261
- current = next
262
- End =>
263
- break
264
164
  return result
265
165
 
266
166
  # Returns a new list with the elements `predicate` holds for removed.
267
167
  fun reject(self, predicate: fn(T) -> Bool) -> List[T] =
268
- result := List(head: End, tail: End, size: 0)
168
+ result := List[T]()
169
+ for i := range self.size
269
- current := self.head
170
+ value := self.data.get(i)
270
- while current != End
271
- match current
272
- Next(Node(value, _, next)) =>
273
- if !predicate(value)
171
+ if !predicate(value)
274
- result.add(value)
172
+ result.push(value)
275
- current = next
276
- End =>
277
- break
278
173
  return result
279
174
 
280
175
  fun any(self, predicate: fn(T) -> Bool) -> Bool =
281
- current := self.head
176
+ for i := range self.size
282
- while current != End
283
- match current
284
- Next(Node(value, _, next)) =>
285
- if predicate(value)
177
+ if predicate(self.data.get(i))
286
- return True
178
+ return True
287
- current = next
288
- End =>
289
- break
290
179
  return False
291
180
 
292
181
  fun every(self, predicate: fn(T) -> Bool) -> Bool =
293
- current := self.head
182
+ for i := range self.size
294
- while current != End
295
- match current
296
- Next(Node(value, _, next)) =>
297
- if !predicate(value)
183
+ if !predicate(self.data.get(i))
298
- return False
184
+ return False
299
- current = next
300
- End =>
301
- break
302
185
  return True
303
186
 
304
187
  # Folds `self`'s elements into a single value, starting from `acc` and
305
- # combining each element in with `cb(accumulatedSoFar, element)`.
188
+ # combining each element in with `cb(accumulatedSoFar, element)`. `U` is
306
- #
307
- # `U` is `reduce`'s OWN generic param (the accumulator's type), separate
189
+ # `reduce`'s own generic param (the accumulator's type).
308
- # from `List[T]`'s own `T` — same per-CALL-SITE resolution as `map`'s `U`.
309
190
  fun reduce(self, acc: U, cb: fn(U, T) -> U) -> U =
310
191
  result := acc
311
- current := self.head
192
+ for i := range self.size
312
- while current != End
313
- match current
314
- Next(Node(value, _, next)) =>
315
- result = cb(result, value)
193
+ result = cb(result, self.data.get(i))
316
- current = next
317
- End =>
318
- break
319
194
  return result
320
195
 
321
196
  # returns the first element in the list
322
197
  fun first(self) -> Try[T, [ListWasEmpty]] =
323
- match self.head
198
+ if self.size == 0
324
- Next(Node(value, _, _)) => Ok(value)
325
- End => Err(ListWasEmpty)
199
+ return Err(ListWasEmpty)
200
+ Ok(self.data.get(0))
326
201
 
327
202
  # returns the last element in the list
328
203
  fun last(self) -> Try[T, [ListWasEmpty]] =
329
- match self.tail
204
+ if self.size == 0
330
- Next(Node(value, _, _)) => Ok(value)
331
- End => Err(ListWasEmpty)
205
+ return Err(ListWasEmpty)
206
+ Ok(self.data.get(self.size - 1))
332
207
 
333
208
  # Returns a new list of the elements at index `[start, end)`, clamped into
334
209
  # range (a `start`/`end` outside `[0, length]` is clamped, not an error).
335
210
  fun sublist(self, start: Int, end: Int) -> List[T] =
336
- result := List(head: End, tail: End, size: 0)
211
+ result := List[T]()
337
212
  s := start < 0 ? 0 : start
338
213
  e := end > self.size ? self.size : end
339
- current := self.head
340
- i := 0
214
+ i := s
341
- while current != End && i < e
342
- match current
343
- Next(Node(value, _, next)) =>
344
- if i >= s
215
+ while i < e
345
- result.add(value)
216
+ result.push(self.data.get(i))
346
- current = next
347
- i = i + 1
217
+ i = i + 1
348
- End =>
349
- break
350
218
  return result
351
219
 
352
220
  # The first `n` elements.
@@ -364,7 +232,7 @@ enum List[T: ToStr](ToStr) =
364
232
  fun sample(self) -> Try[T, [ListWasEmpty]] =
365
233
  if self.size == 0
366
234
  return Err(ListWasEmpty)
367
- return Ok(self.get(randomInt(self.size)).unwrap())
235
+ return Ok(self.data.get(randomInt(self.size)))
368
236
 
369
237
  # Returns a new list with `self`'s elements in random order (Fisher-Yates).
370
238
  fun shuffle(self) -> List[T] =
@@ -372,105 +240,46 @@ enum List[T: ToStr](ToStr) =
372
240
  i := result.size - 1
373
241
  while i > 0
374
242
  j := randomInt(i + 1)
375
- vi := result.get(i).unwrap()
243
+ vi := result.data.get(i)
376
- vj := result.get(j).unwrap()
377
- result.set(i, vj)
244
+ result.data.set(i, result.data.get(j))
378
- result.set(j, vi)
245
+ result.data.set(j, vi)
379
246
  i = i - 1
380
247
  return result
381
248
 
382
- # `chunk`/`partition` both return `List[List[T]]` — the SAME generic class
383
- # (`List`) nested inside itself as a method's own return type. This used to
384
- # be an unconditional stack-overflow landmine for the WHOLE COMPILER (see
385
- # `plum-checker/src/monomorphize.rs`'s `isSelfNested`/`methodMentionsTemplate`
386
- # doc comments for the full fix) — that danger is now FIXED (2026-09-02).
387
- #
388
- # A second, more contained bug surfaced once past that: two SEPARATE bare
389
- # `List(head: End, tail: End, size: 0)` constructions in the SAME method
390
- # body — one for the OUTER `result: List[List[T]]` (matching this
391
- # method's own declared return type) and one for an INNER per-chunk
392
- # `piece: List[T]` — are ambiguous to `resolveClassInstantiation`'s
393
- # `current_return_type` fallback: since `List[T]` and `List[List[T]]` both
394
- # have exactly ONE generic argument, the fallback (which only ever
395
- # compares ARITY, not identity) can't tell "this bare construction IS the
396
- # function's own return value" apart from "this is some OTHER, unrelated
397
- # nested construction of the same template that happens to need the same
398
- # arity" — so `piece` was silently resolved to the SAME type as `result`
399
- # (`List$List$Int` instead of `List$Int`), corrupting its own `add` calls.
400
- # Worked around by building `piece` via `self.sublist(...)` instead of a
401
- # second bare construction — `sublist` is an ordinary method already
249
+ # Splits into consecutive sublists of at most `size` elements each.
402
- # unambiguously resolved through the RECEIVER's own (already-correct)
403
- # binding, no `current_return_type` guessing involved at all.
404
250
  fun chunk(self, size: Int) -> List[List[T]] =
405
- result := List(head: End, tail: End, size: 0)
251
+ result := List[List[T]]()
406
252
  if size <= 0
407
253
  return result
408
254
  i := 0
409
255
  n := self.length()
410
256
  while i < n
411
- result.add(self.sublist(i, i + size))
257
+ result.push(self.sublist(i, i + size))
412
258
  i = i + size
413
259
  return result
414
260
 
415
261
  # Splits into two lists: elements `predicate` holds for, then the rest —
416
262
  # in that order. Returns them as a 2-element `List[List[T]]` (`[matched,
417
- # unmatched]`) since this language has no tuple type. Deliberately built
263
+ # unmatched]`) since this language has no tuple type.
418
- # from `self.retain(predicate)`/`self.reject(predicate)` rather than two
419
- # local `matched`/`unmatched := List(head: End, ...)` bare constructions
420
- # — see `chunk`'s doc comment above for exactly why that would be
421
- # ambiguous (`matched`/`unmatched` need `List[T]`, but a bare
422
- # construction here would resolve via `current_return_type` fallback
423
- # against THIS method's own `List[List[T]]` return instead, same arity
424
- # coincidence). `retain`/`reject` are each already unambiguous (single
425
- # bare construction per method body, matching their own single-level
426
- # return type).
427
264
  fun partition(self, predicate: fn(T) -> Bool) -> List[List[T]] =
428
- result := List(head: End, tail: End, size: 0)
265
+ result := List[List[T]]()
429
- result.add(self.retain(predicate))
266
+ result.push(self.retain(predicate))
430
- result.add(self.reject(predicate))
267
+ result.push(self.reject(predicate))
431
268
  return result
432
269
 
433
- # Grouping by an arbitrary key type would need its own generic param
270
+ # Grouping by an arbitrary key type would need `Map[K, List[T]]`, but
434
- # (`groupBy<K>(keyFn: fn(T) -> K) -> Map[K, List[T]]`) resolved from a
435
- # closure's return type — the same method-level-generics gap documented on
436
- # `List.map` above, PLUS `Map[K, List[T]]` would make `list.plum` depend on
437
- # `map.plum`, which already depends on `list.plum`. Not implemented.
271
+ # `Map.plum` depends on this file. Not implemented.
438
272
  fun groupBy(self) =
439
273
  todo
440
274
 
441
- # Note: NOT written with `each` + a closure-captured "is this the first
442
- # element" flag — a closure's captured variables are snapshotted at
443
- # CREATION time (see `libs/std/list.plum`'s own `add`/`each` usage
444
- # elsewhere), so an assignment to a captured var INSIDE the closure body
445
- # does not persist across that closure's own repeated invocations from one
446
- # `each` call. A plain `while`/`match` traversal (same idiom as `unlink`)
447
- # sidesteps that entirely.
448
275
  fun join(self, sep: Str = ",") -> Str =
449
276
  res := Buffer()
450
- current := self.head
277
+ for i := range self.size
451
- while current != End
452
- match current
453
- Next(Node(value, _, next)) =>
454
- res.write(value.toStr())
455
- if next != End
278
+ if i > 0
456
- res.write(sep)
279
+ res.write(sep)
457
- current = next
280
+ res.write(self.data.get(i).toStr())
458
- End =>
459
- break
460
281
  res.toStr()
461
282
 
462
- # `List[T: ToStr](ToStr)` declared implementing `ToStr`
463
- # from the start but never actually defined `toStr` — harmless for a
464
- # PLAIN `List[Int]`/`List[Str]`/etc (nothing needs to call `.toStr()` on
465
- # the LIST ITSELF, just on its elements, which `join` above already
466
- # does), but a genuine gap the moment a `List` is used AS another
467
- # `List`'s own ELEMENT type (`List[List[T]]`) — the OUTER list's eagerly-
468
- # compiled `join`/`toStr` calls `.toStr()` on each element, and an inner
469
- # `List[T]` element had no such method to call, failing to compile with
470
- # "unknown method 'List$T.toStr'" even for a program that never actually
471
- # calls `.toStr()`/`.join()` on the outer list (per the "eager compile
472
- # every method for every specialization" architecture noted elsewhere in
473
- # this codebase's history).
474
283
  fun toStr(self) -> Str =
475
284
  return "[" + self.join(",") + "]"
476
285
 
@@ -586,3 +395,41 @@ test "partition splits into elements matching a predicate, then the rest"
586
395
  assert parts.length() == 2
587
396
  assert parts.get(0).unwrap().join(",") == "2,4,6"
588
397
  assert parts.get(1).unwrap().join(",") == "1,3,5"
398
+
399
+ test "list_get_set_push_pop: push 1000, get(999), pop until empty, get on empty is Err"
400
+ l := List[Int]()
401
+ for i := range 1000
402
+ l.push(i)
403
+ assert l.length() == 1000
404
+ assert l.get(999).unwrap() == 999
405
+ assert optSumForListTest(l.get(1000)) == -1000
406
+ assert l.set(500, 7).unwrap() == 500
407
+ assert l.get(500).unwrap() == 7
408
+ assert l.pop().unwrap() == 999
409
+ assert l.length() == 999
410
+ while l.length() > 0
411
+ l.pop()
412
+ assert l.length() == 0
413
+ assert l.pop().isErr()
414
+ assert optSumForListTest(l.get(0)) == -1000
415
+ e := l.get(0)
416
+ assert e.isErr()
417
+
418
+ test "list_growth: length stays exact across the doubling boundaries"
419
+ l := List[Int]()
420
+ for i := range 1025
421
+ l.push(i)
422
+ n := i + 1
423
+ if n == 15 || n == 16 || n == 17 || n == 1023 || n == 1024 || n == 1025
424
+ assert l.length() == n
425
+ assert l.get(n - 1).unwrap() == i
426
+ assert l.get(0).unwrap() == 0
427
+ assert l.get(16).unwrap() == 16
428
+ assert l.get(1024).unwrap() == 1024
429
+
430
+ test "list of strings keeps its elements through growth"
431
+ l := List[Str]()
432
+ for i := range 40
433
+ l.push("s{i}")
434
+ assert l.get(39).unwrap() == "s39"
435
+ assert l.get(16).unwrap() == "s16"
plum-std/Map.plum CHANGED
@@ -6,28 +6,18 @@ import std/Bool
6
6
  import std/Number
7
7
  import std/Str
8
8
 
9
- # Any type usable as a `Map` key needs a `hash`, so keys landing in different
9
+ # Any type usable as a `Map` key needs a `hash`. `Map` is compiled once
10
- # buckets (`hash(k) % BUCKET_COUNT`) can be told apart in O(1) without a full
11
- # linear scan of the whole map — see `Str.hash`/`Int.hash` in `str.plum`/
10
+ # (erased); `k.hash()` on a `K: Hashable` dispatches at runtime on the key's
12
- # `int.plum` for the two implementations `libs/std` provides today. Declared
13
- # here (rather than alongside `ToStr`/`Comparable` in `str.plum`) since `Map`
11
+ # type id (`Str.hash`, `Int.hash` for a boxed Int). Declared here rather than
14
- # is its only real consumer; like `Comparable`/`Readable`/`Writable`
12
+ # alongside `ToStr` since `Map` is its only real consumer; `Str`/`Int` don't
15
- # elsewhere in `libs/std`, `Str`/`Int` don't formally list it in their own
16
- # `implements` clause (that would need them to import `map.plum`, an
13
+ # list it in their `implements` clause (that would make them import this file).
17
- # unwanted dependency direction for such foundational types) — this is
18
- # checked the same "unenforced, trust the method exists" way those already
19
- # are (see the README's note on `checkTraitConformance`).
20
14
  trait Hashable =
21
15
  hash() -> Int
22
16
 
23
17
  # A Pair is a grouping of a key with a value.
24
18
  #
25
- # `toStr` (unlike `Try`'s — see `Try.plum`'s note on `toTryStr`) IS safe as an ordinary method here: `Pair[K, V]` is only
26
- # ever used as `List[Pair[K, V]]`'s element type inside `Map[K, V]`, and
19
+ # `toStr` exists because `List[T: ToStr]` requires its element type to have one
27
- # `List[T: ToStr]` already requires its own `T` to support `toStr` —
28
- # so a `Pair` used as a `List` element always has `K`/`V: ToStr` too,
20
+ # and `Map` stores `List[Pair[K, V]]` buckets.
29
- # and `List`'s own eagerly-compiled `join`/`toStr` (which call `.toStr()` on
30
- # every element) need this to exist for every `Map` specialization anyway.
31
21
  enum Pair[K, V](ToStr) =
32
22
  | Pair(key: K, val: V)
33
23
 
@@ -51,8 +41,7 @@ enum Map[K: Hashable, V] =
51
41
 
52
42
  # `Map[Str, Int]()` — explicit generics required (nothing in a no-arg call
53
43
  # pins down `K`/`V`), builds `BUCKET_COUNT` empty bucket lists up front via
54
- # `Array`'s own `push`-to-grow (see `libs/std/array.plum`'s header comment
44
+ # `Array`'s `push`-to-grow.
55
- # on why `Array` itself has no direct "make me length N" constructor).
56
45
  fun init() -> Map[K, V] =
57
46
  b := Array[List[Pair[K, V]]]()
58
47
  i := 0
@@ -71,15 +60,11 @@ enum Map[K: Hashable, V] =
71
60
 
72
61
  # Gets the value for `k`, or `Err(KeyNotFound)` if absent.
73
62
  fun get(self, k: K) -> Try[V, [KeyNotFound]] =
74
- current := self.buckets.get(self.bucketIndex(k)).head
63
+ bucket := self.buckets.get(self.bucketIndex(k))
64
+ for i := range bucket.size
75
- while current != End
65
+ pair := bucket.data.get(i)
76
- match current
77
- Next(Node(Pair(key, val), _, next)) =>
78
- if key == k
66
+ if pair.key == k
79
- return Ok(val)
67
+ return Ok(pair.val)
80
- current = next
81
- End =>
82
- break
83
68
  return Err(KeyNotFound)
84
69
 
85
70
  fun has(self, k: K) -> Bool =
@@ -91,17 +76,12 @@ enum Map[K: Hashable, V] =
91
76
  # unboundedly or leave a stale value reachable by `get`.
92
77
  fun set(self, k: K, v: V) -> Unit =
93
78
  bucket := self.buckets.get(self.bucketIndex(k))
94
- current := bucket.head
79
+ for i := range bucket.size
95
- while current != End
80
+ pair := bucket.data.get(i)
96
- match current
97
- Next(Node(pair, _, next)) =>
98
- if pair.key == k
81
+ if pair.key == k
99
- pair.val = v
82
+ pair.val = v
100
- return
83
+ return
101
- current = next
102
- End =>
103
- break
104
- bucket.add(Pair(key: k, val: v))
84
+ bucket.push(Pair(key: k, val: v))
105
85
  self.size = self.size + 1
106
86
 
107
87
  # Sets `k`'s value to `v` only if `k` isn't already present.
@@ -112,17 +92,11 @@ enum Map[K: Hashable, V] =
112
92
  # Removes `k`'s entry, if present.
113
93
  fun remove(self, k: K) -> Unit =
114
94
  bucket := self.buckets.get(self.bucketIndex(k))
115
- current := bucket.head
95
+ for i := range bucket.size
116
- while current != End
117
- match current
118
- Next(node) =>
119
- if node.value.key == k
96
+ if bucket.data.get(i).key == k
120
- bucket.unlink(node)
97
+ bucket.removeAt(i)
121
- self.size = self.size - 1
98
+ self.size = self.size - 1
122
- return
99
+ return
123
- current = node.next
124
- End =>
125
- break
126
100
 
127
101
  fun length(self) -> Int =
128
102
  return self.size
@@ -131,57 +105,38 @@ enum Map[K: Hashable, V] =
131
105
  return self.size == 0
132
106
 
133
107
  fun clear(self) -> Unit =
134
- i := 0
135
- while i < self.buckets.length()
108
+ for i := range self.buckets.length()
136
109
  self.buckets.get(i).clear()
137
- i = i + 1
138
110
  self.size = 0
139
111
 
140
112
  # Calls `cb` with each key and value in the map. Order is bucket order,
141
113
  # then insertion order within a bucket — NOT overall insertion order (that
142
114
  # would need a separate side list to track, which nothing here needs today).
143
115
  fun each(self, cb: fn(K, V)) -> Unit =
144
- i := 0
145
- while i < self.buckets.length()
116
+ for i := range self.buckets.length()
146
- current := self.buckets.get(i).head
117
+ bucket := self.buckets.get(i)
118
+ for j := range bucket.size
147
- while current != End
119
+ pair := bucket.data.get(j)
148
- match current
149
- Next(Node(Pair(key, val), _, next)) =>
150
- cb(key, val)
120
+ cb(pair.key, pair.val)
151
- current = next
152
- End =>
153
- break
154
- i = i + 1
155
121
 
156
122
  fun keys(self) -> List[K] =
157
- result := List(head: End, tail: End, size: 0)
123
+ result := List[K]()
158
124
  self.each(|k, v| result.add(k))
159
125
  return result
160
126
 
161
127
  fun values(self) -> List[V] =
162
- result := List(head: End, tail: End, size: 0)
128
+ result := List[V]()
163
129
  self.each(|k, v| result.add(v))
164
130
  return result
165
131
 
166
132
  fun map(self, cb: fn(K, V) -> Pair[X, Y]) -> Map[X, Y] =
167
- b := Array[List[Pair[X, Y]]]()
168
- i := 0
169
- while i < BUCKET_COUNT
170
- b = b.push(List[Pair[X, Y]]())
171
- i = i + 1
172
- result := Map(buckets: b, size: 0)
133
+ result := Map[X, Y]()
173
- j := 0
174
- while j < self.buckets.length()
134
+ for i := range self.buckets.length()
175
- current := self.buckets.get(j).head
135
+ bucket := self.buckets.get(i)
136
+ for j := range bucket.size
176
- while current != End
137
+ pair := bucket.data.get(j)
177
- match current
178
- Next(Node(Pair(key, val), _, next)) =>
179
- pair := cb(key, val)
138
+ mapped := cb(pair.key, pair.val)
180
- result.set(pair.key, pair.val)
139
+ result.set(mapped.key, mapped.val)
181
- current = next
182
- End =>
183
- break
184
- j = j + 1
185
140
  return result
186
141
 
187
142
  test "get/set/has round-trip and set replaces an existing key in place"
@@ -259,3 +214,20 @@ test "many keys landing in the same bucket still resolve correctly (collision ch
259
214
  assert m.get(4).unwrap() == 16
260
215
  assert m.get(21).unwrap() == 441
261
216
  assert m.length() == 39
217
+
218
+ test "map_collisions_erased: Map[Str, Int] with 500 keys round-trips"
219
+ m := Map[Str, Int]()
220
+ i := 0
221
+ while i < 500
222
+ m.set("key{i}", i * 3)
223
+ i = i + 1
224
+ assert m.length() == 500
225
+ j := 0
226
+ while j < 500
227
+ assert m.get("key{j}").unwrap() == j * 3
228
+ j = j + 1
229
+ assert m.has("key500") == False
230
+ m.remove("key250")
231
+ assert m.length() == 499
232
+ assert m.has("key250") == False
233
+ assert m.get("key251").unwrap() == 753
plum-std/Number.plum CHANGED
@@ -26,7 +26,7 @@ HALF_PI = 1.5707963267948966f # PI / 2
26
26
  TAU = 6.283185307179586f # 2 * PI
27
27
 
28
28
  # `Int`/`Float` bare-wrap into `Number` with no wrapper syntax at all (see
29
- # `plum-checker`'s `unify`/`monomorphize::wrapPrimitiveAgainstExpected`) —
29
+ # `plum-checker`'s `unify`, `plum-wasm-codegen`'s expected-type wrapping) —
30
30
  # assigning/returning/passing a bare `Int`/`Float` value wherever `Number` is
31
31
  # expected just works. A method not found on `Int`/`Float` directly (they no
32
32
  # longer have their own method tables at all) falls back to `Number`'s,
plum-std/Str.plum CHANGED
@@ -146,10 +146,10 @@ enum Str(Comparable, ToStr, Readable, Writable) =
146
146
  fun toStr(self) -> Str =
147
147
  self
148
148
 
149
- fun matchPattern(self, pattern: Regex) -> List =
149
+ fun matchPattern(self, pattern: Regex) -> List[Str] =
150
150
  todo
151
151
 
152
- fun matchAll(self, pattern: Regex) -> List =
152
+ fun matchAll(self, pattern: Regex) -> List[Str] =
153
153
  todo
154
154
 
155
155
  # Pads the start of `self` with repeated copies of `sub` until its length is
@@ -193,7 +193,7 @@ enum Str(Comparable, ToStr, Readable, Writable) =
193
193
  # `strings.SplitN` semantics, not JS's (which instead discards anything
194
194
  # past the limit). An empty `separator` splits into individual bytes.
195
195
  fun split(self, separator: Str, limit: Int) -> List[Str] =
196
- result := List(head: End, tail: End, size: 0)
196
+ result := List[Str]()
197
197
  len := self.length()
198
198
  sep_len := separator.length()
199
199
  if sep_len == 0
plum-std/Testing.plum CHANGED
@@ -19,12 +19,7 @@ import std/Str
19
19
  #
20
20
  # `expectArray`/`expectOk`/`expectErr` below take a container
21
21
  # type (`List[T]`/`Try[T, E]`) whose OWN generic param is
22
- # nested inside the param type — this used to hit a real monomorphizer bug
22
+ # nested inside the param type.
23
- # (misclassified as a "bare generic class reference" fn, then lost its type
24
- # argument when the specialized signature was registered, mismatching the
25
- # caller's concrete arg type; fixed in `plum-checker/src/monomorphize.rs`'s
26
- # `fnGenericParams`/`resolveFnInstantiation`, see the
27
- # `plum_generic_method_self_nesting_landmine`-adjacent compiler-bugs memory).
28
23
  # The underlying type's own method (`result.isOk()`, `listA.equals(listB)`)
29
24
  # still works too and reads just as well — use whichever fits the call site.
30
25
  #
plum-std/Try.plum CHANGED
@@ -80,8 +80,8 @@ enum Try[T, E] =
80
80
  Err(e) =>
81
81
  return Err(e)
82
82
 
83
- # No implicit `toStr`: every method of a generic type is compiled for every
83
+ # No implicit `toStr`: `T`/`E` carry no `ToStr` bound (a `Try` may hold any
84
- # specialization, so assuming `T`/`E: ToStr` would break unrelated uses.
84
+ # type), so the caller supplies the renderers.
85
85
  fun toTryStr(self, okToStr: fn(T) -> Str, errToStr: fn(E) -> Str) -> Str =
86
86
  match self
87
87
  Ok(v) =>
plum-wasm-codegen/src/lib.rs CHANGED
@@ -1475,8 +1475,30 @@ fn inferFallbackType(expr: &ast::Expr, ctx: &LocalCtx) -> PlumType {
1475
1475
  plum_checker::inferExpr(expr, &env, &cctx).unwrap_or(PlumType::TInt)
1476
1476
  }
1477
1477
 
1478
+ /// Type-checks `source` once, returning the per-expression types codegen needs
1479
+ /// (`plum_checker::typedSource`) or the joined `type error: ...` lines.
1480
+ pub fn typeCheck(source: &ast::Source) -> Result<plum_checker::TypedInfo, String> {
1481
+ plum_checker::typedSource(source).map_err(|errs| {
1482
+ errs.iter()
1483
+ .map(|e| format!("type error: {}", e.message))
1484
+ .collect::<Vec<_>>()
1485
+ .join("\n")
1486
+ })
1487
+ }
1488
+
1489
+ /// Compiles a source already checked by `plum_checker::typedSource(source)`
1490
+ /// (the `TypedInfo` is keyed by expression address, so it must come from this
1491
+ /// very `source` value). Hosts that check once call this; `compileSource`
1492
+ /// checks and compiles in one step.
1493
+ pub fn compileTyped(
1494
+ source: &ast::Source,
1495
+ typed: plum_checker::TypedInfo,
1496
+ ) -> Result<Vec<u8>, String> {
1497
+ compileSourceInner(source, &[], typed).map(|(bytes, _)| bytes)
1498
+ }
1499
+
1478
1500
  pub fn compileSource(source: &ast::Source) -> Result<Vec<u8>, String> {
1479
- compileSourceInner(source, &[]).map(|(bytes, _)| bytes)
1501
+ compileTyped(source, typeCheck(source)?)
1480
1502
  }
1481
1503
 
1482
1504
  /// Like `compileSource`, also returning the key of every compiled function
@@ -1484,7 +1506,7 @@ pub fn compileSource(source: &ast::Source) -> Result<Vec<u8>, String> {
1484
1506
  /// generic function/method compiles exactly once however many types it is
1485
1507
  /// instantiated at.
1486
1508
  pub fn compileSourceWithFunctions(source: &ast::Source) -> Result<(Vec<u8>, Vec<String>), String> {
1487
- compileSourceInner(source, &[])
1509
+ compileSourceInner(source, &[], typeCheck(source)?)
1488
1510
  }
1489
1511
 
1490
1512
  /// Compiles `test "..." { ... }` blocks for `plum test`, stripped from every
@@ -1500,13 +1522,38 @@ pub fn compileSourceWithFunctions(source: &ast::Source) -> Result<(Vec<u8>, Vec<
1500
1522
  /// original display name, in the same order as its exported `"test#<name>"`
1501
1523
  /// function.
1502
1524
  pub fn compileTestSource(source: &ast::Source) -> Result<(Vec<u8>, Vec<String>), String> {
1525
+ let prog = desugarTests(source);
1526
+ let typed = typeCheck(&prog.source)?;
1527
+ compileTestTyped(&prog, typed)
1528
+ }
1529
+
1530
+ /// A source with its `test` blocks desugared into ordinary fns. Type-check
1531
+ /// `source` (not the original) and pass the result to `compileTestTyped`.
1532
+ pub struct TestProgram {
1533
+ pub source: ast::Source,
1534
+ extra_exports: Vec<(String, String)>,
1535
+ pub names: Vec<String>,
1536
+ }
1537
+
1538
+ pub fn desugarTests(source: &ast::Source) -> TestProgram {
1503
1539
  let (desugared, extra_exports) = desugarTestsToFns(source);
1504
1540
  let names = extra_exports
1505
1541
  .iter()
1506
1542
  .map(|(_, export_name)| export_name["test#".len()..].to_string())
1507
1543
  .collect();
1544
+ TestProgram {
1545
+ source: desugared,
1546
+ extra_exports,
1547
+ names,
1548
+ }
1549
+ }
1550
+
1551
+ pub fn compileTestTyped(
1552
+ prog: &TestProgram,
1553
+ typed: plum_checker::TypedInfo,
1554
+ ) -> Result<(Vec<u8>, Vec<String>), String> {
1508
- let (wasm, _) = compileSourceInner(&desugared, &extra_exports)?;
1555
+ let (wasm, _) = compileSourceInner(&prog.source, &prog.extra_exports, typed)?;
1509
- Ok((wasm, names))
1556
+ Ok((wasm, prog.names.clone()))
1510
1557
  }
1511
1558
 
1512
1559
  fn strLit(s: String) -> ast::Expr {
@@ -1675,17 +1722,12 @@ fn desugarAssertStmt(stmt: &ast::Stmt) -> ast::Stmt {
1675
1722
  fn compileSourceInner(
1676
1723
  source: &ast::Source,
1677
1724
  extra_exports: &[(String, String)],
1725
+ typed: plum_checker::TypedInfo,
1678
1726
  ) -> Result<(Vec<u8>, Vec<String>), String> {
1679
1727
  // The source is compiled as written (generics erased, not specialized): the
1680
1728
  // checker's per-expression instantiated types drive every box/unbox decision.
1681
- let typed = plum_checker::typedSource(source).map_err(|errs| {
1682
- errs.iter()
1683
- .map(|e| format!("type error: {}", e.message))
1684
- .collect::<Vec<_>>()
1685
- .join("\n")
1686
- })?;
1687
1729
  let types = typed.expr_types;
1688
- let (global_env, methods, enum_variants, enum_params) = plum_checker::buildTables(source, true);
1730
+ let (global_env, methods, enum_variants, enum_params) = plum_checker::buildGlobalTables(source);
1689
1731
  let min_required = plum_checker::buildMinRequiredArgs(source);
1690
1732
 
1691
1733
  // Intern every tag name the program's types mention, in sorted order.
@@ -2165,7 +2207,9 @@ fn compileSourceInner(
2165
2207
  for f in &fns {
2166
2208
  let body = match compileIntrinsicFnBody(f) {
2167
2209
  Some(body) => body,
2168
- None => compileFnBody(f, &ctx, &mut state)?,
2210
+ None => compileFnBody(f, &ctx, &mut state).map_err(|e| {
2211
+ format!("{e} (in '{}{}')", f.type_param.as_ref().map(|r| format!("{r}.")).unwrap_or_default(), f.name)
2212
+ })?,
2169
2213
  };
2170
2214
  compiled_bodies.push((fnKey(f), body));
2171
2215
  }
@@ -3049,6 +3093,55 @@ fn compileArrayIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
3049
3093
  Instruction::End.encode(&mut body);
3050
3094
  Some(body)
3051
3095
  }
3096
+ // resize(self, n: Int) -> Array[T] — a new array of length `n` holding
3097
+ // `self`'s first `min(len, n)` elements (the rest null). Lets `List`
3098
+ // grow by doubling instead of copying on every `push`.
3099
+ "resize" => {
3100
+ const SELF: u32 = 0;
3101
+ const N: u32 = 1;
3102
+ const OLD_LEN: u32 = 2;
3103
+ const NEW_LEN: u32 = 3;
3104
+ const NEW_ARR: u32 = 4;
3105
+ let array_ref = gcRef(array_type_idx);
3106
+ let mut body = Vec::new();
3107
+ body.extend(encodeLeb128U32(2));
3108
+ body.extend(encodeLeb128U32(2)); // old_len, new_len: i32
3109
+ ValType::I32.encode(&mut body);
3110
+ body.extend(encodeLeb128U32(1)); // new_arr: ref
3111
+ array_ref.encode(&mut body);
3112
+
3113
+ Instruction::LocalGet(SELF).encode(&mut body);
3114
+ Instruction::ArrayLen.encode(&mut body);
3115
+ Instruction::LocalSet(OLD_LEN).encode(&mut body);
3116
+ Instruction::LocalGet(N).encode(&mut body);
3117
+ Instruction::I32WrapI64.encode(&mut body);
3118
+ Instruction::LocalSet(NEW_LEN).encode(&mut body);
3119
+
3120
+ Instruction::LocalGet(NEW_LEN).encode(&mut body);
3121
+ Instruction::ArrayNewDefault(array_type_idx).encode(&mut body);
3122
+ Instruction::LocalSet(NEW_ARR).encode(&mut body);
3123
+
3124
+ // array.copy(new_arr, 0, self, 0, min(old_len, new_len))
3125
+ Instruction::LocalGet(NEW_ARR).encode(&mut body);
3126
+ Instruction::I32Const(0).encode(&mut body);
3127
+ Instruction::LocalGet(SELF).encode(&mut body);
3128
+ Instruction::I32Const(0).encode(&mut body);
3129
+ Instruction::LocalGet(OLD_LEN).encode(&mut body);
3130
+ Instruction::LocalGet(NEW_LEN).encode(&mut body);
3131
+ Instruction::LocalGet(OLD_LEN).encode(&mut body);
3132
+ Instruction::LocalGet(NEW_LEN).encode(&mut body);
3133
+ Instruction::I32LtU.encode(&mut body);
3134
+ Instruction::Select.encode(&mut body);
3135
+ Instruction::ArrayCopy {
3136
+ array_type_index_dst: array_type_idx,
3137
+ array_type_index_src: array_type_idx,
3138
+ }
3139
+ .encode(&mut body);
3140
+
3141
+ Instruction::LocalGet(NEW_ARR).encode(&mut body);
3142
+ Instruction::End.encode(&mut body);
3143
+ Some(body)
3144
+ }
3052
3145
  "get" => {
3053
3146
  let mut body = vec![0u8];
3054
3147
  Instruction::LocalGet(0).encode(&mut body); // self
@@ -6090,12 +6183,12 @@ fn compileExpr(
6090
6183
  .name
6091
6184
  .chars()
6092
6185
  .next()
6093
- .is_some_and(|c| c.is_uppercase() || c == plum_checker::TAG_MARKER)
6186
+ .is_some_and(|c| c.is_uppercase())
6094
6187
  && matches!(inferLocalType(expr, ctx), PlumType::TTags(_))
6095
6188
  {
6096
6189
  let payload = call.args.first().map(argExprOf);
6097
6190
  compileTagLiteral(
6098
- plum_checker::tagLiteralName(&call.name),
6191
+ &call.name,
6099
6192
  payload,
6100
6193
  body,
6101
6194
  ctx,
@@ -6159,7 +6252,7 @@ fn compileExpr(
6159
6252
  // A bare uppercase name the checker typed as a tag union is a tag
6160
6253
  // literal, even when a declared variant shares its name.
6161
6254
  Some(_) if matches!(inferLocalType(expr, ctx), PlumType::TTags(_)) => {
6162
- compileTagLiteral(plum_checker::tagLiteralName(n), None, body, ctx, state)?
6255
+ compileTagLiteral(n, None, body, ctx, state)?
6163
6256
  }
6164
6257
  Some(info) if info.field_types.is_empty() => {
6165
6258
  let global_idx = *ctx.singleton_globals.get(n)
@@ -6194,10 +6287,10 @@ fn compileExpr(
6194
6287
  None if n
6195
6288
  .chars()
6196
6289
  .next()
6197
- .is_some_and(|c| c.is_uppercase() || c == plum_checker::TAG_MARKER)
6290
+ .is_some_and(|c| c.is_uppercase())
6198
6291
  && matches!(inferLocalType(expr, ctx), PlumType::TTags(_)) =>
6199
6292
  {
6200
- compileTagLiteral(plum_checker::tagLiteralName(n), None, body, ctx, state)?
6293
+ compileTagLiteral(n, None, body, ctx, state)?
6201
6294
  }
6202
6295
  None => {
6203
6296
  return Err(format!(