plum

#treesitter#compiler#wasm

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

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


8de13fdPeter John 2026-09-04T15:54:32+05:30
feat(plum-core): enforce imports-first, tests-grouped-last file ordering
examples/closures.plum CHANGED
@@ -10,23 +10,6 @@ fun useCapturingClosure() -> Int =
10
10
  fun main() -> Int =
11
11
  each(|v| v * 3) + useCapturingClosure()
12
12
 
13
- test "closures example computes correctly"
14
- assert main() == 120
15
-
16
- # ---- closure regression tests ----
17
-
18
- test "non capturing closure passed and called runs correctly"
19
- assert each(|v| v) == 5
20
-
21
- # Regression test: the external scanner used to never emit a dedent for a
22
- # multi-line closure body immediately followed by `)` on the same line as
23
- # the body's last statement, so this shape didn't parse at all before.
24
- test "multi line closure call argument with closing paren on last statement line runs correctly"
25
- multiLineResult = each(|v|
26
- bumped = v + 1
27
- bumped * 2)
28
- assert multiLineResult == 12
29
-
30
13
  fun useSnapshotClosure() -> Int =
31
14
  x = 10
32
15
  snapshotCb = |v|
@@ -34,12 +17,6 @@ fun useSnapshotClosure() -> Int =
34
17
  x = 999
35
18
  each(snapshotCb)
36
19
 
37
- test "capturing closure snapshots value at creation time runs correctly"
38
- # each calls its callback with 5, so a correct snapshot of x==10 (its value
39
- # when the closure was created, not 999 — its value when `each(cb)` is
40
- # actually called) gives 10 + 5 = 15.
41
- assert useSnapshotClosure() == 15
42
-
43
20
  fun eachF(cb: fn(Float) -> Float) -> Float =
44
21
  cb(0.0)
45
22
 
@@ -49,9 +26,6 @@ fun useFloatClosure() -> Float =
49
26
  offset + v
50
27
  eachF(floatCb)
51
28
 
52
- test "closure assigned then called at float type runs correctly"
53
- assert useFloatClosure() == 2.5
54
-
55
29
  type ClosureCat =
56
30
  age: Int
57
31
 
@@ -63,18 +37,12 @@ fun useClassClosure() -> Int =
63
37
  c.age
64
38
  eachCat(classCb)
65
39
 
66
- test "closure assigned then called at class type runs correctly"
67
- assert useClassClosure() == 7
68
-
69
40
  fun identity(value: T) -> T =
70
41
  value
71
42
 
72
43
  fun eachViaGeneric(cb: fn(Int) -> Int) -> Int =
73
44
  cb(identity(7))
74
45
 
75
- test "closure passed through already generic higher order function runs correctly"
76
- assert eachViaGeneric(|v| v * 2) == 14
77
-
78
46
  # `inner` (nested inside `outer`'s body) needs `offset` — a name from
79
47
  # `useNestedClosure`'s scope, two levels up from `inner` itself, and not
80
48
  # referenced by `outer` directly — exercising the multi-level capture chain:
@@ -87,6 +55,45 @@ fun useNestedClosure() -> Int =
87
55
  inner(v)
88
56
  each(outer)
89
57
 
58
+ fun doubleForClosureTest(x: Int) -> Int =
59
+ x * 2
60
+
61
+ fun classifySign(n: Int) -> Str =
62
+ cb = |v| v > 0 ? "pos" : "nonpos"
63
+ cb(n)
64
+
65
+ test "closures example computes correctly"
66
+ assert main() == 120
67
+
68
+ # ---- closure regression tests ----
69
+
70
+ test "non capturing closure passed and called runs correctly"
71
+ assert each(|v| v) == 5
72
+
73
+ # Regression test: the external scanner used to never emit a dedent for a
74
+ # multi-line closure body immediately followed by `)` on the same line as
75
+ # the body's last statement, so this shape didn't parse at all before.
76
+ test "multi line closure call argument with closing paren on last statement line runs correctly"
77
+ multiLineResult = each(|v|
78
+ bumped = v + 1
79
+ bumped * 2)
80
+ assert multiLineResult == 12
81
+
82
+ test "capturing closure snapshots value at creation time runs correctly"
83
+ # each calls its callback with 5, so a correct snapshot of x==10 (its value
84
+ # when the closure was created, not 999 — its value when `each(cb)` is
85
+ # actually called) gives 10 + 5 = 15.
86
+ assert useSnapshotClosure() == 15
87
+
88
+ test "closure assigned then called at float type runs correctly"
89
+ assert useFloatClosure() == 2.5
90
+
91
+ test "closure assigned then called at class type runs correctly"
92
+ assert useClassClosure() == 7
93
+
94
+ test "closure passed through already generic higher order function runs correctly"
95
+ assert eachViaGeneric(|v| v * 2) == 14
96
+
90
97
  test "nested closure literal runs correctly"
91
98
  assert useNestedClosure() == 105
92
99
 
@@ -94,9 +101,6 @@ test "nested closure literal passed directly as call argument runs correctly"
94
101
  assert each(|v|
95
102
  each(|w| w + v)) == 10
96
103
 
97
- fun doubleForClosureTest(x: Int) -> Int =
98
- x * 2
99
-
100
104
  test "named function used as a closure typed value runs correctly"
101
105
  assert each(doubleForClosureTest) == 10
102
106
 
@@ -110,10 +114,6 @@ test "named function used as a value alongside a closure at the same call type r
110
114
  # function type, since both flow through the exact same `cb(...)` call site).
111
115
  assert each(doubleForClosureTest) + each(|v| v + 1) == 16
112
116
 
113
- fun classifySign(n: Int) -> Str =
114
- cb = |v| v > 0 ? "pos" : "nonpos"
115
- cb(n)
116
-
117
117
  test "closure with an inline ternary body runs correctly"
118
118
  # A ternary as a closure's own inline body used to parse as an OUTER
119
119
  # ternary wrapping the whole closure instead (`(|v| v > 0) ? "pos" :
examples/control_flow.plum CHANGED
@@ -34,18 +34,12 @@ fun bindExample(n: Int) -> Int =
34
34
  match n
35
35
  x => x
36
36
 
37
- test "tail match without return runs correctly"
38
- assert bindExample(5) == 5
39
-
40
37
  fun abs(n: Int) -> Int =
41
38
  if n < 0
42
39
  -n
43
40
  else
44
41
  n
45
42
 
46
- test "tail if without return runs correctly"
47
- assert abs(-7) == 7
48
-
49
43
  fun classifySign(n: Int) -> Int =
50
44
  match n
51
45
  0 => 1
@@ -55,18 +49,12 @@ fun classifySign(n: Int) -> Int =
55
49
  else
56
50
  2
57
51
 
58
- test "tail if nested inside match arm without return runs correctly"
59
- assert classifySign(-5) == -1
60
-
61
52
  fun describeMagnitude(n: Int) -> Int =
62
53
  match n
63
54
  0 =>
64
55
  return 100
65
56
  x => x * 2
66
57
 
67
- test "tail match mixing return and bare expr arms runs correctly"
68
- assert describeMagnitude(21) == 42
69
-
70
58
  enum TailOption =
71
59
  | TailSome[Int]
72
60
  | TailNone
@@ -76,12 +64,24 @@ fun unwrapTailOr(o: TailOption, default: Int) -> Int =
76
64
  TailSome(v) => v
77
65
  TailNone => default
78
66
 
79
- test "tail enum match without return runs correctly"
80
- assert unwrapTailOr(TailSome(9), 0) == 9
81
-
82
67
  fun checkPositiveAndReturn(n: Int) -> Int =
83
68
  assert n > 0
84
69
  n
85
70
 
71
+ test "tail match without return runs correctly"
72
+ assert bindExample(5) == 5
73
+
74
+ test "tail if without return runs correctly"
75
+ assert abs(-7) == 7
76
+
77
+ test "tail if nested inside match arm without return runs correctly"
78
+ assert classifySign(-5) == -1
79
+
80
+ test "tail match mixing return and bare expr arms runs correctly"
81
+ assert describeMagnitude(21) == 42
82
+
83
+ test "tail enum match without return runs correctly"
84
+ assert unwrapTailOr(TailSome(9), 0) == 9
85
+
86
86
  test "assert passes through on true"
87
87
  assert checkPositiveAndReturn(5) == 5
examples/functions.plum CHANGED
@@ -32,9 +32,6 @@ fun factorial(x: Int) -> Int =
32
32
  return 1
33
33
  return x * factorial(x - 1)
34
34
 
35
- test "factorial runs correctly"
36
- assert factorial(5) == 120
37
-
38
35
  # Regression test: `double(...)` (an all-lowercase, no-uppercase, no-underscore
39
36
  # callee) used to fail to parse at all — `var_identifier` and `fn_identifier`
40
37
  # both matched its text and the grammar's lexer would nondeterministically
@@ -42,9 +39,6 @@ test "factorial runs correctly"
42
39
  fun double(n: Int) -> Int =
43
40
  n * 2
44
41
 
45
- test "lowercase single word function call runs correctly"
46
- assert double(21) == 42
47
-
48
42
  type GenericBox[T] =
49
43
  value: T
50
44
 
@@ -55,31 +49,18 @@ fun useGenericBox() -> Int =
55
49
  b := GenericBox(value: 7)
56
50
  b.getIntValue()
57
51
 
58
- test "generic class specialized at two types does not alias"
59
- assert useGenericBox() == 7
60
-
61
52
  fun identity(value: T) -> T =
62
53
  value
63
54
 
64
- test "generic function called at multiple concrete types runs correctly"
65
- assert identity(5) + identity(37) == 42
66
-
67
55
  type GenericBox2[T] =
68
56
  value: T
69
57
 
70
58
  fun getValue() -> Int =
71
59
  self.value
72
60
 
73
- test "generic method on generic class runs correctly"
74
- b := GenericBox2(value: 9)
75
- assert b.getValue() == 9
76
-
77
61
  fun doubled(value: T) -> Int =
78
62
  identity(value) + identity(value)
79
63
 
80
- test "transitively generic call chain runs correctly"
81
- assert doubled(21) == 42
82
-
83
64
  enum FnOption =
84
65
  | FnSome[T]
85
66
  | FnNone
@@ -89,9 +70,6 @@ fun unwrapFnOptionOr(o: FnOption, default: Int) -> Int =
89
70
  FnSome(v) => v
90
71
  FnNone => default
91
72
 
92
- test "generic enum specialized and matched runs correctly"
93
- assert unwrapFnOptionOr(FnSome(13), 0) == 13
94
-
95
73
  # `Str.length()` is not a real, working method — the point of this test is
96
74
  # that `FnOption$Str` coexists with `FnOption$Int` and both run correctly, not
97
75
  # string processing, so the Str arm returns a fixed literal.
@@ -105,12 +83,6 @@ fun unwrapFnOptionStrOr(o: FnOption, default: Int) -> Int =
105
83
  FnSome(v) => 4
106
84
  FnNone => default
107
85
 
108
- test "generic enum multiple instantiations coexist and run correctly"
109
- assert unwrapFnOptionIntOr(FnSome(13), 0) + unwrapFnOptionStrOr(FnSome("abcd"), 0) == 17
110
-
111
- test "same bare generic enum param function called multiple times runs correctly"
112
- assert unwrapFnOptionOr(FnSome(5), 0) + unwrapFnOptionOr(FnSome(37), 0) == 42
113
-
114
86
  type GenericBox3[T] =
115
87
  value: T
116
88
 
@@ -120,12 +92,40 @@ type GenericBox3[T] =
120
92
  fun sumBox(b: GenericBox3) -> Int =
121
93
  b.getBoxValue()
122
94
 
123
- test "ordinary function with bare generic class param runs correctly"
124
- assert sumBox(GenericBox3(value: 11)) == 11
125
-
126
95
  fun combineVariadic(prefix: Int, rest: ...Int) -> Int =
127
96
  prefix
128
97
 
98
+ test "factorial runs correctly"
99
+ assert factorial(5) == 120
100
+
101
+ test "lowercase single word function call runs correctly"
102
+ assert double(21) == 42
103
+
104
+ test "generic class specialized at two types does not alias"
105
+ assert useGenericBox() == 7
106
+
107
+ test "generic function called at multiple concrete types runs correctly"
108
+ assert identity(5) + identity(37) == 42
109
+
110
+ test "generic method on generic class runs correctly"
111
+ b := GenericBox2(value: 9)
112
+ assert b.getValue() == 9
113
+
114
+ test "transitively generic call chain runs correctly"
115
+ assert doubled(21) == 42
116
+
117
+ test "generic enum specialized and matched runs correctly"
118
+ assert unwrapFnOptionOr(FnSome(13), 0) == 13
119
+
120
+ test "generic enum multiple instantiations coexist and run correctly"
121
+ assert unwrapFnOptionIntOr(FnSome(13), 0) + unwrapFnOptionStrOr(FnSome("abcd"), 0) == 17
122
+
123
+ test "same bare generic enum param function called multiple times runs correctly"
124
+ assert unwrapFnOptionOr(FnSome(5), 0) + unwrapFnOptionOr(FnSome(37), 0) == 42
125
+
126
+ test "ordinary function with bare generic class param runs correctly"
127
+ assert sumBox(GenericBox3(value: 11)) == 11
128
+
129
129
  test "variadic call with varying trailing arg counts runs correctly"
130
130
  a := combineVariadic(10)
131
131
  b := combineVariadic(20, 1)
examples/match.plum CHANGED
@@ -36,9 +36,6 @@ fun describeOption(opt: Option) -> Int =
36
36
  fun main() -> Int =
37
37
  describeOption(Some(5))
38
38
 
39
- test "match example computes correctly"
40
- assert main() == 5
41
-
42
39
  # ---- pattern matching regression tests ----
43
40
 
44
41
  enum Nested =
@@ -51,12 +48,6 @@ fun unwrapNested(n: Nested) -> Int =
51
48
  Wrap(None) => -1
52
49
  Empty => 0
53
50
 
54
- test "nested constructor pattern matches and binds runs correctly"
55
- assert unwrapNested(Wrap(Some(5))) == 5
56
-
57
- test "nested constructor pattern mismatch falls through to next case runs correctly"
58
- assert unwrapNested(Wrap(None)) == -1
59
-
60
51
  enum GenericOption =
61
52
  | GSome[T]
62
53
  | GNone
@@ -71,9 +62,6 @@ fun unwrapGenericBox(b: GenericBox) -> Int =
71
62
  GFull(GNone) => -1
72
63
  GEmpty => 0
73
64
 
74
- test "nested constructor pattern against a specialized generic enum runs correctly"
75
- assert unwrapGenericBox(GFull(GSome(7))) == 7
76
-
77
65
  enum RecOption =
78
66
  | RSome[RecOption]
79
67
  | RNone
@@ -85,41 +73,23 @@ fun unwrapTwice(o: RecOption) -> Int =
85
73
  RNone => 3
86
74
  _ => 0
87
75
 
88
- test "doubly nested constructor pattern runs correctly"
89
- assert unwrapTwice(RSome(RSome(RNone))) == 1
90
-
91
76
  fun classifyForWildcardTest(a: Int) -> Int =
92
77
  match a
93
78
  1 => 100
94
79
  2 => 200
95
80
  _ => 0
96
81
 
97
- test "match int and wildcard run correctly"
98
- assert classifyForWildcardTest(2) == 200
99
-
100
- test "match bool variant pattern runs correctly"
101
- assert describeBool(False) == 0
102
-
103
82
  fun colorCode(c: Color) -> Int =
104
83
  match c
105
84
  Red => 1
106
85
  Green => 2
107
86
  Blue => 3
108
87
 
109
- test "non bool bare tag pattern runs correctly"
110
- assert colorCode(Green) == 2
111
-
112
88
  fun isSome(o: Option) -> Int =
113
89
  match o
114
90
  Some(_) => 1
115
91
  None => 0
116
92
 
117
- test "constructor pattern wildcard field runs correctly"
118
- assert isSome(Some(99)) == 1
119
-
120
- test "constructor pattern does not misfire on payload free sibling"
121
- assert describeOption(None) == 0
122
-
123
93
  # Mirrors libs/std/bool.plum's `and`/`or`: `match a, b` against two Bool
124
94
  # subjects, each case naming a tag pattern per position.
125
95
  fun and(a: Bool, b: Bool) -> Bool =
@@ -136,38 +106,23 @@ fun andOrCheck() -> Int =
136
106
  True, False => 1
137
107
  _, _ => 0
138
108
 
139
- test "multi subject match with enum tags runs correctly"
140
- assert andOrCheck() == 1
141
-
142
109
  fun classifyTwoInts(a: Int, b: Int) -> Int =
143
110
  match a, b
144
111
  1, 1 => 100
145
112
  1, 2 => 200
146
113
  _, _ => 0
147
114
 
148
- test "multi subject match falls through to next case when only first position matches"
149
- # The first case's position-0 pattern (`1`) matches, but position-1 (`1`)
150
- # doesn't (b is 2) — codegen must fall through to the *next case* (trying
151
- # its own position 0 again), not just "move on" within the first case.
152
- assert classifyTwoInts(1, 2) == 200
153
-
154
115
  fun combine(a: Int, b: Int) -> Int =
155
116
  match a, b
156
117
  0, y => y
157
118
  x, 0 => x
158
119
  x, y => x + y
159
120
 
160
- test "multi subject match with binding and wildcard runs correctly"
161
- assert combine(3, 4) == 7
162
-
163
121
  fun bothOptions(a: Option, b: Option) -> Int =
164
122
  match a, b
165
123
  Some(x), Some(y) => x + y
166
124
  _, _ => 0
167
125
 
168
- test "multi subject match with generic enum variant runs correctly"
169
- assert bothOptions(Some(3), Some(4)) == 7
170
-
171
126
  type Point =
172
127
  x: Int
173
128
  y: Int
@@ -182,14 +137,6 @@ type Point =
182
137
  Point(x, 0) => "on x axis"
183
138
  Point(_, _) => "elsewhere"
184
139
 
185
- test "match destructures a plain class the same way it destructures an enum variant"
186
- assert Point(x: 3, y: 4).sum() == 7
187
-
188
- test "match on a plain class supports literal/wildcard sub-patterns and case fallthrough"
189
- assert Point(x: 0, y: 0).classify() == "origin"
190
- assert Point(x: 5, y: 0).classify() == "on x axis"
191
- assert Point(x: 5, y: 5).classify() == "elsewhere"
192
-
193
140
  enum Shape =
194
141
  | Circle[Point]
195
142
  | Square[Point]
@@ -199,10 +146,6 @@ enum Shape =
199
146
  Circle(Point(x, y)) => x + y
200
147
  Square(Point(x, y)) => x * y
201
148
 
202
- test "a plain class nested inside an enum variant pattern destructures correctly"
203
- assert Circle(Point(x: 3, y: 4)).measure() == 7
204
- assert Square(Point(x: 3, y: 4)).measure() == 12
205
-
206
149
  # Named payload fields (`Ring(radius: Int)`) alongside the older
207
150
  # positional/generic-payload form (`Circle[Point]` above) — lets a
208
151
  # multi-field variant like `Rect` name each field instead of leaving them as
@@ -217,6 +160,63 @@ enum Figure =
217
160
  Ring(radius) => radius * radius
218
161
  Rect(w, h) => w * h
219
162
 
163
+ test "match example computes correctly"
164
+ assert main() == 5
165
+
166
+ test "nested constructor pattern matches and binds runs correctly"
167
+ assert unwrapNested(Wrap(Some(5))) == 5
168
+
169
+ test "nested constructor pattern mismatch falls through to next case runs correctly"
170
+ assert unwrapNested(Wrap(None)) == -1
171
+
172
+ test "nested constructor pattern against a specialized generic enum runs correctly"
173
+ assert unwrapGenericBox(GFull(GSome(7))) == 7
174
+
175
+ test "doubly nested constructor pattern runs correctly"
176
+ assert unwrapTwice(RSome(RSome(RNone))) == 1
177
+
178
+ test "match int and wildcard run correctly"
179
+ assert classifyForWildcardTest(2) == 200
180
+
181
+ test "match bool variant pattern runs correctly"
182
+ assert describeBool(False) == 0
183
+
184
+ test "non bool bare tag pattern runs correctly"
185
+ assert colorCode(Green) == 2
186
+
187
+ test "constructor pattern wildcard field runs correctly"
188
+ assert isSome(Some(99)) == 1
189
+
190
+ test "constructor pattern does not misfire on payload free sibling"
191
+ assert describeOption(None) == 0
192
+
193
+ test "multi subject match with enum tags runs correctly"
194
+ assert andOrCheck() == 1
195
+
196
+ test "multi subject match falls through to next case when only first position matches"
197
+ # The first case's position-0 pattern (`1`) matches, but position-1 (`1`)
198
+ # doesn't (b is 2) — codegen must fall through to the *next case* (trying
199
+ # its own position 0 again), not just "move on" within the first case.
200
+ assert classifyTwoInts(1, 2) == 200
201
+
202
+ test "multi subject match with binding and wildcard runs correctly"
203
+ assert combine(3, 4) == 7
204
+
205
+ test "multi subject match with generic enum variant runs correctly"
206
+ assert bothOptions(Some(3), Some(4)) == 7
207
+
208
+ test "match destructures a plain class the same way it destructures an enum variant"
209
+ assert Point(x: 3, y: 4).sum() == 7
210
+
211
+ test "match on a plain class supports literal/wildcard sub-patterns and case fallthrough"
212
+ assert Point(x: 0, y: 0).classify() == "origin"
213
+ assert Point(x: 5, y: 0).classify() == "on x axis"
214
+ assert Point(x: 5, y: 5).classify() == "elsewhere"
215
+
216
+ test "a plain class nested inside an enum variant pattern destructures correctly"
217
+ assert Circle(Point(x: 3, y: 4)).measure() == 7
218
+ assert Square(Point(x: 3, y: 4)).measure() == 12
219
+
220
220
  test "named-payload enum variant constructs via named args and destructures by position"
221
221
  assert Ring(radius: 5).area() == 25
222
222
  assert Rect(w: 3, h: 4).area() == 12
examples/types.plum CHANGED
@@ -39,10 +39,6 @@ type Cat =
39
39
  fun getAge() -> Int =
40
40
  self.age
41
41
 
42
- test "class field and method run correctly"
43
- c := Cat(name: "x", age: 7)
44
- assert c.getAge() == 7
45
-
46
42
  type Dog =
47
43
  name: Str
48
44
  age: Int
@@ -50,10 +46,6 @@ type Dog =
50
46
  fun getAge(self) -> Int =
51
47
  self.age
52
48
 
53
- test "nested method declaration runs correctly"
54
- d := Dog(name: "x", age: 7)
55
- assert d.getAge() == 7
56
-
57
49
  type Pair =
58
50
  a: Int
59
51
  b: Int
@@ -62,10 +54,6 @@ type Wrapper =
62
54
  inner: Pair
63
55
  tag: Int
64
56
 
65
- test "nested class call runs correctly"
66
- w := Wrapper(inner: Pair(a: 11, b: 22), tag: 99)
67
- assert w.inner.b == 22
68
-
69
57
  type LoopBox =
70
58
  v: Int
71
59
 
@@ -76,13 +64,6 @@ fun sumLoopBoxes() -> Int =
76
64
  total = total + b.v
77
65
  return total
78
66
 
79
- test "repeated class call in a loop does not alias"
80
- # Regression test: class instances are bump-allocated at *runtime* (via a
81
- # mutable wasm global), not at a compile-time-fixed address — otherwise
82
- # every iteration's `LoopBox(...)` would alias the same memory and this
83
- # would sum to 5*4=20 instead of 0+1+2+3+4=10.
84
- assert sumLoopBoxes() == 10
85
-
86
67
  enum Step(n: Int) =
87
68
  | ReadMin(10)
88
69
  | ReadMax(20)
@@ -95,12 +76,6 @@ fun stepToNumber(s: Step) -> Int =
95
76
  ReadMin => 1
96
77
  ReadMax => 2
97
78
 
98
- test "enum discriminant value field access runs correctly for each variant"
99
- assert ReadMin.toNumber() * 100 + ReadMax.toNumber() == 1020
100
-
101
- test "enum discriminant value matches by variant name correctly"
102
- assert stepToNumber(ReadMin) * 10 + stepToNumber(ReadMax) == 12
103
-
104
79
  fun unwrapOptionOr(o: Option, default: Int) -> Int =
105
80
  match o
106
81
  Some(v) =>
@@ -108,9 +83,6 @@ fun unwrapOptionOr(o: Option, default: Int) -> Int =
108
83
  None =>
109
84
  return default
110
85
 
111
- test "payload variant construction compiles and runs"
112
- assert unwrapOptionOr(Some(7), 0) == 7
113
-
114
86
  enum ShapeKind =
115
87
  | Rect[Int, Int]
116
88
  | Circle[Int]
@@ -122,9 +94,6 @@ fun area(s: ShapeKind) -> Int =
122
94
  Circle(r) =>
123
95
  return r * r
124
96
 
125
- test "multi field variant construction compiles and runs"
126
- assert area(Rect(3, 4)) == 12
127
-
128
97
  type OptionBox =
129
98
  value: Option
130
99
 
@@ -135,6 +104,37 @@ type OptionBox =
135
104
  None =>
136
105
  return default
137
106
 
107
+ test "class field and method run correctly"
108
+ c := Cat(name: "x", age: 7)
109
+ assert c.getAge() == 7
110
+
111
+ test "nested method declaration runs correctly"
112
+ d := Dog(name: "x", age: 7)
113
+ assert d.getAge() == 7
114
+
115
+ test "nested class call runs correctly"
116
+ w := Wrapper(inner: Pair(a: 11, b: 22), tag: 99)
117
+ assert w.inner.b == 22
118
+
119
+ test "repeated class call in a loop does not alias"
120
+ # Regression test: class instances are bump-allocated at *runtime* (via a
121
+ # mutable wasm global), not at a compile-time-fixed address — otherwise
122
+ # every iteration's `LoopBox(...)` would alias the same memory and this
123
+ # would sum to 5*4=20 instead of 0+1+2+3+4=10.
124
+ assert sumLoopBoxes() == 10
125
+
126
+ test "enum discriminant value field access runs correctly for each variant"
127
+ assert ReadMin.toNumber() * 100 + ReadMax.toNumber() == 1020
128
+
129
+ test "enum discriminant value matches by variant name correctly"
130
+ assert stepToNumber(ReadMin) * 10 + stepToNumber(ReadMax) == 12
131
+
132
+ test "payload variant construction compiles and runs"
133
+ assert unwrapOptionOr(Some(7), 0) == 7
134
+
135
+ test "multi field variant construction compiles and runs"
136
+ assert area(Rect(3, 4)) == 12
137
+
138
138
  test "enum class field construct and destructure runs correctly"
139
139
  b := OptionBox(value: Some(42))
140
140
  assert b.unwrap(0) == 42
libs/std/list.plum CHANGED
@@ -496,16 +496,6 @@ fun exerciseList() -> Int =
496
496
  i = l.length()
497
497
  a + b + c + oldVal + d + e + f + g + h + i
498
498
 
499
- test "list add set remove at remove clear reverse all work correctly"
500
- # add(1,2,3,4,5): a=length=5, b=get(0)=1, c=get(4)=5
501
- # set(2,30): oldVal=3, d=get(2)=30 -> list [1,2,30,4,5]
502
- # removeAt(0): e=length=4, f=get(0)=2 -> list [2,30,4,5]
503
- # remove(30): g=length=3 -> list [2,4,5]
504
- # reverse(): h=get(0)=5 -> list [5,4,2]
505
- # clear(): i=length=0
506
- # 5+1+5+3+30+4+2+3+5+0 = 58
507
- assert exerciseList() == 58
508
-
509
499
  fun removeAllNodesOneAtATime() -> Int =
510
500
  l = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
511
501
  i = 0
@@ -518,24 +508,34 @@ fun removeAllNodesOneAtATime() -> Int =
518
508
  afterReAdd = optSumForListTest(l.get(0))
519
509
  afterLoopLength + isEmpty + afterReAdd
520
510
 
521
- test "removing every node in a loop leaves an empty correctly functioning list"
522
- # afterLoopLength=0, isEmpty(get(0) on empty list)=-1000, afterReAdd=42
523
- # 0 + -1000 + 42 = -958
524
- assert removeAllNodesOneAtATime() == -958
525
-
526
511
  fun exerciseListInit() -> Int =
527
512
  l := List[Int]()
528
513
  before := l.length()
529
514
  l.add(7)
530
515
  before + l.length() + optSumForListTest(l.get(0))
531
516
 
532
- test "List[Int]() with no args runs init() and produces a working empty list"
533
- assert exerciseListInit() == 8
534
-
535
517
  fun exerciseListInitWithValues() -> Int =
536
518
  l := List(10, 20, 30)
537
519
  l.length() + optSumForListTest(l.get(0)) + optSumForListTest(l.get(2))
538
520
 
521
+ test "list add set remove at remove clear reverse all work correctly"
522
+ # add(1,2,3,4,5): a=length=5, b=get(0)=1, c=get(4)=5
523
+ # set(2,30): oldVal=3, d=get(2)=30 -> list [1,2,30,4,5]
524
+ # removeAt(0): e=length=4, f=get(0)=2 -> list [2,30,4,5]
525
+ # remove(30): g=length=3 -> list [2,4,5]
526
+ # reverse(): h=get(0)=5 -> list [5,4,2]
527
+ # clear(): i=length=0
528
+ # 5+1+5+3+30+4+2+3+5+0 = 58
529
+ assert exerciseList() == 58
530
+
531
+ test "removing every node in a loop leaves an empty correctly functioning list"
532
+ # afterLoopLength=0, isEmpty(get(0) on empty list)=-1000, afterReAdd=42
533
+ # 0 + -1000 + 42 = -958
534
+ assert removeAllNodesOneAtATime() == -958
535
+
536
+ test "List[Int]() with no args runs init() and produces a working empty list"
537
+ assert exerciseListInit() == 8
538
+
539
539
  test "List(values) with positional args runs init(values) directly, infers T from args"
540
540
  assert exerciseListInitWithValues() == 43
541
541
 
plum-checker/src/monomorphize.rs CHANGED
@@ -2668,5 +2668,10 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2668
2668
  }
2669
2669
  }
2670
2670
 
2671
+ Ok(ast::Source {
2672
+ module: source.module.clone(),
2671
- Ok(ast::Source { module: source.module.clone(), imports: source.imports.clone(), items: m.produced })
2673
+ imports: source.imports.clone(),
2674
+ items: m.produced,
2675
+ topLevelOrder: Vec::new(),
2676
+ })
2672
2677
  }
plum-core/src/ast.rs CHANGED
@@ -3,6 +3,54 @@ pub struct Source {
3
3
  pub module: Option<Module>,
4
4
  pub imports: Vec<Import>,
5
5
  pub items: Vec<Item>,
6
+ /// Source-order tag for every top-level `import`/`item` node (the `module`
7
+ /// line is excluded), used to check that imports all come before every
8
+ /// other declaration and that `test` blocks form one contiguous group at
9
+ /// the end of the file.
10
+ pub topLevelOrder: Vec<TopLevelKind>,
11
+ }
12
+
13
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14
+ pub enum TopLevelKind {
15
+ Import,
16
+ Test,
17
+ Other,
18
+ }
19
+
20
+ impl Source {
21
+ /// Validates that every `import` appears before all other top-level
22
+ /// declarations, and that `test` blocks form one contiguous group at the
23
+ /// end of the file (nothing else follows the first `test`). Meant to run
24
+ /// per-file, before cross-file merging — a merged multi-file `Source`
25
+ /// (see `plum-core::loader::loadAndMerge`) has no single meaningful
26
+ /// order and isn't checked this way.
27
+ pub fn checkTopLevelOrder(&self) -> Result<(), String> {
28
+ let mut seenOther = false;
29
+ let mut seenTest = false;
30
+ for kind in &self.topLevelOrder {
31
+ match kind {
32
+ TopLevelKind::Import => {
33
+ if seenOther || seenTest {
34
+ return Err(
35
+ "import must appear before all other top-level declarations".to_string(),
36
+ );
37
+ }
38
+ }
39
+ TopLevelKind::Other => {
40
+ if seenTest {
41
+ return Err(
42
+ "test blocks must be grouped together at the end of the file, after every other declaration".to_string(),
43
+ );
44
+ }
45
+ seenOther = true;
46
+ }
47
+ TopLevelKind::Test => {
48
+ seenTest = true;
49
+ }
50
+ }
51
+ }
52
+ Ok(())
53
+ }
6
54
  }
7
55
 
8
56
  #[derive(Debug, Clone, PartialEq)]
plum-core/src/loader.rs CHANGED
@@ -31,6 +31,7 @@ pub fn loadAndMerge(entry: &Path, lib_path: &Path) -> Result<Source, String> {
31
31
  module: entry_source.module,
32
32
  imports: entry_source.imports,
33
33
  items,
34
+ topLevelOrder: Vec::new(),
34
35
  })
35
36
  }
36
37
 
@@ -88,7 +89,11 @@ fn parseFile(path: &Path) -> Result<Source, String> {
88
89
  ));
89
90
  }
90
91
  let ap = AstParser::new(&text);
91
- Ok(ap.parseSource(root))
92
+ let source = ap.parseSource(root);
93
+ source
94
+ .checkTopLevelOrder()
95
+ .map_err(|e| format!("{} in '{}'", e, path.display()))?;
96
+ Ok(source)
92
97
  }
93
98
 
94
99
  /// The first `ERROR` (or MISSING-token) node in `node`'s subtree, depth-first,
plum-core/src/parser.rs CHANGED
@@ -56,31 +56,55 @@ impl<'a> AstParser<'a> {
56
56
  let mut module = None;
57
57
  let mut imports = Vec::new();
58
58
  let mut items = Vec::new();
59
+ let mut topLevelOrder = Vec::new();
59
60
  let mut cursor = node.walk();
60
61
  for child in node.named_children(&mut cursor) {
61
62
  match child.kind() {
62
63
  "module" => module = Some(self.parseModule(child)),
64
+ "import" => {
63
- "import" => imports.push(self.parseImport(child)),
65
+ imports.push(self.parseImport(child));
66
+ topLevelOrder.push(TopLevelKind::Import);
67
+ }
64
68
  "class" => {
65
69
  let c = self.parseClass(child);
66
70
  let nested = self.collectNestedFns(child, &c.name);
67
71
  items.push(Item::Class(c));
72
+ topLevelOrder.push(TopLevelKind::Other);
73
+ for f in nested {
74
+ items.push(Item::Fn(f));
75
+ topLevelOrder.push(TopLevelKind::Other);
76
+ }
77
+ }
78
+ "trait" => {
68
- items.extend(nested.into_iter().map(Item::Fn));
79
+ items.push(Item::Trait(self.parseTrait(child)));
80
+ topLevelOrder.push(TopLevelKind::Other);
69
81
  }
70
- "trait" => items.push(Item::Trait(self.parseTrait(child))),
71
82
  "enum" => {
72
83
  let e = self.parseEnum(child);
73
84
  let nested = self.collectNestedFns(child, &e.name);
74
85
  items.push(Item::Enum(e));
86
+ topLevelOrder.push(TopLevelKind::Other);
87
+ for f in nested {
88
+ items.push(Item::Fn(f));
89
+ topLevelOrder.push(TopLevelKind::Other);
90
+ }
91
+ }
92
+ "fn" => {
93
+ items.push(Item::Fn(self.parseFn(child)));
94
+ topLevelOrder.push(TopLevelKind::Other);
95
+ }
96
+ "const" => {
97
+ items.push(Item::Const(self.parseConst(child)));
98
+ topLevelOrder.push(TopLevelKind::Other);
99
+ }
100
+ "test" => {
75
- items.extend(nested.into_iter().map(Item::Fn));
101
+ items.push(Item::Test(self.parseTest(child)));
102
+ topLevelOrder.push(TopLevelKind::Test);
76
103
  }
77
- "fn" => items.push(Item::Fn(self.parseFn(child))),
78
- "const" => items.push(Item::Const(self.parseConst(child))),
79
- "test" => items.push(Item::Test(self.parseTest(child))),
80
104
  _ => {}
81
105
  }
82
106
  }
83
- Source { module, imports, items }
107
+ Source { module, imports, items, topLevelOrder }
84
108
  }
85
109
 
86
110
  fn parseModule(&self, node: Node) -> Module {
plum-wasm-codegen/src/lib.rs CHANGED
@@ -996,7 +996,15 @@ fn desugarTestsToFns(source: &ast::Source) -> (ast::Source, Vec<(String, String)
996
996
  other => items.push(other.clone()),
997
997
  }
998
998
  }
999
+ (
1000
+ ast::Source {
1001
+ module: source.module.clone(),
999
- (ast::Source { module: source.module.clone(), imports: source.imports.clone(), items }, extra_exports)
1002
+ imports: source.imports.clone(),
1003
+ items,
1004
+ topLevelOrder: Vec::new(),
1005
+ },
1006
+ extra_exports,
1007
+ )
1000
1008
  }
1001
1009
 
1002
1010
  /// Recursively rewrites every `assert cond` (`cond` carrying its own literal