plum

#treesitter#compiler#wasm

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

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


dc1369f — Peter John 2026-11-01T21:39:34+05:30
perf: Json parser/escaper loops instead of per-char recursion; parseString returns Str
Files changed (1) hide show
  1. plum-std/Json.plum +166 -146
plum-std/Json.plum CHANGED
@@ -128,7 +128,8 @@ record JsonParser(src: Str, pos: Int, text: Str) =
128
128
  self.skipSpace()
129
129
  c := self.peek()
130
130
  if c == 34
131
- return self.parseString()
131
+ str := try self.parseString()
132
+ return str
132
133
  if c == 91
133
134
  return self.parseArray()
134
135
  if c == 123
@@ -197,87 +198,73 @@ record JsonParser(src: Str, pos: Int, text: Str) =
197
198
  return error.InvalidJson
198
199
  return mv * 10.pow(Float(ev))
199
200
 
200
- fun parseString(self) -> error{InvalidJson}!Json =
201
+ fun parseString(self) -> error{InvalidJson}!Str =
201
202
  self.advance()
202
- return self.parseStringBody(Buffer())
203
+ buf := Buffer()
203
-
204
- fun parseStringBody(self, buf: Buffer) -> error{InvalidJson}!Json =
204
+ while true
205
- c := self.peek()
205
+ c := self.peek()
206
- if c == 0
206
+ if c == 0
207
- self.note("unterminated string")
207
+ self.note("unterminated string")
208
- return error.InvalidJson
208
+ return error.InvalidJson
209
- if c == 34
209
+ if c == 34
210
- self.advance()
210
+ self.advance()
211
- return buf.toStr()
211
+ return buf.toStr()
212
- if c == 92
212
+ if c == 92
213
- return self.parseEscape(buf)
213
+ try self.parseEscape(buf)
214
+ else
214
- n := self.src.codePointByteLength(self.pos)
215
+ n := self.src.codePointByteLength(self.pos)
215
- buf.write(self.src.sub(self.pos, self.pos + n))
216
+ buf.write(self.src.sub(self.pos, self.pos + n))
216
- self.pos = self.pos + n
217
+ self.pos = self.pos + n
217
- return self.parseStringBody(buf)
218
+ return buf.toStr()
219
+
218
-
220
+ # Handles one escape sequence (the cursor is on the backslash), writing the
221
+ # decoded text into `buf`.
219
- fun parseEscape(self, buf: Buffer) -> error{InvalidJson}!Json =
222
+ fun parseEscape(self, buf: Buffer) -> error{InvalidJson}!Unit =
220
223
  self.advance()
221
224
  esc := self.peek()
225
+ if esc == 117
226
+ self.advance()
227
+ return self.parseUnicodeEscape(buf)
222
228
  if esc == 34
223
229
  buf.write("\"")
224
- self.advance()
225
- return self.parseStringBody(buf)
226
- if esc == 92
230
+ else if esc == 92
227
231
  buf.write("\\")
228
- self.advance()
229
- return self.parseStringBody(buf)
230
- if esc == 47
232
+ else if esc == 47
231
233
  buf.write("/")
232
- self.advance()
233
- return self.parseStringBody(buf)
234
- if esc == 98
234
+ else if esc == 98
235
235
  buf.write("\b")
236
- self.advance()
237
- return self.parseStringBody(buf)
238
- if esc == 102
236
+ else if esc == 102
239
237
  buf.write("\f")
240
- self.advance()
241
- return self.parseStringBody(buf)
242
- if esc == 110
238
+ else if esc == 110
243
239
  buf.write("\n")
244
- self.advance()
245
- return self.parseStringBody(buf)
246
- if esc == 114
240
+ else if esc == 114
247
241
  buf.write("\r")
248
- self.advance()
249
- return self.parseStringBody(buf)
250
- if esc == 116
242
+ else if esc == 116
251
243
  buf.write("\t")
252
- self.advance()
244
+ else
253
- return self.parseStringBody(buf)
254
- if esc == 117
255
- self.advance()
256
- return self.parseUnicodeEscape(buf)
257
- self.note("invalid escape character")
245
+ self.note("invalid escape character")
258
- return error.InvalidJson
259
-
260
- fun parseHex4(self) -> error{InvalidJson}!Int =
261
- return self.parseHex4From(0, 0)
262
-
263
- fun parseHex4From(self, value: Int, count: Int) -> error{InvalidJson}!Int =
264
- if count == 4
265
- return value
266
- d := hexDigitValue(self.peek())
267
- if d < 0
268
- self.note("invalid unicode escape")
269
246
  return error.InvalidJson
270
247
  self.advance()
271
- return self.parseHex4From(value * 16 + d, count + 1)
272
248
 
273
- fun parseUnicodeEscape(self, buf: Buffer) -> error{InvalidJson}!Json =
249
+ fun parseHex4(self) -> error{InvalidJson}!Int =
250
+ value := 0
251
+ count := 0
252
+ while count < 4
274
- cp := try self.parseHex4()
253
+ d := hexDigitValue(self.peek())
254
+ if d < 0
275
- return self.parseUnicodeEscapeCont(buf, cp)
255
+ self.note("invalid unicode escape")
256
+ return error.InvalidJson
257
+ self.advance()
258
+ value = value * 16 + d
259
+ count = count + 1
260
+ return value
276
261
 
277
- # A high surrogate (`0xD800`..`0xDBFF`) must be followed immediately by a
262
+ # Decodes the four hex digits after `\u` (and, for a high surrogate
278
- # low surrogate (`0xDC00`..`0xDFFF`) `\u` escape; the pair together encodes
263
+ # (`0xD800`..`0xDBFF`), the `\u` low surrogate (`0xDC00`..`0xDFFF`) that must
264
+ # follow immediately; the pair encodes one codepoint above `0xFFFF`). Any
279
- # one codepoint above `0xFFFF`. Any other codepoint is used as-is.
265
+ # other codepoint is used as-is.
280
- fun parseUnicodeEscapeCont(self, buf: Buffer, cp: Int) -> error{InvalidJson}!Json =
266
+ fun parseUnicodeEscape(self, buf: Buffer) -> error{InvalidJson}!Unit =
267
+ cp := try self.parseHex4()
281
268
  if cp >= 0xD800 && cp <= 0xDBFF && self.peek() == 92 && self.peekAt(1) == 117
282
269
  self.advance()
283
270
  self.advance()
@@ -287,9 +274,8 @@ record JsonParser(src: Str, pos: Int, text: Str) =
287
274
  return error.InvalidJson
288
275
  combined := 0x10000 + {{cp - 0xD800} << 10} + {low - 0xDC00}
289
276
  buf.writeRune(combined)
290
- return self.parseStringBody(buf)
277
+ return
291
278
  buf.writeRune(cp)
292
- return self.parseStringBody(buf)
293
279
 
294
280
  fun parseArray(self) -> error{InvalidJson}!Json =
295
281
  self.advance()
@@ -298,22 +284,20 @@ record JsonParser(src: Str, pos: Int, text: Str) =
298
284
  if self.peek() == 93
299
285
  self.advance()
300
286
  return items
301
- return self.parseArrayItems(items)
287
+ while true
302
-
303
- fun parseArrayItems(self, items: List[Json]) -> error{InvalidJson}!Json =
304
- self.skipSpace()
288
+ self.skipSpace()
305
- v := try self.parseValue()
289
+ v := try self.parseValue()
306
- items.add(v)
290
+ items.add(v)
307
- self.skipSpace()
291
+ self.skipSpace()
308
- c := self.peek()
292
+ c := self.peek()
309
- if c == 44
293
+ if c == 93
310
- self.advance()
294
+ self.advance()
311
- return self.parseArrayItems(items)
295
+ return items
312
- if c == 93
296
+ if c != 44
297
+ self.note("expected ',' or ']'")
298
+ return error.InvalidJson
313
299
  self.advance()
314
- return items
300
+ return items
315
- self.note("expected ',' or ']'")
316
- return error.InvalidJson
317
301
 
318
302
  fun parseObject(self) -> error{InvalidJson}!Json =
319
303
  self.advance()
@@ -322,40 +306,30 @@ record JsonParser(src: Str, pos: Int, text: Str) =
322
306
  if self.peek() == 125
323
307
  self.advance()
324
308
  return pairs
325
- return self.parseObjectItems(pairs)
309
+ while true
326
-
327
- fun parseObjectItems(self, pairs: Map[Str, Json]) -> error{InvalidJson}!Json =
328
- self.skipSpace()
310
+ self.skipSpace()
329
- if self.peek() != 34
311
+ if self.peek() != 34
330
- self.note("expected string key")
312
+ self.note("expected string key")
331
- return error.InvalidJson
313
+ return error.InvalidJson
332
- keyJson := try self.parseString()
314
+ key := try self.parseString()
333
- return self.parseObjectValue(pairs, jsonStrValue(keyJson))
334
-
335
- fun parseObjectValue(self, pairs: Map[Str, Json], key: Str) -> error{InvalidJson}!Json =
336
- self.skipSpace()
315
+ self.skipSpace()
337
- if self.peek() != 58
316
+ if self.peek() != 58
338
- self.note("expected ':'")
317
+ self.note("expected ':'")
339
- return error.InvalidJson
318
+ return error.InvalidJson
340
- self.advance()
341
- self.skipSpace()
342
- v := try self.parseValue()
343
- pairs.set(key, v)
344
- self.skipSpace()
345
- c := self.peek()
346
- if c == 44
347
319
  self.advance()
320
+ self.skipSpace()
348
- return self.parseObjectItems(pairs)
321
+ v := try self.parseValue()
322
+ pairs.set(key, v)
323
+ self.skipSpace()
324
+ c := self.peek()
349
- if c == 125
325
+ if c == 125
326
+ self.advance()
327
+ return pairs
328
+ if c != 44
329
+ self.note("expected ',' or '}}'")
330
+ return error.InvalidJson
350
331
  self.advance()
351
- return pairs
332
+ return pairs
352
- self.note("expected ',' or '}}'")
353
- return error.InvalidJson
354
-
355
- fun jsonStrValue(j: Json) -> Str =
356
- match j
357
- Str => j
358
- _ => ""
359
333
 
360
334
  # `Map` has no built-in `toStr`/`join` (unlike `List`, which uses `join` for
361
335
  # `Json.toStr`'s `JsonList` case above) — a plain free function taking the
@@ -390,35 +364,36 @@ fun exponentIndex(text: Str) -> Int =
390
364
  if e >= 0 then e else text.indexOf("E")
391
365
 
392
366
  fun escapeJsonStr(s: Str) -> Str =
367
+ buf := Buffer()
393
- escapeJsonFrom(s, 0, s.length())
368
+ len := s.length()
394
-
395
- # Byte-at-a-time except for the pass-through case, which copies a whole
396
- # UTF-8 codepoint at once — copying a lone continuation byte as its own
397
- # "character" (as a naive byte-by-byte pass-through would) would corrupt any
398
- # non-ASCII text, the same hazard documented on `Str`'s own `escapeChar` in
399
- # `libs/std/str.plum`.
369
+ i := 0
400
- fun escapeJsonFrom(s: Str, i: Int, len: Int) -> Str =
401
- if i >= len
370
+ while i < len
402
- return ""
403
- b := s.byteAt(i)
371
+ b := s.byteAt(i)
404
- if b == 34
372
+ if b == 34
405
- return "\\\"" + escapeJsonFrom(s, i + 1, len)
373
+ buf.write("\\\"")
406
- if b == 92
374
+ else if b == 92
407
- return "\\\\" + escapeJsonFrom(s, i + 1, len)
375
+ buf.write("\\\\")
408
- if b == 10
376
+ else if b == 10
409
- return "\\n" + escapeJsonFrom(s, i + 1, len)
377
+ buf.write("\\n")
410
- if b == 13
378
+ else if b == 13
411
- return "\\r" + escapeJsonFrom(s, i + 1, len)
379
+ buf.write("\\r")
412
- if b == 9
380
+ else if b == 9
413
- return "\\t" + escapeJsonFrom(s, i + 1, len)
381
+ buf.write("\\t")
414
- if b == 8
382
+ else if b == 8
415
- return "\\b" + escapeJsonFrom(s, i + 1, len)
383
+ buf.write("\\b")
416
- if b == 12
384
+ else if b == 12
417
- return "\\f" + escapeJsonFrom(s, i + 1, len)
385
+ buf.write("\\f")
418
- if b < 0x20
386
+ else if b < 0x20
387
+ buf.write("\\u00")
388
+ buf.write(hexByteStr(b))
389
+ else
390
+ # Copies a whole UTF-8 codepoint at once: copying a lone continuation
419
- return "\\u00" + hexByteStr(b) + escapeJsonFrom(s, i + 1, len)
391
+ # byte as its own "character" would corrupt any non-ASCII text.
420
- n := s.codePointByteLength(i)
392
+ n := s.codePointByteLength(i)
421
- return s.sub(i, i + n) + escapeJsonFrom(s, i + n, len)
393
+ buf.write(s.sub(i, i + n))
394
+ i = i + n - 1
395
+ i = i + 1
396
+ return buf.toStr()
422
397
 
423
398
  fun hexByteStr(b: Int) -> Str =
424
399
  hexDigitChar(b / 16) + hexDigitChar(b % 16)
@@ -460,7 +435,7 @@ fun isDigit(c: Int) -> Bool =
460
435
 
461
436
  # ---- regression tests ----
462
437
 
463
- fun jsonOf(s: Str) -> Json = s
438
+
464
439
 
465
440
  fun parseFloatValue(s: Str) -> Float =
466
441
  v := Json.fromStr(s) catch e
@@ -527,5 +502,50 @@ test "round-trips a value through toStr and back"
527
502
  assert again.toStr() == v.toStr()
528
503
 
529
504
  test "toStr escapes special characters in strings"
530
- v := jsonOf("a\"b\\c\nd")
505
+ v: Json := "a\"b\\c\nd"
531
506
  assert v.toStr() == "\"a\\\"b\\\\c\\nd\""
507
+
508
+ test "round-trips a 100000 character string"
509
+ buf := Buffer()
510
+ i := 0
511
+ while i < 100000
512
+ buf.write("a")
513
+ i = i + 1
514
+ doc := "\"" + buf.toStr() + "\""
515
+ v := Json.fromStr(doc)!
516
+ assert v.asStr()!.length() == 100000
517
+ assert v.toStr().length() == 100002
518
+
519
+ test "parses a 10000 element array"
520
+ buf := Buffer()
521
+ buf.write("[0")
522
+ i := 1
523
+ while i < 10000
524
+ buf.write(",{i}")
525
+ i = i + 1
526
+ buf.write("]")
527
+ items := Json.fromStr(buf.toStr())!.asList()!
528
+ assert items.length() == 10000
529
+ assert items.get(9999)!.asFloat()! == 9999.0f
530
+
531
+ test "parses an object with 2000 keys"
532
+ buf := Buffer()
533
+ buf.write("{{\"k0\":0")
534
+ i := 1
535
+ while i < 2000
536
+ buf.write(",\"k{i}\":{i}")
537
+ i = i + 1
538
+ buf.write("}}")
539
+ pairs := Json.fromStr(buf.toStr())!.asMap()!
540
+ assert pairs.get("k1999")!.asFloat()! == 1999.0f
541
+ assert pairs.get("k0")!.asFloat()! == 0.0f
542
+
543
+ test "escapes a 50000 character string of quotes and newlines"
544
+ buf := Buffer()
545
+ i := 0
546
+ while i < 25000
547
+ buf.write("\"\n")
548
+ i = i + 1
549
+ j: Json := buf.toStr()
550
+ out := j.toStr()
551
+ assert out.length() == 100002