plum

#treesitter#compiler#wasm

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  module std
2
2
  import std/Str
3
3
  import std/Buffer
4
- import std/Option
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) -> Option[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 (`None`).
108
+ # any point), is an error (`Err(InvalidBase64)`).
109
- fun decode(data: Str, at62: Str = "+", at63: Str = "/", pad: Str = "=") -> Option[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 None
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 Some(res.toStr())
132
+ return Ok(res.toStr())
plum-std/Bool.plum CHANGED
@@ -1,18 +1,18 @@
1
1
  module std
2
- import std/Result
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) -> Result[Bool, 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("could not parse bool from '{v}'")
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/Option
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) -> Result[Json, JsonParseError] =
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) -> Result[Response, 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(e) =>
89
+ Err(_) =>
91
- return Err("invalid status code in http response: {e}")
90
+ return Err("invalid status code in http response")
92
91
 
93
- fun get(url: Str, headers: Map[Str, Str]) -> Result[Response, 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]) -> Result[Response, 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]) -> Result[Response, 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) -> Result[Response, 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) -> Result[Response, 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) -> Result[Response, 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) -> Result[Response, Str] =
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/Option
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: Option[Node[T]]`.
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) -> Result[Json, JsonParseError] =
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) -> Option[Bool] =
56
+ fun asBool(self) -> Try[Bool, [WrongJsonType]] =
59
57
  match self
60
- JsonBool(b) => Some(b)
58
+ JsonBool(b) => Ok(b)
61
- _ => None
59
+ _ => Err(WrongJsonType)
62
60
 
63
- fun asFloat(self) -> Option[Float] =
61
+ fun asFloat(self) -> Try[Float, [WrongJsonType]] =
64
62
  match self
65
- JsonFloat(f) => Some(f)
63
+ JsonFloat(f) => Ok(f)
66
- _ => None
64
+ _ => Err(WrongJsonType)
67
65
 
68
- fun asStr(self) -> Option[Str] =
66
+ fun asStr(self) -> Try[Str, [WrongJsonType]] =
69
67
  match self
70
- JsonStr(s) => Some(s)
68
+ JsonStr(s) => Ok(s)
71
- _ => None
69
+ _ => Err(WrongJsonType)
72
70
 
73
- fun asList(self) -> Option[List[Json]] =
71
+ fun asList(self) -> Try[List[Json], [WrongJsonType]] =
74
72
  match self
75
- JsonList(items) => Some(items)
73
+ JsonList(items) => Ok(items)
76
- _ => None
74
+ _ => Err(WrongJsonType)
77
75
 
78
- fun asMap(self) -> Option[Map[Str, Json]] =
76
+ fun asMap(self) -> Try[Map[Str, Json], [WrongJsonType]] =
79
77
  match self
80
- JsonMap(pairs) => Some(pairs)
78
+ JsonMap(pairs) => Ok(pairs)
81
- _ => None
79
+ _ => Err(WrongJsonType)
82
80
 
83
81
  # Carries where in the source a parse failure happened, alongside the
84
- # message — implements the shared `Err` trait (`libs/std/err.plum`) rather
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(Err) =
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) -> Result[Json, JsonParseError] =
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) -> Result[Json, JsonParseError] =
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) -> Result[Json, JsonParseError] =
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) -> Result[Json, JsonParseError] =
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(m) =>
177
+ Err(_) =>
193
- return Err(self.fail(m))
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(m) =>
182
+ Err(_) =>
198
- return Err(self.fail(m))
183
+ return Err(self.fail("invalid number"))
199
184
  Ok(mv) =>
200
185
  match exponent
201
- Err(m) =>
186
+ Err(_) =>
202
- return Err(self.fail(m))
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) -> Result[Json, JsonParseError] =
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) -> Result[Json, JsonParseError] =
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) -> Result[Json, JsonParseError] =
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) -> Result[Int, JsonParseError] =
249
+ fun parseHex4(self) -> Try[Int, JsonParseError] =
265
250
  return self.parseHex4From(0, 0)
266
251
 
267
- fun parseHex4From(self, value: Int, count: Int) -> Result[Int, JsonParseError] =
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) -> Result[Json, JsonParseError] =
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) -> Result[Json, JsonParseError] =
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) -> Result[Json, JsonParseError] =
289
+ fun parseArray(self) -> Try[Json, JsonParseError] =
305
290
  self.advance()
306
291
  self.skipSpace()
307
- items := List[Json](head: None, tail: None, size: 0)
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]) -> Result[Json, JsonParseError] =
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) -> Result[Json, JsonParseError] =
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]) -> Result[Json, JsonParseError] =
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) -> Result[Json, JsonParseError] =
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/Option
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: Option[Node[T]], next: Option[Node[T]])
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: Option[Node[T]], tail: Option[Node[T]], size: Int)
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: None, tail: None, size: 0)
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) -> Option[T] =
35
+ fun get(self, i: Int) -> Try[T, [OutOfBounds]] =
31
36
  current := self.head
32
37
  index := 0
33
- while current != None
38
+ while current != End
34
39
  match current
35
- Some(Node(value, _, next)) =>
40
+ Next(Node(value, _, next)) =>
36
41
  if index == i
37
- return Some(value)
42
+ return Ok(value)
38
43
  current = next
39
44
  index = index + 1
40
- None =>
45
+ End =>
41
46
  break
42
- None
47
+ Err(OutOfBounds)
43
48
 
44
- # sets the element at i'th index of the list, returning the old value (or None if i is out of bounds)
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) -> Option[T] =
50
+ fun set(self, i: Int, v: T) -> Try[T, [OutOfBounds]] =
46
51
  current := self.head
47
52
  index := 0
48
- while current != None
53
+ while current != End
49
54
  match current
50
- Some(node) =>
55
+ Next(node) =>
51
56
  if index == i
52
57
  old := node.value
53
58
  node.value = v
54
- return Some(old)
59
+ return Ok(old)
55
60
  current = node.next
56
61
  index = index + 1
57
- None =>
62
+ End =>
58
63
  break
59
- None
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: None)
84
+ node := Node(value: v, prev: self.tail, next: End)
80
85
  match self.tail
81
- Some(t) =>
86
+ Next(t) =>
82
- t.next = Some(node)
87
+ t.next = Next(node)
83
- None =>
88
+ End =>
84
- self.head = Some(node)
89
+ self.head = Next(node)
85
- self.tail = Some(node)
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
- Some(p) =>
96
+ Next(p) =>
92
97
  p.next = node.next
93
- None =>
98
+ End =>
94
99
  self.head = node.next
95
100
  match node.next
96
- Some(n) =>
101
+ Next(n) =>
97
102
  n.prev = node.prev
98
- None =>
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 != None
111
+ while current != End
107
112
  match current
108
- Some(node) =>
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
- None =>
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 != None
125
+ while current != End
121
126
  match current
122
- Some(node) =>
127
+ Next(node) =>
123
128
  if node.value == v
124
129
  self.unlink(node)
125
130
  return
126
131
  current = node.next
127
- None =>
132
+ End =>
128
133
  break
129
134
 
130
135
  # removes all objects from this list
131
136
  fun clear(self) =
132
- self.head = None
137
+ self.head = End
133
- self.tail = None
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 != None
144
+ while current != End
140
145
  match current
141
- Some(node) =>
146
+ Next(node) =>
142
147
  next := node.next
143
148
  node.next = node.prev
144
149
  node.prev = next
145
150
  current = next
146
- None =>
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: Some(node))
163
+ new_node := Node(value: value, prev: node.prev, next: Next(node))
159
164
  match node.prev
160
- Some(p) =>
165
+ Next(p) =>
161
- p.next = Some(new_node)
166
+ p.next = Next(new_node)
162
- None =>
167
+ End =>
163
- self.head = Some(new_node)
168
+ self.head = Next(new_node)
164
- node.prev = Some(new_node)
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 != None
177
+ while current != End
173
178
  match current
174
- Some(node) =>
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
- None =>
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: None, tail: None, size: 0)
192
+ result := List(head: End, tail: End, size: 0)
188
193
  current := self.head
189
- while current != None
194
+ while current != End
190
195
  match current
191
- Some(Node(value, _, next)) =>
196
+ Next(Node(value, _, next)) =>
192
197
  result.insertSorted(value, before)
193
198
  current = next
194
- None =>
199
+ End =>
195
200
  break
196
201
  return result
197
202
 
198
- # Returns the first element equal to `search`, or `None`.
203
+ # Returns the first element equal to `search`, or `Err(NotFound)`.
199
- fun find(self, search: T) -> Option[T] =
204
+ fun find(self, search: T) -> Try[T, [NotFound]] =
200
205
  current := self.head
201
- while current != None
206
+ while current != End
202
207
  match current
203
- Some(Node(value, _, next)) =>
208
+ Next(Node(value, _, next)) =>
204
209
  if value == search
205
- return Some(value)
210
+ return Ok(value)
206
211
  current = next
207
- None =>
212
+ End =>
208
213
  break
209
- return None
214
+ return Err(NotFound)
210
215
 
211
216
  fun contains(self, v: T) -> Bool =
212
- return self.find(v) != None
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 != None
222
+ while current != End
218
223
  match current
219
- Some(Node(value, _, next)) =>
224
+ Next(Node(value, _, next)) =>
220
225
  cb(value)
221
226
  current = next
222
- None =>
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: None, tail: None, size: 0)
237
+ result := List(head: End, tail: End, size: 0)
233
238
  current := self.head
234
- while current != None
239
+ while current != End
235
240
  match current
236
- Some(Node(value, _, next)) =>
241
+ Next(Node(value, _, next)) =>
237
242
  result.add(cb(value))
238
243
  current = next
239
- None =>
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: None, tail: None, size: 0)
254
+ result := List(head: End, tail: End, size: 0)
250
255
  current := self.head
251
- while current != None
256
+ while current != End
252
257
  match current
253
- Some(Node(value, _, next)) =>
258
+ Next(Node(value, _, next)) =>
254
259
  if predicate(value)
255
260
  result.add(value)
256
261
  current = next
257
- None =>
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: None, tail: None, size: 0)
268
+ result := List(head: End, tail: End, size: 0)
264
269
  current := self.head
265
- while current != None
270
+ while current != End
266
271
  match current
267
- Some(Node(value, _, next)) =>
272
+ Next(Node(value, _, next)) =>
268
273
  if !predicate(value)
269
274
  result.add(value)
270
275
  current = next
271
- None =>
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 != None
282
+ while current != End
278
283
  match current
279
- Some(Node(value, _, next)) =>
284
+ Next(Node(value, _, next)) =>
280
285
  if predicate(value)
281
286
  return True
282
287
  current = next
283
- None =>
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 != None
294
+ while current != End
290
295
  match current
291
- Some(Node(value, _, next)) =>
296
+ Next(Node(value, _, next)) =>
292
297
  if !predicate(value)
293
298
  return False
294
299
  current = next
295
- None =>
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 != None
312
+ while current != End
308
313
  match current
309
- Some(Node(value, _, next)) =>
314
+ Next(Node(value, _, next)) =>
310
315
  result = cb(result, value)
311
316
  current = next
312
- None =>
317
+ End =>
313
318
  break
314
319
  return result
315
320
 
316
321
  # returns the first element in the list
317
- fun first(self) -> Option[T] =
322
+ fun first(self) -> Try[T, [ListWasEmpty]] =
318
323
  match self.head
319
- Some(Node(value, _, _)) => Some(value)
324
+ Next(Node(value, _, _)) => Ok(value)
320
- None => None
325
+ End => Err(ListWasEmpty)
321
326
 
322
327
  # returns the last element in the list
323
- fun last(self) -> Option[T] =
328
+ fun last(self) -> Try[T, [ListWasEmpty]] =
324
329
  match self.tail
325
- Some(Node(value, _, _)) => Some(value)
330
+ Next(Node(value, _, _)) => Ok(value)
326
- None => None
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: None, tail: None, size: 0)
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 != None && i < e
341
+ while current != End && i < e
337
342
  match current
338
- Some(Node(value, _, next)) =>
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
- None =>
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 `None` if `self` is empty.
363
+ # Returns one uniformly random element, or `Err(ListWasEmpty)` if `self` is empty.
359
- fun sample(self) -> Option[T] =
364
+ fun sample(self) -> Try[T, [ListWasEmpty]] =
360
365
  if self.size == 0
361
- return None
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: None, tail: None, size: 0)` constructions in the SAME method
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: None, tail: None, size: 0)
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: None, ...)` bare constructions
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: None, tail: None, size: 0)
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 != None
451
+ while current != End
447
452
  match current
448
- Some(Node(value, _, next)) =>
453
+ Next(Node(value, _, next)) =>
449
454
  res.write(value.toStr())
450
- if next != None
455
+ if next != End
451
456
  res.write(sep)
452
457
  current = next
453
- None =>
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: Option[Int]) -> Int =
479
+ fun optSumForListTest(o: Try[Int, [OutOfBounds]]) -> Int =
475
480
  match o
476
- Some(v) => v
481
+ Ok(v) => v
477
- None => -1000
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/Option
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
- # `toOptionStr`) IS safe as an ordinary method here: `Pair[K, V]` is only
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 `None` if absent.
72
+ # Gets the value for `k`, or `Err(KeyNotFound)` if absent.
74
- fun get(self, k: K) -> Option[V] =
73
+ fun get(self, k: K) -> Try[V, [KeyNotFound]] =
75
74
  current := self.buckets.get(self.bucketIndex(k)).head
76
- while current != None
75
+ while current != End
77
76
  match current
78
- Some(Node(Pair(key, val), _, next)) =>
77
+ Next(Node(Pair(key, val), _, next)) =>
79
78
  if key == k
80
- return Some(val)
79
+ return Ok(val)
81
80
  current = next
82
- None =>
81
+ End =>
83
82
  break
84
- return None
83
+ return Err(KeyNotFound)
85
84
 
86
85
  fun has(self, k: K) -> Bool =
87
- return self.get(k) != None
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 != None
95
+ while current != End
97
96
  match current
98
- Some(Node(pair, _, next)) =>
97
+ Next(Node(pair, _, next)) =>
99
98
  if pair.key == k
100
99
  pair.val = v
101
100
  return
102
101
  current = next
103
- None =>
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 != None
116
+ while current != End
118
117
  match current
119
- Some(node) =>
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
- None =>
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 != None
147
+ while current != End
149
148
  match current
150
- Some(Node(Pair(key, val), _, next)) =>
149
+ Next(Node(Pair(key, val), _, next)) =>
151
150
  cb(key, val)
152
151
  current = next
153
- None =>
152
+ End =>
154
153
  break
155
154
  i = i + 1
156
155
 
157
156
  fun keys(self) -> List[K] =
158
- result := List(head: None, tail: None, size: 0)
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: None, tail: None, size: 0)
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 != None
176
+ while current != End
178
177
  match current
179
- Some(Node(Pair(key, val), _, next)) =>
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
- None =>
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/Result
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) -> Result[Int, Str] =
306
+ fun parseInt(s: Str) -> Try[Int, [BadNumStr]] =
307
307
  len := s.length()
308
308
  if len == 0
309
- return Err("empty string")
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("invalid integer: '{s}'")
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("invalid integer: '{s}'")
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) -> Result[Float, Str] =
326
+ fun parseFloat(s: Str) -> Try[Float, [BadNumStr]] =
327
327
  len := s.length()
328
328
  if len == 0
329
- return Err("empty string")
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("invalid float: '{s}'")
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("invalid float: '{s}'")
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("invalid float: '{s}'")
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("invalid float: '{s}'")
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/Result
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) -> Result[Str, 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) -> Result[Bool, 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) -> Result[Bool, 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) -> Result[Bool, 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) -> Result[Bool, 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) -> Result[Bool, Str] =
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) -> Result[Bool, 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) -> Result[Bool, 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) -> Result[Bool, Str] =
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) -> Result[Bool, 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) -> Result[Str, 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) -> Result[List[Str], 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) -> Result[Str, 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) -> Result[Str, 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) -> Result[Bool, 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) -> Result[Bool, 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) -> Result[Bool, 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) -> Result[Bool, Str] =
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: None, tail: None, size: 0)
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/Option
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`/`expectSome`/`expectOk`/`expectErr` below take a container
20
+ # `expectArray`/`expectOk`/`expectErr` below take a container
22
- # type (`List[T]`/`Option[T]`/`Result[T, E]`) whose OWN generic param is
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: Result[T, E]) -> Bool =
63
+ fun expectOk(r: Try[T, E]) -> Bool =
71
64
  r.isOk()
72
65
 
73
- fun expectErr(r: Result[T, E]) -> Bool =
66
+ fun expectErr(r: Try[T, E]) -> Bool =
74
67
  r.isErr()
75
68
 
76
- fun makeOkForTestingTest() -> Result[Int, Str] =
69
+ fun makeOkForTestingTest() -> Try[Int, Str] =
77
70
  return Ok(5)
78
71
 
79
- fun makeErrForTestingTest() -> Result[Int, Str] =
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 "Option/Result checks via their own isSome/isNone/isOk/isErr methods"
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 "expectSome/expectNone/expectOk/expectErr check the right variant"
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/Result
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) -> Result[Uuid, 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
+ }