plum

#treesitter#compiler#wasm

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

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


72527fb — Peter John 2026-10-26T20:20:49+05:30
refactor: move range into std/Iter, delete std/Range
README.md CHANGED
@@ -118,7 +118,7 @@ From tightest to loosest binding:
118
118
  | | `\|\|` | |
119
119
  | lowest | `? :` | ternary |
120
120
 
121
- There's no range operator — `for i := range(0, n)` (`range` is a `fun*` from `import std/Range`) is the way to iterate a numeric range. `{ }` is used to group a sub-expression (not `( )`, which is reserved for calls):
121
+ There's no range operator — `for i := range(0, n)` (`range` is a `fun*` from `import std/Iter`) is the way to iterate a numeric range. `{ }` is used to group a sub-expression (not `( )`, which is reserved for calls):
122
122
 
123
123
  ```plum
124
124
  grouped = {1 + 2} * {3 - 1}
@@ -212,7 +212,7 @@ fun sumAll(nums: ...Int) -> Int = # variadic param
212
212
  ```plum
213
213
  for u := units # a list
214
214
  total = total + u.hp
215
- for i := range(0, 10) # a fun* from std/Range
215
+ for i := range(0, 10) # a fun* from std/Iter
216
216
  total = total + i
217
217
  for i, u := indexed(units) # a fun* from std/Iter: two binders
218
218
  printLn("{i}: {u.name}")
plum-examples/control_flow.plum CHANGED
@@ -1,6 +1,6 @@
1
1
  import std/Str
2
2
  import std/Number
3
- import std/Range
3
+ import std/Iter
4
4
 
5
5
  fun loopSum(limit: Int) -> Int =
6
6
  total := 0
plum-examples/loops.plum CHANGED
@@ -3,7 +3,7 @@ import std/Number
3
3
  import std/List
4
4
  import std/Array
5
5
  import std/Testing
6
- import std/Range
6
+ import std/Iter
7
7
 
8
8
  fun sumList(xs: List[Int]) -> Int =
9
9
  total := 0
plum-examples/typed_arrays.plum CHANGED
@@ -2,7 +2,7 @@ module typed_arrays
2
2
  import std/Number
3
3
  import std/Array
4
4
  import std/Grid
5
- import std/Range
5
+ import std/Iter
6
6
 
7
7
  # Tile-grid style data: Array[Int] / Array[Float] / Array[Bool] are native
8
8
  # unboxed wasm arrays (i64 / f64 / i32 elements), not boxed anyref arrays.
plum-examples/types.plum CHANGED
@@ -1,7 +1,7 @@
1
1
  import std/Try
2
2
  import std/Str
3
3
  import std/Number
4
- import std/Range
4
+ import std/Iter
5
5
 
6
6
  enum Point =
7
7
  | Point(x: Int, y: Int)
plum-std/Array.plum CHANGED
@@ -1,6 +1,5 @@
1
1
  module std
2
2
  import std/Number
3
- import std/Range
4
3
 
5
4
  # A fixed-length, O(1)-indexable array of `T`, backed directly by a wasm-gc
6
5
  # `array<anyref>` — `Array[T]` is erased, so every `T` shares the SAME
@@ -71,6 +70,8 @@ enum Array[T] =
71
70
  # in a native `array (mut i64)` / `(mut f64)` / `(mut i32)`.
72
71
  fun filled(n: Int, v: T) -> Array[T] =
73
72
  a := Array[T]().resize(n)
73
+ i := 0
74
- for i := range(0, n)
74
+ while i < n
75
75
  a.set(i, v)
76
+ i = i + 1
76
77
  return a
plum-std/Deque.plum CHANGED
@@ -3,7 +3,6 @@ import std/Array
3
3
  import std/List
4
4
  import std/Try
5
5
  import std/Number
6
- import std/Range
7
6
 
8
7
  # A double-ended queue: a ring buffer over `Array[T]`. `buf`'s length is the
9
8
  # capacity, `head` the index of the front element, `size` the number in use;
@@ -29,8 +28,10 @@ enum Deque[T] =
29
28
  return
30
29
  cap := self.buf.length()
31
30
  nb := Array[T]().resize(cap * 2)
31
+ i := 0
32
- for i := range(0, self.size)
32
+ while i < self.size
33
33
  nb.set(i, self.buf.get({self.head + i} % cap))
34
+ i = i + 1
34
35
  self.buf = nb
35
36
  self.head = 0
36
37
 
@@ -79,19 +80,23 @@ enum Deque[T] =
79
80
  test "deque_wraparound: head wraps past the end of the ring and growth unwraps it"
80
81
  d := Deque[Int]()
81
82
  # keep size small so head keeps advancing around the 8-slot ring
83
+ i := 0
82
- for i := range(0, 50)
84
+ while i < 50
83
85
  d.pushBack(i)
84
86
  d.pushBack(i + 1000)
85
87
  assert d.popFront().unwrap() == i
86
88
  assert d.popFront().unwrap() == i + 1000
89
+ i = i + 1
87
90
  assert d.isEmpty()
88
91
  assert d.head != 0 || d.buf.length() > 8
89
92
  # fill beyond capacity while head is mid-ring
90
93
  d.pushBack(1)
91
94
  d.pushBack(2)
92
95
  d.popFront()
96
+ i = 0
93
- for i := range(0, 20)
97
+ while i < 20
94
98
  d.pushBack(10 + i)
99
+ i = i + 1
95
100
  assert d.length() == 21
96
101
  assert d.get(0).unwrap() == 2
97
102
  assert d.get(20).unwrap() == 29
@@ -99,8 +104,10 @@ test "deque_wraparound: head wraps past the end of the ring and growth unwraps i
99
104
 
100
105
  test "pushFront builds the front in reverse order"
101
106
  d := Deque[Int]()
107
+ i := 0
102
- for i := range(0, 12)
108
+ while i < 12
103
109
  d.pushFront(i)
110
+ i = i + 1
104
111
  assert d.get(0).unwrap() == 11
105
112
  assert d.get(11).unwrap() == 0
106
113
  assert d.popFront().unwrap() == 11
plum-std/Heap.plum CHANGED
@@ -3,7 +3,6 @@ import std/Array
3
3
  import std/List
4
4
  import std/Try
5
5
  import std/Number
6
- import std/Range
7
6
 
8
7
  # A binary MIN-heap ordered by a key function: `pop` returns the element whose
9
8
  # `key(x)` is smallest. Ordering by an explicit `fn(T) -> Float` (rather than a
@@ -81,15 +80,19 @@ enum Heap[T] =
81
80
  test "heap_pop_order: 1000 pseudo-random keys pop in ascending order"
82
81
  h := Heap[Int](items: List[Int](), key: |x| Float(x))
83
82
  x := 12345
83
+ i := 0
84
- for i := range(0, 1000)
84
+ while i < 1000
85
85
  x = {x * 1103515245 + 12345} % 2147483648
86
86
  h.push(x % 100000)
87
+ i = i + 1
87
88
  assert h.length() == 1000
88
89
  prev := -1
90
+ i = 0
89
- for i := range(0, 1000)
91
+ while i < 1000
90
92
  v := h.pop().unwrap()
91
93
  assert v >= prev
92
94
  prev = v
95
+ i = i + 1
93
96
  assert h.isEmpty()
94
97
  assert h.pop().isErr()
95
98
 
plum-std/Iter.plum CHANGED
@@ -3,7 +3,6 @@ module std
3
3
  import std/Number
4
4
  import std/List
5
5
  import std/Try
6
- import std/Range
7
6
 
8
7
  # Iterators: `for u := filter(units)` expands the `fun*`'s body at the call
9
8
  # site (no closure, no call_indirect), with the `for` body in place of each
@@ -29,6 +28,15 @@ import std/Range
29
28
  # may only be passed on to `yield`. `zip`, `reversed` and `chunks` need random
30
29
  # access, so they take a List.
31
30
 
31
+ # `for i := range(0, n)`: start, start + 1, ... end - 1. The counter is bumped
32
+ # before the `yield`, so a `continue` in the body still advances it.
33
+ fun* range(start: Int, end: Int) *Int =
34
+ i := start
35
+ while i < end
36
+ cur := i
37
+ i = i + 1
38
+ yield cur
39
+
32
40
  # The elements for which the body is true.
33
41
  fun* filter(src: *T) *T -> List[T] =
34
42
  out := List()
plum-std/List.plum CHANGED
@@ -5,7 +5,6 @@ import std/Array
5
5
  import std/Buffer
6
6
  import std/Number
7
7
  import std/Str
8
- import std/Range
9
8
 
10
9
  # A growable list: `data` is a backing `Array[T]` whose length is the list's
11
10
  # capacity, `size` the number of slots in use. Capacity doubles when `push`
@@ -55,9 +54,11 @@ enum List[T] =
55
54
  fun equals(self, other: List[T]) -> Bool =
56
55
  if self.length() != other.length()
57
56
  return False
57
+ i := 0
58
- for i := range(0, self.length())
58
+ while i < self.length()
59
59
  if self.data.get(i) != other.data.get(i)
60
60
  return False
61
+ i = i + 1
61
62
  return True
62
63
 
63
64
  # appends `v` to the end of the list, doubling the capacity when full
@@ -92,10 +93,12 @@ enum List[T] =
92
93
 
93
94
  # removes the first element equal to v from list
94
95
  fun remove(self, v: T) =
96
+ i := 0
95
- for i := range(0, self.size)
97
+ while i < self.size
96
98
  if self.data.get(i) == v
97
99
  self.removeAt(i)
98
100
  return
101
+ i = i + 1
99
102
 
100
103
  # removes all objects from this list
101
104
  fun clear(self) =
@@ -132,10 +135,12 @@ enum List[T] =
132
135
 
133
136
  # Returns the first element equal to `search`, or `Err(NotFound)`.
134
137
  fun find(self, search: T) -> T ! [NotFound] =
138
+ i := 0
135
- for i := range(0, self.size)
139
+ while i < self.size
136
140
  value := self.data.get(i)
137
141
  if value == search
138
142
  return Ok(value)
143
+ i = i + 1
139
144
  return Err(NotFound)
140
145
 
141
146
  fun contains(self, v: T) -> Bool =
@@ -143,15 +148,19 @@ enum List[T] =
143
148
 
144
149
  # calls f for each elem in the list
145
150
  fun each(self, cb: fn(T)) -> Unit =
151
+ i := 0
146
- for i := range(0, self.size)
152
+ while i < self.size
147
153
  cb(self.data.get(i))
154
+ i = i + 1
148
155
 
149
156
  # Returns a new list with each element transformed by `cb`. `U` is a
150
157
  # generic param of `map` itself, separate from `List[T]`'s own `T`.
151
158
  fun map(self, cb: fn(T) -> U) -> List[U] =
152
159
  result := List[U]()
160
+ i := 0
153
- for i := range(0, self.size)
161
+ while i < self.size
154
162
  result.push(cb(self.data.get(i)))
163
+ i = i + 1
155
164
  return result
156
165
 
157
166
  # returns a new list with each element flat-mapped
@@ -161,31 +170,39 @@ enum List[T] =
161
170
  # Returns a new list with only the elements `predicate` holds for.
162
171
  fun retain(self, predicate: fn(T) -> Bool) -> List[T] =
163
172
  result := List[T]()
173
+ i := 0
164
- for i := range(0, self.size)
174
+ while i < self.size
165
175
  value := self.data.get(i)
166
176
  if predicate(value)
167
177
  result.push(value)
178
+ i = i + 1
168
179
  return result
169
180
 
170
181
  # Returns a new list with the elements `predicate` holds for removed.
171
182
  fun reject(self, predicate: fn(T) -> Bool) -> List[T] =
172
183
  result := List[T]()
184
+ i := 0
173
- for i := range(0, self.size)
185
+ while i < self.size
174
186
  value := self.data.get(i)
175
187
  if !predicate(value)
176
188
  result.push(value)
189
+ i = i + 1
177
190
  return result
178
191
 
179
192
  fun any(self, predicate: fn(T) -> Bool) -> Bool =
193
+ i := 0
180
- for i := range(0, self.size)
194
+ while i < self.size
181
195
  if predicate(self.data.get(i))
182
196
  return True
197
+ i = i + 1
183
198
  return False
184
199
 
185
200
  fun every(self, predicate: fn(T) -> Bool) -> Bool =
201
+ i := 0
186
- for i := range(0, self.size)
202
+ while i < self.size
187
203
  if !predicate(self.data.get(i))
188
204
  return False
205
+ i = i + 1
189
206
  return True
190
207
 
191
208
  # Folds `self`'s elements into a single value, starting from `acc` and
@@ -193,8 +210,10 @@ enum List[T] =
193
210
  # `reduce`'s own generic param (the accumulator's type).
194
211
  fun reduce(self, acc: U, cb: fn(U, T) -> U) -> U =
195
212
  result := acc
213
+ i := 0
196
- for i := range(0, self.size)
214
+ while i < self.size
197
215
  result = cb(result, self.data.get(i))
216
+ i = i + 1
198
217
  return result
199
218
 
200
219
  # returns the first element in the list
@@ -284,10 +303,12 @@ enum List[T] =
284
303
 
285
304
  fun join(self, sep: Str = ",") -> Str =
286
305
  res := Buffer()
306
+ i := 0
287
- for i := range(0, self.size)
307
+ while i < self.size
288
308
  if i > 0
289
309
  res.write(sep)
290
310
  res.write(self.data.get(i).toStr())
311
+ i = i + 1
291
312
  res.toStr()
292
313
 
293
314
  fun toStr(self) -> Str =
@@ -408,8 +429,10 @@ test "partition splits into elements matching a predicate, then the rest"
408
429
 
409
430
  test "list_get_set_push_pop: push 1000, get(999), pop until empty, get on empty is Err"
410
431
  l := List[Int]()
432
+ i := 0
411
- for i := range(0, 1000)
433
+ while i < 1000
412
434
  l.push(i)
435
+ i = i + 1
413
436
  assert l.length() == 1000
414
437
  assert l.get(999).unwrap() == 999
415
438
  assert optSumForListTest(l.get(1000)) == -1000
@@ -427,19 +450,23 @@ test "list_get_set_push_pop: push 1000, get(999), pop until empty, get on empty
427
450
 
428
451
  test "list_growth: length stays exact across the doubling boundaries"
429
452
  l := List[Int]()
453
+ i := 0
430
- for i := range(0, 1025)
454
+ while i < 1025
431
455
  l.push(i)
432
456
  n := i + 1
433
457
  if n == 15 || n == 16 || n == 17 || n == 1023 || n == 1024 || n == 1025
434
458
  assert l.length() == n
435
459
  assert l.get(n - 1).unwrap() == i
436
460
  assert l.get(0).unwrap() == 0
461
+ i = i + 1
437
462
  assert l.get(16).unwrap() == 16
438
463
  assert l.get(1024).unwrap() == 1024
439
464
 
440
465
  test "list of strings keeps its elements through growth"
441
466
  l := List[Str]()
467
+ i := 0
442
- for i := range(0, 40)
468
+ while i < 40
443
469
  l.push("s{i}")
470
+ i = i + 1
444
471
  assert l.get(39).unwrap() == "s39"
445
472
  assert l.get(16).unwrap() == "s16"
plum-std/Map.plum CHANGED
@@ -4,7 +4,6 @@ import std/Try
4
4
  import std/Array
5
5
  import std/Number
6
6
  import std/Str
7
- import std/Range
8
7
 
9
8
  # A Pair is a grouping of a key with a value.
10
9
  #
@@ -54,10 +53,12 @@ enum Map[K, V] =
54
53
  # Gets the value for `k`, or `Err(KeyNotFound)` if absent.
55
54
  fun get(self, k: K) -> V ! [KeyNotFound] =
56
55
  bucket := self.buckets.get(self.bucketIndex(k))
56
+ i := 0
57
- for i := range(0, bucket.size)
57
+ while i < bucket.size
58
58
  pair := bucket.data.get(i)
59
59
  if pair.key == k
60
60
  return Ok(pair.val)
61
+ i = i + 1
61
62
  return Err(KeyNotFound)
62
63
 
63
64
  fun has(self, k: K) -> Bool =
@@ -69,11 +70,13 @@ enum Map[K, V] =
69
70
  # unboundedly or leave a stale value reachable by `get`.
70
71
  fun set(self, k: K, v: V) -> Unit =
71
72
  bucket := self.buckets.get(self.bucketIndex(k))
73
+ i := 0
72
- for i := range(0, bucket.size)
74
+ while i < bucket.size
73
75
  pair := bucket.data.get(i)
74
76
  if pair.key == k
75
77
  pair.val = v
76
78
  return
79
+ i = i + 1
77
80
  bucket.push(Pair(key: k, val: v))
78
81
  self.size = self.size + 1
79
82
 
@@ -85,11 +88,13 @@ enum Map[K, V] =
85
88
  # Removes `k`'s entry, if present.
86
89
  fun remove(self, k: K) -> Unit =
87
90
  bucket := self.buckets.get(self.bucketIndex(k))
91
+ i := 0
88
- for i := range(0, bucket.size)
92
+ while i < bucket.size
89
93
  if bucket.data.get(i).key == k
90
94
  bucket.removeAt(i)
91
95
  self.size = self.size - 1
92
96
  return
97
+ i = i + 1
93
98
 
94
99
  fun length(self) -> Int =
95
100
  return self.size
@@ -98,19 +103,25 @@ enum Map[K, V] =
98
103
  return self.size == 0
99
104
 
100
105
  fun clear(self) -> Unit =
106
+ i := 0
101
- for i := range(0, self.buckets.length())
107
+ while i < self.buckets.length()
102
108
  self.buckets.get(i).clear()
109
+ i = i + 1
103
110
  self.size = 0
104
111
 
105
112
  # Calls `cb` with each key and value in the map. Order is bucket order,
106
113
  # then insertion order within a bucket — NOT overall insertion order (that
107
114
  # would need a separate side list to track, which nothing here needs today).
108
115
  fun each(self, cb: fn(K, V)) -> Unit =
116
+ i := 0
109
- for i := range(0, self.buckets.length())
117
+ while i < self.buckets.length()
110
118
  bucket := self.buckets.get(i)
119
+ j := 0
111
- for j := range(0, bucket.size)
120
+ while j < bucket.size
112
121
  pair := bucket.data.get(j)
113
122
  cb(pair.key, pair.val)
123
+ j = j + 1
124
+ i = i + 1
114
125
 
115
126
  fun keys(self) -> List[K] =
116
127
  result := List[K]()
@@ -124,12 +135,16 @@ enum Map[K, V] =
124
135
 
125
136
  fun map(self, cb: fn(K, V) -> Pair[X, Y]) -> Map[X, Y] =
126
137
  result := Map[X, Y]()
138
+ i := 0
127
- for i := range(0, self.buckets.length())
139
+ while i < self.buckets.length()
128
140
  bucket := self.buckets.get(i)
141
+ j := 0
129
- for j := range(0, bucket.size)
142
+ while j < bucket.size
130
143
  pair := bucket.data.get(j)
131
144
  mapped := cb(pair.key, pair.val)
132
145
  result.set(mapped.key, mapped.val)
146
+ j = j + 1
147
+ i = i + 1
133
148
  return result
134
149
 
135
150
  test "get/set/has round-trip and set replaces an existing key in place"
plum-std/Random.plum CHANGED
@@ -1,6 +1,5 @@
1
1
  module std
2
2
  import std/Number
3
- import std/Range
4
3
 
5
4
  # A seeded, deterministic pseudo-random generator (xorshift64*): the same seed
6
5
  # always yields the same sequence, so AI and simulation replay exactly. Not
@@ -41,8 +40,10 @@ enum Random =
41
40
  test "random_seed_deterministic: same seed gives the same first 100 values"
42
41
  a := Random(seed: 12345)
43
42
  b := Random(seed: 12345)
43
+ i := 0
44
- for i := range(0, 100)
44
+ while i < 100
45
45
  assert a.nextInt() == b.nextInt()
46
+ i = i + 1
46
47
 
47
48
  test "different seeds diverge and the stream is not constant"
48
49
  a := Random(seed: 1)
@@ -55,22 +56,26 @@ test "different seeds diverge and the stream is not constant"
55
56
  test "nextIntBelow stays in range and covers the range"
56
57
  r := Random(seed: 99)
57
58
  seen := 0
59
+ i := 0
58
- for i := range(0, 1000)
60
+ while i < 1000
59
61
  v := r.nextIntBelow(10)
60
62
  assert v >= 0
61
63
  assert v < 10
62
64
  seen = seen | {1 << v}
65
+ i = i + 1
63
66
  assert seen == 1023
64
67
  assert r.nextIntBelow(0) == 0
65
68
 
66
69
  test "nextFloat is in [0, 1) with a sane mean"
67
70
  r := Random(seed: 5)
68
71
  total := 0.0
72
+ i := 0
69
- for i := range(0, 1000)
73
+ while i < 1000
70
74
  f := r.nextFloat()
71
75
  assert f >= 0.0
72
76
  assert f < 1.0
73
77
  total = total + f
78
+ i = i + 1
74
79
  assert total > 400.0
75
80
  assert total < 600.0
76
81
 
plum-std/Range.plum DELETED
@@ -1,12 +0,0 @@
1
- module std
2
-
3
- import std/Number
4
-
5
- # `for i := range(0, n)`: start, start + 1, ... end - 1. The counter is bumped
6
- # before the `yield`, so a `continue` in the body still advances it.
7
- fun* range(start: Int, end: Int) *Int =
8
- i := start
9
- while i < end
10
- cur := i
11
- i = i + 1
12
- yield cur
plum-std/Set.plum CHANGED
@@ -2,7 +2,6 @@ module std
2
2
  import std/Map
3
3
  import std/List
4
4
  import std/Number
5
- import std/Range
6
5
 
7
6
  # A set of unique `T`s, a thin layer over `Map[T, Bool]` (so `T` needs the same
8
7
  # duck-typed `hash() -> Int` and `==` that `Map`'s keys do). `Set[Int]()`
@@ -38,8 +37,10 @@ enum Set[T] =
38
37
 
39
38
  test "set_dedup: adding duplicates keeps one copy"
40
39
  s := Set[Int]()
40
+ i := 0
41
- for i := range(0, 100)
41
+ while i < 100
42
42
  s.add(i % 10)
43
+ i = i + 1
43
44
  assert s.length() == 10
44
45
  assert s.has(3)
45
46
  assert s.has(10) == False