plum

#treesitter#compiler#wasm

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

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


77038a2 — Peter John 2026-11-01T20:41:40+05:30
T7: pipeline wiring + e2e
plum-checker/src/lib.rs CHANGED
@@ -732,7 +732,7 @@ fn unifyErr(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
732
732
  }
733
733
  Ok(())
734
734
  }
735
- (Some((et, _)), None) => unify(&et, t2, ctx)
735
+ (Some((et, _)), None) => unions::atTop(|| unify(&et, t2, ctx))
736
736
  .map_err(|_| format!("type mismatch: expected {}, found {}", t1, t2)),
737
737
  _ => Err(format!(
738
738
  "type mismatch: expected {}, found {}: {}",
@@ -1146,7 +1146,7 @@ pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
1146
1146
 
1147
1147
  fn checkSourceFull(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
1148
1148
  if !source.formLoops {
1149
- return checkSourceOnce(source);
1149
+ return checkUnions(source, |s| checkSourceOnce(s));
1150
1150
  }
1151
1151
  // `for` loops are lowered by the checker, because how a loop iterates depends
1152
1152
  // on the type of its source. Pass 1 checks the program and lowers each loop as
@@ -1155,12 +1155,63 @@ fn checkSourceFull(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
1155
1155
  // address) and codegen compiles (`TypedInfo::lowered`).
1156
1156
  plum_core::forms::begin(source);
1157
1157
  let _ = checkSourceOnce(source);
1158
- let lowered = std::rc::Rc::new(plum_core::forms::finish(source));
1158
+ let lowered = plum_core::forms::finish(source);
1159
- let (result, mut info) = checkSourceOnce(&lowered);
1159
+ let (result, mut info) = checkUnions(&lowered, |s| checkSourceOnce(s));
1160
+ if info.lowered.is_none() {
1161
+ // No union rewrite happened: `lowered` itself was checked.
1160
- info.lowered = Some(lowered);
1162
+ info.lowered = Some(std::rc::Rc::new(lowered));
1163
+ }
1161
1164
  (result, info)
1162
1165
  }
1163
1166
 
1167
+ /// Whether the program declares a union enum (a hidden `Enum__Member` variant).
1168
+ fn hasUnions(source: &ast::Source) -> bool {
1169
+ source.items.iter().any(|i| match i {
1170
+ ast::Item::Enum(e) => e.variants.iter().any(|v| v.name.starts_with(&format!("{}__", e.name))),
1171
+ _ => false,
1172
+ })
1173
+ }
1174
+
1175
+ /// Checks `source` and, when the check recorded union uses, rewrites an owned
1176
+ /// copy of exactly the checked program (`plum_core::union_lower::applyPlan`),
1177
+ /// checks that again and returns it as `TypedInfo::lowered`. A program with no
1178
+ /// union enum is checked as is.
1179
+ fn checkUnions(
1180
+ source: &ast::Source,
1181
+ check: impl Fn(&ast::Source) -> (CheckResult<()>, TypedInfo),
1182
+ ) -> (CheckResult<()>, TypedInfo) {
1183
+ if !hasUnions(source) {
1184
+ return check(source);
1185
+ }
1186
+ // Addresses are recorded against the program that is checked, so check a
1187
+ // clone and mutate that same clone.
1188
+ let mut copy = source.clone();
1189
+ plum_core::unions::beginPlan();
1190
+ let (result, info) = check(&copy);
1191
+ let plan = plum_core::unions::takePlan();
1192
+ let empty = plan.wraps.is_empty()
1193
+ && plan.patterns.is_empty()
1194
+ && plan.narrowings.is_empty()
1195
+ && plan.dispatches.is_empty();
1196
+ let out = if result.is_err() {
1197
+ // Report errors against the original, whose addresses codegen never sees.
1198
+ (result, info)
1199
+ } else if empty {
1200
+ check(source)
1201
+ } else {
1202
+ plum_core::union_lower::applyPlan(&mut copy, &plan);
1203
+ plum_core::unions::beginPlan();
1204
+ let (result, mut info) = check(&copy);
1205
+ plum_core::unions::takePlan();
1206
+ info.lowered = Some(std::rc::Rc::new(copy));
1207
+ (result, info)
1208
+ };
1209
+ // Leave the plan of the checked program in the recorder, so a caller that
1210
+ // began one can inspect what the check found.
1211
+ unions::restorePlan(&plan);
1212
+ out
1213
+ }
1214
+
1164
1215
  fn checkSourceOnce(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
1165
1216
  RECORD.with(|r| *r.borrow_mut() = Some(HashMap::new()));
1166
1217
  RECORD_BINDS.with(|r| *r.borrow_mut() = Some(Binds::new()));
@@ -2460,6 +2511,8 @@ struct CoverCtor {
2460
2511
  fields: Vec<PlumType>,
2461
2512
  /// A union member: covered by `Name` or `Name(..)` with irrefutable sub-patterns.
2462
2513
  member: bool,
2514
+ /// The hidden variant's full name (`Shape__Circle`), which lowered patterns use.
2515
+ full: String,
2463
2516
  }
2464
2517
 
2465
2518
  static COVER_WILDCARD: ast::CasePattern = ast::CasePattern::Wildcard;
@@ -2496,6 +2549,7 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
2496
2549
  name: n.to_string(),
2497
2550
  fields: Vec::new(),
2498
2551
  member: false,
2552
+ full: n.to_string(),
2499
2553
  })
2500
2554
  .collect(),
2501
2555
  )),
@@ -2507,6 +2561,7 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
2507
2561
  name: n.clone(),
2508
2562
  fields: Vec::new(),
2509
2563
  member: false,
2564
+ full: n.clone(),
2510
2565
  })
2511
2566
  .collect(),
2512
2567
  )),
@@ -2524,9 +2579,10 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
2524
2579
  enum_name.to_string(),
2525
2580
  vs.into_iter()
2526
2581
  .map(|(k, i)| match unions::memberBase(k, i) {
2527
- Some(base) => CoverCtor { name: base, fields: Vec::new(), member: true },
2582
+ Some(base) => CoverCtor { name: base, fields: variantFields(i, ty), member: true, full: k.to_string() },
2528
2583
  None => CoverCtor {
2529
2584
  member: false,
2585
+ full: k.to_string(),
2530
2586
  name: k.to_string(),
2531
2587
  // A discriminant enum's field types are its shared
2532
2588
  // params, which patterns never destructure.
@@ -2584,6 +2640,16 @@ fn coverRows<'a>(
2584
2640
  if patCatchAll(r[0]) {
2585
2641
  row.extend(std::iter::repeat(&COVER_WILDCARD).take(c.fields.len()));
2586
2642
  } else if let Some((n, fs)) = patCtor(r[0]) {
2643
+ if c.member && n == c.full {
2644
+ // A lowered member pattern `Shape__Circle(inner)`.
2645
+ if fs.len() != 1 {
2646
+ continue;
2647
+ }
2648
+ row.extend(fs.iter());
2649
+ row.extend(r[1..].iter().copied());
2650
+ sub.push(row);
2651
+ continue;
2652
+ }
2587
2653
  if n != c.name {
2588
2654
  continue;
2589
2655
  }
@@ -2591,6 +2657,7 @@ fn coverRows<'a>(
2591
2657
  if !fs.iter().all(patCatchAll) {
2592
2658
  continue;
2593
2659
  }
2660
+ row.extend(std::iter::repeat(&COVER_WILDCARD).take(c.fields.len()));
2594
2661
  } else if let ast::CasePattern::Name(_) = r[0] {
2595
2662
  // Bare `Circle` against a payload variant: tag check only.
2596
2663
  row.extend(std::iter::repeat(&COVER_WILDCARD).take(c.fields.len()));
plum-examples/unions.plum ADDED
@@ -0,0 +1,118 @@
1
+ # Union enums: `enum Shape = Circle | Rectangle` lists existing types as members.
2
+ # A member converts to the enum where one is expected, a bare member name in a
3
+ # pattern narrows the subject, and a method every member defines is callable on
4
+ # the enum (auto-dispatch). Members can be structs, records, primitives
5
+ # (`Int | Float | Str`), generic instances (`List[Jv]`) or bare tags.
6
+ module unions
7
+ import std/Str
8
+ import std/Int
9
+ import std/Float
10
+ import std/List
11
+
12
+ struct Circle(r: Float) =
13
+ fun area(self) -> Float = self.r * self.r * 3.0
14
+ fun toStr(self) -> Str = "circle"
15
+
16
+ record Rectangle(w: Float, h: Float) =
17
+ fun area(self) -> Float = self.w * self.h
18
+ fun toStr(self) -> Str = "rect"
19
+
20
+ enum Shape = Circle | Rectangle
21
+
22
+ enum Round = Circle | Oval
23
+
24
+ record Oval(a: Float)
25
+
26
+ enum Tree[T] = Leaf | Node[T]
27
+
28
+ record Leaf(v: Int)
29
+
30
+ record Node[T](left: Tree[T], right: Tree[T])
31
+
32
+ enum Jv = JvNull | Int | Float | Str | List[Jv]
33
+
34
+ fun total(t: Tree[Int]) -> Int =
35
+ match t
36
+ Leaf(v) => v
37
+ Node(l, r) => total(l) + total(r)
38
+
39
+ fun leaf(v: Int) -> Tree[Int] = Leaf(v: v)
40
+
41
+ fun node(l: Tree[Int], r: Tree[Int]) -> Tree[Int] = Node[Int](left: l, right: r)
42
+
43
+ fun describe(j: Jv) -> Str =
44
+ match j
45
+ JvNull => "null"
46
+ Int => "int"
47
+ Float => "float"
48
+ Str => "str {j}"
49
+ List => "list"
50
+
51
+ fun areaOf(s: Shape) -> Float = s.area()
52
+
53
+ fun roundName(r: Round) -> Str =
54
+ match r
55
+ Circle => "circle"
56
+ Oval => "oval"
57
+
58
+ fun make(big: Bool) -> Shape =
59
+ if big
60
+ Rectangle(w: 10.0, h: 10.0)
61
+ else
62
+ Circle(r: 1.0)
63
+
64
+ test "struct_member_copy"
65
+ s := make(false)
66
+ if Circle := s
67
+ c := s
68
+ c.r = 5.0
69
+ assert c.r == 5.0
70
+ assert s.area() == 3.0
71
+ else
72
+ assert false
73
+
74
+ test "list_of_shapes"
75
+ xs := List[Shape]()
76
+ xs.push(Circle(r: 1.0))
77
+ xs.push(Rectangle(w: 2.0, h: 3.0))
78
+ sum := 0.0
79
+ for x := range xs
80
+ sum = sum + x.area()
81
+ assert sum == 9.0
82
+
83
+ test "push_converts"
84
+ xs := List[Shape]()
85
+ xs.push(Circle(r: 2.0))
86
+ xs.push(Rectangle(w: 1.0, h: 1.0))
87
+ assert xs.length() == 2
88
+
89
+ test "tree_generic"
90
+ assert total(node(leaf(1), node(leaf(2), leaf(3)))) == 6
91
+
92
+ test "json_like_primitives"
93
+ assert describe(JvNull) == "null"
94
+ assert describe(7) == "int"
95
+ assert describe(1.5) == "float"
96
+ assert describe("hi") == "str hi"
97
+ js := List[Jv]()
98
+ js.push(1.0)
99
+ js.push(7)
100
+ assert describe(js) == "list"
101
+
102
+ test "two_enums_one_type"
103
+ c := Circle(r: 2.0)
104
+ assert areaOf(c) == 12.0
105
+ assert roundName(c) == "circle"
106
+ assert roundName(Oval(a: 1.0)) == "oval"
107
+
108
+ test "if_narrow"
109
+ s := make(true)
110
+ if Circle := s
111
+ assert false
112
+ else if Rectangle := s
113
+ assert s.w == 10.0
114
+
115
+ test "auto_dispatch"
116
+ assert make(true).area() == 100.0
117
+ assert make(false).area() == 3.0
118
+ assert "{make(false)}" == "circle"
plum-wasm-codegen/tests/unions_e2e_test.rs ADDED
@@ -0,0 +1,189 @@
1
+ #![allow(non_snake_case)]
2
+ use plum_wasm_codegen::compileSource;
3
+
4
+ fn gcEngine() -> wasmtime::Engine {
5
+ let mut config = wasmtime::Config::new();
6
+ config.wasm_gc(true);
7
+ config.wasm_function_references(true);
8
+ wasmtime::Engine::new(&config).expect("engine with GC config should construct")
9
+ }
10
+
11
+ fn repoRoot() -> std::path::PathBuf {
12
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
13
+ }
14
+
15
+ fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
16
+ let dir = std::env::temp_dir().join(format!("plum_unions_{}_{}", name, std::process::id()));
17
+ std::fs::create_dir_all(&dir).unwrap();
18
+ let path = dir.join("main.plum");
19
+ std::fs::write(&path, src).unwrap();
20
+ let r = plum_core::loadAndMerge(&path, &repoRoot());
21
+ let _ = std::fs::remove_dir_all(&dir);
22
+ r.map_err(|e| format!("{e}"))
23
+ }
24
+
25
+ /// Compiles `src` through the real pipeline and runs its `main() -> Int`.
26
+ fn runInt(name: &str, src: &str) -> i64 {
27
+ let source = load(name, src).expect("load");
28
+ let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
29
+ wasmparser::validate(&bytes).expect("valid wasm");
30
+ let engine = gcEngine();
31
+ let module = wasmtime::Module::new(&engine, &bytes).unwrap();
32
+ let mut store = wasmtime::Store::new(&engine, ());
33
+ let mut linker = wasmtime::Linker::new(&engine);
34
+ linker.define_unknown_imports_as_default_values(&mut store, &module).unwrap();
35
+ let instance = linker.instantiate(&mut store, &module).unwrap();
36
+ let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
37
+ main.call(&mut store, ()).unwrap()
38
+ }
39
+
40
+ fn checkErrors(name: &str, src: &str) -> Vec<String> {
41
+ let source = load(name, src).expect("load");
42
+ match plum_checker::checkSource(&source) {
43
+ Ok(()) => Vec::new(),
44
+ Err(e) => e.into_iter().map(|x| x.message).collect(),
45
+ }
46
+ }
47
+
48
+ const DEFS: &str = "\
49
+ import std/Str
50
+ import std/Int
51
+ import std/Float
52
+ import std/List
53
+
54
+ struct Circle(r: Float) =
55
+ fun area(self) -> Float = self.r * self.r * 3.0
56
+
57
+ record Rectangle(w: Float, h: Float) =
58
+ fun area(self) -> Float = self.w * self.h
59
+
60
+ enum Shape = Circle | Rectangle
61
+
62
+ ";
63
+
64
+ fn withDefs(body: &str) -> String {
65
+ format!("{DEFS}{body}")
66
+ }
67
+
68
+ #[test]
69
+ fn list_of_shapes() {
70
+ let src = withDefs(
71
+ "\
72
+ fun main() -> Int =
73
+ xs := List[Shape]()
74
+ xs.push(Circle(r: 1.0))
75
+ xs.push(Rectangle(w: 2.0, h: 3.0))
76
+ sum := 0.0
77
+ for x := range xs
78
+ sum = sum + x.area()
79
+ Int(sum)
80
+ ",
81
+ );
82
+ assert_eq!(runInt("list", &src), 9);
83
+ }
84
+
85
+ #[test]
86
+ fn struct_member_copy() {
87
+ let src = withDefs(
88
+ "\
89
+ fun make() -> Shape = Circle(r: 1.0)
90
+
91
+ fun main() -> Int =
92
+ s := make()
93
+ r := 0.0
94
+ if Circle := s
95
+ c := s
96
+ c.r = 5.0
97
+ r = c.r + s.r
98
+ Int(r)
99
+ ",
100
+ );
101
+ assert_eq!(runInt("copy", &src), 6);
102
+ }
103
+
104
+ #[test]
105
+ fn narrow_and_dispatch() {
106
+ let src = withDefs(
107
+ "\
108
+ fun pick(big: Bool) -> Shape =
109
+ if big
110
+ Rectangle(w: 10.0, h: 10.0)
111
+ else
112
+ Circle(r: 1.0)
113
+
114
+ fun wide(s: Shape) -> Float =
115
+ match s
116
+ Circle => 0.0
117
+ Rectangle => s.w
118
+
119
+ fun main() -> Int =
120
+ a := pick(true)
121
+ b := pick(false)
122
+ Int(a.area() + b.area() + wide(a))
123
+ ",
124
+ );
125
+ assert_eq!(runInt("narrow", &src), 113);
126
+ }
127
+
128
+ #[test]
129
+ fn member_into_optional_and_error_unions() {
130
+ let src = withDefs(
131
+ "\
132
+ fun opt() -> Shape? =
133
+ Circle(r: 2.0)
134
+
135
+ fun bang() -> !Shape =
136
+ Rectangle(w: 2.0, h: 2.0)
137
+
138
+ fun typed() -> ParseError!Shape =
139
+ Circle(r: 1.0)
140
+
141
+ fun main() -> Int =
142
+ a := opt() ?: Rectangle(w: 0.0, h: 0.0)
143
+ b := bang() catch Rectangle(w: 0.0, h: 0.0)
144
+ c := typed() catch Rectangle(w: 0.0, h: 0.0)
145
+ Int(a.area() + b.area() + c.area())
146
+ ",
147
+ );
148
+ let src = src.replace("import std/List\n", "import std/List\n\nerror ParseError = Bad | Worse\n");
149
+ assert_eq!(runInt("optional", &src), 19);
150
+ }
151
+
152
+ #[test]
153
+ fn invariant_list_rejected() {
154
+ let src = withDefs(
155
+ "\
156
+ fun take(xs: List[Shape]) -> Int =
157
+ 1
158
+
159
+ fun go(cs: List[Circle]) -> Int =
160
+ take(cs)
161
+ ",
162
+ );
163
+ assert!(!checkErrors("invariant", &src).is_empty());
164
+ }
165
+
166
+ #[test]
167
+ fn variadic_dispatch_is_an_error() {
168
+ let src = "\
169
+ import std/Str
170
+ import std/Int
171
+ import std/Float
172
+
173
+ record Pa(v: Int) =
174
+ fun sum(self, xs: ...Int) -> Int = self.v
175
+
176
+ record Pb(v: Int) =
177
+ fun sum(self, xs: ...Int) -> Int = self.v
178
+
179
+ enum Pair = Pa | Pb
180
+
181
+ fun go(p: Pair) -> Int =
182
+ p.sum(1, 2)
183
+ ";
184
+ let errs = checkErrors("variadic", src);
185
+ assert!(
186
+ errs.iter().any(|e| e.contains("Pair.sum: auto-dispatch can't forward a variadic parameter; define sum on Pair")),
187
+ "{errs:?}"
188
+ );
189
+ }