plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
45dada4
— Peter John
2026-10-25T16:17:50+05:30
feat(std)!: replace Option/Result with Try and structural error tags
- plum-std/Base64.plum +6 -6
- plum-std/Bool.plum +3 -3
- plum-std/Err.plum +0 -10
- plum-std/Http.plum +12 -13
- plum-std/Json.plum +45 -60
- plum-std/List.plum +113 -108
- plum-std/Map.plum +24 -25
- plum-std/Number.plum +11 -11
- plum-std/Option.plum +0 -144
- plum-std/Os.plum +19 -19
- plum-std/Result.plum +0 -141
- plum-std/Str.plum +1 -1
- plum-std/Testing.plum +9 -24
- plum-std/Try.plum +184 -0
- plum-std/Uuid.plum +2 -2
- plum-wasm-codegen/src/lib.rs +10 -1
- plum-wasm-codegen/tests/codegen_tests.rs +19 -0
plum-std/Base64.plum
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/Str
|
|
3
3
|
import std/Buffer
|
|
4
|
-
import std/
|
|
4
|
+
import std/Try
|
|
5
5
|
import std/Bool
|
|
6
6
|
import std/Number
|
|
7
7
|
|
|
@@ -100,13 +100,13 @@ fun isBase64Whitespace(b: Int) -> Bool =
|
|
|
100
100
|
b == 32 || b == 9 || b == 10 || b == 13
|
|
101
101
|
|
|
102
102
|
# Decode for URLs (RFC 4648).
|
|
103
|
-
fun decodeUrl(data: Str) ->
|
|
103
|
+
fun decodeUrl(data: Str) -> Try[Str, [InvalidBase64]] =
|
|
104
104
|
return decode(data, "-", "_", "=")
|
|
105
105
|
|
|
106
106
|
# Configurable decoding. The defaults are for RFC 4648. Missing padding is
|
|
107
107
|
# not an error. Non-base64 data, other than whitespace (which can appear at
|
|
108
|
-
# any point), is an error (`
|
|
108
|
+
# any point), is an error (`Err(InvalidBase64)`).
|
|
109
|
-
fun decode(data: Str, at62: Str = "+", at63: Str = "/", pad: Str = "=") ->
|
|
109
|
+
fun decode(data: Str, at62: Str = "+", at63: Str = "/", pad: Str = "=") -> Try[Str, [InvalidBase64]] =
|
|
110
110
|
at62b := at62.byteAt(0)
|
|
111
111
|
at63b := at63.byteAt(0)
|
|
112
112
|
has_pad := pad.length() > 0
|
|
@@ -123,10 +123,10 @@ fun decode(data: Str, at62: Str = "+", at63: Str = "/", pad: Str = "=") -> Optio
|
|
|
123
123
|
continue
|
|
124
124
|
v := decodeByte(b, at62b, at63b)
|
|
125
125
|
if v < 0
|
|
126
|
-
return
|
|
126
|
+
return Err(InvalidBase64)
|
|
127
127
|
bits = {bits << 6} | v
|
|
128
128
|
nbits = nbits + 6
|
|
129
129
|
if nbits >= 8
|
|
130
130
|
nbits = nbits - 8
|
|
131
131
|
res.writeByte(Byte({bits >> nbits} & 0xFF))
|
|
132
|
-
return
|
|
132
|
+
return Ok(res.toStr())
|
plum-std/Bool.plum
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
module std
|
|
2
|
-
import std/
|
|
2
|
+
import std/Try
|
|
3
3
|
import std/Str
|
|
4
4
|
|
|
5
5
|
enum Bool =
|
|
6
6
|
| True
|
|
7
7
|
| False
|
|
8
8
|
|
|
9
|
-
fun parse(v: Str) ->
|
|
9
|
+
fun parse(v: Str) -> Try[Bool, [BadBoolStr]] =
|
|
10
10
|
if v == "true"
|
|
11
11
|
Ok(True)
|
|
12
12
|
else if v == "false"
|
|
13
13
|
Ok(False)
|
|
14
14
|
else
|
|
15
|
-
Err(
|
|
15
|
+
Err(BadBoolStr)
|
|
16
16
|
|
|
17
17
|
fun and(self, o: Bool) -> Bool =
|
|
18
18
|
match self, o
|
plum-std/Err.plum
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
module std
|
|
2
|
-
import std/Bool
|
|
3
|
-
import std/Number
|
|
4
|
-
import std/Str
|
|
5
|
-
|
|
6
|
-
# This is used to represent an error value across the language
|
|
7
|
-
trait Err =
|
|
8
|
-
code() -> Int
|
|
9
|
-
msg() -> Str
|
|
10
|
-
cause() -> Option
|
plum-std/Http.plum
CHANGED
|
@@ -4,8 +4,7 @@ import std/Buffer
|
|
|
4
4
|
import std/Json
|
|
5
5
|
import std/List
|
|
6
6
|
import std/Map
|
|
7
|
-
import std/
|
|
7
|
+
import std/Try
|
|
8
|
-
import std/Result
|
|
9
8
|
import std/Str
|
|
10
9
|
import std/Bool
|
|
11
10
|
import std/Number
|
|
@@ -25,7 +24,7 @@ enum Response =
|
|
|
25
24
|
|
|
26
25
|
# Parses `body` as JSON. Fails the same way `Json.fromStr` does for a
|
|
27
26
|
# response body that isn't valid JSON.
|
|
28
|
-
fun json(self) ->
|
|
27
|
+
fun json(self) -> Try[Json, [InvalidJson(Str)]] =
|
|
29
28
|
return Json.fromStr(self.body)
|
|
30
29
|
|
|
31
30
|
fun emptyHeaders() -> Map[Str, Str] =
|
|
@@ -75,7 +74,7 @@ fun decodeHeaders(raw: Str) -> Map[Str, Str] =
|
|
|
75
74
|
# error, ...) — an HTTP-level error status (404, 500, ...) is still a
|
|
76
75
|
# successful round trip, returned as `Ok(Response(...))` with that status
|
|
77
76
|
# (check `Response.ok()`).
|
|
78
|
-
fun request(method: Str, url: Str, headers: Map[Str, Str], body: Str) ->
|
|
77
|
+
fun request(method: Str, url: Str, headers: Map[Str, Str], body: Str) -> Try[Response, Str] =
|
|
79
78
|
raw := rawHttpRequest(method, url, encodeHeaders(headers), body)
|
|
80
79
|
sep := raw.indexOf("\x00")
|
|
81
80
|
meta := sep >= 0 ? raw.sub(0, sep) : raw
|
|
@@ -87,31 +86,31 @@ fun request(method: Str, url: Str, headers: Map[Str, Str], body: Str) -> Result[
|
|
|
87
86
|
match parseInt(parts.get(1).unwrapOr(""))
|
|
88
87
|
Ok(status) =>
|
|
89
88
|
return Ok(Response(status: status, headers: decodeHeaders(parts.get(2).unwrapOr("")), body: resp_body))
|
|
90
|
-
Err(
|
|
89
|
+
Err(_) =>
|
|
91
|
-
return Err("invalid status code in http response
|
|
90
|
+
return Err("invalid status code in http response")
|
|
92
91
|
|
|
93
|
-
fun get(url: Str, headers: Map[Str, Str]) ->
|
|
92
|
+
fun get(url: Str, headers: Map[Str, Str]) -> Try[Response, Str] =
|
|
94
93
|
return request("GET", url, headers, "")
|
|
95
94
|
|
|
96
|
-
fun head(url: Str, headers: Map[Str, Str]) ->
|
|
95
|
+
fun head(url: Str, headers: Map[Str, Str]) -> Try[Response, Str] =
|
|
97
96
|
return request("HEAD", url, headers, "")
|
|
98
97
|
|
|
99
|
-
fun delete(url: Str, headers: Map[Str, Str]) ->
|
|
98
|
+
fun delete(url: Str, headers: Map[Str, Str]) -> Try[Response, Str] =
|
|
100
99
|
return request("DELETE", url, headers, "")
|
|
101
100
|
|
|
102
|
-
fun post(url: Str, headers: Map[Str, Str], body: Str) ->
|
|
101
|
+
fun post(url: Str, headers: Map[Str, Str], body: Str) -> Try[Response, Str] =
|
|
103
102
|
return request("POST", url, headers, body)
|
|
104
103
|
|
|
105
|
-
fun put(url: Str, headers: Map[Str, Str], body: Str) ->
|
|
104
|
+
fun put(url: Str, headers: Map[Str, Str], body: Str) -> Try[Response, Str] =
|
|
106
105
|
return request("PUT", url, headers, body)
|
|
107
106
|
|
|
108
|
-
fun patch(url: Str, headers: Map[Str, Str], body: Str) ->
|
|
107
|
+
fun patch(url: Str, headers: Map[Str, Str], body: Str) -> Try[Response, Str] =
|
|
109
108
|
return request("PATCH", url, headers, body)
|
|
110
109
|
|
|
111
110
|
# Convenience wrapper: sends `body` as a JSON `Str`, adding a
|
|
112
111
|
# `Content-Type: application/json` header, and parses the response back as
|
|
113
112
|
# JSON via `Response.json()`.
|
|
114
|
-
fun postJson(url: Str, headers: Map[Str, Str], body: Json) ->
|
|
113
|
+
fun postJson(url: Str, headers: Map[Str, Str], body: Json) -> Try[Response, Str] =
|
|
115
114
|
headers.set("Content-Type", "application/json")
|
|
116
115
|
return post(url, headers, body.toStr())
|
|
117
116
|
|
plum-std/Json.plum
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
module std
|
|
2
2
|
|
|
3
|
-
import std/
|
|
3
|
+
import std/Try
|
|
4
|
-
import std/Result
|
|
5
4
|
import std/List
|
|
6
5
|
import std/Map
|
|
7
6
|
import std/Number
|
|
8
7
|
import std/Str
|
|
9
8
|
import std/Buffer
|
|
10
|
-
import std/Err
|
|
11
9
|
import std/Bool
|
|
12
10
|
|
|
13
11
|
# A parsed JSON value. Numbers are always stored as `Float` (JSON doesn't
|
|
14
12
|
# distinguish integers from floats the way Plum does), and `JsonList`/
|
|
15
13
|
# `JsonMap` nest `Json` itself — the same self-referential shape `List[T]`
|
|
16
|
-
# already uses internally for `Node[T].next:
|
|
14
|
+
# already uses internally for `Node[T].next: Link[Node[T]]`.
|
|
17
15
|
enum Json =
|
|
18
16
|
| JsonNull
|
|
19
17
|
| JsonBool(Bool)
|
|
@@ -25,17 +23,17 @@ enum Json =
|
|
|
25
23
|
# Parses `s` as a single JSON document, requiring the whole string (aside
|
|
26
24
|
# from surrounding whitespace) to be consumed. Called as `Json.fromStr(s)`,
|
|
27
25
|
# matching `Int.fromStr`/`Float.fromStr`'s naming.
|
|
28
|
-
fun fromStr(s: Str) ->
|
|
26
|
+
fun fromStr(s: Str) -> Try[Json, [InvalidJson(Str)]] =
|
|
29
27
|
parser := JsonParser(src: s, pos: 0)
|
|
30
28
|
parser.skipSpace()
|
|
31
29
|
r := parser.parseValue()
|
|
32
30
|
match r
|
|
33
31
|
Err(e) =>
|
|
34
|
-
return Err(e)
|
|
32
|
+
return Err(InvalidJson(e.msg()))
|
|
35
33
|
Ok(v) =>
|
|
36
34
|
parser.skipSpace()
|
|
37
35
|
if parser.pos < parser.src.length()
|
|
38
|
-
return Err(parser.fail("unexpected trailing characters"))
|
|
36
|
+
return Err(InvalidJson(parser.fail("unexpected trailing characters").msg()))
|
|
39
37
|
return Ok(v)
|
|
40
38
|
|
|
41
39
|
# Renders `self` back to its JSON text form. A literal `{`/`}` is written
|
|
@@ -55,52 +53,39 @@ enum Json =
|
|
|
55
53
|
JsonNull => True
|
|
56
54
|
_ => False
|
|
57
55
|
|
|
58
|
-
fun asBool(self) ->
|
|
56
|
+
fun asBool(self) -> Try[Bool, [WrongJsonType]] =
|
|
59
57
|
match self
|
|
60
|
-
JsonBool(b) =>
|
|
58
|
+
JsonBool(b) => Ok(b)
|
|
61
|
-
_ =>
|
|
59
|
+
_ => Err(WrongJsonType)
|
|
62
60
|
|
|
63
|
-
fun asFloat(self) ->
|
|
61
|
+
fun asFloat(self) -> Try[Float, [WrongJsonType]] =
|
|
64
62
|
match self
|
|
65
|
-
JsonFloat(f) =>
|
|
63
|
+
JsonFloat(f) => Ok(f)
|
|
66
|
-
_ =>
|
|
64
|
+
_ => Err(WrongJsonType)
|
|
67
65
|
|
|
68
|
-
fun asStr(self) ->
|
|
66
|
+
fun asStr(self) -> Try[Str, [WrongJsonType]] =
|
|
69
67
|
match self
|
|
70
|
-
JsonStr(s) =>
|
|
68
|
+
JsonStr(s) => Ok(s)
|
|
71
|
-
_ =>
|
|
69
|
+
_ => Err(WrongJsonType)
|
|
72
70
|
|
|
73
|
-
fun asList(self) ->
|
|
71
|
+
fun asList(self) -> Try[List[Json], [WrongJsonType]] =
|
|
74
72
|
match self
|
|
75
|
-
JsonList(items) =>
|
|
73
|
+
JsonList(items) => Ok(items)
|
|
76
|
-
_ =>
|
|
74
|
+
_ => Err(WrongJsonType)
|
|
77
75
|
|
|
78
|
-
fun asMap(self) ->
|
|
76
|
+
fun asMap(self) -> Try[Map[Str, Json], [WrongJsonType]] =
|
|
79
77
|
match self
|
|
80
|
-
JsonMap(pairs) =>
|
|
78
|
+
JsonMap(pairs) => Ok(pairs)
|
|
81
|
-
_ =>
|
|
79
|
+
_ => Err(WrongJsonType)
|
|
82
80
|
|
|
83
81
|
# Carries where in the source a parse failure happened, alongside the
|
|
84
|
-
# message
|
|
82
|
+
# message, so the public `Json.fromStr` can fold it into `InvalidJson(Str)`.
|
|
85
|
-
# than being a plain `Str`, so a caller that wants the position can get it
|
|
86
|
-
# without re-parsing the message.
|
|
87
|
-
enum JsonParseError
|
|
83
|
+
enum JsonParseError =
|
|
88
84
|
| JsonParseError(pos: Int, text: Str)
|
|
89
85
|
|
|
90
|
-
fun code(self) -> Int =
|
|
91
|
-
1
|
|
92
|
-
|
|
93
86
|
fun msg(self) -> Str =
|
|
94
87
|
"json parse error at position {self.pos}: {self.text}"
|
|
95
88
|
|
|
96
|
-
# A bare payload-free `None` here can't be disambiguated to one specific
|
|
97
|
-
# `Option` instantiation from this expression alone (see the README's
|
|
98
|
-
# "Known gaps" note on this residual generics limitation — unrelated to the
|
|
99
|
-
# 3 points fixed above) — an explicit `Option[JsonParseError]` return type
|
|
100
|
-
# sidesteps it.
|
|
101
|
-
fun cause(self) -> Option[JsonParseError] =
|
|
102
|
-
None
|
|
103
|
-
|
|
104
89
|
# A recursive-descent parser over a `Str` source, tracking its read position
|
|
105
90
|
# in `pos`. (The original design threaded a `Readable` stream through here,
|
|
106
91
|
# but `Readable` is an undefined trait with no methods anywhere in `libs/std`
|
|
@@ -129,7 +114,7 @@ enum JsonParser =
|
|
|
129
114
|
while isSpace(self.peek())
|
|
130
115
|
self.advance()
|
|
131
116
|
|
|
132
|
-
fun parseValue(self) ->
|
|
117
|
+
fun parseValue(self) -> Try[Json, JsonParseError] =
|
|
133
118
|
self.skipSpace()
|
|
134
119
|
c := self.peek()
|
|
135
120
|
if c == 34
|
|
@@ -148,14 +133,14 @@ enum JsonParser =
|
|
|
148
133
|
return self.parseNumber()
|
|
149
134
|
return Err(self.fail("unexpected character"))
|
|
150
135
|
|
|
151
|
-
fun parseLiteral(self, lit: Str, value: Json) ->
|
|
136
|
+
fun parseLiteral(self, lit: Str, value: Json) -> Try[Json, JsonParseError] =
|
|
152
137
|
n := lit.length()
|
|
153
138
|
if !matchesAt(self.src, lit, self.pos, n)
|
|
154
139
|
return Err(self.fail("invalid literal, expected '{lit}'"))
|
|
155
140
|
self.pos = self.pos + n
|
|
156
141
|
return Ok(value)
|
|
157
142
|
|
|
158
|
-
fun parseNumber(self) ->
|
|
143
|
+
fun parseNumber(self) -> Try[Json, JsonParseError] =
|
|
159
144
|
start := self.pos
|
|
160
145
|
if self.peek() == 45
|
|
161
146
|
self.advance()
|
|
@@ -182,32 +167,32 @@ enum JsonParser =
|
|
|
182
167
|
# `Float.fromStr` (`libs/std/float.plum`) doesn't understand exponent
|
|
183
168
|
# notation, so a number that has one is split into its mantissa and
|
|
184
169
|
# exponent around the `e`/`E` and recombined here instead.
|
|
185
|
-
fun parseNumberText(self, text: Str) ->
|
|
170
|
+
fun parseNumberText(self, text: Str) -> Try[Json, JsonParseError] =
|
|
186
171
|
e_index := exponentIndex(text)
|
|
187
172
|
if e_index < 0
|
|
188
173
|
r := parseFloat(text)
|
|
189
174
|
match r
|
|
190
175
|
Ok(f) =>
|
|
191
176
|
return Ok(JsonFloat(f))
|
|
192
|
-
Err(
|
|
177
|
+
Err(_) =>
|
|
193
|
-
return Err(self.fail(
|
|
178
|
+
return Err(self.fail("invalid number"))
|
|
194
179
|
mantissa := parseFloat(text.sub(0, e_index))
|
|
195
180
|
exponent := parseInt(text.sub(e_index + 1, text.length()))
|
|
196
181
|
match mantissa
|
|
197
|
-
Err(
|
|
182
|
+
Err(_) =>
|
|
198
|
-
return Err(self.fail(
|
|
183
|
+
return Err(self.fail("invalid number"))
|
|
199
184
|
Ok(mv) =>
|
|
200
185
|
match exponent
|
|
201
|
-
Err(
|
|
186
|
+
Err(_) =>
|
|
202
|
-
return Err(self.fail(
|
|
187
|
+
return Err(self.fail("invalid number"))
|
|
203
188
|
Ok(ev) =>
|
|
204
189
|
return Ok(JsonFloat(mv * 10.pow(Float(ev))))
|
|
205
190
|
|
|
206
|
-
fun parseString(self) ->
|
|
191
|
+
fun parseString(self) -> Try[Json, JsonParseError] =
|
|
207
192
|
self.advance()
|
|
208
193
|
return self.parseStringBody(Buffer())
|
|
209
194
|
|
|
210
|
-
fun parseStringBody(self, buf: Buffer) ->
|
|
195
|
+
fun parseStringBody(self, buf: Buffer) -> Try[Json, JsonParseError] =
|
|
211
196
|
c := self.peek()
|
|
212
197
|
if c == 0
|
|
213
198
|
return Err(self.fail("unterminated string"))
|
|
@@ -221,7 +206,7 @@ enum JsonParser =
|
|
|
221
206
|
self.pos = self.pos + n
|
|
222
207
|
return self.parseStringBody(buf)
|
|
223
208
|
|
|
224
|
-
fun parseEscape(self, buf: Buffer) ->
|
|
209
|
+
fun parseEscape(self, buf: Buffer) -> Try[Json, JsonParseError] =
|
|
225
210
|
self.advance()
|
|
226
211
|
esc := self.peek()
|
|
227
212
|
if esc == 34
|
|
@@ -261,10 +246,10 @@ enum JsonParser =
|
|
|
261
246
|
return self.parseUnicodeEscape(buf)
|
|
262
247
|
return Err(self.fail("invalid escape character"))
|
|
263
248
|
|
|
264
|
-
fun parseHex4(self) ->
|
|
249
|
+
fun parseHex4(self) -> Try[Int, JsonParseError] =
|
|
265
250
|
return self.parseHex4From(0, 0)
|
|
266
251
|
|
|
267
|
-
fun parseHex4From(self, value: Int, count: Int) ->
|
|
252
|
+
fun parseHex4From(self, value: Int, count: Int) -> Try[Int, JsonParseError] =
|
|
268
253
|
if count == 4
|
|
269
254
|
return Ok(value)
|
|
270
255
|
d := hexDigitValue(self.peek())
|
|
@@ -273,7 +258,7 @@ enum JsonParser =
|
|
|
273
258
|
self.advance()
|
|
274
259
|
return self.parseHex4From(value * 16 + d, count + 1)
|
|
275
260
|
|
|
276
|
-
fun parseUnicodeEscape(self, buf: Buffer) ->
|
|
261
|
+
fun parseUnicodeEscape(self, buf: Buffer) -> Try[Json, JsonParseError] =
|
|
277
262
|
hr := self.parseHex4()
|
|
278
263
|
match hr
|
|
279
264
|
Err(e) =>
|
|
@@ -284,7 +269,7 @@ enum JsonParser =
|
|
|
284
269
|
# A high surrogate (`0xD800`..`0xDBFF`) must be followed immediately by a
|
|
285
270
|
# low surrogate (`0xDC00`..`0xDFFF`) `\u` escape; the pair together encodes
|
|
286
271
|
# one codepoint above `0xFFFF`. Any other codepoint is used as-is.
|
|
287
|
-
fun parseUnicodeEscapeCont(self, buf: Buffer, cp: Int) ->
|
|
272
|
+
fun parseUnicodeEscapeCont(self, buf: Buffer, cp: Int) -> Try[Json, JsonParseError] =
|
|
288
273
|
if cp >= 0xD800 && cp <= 0xDBFF && self.peek() == 92 && self.peekAt(1) == 117
|
|
289
274
|
self.advance()
|
|
290
275
|
self.advance()
|
|
@@ -301,16 +286,16 @@ enum JsonParser =
|
|
|
301
286
|
buf.writeRune(cp)
|
|
302
287
|
return self.parseStringBody(buf)
|
|
303
288
|
|
|
304
|
-
fun parseArray(self) ->
|
|
289
|
+
fun parseArray(self) -> Try[Json, JsonParseError] =
|
|
305
290
|
self.advance()
|
|
306
291
|
self.skipSpace()
|
|
307
|
-
items := List[Json](head:
|
|
292
|
+
items := List[Json](head: End, tail: End, size: 0)
|
|
308
293
|
if self.peek() == 93
|
|
309
294
|
self.advance()
|
|
310
295
|
return Ok(JsonList(items))
|
|
311
296
|
return self.parseArrayItems(items)
|
|
312
297
|
|
|
313
|
-
fun parseArrayItems(self, items: List[Json]) ->
|
|
298
|
+
fun parseArrayItems(self, items: List[Json]) -> Try[Json, JsonParseError] =
|
|
314
299
|
self.skipSpace()
|
|
315
300
|
r := self.parseValue()
|
|
316
301
|
match r
|
|
@@ -328,7 +313,7 @@ enum JsonParser =
|
|
|
328
313
|
return Ok(JsonList(items))
|
|
329
314
|
return Err(self.fail("expected ',' or ']'"))
|
|
330
315
|
|
|
331
|
-
fun parseObject(self) ->
|
|
316
|
+
fun parseObject(self) -> Try[Json, JsonParseError] =
|
|
332
317
|
self.advance()
|
|
333
318
|
self.skipSpace()
|
|
334
319
|
pairs := Map[Str, Json]()
|
|
@@ -337,7 +322,7 @@ enum JsonParser =
|
|
|
337
322
|
return Ok(JsonMap(pairs))
|
|
338
323
|
return self.parseObjectItems(pairs)
|
|
339
324
|
|
|
340
|
-
fun parseObjectItems(self, pairs: Map[Str, Json]) ->
|
|
325
|
+
fun parseObjectItems(self, pairs: Map[Str, Json]) -> Try[Json, JsonParseError] =
|
|
341
326
|
self.skipSpace()
|
|
342
327
|
if self.peek() != 34
|
|
343
328
|
return Err(self.fail("expected string key"))
|
|
@@ -348,7 +333,7 @@ enum JsonParser =
|
|
|
348
333
|
Ok(keyJson) =>
|
|
349
334
|
return self.parseObjectValue(pairs, jsonStrValue(keyJson))
|
|
350
335
|
|
|
351
|
-
fun parseObjectValue(self, pairs: Map[Str, Json], key: Str) ->
|
|
336
|
+
fun parseObjectValue(self, pairs: Map[Str, Json], key: Str) -> Try[Json, JsonParseError] =
|
|
352
337
|
self.skipSpace()
|
|
353
338
|
if self.peek() != 58
|
|
354
339
|
return Err(self.fail("expected ':'"))
|
plum-std/List.plum
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
module std
|
|
2
2
|
|
|
3
|
-
import std/
|
|
3
|
+
import std/Try
|
|
4
4
|
import std/Buffer
|
|
5
5
|
import std/Number
|
|
6
6
|
import std/Bool
|
|
7
7
|
import std/Str
|
|
8
8
|
|
|
9
9
|
# A node stores the data in a list and contains pointers to the previous and next sibling nodes
|
|
10
|
+
# Private link between nodes (not an error tag, so a domain enum rather than Try).
|
|
11
|
+
enum Link[N] =
|
|
12
|
+
| Next(N)
|
|
13
|
+
| End
|
|
14
|
+
|
|
10
15
|
enum Node[T] =
|
|
11
|
-
| Node(value: T, prev:
|
|
16
|
+
| Node(value: T, prev: Link[Node[T]], next: Link[Node[T]])
|
|
12
17
|
|
|
13
18
|
# A list is a data structure describing a contiguous section of an array stored separately from the slice variable itself.
|
|
14
19
|
# It contains the pointers to the start and end nodes (head, tail) and maintains the size as well
|
|
15
20
|
enum List[T: ToStr](ToStr) =
|
|
16
|
-
| List(head:
|
|
21
|
+
| List(head: Link[Node[T]], tail: Link[Node[T]], size: Int)
|
|
17
22
|
|
|
18
23
|
# `List[T](1, 2, 3)` (or, for an empty list, `List[T]()` — explicit
|
|
19
24
|
# generics required either way, since nothing else in a call this shape
|
|
@@ -21,42 +26,42 @@ enum List[T: ToStr](ToStr) =
|
|
|
21
26
|
# `plum-checker`/`plum-wasm-codegen`'s `ClassCall`/`FnCall` handling, which
|
|
22
27
|
# desugars both call shapes to `List.init(...)`.
|
|
23
28
|
fun init(values: ...T) -> List[T] =
|
|
24
|
-
l := List(head:
|
|
29
|
+
l := List(head: End, tail: End, size: 0)
|
|
25
30
|
for v := range values
|
|
26
31
|
l.add(v)
|
|
27
32
|
return l
|
|
28
33
|
|
|
29
34
|
# gets the element at i'th index of the list
|
|
30
|
-
fun get(self, i: Int) ->
|
|
35
|
+
fun get(self, i: Int) -> Try[T, [OutOfBounds]] =
|
|
31
36
|
current := self.head
|
|
32
37
|
index := 0
|
|
33
|
-
while current !=
|
|
38
|
+
while current != End
|
|
34
39
|
match current
|
|
35
|
-
|
|
40
|
+
Next(Node(value, _, next)) =>
|
|
36
41
|
if index == i
|
|
37
|
-
return
|
|
42
|
+
return Ok(value)
|
|
38
43
|
current = next
|
|
39
44
|
index = index + 1
|
|
40
|
-
|
|
45
|
+
End =>
|
|
41
46
|
break
|
|
42
|
-
|
|
47
|
+
Err(OutOfBounds)
|
|
43
48
|
|
|
44
|
-
# sets the element at i'th index of the list, returning the old value (or
|
|
49
|
+
# sets the element at i'th index of the list, returning the old value (or Err(OutOfBounds) if i is out of bounds)
|
|
45
|
-
fun set(self, i: Int, v: T) ->
|
|
50
|
+
fun set(self, i: Int, v: T) -> Try[T, [OutOfBounds]] =
|
|
46
51
|
current := self.head
|
|
47
52
|
index := 0
|
|
48
|
-
while current !=
|
|
53
|
+
while current != End
|
|
49
54
|
match current
|
|
50
|
-
|
|
55
|
+
Next(node) =>
|
|
51
56
|
if index == i
|
|
52
57
|
old := node.value
|
|
53
58
|
node.value = v
|
|
54
|
-
return
|
|
59
|
+
return Ok(old)
|
|
55
60
|
current = node.next
|
|
56
61
|
index = index + 1
|
|
57
|
-
|
|
62
|
+
End =>
|
|
58
63
|
break
|
|
59
|
-
|
|
64
|
+
Err(OutOfBounds)
|
|
60
65
|
|
|
61
66
|
# returns the no of elements in the list
|
|
62
67
|
fun length(self) -> Int =
|
|
@@ -76,26 +81,26 @@ enum List[T: ToStr](ToStr) =
|
|
|
76
81
|
# adds the specified elements to the end of the list
|
|
77
82
|
fun add(self, values: ...T) =
|
|
78
83
|
for v := range values
|
|
79
|
-
node := Node(value: v, prev: self.tail, next:
|
|
84
|
+
node := Node(value: v, prev: self.tail, next: End)
|
|
80
85
|
match self.tail
|
|
81
|
-
|
|
86
|
+
Next(t) =>
|
|
82
|
-
t.next =
|
|
87
|
+
t.next = Next(node)
|
|
83
|
-
|
|
88
|
+
End =>
|
|
84
|
-
self.head =
|
|
89
|
+
self.head = Next(node)
|
|
85
|
-
self.tail =
|
|
90
|
+
self.tail = Next(node)
|
|
86
91
|
self.size = self.size + 1
|
|
87
92
|
|
|
88
93
|
# unlinks node from the list, patching its neighbors (or head/tail) to close the gap
|
|
89
94
|
fun unlink(self, node: Node[T]) =
|
|
90
95
|
match node.prev
|
|
91
|
-
|
|
96
|
+
Next(p) =>
|
|
92
97
|
p.next = node.next
|
|
93
|
-
|
|
98
|
+
End =>
|
|
94
99
|
self.head = node.next
|
|
95
100
|
match node.next
|
|
96
|
-
|
|
101
|
+
Next(n) =>
|
|
97
102
|
n.prev = node.prev
|
|
98
|
-
|
|
103
|
+
End =>
|
|
99
104
|
self.tail = node.prev
|
|
100
105
|
self.size = self.size - 1
|
|
101
106
|
|
|
@@ -103,47 +108,47 @@ enum List[T: ToStr](ToStr) =
|
|
|
103
108
|
fun removeAt(self, i: Int) =
|
|
104
109
|
current := self.head
|
|
105
110
|
index := 0
|
|
106
|
-
while current !=
|
|
111
|
+
while current != End
|
|
107
112
|
match current
|
|
108
|
-
|
|
113
|
+
Next(node) =>
|
|
109
114
|
if index == i
|
|
110
115
|
self.unlink(node)
|
|
111
116
|
return
|
|
112
117
|
current = node.next
|
|
113
118
|
index = index + 1
|
|
114
|
-
|
|
119
|
+
End =>
|
|
115
120
|
break
|
|
116
121
|
|
|
117
122
|
# removes the first element equal to v from list
|
|
118
123
|
fun remove(self, v: T) =
|
|
119
124
|
current := self.head
|
|
120
|
-
while current !=
|
|
125
|
+
while current != End
|
|
121
126
|
match current
|
|
122
|
-
|
|
127
|
+
Next(node) =>
|
|
123
128
|
if node.value == v
|
|
124
129
|
self.unlink(node)
|
|
125
130
|
return
|
|
126
131
|
current = node.next
|
|
127
|
-
|
|
132
|
+
End =>
|
|
128
133
|
break
|
|
129
134
|
|
|
130
135
|
# removes all objects from this list
|
|
131
136
|
fun clear(self) =
|
|
132
|
-
self.head =
|
|
137
|
+
self.head = End
|
|
133
|
-
self.tail =
|
|
138
|
+
self.tail = End
|
|
134
139
|
self.size = 0
|
|
135
140
|
|
|
136
141
|
# returns the list with the elements relinked in reverse order.
|
|
137
142
|
fun reverse(self) -> List =
|
|
138
143
|
current := self.head
|
|
139
|
-
while current !=
|
|
144
|
+
while current != End
|
|
140
145
|
match current
|
|
141
|
-
|
|
146
|
+
Next(node) =>
|
|
142
147
|
next := node.next
|
|
143
148
|
node.next = node.prev
|
|
144
149
|
node.prev = next
|
|
145
150
|
current = next
|
|
146
|
-
|
|
151
|
+
End =>
|
|
147
152
|
break
|
|
148
153
|
old_head := self.head
|
|
149
154
|
self.head = self.tail
|
|
@@ -155,13 +160,13 @@ enum List[T: ToStr](ToStr) =
|
|
|
155
160
|
# `sort`'s pointer surgery, split out since `unlink`'s counterpart already
|
|
156
161
|
# gets its own method.
|
|
157
162
|
fun insertBefore(self, node: Node[T], value: T) -> Unit =
|
|
158
|
-
new_node := Node(value: value, prev: node.prev, next:
|
|
163
|
+
new_node := Node(value: value, prev: node.prev, next: Next(node))
|
|
159
164
|
match node.prev
|
|
160
|
-
|
|
165
|
+
Next(p) =>
|
|
161
|
-
p.next =
|
|
166
|
+
p.next = Next(new_node)
|
|
162
|
-
|
|
167
|
+
End =>
|
|
163
|
-
self.head =
|
|
168
|
+
self.head = Next(new_node)
|
|
164
|
-
node.prev =
|
|
169
|
+
node.prev = Next(new_node)
|
|
165
170
|
self.size = self.size + 1
|
|
166
171
|
|
|
167
172
|
# Inserts `value` into `self` (assumed already sorted by `before`) at the
|
|
@@ -169,14 +174,14 @@ enum List[T: ToStr](ToStr) =
|
|
|
169
174
|
# `before(value, existing)` holds for, or the end if none does.
|
|
170
175
|
fun insertSorted(self, value: T, before: fn(T, T) -> Bool) -> Unit =
|
|
171
176
|
current := self.head
|
|
172
|
-
while current !=
|
|
177
|
+
while current != End
|
|
173
178
|
match current
|
|
174
|
-
|
|
179
|
+
Next(node) =>
|
|
175
180
|
if before(value, node.value)
|
|
176
181
|
self.insertBefore(node, value)
|
|
177
182
|
return
|
|
178
183
|
current = node.next
|
|
179
|
-
|
|
184
|
+
End =>
|
|
180
185
|
break
|
|
181
186
|
self.add(value)
|
|
182
187
|
|
|
@@ -184,42 +189,42 @@ enum List[T: ToStr](ToStr) =
|
|
|
184
189
|
# the "does a belong before b" comparator. Insertion sort (O(n^2)) — simple
|
|
185
190
|
# and adequate for the list sizes a linked list is already suited to.
|
|
186
191
|
fun sort(self, before: fn(T, T) -> Bool) -> List[T] =
|
|
187
|
-
result := List(head:
|
|
192
|
+
result := List(head: End, tail: End, size: 0)
|
|
188
193
|
current := self.head
|
|
189
|
-
while current !=
|
|
194
|
+
while current != End
|
|
190
195
|
match current
|
|
191
|
-
|
|
196
|
+
Next(Node(value, _, next)) =>
|
|
192
197
|
result.insertSorted(value, before)
|
|
193
198
|
current = next
|
|
194
|
-
|
|
199
|
+
End =>
|
|
195
200
|
break
|
|
196
201
|
return result
|
|
197
202
|
|
|
198
|
-
# Returns the first element equal to `search`, or `
|
|
203
|
+
# Returns the first element equal to `search`, or `Err(NotFound)`.
|
|
199
|
-
fun find(self, search: T) ->
|
|
204
|
+
fun find(self, search: T) -> Try[T, [NotFound]] =
|
|
200
205
|
current := self.head
|
|
201
|
-
while current !=
|
|
206
|
+
while current != End
|
|
202
207
|
match current
|
|
203
|
-
|
|
208
|
+
Next(Node(value, _, next)) =>
|
|
204
209
|
if value == search
|
|
205
|
-
return
|
|
210
|
+
return Ok(value)
|
|
206
211
|
current = next
|
|
207
|
-
|
|
212
|
+
End =>
|
|
208
213
|
break
|
|
209
|
-
return
|
|
214
|
+
return Err(NotFound)
|
|
210
215
|
|
|
211
216
|
fun contains(self, v: T) -> Bool =
|
|
212
|
-
return self.find(v)
|
|
217
|
+
return self.find(v).isOk()
|
|
213
218
|
|
|
214
219
|
# calls f for each elem in the list
|
|
215
220
|
fun each(self, cb: fn(T)) -> Unit =
|
|
216
221
|
current := self.head
|
|
217
|
-
while current !=
|
|
222
|
+
while current != End
|
|
218
223
|
match current
|
|
219
|
-
|
|
224
|
+
Next(Node(value, _, next)) =>
|
|
220
225
|
cb(value)
|
|
221
226
|
current = next
|
|
222
|
-
|
|
227
|
+
End =>
|
|
223
228
|
break
|
|
224
229
|
|
|
225
230
|
# Returns a new list with each element transformed by `cb`.
|
|
@@ -229,14 +234,14 @@ enum List[T: ToStr](ToStr) =
|
|
|
229
234
|
# when `List[T]` itself is specialized, via `resolveMethodOwnGenerics` in
|
|
230
235
|
# `plum-checker/src/monomorphize.rs`.
|
|
231
236
|
fun map(self, cb: fn(T) -> U) -> List[U] =
|
|
232
|
-
result := List(head:
|
|
237
|
+
result := List(head: End, tail: End, size: 0)
|
|
233
238
|
current := self.head
|
|
234
|
-
while current !=
|
|
239
|
+
while current != End
|
|
235
240
|
match current
|
|
236
|
-
|
|
241
|
+
Next(Node(value, _, next)) =>
|
|
237
242
|
result.add(cb(value))
|
|
238
243
|
current = next
|
|
239
|
-
|
|
244
|
+
End =>
|
|
240
245
|
break
|
|
241
246
|
return result
|
|
242
247
|
|
|
@@ -246,53 +251,53 @@ enum List[T: ToStr](ToStr) =
|
|
|
246
251
|
|
|
247
252
|
# Returns a new list with only the elements `predicate` holds for.
|
|
248
253
|
fun retain(self, predicate: fn(T) -> Bool) -> List[T] =
|
|
249
|
-
result := List(head:
|
|
254
|
+
result := List(head: End, tail: End, size: 0)
|
|
250
255
|
current := self.head
|
|
251
|
-
while current !=
|
|
256
|
+
while current != End
|
|
252
257
|
match current
|
|
253
|
-
|
|
258
|
+
Next(Node(value, _, next)) =>
|
|
254
259
|
if predicate(value)
|
|
255
260
|
result.add(value)
|
|
256
261
|
current = next
|
|
257
|
-
|
|
262
|
+
End =>
|
|
258
263
|
break
|
|
259
264
|
return result
|
|
260
265
|
|
|
261
266
|
# Returns a new list with the elements `predicate` holds for removed.
|
|
262
267
|
fun reject(self, predicate: fn(T) -> Bool) -> List[T] =
|
|
263
|
-
result := List(head:
|
|
268
|
+
result := List(head: End, tail: End, size: 0)
|
|
264
269
|
current := self.head
|
|
265
|
-
while current !=
|
|
270
|
+
while current != End
|
|
266
271
|
match current
|
|
267
|
-
|
|
272
|
+
Next(Node(value, _, next)) =>
|
|
268
273
|
if !predicate(value)
|
|
269
274
|
result.add(value)
|
|
270
275
|
current = next
|
|
271
|
-
|
|
276
|
+
End =>
|
|
272
277
|
break
|
|
273
278
|
return result
|
|
274
279
|
|
|
275
280
|
fun any(self, predicate: fn(T) -> Bool) -> Bool =
|
|
276
281
|
current := self.head
|
|
277
|
-
while current !=
|
|
282
|
+
while current != End
|
|
278
283
|
match current
|
|
279
|
-
|
|
284
|
+
Next(Node(value, _, next)) =>
|
|
280
285
|
if predicate(value)
|
|
281
286
|
return True
|
|
282
287
|
current = next
|
|
283
|
-
|
|
288
|
+
End =>
|
|
284
289
|
break
|
|
285
290
|
return False
|
|
286
291
|
|
|
287
292
|
fun every(self, predicate: fn(T) -> Bool) -> Bool =
|
|
288
293
|
current := self.head
|
|
289
|
-
while current !=
|
|
294
|
+
while current != End
|
|
290
295
|
match current
|
|
291
|
-
|
|
296
|
+
Next(Node(value, _, next)) =>
|
|
292
297
|
if !predicate(value)
|
|
293
298
|
return False
|
|
294
299
|
current = next
|
|
295
|
-
|
|
300
|
+
End =>
|
|
296
301
|
break
|
|
297
302
|
return True
|
|
298
303
|
|
|
@@ -304,43 +309,43 @@ enum List[T: ToStr](ToStr) =
|
|
|
304
309
|
fun reduce(self, acc: U, cb: fn(U, T) -> U) -> U =
|
|
305
310
|
result := acc
|
|
306
311
|
current := self.head
|
|
307
|
-
while current !=
|
|
312
|
+
while current != End
|
|
308
313
|
match current
|
|
309
|
-
|
|
314
|
+
Next(Node(value, _, next)) =>
|
|
310
315
|
result = cb(result, value)
|
|
311
316
|
current = next
|
|
312
|
-
|
|
317
|
+
End =>
|
|
313
318
|
break
|
|
314
319
|
return result
|
|
315
320
|
|
|
316
321
|
# returns the first element in the list
|
|
317
|
-
fun first(self) ->
|
|
322
|
+
fun first(self) -> Try[T, [ListWasEmpty]] =
|
|
318
323
|
match self.head
|
|
319
|
-
|
|
324
|
+
Next(Node(value, _, _)) => Ok(value)
|
|
320
|
-
|
|
325
|
+
End => Err(ListWasEmpty)
|
|
321
326
|
|
|
322
327
|
# returns the last element in the list
|
|
323
|
-
fun last(self) ->
|
|
328
|
+
fun last(self) -> Try[T, [ListWasEmpty]] =
|
|
324
329
|
match self.tail
|
|
325
|
-
|
|
330
|
+
Next(Node(value, _, _)) => Ok(value)
|
|
326
|
-
|
|
331
|
+
End => Err(ListWasEmpty)
|
|
327
332
|
|
|
328
333
|
# Returns a new list of the elements at index `[start, end)`, clamped into
|
|
329
334
|
# range (a `start`/`end` outside `[0, length]` is clamped, not an error).
|
|
330
335
|
fun sublist(self, start: Int, end: Int) -> List[T] =
|
|
331
|
-
result := List(head:
|
|
336
|
+
result := List(head: End, tail: End, size: 0)
|
|
332
337
|
s := start < 0 ? 0 : start
|
|
333
338
|
e := end > self.size ? self.size : end
|
|
334
339
|
current := self.head
|
|
335
340
|
i := 0
|
|
336
|
-
while current !=
|
|
341
|
+
while current != End && i < e
|
|
337
342
|
match current
|
|
338
|
-
|
|
343
|
+
Next(Node(value, _, next)) =>
|
|
339
344
|
if i >= s
|
|
340
345
|
result.add(value)
|
|
341
346
|
current = next
|
|
342
347
|
i = i + 1
|
|
343
|
-
|
|
348
|
+
End =>
|
|
344
349
|
break
|
|
345
350
|
return result
|
|
346
351
|
|
|
@@ -355,11 +360,11 @@ enum List[T: ToStr](ToStr) =
|
|
|
355
360
|
fun drop(self, n: Int) -> List[T] =
|
|
356
361
|
return self.skip(n)
|
|
357
362
|
|
|
358
|
-
# Returns one uniformly random element, or `
|
|
363
|
+
# Returns one uniformly random element, or `Err(ListWasEmpty)` if `self` is empty.
|
|
359
|
-
fun sample(self) ->
|
|
364
|
+
fun sample(self) -> Try[T, [ListWasEmpty]] =
|
|
360
365
|
if self.size == 0
|
|
361
|
-
return
|
|
366
|
+
return Err(ListWasEmpty)
|
|
362
|
-
return self.get(randomInt(self.size))
|
|
367
|
+
return Ok(self.get(randomInt(self.size)).unwrap())
|
|
363
368
|
|
|
364
369
|
# Returns a new list with `self`'s elements in random order (Fisher-Yates).
|
|
365
370
|
fun shuffle(self) -> List[T] =
|
|
@@ -381,7 +386,7 @@ enum List[T: ToStr](ToStr) =
|
|
|
381
386
|
# doc comments for the full fix) — that danger is now FIXED (2026-09-02).
|
|
382
387
|
#
|
|
383
388
|
# A second, more contained bug surfaced once past that: two SEPARATE bare
|
|
384
|
-
# `List(head:
|
|
389
|
+
# `List(head: End, tail: End, size: 0)` constructions in the SAME method
|
|
385
390
|
# body — one for the OUTER `result: List[List[T]]` (matching this
|
|
386
391
|
# method's own declared return type) and one for an INNER per-chunk
|
|
387
392
|
# `piece: List[T]` — are ambiguous to `resolveClassInstantiation`'s
|
|
@@ -397,7 +402,7 @@ enum List[T: ToStr](ToStr) =
|
|
|
397
402
|
# unambiguously resolved through the RECEIVER's own (already-correct)
|
|
398
403
|
# binding, no `current_return_type` guessing involved at all.
|
|
399
404
|
fun chunk(self, size: Int) -> List[List[T]] =
|
|
400
|
-
result := List(head:
|
|
405
|
+
result := List(head: End, tail: End, size: 0)
|
|
401
406
|
if size <= 0
|
|
402
407
|
return result
|
|
403
408
|
i := 0
|
|
@@ -411,7 +416,7 @@ enum List[T: ToStr](ToStr) =
|
|
|
411
416
|
# in that order. Returns them as a 2-element `List[List[T]]` (`[matched,
|
|
412
417
|
# unmatched]`) since this language has no tuple type. Deliberately built
|
|
413
418
|
# from `self.retain(predicate)`/`self.reject(predicate)` rather than two
|
|
414
|
-
# local `matched`/`unmatched := List(head:
|
|
419
|
+
# local `matched`/`unmatched := List(head: End, ...)` bare constructions
|
|
415
420
|
# — see `chunk`'s doc comment above for exactly why that would be
|
|
416
421
|
# ambiguous (`matched`/`unmatched` need `List[T]`, but a bare
|
|
417
422
|
# construction here would resolve via `current_return_type` fallback
|
|
@@ -420,7 +425,7 @@ enum List[T: ToStr](ToStr) =
|
|
|
420
425
|
# bare construction per method body, matching their own single-level
|
|
421
426
|
# return type).
|
|
422
427
|
fun partition(self, predicate: fn(T) -> Bool) -> List[List[T]] =
|
|
423
|
-
result := List(head:
|
|
428
|
+
result := List(head: End, tail: End, size: 0)
|
|
424
429
|
result.add(self.retain(predicate))
|
|
425
430
|
result.add(self.reject(predicate))
|
|
426
431
|
return result
|
|
@@ -443,14 +448,14 @@ enum List[T: ToStr](ToStr) =
|
|
|
443
448
|
fun join(self, sep: Str = ",") -> Str =
|
|
444
449
|
res := Buffer()
|
|
445
450
|
current := self.head
|
|
446
|
-
while current !=
|
|
451
|
+
while current != End
|
|
447
452
|
match current
|
|
448
|
-
|
|
453
|
+
Next(Node(value, _, next)) =>
|
|
449
454
|
res.write(value.toStr())
|
|
450
|
-
if next !=
|
|
455
|
+
if next != End
|
|
451
456
|
res.write(sep)
|
|
452
457
|
current = next
|
|
453
|
-
|
|
458
|
+
End =>
|
|
454
459
|
break
|
|
455
460
|
res.toStr()
|
|
456
461
|
|
|
@@ -471,10 +476,10 @@ enum List[T: ToStr](ToStr) =
|
|
|
471
476
|
|
|
472
477
|
# ---- regression tests ----
|
|
473
478
|
|
|
474
|
-
fun optSumForListTest(o:
|
|
479
|
+
fun optSumForListTest(o: Try[Int, [OutOfBounds]]) -> Int =
|
|
475
480
|
match o
|
|
476
|
-
|
|
481
|
+
Ok(v) => v
|
|
477
|
-
|
|
482
|
+
Err(_) => -1000
|
|
478
483
|
|
|
479
484
|
fun exerciseList() -> Int =
|
|
480
485
|
l = List(1, 2, 3, 4, 5)
|
plum-std/Map.plum
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/List
|
|
3
|
-
import std/
|
|
3
|
+
import std/Try
|
|
4
4
|
import std/Array
|
|
5
5
|
import std/Bool
|
|
6
6
|
import std/Number
|
|
@@ -22,8 +22,7 @@ trait Hashable =
|
|
|
22
22
|
|
|
23
23
|
# A Pair is a grouping of a key with a value.
|
|
24
24
|
#
|
|
25
|
-
# `toStr` (unlike `Option`'s — see `libs/std/option.plum`'s note on
|
|
26
|
-
# `
|
|
25
|
+
# `toStr` (unlike `Try`'s — see `Try.plum`'s note on `toTryStr`) IS safe as an ordinary method here: `Pair[K, V]` is only
|
|
27
26
|
# ever used as `List[Pair[K, V]]`'s element type inside `Map[K, V]`, and
|
|
28
27
|
# `List[T: ToStr]` already requires its own `T` to support `toStr` —
|
|
29
28
|
# so a `Pair` used as a `List` element always has `K`/`V: ToStr` too,
|
|
@@ -70,21 +69,21 @@ enum Map[K: Hashable, V] =
|
|
|
70
69
|
m := k.hash() % BUCKET_COUNT
|
|
71
70
|
return m < 0 ? m + BUCKET_COUNT : m
|
|
72
71
|
|
|
73
|
-
# Gets the value for `k`, or `
|
|
72
|
+
# Gets the value for `k`, or `Err(KeyNotFound)` if absent.
|
|
74
|
-
fun get(self, k: K) ->
|
|
73
|
+
fun get(self, k: K) -> Try[V, [KeyNotFound]] =
|
|
75
74
|
current := self.buckets.get(self.bucketIndex(k)).head
|
|
76
|
-
while current !=
|
|
75
|
+
while current != End
|
|
77
76
|
match current
|
|
78
|
-
|
|
77
|
+
Next(Node(Pair(key, val), _, next)) =>
|
|
79
78
|
if key == k
|
|
80
|
-
return
|
|
79
|
+
return Ok(val)
|
|
81
80
|
current = next
|
|
82
|
-
|
|
81
|
+
End =>
|
|
83
82
|
break
|
|
84
|
-
return
|
|
83
|
+
return Err(KeyNotFound)
|
|
85
84
|
|
|
86
85
|
fun has(self, k: K) -> Bool =
|
|
87
|
-
return self.get(k)
|
|
86
|
+
return self.get(k).isOk()
|
|
88
87
|
|
|
89
88
|
# Sets `k`'s value to `v` — replacing an existing entry for `k` in place
|
|
90
89
|
# (mutating the stored `Pair`'s `val` field) rather than appending a
|
|
@@ -93,14 +92,14 @@ enum Map[K: Hashable, V] =
|
|
|
93
92
|
fun set(self, k: K, v: V) -> Unit =
|
|
94
93
|
bucket := self.buckets.get(self.bucketIndex(k))
|
|
95
94
|
current := bucket.head
|
|
96
|
-
while current !=
|
|
95
|
+
while current != End
|
|
97
96
|
match current
|
|
98
|
-
|
|
97
|
+
Next(Node(pair, _, next)) =>
|
|
99
98
|
if pair.key == k
|
|
100
99
|
pair.val = v
|
|
101
100
|
return
|
|
102
101
|
current = next
|
|
103
|
-
|
|
102
|
+
End =>
|
|
104
103
|
break
|
|
105
104
|
bucket.add(Pair(key: k, val: v))
|
|
106
105
|
self.size = self.size + 1
|
|
@@ -114,15 +113,15 @@ enum Map[K: Hashable, V] =
|
|
|
114
113
|
fun remove(self, k: K) -> Unit =
|
|
115
114
|
bucket := self.buckets.get(self.bucketIndex(k))
|
|
116
115
|
current := bucket.head
|
|
117
|
-
while current !=
|
|
116
|
+
while current != End
|
|
118
117
|
match current
|
|
119
|
-
|
|
118
|
+
Next(node) =>
|
|
120
119
|
if node.value.key == k
|
|
121
120
|
bucket.unlink(node)
|
|
122
121
|
self.size = self.size - 1
|
|
123
122
|
return
|
|
124
123
|
current = node.next
|
|
125
|
-
|
|
124
|
+
End =>
|
|
126
125
|
break
|
|
127
126
|
|
|
128
127
|
fun length(self) -> Int =
|
|
@@ -145,22 +144,22 @@ enum Map[K: Hashable, V] =
|
|
|
145
144
|
i := 0
|
|
146
145
|
while i < self.buckets.length()
|
|
147
146
|
current := self.buckets.get(i).head
|
|
148
|
-
while current !=
|
|
147
|
+
while current != End
|
|
149
148
|
match current
|
|
150
|
-
|
|
149
|
+
Next(Node(Pair(key, val), _, next)) =>
|
|
151
150
|
cb(key, val)
|
|
152
151
|
current = next
|
|
153
|
-
|
|
152
|
+
End =>
|
|
154
153
|
break
|
|
155
154
|
i = i + 1
|
|
156
155
|
|
|
157
156
|
fun keys(self) -> List[K] =
|
|
158
|
-
result := List(head:
|
|
157
|
+
result := List(head: End, tail: End, size: 0)
|
|
159
158
|
self.each(|k, v| result.add(k))
|
|
160
159
|
return result
|
|
161
160
|
|
|
162
161
|
fun values(self) -> List[V] =
|
|
163
|
-
result := List(head:
|
|
162
|
+
result := List(head: End, tail: End, size: 0)
|
|
164
163
|
self.each(|k, v| result.add(v))
|
|
165
164
|
return result
|
|
166
165
|
|
|
@@ -174,13 +173,13 @@ enum Map[K: Hashable, V] =
|
|
|
174
173
|
j := 0
|
|
175
174
|
while j < self.buckets.length()
|
|
176
175
|
current := self.buckets.get(j).head
|
|
177
|
-
while current !=
|
|
176
|
+
while current != End
|
|
178
177
|
match current
|
|
179
|
-
|
|
178
|
+
Next(Node(Pair(key, val), _, next)) =>
|
|
180
179
|
pair := cb(key, val)
|
|
181
180
|
result.set(pair.key, pair.val)
|
|
182
181
|
current = next
|
|
183
|
-
|
|
182
|
+
End =>
|
|
184
183
|
break
|
|
185
184
|
j = j + 1
|
|
186
185
|
return result
|
plum-std/Number.plum
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/Str
|
|
3
3
|
import std/Bool
|
|
4
|
-
import std/
|
|
4
|
+
import std/Try
|
|
5
5
|
|
|
6
6
|
MIN_VALUE = -0x8000_0000_0000_0000 # Lowest value of Int
|
|
7
7
|
MAX_VALUE = 0x7FFF_FFFF_FFFF_FFFF # Highest value of Int
|
|
@@ -303,41 +303,41 @@ fun powFloat(base: Float, y: Float) -> Float =
|
|
|
303
303
|
return exp(y * ln(base))
|
|
304
304
|
|
|
305
305
|
# Parses a decimal integer, with an optional leading `+`/`-`.
|
|
306
|
-
fun parseInt(s: Str) ->
|
|
306
|
+
fun parseInt(s: Str) -> Try[Int, [BadNumStr]] =
|
|
307
307
|
len := s.length()
|
|
308
308
|
if len == 0
|
|
309
|
-
return Err(
|
|
309
|
+
return Err(BadNumStr)
|
|
310
310
|
neg := s.byteAt(0) == 45
|
|
311
311
|
start := neg || s.byteAt(0) == 43 ? 1 : 0
|
|
312
312
|
if start >= len
|
|
313
|
-
return Err(
|
|
313
|
+
return Err(BadNumStr)
|
|
314
314
|
value := 0
|
|
315
315
|
i := start
|
|
316
316
|
while i < len
|
|
317
317
|
b := s.byteAt(i)
|
|
318
318
|
if b < 48 || b > 57
|
|
319
|
-
return Err(
|
|
319
|
+
return Err(BadNumStr)
|
|
320
320
|
value = value * 10 + b - 48
|
|
321
321
|
i = i + 1
|
|
322
322
|
return Ok(neg ? -value : value)
|
|
323
323
|
|
|
324
324
|
# Parses a decimal float, with an optional leading `+`/`-` and an optional
|
|
325
325
|
# `.` fractional part (no exponent notation).
|
|
326
|
-
fun parseFloat(s: Str) ->
|
|
326
|
+
fun parseFloat(s: Str) -> Try[Float, [BadNumStr]] =
|
|
327
327
|
len := s.length()
|
|
328
328
|
if len == 0
|
|
329
|
-
return Err(
|
|
329
|
+
return Err(BadNumStr)
|
|
330
330
|
neg := s.byteAt(0) == 45
|
|
331
331
|
start := neg || s.byteAt(0) == 43 ? 1 : 0
|
|
332
332
|
if start >= len
|
|
333
|
-
return Err(
|
|
333
|
+
return Err(BadNumStr)
|
|
334
334
|
int_part := 0.0f
|
|
335
335
|
saw_digit := False
|
|
336
336
|
i := start
|
|
337
337
|
while i < len && s.byteAt(i) != 46
|
|
338
338
|
b := s.byteAt(i)
|
|
339
339
|
if b < 48 || b > 57
|
|
340
|
-
return Err(
|
|
340
|
+
return Err(BadNumStr)
|
|
341
341
|
int_part = int_part * 10.0f + Float(b - 48)
|
|
342
342
|
saw_digit = True
|
|
343
343
|
i = i + 1
|
|
@@ -348,13 +348,13 @@ fun parseFloat(s: Str) -> Result[Float, Str] =
|
|
|
348
348
|
while i < len
|
|
349
349
|
b = s.byteAt(i)
|
|
350
350
|
if b < 48 || b > 57
|
|
351
|
-
return Err(
|
|
351
|
+
return Err(BadNumStr)
|
|
352
352
|
frac_scale = frac_scale / 10.0f
|
|
353
353
|
frac_part = frac_part + Float(b - 48) * frac_scale
|
|
354
354
|
saw_digit = True
|
|
355
355
|
i = i + 1
|
|
356
356
|
if !saw_digit
|
|
357
|
-
return Err(
|
|
357
|
+
return Err(BadNumStr)
|
|
358
358
|
value := int_part + frac_part
|
|
359
359
|
return Ok(neg ? -value : value)
|
|
360
360
|
|
plum-std/Option.plum
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
module std
|
|
2
|
-
|
|
3
|
-
import std/Result
|
|
4
|
-
import std/Str
|
|
5
|
-
import std/Bool
|
|
6
|
-
import std/Number
|
|
7
|
-
|
|
8
|
-
# Option[T] represents a value that may or may not be present — Plum's
|
|
9
|
-
# counterpart to Rust's `Option`/Go's "zero value or ok bool" idiom.
|
|
10
|
-
enum Option[T] =
|
|
11
|
-
| Some(T)
|
|
12
|
-
| None
|
|
13
|
-
|
|
14
|
-
fun isSome(self) -> Bool =
|
|
15
|
-
match self
|
|
16
|
-
Some(_) => True
|
|
17
|
-
None => False
|
|
18
|
-
|
|
19
|
-
fun isNone(self) -> Bool =
|
|
20
|
-
match self
|
|
21
|
-
Some(_) => False
|
|
22
|
-
None => True
|
|
23
|
-
|
|
24
|
-
# Returns the wrapped value, or traps if `self` is `None`.
|
|
25
|
-
fun unwrap(self) -> T =
|
|
26
|
-
match self
|
|
27
|
-
Some(v) =>
|
|
28
|
-
return v
|
|
29
|
-
None =>
|
|
30
|
-
todo
|
|
31
|
-
|
|
32
|
-
# Returns the wrapped value, or traps if `self` is `None`. `msg` documents
|
|
33
|
-
# the expectation at the call site (matching Rust's `Option::expect`) but
|
|
34
|
-
# isn't surfaced anywhere at runtime — there's no host-independent "print
|
|
35
|
-
# this and trap" primitive to build that on.
|
|
36
|
-
fun expect(self, msg: Str) -> T =
|
|
37
|
-
match self
|
|
38
|
-
Some(v) =>
|
|
39
|
-
return v
|
|
40
|
-
None =>
|
|
41
|
-
todo
|
|
42
|
-
|
|
43
|
-
fun unwrapOr(self, default: T) -> T =
|
|
44
|
-
match self
|
|
45
|
-
Some(v) =>
|
|
46
|
-
return v
|
|
47
|
-
None =>
|
|
48
|
-
return default
|
|
49
|
-
|
|
50
|
-
# Like `unwrapOr`, but the fallback is computed lazily (only when `self` is
|
|
51
|
-
# `None`) via `cb` — useful when producing the default is expensive.
|
|
52
|
-
fun unwrapOrElse(self, cb: fn() -> T) -> T =
|
|
53
|
-
match self
|
|
54
|
-
Some(v) =>
|
|
55
|
-
return v
|
|
56
|
-
None =>
|
|
57
|
-
return cb()
|
|
58
|
-
|
|
59
|
-
# Keeps `self` only if it's `Some` AND `predicate` holds for its value;
|
|
60
|
-
# otherwise returns `None`.
|
|
61
|
-
fun filter(self, predicate: fn(T) -> Bool) -> Option[T] =
|
|
62
|
-
match self
|
|
63
|
-
Some(v) =>
|
|
64
|
-
if predicate(v)
|
|
65
|
-
return Some(v)
|
|
66
|
-
return None
|
|
67
|
-
None =>
|
|
68
|
-
return None
|
|
69
|
-
|
|
70
|
-
# Transforms the wrapped value with `cb`, leaving `None` as `None`. `U` is
|
|
71
|
-
# `map`'s OWN generic param (the transformed value's type), separate from
|
|
72
|
-
# `Option[T]`'s own `T` — resolved per CALL SITE, not when `Option[T]`
|
|
73
|
-
# itself is specialized (see `resolveMethodOwnGenerics`).
|
|
74
|
-
fun map(self, cb: fn(T) -> U) -> Option[U] =
|
|
75
|
-
match self
|
|
76
|
-
Some(v) =>
|
|
77
|
-
return Some(cb(v))
|
|
78
|
-
None =>
|
|
79
|
-
return None
|
|
80
|
-
|
|
81
|
-
# Like `map`, but `cb` itself returns an `Option[U]` (rather than a bare
|
|
82
|
-
# `U`) and the result isn't re-wrapped — useful for chaining together
|
|
83
|
-
# several fallible steps without nesting (`Option[Option[U]]`). `U` here is
|
|
84
|
-
# `andThen`'s OWN generic param, nested inside `cb`'s declared return type
|
|
85
|
-
# `Option[U]` rather than being `cb`'s bare return type — resolved from the
|
|
86
|
-
# closure's actual inferred return type via `bindGenericArg` in
|
|
87
|
-
# `plum-checker/src/monomorphize.rs`'s `resolveMethodOwnGenerics`.
|
|
88
|
-
fun andThen(self, cb: fn(T) -> Option[U]) -> Option[U] =
|
|
89
|
-
match self
|
|
90
|
-
Some(v) =>
|
|
91
|
-
return cb(v)
|
|
92
|
-
None =>
|
|
93
|
-
return None
|
|
94
|
-
|
|
95
|
-
# Converts to a `Result`, using `err` as the failure value if `self` is
|
|
96
|
-
# `None`. `E` is `okOr`'s OWN generic param, inferred directly from `err`'s
|
|
97
|
-
# own value (same mechanism as `List.reduce`'s accumulator param).
|
|
98
|
-
fun okOr(self, err: E) -> Result[T, E] =
|
|
99
|
-
match self
|
|
100
|
-
Some(v) =>
|
|
101
|
-
return Ok(v)
|
|
102
|
-
None =>
|
|
103
|
-
return Err(err)
|
|
104
|
-
|
|
105
|
-
# No `toStr(self) -> Str` here — every method on a generic type gets
|
|
106
|
-
# compiled for EVERY concrete specialization that type is ever used at
|
|
107
|
-
# ANYWHERE in the whole program, whether or not that particular method is
|
|
108
|
-
# actually called for that specialization (there's no lazy/on-demand
|
|
109
|
-
# method compilation). `libs/std/list.plum`'s `List[T]` internally uses
|
|
110
|
-
# `Option[Node[T]]` for its own `head`/`tail` fields — and `Node` has no
|
|
111
|
-
# `toStr` — so an `Option.toStr` calling `.toStr()` on the wrapped value
|
|
112
|
-
# would fail to compile for THAT specialization even though nothing ever
|
|
113
|
-
# actually calls `.toStr()` on an `Option[Node[Int]]`. Use `toOptionStr`
|
|
114
|
-
# below (an explicit stringifier callback, not an implicit `T: ToStr`
|
|
115
|
-
# bound) wherever printing an `Option` is needed.
|
|
116
|
-
fun toOptionStr(self, valueToStr: fn(T) -> Str) -> Str =
|
|
117
|
-
match self
|
|
118
|
-
Some(v) =>
|
|
119
|
-
return "Some({valueToStr(v)})"
|
|
120
|
-
None =>
|
|
121
|
-
return "None"
|
|
122
|
-
|
|
123
|
-
fun makeNoneIntForOptionTest() -> Option[Int] =
|
|
124
|
-
return None
|
|
125
|
-
|
|
126
|
-
test "filter keeps a Some value only when the predicate holds"
|
|
127
|
-
assert Some(4).filter(|v| v > 2).isSome()
|
|
128
|
-
assert Some(1).filter(|v| v > 2).isNone()
|
|
129
|
-
assert makeNoneIntForOptionTest().filter(|v| v > 2).isNone()
|
|
130
|
-
|
|
131
|
-
test "andThen chains fallible steps without double-wrapping"
|
|
132
|
-
half := |n| n % 2 == 0
|
|
133
|
-
a := Some(8).andThen(|n| Some(n / 2)).andThen(|n| Some(n / 2))
|
|
134
|
-
assert a.isSome()
|
|
135
|
-
assert a.unwrap() == 2
|
|
136
|
-
assert half(a.unwrap())
|
|
137
|
-
b := makeNoneIntForOptionTest().andThen(|n| Some(n / 2))
|
|
138
|
-
assert b.isNone()
|
|
139
|
-
|
|
140
|
-
test "okOr converts to a Result using the given error on None"
|
|
141
|
-
assert Some(5).okOr("missing").isOk()
|
|
142
|
-
assert Some(5).okOr("missing").unwrap() == 5
|
|
143
|
-
assert makeNoneIntForOptionTest().okOr("missing").isErr()
|
|
144
|
-
assert makeNoneIntForOptionTest().okOr("missing").unwrapErr() == "missing"
|
plum-std/Os.plum
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/List
|
|
3
|
-
import std/
|
|
3
|
+
import std/Try
|
|
4
4
|
import std/Str
|
|
5
5
|
import std/Uuid
|
|
6
6
|
import std/Bool
|
|
@@ -58,13 +58,13 @@ extern fun rawReadDir(path: Str) -> Str
|
|
|
58
58
|
|
|
59
59
|
# Reads the whole file at `path` as a `Str` (`Str` is a byte array here — see
|
|
60
60
|
# `libs/std/str.plum` — so this works for binary content too, not just text).
|
|
61
|
-
fun readFile(path: Str) ->
|
|
61
|
+
fun readFile(path: Str) -> Try[Str, Str] =
|
|
62
62
|
if !exists(path)
|
|
63
63
|
return Err("no such file: '{path}'")
|
|
64
64
|
return Ok(rawReadFile(path))
|
|
65
65
|
|
|
66
66
|
# Writes `data` to `path`, replacing its previous contents (or creating it).
|
|
67
|
-
fun writeFile(path: Str, data: Str) ->
|
|
67
|
+
fun writeFile(path: Str, data: Str) -> Try[Bool, Str] =
|
|
68
68
|
if rawWriteFile(path, data) == 1
|
|
69
69
|
return Ok(True)
|
|
70
70
|
return Err("failed to write file: '{path}'")
|
|
@@ -72,13 +72,13 @@ fun writeFile(path: Str, data: Str) -> Result[Bool, Str] =
|
|
|
72
72
|
fun exists(path: Str) -> Bool =
|
|
73
73
|
return rawExists(path) == 1
|
|
74
74
|
|
|
75
|
-
fun mkdir(path: Str) ->
|
|
75
|
+
fun mkdir(path: Str) -> Try[Bool, Str] =
|
|
76
76
|
if rawMkdir(path) == 1
|
|
77
77
|
return Ok(True)
|
|
78
78
|
return Err("failed to create directory: '{path}'")
|
|
79
79
|
|
|
80
80
|
# Removes the file (not directory) at `path`.
|
|
81
|
-
fun remove(path: Str) ->
|
|
81
|
+
fun remove(path: Str) -> Try[Bool, Str] =
|
|
82
82
|
if !exists(path)
|
|
83
83
|
return Err("no such file: '{path}'")
|
|
84
84
|
if rawRemove(path) == 1
|
|
@@ -95,7 +95,7 @@ fun access(path: Str) -> Bool =
|
|
|
95
95
|
# contents) if it doesn't already exist. There's no host-level "open for
|
|
96
96
|
# append" primitive, so this reads the existing bytes and writes the
|
|
97
97
|
# concatenation back — not atomic, and O(existing file size) per call.
|
|
98
|
-
fun appendFile(path: Str, data: Str) ->
|
|
98
|
+
fun appendFile(path: Str, data: Str) -> Try[Bool, Str] =
|
|
99
99
|
if exists(path)
|
|
100
100
|
match readFile(path)
|
|
101
101
|
Ok(existing) =>
|
|
@@ -106,7 +106,7 @@ fun appendFile(path: Str, data: Str) -> Result[Bool, Str] =
|
|
|
106
106
|
|
|
107
107
|
# Unix permission bits only (e.g. `0o644`) — this VM has no Windows ACL
|
|
108
108
|
# equivalent to fall back to.
|
|
109
|
-
fun chmod(path: Str, mode: Int) ->
|
|
109
|
+
fun chmod(path: Str, mode: Int) -> Try[Bool, Str] =
|
|
110
110
|
if rawChmod(path, mode) == 1
|
|
111
111
|
return Ok(True)
|
|
112
112
|
return Err("failed to chmod: '{path}'")
|
|
@@ -116,18 +116,18 @@ fun chmod(path: Str, mode: Int) -> Result[Bool, Str] =
|
|
|
116
116
|
fun chown(path: Str, uid: Int, gid: Int) =
|
|
117
117
|
todo
|
|
118
118
|
|
|
119
|
-
fun copyFile(src: Str, dest: Str) ->
|
|
119
|
+
fun copyFile(src: Str, dest: Str) -> Try[Bool, Str] =
|
|
120
120
|
match readFile(src)
|
|
121
121
|
Ok(data) => writeFile(dest, data)
|
|
122
122
|
Err(e) => Err(e)
|
|
123
123
|
|
|
124
|
-
fun cp(src: Str, dest: Str) ->
|
|
124
|
+
fun cp(src: Str, dest: Str) -> Try[Bool, Str] =
|
|
125
125
|
return copyFile(src, dest)
|
|
126
126
|
|
|
127
127
|
# Symlinks aren't modeled separately from regular files/dirs by this VM's
|
|
128
128
|
# host primitives (`rawChmod` always follows them), so this is the same as
|
|
129
129
|
# `chmod`.
|
|
130
|
-
fun lchmod(path: Str, mode: Int) ->
|
|
130
|
+
fun lchmod(path: Str, mode: Int) -> Try[Bool, Str] =
|
|
131
131
|
return chmod(path, mode)
|
|
132
132
|
|
|
133
133
|
# See `chown` — no uid/gid syscall exposed by this VM.
|
|
@@ -139,7 +139,7 @@ fun lchown(path: Str, uid: Int, gid: Int) =
|
|
|
139
139
|
fun lutimes(path: Str, atime: Int, mtime: Int) =
|
|
140
140
|
todo
|
|
141
141
|
|
|
142
|
-
fun link(existing_path: Str, new_path: Str) ->
|
|
142
|
+
fun link(existing_path: Str, new_path: Str) -> Try[Bool, Str] =
|
|
143
143
|
if rawLink(existing_path, new_path) == 1
|
|
144
144
|
return Ok(True)
|
|
145
145
|
return Err("failed to link '{existing_path}' -> '{new_path}'")
|
|
@@ -152,7 +152,7 @@ fun lstat(path: Str) =
|
|
|
152
152
|
# Creates a fresh directory named `prefix` followed by a random suffix,
|
|
153
153
|
# retrying on the (astronomically unlikely) chance of a collision, and
|
|
154
154
|
# returns the path actually created.
|
|
155
|
-
fun mkdtemp(prefix: Str) ->
|
|
155
|
+
fun mkdtemp(prefix: Str) -> Try[Str, Str] =
|
|
156
156
|
path := prefix + v4().toStr()
|
|
157
157
|
if exists(path)
|
|
158
158
|
return mkdtemp(prefix)
|
|
@@ -173,7 +173,7 @@ fun opendir(path: Str) =
|
|
|
173
173
|
todo
|
|
174
174
|
|
|
175
175
|
# The names of `path`'s directory entries (not full paths).
|
|
176
|
-
fun readdir(path: Str) ->
|
|
176
|
+
fun readdir(path: Str) -> Try[List[Str], Str] =
|
|
177
177
|
if !exists(path)
|
|
178
178
|
return Err("no such directory: '{path}'")
|
|
179
179
|
# `reject`ing empty names handles both a genuinely empty directory (whose
|
|
@@ -182,17 +182,17 @@ fun readdir(path: Str) -> Result[List[Str], Str] =
|
|
|
182
182
|
# check above) the same way — no directory entry is ever named `""`.
|
|
183
183
|
return Ok(rawReadDir(path).split("\n", 0).reject(|n| n == ""))
|
|
184
184
|
|
|
185
|
-
fun readlink(path: Str) ->
|
|
185
|
+
fun readlink(path: Str) -> Try[Str, Str] =
|
|
186
186
|
if !exists(path)
|
|
187
187
|
return Err("no such file: '{path}'")
|
|
188
188
|
return Ok(rawReadlink(path))
|
|
189
189
|
|
|
190
|
-
fun realpath(path: Str) ->
|
|
190
|
+
fun realpath(path: Str) -> Try[Str, Str] =
|
|
191
191
|
if !exists(path)
|
|
192
192
|
return Err("no such file: '{path}'")
|
|
193
193
|
return Ok(rawRealpath(path))
|
|
194
194
|
|
|
195
|
-
fun rename(old_path: Str, new_path: Str) ->
|
|
195
|
+
fun rename(old_path: Str, new_path: Str) -> Try[Bool, Str] =
|
|
196
196
|
if !exists(old_path)
|
|
197
197
|
return Err("no such file: '{old_path}'")
|
|
198
198
|
if rawRename(old_path, new_path) == 1
|
|
@@ -200,7 +200,7 @@ fun rename(old_path: Str, new_path: Str) -> Result[Bool, Str] =
|
|
|
200
200
|
return Err("failed to rename '{old_path}' -> '{new_path}'")
|
|
201
201
|
|
|
202
202
|
# Removes an empty directory. (`remove` above covers plain files.)
|
|
203
|
-
fun rmdir(path: Str) ->
|
|
203
|
+
fun rmdir(path: Str) -> Try[Bool, Str] =
|
|
204
204
|
if !exists(path)
|
|
205
205
|
return Err("no such directory: '{path}'")
|
|
206
206
|
if rawRemoveDir(path) == 1
|
|
@@ -215,14 +215,14 @@ fun stat(path: Str) =
|
|
|
215
215
|
fun statfs(path: Str) =
|
|
216
216
|
todo
|
|
217
217
|
|
|
218
|
-
fun symlink(target: Str, path: Str) ->
|
|
218
|
+
fun symlink(target: Str, path: Str) -> Try[Bool, Str] =
|
|
219
219
|
if rawSymlink(target, path) == 1
|
|
220
220
|
return Ok(True)
|
|
221
221
|
return Err("failed to symlink '{path}' -> '{target}'")
|
|
222
222
|
|
|
223
223
|
# Resizes the file at `path` to exactly `len` bytes — truncated if shorter,
|
|
224
224
|
# zero-padded if longer.
|
|
225
|
-
fun truncate(path: Str, len: Int) ->
|
|
225
|
+
fun truncate(path: Str, len: Int) -> Try[Bool, Str] =
|
|
226
226
|
if !exists(path)
|
|
227
227
|
return Err("no such file: '{path}'")
|
|
228
228
|
if rawTruncate(path, len) == 1
|
plum-std/Result.plum
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
module std
|
|
2
|
-
import std/Option
|
|
3
|
-
import std/Str
|
|
4
|
-
import std/Bool
|
|
5
|
-
import std/Number
|
|
6
|
-
|
|
7
|
-
# Result[T, E] represents either success (`Ok`, carrying a `T`) or failure
|
|
8
|
-
# (`Err`, carrying an `E`) — used throughout std for fallible operations
|
|
9
|
-
# (`Bool.parse`, `Int.fromStr`, `Float.fromStr`).
|
|
10
|
-
#
|
|
11
|
-
# `andThen` (which would need `cb`'s return type's `T` to be found NESTED
|
|
12
|
-
# inside another `Result[U, E]`, not `cb`'s own bare return type) is
|
|
13
|
-
# intentionally not implemented here — same method-level-generics gap
|
|
14
|
-
# documented on `Option.andThen` in `libs/std/option.plum`. `map`/`mapErr`
|
|
15
|
-
# don't have that problem (`cb`'s return type IS directly the new generic
|
|
16
|
-
# param, same shape as `List.map`), so those ARE implemented below.
|
|
17
|
-
enum Result[T, E] =
|
|
18
|
-
| Ok(T)
|
|
19
|
-
| Err(E)
|
|
20
|
-
|
|
21
|
-
fun isOk(self) -> Bool =
|
|
22
|
-
match self
|
|
23
|
-
Ok(_) => True
|
|
24
|
-
Err(_) => False
|
|
25
|
-
|
|
26
|
-
fun isErr(self) -> Bool =
|
|
27
|
-
match self
|
|
28
|
-
Ok(_) => False
|
|
29
|
-
Err(_) => True
|
|
30
|
-
|
|
31
|
-
# Returns the wrapped success value, or traps if `self` is `Err`.
|
|
32
|
-
fun unwrap(self) -> T =
|
|
33
|
-
match self
|
|
34
|
-
Ok(v) =>
|
|
35
|
-
return v
|
|
36
|
-
Err(_) =>
|
|
37
|
-
todo
|
|
38
|
-
|
|
39
|
-
# Returns the wrapped error value, or traps if `self` is `Ok`.
|
|
40
|
-
fun unwrapErr(self) -> E =
|
|
41
|
-
match self
|
|
42
|
-
Ok(_) =>
|
|
43
|
-
todo
|
|
44
|
-
Err(e) =>
|
|
45
|
-
return e
|
|
46
|
-
|
|
47
|
-
# Returns the wrapped success value, or traps if `self` is `Err`. `msg`
|
|
48
|
-
# documents the expectation at the call site (matching Rust's
|
|
49
|
-
# `Result::expect`) but isn't surfaced anywhere at runtime — see the
|
|
50
|
-
# identical note on `Option.expect`.
|
|
51
|
-
fun expect(self, msg: Str) -> T =
|
|
52
|
-
match self
|
|
53
|
-
Ok(v) =>
|
|
54
|
-
return v
|
|
55
|
-
Err(_) =>
|
|
56
|
-
todo
|
|
57
|
-
|
|
58
|
-
fun expectErr(self, msg: Str) -> E =
|
|
59
|
-
match self
|
|
60
|
-
Ok(_) =>
|
|
61
|
-
todo
|
|
62
|
-
Err(e) =>
|
|
63
|
-
return e
|
|
64
|
-
|
|
65
|
-
fun unwrapOr(self, default: T) -> T =
|
|
66
|
-
match self
|
|
67
|
-
Ok(v) =>
|
|
68
|
-
return v
|
|
69
|
-
Err(_) =>
|
|
70
|
-
return default
|
|
71
|
-
|
|
72
|
-
# Like `unwrapOr`, but the fallback is computed lazily from the error via
|
|
73
|
-
# `cb` (only when `self` is `Err`).
|
|
74
|
-
fun unwrapOrElse(self, cb: fn(E) -> T) -> T =
|
|
75
|
-
match self
|
|
76
|
-
Ok(v) =>
|
|
77
|
-
return v
|
|
78
|
-
Err(e) =>
|
|
79
|
-
return cb(e)
|
|
80
|
-
|
|
81
|
-
# Discards the error, keeping only the success value (if any).
|
|
82
|
-
fun ok(self) -> Option[T] =
|
|
83
|
-
match self
|
|
84
|
-
Ok(v) =>
|
|
85
|
-
return Some(v)
|
|
86
|
-
Err(_) =>
|
|
87
|
-
return None
|
|
88
|
-
|
|
89
|
-
# Discards the success value, keeping only the error (if any).
|
|
90
|
-
fun err(self) -> Option[E] =
|
|
91
|
-
match self
|
|
92
|
-
Ok(_) =>
|
|
93
|
-
return None
|
|
94
|
-
Err(e) =>
|
|
95
|
-
return Some(e)
|
|
96
|
-
|
|
97
|
-
# Transforms the success value with `cb`, leaving `Err` untouched. `U` is
|
|
98
|
-
# `map`'s OWN generic param, separate from `Result[T, E]`'s own `T`/`E` —
|
|
99
|
-
# resolved per CALL SITE, same mechanism as `List.map`/`Option.map`.
|
|
100
|
-
fun map(self, cb: fn(T) -> U) -> Result[U, E] =
|
|
101
|
-
match self
|
|
102
|
-
Ok(v) =>
|
|
103
|
-
return Ok(cb(v))
|
|
104
|
-
Err(e) =>
|
|
105
|
-
return Err(e)
|
|
106
|
-
|
|
107
|
-
# Transforms the error value with `cb`, leaving `Ok` untouched. `F` is
|
|
108
|
-
# `mapErr`'s OWN generic param, separate from `Result[T, E]`'s own `T`/`E`.
|
|
109
|
-
fun mapErr(self, cb: fn(E) -> F) -> Result[T, F] =
|
|
110
|
-
match self
|
|
111
|
-
Ok(v) =>
|
|
112
|
-
return Ok(v)
|
|
113
|
-
Err(e) =>
|
|
114
|
-
return Err(cb(e))
|
|
115
|
-
|
|
116
|
-
# No implicit `toStr(self) -> Str` — see the identical note on
|
|
117
|
-
# `Option.toOptionStr` in `libs/std/option.plum`: a method that assumes
|
|
118
|
-
# `T`/`E: ToStr` gets compiled for EVERY specialization of `Result`
|
|
119
|
-
# anywhere in the program (there's no lazy/on-demand method compilation),
|
|
120
|
-
# so an implicit bound like this can break an unrelated specialization
|
|
121
|
-
# whose payload doesn't support it. Explicit stringifier callbacks instead.
|
|
122
|
-
fun toResultStr(self, okToStr: fn(T) -> Str, errToStr: fn(E) -> Str) -> Str =
|
|
123
|
-
match self
|
|
124
|
-
Ok(v) =>
|
|
125
|
-
return "Ok({okToStr(v)})"
|
|
126
|
-
Err(e) =>
|
|
127
|
-
return "Err({errToStr(e)})"
|
|
128
|
-
|
|
129
|
-
fun makeOkForResultTest() -> Result[Int, Str] =
|
|
130
|
-
return Ok(5)
|
|
131
|
-
|
|
132
|
-
fun makeErrForResultTest() -> Result[Int, Str] =
|
|
133
|
-
return Err("bad")
|
|
134
|
-
|
|
135
|
-
test "map transforms an Ok value and leaves Err untouched"
|
|
136
|
-
assert makeOkForResultTest().map(|v| v * 2).unwrap() == 10
|
|
137
|
-
assert makeErrForResultTest().map(|v| v * 2).isErr() == True
|
|
138
|
-
|
|
139
|
-
test "mapErr transforms an Err value and leaves Ok untouched"
|
|
140
|
-
assert makeErrForResultTest().mapErr(|e| e + "!").unwrapErr() == "bad!"
|
|
141
|
-
assert makeOkForResultTest().mapErr(|e| e + "!").unwrap() == 5
|
plum-std/Str.plum
CHANGED
|
@@ -193,7 +193,7 @@ enum Str(Comparable, ToStr, Readable, Writable) =
|
|
|
193
193
|
# `strings.SplitN` semantics, not JS's (which instead discards anything
|
|
194
194
|
# past the limit). An empty `separator` splits into individual bytes.
|
|
195
195
|
fun split(self, separator: Str, limit: Int) -> List[Str] =
|
|
196
|
-
result := List(head:
|
|
196
|
+
result := List(head: End, tail: End, size: 0)
|
|
197
197
|
len := self.length()
|
|
198
198
|
sep_len := separator.length()
|
|
199
199
|
if sep_len == 0
|
plum-std/Testing.plum
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
module std
|
|
2
2
|
|
|
3
|
-
import std/
|
|
3
|
+
import std/Try
|
|
4
|
-
import std/Result
|
|
5
4
|
import std/List
|
|
6
5
|
import std/Bool
|
|
7
6
|
import std/Number
|
|
@@ -18,8 +17,8 @@ import std/Str
|
|
|
18
17
|
# grammar form. They exist purely to make the condition read like an
|
|
19
18
|
# assertion instead of a raw comparison.
|
|
20
19
|
#
|
|
21
|
-
# `expectArray`/`
|
|
20
|
+
# `expectArray`/`expectOk`/`expectErr` below take a container
|
|
22
|
-
# type (`List[T]`/`
|
|
21
|
+
# type (`List[T]`/`Try[T, E]`) whose OWN generic param is
|
|
23
22
|
# nested inside the param type — this used to hit a real monomorphizer bug
|
|
24
23
|
# (misclassified as a "bare generic class reference" fn, then lost its type
|
|
25
24
|
# argument when the specialized signature was registered, mismatching the
|
|
@@ -61,27 +60,18 @@ fun expectObject(a: T, b: T) -> Bool =
|
|
|
61
60
|
fun expectArray(a: List[T], b: List[T]) -> Bool =
|
|
62
61
|
a.equals(b)
|
|
63
62
|
|
|
64
|
-
fun expectSome(o: Option[T]) -> Bool =
|
|
65
|
-
o.isSome()
|
|
66
|
-
|
|
67
|
-
fun expectNone(o: Option[T]) -> Bool =
|
|
68
|
-
o.isNone()
|
|
69
|
-
|
|
70
|
-
fun expectOk(r:
|
|
63
|
+
fun expectOk(r: Try[T, E]) -> Bool =
|
|
71
64
|
r.isOk()
|
|
72
65
|
|
|
73
|
-
fun expectErr(r:
|
|
66
|
+
fun expectErr(r: Try[T, E]) -> Bool =
|
|
74
67
|
r.isErr()
|
|
75
68
|
|
|
76
|
-
fun makeOkForTestingTest() ->
|
|
69
|
+
fun makeOkForTestingTest() -> Try[Int, Str] =
|
|
77
70
|
return Ok(5)
|
|
78
71
|
|
|
79
|
-
fun makeErrForTestingTest() ->
|
|
72
|
+
fun makeErrForTestingTest() -> Try[Int, Str] =
|
|
80
73
|
return Err("bad")
|
|
81
74
|
|
|
82
|
-
fun makeNoneForTestingTest() -> Option[Int] =
|
|
83
|
-
return None
|
|
84
|
-
|
|
85
75
|
test "expectTrue and expectFalse"
|
|
86
76
|
assert expectTrue(1 == 1)
|
|
87
77
|
assert expectFalse(1 == 2)
|
|
@@ -98,9 +88,7 @@ test "list equality via the List.equals method"
|
|
|
98
88
|
assert a.equals(b)
|
|
99
89
|
assert expectFalse(a.equals(c))
|
|
100
90
|
|
|
101
|
-
test "
|
|
91
|
+
test "Try checks via its own isOk/isErr methods"
|
|
102
|
-
assert Some(1).isSome()
|
|
103
|
-
assert makeNoneForTestingTest().isNone()
|
|
104
92
|
assert makeOkForTestingTest().isOk()
|
|
105
93
|
assert makeErrForTestingTest().isErr()
|
|
106
94
|
|
|
@@ -114,10 +102,7 @@ test "expectArray compares two List[T]s elementwise"
|
|
|
114
102
|
e := List("x", "y")
|
|
115
103
|
assert expectArray(d, e)
|
|
116
104
|
|
|
117
|
-
test "
|
|
105
|
+
test "expectOk/expectErr check the right variant"
|
|
118
|
-
assert expectSome(Some(1))
|
|
119
|
-
assert expectFalse(expectSome(makeNoneForTestingTest()))
|
|
120
|
-
assert expectNone(makeNoneForTestingTest())
|
|
121
106
|
assert expectOk(makeOkForTestingTest())
|
|
122
107
|
assert expectFalse(expectOk(makeErrForTestingTest()))
|
|
123
108
|
assert expectErr(makeErrForTestingTest())
|
plum-std/Try.plum
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
module std
|
|
2
|
+
|
|
3
|
+
import std/Str
|
|
4
|
+
import std/Bool
|
|
5
|
+
import std/Number
|
|
6
|
+
import std/List
|
|
7
|
+
import std/Map
|
|
8
|
+
|
|
9
|
+
# Try[T, E] is either success (`Ok`, carrying a `T`) or failure (`Err`,
|
|
10
|
+
# carrying an `E`). `E` is normally a structural tag union such as
|
|
11
|
+
# `[NotFound, BadNum(Str)]`; `?` merges the error unions of different calls
|
|
12
|
+
# into the enclosing function's declared union. A missing value is just an
|
|
13
|
+
# error tag (`[OutOfBounds]`, `[KeyNotFound]`, ...), so there is no Option.
|
|
14
|
+
enum Try[T, E] =
|
|
15
|
+
| Ok(T)
|
|
16
|
+
| Err(E)
|
|
17
|
+
|
|
18
|
+
fun isOk(self) -> Bool =
|
|
19
|
+
match self
|
|
20
|
+
Ok(_) => True
|
|
21
|
+
Err(_) => False
|
|
22
|
+
|
|
23
|
+
fun isErr(self) -> Bool =
|
|
24
|
+
match self
|
|
25
|
+
Ok(_) => False
|
|
26
|
+
Err(_) => True
|
|
27
|
+
|
|
28
|
+
# Returns the wrapped success value, or crashes if `self` is `Err`.
|
|
29
|
+
fun unwrap(self) -> T =
|
|
30
|
+
match self
|
|
31
|
+
Ok(v) =>
|
|
32
|
+
return v
|
|
33
|
+
Err(_) =>
|
|
34
|
+
crash "unwrap called on Err"
|
|
35
|
+
|
|
36
|
+
# Returns the wrapped error value, or crashes if `self` is `Ok`.
|
|
37
|
+
fun unwrapErr(self) -> E =
|
|
38
|
+
match self
|
|
39
|
+
Ok(_) =>
|
|
40
|
+
crash "unwrapErr called on Ok"
|
|
41
|
+
Err(e) =>
|
|
42
|
+
return e
|
|
43
|
+
|
|
44
|
+
fun unwrapOr(self, default: T) -> T =
|
|
45
|
+
match self
|
|
46
|
+
Ok(v) =>
|
|
47
|
+
return v
|
|
48
|
+
Err(_) =>
|
|
49
|
+
return default
|
|
50
|
+
|
|
51
|
+
# Like `unwrapOr`, but the fallback is computed lazily from the error.
|
|
52
|
+
fun unwrapOrElse(self, cb: fn(E) -> T) -> T =
|
|
53
|
+
match self
|
|
54
|
+
Ok(v) =>
|
|
55
|
+
return v
|
|
56
|
+
Err(e) =>
|
|
57
|
+
return cb(e)
|
|
58
|
+
|
|
59
|
+
# Transforms the success value with `cb`, leaving `Err` untouched.
|
|
60
|
+
fun map(self, cb: fn(T) -> U) -> Try[U, E] =
|
|
61
|
+
match self
|
|
62
|
+
Ok(v) =>
|
|
63
|
+
return Ok(cb(v))
|
|
64
|
+
Err(e) =>
|
|
65
|
+
return Err(e)
|
|
66
|
+
|
|
67
|
+
# Transforms the error value with `cb`, leaving `Ok` untouched.
|
|
68
|
+
fun mapErr(self, cb: fn(E) -> F) -> Try[T, F] =
|
|
69
|
+
match self
|
|
70
|
+
Ok(v) =>
|
|
71
|
+
return Ok(v)
|
|
72
|
+
Err(e) =>
|
|
73
|
+
return Err(cb(e))
|
|
74
|
+
|
|
75
|
+
# Like `map`, but `cb` itself returns a `Try` and the result isn't re-wrapped.
|
|
76
|
+
fun andThen(self, cb: fn(T) -> Try[U, E]) -> Try[U, E] =
|
|
77
|
+
match self
|
|
78
|
+
Ok(v) =>
|
|
79
|
+
return cb(v)
|
|
80
|
+
Err(e) =>
|
|
81
|
+
return Err(e)
|
|
82
|
+
|
|
83
|
+
# No implicit `toStr`: every method of a generic type is compiled for every
|
|
84
|
+
# specialization, so assuming `T`/`E: ToStr` would break unrelated uses.
|
|
85
|
+
fun toTryStr(self, okToStr: fn(T) -> Str, errToStr: fn(E) -> Str) -> Str =
|
|
86
|
+
match self
|
|
87
|
+
Ok(v) =>
|
|
88
|
+
return "Ok({okToStr(v)})"
|
|
89
|
+
Err(e) =>
|
|
90
|
+
return "Err({errToStr(e)})"
|
|
91
|
+
|
|
92
|
+
fun okIntForTryTest() -> Try[Int, [Bad]] =
|
|
93
|
+
return Ok(5)
|
|
94
|
+
|
|
95
|
+
fun errIntForTryTest() -> Try[Int, [Bad]] =
|
|
96
|
+
return Err(Bad)
|
|
97
|
+
|
|
98
|
+
fun fifthForTryTest(n: Int) -> Try[Int, [Bad]] =
|
|
99
|
+
return Ok(n / 5)
|
|
100
|
+
|
|
101
|
+
fun mergeForTryTest(s: Str, i: Int) -> Try[Int, [BadNumStr, OutOfBounds]] =
|
|
102
|
+
n := parseInt(s)?
|
|
103
|
+
xs := List(10, 20, 30)
|
|
104
|
+
x := xs.get(i)?
|
|
105
|
+
return Ok(n + x)
|
|
106
|
+
|
|
107
|
+
fun closureMapForTryTest(s: Str) -> Try[Int, [InvalidFirst]] =
|
|
108
|
+
n := parseInt(s) ? |e| InvalidFirst
|
|
109
|
+
return Ok(n)
|
|
110
|
+
|
|
111
|
+
test "map transforms an Ok value and leaves Err untouched"
|
|
112
|
+
assert okIntForTryTest().map(|v| v * 2).unwrap() == 10
|
|
113
|
+
assert errIntForTryTest().map(|v| v * 2).isErr()
|
|
114
|
+
|
|
115
|
+
test "mapErr transforms an Err and leaves Ok untouched"
|
|
116
|
+
assert okIntForTryTest().mapErr(|e| Other).unwrap() == 5
|
|
117
|
+
assert errIntForTryTest().mapErr(|e| Other).isErr()
|
|
118
|
+
|
|
119
|
+
test "andThen chains fallible steps"
|
|
120
|
+
a := okIntForTryTest().andThen(|n| fifthForTryTest(n))
|
|
121
|
+
assert a.unwrap() == 1
|
|
122
|
+
assert errIntForTryTest().andThen(|n| fifthForTryTest(n)).isErr()
|
|
123
|
+
|
|
124
|
+
test "unwrapOr and unwrapOrElse fall back on Err"
|
|
125
|
+
assert errIntForTryTest().unwrapOr(9) == 9
|
|
126
|
+
assert okIntForTryTest().unwrapOr(9) == 5
|
|
127
|
+
assert errIntForTryTest().unwrapOrElse(|e| 3) == 3
|
|
128
|
+
|
|
129
|
+
test "toTryStr renders both variants"
|
|
130
|
+
assert okIntForTryTest().toTryStr(|v| v.toStr(), |e| "bad") == "Ok(5)"
|
|
131
|
+
assert errIntForTryTest().toTryStr(|v| v.toStr(), |e| "bad") == "Err(bad)"
|
|
132
|
+
|
|
133
|
+
test "list_get_out_of_bounds"
|
|
134
|
+
r := List(1).get(5)
|
|
135
|
+
match r
|
|
136
|
+
Ok(_) =>
|
|
137
|
+
assert False
|
|
138
|
+
Err(OutOfBounds) =>
|
|
139
|
+
assert True
|
|
140
|
+
|
|
141
|
+
test "map_get_missing"
|
|
142
|
+
m := Map[Str, Int]()
|
|
143
|
+
match m.get("x")
|
|
144
|
+
Ok(_) =>
|
|
145
|
+
assert False
|
|
146
|
+
Err(KeyNotFound) =>
|
|
147
|
+
assert True
|
|
148
|
+
|
|
149
|
+
test "parse_int_bad"
|
|
150
|
+
match parseInt("x")
|
|
151
|
+
Ok(_) =>
|
|
152
|
+
assert False
|
|
153
|
+
Err(BadNumStr) =>
|
|
154
|
+
assert True
|
|
155
|
+
assert parseInt("42").unwrap() == 42
|
|
156
|
+
|
|
157
|
+
test "question_mark_merges_two_unions"
|
|
158
|
+
assert mergeForTryTest("5", 1).unwrap() == 25
|
|
159
|
+
match mergeForTryTest("x", 1)
|
|
160
|
+
Ok(_) =>
|
|
161
|
+
assert False
|
|
162
|
+
Err(BadNumStr) =>
|
|
163
|
+
assert True
|
|
164
|
+
Err(OutOfBounds) =>
|
|
165
|
+
assert False
|
|
166
|
+
match mergeForTryTest("5", 9)
|
|
167
|
+
Ok(_) =>
|
|
168
|
+
assert False
|
|
169
|
+
Err(BadNumStr) =>
|
|
170
|
+
assert False
|
|
171
|
+
Err(OutOfBounds) =>
|
|
172
|
+
assert True
|
|
173
|
+
|
|
174
|
+
test "try_closure_maps_error"
|
|
175
|
+
assert closureMapForTryTest("7").unwrap() == 7
|
|
176
|
+
match closureMapForTryTest("x")
|
|
177
|
+
Ok(_) =>
|
|
178
|
+
assert False
|
|
179
|
+
Err(InvalidFirst) =>
|
|
180
|
+
assert True
|
|
181
|
+
|
|
182
|
+
test "default_operator"
|
|
183
|
+
assert {List[Int]().first() ?? 7} == 7
|
|
184
|
+
assert {List(3).first() ?? 7} == 3
|
plum-std/Uuid.plum
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/Number
|
|
3
|
-
import std/
|
|
3
|
+
import std/Try
|
|
4
4
|
import std/Str
|
|
5
5
|
import std/Bool
|
|
6
6
|
|
|
@@ -45,7 +45,7 @@ fun nil() -> Uuid =
|
|
|
45
45
|
|
|
46
46
|
# Parses the canonical `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` text form
|
|
47
47
|
# (case-insensitive), returning `Err` if `s` isn't exactly that shape.
|
|
48
|
-
fun fromStr(s: Str) ->
|
|
48
|
+
fun fromStr(s: Str) -> Try[Uuid, Str] =
|
|
49
49
|
if s.length() != 36
|
|
50
50
|
return Err("invalid UUID length: '{s}'")
|
|
51
51
|
i := 0
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -3613,9 +3613,18 @@ impl<'a> Collector<'a> {
|
|
|
3613
3613
|
}
|
|
3614
3614
|
}
|
|
3615
3615
|
ast::Expr::TypeName(_) => {}
|
|
3616
|
+
// `"{x ?? 0}"` / `"{f()?}"` — interpolated parts compile like any
|
|
3617
|
+
// other expression, so any `?`/`??` scratch slot inside needs
|
|
3618
|
+
// allocating here too (`assert a ?? b == c` desugars into one).
|
|
3619
|
+
ast::Expr::String(s) => {
|
|
3620
|
+
for part in &s.parts {
|
|
3621
|
+
if let ast::StringPart::Interp(e) = part {
|
|
3622
|
+
self.walkExpr(e);
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3616
3626
|
ast::Expr::Int(_)
|
|
3617
3627
|
| ast::Expr::Float(_)
|
|
3618
|
-
| ast::Expr::String(_)
|
|
3619
3628
|
| ast::Expr::Self_
|
|
3620
3629
|
| ast::Expr::Var(_) => {}
|
|
3621
3630
|
// A closure literal's body has its own locals, belonging to the separate
|
plum-wasm-codegen/tests/codegen_tests.rs
CHANGED
|
@@ -1326,3 +1326,22 @@ fun main() -> Int =
|
|
|
1326
1326
|
);
|
|
1327
1327
|
assert_eq!(got, 10);
|
|
1328
1328
|
}
|
|
1329
|
+
|
|
1330
|
+
/// Regression (T5): the scratch-slot collector didn't descend into string
|
|
1331
|
+
/// interpolations, so a `??`/`?` inside `"{...}"` -- which is exactly what a
|
|
1332
|
+
/// failing `assert a ?? b == c` in a `test` block desugars to -- hit
|
|
1333
|
+
/// "missing '?:' scratch slot" at compile time.
|
|
1334
|
+
#[test]
|
|
1335
|
+
fn elvis_inside_string_interpolation_compiles() {
|
|
1336
|
+
let got = runTryMain(
|
|
1337
|
+
"\
|
|
1338
|
+
fun make() -> Try[Int, [Nope]] =
|
|
1339
|
+
Err(Nope)
|
|
1340
|
+
|
|
1341
|
+
fun main() -> Int =
|
|
1342
|
+
s := \"v={make() ?? 5}\"
|
|
1343
|
+
return 1
|
|
1344
|
+
",
|
|
1345
|
+
);
|
|
1346
|
+
assert_eq!(got, 1);
|
|
1347
|
+
}
|