plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
1de3be3
— Peter John
2026-10-26T14:09:33+05:30
feat(std): Array.filled, Grid, Set, Deque, Heap, seeded Random; typed consts
- plum-checker/src/lib.rs +15 -1
- plum-examples/constants.plum +33 -0
- plum-examples/typed_arrays.plum +41 -0
- plum-std/Array.plum +10 -0
- plum-std/Deque.plum +109 -0
- plum-std/Grid.plum +49 -0
- plum-std/Heap.plum +105 -0
- plum-std/Random.plum +78 -0
- plum-std/Set.plum +68 -0
- plum-wasm-codegen/tests/examples_test.rs +55 -0
plum-checker/src/lib.rs
CHANGED
|
@@ -1225,7 +1225,7 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1225
1225
|
// Generic items are checked once, structurally (`TApp`/`TParam`), on the
|
|
1226
1226
|
// ORIGINAL source.
|
|
1227
1227
|
let mut errors: Vec<CheckError> = checkBareGenericTypes(source);
|
|
1228
|
-
let (global_env, methods, enum_variants, enum_params) = buildGlobalTables(source);
|
|
1228
|
+
let (mut global_env, methods, enum_variants, enum_params) = buildGlobalTables(source);
|
|
1229
1229
|
let min_required = buildMinRequiredArgs(source);
|
|
1230
1230
|
let ctx = CheckCtx {
|
|
1231
1231
|
methods: &methods,
|
|
@@ -1233,6 +1233,20 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1233
1233
|
enum_params: &enum_params,
|
|
1234
1234
|
min_required: &min_required,
|
|
1235
1235
|
};
|
|
1236
|
+
// Constants are inlined at their use sites, so their type is whatever their
|
|
1237
|
+
// initializer infers to (`FLAG = True`, `N = LIMIT * 2 + 1`, `T = List(1, 2)`).
|
|
1238
|
+
// `buildGlobalTables` only had literals to go on; now that every method/enum
|
|
1239
|
+
// table exists, re-infer each one with the full inferer. Two passes let a const
|
|
1240
|
+
// reference one declared later.
|
|
1241
|
+
for _ in 0..2 {
|
|
1242
|
+
for item in &source.items {
|
|
1243
|
+
if let ast::Item::Const(c) = item {
|
|
1244
|
+
if let Ok(ty) = inferExpr(&c.value, &global_env, &ctx) {
|
|
1245
|
+
global_env.insert(c.name.clone(), TypeScheme::mono(ty));
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1236
1250
|
|
|
1237
1251
|
// A discriminant enum (`enum Foo(n: Int) = ...`) requires every variant to supply
|
|
1238
1252
|
// exactly one value per declared param, unified against that param's type. An
|
plum-examples/constants.plum
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module constants
|
|
2
|
+
import std/Number
|
|
3
|
+
import std/Str
|
|
4
|
+
|
|
5
|
+
# Top-level constants may appear anywhere at top level (before or after enums
|
|
6
|
+
# and fns), hold Int/Float/Str/Bool literals or expressions over other
|
|
7
|
+
# constants, and are usable inside fns, methods and generics. They are inlined
|
|
8
|
+
# at each use. NOTE: constant names are global across imported modules.
|
|
9
|
+
GRID_W = 8
|
|
10
|
+
GRID_H = GRID_W * 2
|
|
11
|
+
CELL_SCALE = 2.5
|
|
12
|
+
GAME_NAME = "plum"
|
|
13
|
+
DEBUG_DRAW = True
|
|
14
|
+
|
|
15
|
+
enum Tile =
|
|
16
|
+
| Tile(x: Int)
|
|
17
|
+
|
|
18
|
+
fun offset(self) -> Int = GRID_W + self.x
|
|
19
|
+
|
|
20
|
+
enum Wrap[T] =
|
|
21
|
+
| Wrap(v: T)
|
|
22
|
+
|
|
23
|
+
fun area(self) -> Int = GRID_W * GRID_H
|
|
24
|
+
|
|
25
|
+
fun scaled(n: Float) -> Float = n * CELL_SCALE
|
|
26
|
+
|
|
27
|
+
test "consts are usable in fns, methods, generics and derived consts"
|
|
28
|
+
assert GRID_H == 16
|
|
29
|
+
assert Tile(x: 1).offset() == 9
|
|
30
|
+
assert Wrap(v: 3.5).area() == 128
|
|
31
|
+
assert scaled(2.0) == 5.0
|
|
32
|
+
assert GAME_NAME == "plum"
|
|
33
|
+
assert DEBUG_DRAW
|
plum-examples/typed_arrays.plum
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module typed_arrays
|
|
2
|
+
import std/Number
|
|
3
|
+
import std/Array
|
|
4
|
+
import std/Grid
|
|
5
|
+
|
|
6
|
+
# Tile-grid style data: Array[Int] / Array[Float] / Array[Bool] are native
|
|
7
|
+
# unboxed wasm arrays (i64 / f64 / i32 elements), not boxed anyref arrays.
|
|
8
|
+
fun sumInts(n: Int) -> Int =
|
|
9
|
+
a := Array.filled(n, 3)
|
|
10
|
+
total := 0
|
|
11
|
+
for i := range n
|
|
12
|
+
total = total + a.get(i)
|
|
13
|
+
return total
|
|
14
|
+
|
|
15
|
+
fun sumFloats(n: Int) -> Float =
|
|
16
|
+
a := Array.filled(n, 0.5)
|
|
17
|
+
total := 0.0
|
|
18
|
+
for i := range n
|
|
19
|
+
total = total + a.get(i)
|
|
20
|
+
return total
|
|
21
|
+
|
|
22
|
+
fun countTrue(n: Int) -> Int =
|
|
23
|
+
a := Array.filled(n, False)
|
|
24
|
+
a.set(1, True)
|
|
25
|
+
a.set(3, True)
|
|
26
|
+
c := 0
|
|
27
|
+
for i := range n
|
|
28
|
+
if a.get(i)
|
|
29
|
+
c = c + 1
|
|
30
|
+
return c
|
|
31
|
+
|
|
32
|
+
test "array_int_native: filled arrays hold their values"
|
|
33
|
+
assert sumInts(10) == 30
|
|
34
|
+
assert sumFloats(4) == 2.0
|
|
35
|
+
assert countTrue(5) == 2
|
|
36
|
+
|
|
37
|
+
test "Grid[Int] indexes (x, y) row-major"
|
|
38
|
+
g := Grid.filled(3, 2, 0)
|
|
39
|
+
g.set(2, 1, 7)
|
|
40
|
+
assert g.get(2, 1) == 7
|
|
41
|
+
assert g.data.get(5) == 7
|
plum-std/Array.plum
CHANGED
|
@@ -59,3 +59,13 @@ enum Array[T] =
|
|
|
59
59
|
# grows by doubling through this.
|
|
60
60
|
fun resize(self, n: Int) -> Array[T] =
|
|
61
61
|
todo
|
|
62
|
+
|
|
63
|
+
# A new array of length `n` with every slot set to `v`:
|
|
64
|
+
# `Array[Int].filled(100, 0)`. Ordinary Plum on top of the intrinsics; for
|
|
65
|
+
# `Int`/`Float`/`Bool` the per-instantiation copy stores the values unboxed
|
|
66
|
+
# in a native `array (mut i64)` / `(mut f64)` / `(mut i32)`.
|
|
67
|
+
fun filled(n: Int, v: T) -> Array[T] =
|
|
68
|
+
a := Array[T]().resize(n)
|
|
69
|
+
for i := range n
|
|
70
|
+
a.set(i, v)
|
|
71
|
+
return a
|
plum-std/Deque.plum
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
module std
|
|
2
|
+
import std/Array
|
|
3
|
+
import std/List
|
|
4
|
+
import std/Try
|
|
5
|
+
import std/Number
|
|
6
|
+
|
|
7
|
+
# A double-ended queue: a ring buffer over `Array[T]`. `buf`'s length is the
|
|
8
|
+
# capacity, `head` the index of the front element, `size` the number in use;
|
|
9
|
+
# element `i` (0 = front) lives at `(head + i) % capacity`. Pushes at either end
|
|
10
|
+
# are O(1) amortized — when full, capacity doubles and the elements are copied
|
|
11
|
+
# out in front-to-back order (so the ring unwraps).
|
|
12
|
+
enum Deque[T] =
|
|
13
|
+
| Deque(buf: Array[T], head: Int, size: Int)
|
|
14
|
+
|
|
15
|
+
fun init() -> Deque[T] =
|
|
16
|
+
b := Array[T]().resize(8)
|
|
17
|
+
return Deque(buf: b, head: 0, size: 0)
|
|
18
|
+
|
|
19
|
+
fun length(self) -> Int =
|
|
20
|
+
return self.size
|
|
21
|
+
|
|
22
|
+
fun isEmpty(self) -> Bool =
|
|
23
|
+
return self.size == 0
|
|
24
|
+
|
|
25
|
+
# Doubles capacity when full, relinearizing the ring so `head` becomes 0.
|
|
26
|
+
fun grow(self) -> Unit =
|
|
27
|
+
if self.size < self.buf.length()
|
|
28
|
+
return
|
|
29
|
+
cap := self.buf.length()
|
|
30
|
+
nb := Array[T]().resize(cap * 2)
|
|
31
|
+
for i := range self.size
|
|
32
|
+
nb.set(i, self.buf.get({self.head + i} % cap))
|
|
33
|
+
self.buf = nb
|
|
34
|
+
self.head = 0
|
|
35
|
+
|
|
36
|
+
fun pushBack(self, v: T) -> Unit =
|
|
37
|
+
self.grow()
|
|
38
|
+
self.buf.set({self.head + self.size} % self.buf.length(), v)
|
|
39
|
+
self.size = self.size + 1
|
|
40
|
+
|
|
41
|
+
fun pushFront(self, v: T) -> Unit =
|
|
42
|
+
self.grow()
|
|
43
|
+
cap := self.buf.length()
|
|
44
|
+
self.head = {self.head + cap - 1} % cap
|
|
45
|
+
self.buf.set(self.head, v)
|
|
46
|
+
self.size = self.size + 1
|
|
47
|
+
|
|
48
|
+
# Removes and returns the front element, or `Err(DequeWasEmpty)`.
|
|
49
|
+
fun popFront(self) -> T ! [DequeWasEmpty] =
|
|
50
|
+
if self.size == 0
|
|
51
|
+
return Err(DequeWasEmpty)
|
|
52
|
+
v := self.buf.get(self.head)
|
|
53
|
+
self.head = {self.head + 1} % self.buf.length()
|
|
54
|
+
self.size = self.size - 1
|
|
55
|
+
Ok(v)
|
|
56
|
+
|
|
57
|
+
# Removes and returns the back element, or `Err(DequeWasEmpty)`.
|
|
58
|
+
fun popBack(self) -> T ! [DequeWasEmpty] =
|
|
59
|
+
if self.size == 0
|
|
60
|
+
return Err(DequeWasEmpty)
|
|
61
|
+
self.size = self.size - 1
|
|
62
|
+
Ok(self.buf.get({self.head + self.size} % self.buf.length()))
|
|
63
|
+
|
|
64
|
+
# The `i`'th element from the front, or `Err(OutOfBounds)`.
|
|
65
|
+
fun get(self, i: Int) -> T ! [OutOfBounds] =
|
|
66
|
+
if i < 0 || i >= self.size
|
|
67
|
+
return Err(OutOfBounds)
|
|
68
|
+
Ok(self.buf.get({self.head + i} % self.buf.length()))
|
|
69
|
+
|
|
70
|
+
fun clear(self) -> Unit =
|
|
71
|
+
self.head = 0
|
|
72
|
+
self.size = 0
|
|
73
|
+
|
|
74
|
+
test "deque_wraparound: head wraps past the end of the ring and growth unwraps it"
|
|
75
|
+
d := Deque[Int]()
|
|
76
|
+
# keep size small so head keeps advancing around the 8-slot ring
|
|
77
|
+
for i := range 50
|
|
78
|
+
d.pushBack(i)
|
|
79
|
+
d.pushBack(i + 1000)
|
|
80
|
+
assert d.popFront().unwrap() == i
|
|
81
|
+
assert d.popFront().unwrap() == i + 1000
|
|
82
|
+
assert d.isEmpty()
|
|
83
|
+
assert d.head != 0 || d.buf.length() > 8
|
|
84
|
+
# fill beyond capacity while head is mid-ring
|
|
85
|
+
d.pushBack(1)
|
|
86
|
+
d.pushBack(2)
|
|
87
|
+
d.popFront()
|
|
88
|
+
for i := range 20
|
|
89
|
+
d.pushBack(10 + i)
|
|
90
|
+
assert d.length() == 21
|
|
91
|
+
assert d.get(0).unwrap() == 2
|
|
92
|
+
assert d.get(20).unwrap() == 29
|
|
93
|
+
assert d.popBack().unwrap() == 29
|
|
94
|
+
|
|
95
|
+
test "pushFront builds the front in reverse order"
|
|
96
|
+
d := Deque[Int]()
|
|
97
|
+
for i := range 12
|
|
98
|
+
d.pushFront(i)
|
|
99
|
+
assert d.get(0).unwrap() == 11
|
|
100
|
+
assert d.get(11).unwrap() == 0
|
|
101
|
+
assert d.popFront().unwrap() == 11
|
|
102
|
+
assert d.popBack().unwrap() == 0
|
|
103
|
+
assert d.length() == 10
|
|
104
|
+
|
|
105
|
+
test "pop on empty is an error"
|
|
106
|
+
d := Deque[Int]()
|
|
107
|
+
assert d.popFront().isErr()
|
|
108
|
+
assert d.popBack().isErr()
|
|
109
|
+
assert d.get(0).isErr()
|
plum-std/Grid.plum
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module std
|
|
2
|
+
import std/Array
|
|
3
|
+
import std/Number
|
|
4
|
+
|
|
5
|
+
# A fixed-size 2D grid stored row-major in ONE flat `Array[T]` (so a
|
|
6
|
+
# `Grid[Int]`/`Grid[Float]`/`Grid[Bool]` is a single native unboxed wasm
|
|
7
|
+
# array). Cell `(x, y)` lives at `y * w + x`; `x` is the column in `[0, w)`,
|
|
8
|
+
# `y` the row in `[0, h)`. `get`/`set` do no bounds check of their own beyond
|
|
9
|
+
# the underlying array's trap — use `inBounds` first when coordinates may stray
|
|
10
|
+
# off the map (neighbour scans, pathfinding).
|
|
11
|
+
enum Grid[T] =
|
|
12
|
+
| Grid(w: Int, h: Int, data: Array[T])
|
|
13
|
+
|
|
14
|
+
# `Grid.filled(w, h, v)` — a `w` x `h` grid with every cell set to `v`
|
|
15
|
+
# (`T` is inferred from `v`).
|
|
16
|
+
fun filled(w: Int, h: Int, v: T) -> Grid[T] =
|
|
17
|
+
return Grid(w: w, h: h, data: Array.filled(w * h, v))
|
|
18
|
+
|
|
19
|
+
# Flat index of cell `(x, y)`.
|
|
20
|
+
fun index(self, x: Int, y: Int) -> Int =
|
|
21
|
+
return y * self.w + x
|
|
22
|
+
|
|
23
|
+
fun inBounds(self, x: Int, y: Int) -> Bool =
|
|
24
|
+
return x >= 0 && y >= 0 && x < self.w && y < self.h
|
|
25
|
+
|
|
26
|
+
fun get(self, x: Int, y: Int) -> T =
|
|
27
|
+
return self.data.get(y * self.w + x)
|
|
28
|
+
|
|
29
|
+
fun set(self, x: Int, y: Int, v: T) -> Unit =
|
|
30
|
+
self.data.set(y * self.w + x, v)
|
|
31
|
+
|
|
32
|
+
test "Grid get/set address cells row-major and fill covers every cell"
|
|
33
|
+
g := Grid.filled(4, 3, 0)
|
|
34
|
+
assert g.w == 4
|
|
35
|
+
assert g.h == 3
|
|
36
|
+
g.set(3, 2, 9)
|
|
37
|
+
g.set(0, 1, 5)
|
|
38
|
+
assert g.get(3, 2) == 9
|
|
39
|
+
assert g.get(0, 1) == 5
|
|
40
|
+
assert g.get(1, 1) == 0
|
|
41
|
+
assert g.index(3, 2) == 11
|
|
42
|
+
assert g.data.get(11) == 9
|
|
43
|
+
|
|
44
|
+
test "Grid inBounds rejects negative and too-large coordinates"
|
|
45
|
+
g := Grid.filled(2, 2, 1.5)
|
|
46
|
+
assert g.inBounds(1, 1)
|
|
47
|
+
assert g.inBounds(2, 0) == False
|
|
48
|
+
assert g.inBounds(0, -1) == False
|
|
49
|
+
assert g.get(1, 1) == 1.5
|
plum-std/Heap.plum
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module std
|
|
2
|
+
import std/Array
|
|
3
|
+
import std/List
|
|
4
|
+
import std/Try
|
|
5
|
+
import std/Number
|
|
6
|
+
|
|
7
|
+
# A binary MIN-heap ordered by a key function: `pop` returns the element whose
|
|
8
|
+
# `key(x)` is smallest. Ordering by an explicit `fn(T) -> Float` (rather than a
|
|
9
|
+
# duck-typed `lt` method) lets one type be queued under different priorities and
|
|
10
|
+
# fits A* directly: `Heap[Node](key: |n| n.f)`. The key is recomputed on each
|
|
11
|
+
# comparison, so keep it cheap and don't mutate an element's key while it is in
|
|
12
|
+
# the heap. `push`/`pop` are O(log n); `peek`/`length` are O(1). Equal keys pop in
|
|
13
|
+
# no guaranteed order.
|
|
14
|
+
enum Heap[T] =
|
|
15
|
+
| Heap(items: List[T], key: fn(T) -> Float)
|
|
16
|
+
|
|
17
|
+
fun length(self) -> Int =
|
|
18
|
+
return self.items.length()
|
|
19
|
+
|
|
20
|
+
fun isEmpty(self) -> Bool =
|
|
21
|
+
return self.items.length() == 0
|
|
22
|
+
|
|
23
|
+
fun clear(self) -> Unit =
|
|
24
|
+
self.items.clear()
|
|
25
|
+
|
|
26
|
+
fun swap(self, i: Int, j: Int) -> Unit =
|
|
27
|
+
a := self.items.data.get(i)
|
|
28
|
+
self.items.data.set(i, self.items.data.get(j))
|
|
29
|
+
self.items.data.set(j, a)
|
|
30
|
+
|
|
31
|
+
fun less(self, i: Int, j: Int) -> Bool =
|
|
32
|
+
a := self.items.data.get(i)
|
|
33
|
+
b := self.items.data.get(j)
|
|
34
|
+
return self.key(a) < self.key(b)
|
|
35
|
+
|
|
36
|
+
fun siftUp(self, start: Int) -> Unit =
|
|
37
|
+
i := start
|
|
38
|
+
while i > 0
|
|
39
|
+
p := {i - 1} / 2
|
|
40
|
+
if !self.less(i, p)
|
|
41
|
+
return
|
|
42
|
+
self.swap(i, p)
|
|
43
|
+
i = p
|
|
44
|
+
|
|
45
|
+
fun siftDown(self, start: Int) -> Unit =
|
|
46
|
+
i := start
|
|
47
|
+
n := self.items.length()
|
|
48
|
+
while True
|
|
49
|
+
l := 2 * i + 1
|
|
50
|
+
r := l + 1
|
|
51
|
+
m := i
|
|
52
|
+
if l < n && self.less(l, m)
|
|
53
|
+
m = l
|
|
54
|
+
if r < n && self.less(r, m)
|
|
55
|
+
m = r
|
|
56
|
+
if m == i
|
|
57
|
+
return
|
|
58
|
+
self.swap(i, m)
|
|
59
|
+
i = m
|
|
60
|
+
|
|
61
|
+
fun push(self, v: T) -> Unit =
|
|
62
|
+
self.items.push(v)
|
|
63
|
+
self.siftUp(self.items.length() - 1)
|
|
64
|
+
|
|
65
|
+
# The smallest element without removing it, or `Err(HeapWasEmpty)`.
|
|
66
|
+
fun peek(self) -> T ! [HeapWasEmpty] =
|
|
67
|
+
if self.items.length() == 0
|
|
68
|
+
return Err(HeapWasEmpty)
|
|
69
|
+
Ok(self.items.data.get(0))
|
|
70
|
+
|
|
71
|
+
# Removes and returns the smallest element, or `Err(HeapWasEmpty)`.
|
|
72
|
+
fun pop(self) -> T ! [HeapWasEmpty] =
|
|
73
|
+
n := self.items.length()
|
|
74
|
+
if n == 0
|
|
75
|
+
return Err(HeapWasEmpty)
|
|
76
|
+
top := self.items.data.get(0)
|
|
77
|
+
self.items.data.set(0, self.items.data.get(n - 1))
|
|
78
|
+
self.items.size = n - 1
|
|
79
|
+
self.siftDown(0)
|
|
80
|
+
Ok(top)
|
|
81
|
+
|
|
82
|
+
test "heap_pop_order: 1000 pseudo-random keys pop in ascending order"
|
|
83
|
+
h := Heap[Int](items: List[Int](), key: |x| Float(x))
|
|
84
|
+
x := 12345
|
|
85
|
+
for i := range 1000
|
|
86
|
+
x = {x * 1103515245 + 12345} % 2147483648
|
|
87
|
+
h.push(x % 100000)
|
|
88
|
+
assert h.length() == 1000
|
|
89
|
+
prev := -1
|
|
90
|
+
for i := range 1000
|
|
91
|
+
v := h.pop().unwrap()
|
|
92
|
+
assert v >= prev
|
|
93
|
+
prev = v
|
|
94
|
+
assert h.isEmpty()
|
|
95
|
+
assert h.pop().isErr()
|
|
96
|
+
|
|
97
|
+
test "peek shows the minimum and Float keys order records"
|
|
98
|
+
h := Heap[Float](items: List[Float](), key: |x| 0.0 - x)
|
|
99
|
+
h.push(1.5)
|
|
100
|
+
h.push(9.5)
|
|
101
|
+
h.push(4.0)
|
|
102
|
+
assert h.peek().unwrap() == 9.5
|
|
103
|
+
assert h.pop().unwrap() == 9.5
|
|
104
|
+
assert h.pop().unwrap() == 4.0
|
|
105
|
+
assert h.length() == 1
|
plum-std/Random.plum
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module std
|
|
2
|
+
import std/Number
|
|
3
|
+
|
|
4
|
+
# A seeded, deterministic pseudo-random generator (xorshift64*): the same seed
|
|
5
|
+
# always yields the same sequence, so AI and simulation replay exactly. Not
|
|
6
|
+
# cryptographically secure. For wall-clock randomness use `random()` from
|
|
7
|
+
# `std/Number` instead.
|
|
8
|
+
#
|
|
9
|
+
# `Random(seed: 42)` — `seed` holds the generator's CURRENT state and advances
|
|
10
|
+
# on every draw (a seed of 0 is a fixed point of xorshift, so it is silently
|
|
11
|
+
# replaced by a non-zero constant on first use).
|
|
12
|
+
#
|
|
13
|
+
# `Int` is signed 64-bit and `>>` is arithmetic, so the logical right shifts
|
|
14
|
+
# xorshift needs are done as `{x >> k} & mask`.
|
|
15
|
+
enum Random =
|
|
16
|
+
| Random(seed: Int)
|
|
17
|
+
|
|
18
|
+
# Advances the state and returns 64 well-mixed bits (may be negative).
|
|
19
|
+
fun nextInt(self) -> Int =
|
|
20
|
+
x := self.seed
|
|
21
|
+
if x == 0
|
|
22
|
+
x = 88172645463325252
|
|
23
|
+
x = x ^ { {x >> 12} & 0xFFFFFFFFFFFFF }
|
|
24
|
+
x = x ^ { x << 25 }
|
|
25
|
+
x = x ^ { {x >> 27} & 0x1FFFFFFFFF }
|
|
26
|
+
self.seed = x
|
|
27
|
+
return x * 2685821657736338717
|
|
28
|
+
|
|
29
|
+
# A uniformly distributed Int in [0, n). Returns 0 for n <= 0.
|
|
30
|
+
fun nextIntBelow(self, n: Int) -> Int =
|
|
31
|
+
if n <= 0
|
|
32
|
+
return 0
|
|
33
|
+
return { self.nextInt() & 0x7FFFFFFFFFFFFFFF } % n
|
|
34
|
+
|
|
35
|
+
# A uniformly distributed Float in [0.0, 1.0) with 53 bits of precision.
|
|
36
|
+
fun nextFloat(self) -> Float =
|
|
37
|
+
bits := {self.nextInt() >> 11} & 0x1FFFFFFFFFFFFF
|
|
38
|
+
return Float(bits) / 9007199254740992.0
|
|
39
|
+
|
|
40
|
+
test "random_seed_deterministic: same seed gives the same first 100 values"
|
|
41
|
+
a := Random(seed: 12345)
|
|
42
|
+
b := Random(seed: 12345)
|
|
43
|
+
for i := range 100
|
|
44
|
+
assert a.nextInt() == b.nextInt()
|
|
45
|
+
|
|
46
|
+
test "different seeds diverge and the stream is not constant"
|
|
47
|
+
a := Random(seed: 1)
|
|
48
|
+
b := Random(seed: 2)
|
|
49
|
+
assert a.nextInt() != b.nextInt()
|
|
50
|
+
c := Random(seed: 7)
|
|
51
|
+
first := c.nextInt()
|
|
52
|
+
assert c.nextInt() != first
|
|
53
|
+
|
|
54
|
+
test "nextIntBelow stays in range and covers the range"
|
|
55
|
+
r := Random(seed: 99)
|
|
56
|
+
seen := 0
|
|
57
|
+
for i := range 1000
|
|
58
|
+
v := r.nextIntBelow(10)
|
|
59
|
+
assert v >= 0
|
|
60
|
+
assert v < 10
|
|
61
|
+
seen = seen | {1 << v}
|
|
62
|
+
assert seen == 1023
|
|
63
|
+
assert r.nextIntBelow(0) == 0
|
|
64
|
+
|
|
65
|
+
test "nextFloat is in [0, 1) with a sane mean"
|
|
66
|
+
r := Random(seed: 5)
|
|
67
|
+
total := 0.0
|
|
68
|
+
for i := range 1000
|
|
69
|
+
f := r.nextFloat()
|
|
70
|
+
assert f >= 0.0
|
|
71
|
+
assert f < 1.0
|
|
72
|
+
total = total + f
|
|
73
|
+
assert total > 400.0
|
|
74
|
+
assert total < 600.0
|
|
75
|
+
|
|
76
|
+
test "seed 0 still produces a varying stream"
|
|
77
|
+
r := Random(seed: 0)
|
|
78
|
+
assert r.nextInt() != r.nextInt()
|
plum-std/Set.plum
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module std
|
|
2
|
+
import std/Map
|
|
3
|
+
import std/List
|
|
4
|
+
import std/Number
|
|
5
|
+
|
|
6
|
+
# A set of unique `T`s, a thin layer over `Map[T, Bool]` (so `T` needs the same
|
|
7
|
+
# duck-typed `hash() -> Int` and `==` that `Map`'s keys do). `Set[Int]()`
|
|
8
|
+
# builds an empty one.
|
|
9
|
+
enum Set[T] =
|
|
10
|
+
| Set(map: Map[T, Bool])
|
|
11
|
+
|
|
12
|
+
fun init() -> Set[T] =
|
|
13
|
+
return Set(map: Map[T, Bool]())
|
|
14
|
+
|
|
15
|
+
# Adds `v`; adding a value already present changes nothing.
|
|
16
|
+
fun add(self, v: T) -> Unit =
|
|
17
|
+
self.map.set(v, True)
|
|
18
|
+
|
|
19
|
+
fun has(self, v: T) -> Bool =
|
|
20
|
+
return self.map.has(v)
|
|
21
|
+
|
|
22
|
+
fun remove(self, v: T) -> Unit =
|
|
23
|
+
self.map.remove(v)
|
|
24
|
+
|
|
25
|
+
fun length(self) -> Int =
|
|
26
|
+
return self.map.length()
|
|
27
|
+
|
|
28
|
+
fun isEmpty(self) -> Bool =
|
|
29
|
+
return self.map.isEmpty()
|
|
30
|
+
|
|
31
|
+
fun clear(self) -> Unit =
|
|
32
|
+
self.map.clear()
|
|
33
|
+
|
|
34
|
+
# Every member, in no particular order.
|
|
35
|
+
fun toList(self) -> List[T] =
|
|
36
|
+
return self.map.keys()
|
|
37
|
+
|
|
38
|
+
test "set_dedup: adding duplicates keeps one copy"
|
|
39
|
+
s := Set[Int]()
|
|
40
|
+
for i := range 100
|
|
41
|
+
s.add(i % 10)
|
|
42
|
+
assert s.length() == 10
|
|
43
|
+
assert s.has(3)
|
|
44
|
+
assert s.has(10) == False
|
|
45
|
+
s.add(3)
|
|
46
|
+
assert s.length() == 10
|
|
47
|
+
|
|
48
|
+
test "remove and clear"
|
|
49
|
+
s := Set[Int]()
|
|
50
|
+
s.add(1)
|
|
51
|
+
s.add(2)
|
|
52
|
+
s.remove(1)
|
|
53
|
+
assert s.has(1) == False
|
|
54
|
+
assert s.has(2)
|
|
55
|
+
assert s.length() == 1
|
|
56
|
+
s.remove(99)
|
|
57
|
+
assert s.length() == 1
|
|
58
|
+
s.clear()
|
|
59
|
+
assert s.isEmpty()
|
|
60
|
+
|
|
61
|
+
test "toList returns each member once"
|
|
62
|
+
s := Set[Int]()
|
|
63
|
+
s.add(5)
|
|
64
|
+
s.add(5)
|
|
65
|
+
s.add(7)
|
|
66
|
+
l := s.toList()
|
|
67
|
+
assert l.length() == 2
|
|
68
|
+
assert l.get(0).unwrap() + l.get(1).unwrap() == 12
|
plum-wasm-codegen/tests/examples_test.rs
CHANGED
|
@@ -210,3 +210,58 @@ fn ioExampleCompilesAndRunsCorrectly() {
|
|
|
210
210
|
vec!["hello from plum".to_string(), "hello, world!".to_string()]
|
|
211
211
|
);
|
|
212
212
|
}
|
|
213
|
+
|
|
214
|
+
/// L1 `array_int_native`: `Array[Int]`/`Array[Float]`/`Array[Bool]` lower to
|
|
215
|
+
/// native unboxed wasm-gc arrays (`array (mut i64)` / `(mut f64)` / `(mut i32)`),
|
|
216
|
+
/// not the boxed `array (mut anyref)`.
|
|
217
|
+
#[test]
|
|
218
|
+
fn array_int_native() {
|
|
219
|
+
use wasmparser::{CompositeInnerType, Parser, Payload, StorageType, ValType};
|
|
220
|
+
let bytes = assertCompiles("typed_arrays.plum");
|
|
221
|
+
let mut elems: Vec<ValType> = Vec::new();
|
|
222
|
+
for payload in Parser::new(0).parse_all(&bytes) {
|
|
223
|
+
if let Payload::TypeSection(reader) = payload.unwrap() {
|
|
224
|
+
for rec in reader {
|
|
225
|
+
for sub in rec.unwrap().into_types() {
|
|
226
|
+
if let CompositeInnerType::Array(a) = &sub.composite_type.inner {
|
|
227
|
+
if a.0.mutable {
|
|
228
|
+
if let StorageType::Val(v) = a.0.element_type {
|
|
229
|
+
elems.push(v);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
assert!(elems.contains(&ValType::I64), "no (mut i64) array in {:?}", elems);
|
|
238
|
+
assert!(elems.contains(&ValType::F64), "no (mut f64) array in {:?}", elems);
|
|
239
|
+
assert!(elems.contains(&ValType::I32), "no (mut i32) array in {:?}", elems);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// L1 `const_table_1000_entries`: 1000 top-level consts (placed before and
|
|
243
|
+
/// between other decls) plus a 1000-element list literal parse, type-check and
|
|
244
|
+
/// are usable inside methods.
|
|
245
|
+
#[test]
|
|
246
|
+
fn const_table_1000_entries() {
|
|
247
|
+
let mut src = String::from("module t\nimport std/Number\nimport std/List\n\n");
|
|
248
|
+
for i in 0..500 {
|
|
249
|
+
src.push_str(&format!("E{} = {}\n", i, i * 3 + 1));
|
|
250
|
+
}
|
|
251
|
+
src.push_str("enum Holder =\n | Holder(n: Int)\n\n fun at(self) -> Int = E499 + self.n\n\n");
|
|
252
|
+
for i in 500..1000 {
|
|
253
|
+
src.push_str(&format!("E{} = {}\n", i, i * 3 + 1));
|
|
254
|
+
}
|
|
255
|
+
src.push_str("FLAG = True\nDOUBLE = E999 * 2 + 1\n\nfun table() -> List[Int] =\n return List(");
|
|
256
|
+
let items: Vec<String> = (0..1000).map(|i| (i * i).to_string()).collect();
|
|
257
|
+
src.push_str(&items.join(", "));
|
|
258
|
+
src.push_str(")\n\nfun probe() -> Int =\n return E999 + DOUBLE\n");
|
|
259
|
+
let dir = std::env::temp_dir().join(format!("plum_const_table_{}", std::process::id()));
|
|
260
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
261
|
+
let path = dir.join("t.plum");
|
|
262
|
+
std::fs::write(&path, src).unwrap();
|
|
263
|
+
let source = plum_core::loadAndMerge(&path, &libPath()).expect("1000-const file should load");
|
|
264
|
+
let bytes = compileSource(&source).expect("1000-const file should compile");
|
|
265
|
+
assert!(wasmparser::validate(&bytes).is_ok());
|
|
266
|
+
std::fs::remove_dir_all(&dir).ok();
|
|
267
|
+
}
|