plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
8de13fd
— Peter John
2026-09-04T15:54:32+05:30
feat(plum-core): enforce imports-first, tests-grouped-last file ordering
- examples/closures.plum +39 -39
- examples/control_flow.plum +15 -15
- examples/functions.plum +31 -31
- examples/match.plum +57 -57
- examples/types.plum +31 -31
- libs/std/list.plum +18 -18
- plum-checker/src/monomorphize.rs +6 -1
- plum-core/src/ast.rs +48 -0
- plum-core/src/loader.rs +6 -1
- plum-core/src/parser.rs +32 -8
- plum-wasm-codegen/src/lib.rs +9 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|