plum

#treesitter#compiler#wasm

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
Files changed (2) hide show
  1. README.md +121 -50
  2. 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 sum type; payload fields are named
24
+ enum Shape = # a union: a Circle or a Rect
22
- | Circle(r: Float)
25
+ | Circle
23
- | Rect(w: Float, h: Float)
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 sum type / record | `enum Shape = \| Circle(r: Float) \| Rect(w: Float, h: Float)` |
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 variant 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`:
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 sum type: a value that is one of several variants. `struct` and `record` are product types: one variant named like the type, with named fields. A `struct` is a value type and a `record` is a reference type.
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
- ### Methods, `self` and field access
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
- A `fun` declared indented directly inside an `enum` body is a method; its first parameter is `self`:
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
- enum Cat =
421
- | Cat(name: Str, age: Int)
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
- | Wrapper(inner: Cat, tag: Int)
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 named field of any enum can be read with `.field`, even on a multi-variant enum (the field must have a unique owning variant; a checked downcast does the access).
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 record-shaped enum value by calling its name with `field: value` pairs (any order; every field required), or positionally:
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 maybe
538
+ match shape
467
- Just(v) => v
539
+ Circle(r) => r
468
- Nothing => 0
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 variant, or an error name when matching an error value, also written `error.Name`; compared), a constructor pattern (`Just(v)`, binding its argument, possibly nested), 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).
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(r: Float)
558
+ | Circle
483
- | Rect(w: Float, h: Float)
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 have one variant named like the type:
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
- | Box(value: T)
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 `enum Box[T]`. Generic types, methods, and free functions are duck-typed and compiled once per distinct instantiation. See [How generics compile](#how-generics-compile).
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
- | Num(add: fn(T, T) -> T, zero: T)
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
- | Person(name: Str, age: Int)
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
- | Target(name: Str)
862
+ record Target(name: Str)
791
863
 
792
- enum User =
793
- | User(name: Str, target: Target?)
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(Str)
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 `JsonList`/
11
+ # distinguish integers from floats the way Plum does), and `List[Json]`/
12
- # `JsonMap` nest `Json` itself — the same self-referential shape `List[T]`
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
- | JsonBool(Bool)
16
+ | Bool
17
- | JsonFloat(Float)
17
+ | Float
18
- | JsonStr(Str)
18
+ | Str
19
- | JsonList(List[Json])
19
+ | List[Json]
20
- | JsonMap(Map[Str, Json])
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
- JsonBool(b) => if b then "true" else "false"
44
+ Bool => if self then "true" else "false"
45
- JsonFloat(f) => f.toStr()
45
+ Float => self.toStr()
46
- JsonStr(s) => "\"" + escapeJsonStr(s) + "\""
46
+ Str => "\"" + escapeJsonStr(self) + "\""
47
- JsonList(items) => "[" + items.join(",") + "]"
47
+ List => "[" + self.join(",") + "]"
48
- JsonMap(pairs) => "{{" + jsonMapToStr(pairs) + "}}"
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
- JsonBool(b) => b
57
+ Bool => self
58
58
  _ => error.WrongJsonType
59
59
 
60
60
  fun asFloat(self) -> error{WrongJsonType}!Float =
61
61
  match self
62
- JsonFloat(f) => f
62
+ Float => self
63
63
  _ => error.WrongJsonType
64
64
 
65
65
  fun asStr(self) -> error{WrongJsonType}!Str =
66
66
  match self
67
- JsonStr(s) => s
67
+ Str => self
68
68
  _ => error.WrongJsonType
69
69
 
70
70
  fun asList(self) -> error{WrongJsonType}!List[Json] =
71
71
  match self
72
- JsonList(items) => items
72
+ List => self
73
73
  _ => error.WrongJsonType
74
74
 
75
75
  fun asMap(self) -> error{WrongJsonType}!Map[Str, Json] =
76
76
  match self
77
- JsonMap(pairs) => pairs
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
- | JsonParsed(value: Json)
81
+ record JsonParsed(value: Json)
82
+
83
- | JsonParseFailed(pos: Int, text: Str)
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", JsonBool(true))
137
+ return self.parseLiteral("true", true)
136
138
  if c == 102
137
- return self.parseLiteral("false", JsonBool(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 JsonFloat(f)
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 JsonFloat(mv * 10.pow(Float(ev)))
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 JsonStr(buf.toStr())
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 JsonList(items)
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 JsonList(items)
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 JsonMap(pairs)
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 JsonMap(pairs)
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
- JsonStr(s) => s
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 := JsonStr("a\"b\\c\nd")
530
+ v := jsonOf("a\"b\\c\nd")
527
531
  assert v.toStr() == "\"a\\\"b\\\\c\\nd\""