plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
eb7b5d4
— Peter John
2026-10-26T10:10:01+05:30
test: permanent erased-generics probes; drop stale monomorphization comments
- plum-checker/src/types.rs +2 -2
- plum-checker/tests/checker_tests.rs +1 -1
- plum-core/src/ast.rs +3 -3
- plum-core/src/parser.rs +2 -2
- plum-examples/generics.plum +215 -0
plum-checker/src/types.rs
CHANGED
|
@@ -7,7 +7,7 @@ pub enum PlumType {
|
|
|
7
7
|
TBool,
|
|
8
8
|
TStr,
|
|
9
9
|
TByte,
|
|
10
|
-
/// `[]Byte` — a fixed builtin, not a
|
|
10
|
+
/// `[]Byte` — a fixed builtin, not a generic. The checker
|
|
11
11
|
/// only ever produces this for the exact source spelling `[]Byte`
|
|
12
12
|
/// (see `plumTypeFromName`); there is no general `[]T` for other `T`.
|
|
13
13
|
TByteSlice,
|
|
@@ -24,7 +24,7 @@ pub enum PlumType {
|
|
|
24
24
|
TVariadic(Box<PlumType>),
|
|
25
25
|
/// A structural tag-union type `[A, B(Str)]`: tag name -> optional payload type.
|
|
26
26
|
/// `Display` is the canonical string (sorted, no spaces) the parser also
|
|
27
|
-
/// produces for the type annotation, so
|
|
27
|
+
/// produces for the type annotation, so names round-trip.
|
|
28
28
|
TTags(BTreeMap<String, Option<Box<PlumType>>>),
|
|
29
29
|
/// A generic type applied to arguments, e.g. `List[Int]` -> `TApp("List", [TInt])`.
|
|
30
30
|
TApp(String, Vec<PlumType>),
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -1867,7 +1867,7 @@ fn repoRoot() -> std::path::PathBuf {
|
|
|
1867
1867
|
}
|
|
1868
1868
|
|
|
1869
1869
|
/// Acceptance: every std and example file checks with zero errors on the
|
|
1870
|
-
///
|
|
1870
|
+
/// source as written (generics are erased, never specialized).
|
|
1871
1871
|
#[test]
|
|
1872
1872
|
fn everyStdAndExampleChecksStructurally() {
|
|
1873
1873
|
let mut failures: Vec<String> = Vec::new();
|
plum-core/src/ast.rs
CHANGED
|
@@ -251,9 +251,9 @@ pub enum Stmt {
|
|
|
251
251
|
/// An `assert` statement's condition, plus enough about where it was
|
|
252
252
|
/// WRITTEN to report a failure the way a Ruby/Groovy spec runner would (a
|
|
253
253
|
/// source location and the literal expression text) — captured once at parse
|
|
254
|
-
/// time, before
|
|
254
|
+
/// time, before desugaring can rewrite `cond` into something
|
|
255
|
-
///
|
|
255
|
+
/// unrecognizable (a rewritten variable, a lowered call, ...). `line`/`column`
|
|
256
|
-
///
|
|
256
|
+
/// are 1-based (editor convention).
|
|
257
257
|
#[derive(Debug, Clone, PartialEq)]
|
|
258
258
|
pub struct Check {
|
|
259
259
|
pub cond: Expr,
|
plum-core/src/parser.rs
CHANGED
|
@@ -527,7 +527,7 @@ impl<'a> AstParser<'a> {
|
|
|
527
527
|
// A slice type (e.g. `[]Byte`) is flattened to a single reserved name
|
|
528
528
|
// `"[]" + element_name` rather than a real generics list — the checker
|
|
529
529
|
// treats it as a fixed builtin (only `[]Byte` is accepted), not a
|
|
530
|
-
//
|
|
530
|
+
// generic, so there are no type args to carry.
|
|
531
531
|
if let Some(element) = node.child_by_field_name("element") {
|
|
532
532
|
let elem = self.parseType(element);
|
|
533
533
|
return Type {
|
|
@@ -1163,7 +1163,7 @@ impl<'a> AstParser<'a> {
|
|
|
1163
1163
|
/// Desugars entirely here into `object.map(|v| v.member(...))` — reusing
|
|
1164
1164
|
/// Try's existing generic `.map` method for the actual
|
|
1165
1165
|
/// unwrap/transform/rewrap, so nothing past the parser (checker,
|
|
1166
|
-
///
|
|
1166
|
+
/// codegen, formatters) needs to know `?.` exists at all.
|
|
1167
1167
|
/// The synthesized param name can't collide with a real variable in scope:
|
|
1168
1168
|
/// the closure body only ever references this one param, never anything
|
|
1169
1169
|
/// from the surrounding scope, so shadowing an outer name of the same
|
plum-examples/generics.plum
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
module generics
|
|
2
|
+
import std/Str
|
|
3
|
+
import std/Number
|
|
4
|
+
import std/Bool
|
|
5
|
+
import std/List
|
|
6
|
+
import std/Map
|
|
7
|
+
import std/Try
|
|
8
|
+
|
|
9
|
+
# Generics are erased: a `T` is a nullable object reference at run time, and
|
|
10
|
+
# Int / Float values are boxed when they cross a generic edge (a `T` param,
|
|
11
|
+
# a List slot, a Map value, a Try payload) and unboxed on the way out. These
|
|
12
|
+
# tests push awkward values through every such edge and check they come back
|
|
13
|
+
# bit-for-bit.
|
|
14
|
+
|
|
15
|
+
fun id(x: T) -> T =
|
|
16
|
+
x
|
|
17
|
+
|
|
18
|
+
fun same(a: T, b: T) -> Bool =
|
|
19
|
+
a == b
|
|
20
|
+
|
|
21
|
+
fun wrap(x: T) -> Try[T, [A]] =
|
|
22
|
+
Ok(x)
|
|
23
|
+
|
|
24
|
+
fun viaQ(x: T) -> Try[T, [A]] =
|
|
25
|
+
y := wrap(x)?
|
|
26
|
+
return Ok(y)
|
|
27
|
+
|
|
28
|
+
fun failing() -> Try[Int, [A]] =
|
|
29
|
+
Err(A)
|
|
30
|
+
|
|
31
|
+
fun viaQErr() -> Try[Int, [A]] =
|
|
32
|
+
y := failing()?
|
|
33
|
+
return Ok(y)
|
|
34
|
+
|
|
35
|
+
fun pick(x: T, f: fn(T) -> T) -> T =
|
|
36
|
+
f(x)
|
|
37
|
+
|
|
38
|
+
fun twice(f: fn(T) -> T, x: T) -> T =
|
|
39
|
+
f(f(x))
|
|
40
|
+
|
|
41
|
+
fun readDeep(outer: List[List[Map[Str, Try[Int, [A]]]]], r: Int, c: Int, key: Str) -> Try[Int, [OutOfBounds, KeyNotFound, A]] =
|
|
42
|
+
row := outer.get(r)?
|
|
43
|
+
m := row.get(c)?
|
|
44
|
+
cell := m.get(key)?
|
|
45
|
+
v := cell?
|
|
46
|
+
return Ok(v)
|
|
47
|
+
|
|
48
|
+
trait Shape =
|
|
49
|
+
area() -> Float
|
|
50
|
+
name() -> Str
|
|
51
|
+
|
|
52
|
+
enum Circle(Shape) =
|
|
53
|
+
| Circle(r: Float)
|
|
54
|
+
|
|
55
|
+
fun area(self) -> Float =
|
|
56
|
+
self.r * self.r * 3.0
|
|
57
|
+
|
|
58
|
+
fun name(self) -> Str =
|
|
59
|
+
"circle"
|
|
60
|
+
|
|
61
|
+
enum Square(Shape) =
|
|
62
|
+
| Square(s: Float)
|
|
63
|
+
|
|
64
|
+
fun area(self) -> Float =
|
|
65
|
+
self.s * self.s
|
|
66
|
+
|
|
67
|
+
fun name(self) -> Str =
|
|
68
|
+
"square"
|
|
69
|
+
|
|
70
|
+
fun nameOf(s: Shape) -> Str =
|
|
71
|
+
s.name()
|
|
72
|
+
|
|
73
|
+
# --- numeric -------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
test "Int extremes survive a generic fn"
|
|
76
|
+
big := 9223372036854775807
|
|
77
|
+
small := 0 - 9223372036854775807 - 1
|
|
78
|
+
assert id(big) == big
|
|
79
|
+
assert id(small) == small
|
|
80
|
+
assert id(9007199254740993) == 9007199254740993
|
|
81
|
+
assert same(big, big)
|
|
82
|
+
assert !same(big, small)
|
|
83
|
+
|
|
84
|
+
test "Float specials survive a generic fn"
|
|
85
|
+
nan := 0.0 / 0.0
|
|
86
|
+
assert id(nan) != id(nan)
|
|
87
|
+
assert id(nan) != 0.0
|
|
88
|
+
negZero := id(-0.0)
|
|
89
|
+
assert 1.0 / negZero < 0.0
|
|
90
|
+
posZero := id(0.0)
|
|
91
|
+
assert 1.0 / posZero > 0.0
|
|
92
|
+
assert id(5e-324) > 0.0
|
|
93
|
+
assert id(5e-324) / 2.0 == 0.0
|
|
94
|
+
assert id(1.5) == 1.5
|
|
95
|
+
|
|
96
|
+
test "Int and Float extremes survive List push/get"
|
|
97
|
+
ints := List[Int]()
|
|
98
|
+
ints.push(9223372036854775807)
|
|
99
|
+
ints.push(0 - 9223372036854775807 - 1)
|
|
100
|
+
ints.push(9007199254740993)
|
|
101
|
+
assert {ints.get(0) ?? 0} == 9223372036854775807
|
|
102
|
+
assert {ints.get(1) ?? 0} == 0 - 9223372036854775807 - 1
|
|
103
|
+
assert {ints.get(2) ?? 0} == 9007199254740993
|
|
104
|
+
floats := List[Float]()
|
|
105
|
+
floats.push(0.0 / 0.0)
|
|
106
|
+
floats.push(-0.0)
|
|
107
|
+
floats.push(5e-324)
|
|
108
|
+
nan := {floats.get(0) ?? 0.0}
|
|
109
|
+
assert nan != nan
|
|
110
|
+
assert 1.0 / {floats.get(1) ?? 1.0} < 0.0
|
|
111
|
+
assert {floats.get(2) ?? 0.0} == 5e-324
|
|
112
|
+
|
|
113
|
+
test "Int and Float extremes survive a Map[Str, V] value"
|
|
114
|
+
mi := Map[Str, Int]()
|
|
115
|
+
mi.set("big", 9223372036854775807)
|
|
116
|
+
mi.set("small", 0 - 9223372036854775807 - 1)
|
|
117
|
+
mi.set("odd", 9007199254740993)
|
|
118
|
+
assert {mi.get("big") ?? 0} == 9223372036854775807
|
|
119
|
+
assert {mi.get("small") ?? 0} == 0 - 9223372036854775807 - 1
|
|
120
|
+
assert {mi.get("odd") ?? 0} == 9007199254740993
|
|
121
|
+
mf := Map[Str, Float]()
|
|
122
|
+
mf.set("nan", 0.0 / 0.0)
|
|
123
|
+
mf.set("negzero", -0.0)
|
|
124
|
+
mf.set("tiny", 5e-324)
|
|
125
|
+
nan := {mf.get("nan") ?? 0.0}
|
|
126
|
+
assert nan != nan
|
|
127
|
+
assert 1.0 / {mf.get("negzero") ?? 1.0} < 0.0
|
|
128
|
+
assert {mf.get("tiny") ?? 0.0} == 5e-324
|
|
129
|
+
|
|
130
|
+
test "Int extremes survive a Try payload with ? and ??"
|
|
131
|
+
assert {wrap(9223372036854775807) ?? 0} == 9223372036854775807
|
|
132
|
+
assert {viaQ(0 - 9223372036854775807 - 1) ?? 0} == 0 - 9223372036854775807 - 1
|
|
133
|
+
assert {viaQ(9007199254740993) ?? 0} == 9007199254740993
|
|
134
|
+
assert {viaQErr() ?? 42} == 42
|
|
135
|
+
|
|
136
|
+
test "Float specials survive a Try payload with ? and ??"
|
|
137
|
+
nan := {viaQ(0.0 / 0.0) ?? 0.0}
|
|
138
|
+
assert nan != nan
|
|
139
|
+
assert 1.0 / {viaQ(-0.0) ?? 1.0} < 0.0
|
|
140
|
+
assert {viaQ(5e-324) ?? 0.0} == 5e-324
|
|
141
|
+
assert {wrap(5e-324) ?? 0.0} == 5e-324
|
|
142
|
+
|
|
143
|
+
# --- deep ----------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
test "List[List[Map[Str, Try[Int, [A]]]]] builds and reads back"
|
|
146
|
+
m1 := Map[Str, Try[Int, [A]]]()
|
|
147
|
+
m1.set("ok", Ok(7))
|
|
148
|
+
m1.set("err", Err(A))
|
|
149
|
+
m2 := Map[Str, Try[Int, [A]]]()
|
|
150
|
+
m2.set("big", Ok(9223372036854775807))
|
|
151
|
+
inner1 := List[Map[Str, Try[Int, [A]]]]()
|
|
152
|
+
inner1.push(m1)
|
|
153
|
+
inner1.push(m2)
|
|
154
|
+
inner2 := List[Map[Str, Try[Int, [A]]]]()
|
|
155
|
+
inner2.push(m2)
|
|
156
|
+
outer := List[List[Map[Str, Try[Int, [A]]]]]()
|
|
157
|
+
outer.push(inner1)
|
|
158
|
+
outer.push(inner2)
|
|
159
|
+
assert outer.length() == 2
|
|
160
|
+
assert {outer.get(0) ?? inner2}.length() == 2
|
|
161
|
+
assert {readDeep(outer, 0, 0, "ok") ?? 0 - 1} == 7
|
|
162
|
+
assert {readDeep(outer, 0, 0, "err") ?? 99} == 99
|
|
163
|
+
assert {readDeep(outer, 0, 0, "nokey") ?? 98} == 98
|
|
164
|
+
assert {readDeep(outer, 0, 1, "big") ?? 0} == 9223372036854775807
|
|
165
|
+
assert {readDeep(outer, 1, 0, "big") ?? 0} == 9223372036854775807
|
|
166
|
+
assert {readDeep(outer, 2, 0, "big") ?? 97} == 97
|
|
167
|
+
|
|
168
|
+
test "a method's own generic is chosen per call on a generic receiver"
|
|
169
|
+
ints := List(1, 22, 333)
|
|
170
|
+
strs := ints.map(|n| "n{n}")
|
|
171
|
+
assert strs.join(",") == "n1,n22,n333"
|
|
172
|
+
lens := strs.map(|s| s.length())
|
|
173
|
+
assert lens.join(",") == "2,3,4"
|
|
174
|
+
back := lens.map(|n| n * 10)
|
|
175
|
+
assert back.reduce(0, |a, b| a + b) == 90
|
|
176
|
+
|
|
177
|
+
test "closures nested three deep capture Int and Str locals through generic params"
|
|
178
|
+
k := 5
|
|
179
|
+
s := "z"
|
|
180
|
+
r := pick(10, |x| pick(x, |y| pick(y, |z| z + k)))
|
|
181
|
+
assert r == 15
|
|
182
|
+
t := pick("a", |x| pick(x, |y| pick(y, |z| z + s)))
|
|
183
|
+
assert t == "az"
|
|
184
|
+
both := pick(1, |x| pick("q", |y| y + s).length() + x + k)
|
|
185
|
+
assert both == 8
|
|
186
|
+
assert twice(|x| twice(|y| y * 2, x) + k, 1) == 41
|
|
187
|
+
|
|
188
|
+
# --- state ---------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
test "dyn dispatch through a trait-typed value and a List[Shape]"
|
|
191
|
+
assert nameOf(Circle(r: 1.0)) == "circle"
|
|
192
|
+
assert nameOf(Square(s: 1.0)) == "square"
|
|
193
|
+
shapes := List[Shape]()
|
|
194
|
+
shapes.push(Circle(r: 2.0))
|
|
195
|
+
shapes.push(Square(s: 3.0))
|
|
196
|
+
assert shapes.map(|s| s.name()).join(",") == "circle,square"
|
|
197
|
+
assert shapes.reduce(0.0, |acc, s| acc + s.area()) == 21.0
|
|
198
|
+
|
|
199
|
+
test "one generic fn serves Int, Str and a class in the same program"
|
|
200
|
+
assert id(3) == 3
|
|
201
|
+
assert id("hi") == "hi"
|
|
202
|
+
sq := id(Square(s: 2.0))
|
|
203
|
+
assert sq.area() == 4.0
|
|
204
|
+
c := id(Circle(r: 1.0))
|
|
205
|
+
assert c.name() == "circle"
|
|
206
|
+
assert id(8) + 1 == 9
|
|
207
|
+
assert id("a") + "b" == "ab"
|
|
208
|
+
|
|
209
|
+
test "== on T works for Int and Str"
|
|
210
|
+
assert same(2, 2)
|
|
211
|
+
assert !same(2, 3)
|
|
212
|
+
assert same("a", "a")
|
|
213
|
+
assert !same("a", "b")
|
|
214
|
+
assert same(9007199254740993, 9007199254740993)
|
|
215
|
+
assert !same(9007199254740993, 9007199254740992)
|