plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


plum-std/Json.plum
141de54 1
module std
141de54 2
b7071c9 3
import std/Option
b7071c9 4
import std/Result
b7071c9 5
import std/List
b7071c9 6
import std/Map
ca5fd6f 7
import std/Number
b7071c9 8
import std/Str
b7071c9 9
import std/Buffer
b7071c9 10
import std/Err
73b5e55 11
import std/Bool
5de508e 12
a43f3af 13
# A parsed JSON value. Numbers are always stored as `Float` (JSON doesn't
a43f3af 14
# distinguish integers from floats the way Plum does), and `JsonList`/
a43f3af 15
# `JsonMap` nest `Json` itself — the same self-referential shape `List[T]`
a43f3af 16
# already uses internally for `Node[T].next: Option[Node[T]]`.
5de508e 17
enum Json =
a43f3af 18
  | JsonNull
287b97c 19
  | JsonBool(Bool)
287b97c 20
  | JsonFloat(Float)
287b97c 21
  | JsonStr(Str)
287b97c 22
  | JsonList(List[Json])
287b97c 23
  | JsonMap(Map[Str, Json])
141de54 24
a43f3af 25
  # Parses `s` as a single JSON document, requiring the whole string (aside
a43f3af 26
  # from surrounding whitespace) to be consumed. Called as `Json.fromStr(s)`,
a43f3af 27
  # matching `Int.fromStr`/`Float.fromStr`'s naming.
a43f3af 28
  fun fromStr(s: Str) -> Result[Json, JsonParseError] =
a43f3af 29
    parser := JsonParser(src: s, pos: 0)
a43f3af 30
    parser.skipSpace()
a43f3af 31
    r := parser.parseValue()
a43f3af 32
    match r
a43f3af 33
      Err(e) =>
a43f3af 34
        return Err(e)
a43f3af 35
      Ok(v) =>
a43f3af 36
        parser.skipSpace()
a43f3af 37
        if parser.pos < parser.src.length()
a43f3af 38
          return Err(parser.fail("unexpected trailing characters"))
a43f3af 39
        return Ok(v)
a43f3af 40
a43f3af 41
  # Renders `self` back to its JSON text form. A literal `{`/`}` is written
a43f3af 42
  # doubled (`{{`/`}}`) — a single, un-doubled brace in a Plum string starts
a43f3af 43
  # or ends a `{expr}` interpolation instead of standing for itself.
a43f3af 44
  fun toStr(self) -> Str =
a43f3af 45
    match self
3a2119e 46
      JsonNull => "null"
3a2119e 47
      JsonBool(b) => b ? "true" : "false"
3a2119e 48
      JsonFloat(f) => f.toStr()
3a2119e 49
      JsonStr(s) => "\"" + escapeJsonStr(s) + "\""
3a2119e 50
      JsonList(items) => "[" + items.join(",") + "]"
3a2119e 51
      JsonMap(pairs) => "{{" + jsonMapToStr(pairs) + "}}"
a43f3af 52
a43f3af 53
  fun isNull(self) -> Bool =
a43f3af 54
    match self
3a2119e 55
      JsonNull => True
3a2119e 56
      _ => False
a43f3af 57
a43f3af 58
  fun asBool(self) -> Option[Bool] =
a43f3af 59
    match self
3a2119e 60
      JsonBool(b) => Some(b)
3a2119e 61
      _ => None
a43f3af 62
a43f3af 63
  fun asFloat(self) -> Option[Float] =
a43f3af 64
    match self
3a2119e 65
      JsonFloat(f) => Some(f)
3a2119e 66
      _ => None
a43f3af 67
a43f3af 68
  fun asStr(self) -> Option[Str] =
a43f3af 69
    match self
3a2119e 70
      JsonStr(s) => Some(s)
3a2119e 71
      _ => None
a43f3af 72
a43f3af 73
  fun asList(self) -> Option[List[Json]] =
a43f3af 74
    match self
3a2119e 75
      JsonList(items) => Some(items)
3a2119e 76
      _ => None
a43f3af 77
a43f3af 78
  fun asMap(self) -> Option[Map[Str, Json]] =
a43f3af 79
    match self
3a2119e 80
      JsonMap(pairs) => Some(pairs)
3a2119e 81
      _ => None
a43f3af 82
a43f3af 83
# Carries where in the source a parse failure happened, alongside the
a43f3af 84
# message — implements the shared `Err` trait (`libs/std/err.plum`) rather
a43f3af 85
# than being a plain `Str`, so a caller that wants the position can get it
a43f3af 86
# without re-parsing the message.
5f2f962 87
enum JsonParseError(Err) =
5f2f962 88
  | JsonParseError(pos: Int, text: Str)
a43f3af 89
a43f3af 90
  fun code(self) -> Int =
a43f3af 91
    1
a43f3af 92
a43f3af 93
  fun msg(self) -> Str =
a43f3af 94
    "json parse error at position {self.pos}: {self.text}"
a43f3af 95
a43f3af 96
  # A bare payload-free `None` here can't be disambiguated to one specific
a43f3af 97
  # `Option` instantiation from this expression alone (see the README's
a43f3af 98
  # "Known gaps" note on this residual generics limitation — unrelated to the
a43f3af 99
  # 3 points fixed above) — an explicit `Option[JsonParseError]` return type
a43f3af 100
  # sidesteps it.
a43f3af 101
  fun cause(self) -> Option[JsonParseError] =
a43f3af 102
    None
e502e58 103
a43f3af 104
# A recursive-descent parser over a `Str` source, tracking its read position
a43f3af 105
# in `pos`. (The original design threaded a `Readable` stream through here,
a43f3af 106
# but `Readable` is an undefined trait with no methods anywhere in `libs/std`
a43f3af 107
# — there is nothing to call on it — so this reads directly from a `Str`
a43f3af 108
# instead, the same way every other stdlib parser, e.g. `Int.fromStr`, does.)
5f2f962 109
enum JsonParser =
5f2f962 110
  | JsonParser(src: Str, pos: Int)
5de508e 111
a43f3af 112
  fun fail(self, text: Str) -> JsonParseError =
a43f3af 113
    JsonParseError(pos: self.pos, text: text)
a43f3af 114
a43f3af 115
  # The raw byte at the current position, or 0 (never a valid JSON byte) past
a43f3af 116
  # the end of `src` — lets every caller test for "end of input" the same way
a43f3af 117
  # it tests for any other byte, with no separate bounds check.
a43f3af 118
  fun peek(self) -> Int =
a43f3af 119
    self.pos >= self.src.length() ? 0 : self.src.byteAt(self.pos)
a43f3af 120
a43f3af 121
  fun peekAt(self, offset: Int) -> Int =
a43f3af 122
    i := self.pos + offset
a43f3af 123
    i >= self.src.length() ? 0 : self.src.byteAt(i)
a43f3af 124
a43f3af 125
  fun advance(self) -> Unit =
a43f3af 126
    self.pos = self.pos + 1
a43f3af 127
a43f3af 128
  fun skipSpace(self) -> Unit =
a43f3af 129
    while isSpace(self.peek())
a43f3af 130
      self.advance()
a43f3af 131
a43f3af 132
  fun parseValue(self) -> Result[Json, JsonParseError] =
a43f3af 133
    self.skipSpace()
a43f3af 134
    c := self.peek()
a43f3af 135
    if c == 34
a43f3af 136
      return self.parseString()
a43f3af 137
    if c == 91
a43f3af 138
      return self.parseArray()
a43f3af 139
    if c == 123
a43f3af 140
      return self.parseObject()
a43f3af 141
    if c == 116
a43f3af 142
      return self.parseLiteral("true", JsonBool(True))
a43f3af 143
    if c == 102
a43f3af 144
      return self.parseLiteral("false", JsonBool(False))
a43f3af 145
    if c == 110
a43f3af 146
      return self.parseLiteral("null", JsonNull)
a43f3af 147
    if c == 45 || isDigit(c)
a43f3af 148
      return self.parseNumber()
a43f3af 149
    return Err(self.fail("unexpected character"))
a43f3af 150
a43f3af 151
  fun parseLiteral(self, lit: Str, value: Json) -> Result[Json, JsonParseError] =
a43f3af 152
    n := lit.length()
a43f3af 153
    if !matchesAt(self.src, lit, self.pos, n)
a43f3af 154
      return Err(self.fail("invalid literal, expected '{lit}'"))
a43f3af 155
    self.pos = self.pos + n
a43f3af 156
    return Ok(value)
a43f3af 157
a43f3af 158
  fun parseNumber(self) -> Result[Json, JsonParseError] =
a43f3af 159
    start := self.pos
a43f3af 160
    if self.peek() == 45
a43f3af 161
      self.advance()
a43f3af 162
    if !isDigit(self.peek())
a43f3af 163
      return Err(self.fail("invalid number"))
a43f3af 164
    while isDigit(self.peek())
a43f3af 165
      self.advance()
a43f3af 166
    if self.peek() == 46
a43f3af 167
      self.advance()
a43f3af 168
      if !isDigit(self.peek())
a43f3af 169
        return Err(self.fail("invalid number"))
a43f3af 170
      while isDigit(self.peek())
a43f3af 171
        self.advance()
a43f3af 172
    if self.peek() == 101 || self.peek() == 69
a43f3af 173
      self.advance()
a43f3af 174
      if self.peek() == 43 || self.peek() == 45
a43f3af 175
        self.advance()
a43f3af 176
      if !isDigit(self.peek())
a43f3af 177
        return Err(self.fail("invalid number"))
a43f3af 178
      while isDigit(self.peek())
a43f3af 179
        self.advance()
a43f3af 180
    return self.parseNumberText(self.src.sub(start, self.pos))
a43f3af 181
a43f3af 182
  # `Float.fromStr` (`libs/std/float.plum`) doesn't understand exponent
a43f3af 183
  # notation, so a number that has one is split into its mantissa and
a43f3af 184
  # exponent around the `e`/`E` and recombined here instead.
a43f3af 185
  fun parseNumberText(self, text: Str) -> Result[Json, JsonParseError] =
a43f3af 186
    e_index := exponentIndex(text)
a43f3af 187
    if e_index < 0
ca5fd6f 188
      r := parseFloat(text)
a43f3af 189
      match r
a43f3af 190
        Ok(f) =>
a43f3af 191
          return Ok(JsonFloat(f))
a43f3af 192
        Err(m) =>
a43f3af 193
          return Err(self.fail(m))
ca5fd6f 194
    mantissa := parseFloat(text.sub(0, e_index))
ca5fd6f 195
    exponent := parseInt(text.sub(e_index + 1, text.length()))
a43f3af 196
    match mantissa
a43f3af 197
      Err(m) =>
a43f3af 198
        return Err(self.fail(m))
a43f3af 199
      Ok(mv) =>
a43f3af 200
        match exponent
a43f3af 201
          Err(m) =>
a43f3af 202
            return Err(self.fail(m))
a43f3af 203
          Ok(ev) =>
a43f3af 204
            return Ok(JsonFloat(mv * 10.pow(Float(ev))))
a43f3af 205
a43f3af 206
  fun parseString(self) -> Result[Json, JsonParseError] =
a43f3af 207
    self.advance()
4a2384c 208
    return self.parseStringBody(Buffer())
a43f3af 209
a43f3af 210
  fun parseStringBody(self, buf: Buffer) -> Result[Json, JsonParseError] =
a43f3af 211
    c := self.peek()
a43f3af 212
    if c == 0
a43f3af 213
      return Err(self.fail("unterminated string"))
a43f3af 214
    if c == 34
a43f3af 215
      self.advance()
a43f3af 216
      return Ok(JsonStr(buf.toStr()))
a43f3af 217
    if c == 92
a43f3af 218
      return self.parseEscape(buf)
a43f3af 219
    n := self.src.codePointByteLength(self.pos)
a43f3af 220
    buf.write(self.src.sub(self.pos, self.pos + n))
a43f3af 221
    self.pos = self.pos + n
a43f3af 222
    return self.parseStringBody(buf)
141de54 223
a43f3af 224
  fun parseEscape(self, buf: Buffer) -> Result[Json, JsonParseError] =
a43f3af 225
    self.advance()
a43f3af 226
    esc := self.peek()
a43f3af 227
    if esc == 34
a43f3af 228
      buf.write("\"")
a43f3af 229
      self.advance()
a43f3af 230
      return self.parseStringBody(buf)
a43f3af 231
    if esc == 92
a43f3af 232
      buf.write("\\")
a43f3af 233
      self.advance()
a43f3af 234
      return self.parseStringBody(buf)
a43f3af 235
    if esc == 47
a43f3af 236
      buf.write("/")
a43f3af 237
      self.advance()
a43f3af 238
      return self.parseStringBody(buf)
a43f3af 239
    if esc == 98
a43f3af 240
      buf.write("\b")
a43f3af 241
      self.advance()
a43f3af 242
      return self.parseStringBody(buf)
a43f3af 243
    if esc == 102
a43f3af 244
      buf.write("\f")
a43f3af 245
      self.advance()
a43f3af 246
      return self.parseStringBody(buf)
a43f3af 247
    if esc == 110
a43f3af 248
      buf.write("\n")
a43f3af 249
      self.advance()
a43f3af 250
      return self.parseStringBody(buf)
a43f3af 251
    if esc == 114
a43f3af 252
      buf.write("\r")
a43f3af 253
      self.advance()
a43f3af 254
      return self.parseStringBody(buf)
a43f3af 255
    if esc == 116
a43f3af 256
      buf.write("\t")
a43f3af 257
      self.advance()
a43f3af 258
      return self.parseStringBody(buf)
a43f3af 259
    if esc == 117
a43f3af 260
      self.advance()
a43f3af 261
      return self.parseUnicodeEscape(buf)
a43f3af 262
    return Err(self.fail("invalid escape character"))
5de508e 263
a43f3af 264
  fun parseHex4(self) -> Result[Int, JsonParseError] =
a43f3af 265
    return self.parseHex4From(0, 0)
5de508e 266
a43f3af 267
  fun parseHex4From(self, value: Int, count: Int) -> Result[Int, JsonParseError] =
a43f3af 268
    if count == 4
a43f3af 269
      return Ok(value)
a43f3af 270
    d := hexDigitValue(self.peek())
a43f3af 271
    if d < 0
a43f3af 272
      return Err(self.fail("invalid unicode escape"))
a43f3af 273
    self.advance()
a43f3af 274
    return self.parseHex4From(value * 16 + d, count + 1)
5de508e 275
a43f3af 276
  fun parseUnicodeEscape(self, buf: Buffer) -> Result[Json, JsonParseError] =
a43f3af 277
    hr := self.parseHex4()
a43f3af 278
    match hr
a43f3af 279
      Err(e) =>
a43f3af 280
        return Err(e)
a43f3af 281
      Ok(cp) =>
a43f3af 282
        return self.parseUnicodeEscapeCont(buf, cp)
5de508e 283
a43f3af 284
  # A high surrogate (`0xD800`..`0xDBFF`) must be followed immediately by a
a43f3af 285
  # low surrogate (`0xDC00`..`0xDFFF`) `\u` escape; the pair together encodes
a43f3af 286
  # one codepoint above `0xFFFF`. Any other codepoint is used as-is.
a43f3af 287
  fun parseUnicodeEscapeCont(self, buf: Buffer, cp: Int) -> Result[Json, JsonParseError] =
a43f3af 288
    if cp >= 0xD800 && cp <= 0xDBFF && self.peek() == 92 && self.peekAt(1) == 117
a43f3af 289
      self.advance()
a43f3af 290
      self.advance()
a43f3af 291
      hr := self.parseHex4()
a43f3af 292
      match hr
a43f3af 293
        Err(e) =>
a43f3af 294
          return Err(e)
a43f3af 295
        Ok(low) =>
a43f3af 296
          if low < 0xDC00 || low > 0xDFFF
a43f3af 297
            return Err(self.fail("invalid low surrogate"))
a43f3af 298
          combined := 0x10000 + {{cp - 0xD800} << 10} + {low - 0xDC00}
a43f3af 299
          buf.writeRune(combined)
a43f3af 300
          return self.parseStringBody(buf)
a43f3af 301
    buf.writeRune(cp)
a43f3af 302
    return self.parseStringBody(buf)
5de508e 303
a43f3af 304
  fun parseArray(self) -> Result[Json, JsonParseError] =
a43f3af 305
    self.advance()
a43f3af 306
    self.skipSpace()
a43f3af 307
    items := List[Json](head: None, tail: None, size: 0)
a43f3af 308
    if self.peek() == 93
a43f3af 309
      self.advance()
a43f3af 310
      return Ok(JsonList(items))
a43f3af 311
    return self.parseArrayItems(items)
5de508e 312
a43f3af 313
  fun parseArrayItems(self, items: List[Json]) -> Result[Json, JsonParseError] =
a43f3af 314
    self.skipSpace()
a43f3af 315
    r := self.parseValue()
a43f3af 316
    match r
a43f3af 317
      Err(e) =>
a43f3af 318
        return Err(e)
a43f3af 319
      Ok(v) =>
a43f3af 320
        items.add(v)
a43f3af 321
        self.skipSpace()
a43f3af 322
        c := self.peek()
a43f3af 323
        if c == 44
a43f3af 324
          self.advance()
a43f3af 325
          return self.parseArrayItems(items)
a43f3af 326
        if c == 93
a43f3af 327
          self.advance()
a43f3af 328
          return Ok(JsonList(items))
a43f3af 329
        return Err(self.fail("expected ',' or ']'"))
5de508e 330
a43f3af 331
  fun parseObject(self) -> Result[Json, JsonParseError] =
a43f3af 332
    self.advance()
a43f3af 333
    self.skipSpace()
ea9b8c1 334
    pairs := Map[Str, Json]()
a43f3af 335
    if self.peek() == 125
a43f3af 336
      self.advance()
a43f3af 337
      return Ok(JsonMap(pairs))
a43f3af 338
    return self.parseObjectItems(pairs)
5de508e 339
a43f3af 340
  fun parseObjectItems(self, pairs: Map[Str, Json]) -> Result[Json, JsonParseError] =
a43f3af 341
    self.skipSpace()
a43f3af 342
    if self.peek() != 34
a43f3af 343
      return Err(self.fail("expected string key"))
a43f3af 344
    kr := self.parseString()
a43f3af 345
    match kr
a43f3af 346
      Err(e) =>
a43f3af 347
        return Err(e)
a43f3af 348
      Ok(keyJson) =>
a43f3af 349
        return self.parseObjectValue(pairs, jsonStrValue(keyJson))
a43f3af 350
a43f3af 351
  fun parseObjectValue(self, pairs: Map[Str, Json], key: Str) -> Result[Json, JsonParseError] =
a43f3af 352
    self.skipSpace()
a43f3af 353
    if self.peek() != 58
a43f3af 354
      return Err(self.fail("expected ':'"))
a43f3af 355
    self.advance()
a43f3af 356
    self.skipSpace()
a43f3af 357
    vr := self.parseValue()
a43f3af 358
    match vr
a43f3af 359
      Err(e) =>
a43f3af 360
        return Err(e)
a43f3af 361
      Ok(v) =>
a43f3af 362
        pairs.set(key, v)
a43f3af 363
        self.skipSpace()
a43f3af 364
        c := self.peek()
a43f3af 365
        if c == 44
a43f3af 366
          self.advance()
a43f3af 367
          return self.parseObjectItems(pairs)
a43f3af 368
        if c == 125
a43f3af 369
          self.advance()
a43f3af 370
          return Ok(JsonMap(pairs))
a43f3af 371
        return Err(self.fail("expected ',' or '}}'"))
a43f3af 372
a43f3af 373
fun jsonStrValue(j: Json) -> Str =
a43f3af 374
  match j
3a2119e 375
    JsonStr(s) => s
3a2119e 376
    _ => ""
a43f3af 377
a43f3af 378
# `Map` has no built-in `toStr`/`join` (unlike `List`, which uses `join` for
a43f3af 379
# `Json.toStr`'s `JsonList` case above) — a plain free function taking the
a43f3af 380
# `Buffer` as a captured-by-reference parameter instead of a closure sidesteps
a43f3af 381
# `libs/std/list.plum`'s `join` note on closures: reassigning a CAPTURED
a43f3af 382
# variable inside a closure body doesn't persist across that closure's own
a43f3af 383
# repeated calls, but mutating a heap object's fields THROUGH a captured
a43f3af 384
# reference (as `appendJsonPair` does to `buf`) does.
a43f3af 385
fun jsonMapToStr(pairs: Map[Str, Json]) -> Str =
4a2384c 386
  buf := Buffer()
a43f3af 387
  pairs.each(|k, v| appendJsonPair(buf, k, v))
a43f3af 388
  return buf.toStr()
a43f3af 389
a43f3af 390
fun appendJsonPair(buf: Buffer, k: Str, v: Json) -> Unit =
a43f3af 391
  if !buf.isEmpty()
a43f3af 392
    buf.write(",")
a43f3af 393
  buf.write("\"")
a43f3af 394
  buf.write(escapeJsonStr(k))
a43f3af 395
  buf.write("\":")
a43f3af 396
  buf.write(v.toStr())
a43f3af 397
a43f3af 398
fun exponentIndex(text: Str) -> Int =
a43f3af 399
  e := text.indexOf("e")
a43f3af 400
  e >= 0 ? e : text.indexOf("E")
a43f3af 401
a43f3af 402
fun escapeJsonStr(s: Str) -> Str =
a43f3af 403
  escapeJsonFrom(s, 0, s.length())
a43f3af 404
a43f3af 405
# Byte-at-a-time except for the pass-through case, which copies a whole
a43f3af 406
# UTF-8 codepoint at once — copying a lone continuation byte as its own
a43f3af 407
# "character" (as a naive byte-by-byte pass-through would) would corrupt any
a43f3af 408
# non-ASCII text, the same hazard documented on `Str`'s own `escapeChar` in
a43f3af 409
# `libs/std/str.plum`.
a43f3af 410
fun escapeJsonFrom(s: Str, i: Int, len: Int) -> Str =
a43f3af 411
  if i >= len
a43f3af 412
    return ""
a43f3af 413
  b := s.byteAt(i)
a43f3af 414
  if b == 34
a43f3af 415
    return "\\\"" + escapeJsonFrom(s, i + 1, len)
a43f3af 416
  if b == 92
a43f3af 417
    return "\\\\" + escapeJsonFrom(s, i + 1, len)
a43f3af 418
  if b == 10
a43f3af 419
    return "\\n" + escapeJsonFrom(s, i + 1, len)
a43f3af 420
  if b == 13
a43f3af 421
    return "\\r" + escapeJsonFrom(s, i + 1, len)
a43f3af 422
  if b == 9
a43f3af 423
    return "\\t" + escapeJsonFrom(s, i + 1, len)
a43f3af 424
  if b == 8
a43f3af 425
    return "\\b" + escapeJsonFrom(s, i + 1, len)
a43f3af 426
  if b == 12
a43f3af 427
    return "\\f" + escapeJsonFrom(s, i + 1, len)
a43f3af 428
  if b < 0x20
a43f3af 429
    return "\\u00" + hexByteStr(b) + escapeJsonFrom(s, i + 1, len)
a43f3af 430
  n := s.codePointByteLength(i)
a43f3af 431
  return s.sub(i, i + n) + escapeJsonFrom(s, i + n, len)
a43f3af 432
a43f3af 433
fun hexByteStr(b: Int) -> Str =
a43f3af 434
  hexDigitChar(b / 16) + hexDigitChar(b % 16)
a43f3af 435
a43f3af 436
fun hexDigitChar(d: Int) -> Str =
a43f3af 437
  match d
a43f3af 438
    0 => "0"
a43f3af 439
    1 => "1"
a43f3af 440
    2 => "2"
a43f3af 441
    3 => "3"
a43f3af 442
    4 => "4"
a43f3af 443
    5 => "5"
a43f3af 444
    6 => "6"
a43f3af 445
    7 => "7"
a43f3af 446
    8 => "8"
a43f3af 447
    9 => "9"
a43f3af 448
    10 => "a"
a43f3af 449
    11 => "b"
a43f3af 450
    12 => "c"
a43f3af 451
    13 => "d"
a43f3af 452
    14 => "e"
a43f3af 453
    15 => "f"
a43f3af 454
    _ => "0"
a43f3af 455
a43f3af 456
fun hexDigitValue(b: Int) -> Int =
a43f3af 457
  if b >= 48 && b <= 57
a43f3af 458
    return b - 48
a43f3af 459
  if b >= 97 && b <= 102
a43f3af 460
    return b - 97 + 10
a43f3af 461
  if b >= 65 && b <= 70
a43f3af 462
    return b - 65 + 10
a43f3af 463
  return -1
5de508e 464
0fe3528 465
fun isSpace(c: Int) -> Bool =
141de54 466
  c == 32 || c >= 9 && c <= 13
5de508e 467
0fe3528 468
fun isDigit(c: Int) -> Bool =
141de54 469
  c >= 48 && c <= 57
a43f3af 470
a43f3af 471
# ---- regression tests ----
a43f3af 472
a43f3af 473
fun parseFloatValue(s: Str) -> Float =
a43f3af 474
  r := Json.fromStr(s)
a43f3af 475
  match r
3a2119e 476
    Ok(v) => v.asFloat().unwrapOr(-1.0f)
3a2119e 477
    Err(_) => -1.0f
a43f3af 478
a43f3af 479
test "parses null, true, and false"
a9a0147 480
  assert Json.fromStr("null").unwrap().isNull()
a9a0147 481
  assert Json.fromStr("true").unwrap().asBool().unwrap()
a9a0147 482
  assert !Json.fromStr("false").unwrap().asBool().unwrap()
a43f3af 483
a43f3af 484
test "parses integers, decimals, negatives, and exponents"
a9a0147 485
  assert parseFloatValue("42") == 42.0f
a9a0147 486
  assert parseFloatValue("-7") == -7.0f
a9a0147 487
  assert parseFloatValue("3.5") == 3.5f
a9a0147 488
  assert parseFloatValue("1e2") == 100.0f
a9a0147 489
  assert parseFloatValue("1.5e-2") == 0.015f
a43f3af 490
a43f3af 491
test "parses a plain string, with escapes"
a9a0147 492
  assert Json.fromStr("\"hello\"").unwrap().asStr().unwrap() == "hello"
a9a0147 493
  assert Json.fromStr("\"a\\nb\"").unwrap().asStr().unwrap() == "a\nb"
a9a0147 494
  assert Json.fromStr("\"\\u0041\"").unwrap().asStr().unwrap() == "A"
a43f3af 495
a43f3af 496
test "parses an array of mixed values"
a43f3af 497
  v := Json.fromStr("[1, \"two\", true, null]").unwrap()
a43f3af 498
  items := v.asList().unwrap()
a9a0147 499
  assert items.length() == 4
a9a0147 500
  assert items.get(0).unwrap().asFloat().unwrap() == 1.0f
a9a0147 501
  assert items.get(1).unwrap().asStr().unwrap() == "two"
a9a0147 502
  assert items.get(2).unwrap().asBool().unwrap()
a9a0147 503
  assert items.get(3).unwrap().isNull()
a43f3af 504
a43f3af 505
test "parses a nested object"
a43f3af 506
  v := Json.fromStr("{{\"a\": 1, \"b\": {{\"c\": [1, 2, 3]}}}}").unwrap()
a43f3af 507
  pairs := v.asMap().unwrap()
a9a0147 508
  assert pairs.get("a").unwrap().asFloat().unwrap() == 1.0f
a43f3af 509
  nested := pairs.get("b").unwrap().asMap().unwrap()
a43f3af 510
  inner := nested.get("c").unwrap().asList().unwrap()
a9a0147 511
  assert inner.length() == 3
a9a0147 512
  assert inner.get(2).unwrap().asFloat().unwrap() == 3.0f
a43f3af 513
a43f3af 514
test "reports an error on malformed input"
a43f3af 515
  r := Json.fromStr("{{\"a\": }}")
a9a0147 516
  assert r.isErr()
a43f3af 517
a43f3af 518
test "round-trips a value through toStr and back"
a43f3af 519
  original := "[1,\"two\",true,null,{{\"k\":3.5}}]"
a43f3af 520
  v := Json.fromStr(original).unwrap()
a43f3af 521
  again := Json.fromStr(v.toStr()).unwrap()
a9a0147 522
  assert again.toStr() == v.toStr()
a43f3af 523
a43f3af 524
test "toStr escapes special characters in strings"
a43f3af 525
  v := JsonStr("a\"b\\c\nd")
a9a0147 526
  assert v.toStr() == "\"a\\\"b\\\\c\\nd\""