plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
748850d
— Peter John
2026-11-01T21:01:25+05:30
T9: Json + README
- README.md +121 -50
- plum-std/Json.plum +35 -31
README.md
CHANGED
|
@@ -18,9 +18,12 @@ import std/Seq
|
|
|
18
18
|
|
|
19
19
|
error ParseError = BadNum | Negative # a named error set
|
|
20
20
|
|
|
21
|
+
struct Circle(r: Float) # small copy-semantics value type
|
|
22
|
+
record Rect(w: Float, h: Float) # reference type
|
|
23
|
+
|
|
21
|
-
enum Shape = # a
|
|
24
|
+
enum Shape = # a union: a Circle or a Rect
|
|
22
|
-
| Circle
|
|
25
|
+
| Circle
|
|
23
|
-
| Rect
|
|
26
|
+
| Rect
|
|
24
27
|
|
|
25
28
|
SIDES_MAX = 4 # associated constant: Shape.SIDES_MAX
|
|
26
29
|
|
|
@@ -74,7 +77,7 @@ test "glance"
|
|
|
74
77
|
| group an expression | `{1 + 2} * 3` (braces; parentheses are for calls) |
|
|
75
78
|
| a conditional expression | `if c then a else b` (there is no `?:` ternary) |
|
|
76
79
|
| loop | `while c`, `for x := range src`, `for i := range n` |
|
|
77
|
-
| a
|
|
80
|
+
| a union of types | `enum Shape = Circle \| Rectangle` |
|
|
78
81
|
| a small value type | `struct Vec2(x: Float, y: Float)` |
|
|
79
82
|
| a record (reference type) | `record User(name: Str, age: Int)` |
|
|
80
83
|
| a method / static / constant | `fun area(self)` / `fun origin()` / `NAME = expr` inside the type body |
|
|
@@ -300,7 +303,7 @@ todo # traps at runtime — marks a body as not yet implemented
|
|
|
300
303
|
|
|
301
304
|
### Pattern `if`
|
|
302
305
|
|
|
303
|
-
`if Pattern := expr` tests one case and binds the pattern's names for that branch only. It uses the same patterns as `match` (constructor patterns with nested fields, literals, `_`, bare
|
|
306
|
+
`if Pattern := expr` tests one case and binds the pattern's names for that branch only. It uses the same patterns as `match` (constructor patterns with nested fields, literals, `_`, bare member, tag or error names), and reads like any other `if`/`else if`/`else` chain. A `, cond` after the value adds more conditions that already see the bindings. It works in `else if`, inline (`if Circle(r) := s then r else 0.0`) and in `while`:
|
|
304
307
|
|
|
305
308
|
```plum
|
|
306
309
|
fun draw(shape: Shape) =
|
|
@@ -312,6 +315,16 @@ fun draw(shape: Shape) =
|
|
|
312
315
|
drawOther(shape)
|
|
313
316
|
```
|
|
314
317
|
|
|
318
|
+
A bare member name narrows the value inside the branch, so the member's own fields and methods are available:
|
|
319
|
+
|
|
320
|
+
```plum
|
|
321
|
+
fun width(s: Shape) -> Float =
|
|
322
|
+
if Rectangle := s
|
|
323
|
+
s.w
|
|
324
|
+
else
|
|
325
|
+
0.0
|
|
326
|
+
```
|
|
327
|
+
|
|
315
328
|
```plum
|
|
316
329
|
while Busy(task) := worker.state # loops while the pattern matches
|
|
317
330
|
task.step()
|
|
@@ -366,7 +379,7 @@ A function without a return type returns `Unit`; the last expression of a body i
|
|
|
366
379
|
|
|
367
380
|
## Types: struct, record and enum
|
|
368
381
|
|
|
369
|
-
`enum` is a
|
|
382
|
+
`enum` is a union: a value that is one of several member types. `struct` and `record` are product types with named fields. A `struct` is a value type and a `record` is a reference type.
|
|
370
383
|
|
|
371
384
|
```plum
|
|
372
385
|
struct Vec2(x: Float, y: Float) =
|
|
@@ -378,23 +391,6 @@ struct Vec2(x: Float, y: Float) =
|
|
|
378
391
|
record User(name: Str, age: Int)
|
|
379
392
|
|
|
380
393
|
record Pair[K, V](key: K, val: V)
|
|
381
|
-
|
|
382
|
-
enum Color =
|
|
383
|
-
| Red
|
|
384
|
-
| Green
|
|
385
|
-
| Blue
|
|
386
|
-
|
|
387
|
-
enum Maybe =
|
|
388
|
-
| Just(Int) # unnamed positional payload — Just(5)
|
|
389
|
-
| Nothing
|
|
390
|
-
|
|
391
|
-
enum Shape =
|
|
392
|
-
| Circle(radius: Int) # named payload fields — Circle(radius: 5) or Circle(5)
|
|
393
|
-
| Square(x: Int, y: Int)
|
|
394
|
-
|
|
395
|
-
enum Step(n: Int) = # a shared field on every variant ...
|
|
396
|
-
| ReadMin(10) # ... each variant supplies its own value for it
|
|
397
|
-
| ReadMax(20)
|
|
398
394
|
```
|
|
399
395
|
|
|
400
396
|
The header ends with `=` only when an indented body follows: `struct Vec2(x: Float, y: Float)` with no body has no `=`.
|
|
@@ -412,22 +408,98 @@ u2.age = 4 # u.age is now 4: a record is shared
|
|
|
412
408
|
|
|
413
409
|
The other declaration forms are `struct` and `record` (below), `error Name = A | B` ([Error handling](#error-handling)) and `primitive Name =` ([Constants, statics and `primitive`](#constants-statics-and-primitive)).
|
|
414
410
|
|
|
415
|
-
###
|
|
411
|
+
### Enums
|
|
412
|
+
|
|
413
|
+
An `enum` lists the types it can hold. A member is a declared `struct` or `record`, a built-in (`Int`, `Float`, `Bool`, `Byte`), a std type (`Str`, `List[Json]`, `Map[Str, Json]`), or a bare name that is not a declared type, which becomes a payload-less tag. A short list fits on one line; a longer one, or one with methods, goes on indented `|` lines:
|
|
414
|
+
|
|
415
|
+
```plum
|
|
416
|
+
struct Circle(r: Float)
|
|
417
|
+
|
|
418
|
+
record Rectangle(w: Float, h: Float)
|
|
419
|
+
|
|
420
|
+
enum Shape = Circle | Rectangle
|
|
421
|
+
|
|
422
|
+
enum Color =
|
|
423
|
+
| Red
|
|
424
|
+
| Green
|
|
425
|
+
| Blue
|
|
426
|
+
|
|
427
|
+
enum Json =
|
|
428
|
+
| JsonNull
|
|
429
|
+
| Bool
|
|
430
|
+
| Float
|
|
431
|
+
| Str
|
|
432
|
+
| List[Json]
|
|
433
|
+
| Map[Str, Json]
|
|
434
|
+
```
|
|
416
435
|
|
|
417
|
-
|
|
436
|
+
`Red`, `Green`, `Blue` and `JsonNull` are tags: values with no payload. Every other member is an existing type, and the enum adds no new constructor for it.
|
|
437
|
+
|
|
438
|
+
A member converts to its enum wherever the enum is expected: an argument, a `return`, a tail expression, `push`, a `T?` or a `!T` slot. Nothing is written at the call site:
|
|
418
439
|
|
|
419
440
|
```plum
|
|
420
|
-
|
|
421
|
-
|
|
441
|
+
fun make(big: Bool) -> Shape =
|
|
442
|
+
if big
|
|
443
|
+
Rectangle(w: 10.0, h: 10.0)
|
|
444
|
+
else
|
|
445
|
+
Circle(r: 1.0)
|
|
446
|
+
|
|
447
|
+
fun label(j: Json) -> Str =
|
|
448
|
+
"json"
|
|
449
|
+
|
|
450
|
+
fun use() -> Str =
|
|
451
|
+
label(2.5)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
A pattern names a member to take the value apart (`Circle(r)`) or, written as just the member's name, to narrow the subject to that type. In an arm that names `Float`, the subject is a `Float`; the narrowed subject must be a local, a parameter or `self`:
|
|
422
455
|
|
|
456
|
+
```plum
|
|
457
|
+
fun show(j: Json) -> Str =
|
|
458
|
+
match j
|
|
459
|
+
JsonNull => "null"
|
|
460
|
+
Bool => if j then "true" else "false"
|
|
461
|
+
Float => j.toStr()
|
|
462
|
+
Str => "\"{j}\""
|
|
463
|
+
List => "list"
|
|
464
|
+
Map => "map"
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
A method that every member defines is callable on the enum itself, and the call goes to the member's own method (auto-dispatch):
|
|
468
|
+
|
|
469
|
+
```plum
|
|
470
|
+
fun areaOf(s: Shape) -> Float =
|
|
471
|
+
s.area()
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
One type can be a member of several enums. A `Circle` is a `Shape` and also a `Round`, and converts to whichever the context asks for:
|
|
475
|
+
|
|
476
|
+
```plum
|
|
477
|
+
enum Round = Circle | Oval
|
|
478
|
+
|
|
479
|
+
record Oval(a: Float)
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
A discriminant enum gives every tag a shared number:
|
|
483
|
+
|
|
484
|
+
```plum
|
|
485
|
+
enum Step(n: Int) =
|
|
486
|
+
| ReadMin(10) # each tag supplies its own value for the shared field
|
|
487
|
+
| ReadMax(20)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Methods, `self` and field access
|
|
491
|
+
|
|
492
|
+
A `fun` declared indented directly inside an `enum`, `struct` or `record` body is a method; its first parameter is `self`:
|
|
493
|
+
|
|
494
|
+
```plum
|
|
495
|
+
record Cat(name: Str, age: Int) =
|
|
423
496
|
fun getAge(self) -> Int =
|
|
424
497
|
self.age
|
|
425
498
|
|
|
426
499
|
fun birthday(self) -> Int =
|
|
427
500
|
self.age + 1
|
|
428
501
|
|
|
429
|
-
enum Wrapper =
|
|
430
|
-
|
|
502
|
+
record Wrapper(inner: Cat, tag: Int)
|
|
431
503
|
|
|
432
504
|
fun main() -> Int =
|
|
433
505
|
c := Cat(name: "Whiskers", age: 3)
|
|
@@ -436,11 +508,11 @@ fun main() -> Int =
|
|
|
436
508
|
a + w.inner.age # chained field access
|
|
437
509
|
```
|
|
438
510
|
|
|
439
|
-
A
|
|
511
|
+
A field of a `struct` or `record` is read with `.field`. On a union it is read after narrowing (`if Rectangle := s`, then `s.w`).
|
|
440
512
|
|
|
441
513
|
### Constructing values
|
|
442
514
|
|
|
443
|
-
Construct a
|
|
515
|
+
Construct a `struct` or `record` by calling its name with `field: value` pairs (any order; every field required), or positionally:
|
|
444
516
|
|
|
445
517
|
```plum
|
|
446
518
|
cat := Cat(name: "Whiskers", age: 3)
|
|
@@ -463,9 +535,9 @@ match n
|
|
|
463
535
|
else
|
|
464
536
|
2
|
|
465
537
|
|
|
466
|
-
match
|
|
538
|
+
match shape
|
|
467
|
-
|
|
539
|
+
Circle(r) => r
|
|
468
|
-
|
|
540
|
+
Rectangle(w, h) => w * h
|
|
469
541
|
|
|
470
542
|
match book
|
|
471
543
|
FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures =>
|
|
@@ -473,14 +545,18 @@ match book
|
|
|
473
545
|
FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
|
|
474
546
|
```
|
|
475
547
|
|
|
476
|
-
Patterns can be literals, a bare identifier (binds), a bare capitalized name (an enum
|
|
548
|
+
Patterns can be literals, a bare identifier (binds), a bare capitalized name (a member or tag of an enum, or an error name when matching an error value, also written `error.Name`; compared), a constructor pattern (`Circle(r)`, binding its fields, possibly nested), a bare member name (`Float`, which narrows the subject to that type), or `_` (wildcard). `match a, b, ...` supports multiple subjects, and a case can carry a `when <expr>` guard. A `match` over an enum must be exhaustive; to test just one case, use [pattern `if`](#pattern-if) (`if Circle(r) := shape`). See [`plum-examples/match.plum`](plum-examples/match.plum), [`plum-examples/dop_visitor.plum`](plum-examples/dop_visitor.plum) and [`plum-examples/oop_visitor.plum`](plum-examples/oop_visitor.plum).
|
|
477
549
|
|
|
478
550
|
Mixed kinds of value live in one enum with a `match self` method per operation:
|
|
479
551
|
|
|
480
552
|
```plum
|
|
553
|
+
struct Circle(r: Float)
|
|
554
|
+
|
|
555
|
+
record Rect(w: Float, h: Float)
|
|
556
|
+
|
|
481
557
|
enum Shape =
|
|
482
|
-
| Circle
|
|
558
|
+
| Circle
|
|
483
|
-
| Rect
|
|
559
|
+
| Rect
|
|
484
560
|
|
|
485
561
|
fun area(self) -> Float =
|
|
486
562
|
match self
|
|
@@ -492,7 +568,7 @@ See [`plum-examples/shapes.plum`](plum-examples/shapes.plum) and [`plum-examples
|
|
|
492
568
|
|
|
493
569
|
### `struct` and `record`
|
|
494
570
|
|
|
495
|
-
`struct` declares a small, copy-semantics value type (a vector, a color, a rectangle) that is flattened into wasm locals instead of being heap-allocated. `record` declares a reference type. Both
|
|
571
|
+
`struct` declares a small, copy-semantics value type (a vector, a color, a rectangle) that is flattened into wasm locals instead of being heap-allocated. `record` declares a reference type. Both are declared with named fields:
|
|
496
572
|
|
|
497
573
|
```plum
|
|
498
574
|
struct Vec2(x: Float, y: Float) =
|
|
@@ -509,14 +585,13 @@ A `struct` has no generics; its fields are only `Int`, `Float`, `Bool` or other
|
|
|
509
585
|
## Generics
|
|
510
586
|
|
|
511
587
|
```plum
|
|
512
|
-
enum Box[T] =
|
|
513
|
-
|
|
588
|
+
record Box[T](value: T)
|
|
514
589
|
|
|
515
590
|
fun wrap(value: T) -> Bool =
|
|
516
591
|
true
|
|
517
592
|
```
|
|
518
593
|
|
|
519
|
-
A free function's generic parameters are its single-uppercase-letter type names (`T`, `U`); a type declares its own with `
|
|
594
|
+
A free function's generic parameters are its single-uppercase-letter type names (`T`, `U`); a type declares its own with `record Box[T]`. Generic types, methods, and free functions are duck-typed and compiled once per distinct instantiation. See [How generics compile](#how-generics-compile).
|
|
520
595
|
|
|
521
596
|
### How generics compile
|
|
522
597
|
|
|
@@ -537,8 +612,7 @@ in outer[Plain] (called at main.plum:9:3):
|
|
|
537
612
|
- **Explicit behavior values.** When a generic needs behavior that is not a method on `T`, pass it explicitly, like a Zig comptime parameter or a Haskell dictionary:
|
|
538
613
|
|
|
539
614
|
```plum
|
|
540
|
-
enum Num[T] =
|
|
541
|
-
|
|
615
|
+
record Num[T](add: fn(T, T) -> T, zero: T)
|
|
542
616
|
|
|
543
617
|
fun sumWith(xs: List[T], num: Num[T]) -> T =
|
|
544
618
|
acc := num.zero
|
|
@@ -746,8 +820,7 @@ See [`plum-examples/seq.plum`](plum-examples/seq.plum) (one test per adapter and
|
|
|
746
820
|
An optional is `T?`: a `T`, or `null`. It is its own type, not an error: it carries no error name, and `catch` and `try` do not apply to it. A plain `T` in a `T?` slot is the present value, and `null` is the absent value wherever a `T?` is expected (a call or constructor argument, a `return`, a tail, a list element, a parameter default such as `n: Int? = null`). `null` with nothing to say what it is an optional of is an error (`x := null`: `cannot infer the type of null`). For a reference type (a class, enum, `Str`, `List`, ...) a `T?` is stored as a nullable reference, so a present value costs no allocation; for `Int`, `Float`, `Bool` and structs it is the value plus a present flag. A doubled `T??` is not allowed.
|
|
747
821
|
|
|
748
822
|
```plum
|
|
749
|
-
enum Person =
|
|
750
|
-
|
|
823
|
+
record Person(name: Str, age: Int)
|
|
751
824
|
|
|
752
825
|
fun find(n: Int) -> Person? =
|
|
753
826
|
if n > 0
|
|
@@ -786,11 +859,9 @@ fun total(xs: List[Int?]) -> Int =
|
|
|
786
859
|
`obj?.field` / `obj?.method(args)` (safe navigation, Groovy/Kotlin-style) reaches into an optional (or an error union) without unwrapping it -- a present value maps the field or method access over the inner value and re-wraps it, null (or an error) passes through untouched. A field that is itself a `T?` flattens, so a chain is one `?` value, not a nested one:
|
|
787
860
|
|
|
788
861
|
```plum
|
|
789
|
-
enum Target =
|
|
790
|
-
|
|
862
|
+
record Target(name: Str)
|
|
791
863
|
|
|
792
|
-
enum User =
|
|
793
|
-
|
|
864
|
+
record User(name: Str, target: Target?)
|
|
794
865
|
|
|
795
866
|
fun targetName(u: User?) -> Str? =
|
|
796
867
|
u?.target?.name
|
|
@@ -903,7 +974,7 @@ A stored value (`r := f()`) is handled later with `try r`, `r!` or `r catch ...`
|
|
|
903
974
|
enum Failure =
|
|
904
975
|
| Timeout
|
|
905
976
|
| Refused
|
|
906
|
-
| BadRequest
|
|
977
|
+
| BadRequest
|
|
907
978
|
```
|
|
908
979
|
|
|
909
980
|
An error union can have an optional as its success value: `!T?` (`error{Bad}!Int?`). `try x` gives the `T?`, and `?:` then handles the null: `v := try parse(s)` followed by `v ?: 0`. An error union can also be the success type of another (`List[!Int].get(i)` is a `!!Int`): unwrap it one level at a time with `!`, `try` or `catch`. See [`plum-examples/error_propagation.plum`](plum-examples/error_propagation.plum).
|
plum-std/Json.plum
CHANGED
|
@@ -8,16 +8,16 @@ import std/Str
|
|
|
8
8
|
import std/Buffer
|
|
9
9
|
|
|
10
10
|
# A parsed JSON value. Numbers are always stored as `Float` (JSON doesn't
|
|
11
|
-
# distinguish integers from floats the way Plum does), and `
|
|
11
|
+
# distinguish integers from floats the way Plum does), and `List[Json]`/
|
|
12
|
-
# `
|
|
12
|
+
# `Map[Str, Json]` nest `Json` itself — the same self-referential shape `List[T]`
|
|
13
13
|
# already uses internally for `Node[T].next: Link[Node[T]]`.
|
|
14
14
|
enum Json =
|
|
15
15
|
| JsonNull
|
|
16
|
-
|
|
|
16
|
+
| Bool
|
|
17
|
-
|
|
|
17
|
+
| Float
|
|
18
|
-
|
|
|
18
|
+
| Str
|
|
19
|
-
|
|
|
19
|
+
| List[Json]
|
|
20
|
-
|
|
|
20
|
+
| Map[Str, Json]
|
|
21
21
|
|
|
22
22
|
# Parses `s` as a single JSON document, requiring the whole string (aside
|
|
23
23
|
# from surrounding whitespace) to be consumed. Called as `Json.fromStr(s)`,
|
|
@@ -41,11 +41,11 @@ enum Json =
|
|
|
41
41
|
fun toStr(self) -> Str =
|
|
42
42
|
match self
|
|
43
43
|
JsonNull => "null"
|
|
44
|
-
|
|
44
|
+
Bool => if self then "true" else "false"
|
|
45
|
-
|
|
45
|
+
Float => self.toStr()
|
|
46
|
-
|
|
46
|
+
Str => "\"" + escapeJsonStr(self) + "\""
|
|
47
|
-
|
|
47
|
+
List => "[" + self.join(",") + "]"
|
|
48
|
-
|
|
48
|
+
Map => "{{" + jsonMapToStr(self) + "}}"
|
|
49
49
|
|
|
50
50
|
fun isNull(self) -> Bool =
|
|
51
51
|
match self
|
|
@@ -54,33 +54,35 @@ enum Json =
|
|
|
54
54
|
|
|
55
55
|
fun asBool(self) -> error{WrongJsonType}!Bool =
|
|
56
56
|
match self
|
|
57
|
-
|
|
57
|
+
Bool => self
|
|
58
58
|
_ => error.WrongJsonType
|
|
59
59
|
|
|
60
60
|
fun asFloat(self) -> error{WrongJsonType}!Float =
|
|
61
61
|
match self
|
|
62
|
-
|
|
62
|
+
Float => self
|
|
63
63
|
_ => error.WrongJsonType
|
|
64
64
|
|
|
65
65
|
fun asStr(self) -> error{WrongJsonType}!Str =
|
|
66
66
|
match self
|
|
67
|
-
|
|
67
|
+
Str => self
|
|
68
68
|
_ => error.WrongJsonType
|
|
69
69
|
|
|
70
70
|
fun asList(self) -> error{WrongJsonType}!List[Json] =
|
|
71
71
|
match self
|
|
72
|
-
|
|
72
|
+
List => self
|
|
73
73
|
_ => error.WrongJsonType
|
|
74
74
|
|
|
75
75
|
fun asMap(self) -> error{WrongJsonType}!Map[Str, Json] =
|
|
76
76
|
match self
|
|
77
|
-
|
|
77
|
+
Map => self
|
|
78
78
|
_ => error.WrongJsonType
|
|
79
79
|
|
|
80
80
|
# The outcome of `Json.parse`: the document, or where and why parsing failed.
|
|
81
|
-
enum JsonParse =
|
|
82
|
-
|
|
81
|
+
record JsonParsed(value: Json)
|
|
82
|
+
|
|
83
|
-
|
|
83
|
+
record JsonParseFailed(pos: Int, text: Str)
|
|
84
|
+
|
|
85
|
+
enum JsonParse = JsonParsed | JsonParseFailed
|
|
84
86
|
|
|
85
87
|
# A recursive-descent parser over a `Str` source, tracking its read position
|
|
86
88
|
# in `pos`. It reads directly from a `Str`, the same way every other stdlib
|
|
@@ -132,9 +134,9 @@ record JsonParser(src: Str, pos: Int, text: Str) =
|
|
|
132
134
|
if c == 123
|
|
133
135
|
return self.parseObject()
|
|
134
136
|
if c == 116
|
|
135
|
-
return self.parseLiteral("true",
|
|
137
|
+
return self.parseLiteral("true", true)
|
|
136
138
|
if c == 102
|
|
137
|
-
return self.parseLiteral("false",
|
|
139
|
+
return self.parseLiteral("false", false)
|
|
138
140
|
if c == 110
|
|
139
141
|
return self.parseLiteral("null", JsonNull)
|
|
140
142
|
if c == 45 || isDigit(c)
|
|
@@ -186,14 +188,14 @@ record JsonParser(src: Str, pos: Int, text: Str) =
|
|
|
186
188
|
f := Float.parse(text) catch e
|
|
187
189
|
self.note("invalid number")
|
|
188
190
|
return error.InvalidJson
|
|
189
|
-
return
|
|
191
|
+
return f
|
|
190
192
|
mv := Float.parse(text.sub(0, e_index)) catch e
|
|
191
193
|
self.note("invalid number")
|
|
192
194
|
return error.InvalidJson
|
|
193
195
|
ev := Int.parse(text.sub(e_index + 1, text.length())) catch e
|
|
194
196
|
self.note("invalid number")
|
|
195
197
|
return error.InvalidJson
|
|
196
|
-
return
|
|
198
|
+
return mv * 10.pow(Float(ev))
|
|
197
199
|
|
|
198
200
|
fun parseString(self) -> error{InvalidJson}!Json =
|
|
199
201
|
self.advance()
|
|
@@ -206,7 +208,7 @@ record JsonParser(src: Str, pos: Int, text: Str) =
|
|
|
206
208
|
return error.InvalidJson
|
|
207
209
|
if c == 34
|
|
208
210
|
self.advance()
|
|
209
|
-
return
|
|
211
|
+
return buf.toStr()
|
|
210
212
|
if c == 92
|
|
211
213
|
return self.parseEscape(buf)
|
|
212
214
|
n := self.src.codePointByteLength(self.pos)
|
|
@@ -295,7 +297,7 @@ record JsonParser(src: Str, pos: Int, text: Str) =
|
|
|
295
297
|
items := List[Json]()
|
|
296
298
|
if self.peek() == 93
|
|
297
299
|
self.advance()
|
|
298
|
-
return
|
|
300
|
+
return items
|
|
299
301
|
return self.parseArrayItems(items)
|
|
300
302
|
|
|
301
303
|
fun parseArrayItems(self, items: List[Json]) -> error{InvalidJson}!Json =
|
|
@@ -309,7 +311,7 @@ record JsonParser(src: Str, pos: Int, text: Str) =
|
|
|
309
311
|
return self.parseArrayItems(items)
|
|
310
312
|
if c == 93
|
|
311
313
|
self.advance()
|
|
312
|
-
return
|
|
314
|
+
return items
|
|
313
315
|
self.note("expected ',' or ']'")
|
|
314
316
|
return error.InvalidJson
|
|
315
317
|
|
|
@@ -319,7 +321,7 @@ record JsonParser(src: Str, pos: Int, text: Str) =
|
|
|
319
321
|
pairs := Map[Str, Json]()
|
|
320
322
|
if self.peek() == 125
|
|
321
323
|
self.advance()
|
|
322
|
-
return
|
|
324
|
+
return pairs
|
|
323
325
|
return self.parseObjectItems(pairs)
|
|
324
326
|
|
|
325
327
|
fun parseObjectItems(self, pairs: Map[Str, Json]) -> error{InvalidJson}!Json =
|
|
@@ -346,13 +348,13 @@ record JsonParser(src: Str, pos: Int, text: Str) =
|
|
|
346
348
|
return self.parseObjectItems(pairs)
|
|
347
349
|
if c == 125
|
|
348
350
|
self.advance()
|
|
349
|
-
return
|
|
351
|
+
return pairs
|
|
350
352
|
self.note("expected ',' or '}}'")
|
|
351
353
|
return error.InvalidJson
|
|
352
354
|
|
|
353
355
|
fun jsonStrValue(j: Json) -> Str =
|
|
354
356
|
match j
|
|
355
|
-
|
|
357
|
+
Str => j
|
|
356
358
|
_ => ""
|
|
357
359
|
|
|
358
360
|
# `Map` has no built-in `toStr`/`join` (unlike `List`, which uses `join` for
|
|
@@ -458,6 +460,8 @@ fun isDigit(c: Int) -> Bool =
|
|
|
458
460
|
|
|
459
461
|
# ---- regression tests ----
|
|
460
462
|
|
|
463
|
+
fun jsonOf(s: Str) -> Json = s
|
|
464
|
+
|
|
461
465
|
fun parseFloatValue(s: Str) -> Float =
|
|
462
466
|
v := Json.fromStr(s) catch e
|
|
463
467
|
return -1.0f
|
|
@@ -523,5 +527,5 @@ test "round-trips a value through toStr and back"
|
|
|
523
527
|
assert again.toStr() == v.toStr()
|
|
524
528
|
|
|
525
529
|
test "toStr escapes special characters in strings"
|
|
526
|
-
v :=
|
|
530
|
+
v := jsonOf("a\"b\\c\nd")
|
|
527
531
|
assert v.toStr() == "\"a\\\"b\\\\c\\nd\""
|