plum

#treesitter#compiler#wasm

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

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


6d21ab6 — Peter John 2026-11-02T10:51:33+05:30
refactor(std): no free functions; every std function is a static
README.md CHANGED
@@ -663,7 +663,7 @@ primitive Byte = # in plum-std/Byte.plum
663
663
  Int(self)
664
664
  ```
665
665
 
666
- The std library uses namespaces where free functions clearly belong together: `Math` (see [Standard library highlights](#standard-library-highlights)). `Time.now()`, `Uuid.v4()`, `Uuid.fromStr(s)`, `Random.float()` and `Random.intBelow(n)` are statics of their types. See [`plum-examples/constants.plum`](plum-examples/constants.plum) and [`plum-examples/numbers.plum`](plum-examples/numbers.plum).
666
+ The std library has no free functions: each function is a static of its type or of a `primitive` namespace, so `Math.sqrt(x)`, `Os.readFile(path)`, `Base64.encode(s)`, `Http.get(url)` and `Testing.expectEq(a, b)` (see [Standard library highlights](#standard-library-highlights)). `Time.now()`, `Uuid.v4()`, `Uuid.fromStr(s)`, `Random.float()` and `Random.intBelow(n)` are statics of their types. See [`plum-examples/constants.plum`](plum-examples/constants.plum) and [`plum-examples/numbers.plum`](plum-examples/numbers.plum).
667
667
 
668
668
  ## Closures
669
669
 
@@ -985,7 +985,7 @@ $ plum test plum-examples/testing.plum
985
985
  1 passed, 0 failed
986
986
  ```
987
987
 
988
- A failing `assert` prints the actual and expected values for `==` comparisons. `import std/Testing` adds small assertion helpers (`expectEq`, `expectTrue`, `expectSome`, ...). The `plum-std` library and every file in `plum-examples` carry their own tests: `plum test plum-std plum-examples`.
988
+ A failing `assert` prints the actual and expected values for `==` comparisons. `import std/Testing` adds small assertion helpers (`Testing.expectEq`, `Testing.expectTrue`, `Testing.expectSuccess`, ...). The `plum-std` library and every file in `plum-examples` carry their own tests: `plum test plum-std plum-examples`.
989
989
 
990
990
  ## `extern` functions and hosts
991
991
 
@@ -1006,8 +1006,8 @@ hello, world!
1006
1006
  - **`Bool`** — `true`/`false`, `if`, `&&`/`||`/`!` and `==` are built in (no import); `toStr` (also used by `"{b}"` interpolation) and `Bool.parse("true")` need `import std/Bool`
1007
1007
  - **`Int`** / **`Float`** (`import std/Int`, `import std/Float`) — built-in numbers with their API in `primitive Int` / `primitive Float`: `abs`, `sign`, `min`, `max`, `sqrt`, `pow`, `floor`, `round`, `toStr`, ... (constants and `parse`: see [Constants](#constants-statics-and-primitive))
1008
1008
  - **`Math`** (`import std/Math`) — a `primitive` namespace of floating-point functions: `Math.sqrt`, `Math.sin`, `Math.exp`, `Math.ln`, ...
1009
- - **`Str`** (`import std/Str`) — a byte array with a codepoint-aware layer for Unicode correctness: `runeLength`, `runeAt`, `codePointAt`, `codePointToStr`
1010
- - **`Byte`** / **`ByteSlice`** (`import std/Byte`, `import std/ByteSlice`) — a scalar unsigned 8-bit value and a fixed-length mutable byte slice
1009
+ - **`Str`** (`import std/Str`) — a byte array with a codepoint-aware layer for Unicode correctness: `runeLength`, `runeAt`, `codePointAt`, `Str.codePointToStr(cp)`
1010
+ - **`Byte`** / **`ByteSlice`** (`import std/Byte`, `import std/ByteSlice`) — a scalar unsigned 8-bit value and a fixed-length mutable byte slice (`ByteSlice.make(n)`, `ByteSlice.copy`, `ByteSlice.equal`)
1011
1011
  - **`Buffer`** (`import std/Buffer`) — a growable, mutable byte sequence for building up a `Str`, amortized O(n) writes
1012
1012
  - **`Array[T]`** (`import std/Array`) — the compiler-intrinsic generic array that `List` and `Map` are written on; `Grid[T]` is a flat 2D array
1013
1013
  - **`List[T]`** (`import std/List`) — a growable list built on `Array`, with `get`/`add`/`set`/`removeAt`/`remove`/`sort`/`join`/`chunk` (iteration and transformation via `for`)
@@ -1015,7 +1015,7 @@ hello, world!
1015
1015
  - **`Set[T]`**, **`Deque[T]`**, **`Heap[T]`** — a set over `Map`'s bucketed hash table, a ring-buffer double-ended queue and a key-ordered min-heap
1016
1016
  - **`Seq[T]`** (`import std/Seq`) — the adapters and terminals of the `for` chains
1017
1017
  - **`Json`**, **`Base64`**, **`Uuid`**, **`Time`**, **`Random`** — parsing, encoding, ids, wall-clock time and a seeded deterministic generator (the `Random` statics use the wall clock)
1018
- - **`Os`**, **`Http`** — printing and the file system; HTTP requests through the host
1018
+ - **`Os`**, **`Http`** — printing and the file system (`Os.readFile`, `Os.writeFile`, ...); HTTP requests through the host (`Http.get`, `Http.post`, ...)
1019
1019
  - **`Testing`** (`import std/Testing`) — assertion helpers for `test` blocks
1020
1020
 
1021
1021
  See [`plum-examples/`](plum-examples) for a runnable file per feature above, and [`plum-std/`](plum-std) for the standard library source.
plum-std/Base64.plum CHANGED
@@ -16,116 +16,121 @@ import std/Float
16
16
  # byte array here (see `libs/std/str.plum`), so this is a plain byte string,
17
17
  # not a claim that the content is valid UTF-8 text.
18
18
 
19
+ primitive Base64 =
19
- # Encode for PEM (RFC 1421): 64-character lines.
20
+ # Encode for PEM (RFC 1421): 64-character lines.
20
- fun encodePEM(data: Str) -> Str =
21
+ fun encodePEM(data: Str) -> Str =
21
- return encode(data, "+", "/", "=", 64, "\r\n")
22
+ return encode(data, "+", "/", "=", 64, "\r\n")
22
23
 
23
- # Encode for MIME (RFC 2045): 76-character lines.
24
+ # Encode for MIME (RFC 2045): 76-character lines.
24
- fun encodeMIME(data: Str) -> Str =
25
+ fun encodeMIME(data: Str) -> Str =
25
- return encode(data, "+", "/", "=", 76, "\r\n")
26
+ return encode(data, "+", "/", "=", 76, "\r\n")
26
27
 
27
- # Encode for URLs (RFC 4648). Padding characters are stripped by default.
28
+ # Encode for URLs (RFC 4648). Padding characters are stripped by default.
28
- fun encodeURL(data: Str, pad: Bool = false) -> Str =
29
+ fun encodeURL(data: Str, pad: Bool = false) -> Str =
29
- return encode(data, "-", "_", if pad then "=" else "", 0, "")
30
+ return encode(data, "-", "_", if pad then "=" else "", 0, "")
30
31
 
31
- # Configurable encoding. The defaults are for RFC 4648. `pad` is the padding
32
+ # Configurable encoding. The defaults are for RFC 4648. `pad` is the padding
32
- # character, or `""` to omit padding entirely; `linelen` is the output line
33
+ # character, or `""` to omit padding entirely; `linelen` is the output line
33
- # length to wrap at (`0` disables wrapping), separated by `linesep`.
34
+ # length to wrap at (`0` disables wrapping), separated by `linesep`.
34
- fun encode(data: Str, at62: Str = "+", at63: Str = "/", pad: Str = "=", linelen: Int = 0, linesep: Str = "\r\n") -> Str =
35
+ fun encode(data: Str, at62: Str = "+", at63: Str = "/", pad: Str = "=", linelen: Int = 0, linesep: Str = "\r\n") -> Str =
35
- at62b := at62.byteAt(0)
36
+ at62b := at62.byteAt(0)
36
- at63b := at63.byteAt(0)
37
+ at63b := at63.byteAt(0)
37
- has_pad := pad.length() > 0
38
+ has_pad := pad.length() > 0
38
- padb := if has_pad then pad.byteAt(0) else 0
39
+ padb := if has_pad then pad.byteAt(0) else 0
39
- res := Buffer()
40
+ res := Buffer()
40
- n := data.length()
41
+ n := data.length()
41
- i := 0
42
+ i := 0
42
- line_chars := 0
43
+ line_chars := 0
43
- while i < n
44
+ while i < n
44
- b0 := data.byteAt(i)
45
+ b0 := data.byteAt(i)
45
- has1 := i + 1 < n
46
+ has1 := i + 1 < n
46
- has2 := i + 2 < n
47
+ has2 := i + 2 < n
47
- b1 := if has1 then data.byteAt(i + 1) else 0
48
+ b1 := if has1 then data.byteAt(i + 1) else 0
48
- b2 := if has2 then data.byteAt(i + 2) else 0
49
+ b2 := if has2 then data.byteAt(i + 2) else 0
49
- res.writeByte(Byte(encodeByte(b0 >> 2, at62b, at63b)))
50
+ res.writeByte(Byte(encodeByte(b0 >> 2, at62b, at63b)))
50
- res.writeByte(Byte(encodeByte({b0 & 0x03} << 4 | b1 >> 4, at62b, at63b)))
51
+ res.writeByte(Byte(encodeByte({b0 & 0x03} << 4 | b1 >> 4, at62b, at63b)))
51
- line_chars = line_chars + 2
52
+ line_chars = line_chars + 2
52
- if has1
53
+ if has1
53
- res.writeByte(Byte(encodeByte({b1 & 0x0F} << 2 | b2 >> 6, at62b, at63b)))
54
+ res.writeByte(Byte(encodeByte({b1 & 0x0F} << 2 | b2 >> 6, at62b, at63b)))
54
- line_chars = line_chars + 1
55
+ line_chars = line_chars + 1
55
- else if has_pad
56
+ else if has_pad
56
- res.writeByte(Byte(padb))
57
+ res.writeByte(Byte(padb))
57
- line_chars = line_chars + 1
58
+ line_chars = line_chars + 1
58
- if has2
59
+ if has2
59
- res.writeByte(Byte(encodeByte(b2 & 0x3F, at62b, at63b)))
60
+ res.writeByte(Byte(encodeByte(b2 & 0x3F, at62b, at63b)))
60
- line_chars = line_chars + 1
61
+ line_chars = line_chars + 1
61
- else if has_pad
62
+ else if has_pad
62
- res.writeByte(Byte(padb))
63
+ res.writeByte(Byte(padb))
63
- line_chars = line_chars + 1
64
+ line_chars = line_chars + 1
64
- i = i + 3
65
+ i = i + 3
65
- if linelen > 0 && line_chars >= linelen && i < n
66
+ if linelen > 0 && line_chars >= linelen && i < n
66
- res.write(linesep)
67
+ res.write(linesep)
67
- line_chars = 0
68
+ line_chars = 0
68
- return res.toStr()
69
+ return res.toStr()
69
70
 
70
- # Encodes a single 6-bit value (0-63) as its base64 alphabet character.
71
+ # Encodes a single 6-bit value (0-63) as its base64 alphabet character.
71
- fun encodeByte(i: Int, at62: Int, at63: Int) -> Int =
72
+ fun encodeByte(i: Int, at62: Int, at63: Int) -> Int =
72
- if i < 26
73
+ if i < 26
73
- return 65 + i # 'A'..'Z'
74
+ return 65 + i # 'A'..'Z'
74
- if i < 52
75
+ if i < 52
75
- return 97 + {i - 26} # 'a'..'z'
76
+ return 97 + {i - 26} # 'a'..'z'
76
- if i < 62
77
+ if i < 62
77
- return 48 + {i - 52} # '0'..'9'
78
+ return 48 + {i - 52} # '0'..'9'
78
- if i == 62
79
+ if i == 62
79
- return at62
80
+ return at62
80
- return at63
81
+ return at63
81
82
 
82
- # The inverse of `encodeByte`: a base64 alphabet character's 6-bit value, or
83
+ # The inverse of `encodeByte`: a base64 alphabet character's 6-bit value, or
83
- # -1 if `b` isn't one of the 64 alphabet characters (whitespace/padding are
84
+ # -1 if `b` isn't one of the 64 alphabet characters (whitespace/padding are
84
- # handled by the caller, not here).
85
+ # handled by the caller, not here).
85
- fun decodeByte(b: Int, at62: Int, at63: Int) -> Int =
86
+ fun decodeByte(b: Int, at62: Int, at63: Int) -> Int =
86
- if b >= 65 && b <= 90
87
+ if b >= 65 && b <= 90
87
- return b - 65
88
+ return b - 65
88
- if b >= 97 && b <= 122
89
+ if b >= 97 && b <= 122
89
- return b - 97 + 26
90
+ return b - 97 + 26
90
- if b >= 48 && b <= 57
91
+ if b >= 48 && b <= 57
91
- return b - 48 + 52
92
+ return b - 48 + 52
92
- if b == at62
93
+ if b == at62
93
- return 62
94
+ return 62
94
- if b == at63
95
+ if b == at63
95
- return 63
96
+ return 63
96
- return -1
97
+ return -1
97
98
 
98
- fun isBase64Whitespace(b: Int) -> Bool =
99
+ fun isBase64Whitespace(b: Int) -> Bool =
99
- b == 32 || b == 9 || b == 10 || b == 13
100
+ b == 32 || b == 9 || b == 10 || b == 13
100
101
 
101
- # Decode for URLs (RFC 4648).
102
+ # Decode for URLs (RFC 4648).
102
- fun decodeUrl(data: Str) -> error{InvalidBase64}!Str =
103
+ fun decodeUrl(data: Str) -> error{InvalidBase64}!Str =
103
- return decode(data, "-", "_", "=")
104
+ return decode(data, "-", "_", "=")
104
105
 
105
- # Configurable decoding. The defaults are for RFC 4648. Missing padding is
106
+ # Configurable decoding. The defaults are for RFC 4648. Missing padding is
106
- # not an error. Non-base64 data, other than whitespace (which can appear at
107
+ # not an error. Non-base64 data, other than whitespace (which can appear at
107
- # any point), fails with `InvalidBase64`.
108
+ # any point), fails with `InvalidBase64`.
108
- fun decode(data: Str, at62: Str = "+", at63: Str = "/", pad: Str = "=") -> error{InvalidBase64}!Str =
109
+ fun decode(data: Str, at62: Str = "+", at63: Str = "/", pad: Str = "=") -> error{InvalidBase64}!Str =
109
- at62b := at62.byteAt(0)
110
+ at62b := at62.byteAt(0)
110
- at63b := at63.byteAt(0)
111
+ at63b := at63.byteAt(0)
111
- has_pad := pad.length() > 0
112
+ has_pad := pad.length() > 0
112
- padb := if has_pad then pad.byteAt(0) else -1
113
+ padb := if has_pad then pad.byteAt(0) else -1
113
- res := Buffer()
114
+ res := Buffer()
114
- n := data.length()
115
+ n := data.length()
115
- i := 0
116
+ i := 0
116
- bits := 0
117
+ bits := 0
117
- nbits := 0
118
+ nbits := 0
118
- while i < n
119
+ while i < n
119
- b := data.byteAt(i)
120
+ b := data.byteAt(i)
120
- i = i + 1
121
+ i = i + 1
121
- if isBase64Whitespace(b) || b == padb
122
+ if isBase64Whitespace(b) || b == padb
122
- continue
123
+ continue
123
- v := decodeByte(b, at62b, at63b)
124
+ v := decodeByte(b, at62b, at63b)
124
- if v < 0
125
+ if v < 0
125
- return error.InvalidBase64
126
+ return error.InvalidBase64
126
- bits = {bits << 6} | v
127
+ bits = {bits << 6} | v
127
- nbits = nbits + 6
128
+ nbits = nbits + 6
128
- if nbits >= 8
129
+ if nbits >= 8
129
- nbits = nbits - 8
130
+ nbits = nbits - 8
130
- res.writeByte(Byte({bits >> nbits} & 0xFF))
131
+ res.writeByte(Byte({bits >> nbits} & 0xFF))
131
- return res.toStr()
132
+ return res.toStr()
133
+
134
+ test "statics are reached through the Base64 namespace and round-trip"
135
+ assert Base64.decode(Base64.encodePEM("hello"))! == "hello"
136
+ assert Base64.decode(Base64.encodeMIME("hi there"))! == "hi there"
plum-std/Buffer.plum CHANGED
@@ -21,13 +21,13 @@ record Buffer(data: []Byte, len: Int) =
21
21
  # and `plum-wasm-codegen`'s `ClassCall` handling), so this is the only place
22
22
  # that ever needs to know `data`/`len`'s starting values.
23
23
  fun init() -> Buffer =
24
- Buffer(data: makeBytes(0), len: 0)
24
+ Buffer(data: ByteSlice.make(0), len: 0)
25
25
 
26
26
  # Appends `s` to the buffer.
27
27
  fun write(self, s: Str) -> Unit =
28
28
  n := s.length()
29
29
  self.ensureCapacity(self.len + n)
30
- copyStrToBytes(self.data, self.len, s, 0, n)
30
+ ByteSlice.copy(self.data, self.len, s.bytes, 0, n)
31
31
  self.len = self.len + n
32
32
 
33
33
  # Appends the single raw byte `b` to the buffer.
@@ -38,11 +38,11 @@ record Buffer(data: []Byte, len: Int) =
38
38
 
39
39
  # Appends the Unicode codepoint `cp`, UTF-8 encoded, to the buffer.
40
40
  fun writeRune(self, cp: Int) -> Unit =
41
- self.write(codePointToStr(cp))
41
+ self.write(Str.codePointToStr(cp))
42
42
 
43
43
  # Returns the buffer's contents as a `Str`.
44
44
  fun toStr(self) -> Str =
45
- bytesToStr(self.data, 0, self.len)
45
+ Str.fromBytes(self.data, 0, self.len)
46
46
 
47
47
  # Number of bytes currently in the buffer.
48
48
  fun length(self) -> Int =
@@ -67,18 +67,15 @@ record Buffer(data: []Byte, len: Int) =
67
67
  return
68
68
  new_cap := if cap == 0 then 16 else cap * 2
69
69
  new_cap = if needed > new_cap then needed else new_cap
70
- new_data := makeBytes(new_cap)
70
+ new_data := ByteSlice.make(new_cap)
71
- copyBytes(new_data, 0, self.data, 0, self.len)
71
+ ByteSlice.copy(new_data, 0, self.data, 0, self.len)
72
72
  self.data = new_data
73
73
 
74
- fun exerciseBufferInit() -> Str =
74
+ test "Buffer() with no args runs init() and works immediately"
75
75
  b := Buffer()
76
76
  b.write("hello ")
77
77
  b.write("world")
78
- b.toStr()
79
-
80
- test "Buffer() with no args runs init() and works immediately"
81
- assert exerciseBufferInit() == "hello world"
78
+ assert b.toStr() == "hello world"
82
79
 
83
80
  test "toStr length equals bytes written, even after capacity doubling"
84
81
  b := Buffer()
plum-std/ByteSlice.plum CHANGED
@@ -8,10 +8,10 @@ import std/Float
8
8
  # fixed-length, mutable sequence of raw bytes backed directly by a wasm-gc
9
9
  # `array<i8>` (see `plum-wasm-codegen`'s `PlumType::TByteSlice`) — the same
10
10
  # raw array type `Buffer`'s own `data` field uses underneath (`Str` is a record wrapping one of these;
11
- # see `Str.plum`). Every method here, and `makeBytes`/
11
+ # see `Str.plum`). Every method here, including the statics
12
- # `copyBytes` below, is a compiler intrinsic (see `compileIntrinsicFnBody`)
12
+ # `make`/`copy`/`equal`, is a compiler intrinsic (see `compileIntrinsicFnBody`)
13
13
  # — there's no way to express raw array length/indexing/construction/copying
14
- # in Plum source itself; `copyStrToBytes`/`bytesToStr` build on top of them
14
+ # in Plum source itself; `Str.fromBytes` and `Buffer` build on top of them
15
15
  # instead of needing intrinsics of their own.
16
16
  primitive ByteSlice =
17
17
 
@@ -27,49 +27,30 @@ primitive ByteSlice =
27
27
  fun set(self, i: Int, b: Byte) -> Unit =
28
28
  todo
29
29
 
30
- # A new zero-filled `[]Byte` of length `n`.
30
+ # A new zero-filled `[]Byte` of length `n`.
31
- fun makeBytes(n: Int) -> []Byte =
31
+ fun make(n: Int) -> []Byte =
32
- todo
32
+ todo
33
-
34
- # Copies `n` bytes from `src` (starting at `srcStart`) into `dst` (starting at
35
- # `dstStart`). `dst` and `src` may be the same slice — overlapping ranges are
36
- # handled correctly, matching wasm's `array.copy`.
37
- fun copyBytes(dst: []Byte, dstStart: Int, src: []Byte, srcStart: Int, n: Int) -> Unit =
38
- todo
39
-
40
- # True when `a[aStart..aStart+n]` and `b[bStart..bStart+n]` hold the same
41
- # bytes (the memcmp analog; `n == 0` is always true). Traps if out of range.
42
- fun bytesEqual(a: []Byte, aStart: Int, b: []Byte, bStart: Int, n: Int) -> Bool =
43
- todo
44
-
45
- # Like `copyBytes`, but the source is a `Str` — ordinary Plum glue over
46
- # `copyBytes` itself, reaching through `Str`'s `bytes: []Byte` field.
47
- fun copyStrToBytes(dst: []Byte, dstStart: Int, src: Str, srcStart: Int, n: Int) -> Unit =
48
- copyBytes(dst, dstStart, src.bytes, srcStart, n)
49
33
 
50
- # Copies out `n` bytes of `src` starting at `start` into a fresh, independent
34
+ # Copies `n` bytes from `src` (starting at `srcStart`) into `dst` (starting at
51
- # `Str` — never aliases `src`, so mutating `src` afterwards can't
35
+ # `dstStart`). `dst` and `src` may be the same slice — overlapping ranges are
52
- # retroactively change an already-returned `Str`. Ordinary Plum: allocate a
53
- # right-sized `[]Byte`, copy into it, wrap it in a fresh `Str`.
36
+ # handled correctly, matching wasm's `array.copy`.
54
- fun bytesToStr(src: []Byte, start: Int, n: Int) -> Str =
37
+ fun copy(dst: []Byte, dstStart: Int, src: []Byte, srcStart: Int, n: Int) -> Unit =
55
- data := makeBytes(n)
56
- copyBytes(data, 0, src, start, n)
57
- return Str(bytes: data)
38
+ todo
58
39
 
40
+ # True when `a[aStart..aStart+n]` and `b[bStart..bStart+n]` hold the same
41
+ # bytes (the memcmp analog; `n == 0` is always true). Traps if out of range.
59
- fun testBytesOf(s: Str) -> []Byte =
42
+ fun equal(a: []Byte, aStart: Int, b: []Byte, bStart: Int, n: Int) -> Bool =
60
- out := makeBytes(s.length())
61
- copyStrToBytes(out, 0, s, 0, s.length())
62
- return out
43
+ todo
63
44
 
64
- test "bytesEqual: equal, differ at first, differ at last, n == 0, offsets"
45
+ test "ByteSlice.equal: equal, differ at first, differ at last, n == 0, offsets"
65
- a := testBytesOf("hello world")
46
+ a := "hello world".bytes
66
- b := testBytesOf("hello world")
47
+ b := "hello world".bytes
67
- assert bytesEqual(a, 0, b, 0, 11)
48
+ assert ByteSlice.equal(a, 0, b, 0, 11)
68
- c := testBytesOf("jello world")
49
+ c := "jello world".bytes
69
- assert !bytesEqual(a, 0, c, 0, 11)
50
+ assert !ByteSlice.equal(a, 0, c, 0, 11)
70
- d := testBytesOf("hello worlD")
51
+ d := "hello worlD".bytes
71
- assert !bytesEqual(a, 0, d, 0, 11)
52
+ assert !ByteSlice.equal(a, 0, d, 0, 11)
72
- assert bytesEqual(a, 0, d, 0, 10)
53
+ assert ByteSlice.equal(a, 0, d, 0, 10)
73
- assert bytesEqual(a, 3, c, 3, 0)
54
+ assert ByteSlice.equal(a, 3, c, 3, 0)
74
- assert bytesEqual(a, 6, testBytesOf("xxworld"), 2, 5)
55
+ assert ByteSlice.equal(a, 6, "xxworld".bytes, 2, 5)
75
- assert !bytesEqual(a, 6, testBytesOf("xxwurld"), 2, 5)
56
+ assert !ByteSlice.equal(a, 6, "xxwurld".bytes, 2, 5)
plum-std/Float.plum CHANGED
@@ -183,5 +183,5 @@ primitive Float =
183
183
  carry := scaled >= 1000000
184
184
  whole_part := if carry then int_part + 1 else int_part
185
185
  frac := if carry then 0 else scaled
186
- s := intToStr(whole_part) + "." + trimmedFracDigits(frac, 6)
186
+ s := Int.intToStr(whole_part) + "." + Int.trimmedFracDigits(frac, 6)
187
187
  return if neg then "-" + s else s
plum-std/Http.plum CHANGED
@@ -28,9 +28,6 @@ record Response(status: Int, headers: Map[Str, Str], body: Str) =
28
28
  fun json(self) -> error{InvalidJson}!Json =
29
29
  return try Json.fromStr(self.body)
30
30
 
31
- fun emptyHeaders() -> Map[Str, Str] =
32
- return Map[Str, Str]()
33
-
34
31
  # A single blocking network round trip — this VM has no async/event-loop
35
32
  # mechanism, so `rawHttpRequest` runs to completion before returning (same
36
33
  # idea as `libs/std/os.plum`'s `rawReadFile`). `headers` is `"Key: Value"`
@@ -44,102 +41,106 @@ fun emptyHeaders() -> Map[Str, Str] =
44
41
  # when the body itself is arbitrary binary data.
45
42
  extern fun rawHttpRequest(method: Str, url: Str, headers: Str, body: Str) -> Str
46
43
 
44
+ primitive Http =
45
+ fun emptyHeaders() -> Map[Str, Str] =
46
+ return Map[Str, Str]()
47
+
47
- # Packs `headers` into the `"Key: Value"`-per-line format `rawHttpRequest`
48
+ # Packs `headers` into the `"Key: Value"`-per-line format `rawHttpRequest`
48
- # expects for its own `headers` argument.
49
+ # expects for its own `headers` argument.
49
- fun encodeHeaders(headers: Map[Str, Str]) -> Str =
50
+ fun encodeHeaders(headers: Map[Str, Str]) -> Str =
50
- buf := Buffer()
51
+ buf := Buffer()
51
- for k, v := range headers
52
+ for k, v := range headers
52
- if buf.length() > 0
53
+ if buf.length() > 0
53
- buf.write("\n")
54
+ buf.write("\n")
54
- buf.write(k)
55
+ buf.write(k)
55
- buf.write(": ")
56
+ buf.write(": ")
56
- buf.write(v)
57
+ buf.write(v)
57
- return buf.toStr()
58
+ return buf.toStr()
58
-
59
+
59
- # The reverse of `encodeHeaders`, for the response headers `rawHttpRequest`
60
+ # The reverse of `encodeHeaders`, for the response headers `rawHttpRequest`
60
- # packs into its result.
61
+ # packs into its result.
61
- fun decodeHeaders(raw: Str) -> Map[Str, Str] =
62
+ fun decodeHeaders(raw: Str) -> Map[Str, Str] =
62
- result := emptyHeaders()
63
+ result := emptyHeaders()
63
- if raw == ""
64
+ if raw == ""
65
+ return result
66
+ for line := range raw.split("\n", 0)
67
+ idx := line.indexOf(": ")
68
+ if idx >= 0
69
+ key := line.sub(0, idx)
70
+ val := line.sub(idx + 2, line.length())
71
+ result.set(key, val)
64
72
  return result
65
- for line := range raw.split("\n", 0)
73
+
66
- idx := line.indexOf(": ")
67
- if idx >= 0
68
- key := line.sub(0, idx)
69
- val := line.sub(idx + 2, line.length())
70
- result.set(key, val)
71
- return result
72
-
73
- # Performs a single HTTP request and waits for the full response. Fails
74
+ # Performs a single HTTP request and waits for the full response. Fails
74
- # only for a transport-level problem (DNS failure, connection refused, TLS
75
+ # only for a transport-level problem (DNS failure, connection refused, TLS
75
- # error, ...) — an HTTP-level error status (404, 500, ...) is still a
76
+ # error, ...) — an HTTP-level error status (404, 500, ...) is still a
76
- # successful round trip, returned as a `Response` with that status (check
77
+ # successful round trip, returned as a `Response` with that status (check
77
- # `Response.ok()`). Fails with `HttpFailed` for a transport error and
78
+ # `Response.ok()`). Fails with `HttpFailed` for a transport error and
78
- # `BadResponse` for a reply whose status line can't be read.
79
+ # `BadResponse` for a reply whose status line can't be read.
79
- fun request(method: Str, url: Str, headers: Map[Str, Str], body: Str) -> HttpError!Response =
80
+ fun request(method: Str, url: Str, headers: Map[Str, Str], body: Str) -> HttpError!Response =
80
- raw := rawHttpRequest(method, url, encodeHeaders(headers), body)
81
+ raw := rawHttpRequest(method, url, encodeHeaders(headers), body)
81
- sep := raw.indexOf("\x00")
82
+ sep := raw.indexOf("\x00")
82
- meta := if sep >= 0 then raw.sub(0, sep) else raw
83
+ meta := if sep >= 0 then raw.sub(0, sep) else raw
83
- resp_body := if sep >= 0 then raw.sub(sep + 1, raw.length()) else ""
84
+ resp_body := if sep >= 0 then raw.sub(sep + 1, raw.length()) else ""
84
- parts := meta.split("\x01", 3)
85
+ parts := meta.split("\x01", 3)
85
- flag := parts.get(0) catch ""
86
+ flag := parts.get(0) catch ""
86
- if flag != "1"
87
+ if flag != "1"
87
- return error.HttpFailed
88
+ return error.HttpFailed
88
- status := Int.parse(parts.get(1) catch "") catch return error.BadResponse
89
+ status := Int.parse(parts.get(1) catch "") catch return error.BadResponse
89
- return Response(status: status, headers: decodeHeaders(parts.get(2) catch ""), body: resp_body)
90
+ return Response(status: status, headers: decodeHeaders(parts.get(2) catch ""), body: resp_body)
90
-
91
+
91
- fun get(url: Str, headers: Map[Str, Str]) -> HttpError!Response =
92
+ fun get(url: Str, headers: Map[Str, Str]) -> HttpError!Response =
92
- return request("GET", url, headers, "")
93
+ return request("GET", url, headers, "")
93
-
94
+
94
- fun head(url: Str, headers: Map[Str, Str]) -> HttpError!Response =
95
+ fun head(url: Str, headers: Map[Str, Str]) -> HttpError!Response =
95
- return request("HEAD", url, headers, "")
96
+ return request("HEAD", url, headers, "")
96
-
97
+
97
- fun delete(url: Str, headers: Map[Str, Str]) -> HttpError!Response =
98
+ fun delete(url: Str, headers: Map[Str, Str]) -> HttpError!Response =
98
- return request("DELETE", url, headers, "")
99
+ return request("DELETE", url, headers, "")
99
-
100
+
100
- fun post(url: Str, headers: Map[Str, Str], body: Str) -> HttpError!Response =
101
+ fun post(url: Str, headers: Map[Str, Str], body: Str) -> HttpError!Response =
101
- return request("POST", url, headers, body)
102
+ return request("POST", url, headers, body)
102
-
103
+
103
- fun put(url: Str, headers: Map[Str, Str], body: Str) -> HttpError!Response =
104
+ fun put(url: Str, headers: Map[Str, Str], body: Str) -> HttpError!Response =
104
- return request("PUT", url, headers, body)
105
+ return request("PUT", url, headers, body)
105
-
106
+
106
- fun patch(url: Str, headers: Map[Str, Str], body: Str) -> HttpError!Response =
107
+ fun patch(url: Str, headers: Map[Str, Str], body: Str) -> HttpError!Response =
107
- return request("PATCH", url, headers, body)
108
+ return request("PATCH", url, headers, body)
108
-
109
+
109
- # Convenience wrapper: sends `body` as a JSON `Str`, adding a
110
+ # Convenience wrapper: sends `body` as a JSON `Str`, adding a
110
- # `Content-Type: application/json` header, and parses the response back as
111
+ # `Content-Type: application/json` header, and parses the response back as
111
- # JSON via `Response.json()`.
112
+ # JSON via `Response.json()`.
112
- fun postJson(url: Str, headers: Map[Str, Str], body: Json) -> HttpError!Response =
113
+ fun postJson(url: Str, headers: Map[Str, Str], body: Json) -> HttpError!Response =
113
- headers.set("Content-Type", "application/json")
114
+ headers.set("Content-Type", "application/json")
114
- return post(url, headers, body.toStr())
115
+ return post(url, headers, body.toStr())
115
116
 
116
117
  test "encodeHeaders/decodeHeaders round-trip a header map"
117
- h := emptyHeaders()
118
+ h := Http.emptyHeaders()
118
119
  h.set("Content-Type", "application/json")
119
120
  h.set("Accept", "*/*")
120
- decoded := decodeHeaders(encodeHeaders(h))
121
+ decoded := Http.decodeHeaders(Http.encodeHeaders(h))
121
122
  assert decoded.get("Content-Type")! == "application/json"
122
123
  assert decoded.get("Accept")! == "*/*"
123
124
 
124
125
  test "decodeHeaders returns an empty map for an empty string"
125
- assert decodeHeaders("").isEmpty() == true
126
+ assert Http.decodeHeaders("").isEmpty() == true
126
127
 
127
128
  test "get performs a real HTTP round trip"
128
- resp := get("https://httpbin.org/get", emptyHeaders())!
129
+ resp := Http.get("https://httpbin.org/get", Http.emptyHeaders())!
129
130
  assert resp.ok() == true
130
131
  assert resp.status == 200
131
132
 
132
133
  test "get reports a non-2xx status without failing the request"
133
- resp := get("https://httpbin.org/status/404", emptyHeaders())!
134
+ resp := Http.get("https://httpbin.org/status/404", Http.emptyHeaders())!
134
135
  assert resp.ok() == false
135
136
  assert resp.status == 404
136
137
 
137
138
  test "post sends a body and the server echoes it back"
138
- resp := post("https://httpbin.org/post", emptyHeaders(), "hello world")!
139
+ resp := Http.post("https://httpbin.org/post", Http.emptyHeaders(), "hello world")!
139
140
  assert resp.ok() == true
140
141
  j := resp.json()!
141
142
  assert j.asMap()!.get("data")!.asStr()! == "hello world"
142
143
 
143
144
  test "request fails for an unreachable host"
144
- resp := get("http://this-host-does-not-exist.invalid/", emptyHeaders()) catch Response(status: 0, headers: emptyHeaders(), body: "")
145
+ resp := Http.get("http://this-host-does-not-exist.invalid/", Http.emptyHeaders()) catch Response(status: 0, headers: Http.emptyHeaders(), body: "")
145
146
  assert resp.status == 0
plum-std/Int.plum CHANGED
@@ -146,60 +146,60 @@ primitive Int =
146
146
  i = i - 1
147
147
  yield cur
148
148
 
149
- # ---- free-function helpers (no `self` to dispatch on) ----
150
-
151
- # Plain decimal rendering of an `Int` — factored out of `Float.toStr` so the
152
- # `Float` branch can reuse it (via `intToStr(whole_part)`) without
153
- # re-dispatching back through `Float.toStr` itself.
154
- fun intToStr(n: Int) -> Str =
155
- if n == 0
156
- return "0"
157
- neg := n < 0
158
- v := if neg then -n else n
159
- s := digitsToStr(v)
160
- return if neg then "-" + s else s
161
-
162
- # Builds the decimal digits of a positive Int, most significant first, by
163
- # peeling off one digit at a time from the least significant end (so the
164
- # recursion emits digits in reverse call order — the leading, smallest-place
165
- # digit is appended last).
166
- fun digitsToStr(n: Int) -> Str =
167
- if n == 0
168
- return ""
169
- return digitsToStr(n / 10) + digitChar(n % 10)
170
-
171
- fun digitChar(d: Int) -> Str =
172
- match d
173
- 0 => "0"
174
- 1 => "1"
175
- 2 => "2"
176
- 3 => "3"
177
- 4 => "4"
178
- 5 => "5"
179
- 6 => "6"
180
- 7 => "7"
181
- 8 => "8"
182
- 9 => "9"
183
- _ => "0"
184
-
185
- # Renders `n`'s digits most-significant-first, always emitting exactly as many
186
- # digits as `place_value` has powers of ten (leading zeros included) — used to
187
- # render a fractional part at a fixed width regardless of its own leading zeros.
188
- fun fixedFracDigits(n: Int, place_value: Int) -> Str =
189
- if place_value == 0
190
- return ""
191
- return digitChar(n / place_value % 10) + fixedFracDigits(n, place_value / 10)
192
-
193
- fun pow10(n: Int) -> Int =
194
- if n <= 0
195
- return 1
196
- return 10 * pow10(n - 1)
197
-
198
- # `fixedFracDigits(n, pow10(digits - 1))`, minus however many trailing-zero
199
- # digits `n` has — but always at least one digit ("0" if `n` is 0 outright).
200
- fun trimmedFracDigits(n: Int, digits: Int) -> Str =
201
- if n == 0 || digits <= 1
202
- return digitChar(n % 10)
203
- if n % 10 == 0
204
- return trimmedFracDigits(n / 10, digits - 1)
205
- return fixedFracDigits(n, pow10(digits - 1))
149
+ # ---- static helpers (no `self`) ----
150
+
151
+ # Plain decimal rendering of an `Int` — factored out of `Float.toStr` so the
152
+ # `Float` branch can reuse it (via `intToStr(whole_part)`) without
153
+ # re-dispatching back through `Float.toStr` itself.
154
+ fun intToStr(n: Int) -> Str =
155
+ if n == 0
156
+ return "0"
157
+ neg := n < 0
158
+ v := if neg then -n else n
159
+ s := digitsToStr(v)
160
+ return if neg then "-" + s else s
161
+
162
+ # Builds the decimal digits of a positive Int, most significant first, by
163
+ # peeling off one digit at a time from the least significant end (so the
164
+ # recursion emits digits in reverse call order — the leading, smallest-place
165
+ # digit is appended last).
166
+ fun digitsToStr(n: Int) -> Str =
167
+ if n == 0
168
+ return ""
169
+ return digitsToStr(n / 10) + digitChar(n % 10)
170
+
171
+ fun digitChar(d: Int) -> Str =
172
+ match d
173
+ 0 => "0"
174
+ 1 => "1"
175
+ 2 => "2"
176
+ 3 => "3"
177
+ 4 => "4"
178
+ 5 => "5"
179
+ 6 => "6"
180
+ 7 => "7"
181
+ 8 => "8"
182
+ 9 => "9"
183
+ _ => "0"
184
+
185
+ # Renders `n`'s digits most-significant-first, always emitting exactly as many
186
+ # digits as `place_value` has powers of ten (leading zeros included) — used to
187
+ # render a fractional part at a fixed width regardless of its own leading zeros.
188
+ fun fixedFracDigits(n: Int, place_value: Int) -> Str =
189
+ if place_value == 0
190
+ return ""
191
+ return digitChar(n / place_value % 10) + fixedFracDigits(n, place_value / 10)
192
+
193
+ fun pow10(n: Int) -> Int =
194
+ if n <= 0
195
+ return 1
196
+ return 10 * pow10(n - 1)
197
+
198
+ # `fixedFracDigits(n, pow10(digits - 1))`, minus however many trailing-zero
199
+ # digits `n` has — but always at least one digit ("0" if `n` is 0 outright).
200
+ fun trimmedFracDigits(n: Int, digits: Int) -> Str =
201
+ if n == 0 || digits <= 1
202
+ return digitChar(n % 10)
203
+ if n % 10 == 0
204
+ return trimmedFracDigits(n / 10, digits - 1)
205
+ return fixedFracDigits(n, pow10(digits - 1))
plum-std/Json.plum CHANGED
@@ -77,6 +77,82 @@ enum Json =
77
77
  Map => self
78
78
  _ => error.WrongJsonType
79
79
 
80
+ # `Map` has no built-in `toStr`/`join` (unlike `List`, which uses `join` for
81
+ # `Json.toStr`'s `JsonList` case above) — a static taking the
82
+ # `Buffer` as a captured-by-reference parameter instead of a closure sidesteps
83
+ # `libs/std/list.plum`'s `join` note on closures: reassigning a CAPTURED
84
+ # variable inside a closure body doesn't persist across that closure's own
85
+ # repeated calls, but mutating a heap object's fields THROUGH a captured
86
+ # reference (as `appendJsonPair` does to `buf`) does.
87
+ fun jsonMapToStr(pairs: Map[Str, Json]) -> Str =
88
+ buf := Buffer()
89
+ for k, v := range pairs
90
+ appendJsonPair(buf, k, v)
91
+ return buf.toStr()
92
+
93
+ fun appendJsonPair(buf: Buffer, k: Str, v: Json) -> Unit =
94
+ if !buf.isEmpty()
95
+ buf.write(",")
96
+ buf.write("\"")
97
+ buf.write(escapeJsonStr(k))
98
+ buf.write("\":")
99
+ buf.write(v.toStr())
100
+
101
+ fun escapeJsonStr(s: Str) -> Str =
102
+ buf := Buffer()
103
+ len := s.length()
104
+ i := 0
105
+ while i < len
106
+ b := s.byteAt(i)
107
+ if b == 34
108
+ buf.write("\\\"")
109
+ else if b == 92
110
+ buf.write("\\\\")
111
+ else if b == 10
112
+ buf.write("\\n")
113
+ else if b == 13
114
+ buf.write("\\r")
115
+ else if b == 9
116
+ buf.write("\\t")
117
+ else if b == 8
118
+ buf.write("\\b")
119
+ else if b == 12
120
+ buf.write("\\f")
121
+ else if b < 0x20
122
+ buf.write("\\u00")
123
+ buf.write(hexByteStr(b))
124
+ else
125
+ # Copies a whole UTF-8 codepoint at once: copying a lone continuation
126
+ # byte as its own "character" would corrupt any non-ASCII text.
127
+ n := s.codePointByteLength(i)
128
+ buf.write(s.sub(i, i + n))
129
+ i = i + n - 1
130
+ i = i + 1
131
+ return buf.toStr()
132
+
133
+ fun hexByteStr(b: Int) -> Str =
134
+ hexDigitChar(b / 16) + hexDigitChar(b % 16)
135
+
136
+ fun hexDigitChar(d: Int) -> Str =
137
+ match d
138
+ 0 => "0"
139
+ 1 => "1"
140
+ 2 => "2"
141
+ 3 => "3"
142
+ 4 => "4"
143
+ 5 => "5"
144
+ 6 => "6"
145
+ 7 => "7"
146
+ 8 => "8"
147
+ 9 => "9"
148
+ 10 => "a"
149
+ 11 => "b"
150
+ 12 => "c"
151
+ 13 => "d"
152
+ 14 => "e"
153
+ 15 => "f"
154
+ _ => "0"
155
+
80
156
  # The outcome of `Json.parse`: the document, or where and why parsing failed.
81
157
  record JsonParsed(value: Json)
82
158
 
@@ -147,7 +223,7 @@ record JsonParser(src: Str, pos: Int, text: Str) =
147
223
 
148
224
  fun parseLiteral(self, lit: Str, value: Json) -> error{InvalidJson}!Json =
149
225
  n := lit.length()
150
- if !matchesAt(self.src, lit, self.pos, n)
226
+ if !Str.matchesAt(self.src, lit, self.pos, n)
151
227
  self.note("invalid literal, expected '{lit}'")
152
228
  return error.InvalidJson
153
229
  self.pos = self.pos + n
@@ -331,109 +407,26 @@ record JsonParser(src: Str, pos: Int, text: Str) =
331
407
  self.advance()
332
408
  return pairs
333
409
 
334
- # `Map` has no built-in `toStr`/`join` (unlike `List`, which uses `join` for
335
- # `Json.toStr`'s `JsonList` case above) — a plain free function taking the
336
- # `Buffer` as a captured-by-reference parameter instead of a closure sidesteps
337
- # `libs/std/list.plum`'s `join` note on closures: reassigning a CAPTURED
338
- # variable inside a closure body doesn't persist across that closure's own
339
- # repeated calls, but mutating a heap object's fields THROUGH a captured
340
- # reference (as `appendJsonPair` does to `buf`) does.
341
- fun jsonMapToStr(pairs: Map[Str, Json]) -> Str =
342
- buf := Buffer()
343
- for k, v := range pairs
344
- appendJsonPair(buf, k, v)
345
- return buf.toStr()
346
-
347
- fun appendJsonPair(buf: Buffer, k: Str, v: Json) -> Unit =
348
- if !buf.isEmpty()
349
- buf.write(",")
350
- buf.write("\"")
351
- buf.write(escapeJsonStr(k))
352
- buf.write("\":")
353
- buf.write(v.toStr())
354
-
355
- fun exponentIndex(text: Str) -> Int =
410
+ fun exponentIndex(text: Str) -> Int =
356
- e := text.indexOf("e")
411
+ e := text.indexOf("e")
357
- if e >= 0 then e else text.indexOf("E")
412
+ if e >= 0 then e else text.indexOf("E")
358
-
359
- fun escapeJsonStr(s: Str) -> Str =
360
- buf := Buffer()
361
- len := s.length()
362
- i := 0
363
- while i < len
364
- b := s.byteAt(i)
365
- if b == 34
366
- buf.write("\\\"")
367
- else if b == 92
368
- buf.write("\\\\")
369
- else if b == 10
370
- buf.write("\\n")
371
- else if b == 13
372
- buf.write("\\r")
373
- else if b == 9
374
- buf.write("\\t")
375
- else if b == 8
376
- buf.write("\\b")
377
- else if b == 12
378
- buf.write("\\f")
379
- else if b < 0x20
380
- buf.write("\\u00")
381
- buf.write(hexByteStr(b))
382
- else
383
- # Copies a whole UTF-8 codepoint at once: copying a lone continuation
384
- # byte as its own "character" would corrupt any non-ASCII text.
385
- n := s.codePointByteLength(i)
386
- buf.write(s.sub(i, i + n))
387
- i = i + n - 1
388
- i = i + 1
389
- return buf.toStr()
390
-
391
- fun hexByteStr(b: Int) -> Str =
392
- hexDigitChar(b / 16) + hexDigitChar(b % 16)
393
-
394
- fun hexDigitChar(d: Int) -> Str =
395
- match d
396
- 0 => "0"
397
- 1 => "1"
398
- 2 => "2"
399
- 3 => "3"
400
- 4 => "4"
401
- 5 => "5"
402
- 6 => "6"
403
- 7 => "7"
404
- 8 => "8"
405
- 9 => "9"
406
- 10 => "a"
407
- 11 => "b"
408
- 12 => "c"
409
- 13 => "d"
410
- 14 => "e"
411
- 15 => "f"
412
- _ => "0"
413
-
414
- fun hexDigitValue(b: Int) -> Int =
415
- if b >= 48 && b <= 57
416
- return b - 48
417
- if b >= 97 && b <= 102
418
- return b - 97 + 10
419
- if b >= 65 && b <= 70
420
- return b - 65 + 10
421
- return -1
422
-
423
- fun isSpace(c: Int) -> Bool =
424
- c == 32 || c >= 9 && c <= 13
425
-
426
- fun isDigit(c: Int) -> Bool =
427
- c >= 48 && c <= 57
428
413
 
429
- # ---- regression tests ----
414
+ fun hexDigitValue(b: Int) -> Int =
415
+ if b >= 48 && b <= 57
416
+ return b - 48
417
+ if b >= 97 && b <= 102
418
+ return b - 97 + 10
419
+ if b >= 65 && b <= 70
420
+ return b - 65 + 10
421
+ return -1
430
422
 
423
+ fun isSpace(c: Int) -> Bool =
424
+ c == 32 || c >= 9 && c <= 13
431
425
 
426
+ fun isDigit(c: Int) -> Bool =
427
+ c >= 48 && c <= 57
432
428
 
433
- fun parseFloatValue(s: Str) -> Float =
434
- v := Json.fromStr(s) catch e
429
+ # ---- regression tests ----
435
- return -1.0f
436
- v.asFloat() catch -1.0f
437
430
 
438
431
  test "parses null, true, and false"
439
432
  assert Json.fromStr("null")!.isNull()
@@ -441,11 +434,11 @@ test "parses null, true, and false"
441
434
  assert !Json.fromStr("false")!.asBool()!
442
435
 
443
436
  test "parses integers, decimals, negatives, and exponents"
444
- assert parseFloatValue("42") == 42.0f
437
+ assert Json.fromStr("42")!.asFloat()! == 42.0f
445
- assert parseFloatValue("-7") == -7.0f
438
+ assert Json.fromStr("-7")!.asFloat()! == -7.0f
446
- assert parseFloatValue("3.5") == 3.5f
439
+ assert Json.fromStr("3.5")!.asFloat()! == 3.5f
447
- assert parseFloatValue("1e2") == 100.0f
440
+ assert Json.fromStr("1e2")!.asFloat()! == 100.0f
448
- assert parseFloatValue("1.5e-2") == 0.015f
441
+ assert Json.fromStr("1.5e-2")!.asFloat()! == 0.015f
449
442
 
450
443
  test "parses a plain string, with escapes"
451
444
  assert Json.fromStr("\"hello\"")!.asStr()! == "hello"
plum-std/List.plum CHANGED
@@ -234,69 +234,46 @@ record List[T](data: Array[T], size: Int) =
234
234
 
235
235
  # ---- regression tests ----
236
236
 
237
- fun optSumForListTest(o: error{OutOfBounds}!Int) -> Int =
237
+ test "list add set remove at remove clear reverse all work correctly"
238
- o catch -1000
239
-
240
- fun exerciseList() -> Int =
241
- l = List(1, 2, 3, 4, 5)
238
+ l := List(1, 2, 3, 4, 5)
242
- a = l.length()
239
+ assert l.length() == 5
243
- b = optSumForListTest(l.get(0))
240
+ assert l.get(0)! == 1
244
- c = optSumForListTest(l.get(4))
241
+ assert l.get(4)! == 5
245
- oldVal = optSumForListTest(l.set(2, 30))
242
+ assert l.set(2, 30)! == 3
246
- d = optSumForListTest(l.get(2))
243
+ assert l.get(2)! == 30
247
244
  l.removeAt(0)
248
- e = l.length()
245
+ assert l.length() == 4
249
- f = optSumForListTest(l.get(0))
246
+ assert l.get(0)! == 2
250
247
  l.remove(30)
251
- g = l.length()
248
+ assert l.length() == 3
252
249
  l.reverse()
253
- h = optSumForListTest(l.get(0))
250
+ assert l.get(0)! == 5
254
251
  l.clear()
255
- i = l.length()
252
+ assert l.length() == 0
256
- a + b + c + oldVal + d + e + f + g + h + i
257
253
 
258
- fun removeAllNodesOneAtATime() -> Int =
254
+ test "removing every node in a loop leaves an empty correctly functioning list"
259
- l = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
255
+ l := List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
260
- i = 0
256
+ i := 0
261
257
  while i < 10
262
258
  l.removeAt(0)
263
259
  i = i + 1
264
- afterLoopLength = l.length()
260
+ assert l.length() == 0
265
- isEmpty = optSumForListTest(l.get(0))
261
+ assert {l.get(0) catch -1000} == -1000
266
262
  l.add(42)
267
- afterReAdd = optSumForListTest(l.get(0))
263
+ assert l.get(0)! == 42
268
- afterLoopLength + isEmpty + afterReAdd
269
264
 
270
- fun exerciseListInit() -> Int =
265
+ test "List[Int]() with no args runs init() and produces a working empty list"
271
266
  l := List[Int]()
272
- before := l.length()
267
+ assert l.length() == 0
273
268
  l.add(7)
274
- before + l.length() + optSumForListTest(l.get(0))
275
-
276
- fun exerciseListInitWithValues() -> Int =
277
- l := List(10, 20, 30)
278
- l.length() + optSumForListTest(l.get(0)) + optSumForListTest(l.get(2))
279
-
280
- test "list add set remove at remove clear reverse all work correctly"
281
- # add(1,2,3,4,5): a=length=5, b=get(0)=1, c=get(4)=5
282
- # set(2,30): oldVal=3, d=get(2)=30 -> list [1,2,30,4,5]
283
- # removeAt(0): e=length=4, f=get(0)=2 -> list [2,30,4,5]
284
- # remove(30): g=length=3 -> list [2,4,5]
285
- # reverse(): h=get(0)=5 -> list [5,4,2]
286
- # clear(): i=length=0
287
- # 5+1+5+3+30+4+2+3+5+0 = 58
288
- assert exerciseList() == 58
269
+ assert l.length() == 1
289
-
290
- test "removing every node in a loop leaves an empty correctly functioning list"
291
- # afterLoopLength=0, isEmpty(get(0) on empty list)=-1000, afterReAdd=42
292
- # 0 + -1000 + 42 = -958
293
- assert removeAllNodesOneAtATime() == -958
294
-
295
- test "List[Int]() with no args runs init() and produces a working empty list"
296
- assert exerciseListInit() == 8
270
+ assert l.get(0)! == 7
297
271
 
298
272
  test "List(values) with positional args runs init(values) directly, infers T from args"
273
+ l := List(10, 20, 30)
299
- assert exerciseListInitWithValues() == 43
274
+ assert l.length() == 3
275
+ assert l.get(0)! == 10
276
+ assert l.get(2)! == 30
300
277
 
301
278
  test "list equals compares elementwise"
302
279
  a = List(1, 2, 3)
@@ -308,8 +285,8 @@ test "list equals compares elementwise"
308
285
  test "list sort orders elements correctly"
309
286
  l = List(3, 1, 4, 1, 5)
310
287
  sorted = l.sort(|a, b| a < b)
311
- assert optSumForListTest(sorted.get(0)) == 1
288
+ assert {sorted.get(0) catch -1000} == 1
312
- assert optSumForListTest(sorted.get(4)) == 5
289
+ assert {sorted.get(4) catch -1000} == 5
313
290
 
314
291
  test "list join renders elements correctly with separator"
315
292
  l = List(1, 2, 3)
@@ -338,7 +315,7 @@ test "list_get_set_push_pop: push 1000, get(999), pop until empty, get on empty
338
315
  i = i + 1
339
316
  assert l.length() == 1000
340
317
  assert l.get(999)! == 999
341
- assert optSumForListTest(l.get(1000)) == -1000
318
+ assert {l.get(1000) catch -1000} == -1000
342
319
  assert l.set(500, 7)! == 500
343
320
  assert l.get(500)! == 7
344
321
  assert l.pop()! == 999
@@ -347,7 +324,7 @@ test "list_get_set_push_pop: push 1000, get(999), pop until empty, get on empty
347
324
  l.pop()!
348
325
  assert l.length() == 0
349
326
  assert {l.pop() catch -1} == -1
350
- assert optSumForListTest(l.get(0)) == -1000
327
+ assert {l.get(0) catch -1000} == -1000
351
328
  e := l.get(0)
352
329
  assert {e catch -1} == -1
353
330
 
plum-std/Os.plum CHANGED
@@ -8,20 +8,6 @@ import std/Float
8
8
  # What the file functions that may hit either problem can fail with.
9
9
  error IoError = IoFailed | NoSuchPath
10
10
 
11
- # NOTE: the original file modeled `stdin`/`stdout`/`stderr` as bare top-level
12
- # variable bindings and a `File(...)` constructor, but this language has no
13
- # top-level `let` and `File` is never defined anywhere in the stdlib. Reduced
14
- # to the underlying path so this at least parses; a real `File` type is
15
- # still needed here.
16
- fun stdin() -> Str =
17
- "/dev/stdin"
18
-
19
- fun stdout() -> Str =
20
- "/dev/stdout"
21
-
22
- fun stderr() -> Str =
23
- "/dev/stderr"
24
-
25
11
  # Writes the specified data, followed by the current line terminator, to the
26
12
  # standard output stream. `extern` means this compiles to a genuine wasm import
27
13
  # (`plum::printLn`) instead of a Plum body — printing isn't expressible in wasm
@@ -57,261 +43,276 @@ extern fun rawChmod(path: Str, mode: Int) -> Int
57
43
  # an empty file — `exists`/`readdir` below both check existence first).
58
44
  extern fun rawReadDir(path: Str) -> Str
59
45
 
60
- # Reads the whole file at `path` as a `Str` (`Str` is a byte array here — see
61
- # `libs/std/str.plum` — so this works for binary content too, not just text).
62
- fun readFile(path: Str) -> error{NoSuchPath}!Str =
63
- if !exists(path)
64
- return error.NoSuchPath
65
- return rawReadFile(path)
66
-
67
- # Writes `data` to `path`, replacing its previous contents (or creating it).
68
- fun writeFile(path: Str, data: Str) -> error{IoFailed}!Unit =
69
- if rawWriteFile(path, data) != 1
70
- return error.IoFailed
71
-
72
- fun exists(path: Str) -> Bool =
73
- return rawExists(path) == 1
74
-
75
- fun mkdir(path: Str) -> error{IoFailed}!Unit =
76
- if rawMkdir(path) != 1
77
- return error.IoFailed
78
-
79
- # Removes the file (not directory) at `path`.
80
- fun remove(path: Str) -> IoError!Unit =
81
- if !exists(path)
82
- return error.NoSuchPath
83
- if rawRemove(path) != 1
84
- return error.IoFailed
85
-
86
- # Whether `path` exists and is reachable — this VM has no notion of a
87
- # process' effective uid/gid, so (unlike POSIX `access(2)`) there is no mode
88
- # argument to check permission bits against.
89
- fun access(path: Str) -> Bool =
90
- return exists(path)
91
-
92
- # Appends `data` to the file at `path`, creating it (with just `data` as its
93
- # contents) if it doesn't already exist. There's no host-level "open for
94
- # append" primitive, so this reads the existing bytes and writes the
95
- # concatenation back — not atomic, and O(existing file size) per call.
96
- fun appendFile(path: Str, data: Str) -> IoError!Unit =
97
- if exists(path)
98
- existing := try readFile(path)
99
- try writeFile(path, existing + data)
100
- return
101
- try writeFile(path, data)
102
-
103
- # Unix permission bits only (e.g. `0o644`) — this VM has no Windows ACL
104
- # equivalent to fall back to.
105
- fun chmod(path: Str, mode: Int) -> error{IoFailed}!Unit =
106
- if rawChmod(path, mode) != 1
107
- return error.IoFailed
108
-
109
- # There is no host primitive for changing file ownership (it needs a uid/gid
110
- # syscall this VM doesn't expose), unlike the permission-bits-only `chmod`.
111
- fun chown(path: Str, uid: Int, gid: Int) =
112
- todo
113
-
114
- fun copyFile(src: Str, dest: Str) -> IoError!Unit =
115
- data := try readFile(src)
116
- try writeFile(dest, data)
117
-
118
- fun cp(src: Str, dest: Str) -> IoError!Unit =
119
- try copyFile(src, dest)
120
-
121
- # Symlinks aren't modeled separately from regular files/dirs by this VM's
122
- # host primitives (`rawChmod` always follows them), so this is the same as
123
- # `chmod`.
124
- fun lchmod(path: Str, mode: Int) -> error{IoFailed}!Unit =
125
- try chmod(path, mode)
126
-
127
- # See `chown` — no uid/gid syscall exposed by this VM.
128
- fun lchown(path: Str, uid: Int, gid: Int) =
129
- todo
130
-
131
- # Setting a file's atime/mtime needs a syscall (`utimensat`/`SetFileTime`)
132
- # this VM doesn't expose (`std::fs` itself has no equivalent either).
133
- fun lutimes(path: Str, atime: Int, mtime: Int) =
134
- todo
135
-
136
- fun link(existing_path: Str, new_path: Str) -> error{IoFailed}!Unit =
137
- if rawLink(existing_path, new_path) != 1
138
- return error.IoFailed
139
-
140
- # Would need a real `Stat` struct type (size/mode/mtime/uid/gid/...) to
141
- # return anything useful — no such type exists in the stdlib yet.
142
- fun lstat(path: Str) =
143
- todo
144
-
145
- # Creates a fresh directory named `prefix` followed by a random suffix,
146
- # retrying on the (astronomically unlikely) chance of a collision, and
147
- # returns the path actually created.
148
- fun mkdtemp(prefix: Str) -> error{IoFailed}!Str =
149
- path := prefix + Uuid.v4().toStr()
150
- if exists(path)
151
- return mkdtemp(prefix)
152
- try mkdir(path)
153
- return path
154
-
155
- # Would need a host-side open-file-descriptor table (this VM's other file
156
- # ops are all one-shot: open+read/write+close in a single host call) plus a
157
- # `File` handle type — neither exists yet.
158
- fun open(path: Str, flags: Int) =
159
- todo
160
-
161
- # See `open` — same missing handle-table/type problem, for directories.
162
- fun opendir(path: Str) =
163
- todo
164
-
165
- # The names of `path`'s directory entries (not full paths).
166
- fun readdir(path: Str) -> error{NoSuchPath}!List[Str] =
167
- if !exists(path)
168
- return error.NoSuchPath
169
- # skipping empty names handles both a genuinely empty directory (whose
170
- # joined `""` would otherwise split into one bogus `""` entry) and
171
- # `rawReadDir`'s "unreadable" fallback (already ruled out by the `exists`
172
- # check above) the same way — no directory entry is ever named `""`.
173
- names := List[Str]()
174
- for n := range rawReadDir(path).split("\n", 0)
175
- if n != ""
176
- names.push(n)
177
- return names
178
-
179
- fun readlink(path: Str) -> error{NoSuchPath}!Str =
180
- if !exists(path)
181
- return error.NoSuchPath
182
- return rawReadlink(path)
183
-
184
- fun realpath(path: Str) -> error{NoSuchPath}!Str =
185
- if !exists(path)
186
- return error.NoSuchPath
187
- return rawRealpath(path)
188
-
189
- fun rename(old_path: Str, new_path: Str) -> IoError!Unit =
190
- if !exists(old_path)
191
- return error.NoSuchPath
192
- if rawRename(old_path, new_path) != 1
193
- return error.IoFailed
194
-
195
- # Removes an empty directory. (`remove` above covers plain files.)
196
- fun rmdir(path: Str) -> IoError!Unit =
197
- if !exists(path)
198
- return error.NoSuchPath
199
- if rawRemoveDir(path) != 1
200
- return error.IoFailed
201
-
202
- # See `lstat` — needs a `Stat` struct type.
203
- fun stat(path: Str) =
204
- todo
205
-
206
- # Needs a filesystem-info struct type, same gap as `stat`.
207
- fun statfs(path: Str) =
208
- todo
209
-
210
- fun symlink(target: Str, path: Str) -> error{IoFailed}!Unit =
211
- if rawSymlink(target, path) != 1
212
- return error.IoFailed
213
-
214
- # Resizes the file at `path` to exactly `len` bytes — truncated if shorter,
215
- # zero-padded if longer.
216
- fun truncate(path: Str, len: Int) -> IoError!Unit =
217
- if !exists(path)
218
- return error.NoSuchPath
219
- if rawTruncate(path, len) != 1
220
- return error.IoFailed
221
-
222
- # See `lutimes` — same missing syscall.
223
- fun utimes(path: Str, atime: Int, mtime: Int) =
224
- todo
225
-
226
- # Would need an async/callback mechanism to deliver filesystem change events
227
- # into a running Plum program — nothing like that exists in this VM (every
228
- # host call here is a synchronous, one-shot request/response).
229
- fun watch(filename: Str) =
230
- todo
46
+ primitive Os =
47
+ # NOTE: the original file modeled `stdin`/`stdout`/`stderr` as bare top-level
48
+ # variable bindings and a `File(...)` constructor, but this language has no
49
+ # top-level `let` and `File` is never defined anywhere in the stdlib. Reduced
50
+ # to the underlying path so this at least parses; a real `File` type is
51
+ # still needed here.
52
+ fun stdin() -> Str =
53
+ "/dev/stdin"
54
+
55
+ fun stdout() -> Str =
56
+ "/dev/stdout"
57
+
58
+ fun stderr() -> Str =
59
+ "/dev/stderr"
60
+
61
+ # Reads the whole file at `path` as a `Str` (`Str` is a byte array here — see
62
+ # `libs/std/str.plum` — so this works for binary content too, not just text).
63
+ fun readFile(path: Str) -> error{NoSuchPath}!Str =
64
+ if !exists(path)
65
+ return error.NoSuchPath
66
+ return rawReadFile(path)
67
+
68
+ # Writes `data` to `path`, replacing its previous contents (or creating it).
69
+ fun writeFile(path: Str, data: Str) -> error{IoFailed}!Unit =
70
+ if rawWriteFile(path, data) != 1
71
+ return error.IoFailed
72
+
73
+ fun exists(path: Str) -> Bool =
74
+ return rawExists(path) == 1
75
+
76
+ fun mkdir(path: Str) -> error{IoFailed}!Unit =
77
+ if rawMkdir(path) != 1
78
+ return error.IoFailed
79
+
80
+ # Removes the file (not directory) at `path`.
81
+ fun remove(path: Str) -> IoError!Unit =
82
+ if !exists(path)
83
+ return error.NoSuchPath
84
+ if rawRemove(path) != 1
85
+ return error.IoFailed
86
+
87
+ # Whether `path` exists and is reachable — this VM has no notion of a
88
+ # process' effective uid/gid, so (unlike POSIX `access(2)`) there is no mode
89
+ # argument to check permission bits against.
90
+ fun access(path: Str) -> Bool =
91
+ return exists(path)
92
+
93
+ # Appends `data` to the file at `path`, creating it (with just `data` as its
94
+ # contents) if it doesn't already exist. There's no host-level "open for
95
+ # append" primitive, so this reads the existing bytes and writes the
96
+ # concatenation back — not atomic, and O(existing file size) per call.
97
+ fun appendFile(path: Str, data: Str) -> IoError!Unit =
98
+ if exists(path)
99
+ existing := try readFile(path)
100
+ try writeFile(path, existing + data)
101
+ return
102
+ try writeFile(path, data)
103
+
104
+ # Unix permission bits only (e.g. `0o644`) — this VM has no Windows ACL
105
+ # equivalent to fall back to.
106
+ fun chmod(path: Str, mode: Int) -> error{IoFailed}!Unit =
107
+ if rawChmod(path, mode) != 1
108
+ return error.IoFailed
109
+
110
+ # There is no host primitive for changing file ownership (it needs a uid/gid
111
+ # syscall this VM doesn't expose), unlike the permission-bits-only `chmod`.
112
+ fun chown(path: Str, uid: Int, gid: Int) =
113
+ todo
114
+
115
+ fun copyFile(src: Str, dest: Str) -> IoError!Unit =
116
+ data := try readFile(src)
117
+ try writeFile(dest, data)
118
+
119
+ fun cp(src: Str, dest: Str) -> IoError!Unit =
120
+ try copyFile(src, dest)
121
+
122
+ # Symlinks aren't modeled separately from regular files/dirs by this VM's
123
+ # host primitives (`rawChmod` always follows them), so this is the same as
124
+ # `chmod`.
125
+ fun lchmod(path: Str, mode: Int) -> error{IoFailed}!Unit =
126
+ try chmod(path, mode)
127
+
128
+ # See `chown` — no uid/gid syscall exposed by this VM.
129
+ fun lchown(path: Str, uid: Int, gid: Int) =
130
+ todo
131
+
132
+ # Setting a file's atime/mtime needs a syscall (`utimensat`/`SetFileTime`)
133
+ # this VM doesn't expose (`std::fs` itself has no equivalent either).
134
+ fun lutimes(path: Str, atime: Int, mtime: Int) =
135
+ todo
136
+
137
+ fun link(existing_path: Str, new_path: Str) -> error{IoFailed}!Unit =
138
+ if rawLink(existing_path, new_path) != 1
139
+ return error.IoFailed
140
+
141
+ # Would need a real `Stat` struct type (size/mode/mtime/uid/gid/...) to
142
+ # return anything useful — no such type exists in the stdlib yet.
143
+ fun lstat(path: Str) =
144
+ todo
145
+
146
+ # Creates a fresh directory named `prefix` followed by a random suffix,
147
+ # retrying on the (astronomically unlikely) chance of a collision, and
148
+ # returns the path actually created.
149
+ fun mkdtemp(prefix: Str) -> error{IoFailed}!Str =
150
+ path := prefix + Uuid.v4().toStr()
151
+ if exists(path)
152
+ return mkdtemp(prefix)
153
+ try mkdir(path)
154
+ return path
155
+
156
+ # Would need a host-side open-file-descriptor table (this VM's other file
157
+ # ops are all one-shot: open+read/write+close in a single host call) plus a
158
+ # `File` handle type — neither exists yet.
159
+ fun open(path: Str, flags: Int) =
160
+ todo
161
+
162
+ # See `open` — same missing handle-table/type problem, for directories.
163
+ fun opendir(path: Str) =
164
+ todo
165
+
166
+ # The names of `path`'s directory entries (not full paths).
167
+ fun readdir(path: Str) -> error{NoSuchPath}!List[Str] =
168
+ if !exists(path)
169
+ return error.NoSuchPath
170
+ # skipping empty names handles both a genuinely empty directory (whose
171
+ # joined `""` would otherwise split into one bogus `""` entry) and
172
+ # `rawReadDir`'s "unreadable" fallback (already ruled out by the `exists`
173
+ # check above) the same way — no directory entry is ever named `""`.
174
+ names := List[Str]()
175
+ for n := range rawReadDir(path).split("\n", 0)
176
+ if n != ""
177
+ names.push(n)
178
+ return names
179
+
180
+ fun readlink(path: Str) -> error{NoSuchPath}!Str =
181
+ if !exists(path)
182
+ return error.NoSuchPath
183
+ return rawReadlink(path)
184
+
185
+ fun realpath(path: Str) -> error{NoSuchPath}!Str =
186
+ if !exists(path)
187
+ return error.NoSuchPath
188
+ return rawRealpath(path)
189
+
190
+ fun rename(old_path: Str, new_path: Str) -> IoError!Unit =
191
+ if !exists(old_path)
192
+ return error.NoSuchPath
193
+ if rawRename(old_path, new_path) != 1
194
+ return error.IoFailed
195
+
196
+ # Removes an empty directory. (`remove` above covers plain files.)
197
+ fun rmdir(path: Str) -> IoError!Unit =
198
+ if !exists(path)
199
+ return error.NoSuchPath
200
+ if rawRemoveDir(path) != 1
201
+ return error.IoFailed
202
+
203
+ # See `lstat` — needs a `Stat` struct type.
204
+ fun stat(path: Str) =
205
+ todo
206
+
207
+ # Needs a filesystem-info struct type, same gap as `stat`.
208
+ fun statfs(path: Str) =
209
+ todo
210
+
211
+ fun symlink(target: Str, path: Str) -> error{IoFailed}!Unit =
212
+ if rawSymlink(target, path) != 1
213
+ return error.IoFailed
214
+
215
+ # Resizes the file at `path` to exactly `len` bytes — truncated if shorter,
216
+ # zero-padded if longer.
217
+ fun truncate(path: Str, len: Int) -> IoError!Unit =
218
+ if !exists(path)
219
+ return error.NoSuchPath
220
+ if rawTruncate(path, len) != 1
221
+ return error.IoFailed
222
+
223
+ # See `lutimes` — same missing syscall.
224
+ fun utimes(path: Str, atime: Int, mtime: Int) =
225
+ todo
226
+
227
+ # Would need an async/callback mechanism to deliver filesystem change events
228
+ # into a running Plum program — nothing like that exists in this VM (every
229
+ # host call here is a synchronous, one-shot request/response).
230
+ fun watch(filename: Str) =
231
+ todo
231
232
 
232
233
  test "writeFile/readFile/appendFile/remove round-trip on a real file"
233
- dir := mkdtemp("/tmp/plum_os_test_")!
234
+ dir := Os.mkdtemp("/tmp/plum_os_test_")!
234
235
  path := dir + "/a.txt"
235
- writeFile(path, "hello")!
236
+ Os.writeFile(path, "hello")!
236
- assert readFile(path)! == "hello"
237
+ assert Os.readFile(path)! == "hello"
237
- appendFile(path, " world")!
238
+ Os.appendFile(path, " world")!
238
- assert readFile(path)! == "hello world"
239
+ assert Os.readFile(path)! == "hello world"
239
- assert exists(path) == true
240
+ assert Os.exists(path) == true
240
- remove(path)!
241
+ Os.remove(path)!
241
- assert exists(path) == false
242
+ assert Os.exists(path) == false
242
- rmdir(dir)!
243
+ Os.rmdir(dir)!
243
244
 
244
245
  test "copyFile/cp/rename move and duplicate file contents correctly"
245
- dir := mkdtemp("/tmp/plum_os_test_")!
246
+ dir := Os.mkdtemp("/tmp/plum_os_test_")!
246
247
  a := dir + "/a.txt"
247
248
  b := dir + "/b.txt"
248
249
  c := dir + "/c.txt"
249
- writeFile(a, "data")!
250
+ Os.writeFile(a, "data")!
250
- copyFile(a, b)!
251
+ Os.copyFile(a, b)!
251
- assert readFile(b)! == "data"
252
+ assert Os.readFile(b)! == "data"
252
- rename(b, c)!
253
+ Os.rename(b, c)!
253
- assert exists(b) == false
254
+ assert Os.exists(b) == false
254
- assert readFile(c)! == "data"
255
+ assert Os.readFile(c)! == "data"
255
- remove(a)!
256
+ Os.remove(a)!
256
- remove(c)!
257
+ Os.remove(c)!
257
- rmdir(dir)!
258
+ Os.rmdir(dir)!
258
259
 
259
260
  test "readdir lists the entries created in a fresh directory"
260
- dir := mkdtemp("/tmp/plum_os_test_")!
261
+ dir := Os.mkdtemp("/tmp/plum_os_test_")!
261
- assert readdir(dir)!.length() == 0
262
+ assert Os.readdir(dir)!.length() == 0
262
- writeFile(dir + "/only.txt", "x")!
263
+ Os.writeFile(dir + "/only.txt", "x")!
263
- names := readdir(dir)!
264
+ names := Os.readdir(dir)!
264
265
  assert names.length() == 1
265
266
  assert names.join(",") == "only.txt"
266
- remove(dir + "/only.txt")!
267
+ Os.remove(dir + "/only.txt")!
267
- rmdir(dir)!
268
+ Os.rmdir(dir)!
268
269
 
269
270
  test "symlink/readlink/realpath resolve a linked file correctly"
270
- dir := mkdtemp("/tmp/plum_os_test_")!
271
+ dir := Os.mkdtemp("/tmp/plum_os_test_")!
271
272
  target := dir + "/target.txt"
272
273
  link_path := dir + "/link.txt"
273
- writeFile(target, "linked")!
274
+ Os.writeFile(target, "linked")!
274
- symlink(target, link_path)!
275
+ Os.symlink(target, link_path)!
275
- assert readlink(link_path)! == target
276
+ assert Os.readlink(link_path)! == target
276
- assert readFile(link_path)! == "linked"
277
+ assert Os.readFile(link_path)! == "linked"
277
- realpath(link_path)!
278
+ Os.realpath(link_path)!
278
- remove(link_path)!
279
+ Os.remove(link_path)!
279
- remove(target)!
280
+ Os.remove(target)!
280
- rmdir(dir)!
281
+ Os.rmdir(dir)!
281
282
 
282
283
  test "truncate resizes a file's contents"
283
- dir := mkdtemp("/tmp/plum_os_test_")!
284
+ dir := Os.mkdtemp("/tmp/plum_os_test_")!
284
285
  path := dir + "/a.txt"
285
- writeFile(path, "hello world")!
286
+ Os.writeFile(path, "hello world")!
286
- truncate(path, 5)!
287
+ Os.truncate(path, 5)!
287
- assert readFile(path)! == "hello"
288
+ assert Os.readFile(path)! == "hello"
288
- remove(path)!
289
+ Os.remove(path)!
289
- rmdir(dir)!
290
+ Os.rmdir(dir)!
290
291
 
291
292
  test "chmod changes a file's permission bits"
292
- dir := mkdtemp("/tmp/plum_os_test_")!
293
+ dir := Os.mkdtemp("/tmp/plum_os_test_")!
293
294
  path := dir + "/a.txt"
294
- writeFile(path, "x")!
295
+ Os.writeFile(path, "x")!
295
- chmod(path, 384)! # 0o600
296
+ Os.chmod(path, 384)! # 0o600
296
- remove(path)!
297
+ Os.remove(path)!
297
- rmdir(dir)!
298
+ Os.rmdir(dir)!
298
299
 
299
300
  test "link creates a second name for the same file"
300
- dir := mkdtemp("/tmp/plum_os_test_")!
301
+ dir := Os.mkdtemp("/tmp/plum_os_test_")!
301
302
  a := dir + "/a.txt"
302
303
  b := dir + "/b.txt"
303
- writeFile(a, "shared")!
304
+ Os.writeFile(a, "shared")!
304
- link(a, b)!
305
+ Os.link(a, b)!
305
- assert readFile(b)! == "shared"
306
+ assert Os.readFile(b)! == "shared"
306
- remove(a)!
307
+ Os.remove(a)!
307
- remove(b)!
308
+ Os.remove(b)!
308
- rmdir(dir)!
309
+ Os.rmdir(dir)!
309
310
 
310
311
  test "readFile/remove/rmdir report errors for missing paths"
311
- assert {readFile("/tmp/plum_os_test_does_not_exist.txt") catch "gone"} == "gone"
312
+ assert {Os.readFile("/tmp/plum_os_test_does_not_exist.txt") catch "gone"} == "gone"
312
313
  failed := 0
313
- remove("/tmp/plum_os_test_does_not_exist.txt") catch e
314
+ Os.remove("/tmp/plum_os_test_does_not_exist.txt") catch e
314
315
  failed = failed + 1
315
- rmdir("/tmp/plum_os_test_does_not_exist_dir") catch e
316
+ Os.rmdir("/tmp/plum_os_test_does_not_exist_dir") catch e
316
317
  failed = failed + 1
317
318
  assert failed == 2
plum-std/Str.plum CHANGED
@@ -146,9 +146,9 @@ record Str(bytes: []Byte) =
146
146
  return self
147
147
  if n == 0
148
148
  return o
149
- out := makeBytes(n + m)
149
+ out := ByteSlice.make(n + m)
150
- copyBytes(out, 0, self.bytes, 0, n)
150
+ ByteSlice.copy(out, 0, self.bytes, 0, n)
151
- copyBytes(out, n, o.bytes, 0, m)
151
+ ByteSlice.copy(out, n, o.bytes, 0, m)
152
152
  return Str(bytes: out)
153
153
 
154
154
  # `a == b` / `a != b`: CONTENT equality (never reference equality).
@@ -156,7 +156,7 @@ record Str(bytes: []Byte) =
156
156
  n := self.length()
157
157
  if n != o.length()
158
158
  return false
159
- return bytesEqual(self.bytes, 0, o.bytes, 0, n)
159
+ return ByteSlice.equal(self.bytes, 0, o.bytes, 0, n)
160
160
 
161
161
  fun toStr(self) -> Str =
162
162
  self
@@ -280,7 +280,7 @@ record Str(bytes: []Byte) =
280
280
  en := if e > len then len else e
281
281
  if s >= en
282
282
  return ""
283
- return bytesToStr(self.bytes, s, en - s)
283
+ return fromBytes(self.bytes, s, en - s)
284
284
 
285
285
  fun toLower(self) -> Str =
286
286
  mapCase(self, false)
@@ -290,15 +290,15 @@ record Str(bytes: []Byte) =
290
290
  # along with the string's, corrupting any non-ASCII text.
291
291
  fun reverse(self) -> Str =
292
292
  n := self.length()
293
- out := makeBytes(n)
293
+ out := ByteSlice.make(n)
294
294
  i := 0
295
295
  while i < n
296
296
  cl := self.codePointByteLength(i)
297
297
  if i + cl > n
298
298
  cl = n - i
299
- copyBytes(out, n - i - cl, self.bytes, i, cl)
299
+ ByteSlice.copy(out, n - i - cl, self.bytes, i, cl)
300
300
  i = i + cl
301
- return bytesToStr(out, 0, n)
301
+ return fromBytes(out, 0, n)
302
302
 
303
303
  fun camelCase(self) -> Str =
304
304
  caseConvert(self, "", 2)
@@ -414,235 +414,235 @@ record Str(bytes: []Byte) =
414
414
  fun words(self) -> Str =
415
415
  caseConvert(self, " ", 0)
416
416
 
417
+ # Copies out `n` bytes of `src` starting at `start` into a fresh, independent
418
+ # `Str`: it never aliases `src`, so mutating `src` afterwards can't change an
419
+ # already-returned `Str`.
420
+ fun fromBytes(src: []Byte, start: Int, n: Int) -> Str =
421
+ data := ByteSlice.make(n)
422
+ ByteSlice.copy(data, 0, src, start, n)
423
+ return Str(bytes: data)
424
+
417
- # A new 1-byte Str holding `b`'s low 8 bits, the counterpart to `byteAt`.
425
+ # A new 1-byte Str holding `b`'s low 8 bits, the counterpart to `byteAt`.
418
- fun byteToStr(b: Int) -> Str =
426
+ fun byteToStr(b: Int) -> Str =
419
- out := makeBytes(1)
427
+ out := ByteSlice.make(1)
420
- out.set(0, Byte(b))
428
+ out.set(0, Byte(b))
421
- return Str(bytes: out)
429
+ return Str(bytes: out)
422
-
430
+
423
- fun isAsciiUpper(b: Int) -> Bool =
431
+ fun isAsciiUpper(b: Int) -> Bool =
424
- b >= 65 && b <= 90 # 'A'..'Z'
432
+ b >= 65 && b <= 90 # 'A'..'Z'
425
-
433
+
426
- fun isAsciiLower(b: Int) -> Bool =
434
+ fun isAsciiLower(b: Int) -> Bool =
427
- b >= 97 && b <= 122 # 'a'..'z'
435
+ b >= 97 && b <= 122 # 'a'..'z'
428
-
436
+
429
- fun isAsciiDigit(b: Int) -> Bool =
437
+ fun isAsciiDigit(b: Int) -> Bool =
430
- b >= 48 && b <= 57 # '0'..'9'
438
+ b >= 48 && b <= 57 # '0'..'9'
431
-
439
+
432
- # Byte 0x80+ is either a UTF-8 continuation byte or the lead byte of a
440
+ # Byte 0x80+ is either a UTF-8 continuation byte or the lead byte of a
433
- # multi-byte character — never an ASCII separator — so it's treated as
441
+ # multi-byte character — never an ASCII separator — so it's treated as
434
- # "part of a word" here. Without this, `caseConvert` (backing `words`/
442
+ # "part of a word" here. Without this, `caseConvert` (backing `words`/
435
- # `camelCase`/`snakeCase`/`kebabCase`/`startCase`) would treat every
443
+ # `camelCase`/`snakeCase`/`kebabCase`/`startCase`) would treat every
436
- # non-ASCII character as a word BOUNDARY, shredding any text that isn't
444
+ # non-ASCII character as a word BOUNDARY, shredding any text that isn't
437
- # pure ASCII into a separate "word" per byte.
445
+ # pure ASCII into a separate "word" per byte.
438
- fun isWordByte(b: Int) -> Bool =
446
+ fun isWordByte(b: Int) -> Bool =
439
- isAsciiUpper(b) || isAsciiLower(b) || isAsciiDigit(b) || b >= 0x80
447
+ isAsciiUpper(b) || isAsciiLower(b) || isAsciiDigit(b) || b >= 0x80
440
-
448
+
441
- fun toLowerByte(b: Int) -> Int =
449
+ fun toLowerByte(b: Int) -> Int =
442
- if isAsciiUpper(b) then b + 32 else b
450
+ if isAsciiUpper(b) then b + 32 else b
443
-
451
+
444
- fun toUpperByte(b: Int) -> Int =
452
+ fun toUpperByte(b: Int) -> Int =
445
- if isAsciiLower(b) then b - 32 else b
453
+ if isAsciiLower(b) then b - 32 else b
446
-
454
+
447
- fun isCamelBoundary(prev_byte: Int, cur_byte: Int) -> Bool =
455
+ fun isCamelBoundary(prev_byte: Int, cur_byte: Int) -> Bool =
448
- isAsciiLower(prev_byte) && isAsciiUpper(cur_byte)
456
+ isAsciiLower(prev_byte) && isAsciiUpper(cur_byte)
449
-
457
+
450
- # true if `s.byteAt(offset)..offset+subLen` equals `sub` exactly (and is in
458
+ # true if `s.byteAt(offset)..offset+subLen` equals `sub` exactly (and is in
451
- # bounds — out-of-range always reports no match rather than trapping, so every
459
+ # bounds — out-of-range always reports no match rather than trapping, so every
452
- # caller can test candidate positions/lengths freely).
460
+ # caller can test candidate positions/lengths freely).
453
- fun matchesAt(s: Str, sub: Str, offset: Int, sub_len: Int) -> Bool =
461
+ fun matchesAt(s: Str, sub: Str, offset: Int, sub_len: Int) -> Bool =
454
- if offset < 0 || offset + sub_len > s.length()
462
+ if offset < 0 || offset + sub_len > s.length()
455
- return false
456
- j := 0
457
- while j < sub_len
458
- if s.byteAt(offset + j) != sub.byteAt(j)
459
463
  return false
460
- j = j + 1
461
- return true
462
-
463
- # Encodes a single Unicode codepoint (its raw integer value) as its own
464
- # UTF-8 `Str` (1-4 bytes) — the reverse of `Str.codePointAt`. `b0`/`b1`/`b2`
465
- # are declared ONCE up front and reassigned per branch (rather than
466
- # redeclared with `:=` in each `if`) — `if` bodies share one flat scope with
467
- # the rest of the function here (there's no per-branch isolation), so a
468
- # second `:=` for the same name would be a compile error.
469
- fun codePointToStr(cp: Int) -> Str =
470
- if cp < 0x80
471
- return byteToStr(cp)
472
- b0 := 0xC0 | {cp >> 6}
473
- b1 := 0x80 | {cp & 0x3F}
474
- if cp < 0x800
475
- return byteToStr(b0) + byteToStr(b1)
476
- b0 = 0xE0 | {cp >> 12}
477
- b1 = 0x80 | {{cp >> 6} & 0x3F}
478
- b2 := 0x80 | {cp & 0x3F}
479
- if cp < 0x10000
480
- return byteToStr(b0) + byteToStr(b1) + byteToStr(b2)
481
- b0 = 0xF0 | {cp >> 18}
482
- b1 = 0x80 | {{cp >> 12} & 0x3F}
483
- b2 = 0x80 | {{cp >> 6} & 0x3F}
484
- b3 := 0x80 | {cp & 0x3F}
485
- return byteToStr(b0) + byteToStr(b1) + byteToStr(b2) + byteToStr(b3)
486
-
487
- fun isTrimByte(b: Int) -> Bool =
488
- b == 32 || b == 9 || b == 10 || b == 13
489
-
490
- fun trimStartIndex(s: Str) -> Int =
491
- n := s.length()
492
- i := 0
493
- while i < n && isTrimByte(s.byteAt(i))
494
- i = i + 1
495
- return i
496
-
497
- # Exclusive end index of `s` with trailing whitespace dropped.
498
- fun trimEndIndex(s: Str) -> Int =
499
- n := s.length()
500
- while n > 0 && isTrimByte(s.byteAt(n - 1))
501
- n = n - 1
502
- return n
503
-
504
- fun htmlEntity(b: Int) -> Str =
505
- match b
506
- 38 => "&amp;"
507
- 60 => "&lt;"
508
- 62 => "&gt;"
509
- 34 => "&quot;"
510
- 39 => "&#39;"
511
- _ => ""
512
-
513
- fun escapeHtml(s: Str) -> Str =
514
- n := s.length()
515
- buf := Buffer()
516
- i := 0
517
- while i < n
518
- b := s.byteAt(i)
519
- ent := htmlEntity(b)
520
- if ent.length() == 0
464
+ j := 0
465
+ while j < sub_len
466
+ if s.byteAt(offset + j) != sub.byteAt(j)
467
+ return false
468
+ j = j + 1
469
+ return true
470
+
471
+ # Encodes a single Unicode codepoint (its raw integer value) as its own
472
+ # UTF-8 `Str` (1-4 bytes) — the reverse of `Str.codePointAt`. `b0`/`b1`/`b2`
473
+ # are declared ONCE up front and reassigned per branch (rather than
474
+ # redeclared with `:=` in each `if`) — `if` bodies share one flat scope with
475
+ # the rest of the function here (there's no per-branch isolation), so a
476
+ # second `:=` for the same name would be a compile error.
477
+ fun codePointToStr(cp: Int) -> Str =
478
+ if cp < 0x80
479
+ return byteToStr(cp)
480
+ b0 := 0xC0 | {cp >> 6}
481
+ b1 := 0x80 | {cp & 0x3F}
482
+ if cp < 0x800
483
+ return byteToStr(b0) + byteToStr(b1)
484
+ b0 = 0xE0 | {cp >> 12}
485
+ b1 = 0x80 | {{cp >> 6} & 0x3F}
486
+ b2 := 0x80 | {cp & 0x3F}
487
+ if cp < 0x10000
488
+ return byteToStr(b0) + byteToStr(b1) + byteToStr(b2)
489
+ b0 = 0xF0 | {cp >> 18}
490
+ b1 = 0x80 | {{cp >> 12} & 0x3F}
491
+ b2 = 0x80 | {{cp >> 6} & 0x3F}
492
+ b3 := 0x80 | {cp & 0x3F}
493
+ return byteToStr(b0) + byteToStr(b1) + byteToStr(b2) + byteToStr(b3)
494
+
495
+ fun isTrimByte(b: Int) -> Bool =
496
+ b == 32 || b == 9 || b == 10 || b == 13
497
+
498
+ fun trimStartIndex(s: Str) -> Int =
499
+ n := s.length()
500
+ i := 0
501
+ while i < n && isTrimByte(s.byteAt(i))
502
+ i = i + 1
503
+ return i
504
+
505
+ # Exclusive end index of `s` with trailing whitespace dropped.
506
+ fun trimEndIndex(s: Str) -> Int =
507
+ n := s.length()
508
+ while n > 0 && isTrimByte(s.byteAt(n - 1))
509
+ n = n - 1
510
+ return n
511
+
512
+ fun htmlEntity(b: Int) -> Str =
513
+ match b
514
+ 38 => "&amp;"
515
+ 60 => "&lt;"
516
+ 62 => "&gt;"
517
+ 34 => "&quot;"
518
+ 39 => "&#39;"
519
+ _ => ""
520
+
521
+ fun escapeHtml(s: Str) -> Str =
522
+ n := s.length()
523
+ buf := Buffer()
524
+ i := 0
525
+ while i < n
526
+ b := s.byteAt(i)
527
+ ent := htmlEntity(b)
528
+ if ent.length() == 0
529
+ buf.writeByte(Byte(b))
530
+ else
531
+ buf.write(ent)
532
+ i = i + 1
533
+ return buf.toStr()
534
+
535
+ fun isRegExpMetaByte(b: Int) -> Bool =
536
+ b == 94 || b == 36 || b == 92 || b == 46 || b == 42 || b == 43 || b == 63
537
+ || b == 40 || b == 41 || b == 91 || b == 93 || b == 123 || b == 125 || b == 124
538
+
539
+ fun escapeRegExpStr(s: Str) -> Str =
540
+ n := s.length()
541
+ buf := Buffer()
542
+ i := 0
543
+ while i < n
544
+ b := s.byteAt(i)
545
+ if isRegExpMetaByte(b)
546
+ buf.writeByte(Byte(92))
521
547
  buf.writeByte(Byte(b))
522
- else
523
- buf.write(ent)
524
- i = i + 1
548
+ i = i + 1
525
- return buf.toStr()
549
+ return buf.toStr()
550
+
551
+ # Length of the HTML entity starting at `i` (and writes its decoded byte
552
+ # into `buf`), or 0 if none starts there.
553
+ fun unescapeEntityAt(s: Str, i: Int, buf: Buffer) -> Int =
554
+ if matchesAt(s, "&amp;", i, 5)
555
+ buf.writeByte(Byte(38))
556
+ return 5
557
+ if matchesAt(s, "&lt;", i, 4)
558
+ buf.writeByte(Byte(60))
559
+ return 4
560
+ if matchesAt(s, "&gt;", i, 4)
561
+ buf.writeByte(Byte(62))
562
+ return 4
563
+ if matchesAt(s, "&quot;", i, 6)
564
+ buf.writeByte(Byte(34))
565
+ return 6
566
+ if matchesAt(s, "&#39;", i, 5)
567
+ buf.writeByte(Byte(39))
568
+ return 5
569
+ return 0
570
+
571
+ fun unescapeHtml(s: Str) -> Str =
572
+ n := s.length()
573
+ buf := Buffer()
574
+ i := 0
575
+ while i < n
576
+ k := 0
577
+ if s.byteAt(i) == 38
578
+ k = unescapeEntityAt(s, i, buf)
579
+ if k == 0
580
+ buf.writeByte(Byte(s.byteAt(i)))
581
+ k = 1
582
+ i = i + k
583
+ return buf.toStr()
526
584
 
585
+ # Shared walker behind `words`/`camelCase`/`snakeCase`/`kebabCase`/`startCase`:
586
+ # splits on non-word bytes and on a lower->upper "camel" transition, joining
587
+ # the surviving words with `sep` and case-mapping each byte per `mode`:
588
+ # 0 = leave case as-is (words) 1 = lowercase everything (snake/kebab)
589
+ # 2 = camelCase (word 1 lowercase, rest Capitalized) 4 = Capitalize every word (startCase)
590
+ # (`toLower`/`upperCase` use `mapCase` — they map every byte 1:1 including
591
+ # non-word bytes like spaces/punctuation, which this function drops.)
527
- fun isRegExpMetaByte(b: Int) -> Bool =
592
+ fun caseConvert(s: Str, sep: Str, mode: Int) -> Str =
593
+ n := s.length()
594
+ buf := Buffer()
595
+ word_index := 0
596
+ prev_byte := 0
597
+ i := 0
598
+ while i < n
599
+ b := s.byteAt(i)
600
+ if !isWordByte(b)
601
+ prev_byte = 0
602
+ else
528
- b == 94 || b == 36 || b == 92 || b == 46 || b == 42 || b == 43 || b == 63
603
+ is_word_start := prev_byte == 0 || isCamelBoundary(prev_byte, b)
604
+ if is_word_start
605
+ word_index = word_index + 1
606
+ if word_index > 1
607
+ buf.write(sep)
529
- || b == 40 || b == 41 || b == 91 || b == 93 || b == 123 || b == 125 || b == 124
608
+ buf.writeByte(Byte(caseConvertByte(b, mode, is_word_start, word_index)))
609
+ prev_byte = b
610
+ i = i + 1
611
+ return buf.toStr()
530
612
 
613
+ fun caseConvertByte(b: Int, mode: Int, is_word_start: Bool, word_index: Int) -> Int =
614
+ match mode
615
+ 0 => b
616
+ 1 => toLowerByte(b)
617
+ 2 => if word_index == 1 then toLowerByte(b) else {if is_word_start then toUpperByte(b) else toLowerByte(b)}
618
+ 4 => if is_word_start then toUpperByte(b) else toLowerByte(b)
619
+ _ => b
620
+
621
+ # Simple 1:1 byte map — every byte (including non-word bytes like spaces and
622
+ # punctuation) is preserved; only letters are case-mapped.
531
- fun escapeRegExpStr(s: Str) -> Str =
623
+ fun mapCase(s: Str, upper: Bool) -> Str =
532
- n := s.length()
624
+ n := s.length()
533
- buf := Buffer()
625
+ buf := Buffer()
534
- i := 0
626
+ i := 0
535
- while i < n
627
+ while i < n
536
- b := s.byteAt(i)
628
+ b := s.byteAt(i)
537
- if isRegExpMetaByte(b)
538
- buf.writeByte(Byte(92))
539
- buf.writeByte(Byte(b))
629
+ buf.writeByte(Byte(if upper then toUpperByte(b) else toLowerByte(b)))
540
- i = i + 1
630
+ i = i + 1
541
- return buf.toStr()
631
+ return buf.toStr()
542
-
543
- # Length of the HTML entity starting at `i` (and writes its decoded byte
544
- # into `buf`), or 0 if none starts there.
545
- fun unescapeEntityAt(s: Str, i: Int, buf: Buffer) -> Int =
546
- if matchesAt(s, "&amp;", i, 5)
547
- buf.writeByte(Byte(38))
548
- return 5
549
- if matchesAt(s, "&lt;", i, 4)
550
- buf.writeByte(Byte(60))
551
- return 4
552
- if matchesAt(s, "&gt;", i, 4)
553
- buf.writeByte(Byte(62))
554
- return 4
555
- if matchesAt(s, "&quot;", i, 6)
556
- buf.writeByte(Byte(34))
557
- return 6
558
- if matchesAt(s, "&#39;", i, 5)
559
- buf.writeByte(Byte(39))
560
- return 5
561
- return 0
562
-
563
- fun unescapeHtml(s: Str) -> Str =
564
- n := s.length()
565
- buf := Buffer()
566
- i := 0
567
- while i < n
568
- k := 0
569
- if s.byteAt(i) == 38
570
- k = unescapeEntityAt(s, i, buf)
571
- if k == 0
572
- buf.writeByte(Byte(s.byteAt(i)))
573
- k = 1
574
- i = i + k
575
- return buf.toStr()
576
-
577
- # Shared walker behind `words`/`camelCase`/`snakeCase`/`kebabCase`/`startCase`:
578
- # splits on non-word bytes and on a lower->upper "camel" transition, joining
579
- # the surviving words with `sep` and case-mapping each byte per `mode`:
580
- # 0 = leave case as-is (words) 1 = lowercase everything (snake/kebab)
581
- # 2 = camelCase (word 1 lowercase, rest Capitalized) 4 = Capitalize every word (startCase)
582
- # (`toLower`/`upperCase` use `mapCase` — they map every byte 1:1 including
583
- # non-word bytes like spaces/punctuation, which this function drops.)
584
- fun caseConvert(s: Str, sep: Str, mode: Int) -> Str =
585
- n := s.length()
586
- buf := Buffer()
587
- word_index := 0
588
- prev_byte := 0
589
- i := 0
590
- while i < n
591
- b := s.byteAt(i)
592
- if !isWordByte(b)
593
- prev_byte = 0
594
- else
595
- is_word_start := prev_byte == 0 || isCamelBoundary(prev_byte, b)
596
- if is_word_start
597
- word_index = word_index + 1
598
- if word_index > 1
599
- buf.write(sep)
600
- buf.writeByte(Byte(caseConvertByte(b, mode, is_word_start, word_index)))
601
- prev_byte = b
602
- i = i + 1
603
- return buf.toStr()
604
-
605
- fun caseConvertByte(b: Int, mode: Int, is_word_start: Bool, word_index: Int) -> Int =
606
- match mode
607
- 0 => b
608
- 1 => toLowerByte(b)
609
- 2 => if word_index == 1 then toLowerByte(b) else {if is_word_start then toUpperByte(b) else toLowerByte(b)}
610
- 4 => if is_word_start then toUpperByte(b) else toLowerByte(b)
611
- _ => b
612
-
613
- # Simple 1:1 byte map — every byte (including non-word bytes like spaces and
614
- # punctuation) is preserved; only letters are case-mapped.
615
- fun mapCase(s: Str, upper: Bool) -> Str =
616
- n := s.length()
617
- buf := Buffer()
618
- i := 0
619
- while i < n
620
- b := s.byteAt(i)
621
- buf.writeByte(Byte(if upper then toUpperByte(b) else toLowerByte(b)))
622
- i = i + 1
623
- return buf.toStr()
624
632
 
625
- # 2-char cells (space padded), one per codepoint U+00C0..U+00FF. `#` = keep
633
+ # 2-char cells (space padded), one per codepoint U+00C0..U+00FF. `#` = keep
626
- # the original character (`×`, `÷`).
634
+ # the original character (`×`, `÷`).
627
- fun deburrLatin1() -> Str =
635
+ fun deburrLatin1() -> Str =
628
- "A A A A A A AEC E E E E I I I I D N O O O O O # O U U U U Y Thssa a a a a a aec e e e e i i i i d n o o o o o # o u u u u y thy "
636
+ "A A A A A A AEC E E E E I I I I D N O O O O O # O U U U U Y Thssa a a a a a aec e e e e i i i i d n o o o o o # o u u u u y thy "
629
637
 
630
- # Same layout for U+0100..U+017F (Latin Extended-A).
631
- fun deburrLatinExtA() -> Str =
632
- "A a A a A a C c C c C c C c D d D d E e E e E e E e E e G g G g G g G g H h H h I i I i I i I i I i IJijJ j K k k L l L l L l L l L l N n N n N n n N n O o O o O o OEoeR r R r R r S s S s S s S s T t T t T t U u U u U u U u U u U u W w Y y Y Z z Z z Z z s "
638
+ # Same layout for U+0100..U+017F (Latin Extended-A).
639
+ fun deburrLatinExtA() -> Str =
640
+ "A a A a A a C c C c C c C c D d D d E e E e E e E e E e G g G g G g G g H h H h I i I i I i I i I i IJijJ j K k k L l L l L l L l L l N n N n N n n N n O o O o O o OEoeR r R r R r S s S s S s S s T t T t T t U u U u U u U u U u U u W w Y y Y Z z Z z Z z s "
633
641
 
634
642
  # ---- string interpolation regression tests ----
635
643
  # `"{expr}"` interpolation is a compiler/codegen feature (not a Str method
636
644
  # above), but Str is what it produces, so its regression coverage lives here.
637
645
 
638
- fun bigAscii(n: Int, b: Int) -> Str =
639
- buf := Buffer()
640
- i := 0
641
- while i < n
642
- buf.writeByte(Byte(b))
643
- i = i + 1
644
- return buf.toStr()
645
-
646
646
  test "split divides on every occurrence of the separator"
647
647
  parts := "a,b,c".split(",", 0)
648
648
  assert parts.length() == 3
@@ -727,7 +727,7 @@ test "escape/unescape round-trip HTML-sensitive characters"
727
727
  # ---- large-input (linear-time, no stack growth) regression tests ----
728
728
 
729
729
  test "sub of a 200000-byte string is a single copy"
730
- s := bigAscii(200000, 97)
730
+ s := "a".repeat(200000)
731
731
  r := s.sub(10, 199990)
732
732
  assert r.length() == 199980
733
733
  assert s.sub(0, 200000).length() == 200000
@@ -745,7 +745,7 @@ test "reverse of a 100000-char string"
745
745
  assert r.byteAt(0) == 97
746
746
 
747
747
  test "escape and unescape of a 100000-char string"
748
- s := bigAscii(100000, 60)
748
+ s := "<".repeat(100000)
749
749
  e := s.escape()
750
750
  assert e.length() == 400000
751
751
  assert e.unescape() == s
plum-std/Testing.plum CHANGED
@@ -8,10 +8,10 @@ import std/Str
8
8
  # Small jest/rspec-style assertion helpers for use inside `test` blocks, e.g.
9
9
  #
10
10
  # test "add works"
11
- # assert expectEq(add(1, 2), 3)
11
+ # assert Testing.expectEq(add(1, 2), 3)
12
- # assert expectTrue(isEven(4))
12
+ # assert Testing.expectTrue(isEven(4))
13
13
  #
14
- # These are plain functions returning `Bool` (`assert` only takes a bare
14
+ # These are statics returning `Bool` (`assert` only takes a bare
15
15
  # boolean expression — see `plum-core/parser.rs`'s `assert` rule), not a new
16
16
  # grammar form. They exist purely to make the condition read like an
17
17
  # assertion instead of a raw comparison.
@@ -30,85 +30,84 @@ import std/Str
30
30
  # those types, prefer the type's own `equals(self, other)` method (e.g.
31
31
  # `Uuid.equals`, `Time.equals`) directly in `assert` instead.
32
32
 
33
+ primitive Testing =
33
- fun expectTrue(cond: Bool) -> Bool =
34
+ fun expectTrue(cond: Bool) -> Bool =
34
- cond
35
+ cond
35
36
 
36
- fun expectFalse(cond: Bool) -> Bool =
37
+ fun expectFalse(cond: Bool) -> Bool =
37
- !cond
38
+ !cond
38
39
 
39
- fun expectEq(a: T, b: T) -> Bool =
40
+ fun expectEq(a: T, b: T) -> Bool =
40
- a == b
41
+ a == b
41
42
 
42
- fun expectNotEq(a: T, b: T) -> Bool =
43
+ fun expectNotEq(a: T, b: T) -> Bool =
43
- a != b
44
+ a != b
44
45
 
45
- # See the module-level CAVEAT above — this is `expectEq` under a name that
46
+ # See the module-level CAVEAT above — this is `expectEq` under a name that
46
- # reads better at a class/struct call site; it does NOT do structural
47
+ # reads better at a class/struct call site; it does NOT do structural
47
- # (field-by-field) comparison.
48
+ # (field-by-field) comparison.
48
- fun expectObject(a: T, b: T) -> Bool =
49
+ fun expectObject(a: T, b: T) -> Bool =
49
- a == b
50
+ a == b
50
51
 
51
- # Elementwise `List[T]` comparison — see the module-level CAVEAT above for
52
+ # Elementwise `List[T]` comparison — see the module-level CAVEAT above for
52
- # what `List.equals`'s own `==` means when `T` is a class/enum.
53
+ # what `List.equals`'s own `==` means when `T` is a class/enum.
53
- fun expectArray(a: List[T], b: List[T]) -> Bool =
54
+ fun expectArray(a: List[T], b: List[T]) -> Bool =
54
- a.equals(b)
55
+ a.equals(b)
55
56
 
56
- fun expectSuccess(r: !T) -> Bool =
57
+ fun expectSuccess(r: !T) -> Bool =
57
- r catch e
58
+ r catch e
58
- return false
59
+ return false
59
- true
60
+ true
60
61
 
61
- fun expectFailure(r: !T) -> Bool =
62
+ fun expectFailure(r: !T) -> Bool =
62
- r catch e
63
+ r catch e
63
- return true
64
+ return true
64
- false
65
+ false
65
66
 
66
- # True when `r` failed with the error named `name`.
67
+ # True when `r` failed with the error named `name`.
67
- fun expectFailureNamed(r: !T, name: Str) -> Bool =
68
+ fun expectFailureNamed(r: !T, name: Str) -> Bool =
68
- r catch e
69
+ r catch e
69
- return e.name() == name
70
+ return e.name() == name
70
- false
71
+ false
71
-
72
- fun makeSuccessForTestingTest() -> error{Bad}!Int =
73
- return 5
74
-
75
- fun makeFailureForTestingTest() -> error{Bad}!Int =
76
- return error.Bad
77
72
 
78
73
  test "expectTrue and expectFalse"
79
- assert expectTrue(1 == 1)
74
+ assert Testing.expectTrue(1 == 1)
80
- assert expectFalse(1 == 2)
75
+ assert Testing.expectFalse(1 == 2)
81
76
 
82
77
  test "expectEq and expectNotEq"
83
- assert expectEq(2 + 2, 4)
78
+ assert Testing.expectEq(2 + 2, 4)
84
- assert expectEq("ab" + "c", "abc")
79
+ assert Testing.expectEq("ab" + "c", "abc")
85
- assert expectNotEq(2 + 2, 5)
80
+ assert Testing.expectNotEq(2 + 2, 5)
86
81
 
87
82
  test "list equality via the List.equals method"
88
83
  a := List(1, 2, 3)
89
84
  b := List(1, 2, 3)
90
85
  c := List(1, 2)
91
86
  assert a.equals(b)
92
- assert expectFalse(a.equals(c))
87
+ assert Testing.expectFalse(a.equals(c))
93
88
 
94
89
  test "error unions are inspected with catch and !"
90
+ ok: error{Bad}!Int := 5
91
+ bad: error{Bad}!Int := error.Bad
95
- assert makeSuccessForTestingTest()! == 5
92
+ assert ok! == 5
96
- assert {makeFailureForTestingTest() catch -1} == -1
93
+ assert {bad catch -1} == -1
97
94
 
98
95
  test "expectArray compares two List[T]s elementwise"
99
96
  a := List(1, 2, 3)
100
97
  b := List(1, 2, 3)
101
98
  c := List(1, 2)
102
- assert expectArray(a, b)
99
+ assert Testing.expectArray(a, b)
103
- assert expectFalse(expectArray(a, c))
100
+ assert Testing.expectFalse(Testing.expectArray(a, c))
104
101
  d := List("x", "y")
105
102
  e := List("x", "y")
106
- assert expectArray(d, e)
103
+ assert Testing.expectArray(d, e)
107
104
 
108
105
  test "expectSuccess/expectFailure check the right variant"
106
+ ok: error{Bad}!Int := 5
107
+ bad: error{Bad}!Int := error.Bad
109
- assert expectSuccess(makeSuccessForTestingTest())
108
+ assert Testing.expectSuccess(ok)
110
- assert expectFalse(expectSuccess(makeFailureForTestingTest()))
111
- assert expectFailure(makeFailureForTestingTest())
112
- assert expectFalse(expectFailure(makeSuccessForTestingTest()))
109
+ assert Testing.expectFalse(Testing.expectSuccess(bad))
110
+ assert Testing.expectFailure(bad)
111
+ assert Testing.expectFalse(Testing.expectFailure(ok))
113
- assert expectFailureNamed(makeFailureForTestingTest(), "Bad")
112
+ assert Testing.expectFailureNamed(bad, "Bad")
114
- assert expectFalse(expectFailureNamed(makeSuccessForTestingTest(), "Bad"))
113
+ assert Testing.expectFalse(Testing.expectFailureNamed(ok, "Bad"))
plum-std/Time.plum CHANGED
@@ -80,38 +80,38 @@ record Time(value: Int) =
80
80
  + ":" + self.minute().toStr().padStart("0", 2) + ":" + self.second().toStr().padStart("0", 2)
81
81
  + "." + self.millisecond().toStr().padStart("0", 3) + "Z"
82
82
 
83
+ # Days-since-epoch to proleptic Gregorian year/month/day, via Howard
84
+ # Hinnant's `civil_from_days` (http://howardhinnant.github.io/date_algorithms.html) —
85
+ # pure integer math, valid for the whole range representable by `Int`, and
86
+ # exact (no floating-point rounding).
87
+ fun civilFromDays(z: Int) -> Civil =
88
+ zz := z + 719468
89
+ era := if zz >= 0 then zz / 146097 else { zz - 146096 } / 146097
90
+ doe := zz - era * 146097
91
+ yoe := { doe - doe / 1460 + doe / 36524 - doe / 146096 } / 365
92
+ y := yoe + era * 400
93
+ doy := doe - { 365 * yoe + yoe / 4 - yoe / 100 }
94
+ mp := { 5 * doy + 2 } / 153
95
+ d := doy - { 153 * mp + 2 } / 5 + 1
96
+ m := if mp < 10 then mp + 3 else mp - 9
97
+ return Civil(year: if m <= 2 then y + 1 else y, month: m, day: d)
98
+
99
+ # Integer division rounding toward negative infinity, unlike `/`'s
100
+ # round-toward-zero — needed so times before the epoch (negative `value`)
101
+ # still decompose into the correct calendar day/hour/etc rather than one
102
+ # past it.
103
+ fun floorDiv(a: Int, b: Int) -> Int =
104
+ q := a / b
105
+ r := a % b
106
+ return if { { r < 0 && b > 0 } || { r > 0 && b < 0 } } then q - 1 else q
107
+
108
+ fun floorMod(a: Int, b: Int) -> Int =
109
+ r := a % b
110
+ return if { { r < 0 && b > 0 } || { r > 0 && b < 0 } } then r + b else r
111
+
83
112
  # A Gregorian calendar date, decomposed from a day count by `civilFromDays`.
84
113
  record Civil(year: Int, month: Int, day: Int)
85
114
 
86
- # Days-since-epoch to proleptic Gregorian year/month/day, via Howard
87
- # Hinnant's `civil_from_days` (http://howardhinnant.github.io/date_algorithms.html) —
88
- # pure integer math, valid for the whole range representable by `Int`, and
89
- # exact (no floating-point rounding).
90
- fun civilFromDays(z: Int) -> Civil =
91
- zz := z + 719468
92
- era := if zz >= 0 then zz / 146097 else { zz - 146096 } / 146097
93
- doe := zz - era * 146097
94
- yoe := { doe - doe / 1460 + doe / 36524 - doe / 146096 } / 365
95
- y := yoe + era * 400
96
- doy := doe - { 365 * yoe + yoe / 4 - yoe / 100 }
97
- mp := { 5 * doy + 2 } / 153
98
- d := doy - { 153 * mp + 2 } / 5 + 1
99
- m := if mp < 10 then mp + 3 else mp - 9
100
- return Civil(year: if m <= 2 then y + 1 else y, month: m, day: d)
101
-
102
- # Integer division rounding toward negative infinity, unlike `/`'s
103
- # round-toward-zero — needed so times before the epoch (negative `value`)
104
- # still decompose into the correct calendar day/hour/etc rather than one
105
- # past it.
106
- fun floorDiv(a: Int, b: Int) -> Int =
107
- q := a / b
108
- r := a % b
109
- return if { { r < 0 && b > 0 } || { r > 0 && b < 0 } } then q - 1 else q
110
-
111
- fun floorMod(a: Int, b: Int) -> Int =
112
- r := a % b
113
- return if { { r < 0 && b > 0 } || { r > 0 && b < 0 } } then r + b else r
114
-
115
115
  # A span of time, stored as milliseconds. Negative values are meaningful
116
116
  # (e.g. the result of `Time.since` when `self` is earlier than `other`).
117
117
  record Duration(value: Int) =
plum-std/Uuid.plum CHANGED
@@ -78,36 +78,36 @@ record Uuid(value: Str) =
78
78
  + hexDigits(node, 12)
79
79
  return Uuid(value: s)
80
80
 
81
- # The numeric value (0-15) of an ASCII hex digit byte, or -1 if it isn't one.
81
+ # The numeric value (0-15) of an ASCII hex digit byte, or -1 if it isn't one.
82
- fun hexValue(byte: Int) -> Int =
82
+ fun hexValue(byte: Int) -> Int =
83
- # '0'-'9'
83
+ # '0'-'9'
84
- if byte >= 48 && byte <= 57
84
+ if byte >= 48 && byte <= 57
85
- return byte - 48
85
+ return byte - 48
86
- # 'a'-'f'
86
+ # 'a'-'f'
87
- if byte >= 97 && byte <= 102
87
+ if byte >= 97 && byte <= 102
88
- return byte - 97 + 10
88
+ return byte - 97 + 10
89
- # 'A'-'F'
89
+ # 'A'-'F'
90
- if byte >= 65 && byte <= 70
90
+ if byte >= 65 && byte <= 70
91
- return byte - 65 + 10
91
+ return byte - 65 + 10
92
- return -1
92
+ return -1
93
-
93
+
94
- fun hexDigit(n: Int) -> Str =
94
+ fun hexDigit(n: Int) -> Str =
95
- d := n & 0xF
95
+ d := n & 0xF
96
- if d < 10
96
+ if d < 10
97
- return digitChar(d)
97
+ return Int.digitChar(d)
98
- match d
98
+ match d
99
- 10 => "a"
99
+ 10 => "a"
100
- 11 => "b"
100
+ 11 => "b"
101
- 12 => "c"
101
+ 12 => "c"
102
- 13 => "d"
102
+ 13 => "d"
103
- 14 => "e"
103
+ 14 => "e"
104
- _ => "f"
104
+ _ => "f"
105
-
105
+
106
- # The low `count` hex digits of `n`, most-significant first.
106
+ # The low `count` hex digits of `n`, most-significant first.
107
- fun hexDigits(n: Int, count: Int) -> Str =
107
+ fun hexDigits(n: Int, count: Int) -> Str =
108
- if count <= 0
108
+ if count <= 0
109
- return ""
109
+ return ""
110
- return hexDigits(n >> 4, count - 1) + hexDigit(n)
110
+ return hexDigits(n >> 4, count - 1) + hexDigit(n)
111
111
 
112
112
  test "nil is the all-zero UUID and is recognized as such"
113
113
  assert Uuid.nil().toStr() == "00000000-0000-0000-0000-000000000000"
plum-wasm-codegen/src/lib.rs CHANGED
@@ -2735,25 +2735,26 @@ fn compileIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
2735
2735
  Instruction::End.encode(&mut body);
2736
2736
  Some(body)
2737
2737
  }
2738
- // makeBytes(n: Int) -> []Byte — a fresh, zero-filled byte slice of length `n`.
2738
+ // ByteSlice.make(n: Int) -> []Byte — a fresh, zero-filled byte slice of length `n`.
2739
+ // A static keeps a placeholder `self` in local 0, so its params start at 1.
2739
- (None, "makeBytes") => {
2740
+ (Some("ByteSlice"), "make") => {
2740
2741
  let mut body = vec![0u8];
2741
- Instruction::LocalGet(0).encode(&mut body); // n
2742
+ Instruction::LocalGet(1).encode(&mut body); // n
2742
2743
  Instruction::I32WrapI64.encode(&mut body);
2743
2744
  Instruction::ArrayNewDefault(byte_array_type_idx).encode(&mut body);
2744
2745
  Instruction::End.encode(&mut body);
2745
2746
  Some(body)
2746
2747
  }
2747
- // copyBytes(dst: []Byte, dstStart: Int, src: []Byte, srcStart: Int, n: Int) -> Unit
2748
+ // ByteSlice.copy(dst: []Byte, dstStart: Int, src: []Byte, srcStart: Int, n: Int) -> Unit
2748
- (None, "copyBytes") => {
2749
+ (Some("ByteSlice"), "copy") => {
2749
2750
  let mut body = vec![0u8];
2750
- Instruction::LocalGet(0).encode(&mut body); // dst
2751
+ Instruction::LocalGet(1).encode(&mut body); // dst
2751
- Instruction::LocalGet(1).encode(&mut body); // dstStart
2752
+ Instruction::LocalGet(2).encode(&mut body); // dstStart
2752
2753
  Instruction::I32WrapI64.encode(&mut body);
2753
- Instruction::LocalGet(2).encode(&mut body); // src
2754
+ Instruction::LocalGet(3).encode(&mut body); // src
2754
- Instruction::LocalGet(3).encode(&mut body); // srcStart
2755
+ Instruction::LocalGet(4).encode(&mut body); // srcStart
2755
2756
  Instruction::I32WrapI64.encode(&mut body);
2756
- Instruction::LocalGet(4).encode(&mut body); // n
2757
+ Instruction::LocalGet(5).encode(&mut body); // n
2757
2758
  Instruction::I32WrapI64.encode(&mut body);
2758
2759
  Instruction::ArrayCopy {
2759
2760
  array_type_index_dst: byte_array_type_idx,
@@ -2763,38 +2764,38 @@ fn compileIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
2763
2764
  Instruction::End.encode(&mut body);
2764
2765
  Some(body)
2765
2766
  }
2766
- // bytesEqual(a: []Byte, aStart: Int, b: []Byte, bStart: Int, n: Int) -> Bool
2767
+ // ByteSlice.equal(a: []Byte, aStart: Int, b: []Byte, bStart: Int, n: Int) -> Bool
2767
2768
  // — the memcmp analog: a tight loop with early exit on mismatch.
2768
- // Locals: 5 = i (i32), 6 = n (i32), 7 = aStart (i32), 8 = bStart (i32).
2769
+ // Local 0 is the unused static `self` slot; 6 = i, 7 = n, 8 = aStart, 9 = bStart (i32).
2769
- (None, "bytesEqual") => {
2770
+ (Some("ByteSlice"), "equal") => {
2770
2771
  let mut body = vec![1u8, 4u8, 0x7f];
2771
2772
  let wrap = |b: &mut Vec<u8>, l: u32| {
2772
2773
  Instruction::LocalGet(l).encode(b);
2773
2774
  Instruction::I32WrapI64.encode(b);
2774
2775
  };
2775
- wrap(&mut body, 4);
2776
+ wrap(&mut body, 5);
2776
- Instruction::LocalSet(6).encode(&mut body);
2777
- wrap(&mut body, 1);
2778
2777
  Instruction::LocalSet(7).encode(&mut body);
2779
- wrap(&mut body, 3);
2778
+ wrap(&mut body, 2);
2780
2779
  Instruction::LocalSet(8).encode(&mut body);
2780
+ wrap(&mut body, 4);
2781
+ Instruction::LocalSet(9).encode(&mut body);
2781
2782
  Instruction::Block(BlockType::Result(ValType::I32)).encode(&mut body);
2782
2783
  Instruction::Loop(BlockType::Empty).encode(&mut body);
2783
- Instruction::LocalGet(5).encode(&mut body);
2784
2784
  Instruction::LocalGet(6).encode(&mut body);
2785
+ Instruction::LocalGet(7).encode(&mut body);
2785
2786
  Instruction::I32GeS.encode(&mut body);
2786
2787
  Instruction::If(BlockType::Empty).encode(&mut body);
2787
2788
  Instruction::I32Const(1).encode(&mut body);
2788
2789
  Instruction::Br(2).encode(&mut body);
2789
2790
  Instruction::End.encode(&mut body);
2790
- Instruction::LocalGet(0).encode(&mut body);
2791
+ Instruction::LocalGet(1).encode(&mut body);
2791
- Instruction::LocalGet(7).encode(&mut body);
2792
+ Instruction::LocalGet(8).encode(&mut body);
2792
- Instruction::LocalGet(5).encode(&mut body);
2793
+ Instruction::LocalGet(6).encode(&mut body);
2793
2794
  Instruction::I32Add.encode(&mut body);
2794
2795
  Instruction::ArrayGetU(byte_array_type_idx).encode(&mut body);
2795
- Instruction::LocalGet(2).encode(&mut body);
2796
+ Instruction::LocalGet(3).encode(&mut body);
2796
- Instruction::LocalGet(8).encode(&mut body);
2797
+ Instruction::LocalGet(9).encode(&mut body);
2797
- Instruction::LocalGet(5).encode(&mut body);
2798
+ Instruction::LocalGet(6).encode(&mut body);
2798
2799
  Instruction::I32Add.encode(&mut body);
2799
2800
  Instruction::ArrayGetU(byte_array_type_idx).encode(&mut body);
2800
2801
  Instruction::I32Ne.encode(&mut body);
@@ -2802,10 +2803,10 @@ fn compileIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
2802
2803
  Instruction::I32Const(0).encode(&mut body);
2803
2804
  Instruction::Br(2).encode(&mut body);
2804
2805
  Instruction::End.encode(&mut body);
2805
- Instruction::LocalGet(5).encode(&mut body);
2806
+ Instruction::LocalGet(6).encode(&mut body);
2806
2807
  Instruction::I32Const(1).encode(&mut body);
2807
2808
  Instruction::I32Add.encode(&mut body);
2808
- Instruction::LocalSet(5).encode(&mut body);
2809
+ Instruction::LocalSet(6).encode(&mut body);
2809
2810
  Instruction::Br(0).encode(&mut body);
2810
2811
  Instruction::End.encode(&mut body); // loop
2811
2812
  Instruction::Unreachable.encode(&mut body);