@grain/stdlib 0.5.13 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/LICENSE +1 -1
  3. package/README.md +25 -2
  4. package/array.gr +1512 -199
  5. package/array.md +2032 -94
  6. package/bigint.gr +239 -140
  7. package/bigint.md +450 -106
  8. package/buffer.gr +595 -102
  9. package/buffer.md +903 -145
  10. package/bytes.gr +401 -110
  11. package/bytes.md +551 -63
  12. package/char.gr +228 -49
  13. package/char.md +373 -7
  14. package/exception.gr +26 -12
  15. package/exception.md +29 -5
  16. package/float32.gr +130 -109
  17. package/float32.md +185 -57
  18. package/float64.gr +112 -99
  19. package/float64.md +185 -57
  20. package/hash.gr +47 -37
  21. package/hash.md +21 -3
  22. package/int16.gr +430 -0
  23. package/int16.md +618 -0
  24. package/int32.gr +200 -269
  25. package/int32.md +254 -289
  26. package/int64.gr +142 -225
  27. package/int64.md +254 -289
  28. package/int8.gr +511 -0
  29. package/int8.md +786 -0
  30. package/json.gr +2084 -0
  31. package/json.md +608 -0
  32. package/list.gr +120 -68
  33. package/list.md +125 -80
  34. package/map.gr +560 -57
  35. package/map.md +672 -56
  36. package/marshal.gr +239 -227
  37. package/marshal.md +36 -4
  38. package/number.gr +626 -676
  39. package/number.md +738 -153
  40. package/option.gr +33 -35
  41. package/option.md +58 -42
  42. package/package.json +2 -2
  43. package/path.gr +148 -187
  44. package/path.md +47 -96
  45. package/pervasives.gr +75 -416
  46. package/pervasives.md +85 -180
  47. package/priorityqueue.gr +433 -74
  48. package/priorityqueue.md +422 -54
  49. package/queue.gr +362 -80
  50. package/queue.md +433 -38
  51. package/random.gr +67 -75
  52. package/random.md +68 -40
  53. package/range.gr +135 -63
  54. package/range.md +198 -43
  55. package/rational.gr +284 -0
  56. package/rational.md +545 -0
  57. package/regex.gr +933 -1066
  58. package/regex.md +59 -60
  59. package/result.gr +23 -25
  60. package/result.md +54 -39
  61. package/runtime/atof/common.gr +78 -82
  62. package/runtime/atof/common.md +22 -10
  63. package/runtime/atof/decimal.gr +102 -127
  64. package/runtime/atof/decimal.md +28 -7
  65. package/runtime/atof/lemire.gr +56 -71
  66. package/runtime/atof/lemire.md +9 -1
  67. package/runtime/atof/parse.gr +83 -110
  68. package/runtime/atof/parse.md +12 -2
  69. package/runtime/atof/slow.gr +28 -35
  70. package/runtime/atof/slow.md +9 -1
  71. package/runtime/atof/table.gr +19 -18
  72. package/runtime/atof/table.md +10 -2
  73. package/runtime/atoi/parse.gr +153 -136
  74. package/runtime/atoi/parse.md +50 -1
  75. package/runtime/bigint.gr +410 -517
  76. package/runtime/bigint.md +71 -57
  77. package/runtime/compare.gr +176 -85
  78. package/runtime/compare.md +31 -1
  79. package/runtime/dataStructures.gr +144 -32
  80. package/runtime/dataStructures.md +267 -31
  81. package/runtime/debugPrint.gr +34 -15
  82. package/runtime/debugPrint.md +37 -5
  83. package/runtime/equal.gr +53 -52
  84. package/runtime/equal.md +30 -1
  85. package/runtime/exception.gr +38 -47
  86. package/runtime/exception.md +10 -8
  87. package/runtime/gc.gr +23 -152
  88. package/runtime/gc.md +13 -17
  89. package/runtime/malloc.gr +31 -31
  90. package/runtime/malloc.md +11 -3
  91. package/runtime/numberUtils.gr +191 -172
  92. package/runtime/numberUtils.md +17 -9
  93. package/runtime/numbers.gr +1695 -1021
  94. package/runtime/numbers.md +1098 -134
  95. package/runtime/string.gr +540 -242
  96. package/runtime/string.md +76 -6
  97. package/runtime/unsafe/constants.gr +30 -13
  98. package/runtime/unsafe/constants.md +80 -0
  99. package/runtime/unsafe/conv.gr +55 -28
  100. package/runtime/unsafe/conv.md +41 -9
  101. package/runtime/unsafe/memory.gr +10 -30
  102. package/runtime/unsafe/memory.md +15 -19
  103. package/runtime/unsafe/tags.gr +37 -21
  104. package/runtime/unsafe/tags.md +88 -8
  105. package/runtime/unsafe/wasmf32.gr +30 -36
  106. package/runtime/unsafe/wasmf32.md +64 -56
  107. package/runtime/unsafe/wasmf64.gr +30 -36
  108. package/runtime/unsafe/wasmf64.md +64 -56
  109. package/runtime/unsafe/wasmi32.gr +49 -66
  110. package/runtime/unsafe/wasmi32.md +102 -94
  111. package/runtime/unsafe/wasmi64.gr +52 -79
  112. package/runtime/unsafe/wasmi64.md +108 -100
  113. package/runtime/utils/printing.gr +13 -15
  114. package/runtime/utils/printing.md +11 -3
  115. package/runtime/wasi.gr +294 -295
  116. package/runtime/wasi.md +62 -42
  117. package/set.gr +574 -64
  118. package/set.md +634 -54
  119. package/stack.gr +181 -64
  120. package/stack.md +271 -42
  121. package/string.gr +453 -533
  122. package/string.md +241 -151
  123. package/uint16.gr +369 -0
  124. package/uint16.md +585 -0
  125. package/uint32.gr +470 -0
  126. package/uint32.md +737 -0
  127. package/uint64.gr +471 -0
  128. package/uint64.md +737 -0
  129. package/uint8.gr +369 -0
  130. package/uint8.md +585 -0
  131. package/uri.gr +1093 -0
  132. package/uri.md +477 -0
  133. package/{sys → wasi}/file.gr +914 -500
  134. package/{sys → wasi}/file.md +454 -50
  135. package/wasi/process.gr +292 -0
  136. package/{sys → wasi}/process.md +164 -6
  137. package/wasi/random.gr +77 -0
  138. package/wasi/random.md +80 -0
  139. package/{sys → wasi}/time.gr +15 -22
  140. package/{sys → wasi}/time.md +5 -5
  141. package/immutablearray.gr +0 -929
  142. package/immutablearray.md +0 -1038
  143. package/immutablemap.gr +0 -493
  144. package/immutablemap.md +0 -479
  145. package/immutablepriorityqueue.gr +0 -360
  146. package/immutablepriorityqueue.md +0 -291
  147. package/immutableset.gr +0 -498
  148. package/immutableset.md +0 -449
  149. package/runtime/debug.gr +0 -2
  150. package/runtime/debug.md +0 -6
  151. package/runtime/unsafe/errors.gr +0 -36
  152. package/runtime/unsafe/errors.md +0 -204
  153. package/sys/process.gr +0 -254
  154. package/sys/random.gr +0 -79
  155. package/sys/random.md +0 -66
package/json.gr ADDED
@@ -0,0 +1,2084 @@
1
+ /**
2
+ * JSON (JavaScript Object Notation) parsing, printing, and access utilities.
3
+ *
4
+ * @example from "json" include Json
5
+ * @example Json.parse("{\"currency\":\"€\",\"price\":99.99}")
6
+ * @example
7
+ * print(
8
+ * toString(
9
+ * format=Pretty,
10
+ * JsonObject([("currency", JsonString("€")), ("price", JsonNumber(99.9))])
11
+ * )
12
+ * )
13
+ */
14
+ module Json
15
+
16
+ from "runtime/bigint" include Bigint as BI
17
+ from "runtime/dataStructures" include DataStructures
18
+ from "runtime/numbers" include Numbers
19
+ from "runtime/numberUtils" include NumberUtils
20
+ from "runtime/string" include String as RuntimeString
21
+ from "runtime/unsafe/tags" include Tags
22
+ from "runtime/unsafe/wasmi32" include WasmI32
23
+ from "runtime/unsafe/wasmi64" include WasmI64
24
+ from "runtime/unsafe/wasmf64" include WasmF64
25
+ from "runtime/atof/parse" include Parse as Atof
26
+ from "buffer" include Buffer
27
+ from "char" include Char
28
+ from "string" include String
29
+ from "list" include List
30
+ from "uint8" include Uint8
31
+ use RuntimeString.{ toString as runtimeToString, getCodePoint }
32
+ use Numbers.{ coerceNumberToWasmI32 }
33
+ use DataStructures.{ tagSimpleNumber, untagSimpleNumber }
34
+
35
+ // Primitive offsets
36
+ // TODO(#703): Get these offsets from the runtime
37
+ @unsafe
38
+ let _INT64_BOXED_VALUE_OFFSET = 8n
39
+ @unsafe
40
+ let _Float64_BOXED_VALUE_OFFSET = 8n
41
+
42
+ /**
43
+ * Data structure representing JSON in Grain.
44
+ *
45
+ * @example
46
+ * assert Json.parse("{\"currency\":\"€\",\"price\":99.99}") == JsonObject([
47
+ * ("currency", JsonString("€")),
48
+ * ("price", JsonNumber(99.99)),
49
+ * ])
50
+ *
51
+ * @example
52
+ * assert Json.parse("{\n\"currency\":\"€\",\n\"price\":99.99\n}") == JsonObject([
53
+ * ("currency", JsonString("€")),
54
+ * ("price", JsonNumber(99.99)),
55
+ * ])
56
+ */
57
+ provide enum rec Json {
58
+ JsonNull,
59
+ JsonBoolean(Bool),
60
+ JsonNumber(Number),
61
+ JsonString(String),
62
+ JsonArray(List<Json>),
63
+ // Note that JsonObject here is deliberately defined as a simple list of key value pair tuples as opposed
64
+ // to for example a Map in order to accommodate the fact that the ECMA-404 standard doesn't prohibit
65
+ // duplicate names in Objects. Such JSON should be representable by the JSON data structure for lossless
66
+ // processing. This also simplifies implementation by not requiring a purpose built data structure and
67
+ // has the benefit of List's immutability. It's a conscious decision that sacrifices ease of use of the
68
+ // API for lossless handing of these edge cases with intention of later building more ergonomic APIs on a
69
+ // higher level of abstraction.
70
+ JsonObject(List<(String, Json)>),
71
+ }
72
+
73
+ /**
74
+ * Represents errors for cases where a `Json` data structure cannot be represented as a
75
+ * JSON string.
76
+ */
77
+ provide enum JsonToStringError {
78
+ /**
79
+ * The `Json` data structure contains a number value of `NaN`, `Infinity`, or `-Infinity`.
80
+ */
81
+ InvalidNumber(String),
82
+ }
83
+
84
+ /**
85
+ * Controls how indentation is output in custom formatting.
86
+ */
87
+ provide enum IndentationFormat {
88
+ /**
89
+ * No indentation is emitted.
90
+ *
91
+ * ```json
92
+ * {
93
+ * "currency": "€",
94
+ * "price": 99.9
95
+ * }
96
+ * ```
97
+ */
98
+ NoIndentation,
99
+ /**
100
+ * Tabs are emitted.
101
+ *
102
+ * ```json
103
+ * {
104
+ * "currency": "€",
105
+ * "price": 99.9
106
+ * }
107
+ * ```
108
+ */
109
+ IndentWithTab,
110
+ /**
111
+ * The desired number of spaces are emitted.
112
+ *
113
+ * `IndentWithSpaces(2)`
114
+ * ```json
115
+ * {
116
+ * "currency": "€",
117
+ * "price": 99.9
118
+ * }
119
+ * ```
120
+ *
121
+ * `IndentWithSpaces(4)`
122
+ * ```json
123
+ * {
124
+ * "currency": "€",
125
+ * "price": 99.9
126
+ * }
127
+ * ```
128
+ */
129
+ IndentWithSpaces(Number),
130
+ }
131
+
132
+ /**
133
+ * Controls how arrays are output in custom formatting.
134
+ */
135
+ provide enum ArrayFormat {
136
+ /**
137
+ * Arrays are emitted in a compact manner.
138
+ *
139
+ * ```json
140
+ * []
141
+ * ```
142
+ *
143
+ * ```json
144
+ * [1]
145
+ * ```
146
+ *
147
+ * ```json
148
+ * [1,2,3]
149
+ * ```
150
+ */
151
+ CompactArrayEntries,
152
+ /**
153
+ * Arrays are emitted with spaces between elements.
154
+ *
155
+ * ```json
156
+ * [ ]
157
+ * ```
158
+ *
159
+ * ```json
160
+ * [1]
161
+ * ```
162
+ *
163
+ * ```json
164
+ * [1, 2, 3]
165
+ * ```
166
+ */
167
+ SpacedArrayEntries,
168
+ /**
169
+ * Arrays are emitted with newlines and indentation between each element.
170
+ *
171
+ * ```json
172
+ * []
173
+ * ```
174
+ *
175
+ * ```json
176
+ * [
177
+ * 1
178
+ * ]
179
+ * ```
180
+ *
181
+ * ```json
182
+ * [
183
+ * 1,
184
+ * 2,
185
+ * 3
186
+ * ]
187
+ * ```
188
+ */
189
+ OneArrayEntryPerLine,
190
+ }
191
+
192
+ /**
193
+ * Controls how objects are output in custom formatting.
194
+ */
195
+ provide enum ObjectFormat {
196
+ /**
197
+ * Objects are emitted in a compact manner.
198
+ *
199
+ * ```json
200
+ * {}
201
+ * ```
202
+ *
203
+ * ```json
204
+ * {"a":1}
205
+ * ```
206
+ *
207
+ * ```json
208
+ * {"a":1,"b":2,"c":3}
209
+ * ```
210
+ */
211
+ CompactObjectEntries,
212
+ /**
213
+ * Objects are emitted with spaces between entries.
214
+ *
215
+ * ```json
216
+ * { }
217
+ * ```
218
+ *
219
+ * ```json
220
+ * {"a": 1}
221
+ * ```
222
+ *
223
+ * ```json
224
+ * {"a": 1, "b": 2, "c": 3}
225
+ * ```
226
+ */
227
+ SpacedObjectEntries,
228
+ /**
229
+ * Objects are emitted with each entry on a new line.
230
+ *
231
+ * ```
232
+ * {}
233
+ * ```
234
+ *
235
+ * ```
236
+ * {
237
+ * "a": 1
238
+ * }
239
+ * ```
240
+ *
241
+ * ```
242
+ * {
243
+ * "a": 1,
244
+ * "b": 2,
245
+ * "c": 3
246
+ * }
247
+ * ```
248
+ */
249
+ OneObjectEntryPerLine,
250
+ }
251
+
252
+ /**
253
+ * Controls how line endings are output in custom formatting.
254
+ */
255
+ provide enum LineEnding {
256
+ /**
257
+ * No line endings will be emitted.
258
+ */
259
+ NoLineEnding,
260
+ /**
261
+ * A `\n` will be emitted at the end of each line.
262
+ */
263
+ LineFeed,
264
+ /**
265
+ * A `\r\n` will be emitted at the end of each line.
266
+ */
267
+ CarriageReturnLineFeed,
268
+ /**
269
+ * A `\r` will be emitted at the end of each line.
270
+ */
271
+ CarriageReturn,
272
+ }
273
+
274
+ /*
275
+ * Allows fine-grained control of formatting in JSON output.
276
+ */
277
+ record FormattingSettings {
278
+ indentation: IndentationFormat,
279
+ arrayFormat: ArrayFormat,
280
+ objectFormat: ObjectFormat,
281
+ lineEnding: LineEnding,
282
+ finishWithNewLine: Bool,
283
+ escapeAllControlPoints: Bool,
284
+ escapeHTMLUnsafeSequences: Bool,
285
+ escapeNonASCII: Bool,
286
+ }
287
+
288
+ /**
289
+ * Allows control of formatting in JSON output.
290
+ */
291
+ provide enum FormattingChoices {
292
+ /**
293
+ * Recommended human readable formatting.
294
+ *
295
+ * Escapes all control points for the sake of clarity, but outputs unicode
296
+ * codepoints directly so the result needs to be treated as proper unicode and
297
+ * is not safe to be transported in ASCII encoding.
298
+ *
299
+ * Roughly Equivalent to:
300
+ * ```grain
301
+ * Custom{
302
+ * indentation: IndentWithSpaces(2),
303
+ * arrayFormat: OneArrayEntryPerLine,
304
+ * objectFormat: OneObjectEntryPerLine,
305
+ * lineEnding: LineFeed,
306
+ * finishWithNewLine: true,
307
+ * escapeAllControlPoints: true,
308
+ * escapeHTMLUnsafeSequences: false,
309
+ * escapeNonASCII: false,
310
+ * }
311
+ * ```
312
+ *
313
+ * ```json
314
+ * {
315
+ * "currency": "€",
316
+ * "price": 99.9,
317
+ * "currencyDescription": "EURO\u007f",
318
+ * }
319
+ * ```
320
+ */
321
+ Pretty,
322
+ /**
323
+ * Compact formatting that minimizes the size of resulting JSON at cost of not
324
+ * being easily human readable.
325
+ *
326
+ * Only performs minimal string escaping as required by the ECMA-404 standard,
327
+ * so the result needs to be treated as proper unicode and is not safe to be
328
+ * transported in ASCII encoding.
329
+ *
330
+ * Roughly Equivalent to:
331
+ * ```grain
332
+ * Custom{
333
+ * indentation: NoIndentation,
334
+ * arrayFormat: CompactArrayEntries,
335
+ * objectFormat: CompactObjectEntries,
336
+ * lineEnding: NoLineEnding,
337
+ * finishWithNewLine: false,
338
+ * escapeAllControlPoints: false,
339
+ * escapeHTMLUnsafeSequences: false,
340
+ * escapeNonASCII: false,
341
+ * }
342
+ * ```
343
+ *
344
+ * ```json
345
+ * {"currency":"€","price":99.9,"currencyDescription":"EURO␡"}
346
+ * ```
347
+ */
348
+ Compact,
349
+ /**
350
+ * Pretty and conservative formatting to maximize compatibility and
351
+ * embeddability of the resulting JSON.
352
+ *
353
+ * Should be safe to copy and paste directly into HTML and to be transported in
354
+ * plain ASCII.
355
+ *
356
+ * Roughly Equivalent to:
357
+ * ```grain
358
+ * Custom{
359
+ * indentation: IndentWithSpaces(2),
360
+ * arrayFormat: OneArrayEntryPerLine,
361
+ * objectFormat: OneObjectEntryPerLine,
362
+ * lineEnding: LineFeed,
363
+ * finishWithNewLine: true,
364
+ * escapeAllControlPoints: true,
365
+ * escapeHTMLUnsafeSequences: true,
366
+ * escapeNonASCII: true,
367
+ * }
368
+ * ```
369
+ *
370
+ * ```json
371
+ * {
372
+ * "currency": "\u20ac",
373
+ * "price": 99.9,
374
+ * "currencyDescription": "EURO\u007f",
375
+ * }
376
+ * ```
377
+ */
378
+ PrettyAndSafe,
379
+ /**
380
+ * Compact and conservative formatting to maximize compatibility and
381
+ * embeddability of the resulting JSON.
382
+ *
383
+ * Should be safe to copy and paste directly into HTML and to transported in
384
+ * plain ASCII.
385
+ *
386
+ * Roughly Equivalent to:
387
+ * ```grain
388
+ * Custom{
389
+ * indentation: NoIndentation,
390
+ * arrayFormat: CompactArrayEntries,
391
+ * objectFormat: CompactObjectEntries,
392
+ * lineEnding: NoLineEnding,
393
+ * finishWithNewLine: false,
394
+ * escapeAllControlPoints: true,
395
+ * escapeHTMLUnsafeSequences: true,
396
+ * escapeNonASCII: true,
397
+ * }
398
+ * ```
399
+ *
400
+ * ```json
401
+ * {"currency":"\u20ac","price":99.9,"currencyDescription":"EURO\u007f"}
402
+ * ```
403
+ */
404
+ CompactAndSafe,
405
+ /**
406
+ * Allows for fined grained control of the formatting output.
407
+ */
408
+ Custom{
409
+ indentation: IndentationFormat,
410
+ arrayFormat: ArrayFormat,
411
+ objectFormat: ObjectFormat,
412
+ lineEnding: LineEnding,
413
+ finishWithNewLine: Bool,
414
+ escapeAllControlPoints: Bool,
415
+ escapeHTMLUnsafeSequences: Bool,
416
+ escapeNonASCII: Bool,
417
+ },
418
+ }
419
+
420
+ record JsonWriterConfig {
421
+ format: FormattingSettings,
422
+ buffer: Buffer.Buffer,
423
+ emitEscapedQuotedString: String => Void,
424
+ printNewLine: Option<() => Void>,
425
+ printIndentation: Option<Number => Void>,
426
+ }
427
+
428
+ // The idea for this type is to allow reusing a bit of work done in preparing for printing JSON.
429
+ // For now this is not exposed and remains an internal implementation detail.
430
+ // It may make sense in the future to expose it and let the user reuse a writer for multiple
431
+ // JSON emit operations without reallocating new closures and buffers each time.
432
+ record JsonWriter {
433
+ emit: Json => Option<JsonToStringError>,
434
+ }
435
+
436
+ let emitUTF16EscapeSequence = (codePoint: Number, buffer: Buffer.Buffer) => {
437
+ // Emit the "\u" followed by hexadecimal representation of the codepoint
438
+ // with fixed length of 4 hexadecimal digits corresponding to the two byte
439
+ // codepoint. No checks are performed here if the codepoint is in the
440
+ // "Basic Multilingual Plane" (0000-FFFF) as this function is only called
441
+ // internally.
442
+ // An alternative was to this implementation was to use NumberUtils.itoa32,
443
+ // but this avoids unnecessary heap allocations. As a possible future
444
+ // optimization this loop could be unrolled possibly even converted to be
445
+ // branchless and SIMD optimized, but it could be a bit of an overkill as
446
+ // this codepath is only for escape sequences, which probably aren't all
447
+ // that common occurrence.
448
+
449
+ Buffer.addChar('\\', buffer)
450
+ Buffer.addChar('u', buffer)
451
+ // Loop over the four digit from most to least significant.
452
+ for (let mut digitIndex = 3; digitIndex >= 0; digitIndex -= 1) {
453
+ // Use bit masking and shifting to extract from the codepoint a number
454
+ // with just the bits corresponding to this hexadecimal digit.
455
+ let shift = digitIndex * 4
456
+ let mask = 0b1111 << shift
457
+ let digit = (codePoint & mask) >>> shift
458
+
459
+ // Digit now is a number in the range 0..15 and we need to translate it
460
+ // into a unicode codepoint representing the hexadecimal digit
461
+ // (0..9/a..f). We can use the fact that digits and latin letters in
462
+ // ASCII and by extension in Unicode are adjacent and ordered.
463
+ let hexDigitCodePoint = if (digit <= 9) {
464
+ // 48 is codepoint for char '0'
465
+ digit + 48
466
+ } else {
467
+ // 97 is codepoint for char 'a'
468
+ // But we also need to subtract 10 from it because we need
469
+ // the 10..15 number range translated to 0..5 in order to
470
+ // serve as an index in the ASCI range 'a'..'f'.
471
+ digit + 87
472
+ }
473
+
474
+ Buffer.addCharFromCodePoint(hexDigitCodePoint, buffer)
475
+ }
476
+ }
477
+
478
+ let emitEscapedUnicodeSequence = (codePoint: Number, buffer: Buffer.Buffer) => {
479
+ // See the String section in the ECMA-404 doc.
480
+ // If the code point is "in the Basic Multilingual Plane", that is in range
481
+ // 0..65535. Greater values need to be split into two UTF-16 chunks.
482
+ if (codePoint <= 0xFFFF) {
483
+ emitUTF16EscapeSequence(codePoint, buffer)
484
+ } else {
485
+ // The following three lines are copied from String module of Grain's
486
+ // stdlib. It would be nice to share more code. On the other hand it
487
+ // may make sense to just have these few instructions directly here
488
+ // from the performance standpoint so we can print millions of emojis
489
+ // per second 😄.
490
+
491
+ // https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
492
+ let uPrime = codePoint - 0x10000
493
+ let highSurrogate = ((uPrime & 0b11111111110000000000) >>> 10) + 0xD800
494
+ // High surrogate
495
+ let lowSurrogate = (uPrime & 0b00000000001111111111) + 0xDC00
496
+ // Low surrogate
497
+
498
+ emitUTF16EscapeSequence(highSurrogate, buffer)
499
+ emitUTF16EscapeSequence(lowSurrogate, buffer)
500
+ }
501
+ }
502
+
503
+ let emitEscapedCodePoint = (codePoint: Number, buffer: Buffer.Buffer) => {
504
+ match (codePoint) {
505
+ 0x0008 => { // backspace
506
+ Buffer.addChar('\\', buffer)
507
+ Buffer.addChar('b', buffer)
508
+ },
509
+ 0x0009 => { // tab
510
+ Buffer.addChar('\\', buffer)
511
+ Buffer.addChar('t', buffer)
512
+ },
513
+ 0x000A => { // line feed
514
+ Buffer.addChar('\\', buffer)
515
+ Buffer.addChar('n', buffer)
516
+ },
517
+ 0x000C => { // form feed
518
+ Buffer.addChar('\\', buffer)
519
+ Buffer.addChar('f', buffer)
520
+ },
521
+ 0x000D => { // carriage return
522
+ Buffer.addChar('\\', buffer)
523
+ Buffer.addChar('r', buffer)
524
+ },
525
+ 0x0022 => { // quotation mark
526
+ Buffer.addChar('\\', buffer)
527
+ Buffer.addChar('"', buffer)
528
+ },
529
+ 0x005C => { // backslash or "Reverse Solidus"
530
+ Buffer.addChar('\\', buffer)
531
+ Buffer.addChar('\\', buffer)
532
+ },
533
+ _ => {
534
+ emitEscapedUnicodeSequence(codePoint, buffer)
535
+ },
536
+ }
537
+ }
538
+
539
+ let printNull = (buffer: Buffer.Buffer) => Buffer.addString("null", buffer)
540
+
541
+ let printBool = (b: Bool, buffer: Buffer.Buffer) => {
542
+ if (b) {
543
+ Buffer.addString("true", buffer)
544
+ } else {
545
+ Buffer.addString("false", buffer)
546
+ }
547
+ }
548
+
549
+ @unsafe
550
+ let printNumberWasmI32 = (value: WasmI32, buffer: Buffer.Buffer) => {
551
+ let s = NumberUtils.itoa32(value, 10n)
552
+ Buffer.addString(s, buffer)
553
+ }
554
+
555
+ @unsafe
556
+ let printNumberWasmI64 = (value: WasmI64, buffer: Buffer.Buffer) => {
557
+ let s = NumberUtils.itoa64(value, 10n)
558
+ Buffer.addString(s, buffer)
559
+ }
560
+
561
+ @unsafe
562
+ let isFinite = (value: WasmF64) => {
563
+ use WasmF64.{ (==), (-) }
564
+ value - value == 0.0W
565
+ }
566
+
567
+ @unsafe
568
+ let isNaN = (value: WasmF64) => {
569
+ use WasmF64.{ (!=) }
570
+ value != value
571
+ }
572
+
573
+ @unsafe
574
+ let printNumberWasmF64 = (value: WasmF64, buffer: Buffer.Buffer) => {
575
+ if (isFinite(value)) {
576
+ let s = NumberUtils.dtoa(value)
577
+ Buffer.addString(s, buffer)
578
+ None
579
+ } else {
580
+ use WasmF64.{ (<) }
581
+ // JSON standard doesn't allow NaN or infinite values in numbers,
582
+ // but WASM f64 (IEEE 754-2008), as well as Grain's number types do
583
+ // (Float64 as well as Number). This is the only reason that the
584
+ // formatting needs to return a Result and not just a String
585
+ // directly. Other possible choices were to throw exceptions or to
586
+ // continue formatting without representing these values correctly
587
+ // (like JavaScript's JSON.stringify).
588
+ if (isNaN(value)) {
589
+ Some(InvalidNumber("NaN is not allowed in JsonNumber"))
590
+ } else if (value < 0.0W) {
591
+ Some(InvalidNumber("-Infinity is not allowed in JsonNumber"))
592
+ } else {
593
+ Some(InvalidNumber("Infinity is not allowed in JsonNumber"))
594
+ }
595
+ }
596
+ }
597
+
598
+ @unsafe
599
+ let printNumber = (value: Number, buffer: Buffer.Buffer) => {
600
+ use WasmI32.{ (&), (==), (!=), (<<), (>>) }
601
+
602
+ let ptr = WasmI32.fromGrain(value)
603
+ let ret = if ((ptr & 1n) != 0n) {
604
+ printNumberWasmI32(untagSimpleNumber(value), buffer)
605
+ None
606
+ } else if ((ptr & 7n) == Tags._GRAIN_GENERIC_HEAP_TAG_TYPE) {
607
+ let tag = WasmI32.load(ptr, 0n)
608
+ match (tag) {
609
+ t when t == Tags._GRAIN_BOXED_NUM_HEAP_TAG => {
610
+ let numberTag = WasmI32.load(ptr, 4n)
611
+ match (numberTag) {
612
+ t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
613
+ let asWasmI64 = WasmI64.load(ptr, _INT64_BOXED_VALUE_OFFSET)
614
+ printNumberWasmI64(asWasmI64, buffer)
615
+ None
616
+ },
617
+ t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
618
+ Buffer.addString(BI.bigIntToString10(ptr), buffer)
619
+ None
620
+ },
621
+ t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
622
+ // JSON does not support rationals as a compromise
623
+ // we coerce them to an f64 and print that
624
+ // this means there is a slight loss in precision
625
+ let asFloat64 = Numbers.coerceNumberToFloat64(value)
626
+ let ptr = WasmI32.fromGrain(asFloat64)
627
+ let asWasmF64 = WasmF64.load(ptr, _Float64_BOXED_VALUE_OFFSET)
628
+ printNumberWasmF64(asWasmF64, buffer)
629
+ },
630
+ t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
631
+ let asWasmF64 = WasmF64.load(ptr, _Float64_BOXED_VALUE_OFFSET)
632
+ printNumberWasmF64(asWasmF64, buffer)
633
+ },
634
+ _ => {
635
+ fail "Impossible: Json.toString encountered an unknown number tag"
636
+ },
637
+ }
638
+ },
639
+ _ => {
640
+ fail "Impossible: Json.toString encountered an unknown number tag"
641
+ },
642
+ }
643
+ } else {
644
+ fail "Impossible: Json.toString encountered an unknown number tag"
645
+ }
646
+ // This keeps the gc from prematurely freeing the value
647
+ ignore(value)
648
+ ret
649
+ }
650
+
651
+ // Note that this compromises on peak performance by also handling
652
+ // the compact printing case, merging these two together greatly simplifies the amount
653
+ // of code we need to maintain so it seems worth it.
654
+ let rec printElement = (
655
+ json: Json,
656
+ implHelper: JsonWriterConfig,
657
+ indentationLevel: Number,
658
+ ) => {
659
+ let buffer = implHelper.buffer
660
+ match (json) {
661
+ JsonNull => {
662
+ printNull(buffer)
663
+ return None
664
+ },
665
+ JsonBoolean(b) => {
666
+ printBool(b, buffer)
667
+ return None
668
+ },
669
+ JsonNumber(n) => return printNumber(n, buffer),
670
+ JsonString(s) => {
671
+ implHelper.emitEscapedQuotedString(s)
672
+ return None
673
+ },
674
+ JsonArray(elems) => {
675
+ match (elems) {
676
+ [] => {
677
+ Buffer.addChar('[', buffer)
678
+ if (implHelper.format.arrayFormat == SpacedArrayEntries) {
679
+ Buffer.addChar(' ', buffer)
680
+ }
681
+ Buffer.addChar(']', buffer)
682
+ return None
683
+ },
684
+ [e] => {
685
+ let format = implHelper.format
686
+
687
+ Buffer.addChar('[', buffer)
688
+
689
+ if (format.arrayFormat == OneArrayEntryPerLine) {
690
+ match (implHelper.printNewLine) {
691
+ Some(printNewLine) => printNewLine(),
692
+ None => void,
693
+ }
694
+ }
695
+
696
+ let elemLevel = indentationLevel + 1
697
+
698
+ if (format.arrayFormat == OneArrayEntryPerLine) {
699
+ match (implHelper.printIndentation) {
700
+ Some(printIndentation) => printIndentation(elemLevel),
701
+ None => void,
702
+ }
703
+ }
704
+
705
+ match (printElement(e, implHelper, elemLevel)) {
706
+ None => void,
707
+ err => return err,
708
+ }
709
+
710
+ if (format.arrayFormat == OneArrayEntryPerLine) {
711
+ match (implHelper.printNewLine) {
712
+ Some(printNewLine) => printNewLine(),
713
+ None => void,
714
+ }
715
+ match (implHelper.printIndentation) {
716
+ Some(printIndentation) => printIndentation(indentationLevel),
717
+ None => void,
718
+ }
719
+ }
720
+
721
+ Buffer.addChar(']', buffer)
722
+
723
+ return None
724
+ },
725
+ [initialHead, ...initialRest] => {
726
+ let format = implHelper.format
727
+
728
+ Buffer.addChar('[', buffer)
729
+
730
+ if (format.arrayFormat == OneArrayEntryPerLine) {
731
+ match (implHelper.printNewLine) {
732
+ Some(printNewLine) => printNewLine(),
733
+ None => void,
734
+ }
735
+ }
736
+
737
+ let mut currentHead = initialHead
738
+ let mut currentRest = initialRest
739
+
740
+ let elemLevel = indentationLevel + 1
741
+
742
+ for (let mut index = 0;; index += 1) {
743
+ if (index > 0) {
744
+ Buffer.addChar(',', buffer)
745
+ if (format.arrayFormat == SpacedArrayEntries) {
746
+ Buffer.addChar(' ', buffer)
747
+ }
748
+
749
+ if (format.arrayFormat == OneArrayEntryPerLine) {
750
+ match (implHelper.printNewLine) {
751
+ Some(printNewLine) => printNewLine(),
752
+ None => void,
753
+ }
754
+ }
755
+ }
756
+
757
+ if (format.arrayFormat == OneArrayEntryPerLine) {
758
+ match (implHelper.printIndentation) {
759
+ Some(printIndentation) => printIndentation(elemLevel),
760
+ None => void,
761
+ }
762
+ }
763
+
764
+ match (printElement(currentHead, implHelper, elemLevel)) {
765
+ None => void,
766
+ err => return err,
767
+ }
768
+
769
+ match (currentRest) {
770
+ [] => break,
771
+ [newHead, ...newRest] => {
772
+ currentHead = newHead
773
+ currentRest = newRest
774
+ },
775
+ }
776
+ }
777
+
778
+ if (format.arrayFormat == OneArrayEntryPerLine) {
779
+ match (implHelper.printNewLine) {
780
+ Some(printNewLine) => printNewLine(),
781
+ None => void,
782
+ }
783
+ match (implHelper.printIndentation) {
784
+ Some(printIndentation) => printIndentation(indentationLevel),
785
+ None => void,
786
+ }
787
+ }
788
+
789
+ Buffer.addChar(']', buffer)
790
+
791
+ return None
792
+ },
793
+ }
794
+ },
795
+ JsonObject(entries) => {
796
+ match (entries) {
797
+ [] => {
798
+ Buffer.addChar('{', buffer)
799
+ if (implHelper.format.objectFormat == SpacedObjectEntries) {
800
+ Buffer.addChar(' ', buffer)
801
+ }
802
+ Buffer.addChar('}', buffer)
803
+ return None
804
+ },
805
+ [(key, value)] => {
806
+ let format = implHelper.format
807
+
808
+ Buffer.addChar('{', buffer)
809
+
810
+ let elemLevel = indentationLevel + 1
811
+
812
+ if (format.objectFormat == OneObjectEntryPerLine) {
813
+ match (implHelper.printNewLine) {
814
+ Some(printNewLine) => printNewLine(),
815
+ None => void,
816
+ }
817
+ match (implHelper.printIndentation) {
818
+ Some(printIndentation) => printIndentation(elemLevel),
819
+ None => void,
820
+ }
821
+ }
822
+
823
+ implHelper.emitEscapedQuotedString(key)
824
+
825
+ Buffer.addChar(':', buffer)
826
+ match (format.objectFormat) {
827
+ CompactObjectEntries => void,
828
+ SpacedObjectEntries | OneObjectEntryPerLine => {
829
+ Buffer.addChar(' ', buffer)
830
+ },
831
+ }
832
+
833
+ match (printElement(value, implHelper, elemLevel)) {
834
+ None => void,
835
+ err => return err,
836
+ }
837
+
838
+ if (format.objectFormat == OneObjectEntryPerLine) {
839
+ match (implHelper.printNewLine) {
840
+ Some(printNewLine) => printNewLine(),
841
+ None => void,
842
+ }
843
+ match (implHelper.printIndentation) {
844
+ Some(printIndentation) => printIndentation(indentationLevel),
845
+ None => void,
846
+ }
847
+ }
848
+
849
+ Buffer.addChar('}', buffer)
850
+
851
+ return None
852
+ },
853
+ [initialHead, ...initialRest] => {
854
+ let format = implHelper.format
855
+
856
+ Buffer.addChar('{', buffer)
857
+
858
+ if (format.objectFormat == OneObjectEntryPerLine) {
859
+ match (implHelper.printNewLine) {
860
+ Some(printNewLine) => printNewLine(),
861
+ None => void,
862
+ }
863
+ }
864
+
865
+ let mut currentHead = initialHead
866
+ let mut currentRest = initialRest
867
+
868
+ let elemLevel = indentationLevel + 1
869
+
870
+ for (let mut index = 0;; index += 1) {
871
+ if (index > 0) {
872
+ Buffer.addChar(',', buffer)
873
+ if (format.objectFormat == SpacedObjectEntries) {
874
+ Buffer.addChar(' ', buffer)
875
+ }
876
+
877
+ if (format.objectFormat == OneObjectEntryPerLine) {
878
+ match (implHelper.printNewLine) {
879
+ Some(printNewLine) => printNewLine(),
880
+ None => void,
881
+ }
882
+ }
883
+ }
884
+
885
+ if (format.objectFormat == OneObjectEntryPerLine) {
886
+ match (implHelper.printIndentation) {
887
+ Some(printIndentation) => printIndentation(elemLevel),
888
+ None => void,
889
+ }
890
+ }
891
+
892
+ let (key, value) = currentHead
893
+
894
+ implHelper.emitEscapedQuotedString(key)
895
+
896
+ Buffer.addChar(':', buffer)
897
+ match (format.objectFormat) {
898
+ CompactObjectEntries => void,
899
+ SpacedObjectEntries | OneObjectEntryPerLine => {
900
+ Buffer.addChar(' ', buffer)
901
+ },
902
+ }
903
+
904
+ match (printElement(value, implHelper, elemLevel)) {
905
+ None => void,
906
+ err => return err,
907
+ }
908
+
909
+ match (currentRest) {
910
+ [] => break,
911
+ [newHead, ...newRest] => {
912
+ currentHead = newHead
913
+ currentRest = newRest
914
+ },
915
+ }
916
+ }
917
+
918
+ if (format.objectFormat == OneObjectEntryPerLine) {
919
+ match (implHelper.printNewLine) {
920
+ Some(printNewLine) => printNewLine(),
921
+ None => void,
922
+ }
923
+ match (implHelper.printIndentation) {
924
+ Some(printIndentation) => printIndentation(indentationLevel),
925
+ None => void,
926
+ }
927
+ }
928
+
929
+ Buffer.addChar('}', buffer)
930
+
931
+ return None
932
+ },
933
+ }
934
+ },
935
+ }
936
+ }
937
+
938
+ let isCodePointInBasicMultilingualPlane = (code: Number) =>
939
+ code >= 0x0000 && code <= 0xFFFF
940
+
941
+ let isHighSurrogate = (code: Number) => code >= 0xD800 && code <= 0xDBFF
942
+
943
+ let isLowSurrogate = (code: Number) => code >= 0xDC00 && code <= 0xDFFF
944
+
945
+ let combineSurrogatePairToCodePoint = (
946
+ highSurrogate: Number,
947
+ lowSurrogate: Number,
948
+ ) => {
949
+ // If this was a method exposed by itself in a library then it should check the
950
+ // ranges of the input surrogates, but here it's necessary because checks are made
951
+ // as part of the parsing logic.
952
+ ((highSurrogate - 0xD800) << 10) + (lowSurrogate - 0xDC00) + 0x10000
953
+ }
954
+
955
+ let makeJsonWriter = (format: FormattingSettings, buffer: Buffer.Buffer) => {
956
+ let printNewLine = match (format.lineEnding) {
957
+ NoLineEnding => None,
958
+ LineFeed => Some(() => {
959
+ Buffer.addChar('\n', buffer)
960
+ }),
961
+ CarriageReturnLineFeed => Some(() => {
962
+ Buffer.addChar('\r', buffer)
963
+ Buffer.addChar('\n', buffer)
964
+ }),
965
+ CarriageReturn => Some(() => {
966
+ Buffer.addChar('\r', buffer)
967
+ }),
968
+ }
969
+
970
+ let printIndentation = match (format.indentation) {
971
+ IndentWithTab => Some(indentationLevel => {
972
+ for (let mut count = 0; count < indentationLevel; count += 1) {
973
+ Buffer.addChar('\t', buffer)
974
+ }
975
+ }),
976
+ // Implement fast path, for common indentation level to avoid closure
977
+ IndentWithSpaces(spacesPerIndentation) when spacesPerIndentation == 2 =>
978
+ Some(indentationLevel => {
979
+ let spaceCount = indentationLevel * 2
980
+ for (let mut count = 0; count < spaceCount; count += 1) {
981
+ Buffer.addChar(' ', buffer)
982
+ }
983
+ }),
984
+ // Implement fast path, for common indentation level to avoid closure
985
+ IndentWithSpaces(spacesPerIndentation) when spacesPerIndentation == 4 =>
986
+ Some(indentationLevel => {
987
+ let spaceCount = indentationLevel * 4
988
+ for (let mut count = 0; count < spaceCount; count += 1) {
989
+ Buffer.addChar(' ', buffer)
990
+ }
991
+ }),
992
+ IndentWithSpaces(spacesPerIndentation) => Some(indentationLevel => {
993
+ let spaceCount = indentationLevel * spacesPerIndentation
994
+ for (let mut count = 0; count < spaceCount; count += 1) {
995
+ Buffer.addChar(' ', buffer)
996
+ }
997
+ }),
998
+ NoIndentation => None,
999
+ }
1000
+
1001
+ // A possible optimization to make this faster would be to
1002
+ // prepare a different closure for each combination of escaping options.
1003
+ // This way unnecessary branching is avoided.
1004
+ // The most important thing is that the non pretty printed format is optimized for
1005
+ // as this is where the performance is most likely to matter.
1006
+
1007
+ // In every case code points 0..31 must be escaped as
1008
+ // required by ECMA-404 (the so called "C0" control point group).
1009
+
1010
+ // For the non pretty printed case it is fastest to escape only what is
1011
+ // strictly required to avoid increasing output size
1012
+ // But for pretty printing or compatibility it may be desirable to escape other control points
1013
+ // or even everything other than printable ASCII characters.
1014
+ // for this reason options for this control has been exposed otherwise
1015
+ // just a sane default would suffice.
1016
+ // Additionally many JSON libraries escape additional two character
1017
+ // sequences for direct embedding into html for example. This is
1018
+ // specifically to avoid emitting the sequence "</" like in "</script>".
1019
+ // The lazy approach would be to just escape the slash (which can become
1020
+ // "\\/", not necessarily "\u002F"). This more conservative approach only
1021
+ // escapes it when needed, but requires to keep track of the previous code
1022
+ // point in the iteration so it's more complicated and handled separately.
1023
+ let emitCodePoint = if (
1024
+ !format.escapeAllControlPoints &&
1025
+ !format.escapeNonASCII
1026
+ ) {
1027
+ (codePoint: Number) => {
1028
+ if (codePoint > 31 && codePoint != 0x0022 && codePoint != 0x005C) {
1029
+ Buffer.addCharFromCodePoint(codePoint, buffer)
1030
+ } else {
1031
+ emitEscapedCodePoint(codePoint, buffer)
1032
+ }
1033
+ }
1034
+ } else if (!format.escapeAllControlPoints && format.escapeNonASCII) {
1035
+ // If desired, escape all non ASCII code points. So the only non
1036
+ // escaped code points are those in the range of ASCII characters
1037
+ // from 31 to 127.
1038
+ (codePoint: Number) => {
1039
+ if (
1040
+ codePoint > 31 &&
1041
+ codePoint != 0x0022 &&
1042
+ codePoint != 0x005C &&
1043
+ codePoint < 128
1044
+ ) {
1045
+ Buffer.addCharFromCodePoint(codePoint, buffer)
1046
+ } else {
1047
+ emitEscapedCodePoint(codePoint, buffer)
1048
+ }
1049
+ }
1050
+ } else if (format.escapeAllControlPoints && !format.escapeNonASCII) {
1051
+ // If desired, in addition to the required 0..31 control points,
1052
+ // also escape unicode control point group C1 (128-159).
1053
+ // There could be more control points or otherwise escape worthy
1054
+ // codepoints, but covering that would be overkill.
1055
+ (codePoint: Number) => {
1056
+ if (
1057
+ codePoint > 31 &&
1058
+ codePoint != 0x0022 &&
1059
+ codePoint != 0x005C &&
1060
+ codePoint < 127 ||
1061
+ codePoint > 159
1062
+ ) {
1063
+ Buffer.addCharFromCodePoint(codePoint, buffer)
1064
+ } else {
1065
+ emitEscapedCodePoint(codePoint, buffer)
1066
+ }
1067
+ }
1068
+ } else {
1069
+ // And this is just the combination of both flags, which means
1070
+ // doing almost the same as for the case above for
1071
+ // escapeNonASCII=true, but also escape the ASCII control codepoint
1072
+ // 127 (Delete).
1073
+ (codePoint: Number) => {
1074
+ if (
1075
+ codePoint > 31 &&
1076
+ codePoint != 0x0022 &&
1077
+ codePoint != 0x005C &&
1078
+ codePoint < 127
1079
+ ) {
1080
+ // fast path for chars that never need any escaping
1081
+ Buffer.addCharFromCodePoint(codePoint, buffer)
1082
+ } else {
1083
+ emitEscapedCodePoint(codePoint, buffer)
1084
+ }
1085
+ }
1086
+ }
1087
+
1088
+ let emitEscapedQuotedString = if (!format.escapeHTMLUnsafeSequences) {
1089
+ (s: String) => {
1090
+ Buffer.addChar('"', buffer)
1091
+
1092
+ // Note that it's important for performance that the closure passed to forEachCodePoint
1093
+ // is not allocated inline here, but just once when creating the writer.
1094
+
1095
+ String.forEachCodePoint(emitCodePoint, s)
1096
+
1097
+ Buffer.addChar('"', buffer)
1098
+ }
1099
+ } else {
1100
+ // Special handling for the escapeHTMLUnsafeSequences flag.
1101
+ // Escaping a sequence requires keeping track of previous characters,
1102
+ // which is difficult and suboptimal when using a function to iterate
1103
+ // the input string. So we don't want to pay the price in other cases.
1104
+ // This cannot be done just in the emitCodePoint function.
1105
+ // It could be possible to implement more optimally, but would
1106
+ // complicate things even more than this.
1107
+ (s: String) => {
1108
+ Buffer.addChar('"', buffer)
1109
+
1110
+ let mut prevCodePoint = 0
1111
+
1112
+ String.forEachCodePoint(codePoint => {
1113
+ if (codePoint == 47) {
1114
+ if (prevCodePoint == 60) {
1115
+ Buffer.addChar('\\', buffer)
1116
+ Buffer.addChar('/', buffer)
1117
+ } else {
1118
+ // otherwise just emit the slash as-is
1119
+ Buffer.addChar('/', buffer)
1120
+ }
1121
+ } else {
1122
+ emitCodePoint(codePoint)
1123
+ }
1124
+
1125
+ prevCodePoint = codePoint
1126
+ }, s)
1127
+
1128
+ Buffer.addChar('"', buffer)
1129
+ }
1130
+ }
1131
+
1132
+ let implHelper = {
1133
+ format,
1134
+ buffer,
1135
+ emitEscapedQuotedString,
1136
+ printNewLine,
1137
+ printIndentation,
1138
+ }: JsonWriterConfig
1139
+
1140
+ { emit: json => {
1141
+ match (printElement(json, implHelper, 0)) {
1142
+ None => void,
1143
+ err => return err,
1144
+ }
1145
+ if (format.finishWithNewLine) {
1146
+ match (printNewLine) {
1147
+ Some(printNewLine) => printNewLine(),
1148
+ None => void,
1149
+ }
1150
+ }
1151
+ return None
1152
+ }, }: JsonWriter
1153
+ }
1154
+
1155
+ /**
1156
+ * Converts the `Json` data structure into a JSON string with specific formatting settings.
1157
+ *
1158
+ * @param format: Formatting options
1159
+ * @param json: The `Json` data structure to convert
1160
+ * @returns `Ok(str)` containing the JSON string or `Err(err)` if the provided `Json` data structure cannot be converted to a string
1161
+ *
1162
+ * @example
1163
+ * assert toString(
1164
+ * JsonObject([("currency", JsonString("€")), ("price", JsonNumber(99.9))]
1165
+ * ) == Ok("{\"currency\":\"€\",\"price\":99.9}")
1166
+ * @example
1167
+ * assert toString(
1168
+ * format=Compact
1169
+ * JsonObject([("currency", JsonString("€")), ("price", JsonNumber(99.9))])
1170
+ * ) == Ok("{\"currency\":\"€\",\"price\":99.9}")
1171
+ * @example
1172
+ * assert toString(
1173
+ * format=Pretty,
1174
+ * JsonObject([("currency", JsonString("€")), ("price", JsonNumber(99.9))])
1175
+ * ) == Ok("{
1176
+ * \"currency\": \"€\",
1177
+ * \"price\": 99.9
1178
+ * }")
1179
+ * @example
1180
+ * assert toString(
1181
+ * format=Custom{
1182
+ * indentation: NoIndentation,
1183
+ * arrayFormat: CompactArrayEntries,
1184
+ * objectFormat: CompactObjectEntries,
1185
+ * lineEnding: NoLineEnding,
1186
+ * finishWithNewLine: false,
1187
+ * escapeAllControlPoints: true,
1188
+ * escapeHTMLUnsafeSequences: true,
1189
+ * escapeNonASCII: true,
1190
+ * },
1191
+ * JsonObject([("currency", JsonString("€")), ("price", JsonNumber(99.9))])
1192
+ * ) == Ok("{\"currency\":\"\\u20ac\",\"price\":99.9}")
1193
+ *
1194
+ * @since v0.6.0
1195
+ */
1196
+ provide let toString = (format=Compact, json: Json) => {
1197
+ let buf = Buffer.make(16)
1198
+ let format = match (format) {
1199
+ Pretty =>
1200
+ {
1201
+ indentation: IndentWithSpaces(2),
1202
+ arrayFormat: OneArrayEntryPerLine,
1203
+ objectFormat: OneObjectEntryPerLine,
1204
+ lineEnding: LineFeed,
1205
+ finishWithNewLine: true,
1206
+ escapeAllControlPoints: true,
1207
+ escapeHTMLUnsafeSequences: false,
1208
+ escapeNonASCII: false,
1209
+ },
1210
+ Compact =>
1211
+ {
1212
+ indentation: NoIndentation,
1213
+ arrayFormat: CompactArrayEntries,
1214
+ objectFormat: CompactObjectEntries,
1215
+ lineEnding: NoLineEnding,
1216
+ finishWithNewLine: false,
1217
+ escapeAllControlPoints: false,
1218
+ escapeHTMLUnsafeSequences: false,
1219
+ escapeNonASCII: false,
1220
+ },
1221
+ PrettyAndSafe =>
1222
+ {
1223
+ indentation: IndentWithSpaces(2),
1224
+ arrayFormat: OneArrayEntryPerLine,
1225
+ objectFormat: OneObjectEntryPerLine,
1226
+ lineEnding: LineFeed,
1227
+ finishWithNewLine: true,
1228
+ escapeAllControlPoints: true,
1229
+ escapeHTMLUnsafeSequences: true,
1230
+ escapeNonASCII: true,
1231
+ },
1232
+ CompactAndSafe =>
1233
+ {
1234
+ indentation: NoIndentation,
1235
+ arrayFormat: CompactArrayEntries,
1236
+ objectFormat: CompactObjectEntries,
1237
+ lineEnding: NoLineEnding,
1238
+ finishWithNewLine: false,
1239
+ escapeAllControlPoints: true,
1240
+ escapeHTMLUnsafeSequences: true,
1241
+ escapeNonASCII: true,
1242
+ },
1243
+ Custom{
1244
+ indentation,
1245
+ arrayFormat,
1246
+ objectFormat,
1247
+ lineEnding,
1248
+ finishWithNewLine,
1249
+ escapeAllControlPoints,
1250
+ escapeHTMLUnsafeSequences,
1251
+ escapeNonASCII,
1252
+ } =>
1253
+ {
1254
+ indentation,
1255
+ arrayFormat,
1256
+ objectFormat,
1257
+ lineEnding,
1258
+ finishWithNewLine,
1259
+ escapeAllControlPoints,
1260
+ escapeHTMLUnsafeSequences,
1261
+ escapeNonASCII,
1262
+ },
1263
+ }
1264
+ let writer = makeJsonWriter(format, buf)
1265
+ let error = writer.emit(json)
1266
+
1267
+ match (error) {
1268
+ None => Ok(Buffer.toString(buf)),
1269
+ Some(e) => Err(e),
1270
+ }
1271
+ }
1272
+
1273
+ /**
1274
+ * Represents errors for JSON parsing along with a human readable message.
1275
+ */
1276
+ provide enum JsonParseError {
1277
+ UnexpectedEndOfInput(String),
1278
+ UnexpectedToken(String),
1279
+ InvalidUTF16SurrogatePair(String),
1280
+ }
1281
+
1282
+ /*
1283
+ * Internal data structure used during parsing.
1284
+ */
1285
+ record JsonParserState {
1286
+ string: String,
1287
+ bufferParse: Buffer.Buffer,
1288
+ mut currentCodePoint: Number,
1289
+ mut pos: Number,
1290
+ mut bytePos: Number,
1291
+ }
1292
+
1293
+ let isInterTokenWhiteSpace = (codePoint: Number) => {
1294
+ match (codePoint) {
1295
+ 0x0009 => true, // tab
1296
+ 0x000A => true, // line feed
1297
+ 0x000D => true, // carriage return
1298
+ 0x0020 => true, // space
1299
+ _ => false,
1300
+ }
1301
+ }
1302
+
1303
+ let _END_OF_INPUT = -1
1304
+
1305
+ @unsafe
1306
+ let rec readCodePoint = (bytePosition: Number, string: String) => {
1307
+ use WasmI32.{ (+), (<) }
1308
+
1309
+ let strPtr = WasmI32.fromGrain(string)
1310
+
1311
+ let byteSize = WasmI32.load(strPtr, 4n)
1312
+
1313
+ let bytePositionW32 = coerceNumberToWasmI32(bytePosition)
1314
+
1315
+ let ptr = strPtr + 8n + bytePositionW32
1316
+
1317
+ if (bytePositionW32 < byteSize) {
1318
+ let codePoint = getCodePoint(ptr)
1319
+ tagSimpleNumber(codePoint)
1320
+ } else {
1321
+ _END_OF_INPUT
1322
+ }
1323
+ }
1324
+
1325
+ let codePointUTF8ByteCount = (usv: Number) => {
1326
+ if (!Char.isValid(usv)) {
1327
+ fail "Impossible: JSON parser encountered an invalid unicode scalar value in codePointUTF8ByteCount"
1328
+ }
1329
+
1330
+ if (usv <= 0x7f) {
1331
+ 1
1332
+ } else if (usv <= 0x7ff) {
1333
+ 2
1334
+ } else if (usv <= 0xffff) {
1335
+ 3
1336
+ } else {
1337
+ 4
1338
+ }
1339
+ }
1340
+
1341
+ let isAtEndOfInput = (parserState: JsonParserState) => {
1342
+ parserState.currentCodePoint == _END_OF_INPUT
1343
+ }
1344
+
1345
+ let next = (parserState: JsonParserState) => {
1346
+ let mut c = parserState.currentCodePoint
1347
+ if (c != _END_OF_INPUT) {
1348
+ parserState.bytePos += codePointUTF8ByteCount(c)
1349
+
1350
+ c = readCodePoint(parserState.bytePos, parserState.string)
1351
+
1352
+ parserState.currentCodePoint = c
1353
+ parserState.pos += 1
1354
+ }
1355
+ c
1356
+ }
1357
+
1358
+ let skipWhiteSpace = (parserState: JsonParserState) => {
1359
+ // isAtEndOfInput is not strictly necessary here
1360
+ // could remove as an optimization
1361
+ while (
1362
+ isInterTokenWhiteSpace(parserState.currentCodePoint) &&
1363
+ !isAtEndOfInput(parserState)
1364
+ ) {
1365
+ next(parserState)
1366
+ void
1367
+ }
1368
+ }
1369
+
1370
+ let buildUnexpectedTokenError = (parserState: JsonParserState, detail: String) => {
1371
+ let codePoint = parserState.currentCodePoint
1372
+ let pos = parserState.pos
1373
+ if (codePoint == _END_OF_INPUT) {
1374
+ UnexpectedEndOfInput(
1375
+ "Unexpected token at position " ++ runtimeToString(pos) ++ ": " ++ detail,
1376
+ )
1377
+ } else {
1378
+ UnexpectedToken(
1379
+ "Unexpected token at position " ++ runtimeToString(pos) ++ ": " ++ detail,
1380
+ )
1381
+ }
1382
+ }
1383
+
1384
+ @unsafe
1385
+ let toHex = (n: Number) => {
1386
+ let x = coerceNumberToWasmI32(n)
1387
+ NumberUtils.itoa32(x, 16n)
1388
+ }
1389
+
1390
+ let toHexWithZeroPadding = (n: Number, padTo: Number) => {
1391
+ // Note that this function is only called in exceptional cases so no effort
1392
+ // was made to optimize it.
1393
+ let mut result = toHex(n)
1394
+ for (let mut i = String.length(result); i < padTo; i += 1) {
1395
+ result = "0" ++ result
1396
+ }
1397
+ result
1398
+ }
1399
+
1400
+ let formatCodePointOrEOF = (codePoint: Number) => {
1401
+ if (codePoint >= 32 && codePoint <= 126) {
1402
+ // If the codepoint is in the range of printable ASCII characters, then
1403
+ // display the character itself . Whether it's a good idea to display
1404
+ // all of them, especially space is up for debate.
1405
+ "'" ++ Char.toString(Char.fromCode(codePoint)) ++ "'"
1406
+ } else if (codePoint == -1) {
1407
+ // Special case for value used by the parsing code to avoid heap allocations.
1408
+ "end of input"
1409
+ } else {
1410
+ // Format any other code point as hexadecimal value.
1411
+ "U+" ++ toHexWithZeroPadding(codePoint, 4)
1412
+ }
1413
+ }
1414
+
1415
+ let expectCodePointAndAdvance = (
1416
+ expectedCodePoint: Number,
1417
+ parserState: JsonParserState,
1418
+ ) => {
1419
+ let c = parserState.currentCodePoint
1420
+ if (c == expectedCodePoint) {
1421
+ next(parserState)
1422
+ None
1423
+ } else {
1424
+ let detail = "expected " ++
1425
+ formatCodePointOrEOF(expectedCodePoint) ++
1426
+ ", found " ++
1427
+ formatCodePointOrEOF(c)
1428
+ Some(buildUnexpectedTokenError(parserState, detail))
1429
+ }
1430
+ }
1431
+ let atoiFast = buffer => {
1432
+ let bufLen = Buffer.length(buffer)
1433
+ let mut result = 0
1434
+ for (let mut i = 0; i < bufLen; i += 1) {
1435
+ use Uint8.{ (-) }
1436
+ result = (result << 1) +
1437
+ (result << 3) +
1438
+ Uint8.toNumber(Buffer.getUint8(i, buffer) - 48us)
1439
+ }
1440
+ result
1441
+ }
1442
+ let rec parseValue = (parserState: JsonParserState) => {
1443
+ skipWhiteSpace(parserState)
1444
+
1445
+ let result = match (parserState.currentCodePoint) {
1446
+ 0x7B => parseObject(parserState), // '{'
1447
+ 0x5B => parseArray(parserState), // '['
1448
+ 0x22 => parseStringValue(parserState), // '"'
1449
+ 0x74 => parseTrueValue(parserState), // 't'
1450
+ 0x66 => parseFalseValue(parserState), // 'f'
1451
+ 0x6E => parseNullValue(parserState), // 'n'
1452
+ // Numbers
1453
+ 0x30 => parseNumberValue(parserState), // '0'
1454
+ 0x31 => parseNumberValue(parserState), // '1'
1455
+ 0x32 => parseNumberValue(parserState), // '2'
1456
+ 0x33 => parseNumberValue(parserState), // '3'
1457
+ 0x34 => parseNumberValue(parserState), // '4'
1458
+ 0x35 => parseNumberValue(parserState), // '5'
1459
+ 0x36 => parseNumberValue(parserState), // '6'
1460
+ 0x37 => parseNumberValue(parserState), // '7'
1461
+ 0x38 => parseNumberValue(parserState), // '8'
1462
+ 0x39 => parseNumberValue(parserState), // '9'
1463
+ 0x2D => parseNumberValue(parserState), // '-'
1464
+ c => {
1465
+ let detail = "expected start of a JSON value, found " ++
1466
+ formatCodePointOrEOF(c)
1467
+ Err(buildUnexpectedTokenError(parserState, detail))
1468
+ },
1469
+ }
1470
+
1471
+ skipWhiteSpace(parserState)
1472
+
1473
+ result
1474
+ }
1475
+ and parseNullValue = (parserState: JsonParserState) => {
1476
+ match (expectCodePointAndAdvance(0x6E, parserState)) {
1477
+ // 'n'
1478
+ Some(e) => Err(e),
1479
+ None => {
1480
+ match (expectCodePointAndAdvance(0x75, parserState)) {
1481
+ // 'u'
1482
+ Some(e) => Err(e),
1483
+ None => {
1484
+ match (expectCodePointAndAdvance(0x6C, parserState)) {
1485
+ // 'l'
1486
+ Some(e) => Err(e),
1487
+ None => {
1488
+ match (expectCodePointAndAdvance(0x6C, parserState)) {
1489
+ // 'l'
1490
+ Some(e) => Err(e),
1491
+ None => Ok(JsonNull),
1492
+ }
1493
+ },
1494
+ }
1495
+ },
1496
+ }
1497
+ },
1498
+ }
1499
+ }
1500
+ and parseTrueValue = (parserState: JsonParserState) => {
1501
+ match (expectCodePointAndAdvance(0x74, parserState)) {
1502
+ // 't'
1503
+ Some(e) => Err(e),
1504
+ None => {
1505
+ match (expectCodePointAndAdvance(0x72, parserState)) {
1506
+ // 'r'
1507
+ Some(e) => Err(e),
1508
+ None => {
1509
+ match (expectCodePointAndAdvance(0x75, parserState)) {
1510
+ // 'u'
1511
+ Some(e) => Err(e),
1512
+ None => {
1513
+ match (expectCodePointAndAdvance(0x65, parserState)) {
1514
+ // 'e'
1515
+ Some(e) => Err(e),
1516
+ None => Ok(JsonBoolean(true)),
1517
+ }
1518
+ },
1519
+ }
1520
+ },
1521
+ }
1522
+ },
1523
+ }
1524
+ }
1525
+ and parseFalseValue = (parserState: JsonParserState) => {
1526
+ match (expectCodePointAndAdvance(0x66, parserState)) {
1527
+ // 'f'
1528
+ Some(e) => Err(e),
1529
+ None => {
1530
+ match (expectCodePointAndAdvance(0x61, parserState)) {
1531
+ // 'a'
1532
+ Some(e) => Err(e),
1533
+ None => {
1534
+ match (expectCodePointAndAdvance(0x6C, parserState)) {
1535
+ // 'l'
1536
+ Some(e) => Err(e),
1537
+ None => {
1538
+ match (expectCodePointAndAdvance(0x73, parserState)) {
1539
+ // 's'
1540
+ Some(e) => Err(e),
1541
+ None => {
1542
+ match (expectCodePointAndAdvance(0x65, parserState)) {
1543
+ // 'e'
1544
+ Some(e) => Err(e),
1545
+ None => Ok(JsonBoolean(false)),
1546
+ }
1547
+ },
1548
+ }
1549
+ },
1550
+ }
1551
+ },
1552
+ }
1553
+ },
1554
+ }
1555
+ }
1556
+ and parseString = (parserState: JsonParserState) => {
1557
+ match (expectCodePointAndAdvance(0x22, parserState)) {
1558
+ // '"'
1559
+ Some(e) => return Err(e),
1560
+ None => {
1561
+ let mut done = false
1562
+ let buffer = parserState.bufferParse
1563
+ Buffer.clear(buffer)
1564
+
1565
+ while (!done) {
1566
+ match (parserState.currentCodePoint) {
1567
+ 0x22 => { // '"'
1568
+ next(parserState)
1569
+ done = true
1570
+ break
1571
+ },
1572
+ -1 => {
1573
+ // just end the loop without setting done to true
1574
+ break
1575
+ },
1576
+ 0x5C => { // '\'
1577
+ // Keep the starting position for better error reporting.
1578
+ let escapeStartPos = parserState.pos
1579
+
1580
+ next(parserState)
1581
+
1582
+ match (parserState.currentCodePoint) {
1583
+ 0x22 => { // '"'
1584
+ Buffer.addChar('"', buffer)
1585
+ ignore(next(parserState))
1586
+ },
1587
+ 0x5C => { // '\'
1588
+ Buffer.addChar('\\', buffer)
1589
+ ignore(next(parserState))
1590
+ },
1591
+ 0x2F => { // '/'
1592
+ Buffer.addChar('/', buffer)
1593
+ ignore(next(parserState))
1594
+ },
1595
+ 0x62 => { // letter 'b' as in Backspace
1596
+ // emit backspace control code
1597
+ Buffer.addChar('\u{08}', buffer)
1598
+ ignore(next(parserState))
1599
+ },
1600
+ 0x66 => { // letter 'f' as in Form Feed
1601
+ // emit Form Feed control code
1602
+ Buffer.addChar('\u{0C}', buffer)
1603
+ ignore(next(parserState))
1604
+ },
1605
+ 0x6E => { // letter 'n' as in New line
1606
+ // emit Line Feed control code
1607
+ Buffer.addChar('\u{0A}', buffer)
1608
+ ignore(next(parserState))
1609
+ },
1610
+ 0x72 => { // letter 'r' as in carriage Return
1611
+ // emit Carriage Return control code
1612
+ Buffer.addChar('\u{0D}', buffer)
1613
+ ignore(next(parserState))
1614
+ },
1615
+ 0x74 => { // letter 't' as in Tab
1616
+ // emit Tab control code
1617
+ Buffer.addChar('\u{09}', buffer)
1618
+ ignore(next(parserState))
1619
+ },
1620
+ 0x75 => { // 'u' (start of hexadecimal UTF-16 escape sequence)
1621
+ next(parserState)
1622
+
1623
+ // The escape sequence can either be a standalone code point or
1624
+ // a UTF-16 surrogate pair made of two code units that have to
1625
+ // be combined to form a code point. This is legacy of
1626
+ // JavaScript's UTF-16 string representation, despite JSON
1627
+ // mandating UTF-8 (kind of, as stated in rfc8259: "JSON text
1628
+ // exchanged between systems that are not part of a closed
1629
+ // ecosystem MUST be encoded using UTF-8").
1630
+ // This would be easy to do using a function for shared logic,
1631
+ // but in order to avoid heap allocation I've chosen to instead
1632
+ // use a loop and local state.
1633
+
1634
+ let mut highSurrogate = -1
1635
+
1636
+ while (true) {
1637
+ let mut codeUnit = 0
1638
+
1639
+ for (
1640
+ let mut digitIndex = 3;
1641
+ digitIndex >= 0;
1642
+ digitIndex -= 1
1643
+ ) {
1644
+ let hexDigitCodePoint = parserState.currentCodePoint
1645
+
1646
+ let mut digit = hexDigitCodePoint
1647
+
1648
+ if (hexDigitCodePoint >= 48 && hexDigitCodePoint <= 57) { // 0..9
1649
+ digit -= 48
1650
+ } else if (
1651
+ hexDigitCodePoint >= 65 &&
1652
+ hexDigitCodePoint <= 70
1653
+ ) { // A..F
1654
+ digit -= 55 // (65 - 10)
1655
+ } else if (
1656
+ hexDigitCodePoint >= 97 &&
1657
+ hexDigitCodePoint <= 102
1658
+ ) { // a..f
1659
+ digit -= 87 // (97 - 10)
1660
+ } else {
1661
+ let digitsSoFar = 3 - digitIndex
1662
+ let detail =
1663
+ "expected exactly 4 hexadecimal digits in the UTF-16 escape sequence, found only " ++
1664
+ runtimeToString(digitsSoFar)
1665
+ return Err(buildUnexpectedTokenError(parserState, detail))
1666
+ }
1667
+
1668
+ let shift = digitIndex * 4
1669
+ codeUnit = codeUnit | digit << shift
1670
+
1671
+ ignore(next(parserState))
1672
+ }
1673
+
1674
+ if (highSurrogate == -1) {
1675
+ // This is the first iteration of the loop.
1676
+ // The code unit should either be the high surrogate of the
1677
+ // pair or a full code point in the Basic Multilingual
1678
+ // Plane (U+0000..U+FFFF).
1679
+ if (isHighSurrogate(codeUnit)) {
1680
+ // Next characters should be "\u"
1681
+ // '\'
1682
+ match (expectCodePointAndAdvance(0x5C, parserState)) {
1683
+ Some(e) => return Err(e),
1684
+ None => void,
1685
+ }
1686
+ // 'u'
1687
+ match (expectCodePointAndAdvance(0x75, parserState)) {
1688
+ Some(e) => return Err(e),
1689
+ None => void,
1690
+ }
1691
+
1692
+ // Keep the high surrogate and proceed to the second
1693
+ // iteration of the loop.
1694
+ highSurrogate = codeUnit
1695
+ } else if (
1696
+ isCodePointInBasicMultilingualPlane(codeUnit) &&
1697
+ !isLowSurrogate(codeUnit)
1698
+ ) {
1699
+ let codePoint = codeUnit
1700
+ Buffer.addCharFromCodePoint(codePoint, buffer)
1701
+ break
1702
+ } else {
1703
+ let message =
1704
+ "Invalid character escape sequence at position " ++
1705
+ runtimeToString(escapeStartPos) ++
1706
+ ": expected a Unicode code point in Basic Multilingual Plane (U+0000..U+FFFF) or a high surrogate (0xD800..0xDBFF) of a UTF-16 surrogate pair, found " ++
1707
+ "0x" ++
1708
+ toHexWithZeroPadding(codeUnit, 4)
1709
+ return Err(InvalidUTF16SurrogatePair(message))
1710
+ }
1711
+ } else {
1712
+ // This is the second iteration of the loop.
1713
+ // The code unit should be the low surrogate of the pair.
1714
+ if (isLowSurrogate(codeUnit)) {
1715
+ let lowSurrogate = codeUnit
1716
+ let combinedCodePoint = combineSurrogatePairToCodePoint(
1717
+ highSurrogate,
1718
+ lowSurrogate
1719
+ )
1720
+ Buffer.addCharFromCodePoint(combinedCodePoint, buffer)
1721
+ break
1722
+ } else {
1723
+ let message =
1724
+ "Invalid character escape sequence at position " ++
1725
+ runtimeToString(escapeStartPos) ++
1726
+ ": expected a low surrogate (0xDC00..0xDFFF) in the second code unit of the UTF-16 sequence, found " ++
1727
+ "0x" ++
1728
+ toHexWithZeroPadding(codeUnit, 4)
1729
+ return Err(InvalidUTF16SurrogatePair(message))
1730
+ }
1731
+ }
1732
+ }
1733
+ },
1734
+ unexpectedCodePoint => {
1735
+ // JSON doesn't allow arbitrary characters to be preceded by backslash escape.
1736
+ // Only the ones above.
1737
+ let detail =
1738
+ "expected a valid escape sequence or the end of string, found " ++
1739
+ formatCodePointOrEOF(unexpectedCodePoint)
1740
+ return Err(buildUnexpectedTokenError(parserState, detail))
1741
+ },
1742
+ }
1743
+ },
1744
+ c => {
1745
+ if (c >= 0x00 && c <= 0x1F) {
1746
+ return Err(
1747
+ buildUnexpectedTokenError(
1748
+ parserState,
1749
+ "Bad control character in string literal"
1750
+ ),
1751
+ )
1752
+ }
1753
+ // Finally the happy case of a simple unescaped code point.
1754
+ next(parserState)
1755
+ Buffer.addCharFromCodePoint(c, buffer)
1756
+ },
1757
+ }
1758
+ }
1759
+
1760
+ if (done) {
1761
+ let s = Buffer.toString(buffer)
1762
+ return Ok(s)
1763
+ } else {
1764
+ return Err(
1765
+ buildUnexpectedTokenError(
1766
+ parserState,
1767
+ "unexpected end of string value"
1768
+ ),
1769
+ )
1770
+ }
1771
+ },
1772
+ }
1773
+ }
1774
+ and parseStringValue = (parserState: JsonParserState) => {
1775
+ match (parseString(parserState)) {
1776
+ Ok(s) => Ok(JsonString(s)),
1777
+ Err(e) => Err(e),
1778
+ }
1779
+ }
1780
+ and parseNumberValue = (parserState: JsonParserState) => {
1781
+ // TODO(#1878): Use a streaming-optimized way to parse numbers
1782
+ let buffer = parserState.bufferParse
1783
+ Buffer.clear(buffer)
1784
+ // First char can optionally be a minus sign.
1785
+ let mut c = parserState.currentCodePoint
1786
+ let mut isFloat = false
1787
+ let isNegative = c == 0x2D
1788
+ // '-'
1789
+ if (isNegative) {
1790
+ c = next(parserState)
1791
+ }
1792
+
1793
+ // After that, the first/second char can only be a decimal digit ('0'..'9').
1794
+ match (c) {
1795
+ 0x30 => { // '0'
1796
+ // JSON doesn't allow numbers with additional leading zeros like
1797
+ // "01". Which means that if a number starts with zero then the
1798
+ // integer part is just zero and the next one can only be one of
1799
+ // '.', 'e' or 'E'. In any case all that needs to be done here is
1800
+ // to advance over the zero character and proceed to the optional
1801
+ // fractional and exponential parts. If another digit follows then
1802
+ // a parsing error will occur as expected, but implicitly because
1803
+ // this function finishes with the parser positioned on a digit
1804
+ // and not on a token expected after a number like ',', ']', '}' or
1805
+ // EOF.
1806
+ Buffer.addCharFromCodePoint(c, buffer)
1807
+ c = next(parserState)
1808
+ },
1809
+ x when x >= 0x31 && x <= 0x39 => { // '1'..'9'
1810
+ while (true) {
1811
+ Buffer.addCharFromCodePoint(c, buffer)
1812
+ c = next(parserState)
1813
+ if (c < 0x30 || c > 0x39) {
1814
+ break
1815
+ }
1816
+ }
1817
+ },
1818
+ unexpectedCodePoint => {
1819
+ // The integer part of the number has to have at least one digit.
1820
+ // JSON doesn't allow numbers starting with decimal separator like ".1".
1821
+ let detail = "expected a decimal digit, found " ++
1822
+ formatCodePointOrEOF(unexpectedCodePoint)
1823
+ return Err(buildUnexpectedTokenError(parserState, detail))
1824
+ },
1825
+ }
1826
+ // Optional fractional part of the number.
1827
+ if (c == 0x2E) { // '.'
1828
+ isFloat = true
1829
+ Buffer.addChar('.', buffer)
1830
+ c = next(parserState)
1831
+ let mut hasHitDigit = false
1832
+ for (; c >= 0x30 && c <= 0x39;) {
1833
+ hasHitDigit = true
1834
+ Buffer.addCharFromCodePoint(c, buffer)
1835
+ c = next(parserState)
1836
+ }
1837
+ if (!hasHitDigit)
1838
+ return Err(
1839
+ buildUnexpectedTokenError(
1840
+ parserState,
1841
+ "exponent part is missing in number"
1842
+ ),
1843
+ )
1844
+ }
1845
+ // Optional exponential part of the number.
1846
+ if (c == 0x65 || c == 0x45) { // 'e' or 'E'
1847
+ isFloat = true
1848
+ Buffer.addChar('e', buffer)
1849
+ c = next(parserState)
1850
+ // can start with optional plus or minus sign
1851
+ match (c) {
1852
+ 0x2D => { // '-'
1853
+ c = next(parserState)
1854
+ Buffer.addChar('-', buffer)
1855
+ },
1856
+ 0x2B => { // '+'
1857
+ c = next(parserState)
1858
+ },
1859
+ _ => void,
1860
+ }
1861
+ // followed by one or more digits (0-9)
1862
+ let mut hasHitDigit = false
1863
+ for (; c >= 0x30 && c <= 0x39;) {
1864
+ hasHitDigit = true
1865
+ Buffer.addCharFromCodePoint(c, buffer)
1866
+ c = next(parserState)
1867
+ }
1868
+ if (!hasHitDigit)
1869
+ return Err(
1870
+ buildUnexpectedTokenError(
1871
+ parserState,
1872
+ "exponent part is missing in number"
1873
+ ),
1874
+ )
1875
+ }
1876
+ // Note that unlike all other JSON value types there's no explicit ending
1877
+ // character like ('"' for strings, ']' for arrays,'}' for objects etc). We
1878
+ // just leave the parser state at current position and the reading of next
1879
+ // token will succeed or fail, but number parsing just ends here.
1880
+ let result = match (isFloat) {
1881
+ false => atoiFast(buffer),
1882
+ true => {
1883
+ let str = Buffer.toString(buffer)
1884
+ match (Atof.parseFloat(str)) {
1885
+ Err(err) => fail "Impossible: Json parse float on invalid float",
1886
+ Ok(n) => n,
1887
+ }
1888
+ },
1889
+ }
1890
+ if (result == 0 && isNegative)
1891
+ return Ok(JsonNumber(-0.0))
1892
+ else
1893
+ return Ok(JsonNumber(if (isNegative) result * -1 else result))
1894
+ }
1895
+ and parseArray = (parserState: JsonParserState) => {
1896
+ match (expectCodePointAndAdvance(0x5B, parserState)) {
1897
+ // '['
1898
+ Some(e) => return Err(e),
1899
+ None => {
1900
+ skipWhiteSpace(parserState)
1901
+
1902
+ let mut elems = []: List<Json>
1903
+
1904
+ let mut done = false
1905
+ let mut first = true
1906
+ let mut trailingComma = false
1907
+ while (!done) {
1908
+ let c = parserState.currentCodePoint
1909
+ match (c) {
1910
+ 0x2C => { // ','
1911
+ if (first) {
1912
+ return Err(
1913
+ buildUnexpectedTokenError(
1914
+ parserState,
1915
+ "unexpected comma at beginning of array"
1916
+ ),
1917
+ )
1918
+ }
1919
+ trailingComma = true
1920
+ next(parserState)
1921
+ skipWhiteSpace(parserState)
1922
+ },
1923
+ 0x5D => { // ']'
1924
+ next(parserState)
1925
+ done = true
1926
+ break
1927
+ },
1928
+ -1 => {
1929
+ // just end the loop without setting done to true
1930
+ break
1931
+ },
1932
+ _ => {
1933
+ // note that parseValue skips initial and final whitespace
1934
+ match (parseValue(parserState)) {
1935
+ Ok(elem) => {
1936
+ first = false
1937
+ trailingComma = false
1938
+ elems = [elem, ...elems]
1939
+ },
1940
+ Err(e) => return Err(e),
1941
+ }
1942
+ },
1943
+ }
1944
+ }
1945
+
1946
+ if (trailingComma) {
1947
+ return Err(
1948
+ buildUnexpectedTokenError(parserState, "unexpected end of array"),
1949
+ )
1950
+ } else if (done) {
1951
+ return Ok(JsonArray(List.reverse(elems)))
1952
+ } else {
1953
+ return Err(
1954
+ buildUnexpectedTokenError(parserState, "unexpected end of array"),
1955
+ )
1956
+ }
1957
+ },
1958
+ }
1959
+ }
1960
+ and parseObject = (parserState: JsonParserState) => {
1961
+ match (expectCodePointAndAdvance(0x7B, parserState)) {
1962
+ // '{'
1963
+ Some(e) => return Err(e),
1964
+ None => {
1965
+ let mut entries = []: List<(String, Json)>
1966
+
1967
+ let mut done = false
1968
+ let mut first = true
1969
+
1970
+ // one iteration of this loop should correspond to a key-value pair
1971
+ let mut trailingComma = false
1972
+ while (!done) {
1973
+ skipWhiteSpace(parserState)
1974
+
1975
+ let c = parserState.currentCodePoint
1976
+ match (c) {
1977
+ -1 => {
1978
+ let detail = "expected a key-value pair or the end of the object"
1979
+ return Err(buildUnexpectedTokenError(parserState, detail))
1980
+ },
1981
+ 0x2C => { // ','
1982
+ trailingComma = true
1983
+ if (first) {
1984
+ let detail =
1985
+ "expected a key-value pair or the end of the object, found ','"
1986
+ return Err(buildUnexpectedTokenError(parserState, detail))
1987
+ } else {
1988
+ ignore(next(parserState))
1989
+ }
1990
+ },
1991
+ 0x7D => { // '}'
1992
+ if (trailingComma) {
1993
+ let detail = "unexpected trailing comma in object"
1994
+ return Err(buildUnexpectedTokenError(parserState, detail))
1995
+ }
1996
+ next(parserState)
1997
+ done = true
1998
+ break
1999
+ },
2000
+ _ => {
2001
+ trailingComma = false
2002
+ // A new entry in current object.
2003
+ // Just call parseString directly. In case the current character id not '"', it will return an error we can pass along.
2004
+ match (parseString(parserState)) {
2005
+ Ok(key) => {
2006
+ skipWhiteSpace(parserState)
2007
+
2008
+ match (expectCodePointAndAdvance(0x3A, parserState)) {
2009
+ // ':'
2010
+ None => {
2011
+ // note that parseValue skips initial and final whitespace
2012
+ match (parseValue(parserState)) {
2013
+ Ok(value) => {
2014
+ entries = [(key, value), ...entries]
2015
+ first = false
2016
+ },
2017
+ Err(e) => return Err(e),
2018
+ }
2019
+ },
2020
+ Some(e) => return Err(e),
2021
+ }
2022
+ },
2023
+ Err(e) => return Err(e),
2024
+ }
2025
+ },
2026
+ }
2027
+ }
2028
+ // end of entry loop
2029
+
2030
+ if (done) {
2031
+ return Ok(JsonObject(List.reverse(entries)))
2032
+ } else {
2033
+ // This branch is not expected to actually execute,
2034
+ // but in case it does, may just as well do the right thing.
2035
+ return Err(
2036
+ buildUnexpectedTokenError(parserState, "unexpected end of object"),
2037
+ )
2038
+ }
2039
+ },
2040
+ }
2041
+ }
2042
+
2043
+ /**
2044
+ * Parses JSON string into a `Json` data structure.
2045
+ *
2046
+ * @param str: The JSON string to parse
2047
+ * @returns `Ok(json)` containing the parsed data structure on a successful parse or `Err(err)` containing a parse error otherwise
2048
+ *
2049
+ * @example
2050
+ * assert parse("{\"currency\":\"$\",\"price\":119}") == Ok(
2051
+ * JsonObject([
2052
+ * ("currency", JsonString("$")),
2053
+ * ("price", JsonNumber(119))
2054
+ * ])
2055
+ * )
2056
+ *
2057
+ * @since v0.6.0
2058
+ */
2059
+ provide let parse: (str: String) => Result<Json, JsonParseError> = (str: String) => {
2060
+ let parserState = {
2061
+ string: str,
2062
+ bufferParse: Buffer.make(16),
2063
+ currentCodePoint: readCodePoint(0, str),
2064
+ pos: 0,
2065
+ bytePos: 0,
2066
+ }: JsonParserState
2067
+
2068
+ let root = parseValue(parserState)
2069
+
2070
+ skipWhiteSpace(parserState)
2071
+
2072
+ if (isAtEndOfInput(parserState)) {
2073
+ root
2074
+ } else {
2075
+ match (root) {
2076
+ Ok(_) => {
2077
+ let detail = "expected end of input, found " ++
2078
+ formatCodePointOrEOF(parserState.currentCodePoint)
2079
+ Err(buildUnexpectedTokenError(parserState, detail))
2080
+ },
2081
+ e => e,
2082
+ }
2083
+ }
2084
+ }