plum
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 +5 -5
- plum-std/Base64.plum +110 -105
- plum-std/Buffer.plum +8 -11
- plum-std/ByteSlice.plum +27 -46
- plum-std/Float.plum +1 -1
- plum-std/Http.plum +78 -77
- plum-std/Int.plum +57 -57
- plum-std/Json.plum +98 -105
- plum-std/List.plum +30 -53
- plum-std/Os.plum +241 -240
- plum-std/Str.plum +228 -228
- plum-std/Testing.plum +55 -56
- plum-std/Time.plum +29 -29
- plum-std/Uuid.plum +30 -30
- plum-wasm-codegen/src/lib.rs +27 -26
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
|
|
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`, `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
at62b := at62.byteAt(0)
|
|
36
|
-
|
|
37
|
+
at63b := at63.byteAt(0)
|
|
37
|
-
|
|
38
|
+
has_pad := pad.length() > 0
|
|
38
|
-
|
|
39
|
+
padb := if has_pad then pad.byteAt(0) else 0
|
|
39
|
-
|
|
40
|
+
res := Buffer()
|
|
40
|
-
|
|
41
|
+
n := data.length()
|
|
41
|
-
|
|
42
|
+
i := 0
|
|
42
|
-
|
|
43
|
+
line_chars := 0
|
|
43
|
-
|
|
44
|
+
while i < n
|
|
44
|
-
|
|
45
|
+
b0 := data.byteAt(i)
|
|
45
|
-
|
|
46
|
+
has1 := i + 1 < n
|
|
46
|
-
|
|
47
|
+
has2 := i + 2 < n
|
|
47
|
-
|
|
48
|
+
b1 := if has1 then data.byteAt(i + 1) else 0
|
|
48
|
-
|
|
49
|
+
b2 := if has2 then data.byteAt(i + 2) else 0
|
|
49
|
-
|
|
50
|
+
res.writeByte(Byte(encodeByte(b0 >> 2, at62b, at63b)))
|
|
50
|
-
|
|
51
|
+
res.writeByte(Byte(encodeByte({b0 & 0x03} << 4 | b1 >> 4, at62b, at63b)))
|
|
51
|
-
|
|
52
|
+
line_chars = line_chars + 2
|
|
52
|
-
|
|
53
|
+
if has1
|
|
53
|
-
|
|
54
|
+
res.writeByte(Byte(encodeByte({b1 & 0x0F} << 2 | b2 >> 6, at62b, at63b)))
|
|
54
|
-
|
|
55
|
+
line_chars = line_chars + 1
|
|
55
|
-
|
|
56
|
+
else if has_pad
|
|
56
|
-
|
|
57
|
+
res.writeByte(Byte(padb))
|
|
57
|
-
|
|
58
|
+
line_chars = line_chars + 1
|
|
58
|
-
|
|
59
|
+
if has2
|
|
59
|
-
|
|
60
|
+
res.writeByte(Byte(encodeByte(b2 & 0x3F, at62b, at63b)))
|
|
60
|
-
|
|
61
|
+
line_chars = line_chars + 1
|
|
61
|
-
|
|
62
|
+
else if has_pad
|
|
62
|
-
|
|
63
|
+
res.writeByte(Byte(padb))
|
|
63
|
-
|
|
64
|
+
line_chars = line_chars + 1
|
|
64
|
-
|
|
65
|
+
i = i + 3
|
|
65
|
-
|
|
66
|
+
if linelen > 0 && line_chars >= linelen && i < n
|
|
66
|
-
|
|
67
|
+
res.write(linesep)
|
|
67
|
-
|
|
68
|
+
line_chars = 0
|
|
68
|
-
|
|
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
|
-
|
|
73
|
+
if i < 26
|
|
73
|
-
|
|
74
|
+
return 65 + i # 'A'..'Z'
|
|
74
|
-
|
|
75
|
+
if i < 52
|
|
75
|
-
|
|
76
|
+
return 97 + {i - 26} # 'a'..'z'
|
|
76
|
-
|
|
77
|
+
if i < 62
|
|
77
|
-
|
|
78
|
+
return 48 + {i - 52} # '0'..'9'
|
|
78
|
-
|
|
79
|
+
if i == 62
|
|
79
|
-
|
|
80
|
+
return at62
|
|
80
|
-
|
|
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
|
-
|
|
87
|
+
if b >= 65 && b <= 90
|
|
87
|
-
|
|
88
|
+
return b - 65
|
|
88
|
-
|
|
89
|
+
if b >= 97 && b <= 122
|
|
89
|
-
|
|
90
|
+
return b - 97 + 26
|
|
90
|
-
|
|
91
|
+
if b >= 48 && b <= 57
|
|
91
|
-
|
|
92
|
+
return b - 48 + 52
|
|
92
|
-
|
|
93
|
+
if b == at62
|
|
93
|
-
|
|
94
|
+
return 62
|
|
94
|
-
|
|
95
|
+
if b == at63
|
|
95
|
-
|
|
96
|
+
return 63
|
|
96
|
-
|
|
97
|
+
return -1
|
|
97
98
|
|
|
98
|
-
fun isBase64Whitespace(b: Int) -> Bool =
|
|
99
|
+
fun isBase64Whitespace(b: Int) -> Bool =
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
+
at62b := at62.byteAt(0)
|
|
110
|
-
|
|
111
|
+
at63b := at63.byteAt(0)
|
|
111
|
-
|
|
112
|
+
has_pad := pad.length() > 0
|
|
112
|
-
|
|
113
|
+
padb := if has_pad then pad.byteAt(0) else -1
|
|
113
|
-
|
|
114
|
+
res := Buffer()
|
|
114
|
-
|
|
115
|
+
n := data.length()
|
|
115
|
-
|
|
116
|
+
i := 0
|
|
116
|
-
|
|
117
|
+
bits := 0
|
|
117
|
-
|
|
118
|
+
nbits := 0
|
|
118
|
-
|
|
119
|
+
while i < n
|
|
119
|
-
|
|
120
|
+
b := data.byteAt(i)
|
|
120
|
-
|
|
121
|
+
i = i + 1
|
|
121
|
-
|
|
122
|
+
if isBase64Whitespace(b) || b == padb
|
|
122
|
-
|
|
123
|
+
continue
|
|
123
|
-
|
|
124
|
+
v := decodeByte(b, at62b, at63b)
|
|
124
|
-
|
|
125
|
+
if v < 0
|
|
125
|
-
|
|
126
|
+
return error.InvalidBase64
|
|
126
|
-
|
|
127
|
+
bits = {bits << 6} | v
|
|
127
|
-
|
|
128
|
+
nbits = nbits + 6
|
|
128
|
-
|
|
129
|
+
if nbits >= 8
|
|
129
|
-
|
|
130
|
+
nbits = nbits - 8
|
|
130
|
-
|
|
131
|
+
res.writeByte(Byte({bits >> nbits} & 0xFF))
|
|
131
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 :=
|
|
70
|
+
new_data := ByteSlice.make(new_cap)
|
|
71
|
-
|
|
71
|
+
ByteSlice.copy(new_data, 0, self.data, 0, self.len)
|
|
72
72
|
self.data = new_data
|
|
73
73
|
|
|
74
|
-
|
|
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
|
|
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,
|
|
11
|
+
# see `Str.plum`). Every method here, including the statics
|
|
12
|
-
# `
|
|
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; `
|
|
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
|
|
31
|
+
fun make(n: Int) -> []Byte =
|
|
32
|
-
|
|
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
|
|
34
|
+
# Copies `n` bytes from `src` (starting at `srcStart`) into `dst` (starting at
|
|
51
|
-
# `
|
|
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
|
-
#
|
|
36
|
+
# handled correctly, matching wasm's `array.copy`.
|
|
54
|
-
fun
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
43
|
+
todo
|
|
63
44
|
|
|
64
|
-
test "
|
|
45
|
+
test "ByteSlice.equal: equal, differ at first, differ at last, n == 0, offsets"
|
|
65
|
-
a :=
|
|
46
|
+
a := "hello world".bytes
|
|
66
|
-
b :=
|
|
47
|
+
b := "hello world".bytes
|
|
67
|
-
assert
|
|
48
|
+
assert ByteSlice.equal(a, 0, b, 0, 11)
|
|
68
|
-
c :=
|
|
49
|
+
c := "jello world".bytes
|
|
69
|
-
assert !
|
|
50
|
+
assert !ByteSlice.equal(a, 0, c, 0, 11)
|
|
70
|
-
d :=
|
|
51
|
+
d := "hello worlD".bytes
|
|
71
|
-
assert !
|
|
52
|
+
assert !ByteSlice.equal(a, 0, d, 0, 11)
|
|
72
|
-
assert
|
|
53
|
+
assert ByteSlice.equal(a, 0, d, 0, 10)
|
|
73
|
-
assert
|
|
54
|
+
assert ByteSlice.equal(a, 3, c, 3, 0)
|
|
74
|
-
assert
|
|
55
|
+
assert ByteSlice.equal(a, 6, "xxworld".bytes, 2, 5)
|
|
75
|
-
assert !
|
|
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
|
-
|
|
51
|
+
buf := Buffer()
|
|
51
|
-
|
|
52
|
+
for k, v := range headers
|
|
52
|
-
|
|
53
|
+
if buf.length() > 0
|
|
53
|
-
|
|
54
|
+
buf.write("\n")
|
|
54
|
-
|
|
55
|
+
buf.write(k)
|
|
55
|
-
|
|
56
|
+
buf.write(": ")
|
|
56
|
-
|
|
57
|
+
buf.write(v)
|
|
57
|
-
|
|
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
|
-
|
|
63
|
+
result := emptyHeaders()
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
raw := rawHttpRequest(method, url, encodeHeaders(headers), body)
|
|
81
|
-
|
|
82
|
+
sep := raw.indexOf("\x00")
|
|
82
|
-
|
|
83
|
+
meta := if sep >= 0 then raw.sub(0, sep) else raw
|
|
83
|
-
|
|
84
|
+
resp_body := if sep >= 0 then raw.sub(sep + 1, raw.length()) else ""
|
|
84
|
-
|
|
85
|
+
parts := meta.split("\x01", 3)
|
|
85
|
-
|
|
86
|
+
flag := parts.get(0) catch ""
|
|
86
|
-
|
|
87
|
+
if flag != "1"
|
|
87
|
-
|
|
88
|
+
return error.HttpFailed
|
|
88
|
-
|
|
89
|
+
status := Int.parse(parts.get(1) catch "") catch return error.BadResponse
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
+
headers.set("Content-Type", "application/json")
|
|
114
|
-
|
|
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
|
-
# ----
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
fun digitChar(d: Int) -> Str =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
fun pow10(n: Int) -> Int =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
411
|
+
e := text.indexOf("e")
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
437
|
+
assert Json.fromStr("42")!.asFloat()! == 42.0f
|
|
445
|
-
assert
|
|
438
|
+
assert Json.fromStr("-7")!.asFloat()! == -7.0f
|
|
446
|
-
assert
|
|
439
|
+
assert Json.fromStr("3.5")!.asFloat()! == 3.5f
|
|
447
|
-
assert
|
|
440
|
+
assert Json.fromStr("1e2")!.asFloat()! == 100.0f
|
|
448
|
-
assert
|
|
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
|
-
|
|
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
|
-
|
|
239
|
+
assert l.length() == 5
|
|
243
|
-
|
|
240
|
+
assert l.get(0)! == 1
|
|
244
|
-
|
|
241
|
+
assert l.get(4)! == 5
|
|
245
|
-
|
|
242
|
+
assert l.set(2, 30)! == 3
|
|
246
|
-
|
|
243
|
+
assert l.get(2)! == 30
|
|
247
244
|
l.removeAt(0)
|
|
248
|
-
|
|
245
|
+
assert l.length() == 4
|
|
249
|
-
|
|
246
|
+
assert l.get(0)! == 2
|
|
250
247
|
l.remove(30)
|
|
251
|
-
|
|
248
|
+
assert l.length() == 3
|
|
252
249
|
l.reverse()
|
|
253
|
-
|
|
250
|
+
assert l.get(0)! == 5
|
|
254
251
|
l.clear()
|
|
255
|
-
|
|
252
|
+
assert l.length() == 0
|
|
256
|
-
a + b + c + oldVal + d + e + f + g + h + i
|
|
257
253
|
|
|
258
|
-
|
|
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
|
-
|
|
260
|
+
assert l.length() == 0
|
|
265
|
-
|
|
261
|
+
assert {l.get(0) catch -1000} == -1000
|
|
266
262
|
l.add(42)
|
|
267
|
-
|
|
263
|
+
assert l.get(0)! == 42
|
|
268
|
-
afterLoopLength + isEmpty + afterReAdd
|
|
269
264
|
|
|
270
|
-
|
|
265
|
+
test "List[Int]() with no args runs init() and produces a working empty list"
|
|
271
266
|
l := List[Int]()
|
|
272
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
288
|
+
assert {sorted.get(0) catch -1000} == 1
|
|
312
|
-
assert
|
|
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
|
|
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
|
|
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
|
-
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
fun
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
fun
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
fun
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
fun
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
# Would need a
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
#
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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 :=
|
|
149
|
+
out := ByteSlice.make(n + m)
|
|
150
|
-
|
|
150
|
+
ByteSlice.copy(out, 0, self.bytes, 0, n)
|
|
151
|
-
|
|
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
|
|
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
|
|
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 :=
|
|
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
|
-
|
|
299
|
+
ByteSlice.copy(out, n - i - cl, self.bytes, i, cl)
|
|
300
300
|
i = i + cl
|
|
301
|
-
return
|
|
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
|
-
|
|
427
|
+
out := ByteSlice.make(1)
|
|
420
|
-
|
|
428
|
+
out.set(0, Byte(b))
|
|
421
|
-
|
|
429
|
+
return Str(bytes: out)
|
|
422
|
-
|
|
430
|
+
|
|
423
|
-
fun isAsciiUpper(b: Int) -> Bool =
|
|
431
|
+
fun isAsciiUpper(b: Int) -> Bool =
|
|
424
|
-
|
|
432
|
+
b >= 65 && b <= 90 # 'A'..'Z'
|
|
425
|
-
|
|
433
|
+
|
|
426
|
-
fun isAsciiLower(b: Int) -> Bool =
|
|
434
|
+
fun isAsciiLower(b: Int) -> Bool =
|
|
427
|
-
|
|
435
|
+
b >= 97 && b <= 122 # 'a'..'z'
|
|
428
|
-
|
|
436
|
+
|
|
429
|
-
fun isAsciiDigit(b: Int) -> Bool =
|
|
437
|
+
fun isAsciiDigit(b: Int) -> Bool =
|
|
430
|
-
|
|
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
|
-
|
|
447
|
+
isAsciiUpper(b) || isAsciiLower(b) || isAsciiDigit(b) || b >= 0x80
|
|
440
|
-
|
|
448
|
+
|
|
441
|
-
fun toLowerByte(b: Int) -> Int =
|
|
449
|
+
fun toLowerByte(b: Int) -> Int =
|
|
442
|
-
|
|
450
|
+
if isAsciiUpper(b) then b + 32 else b
|
|
443
|
-
|
|
451
|
+
|
|
444
|
-
fun toUpperByte(b: Int) -> Int =
|
|
452
|
+
fun toUpperByte(b: Int) -> Int =
|
|
445
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
#
|
|
468
|
-
#
|
|
469
|
-
|
|
470
|
-
if
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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 => "&"
|
|
515
|
+
60 => "<"
|
|
516
|
+
62 => ">"
|
|
517
|
+
34 => """
|
|
518
|
+
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
|
-
|
|
548
|
+
i = i + 1
|
|
525
|
-
|
|
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, "&", i, 5)
|
|
555
|
+
buf.writeByte(Byte(38))
|
|
556
|
+
return 5
|
|
557
|
+
if matchesAt(s, "<", i, 4)
|
|
558
|
+
buf.writeByte(Byte(60))
|
|
559
|
+
return 4
|
|
560
|
+
if matchesAt(s, ">", i, 4)
|
|
561
|
+
buf.writeByte(Byte(62))
|
|
562
|
+
return 4
|
|
563
|
+
if matchesAt(s, """, i, 6)
|
|
564
|
+
buf.writeByte(Byte(34))
|
|
565
|
+
return 6
|
|
566
|
+
if matchesAt(s, "'", 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
623
|
+
fun mapCase(s: Str, upper: Bool) -> Str =
|
|
532
|
-
|
|
624
|
+
n := s.length()
|
|
533
|
-
|
|
625
|
+
buf := Buffer()
|
|
534
|
-
|
|
626
|
+
i := 0
|
|
535
|
-
|
|
627
|
+
while i < n
|
|
536
|
-
|
|
628
|
+
b := s.byteAt(i)
|
|
537
|
-
if isRegExpMetaByte(b)
|
|
538
|
-
buf.writeByte(Byte(92))
|
|
539
|
-
|
|
629
|
+
buf.writeByte(Byte(if upper then toUpperByte(b) else toLowerByte(b)))
|
|
540
|
-
|
|
630
|
+
i = i + 1
|
|
541
|
-
|
|
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, "&", i, 5)
|
|
547
|
-
buf.writeByte(Byte(38))
|
|
548
|
-
return 5
|
|
549
|
-
if matchesAt(s, "<", i, 4)
|
|
550
|
-
buf.writeByte(Byte(60))
|
|
551
|
-
return 4
|
|
552
|
-
if matchesAt(s, ">", i, 4)
|
|
553
|
-
buf.writeByte(Byte(62))
|
|
554
|
-
return 4
|
|
555
|
-
if matchesAt(s, """, i, 6)
|
|
556
|
-
buf.writeByte(Byte(34))
|
|
557
|
-
return 6
|
|
558
|
-
if matchesAt(s, "'", 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
|
-
|
|
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
|
-
|
|
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 :=
|
|
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 :=
|
|
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
|
|
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
|
-
|
|
35
|
+
cond
|
|
35
36
|
|
|
36
|
-
fun expectFalse(cond: Bool) -> Bool =
|
|
37
|
+
fun expectFalse(cond: Bool) -> Bool =
|
|
37
|
-
|
|
38
|
+
!cond
|
|
38
39
|
|
|
39
|
-
fun expectEq(a: T, b: T) -> Bool =
|
|
40
|
+
fun expectEq(a: T, b: T) -> Bool =
|
|
40
|
-
|
|
41
|
+
a == b
|
|
41
42
|
|
|
42
|
-
fun expectNotEq(a: T, b: T) -> Bool =
|
|
43
|
+
fun expectNotEq(a: T, b: T) -> Bool =
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
a.equals(b)
|
|
55
56
|
|
|
56
|
-
fun expectSuccess(r: !T) -> Bool =
|
|
57
|
+
fun expectSuccess(r: !T) -> Bool =
|
|
57
|
-
|
|
58
|
+
r catch e
|
|
58
|
-
|
|
59
|
+
return false
|
|
59
|
-
|
|
60
|
+
true
|
|
60
61
|
|
|
61
|
-
fun expectFailure(r: !T) -> Bool =
|
|
62
|
+
fun expectFailure(r: !T) -> Bool =
|
|
62
|
-
|
|
63
|
+
r catch e
|
|
63
|
-
|
|
64
|
+
return true
|
|
64
|
-
|
|
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
|
-
|
|
69
|
+
r catch e
|
|
69
|
-
|
|
70
|
+
return e.name() == name
|
|
70
|
-
|
|
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
|
|
92
|
+
assert ok! == 5
|
|
96
|
-
assert {
|
|
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(
|
|
108
|
+
assert Testing.expectSuccess(ok)
|
|
110
|
-
assert expectFalse(expectSuccess(makeFailureForTestingTest()))
|
|
111
|
-
assert expectFailure(makeFailureForTestingTest())
|
|
112
|
-
assert expectFalse(
|
|
109
|
+
assert Testing.expectFalse(Testing.expectSuccess(bad))
|
|
110
|
+
assert Testing.expectFailure(bad)
|
|
111
|
+
assert Testing.expectFalse(Testing.expectFailure(ok))
|
|
113
|
-
assert expectFailureNamed(
|
|
112
|
+
assert Testing.expectFailureNamed(bad, "Bad")
|
|
114
|
-
assert expectFalse(expectFailureNamed(
|
|
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
|
-
|
|
83
|
+
# '0'-'9'
|
|
84
|
-
|
|
84
|
+
if byte >= 48 && byte <= 57
|
|
85
|
-
|
|
85
|
+
return byte - 48
|
|
86
|
-
|
|
86
|
+
# 'a'-'f'
|
|
87
|
-
|
|
87
|
+
if byte >= 97 && byte <= 102
|
|
88
|
-
|
|
88
|
+
return byte - 97 + 10
|
|
89
|
-
|
|
89
|
+
# 'A'-'F'
|
|
90
|
-
|
|
90
|
+
if byte >= 65 && byte <= 70
|
|
91
|
-
|
|
91
|
+
return byte - 65 + 10
|
|
92
|
-
|
|
92
|
+
return -1
|
|
93
|
-
|
|
93
|
+
|
|
94
|
-
fun hexDigit(n: Int) -> Str =
|
|
94
|
+
fun hexDigit(n: Int) -> Str =
|
|
95
|
-
|
|
95
|
+
d := n & 0xF
|
|
96
|
-
|
|
96
|
+
if d < 10
|
|
97
|
-
|
|
97
|
+
return Int.digitChar(d)
|
|
98
|
-
|
|
98
|
+
match d
|
|
99
|
-
|
|
99
|
+
10 => "a"
|
|
100
|
-
|
|
100
|
+
11 => "b"
|
|
101
|
-
|
|
101
|
+
12 => "c"
|
|
102
|
-
|
|
102
|
+
13 => "d"
|
|
103
|
-
|
|
103
|
+
14 => "e"
|
|
104
|
-
|
|
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
|
-
|
|
108
|
+
if count <= 0
|
|
109
|
-
|
|
109
|
+
return ""
|
|
110
|
-
|
|
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
|
-
//
|
|
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
|
-
(
|
|
2740
|
+
(Some("ByteSlice"), "make") => {
|
|
2740
2741
|
let mut body = vec![0u8];
|
|
2741
|
-
Instruction::LocalGet(
|
|
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
|
-
//
|
|
2748
|
+
// ByteSlice.copy(dst: []Byte, dstStart: Int, src: []Byte, srcStart: Int, n: Int) -> Unit
|
|
2748
|
-
(
|
|
2749
|
+
(Some("ByteSlice"), "copy") => {
|
|
2749
2750
|
let mut body = vec![0u8];
|
|
2750
|
-
Instruction::LocalGet(
|
|
2751
|
+
Instruction::LocalGet(1).encode(&mut body); // dst
|
|
2751
|
-
Instruction::LocalGet(
|
|
2752
|
+
Instruction::LocalGet(2).encode(&mut body); // dstStart
|
|
2752
2753
|
Instruction::I32WrapI64.encode(&mut body);
|
|
2753
|
-
Instruction::LocalGet(
|
|
2754
|
+
Instruction::LocalGet(3).encode(&mut body); // src
|
|
2754
|
-
Instruction::LocalGet(
|
|
2755
|
+
Instruction::LocalGet(4).encode(&mut body); // srcStart
|
|
2755
2756
|
Instruction::I32WrapI64.encode(&mut body);
|
|
2756
|
-
Instruction::LocalGet(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
2769
|
+
// Local 0 is the unused static `self` slot; 6 = i, 7 = n, 8 = aStart, 9 = bStart (i32).
|
|
2769
|
-
(
|
|
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,
|
|
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,
|
|
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(
|
|
2791
|
+
Instruction::LocalGet(1).encode(&mut body);
|
|
2791
|
-
Instruction::LocalGet(
|
|
2792
|
+
Instruction::LocalGet(8).encode(&mut body);
|
|
2792
|
-
Instruction::LocalGet(
|
|
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(
|
|
2796
|
+
Instruction::LocalGet(3).encode(&mut body);
|
|
2796
|
-
Instruction::LocalGet(
|
|
2797
|
+
Instruction::LocalGet(9).encode(&mut body);
|
|
2797
|
-
Instruction::LocalGet(
|
|
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(
|
|
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(
|
|
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);
|