plum

#treesitter#compiler#wasm

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 CHANGED
@@ -7,7 +7,7 @@ pub enum PlumType {
7
7
  TBool,
8
8
  TStr,
9
9
  TByte,
10
- /// `[]Byte` — a fixed builtin, not a monomorphized generic. The checker
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 mangled names round-trip.
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
- /// ORIGINAL (un-monomorphized) source.
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 monomorphization/desugaring can rewrite `cond` into something
254
+ /// time, before desugaring can rewrite `cond` into something
255
- /// mangled or unrecognizable (a specialized generic call's real name, a
255
+ /// unrecognizable (a rewritten variable, a lowered call, ...). `line`/`column`
256
- /// rewritten variable, ...). `line`/`column` are 1-based (editor convention).
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
- // monomorphized generic, so there's no template to carry args for.
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
- /// monomorphizer, codegen, formatters) needs to know `?.` exists at all.
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)