nova-std 0.3.0

The Nova language's standard library sources (internal to the nova toolchain)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
//! JSON: text parsed into a `JsonValue`, and a `JsonValue` written as text.
//!
//! `parse` reads JSON text strictly, as RFC 8259 defines it, and `stringify`
//! writes a `JsonValue` as compact JSON text. `ToJson` and `FromJson` convert
//! between a `JsonValue` and another type; std implements both for `Bool`,
//! `String`, `Float` and `Int`. Every module sees these names without an
//! import, and a module's own definition of one of them shadows the one here.
//!
//! Every JSON number is a `Float`, so an integer larger in magnitude than
//! 2^53 (9,007,199,254,740,992) may not keep its exact value. An object is a
//! `Map`, which holds one value per key and keeps no order: `stringify`
//! writes an object's members in an order that can change from one run of a
//! program to the next.
//!
//! Parsing JSON text, reading values out of an object, and writing a value
//! back as compact text:
//!
//! ```
//! let doc = parse("{\"name\": \"nova\", \"stars\": 42}").unwrap()
//! let fields = match doc {
//!     Object(m) => m
//!     _ => panic("expected an object")
//! }
//! assert_eq(String::from_json(fields.get("name").unwrap()).unwrap(), "nova")
//! assert_eq(Int::from_json(fields.get("stars").unwrap()).unwrap(), 42)
//!
//! // stringify writes compact text; every number is a Float, and 1.0 is `1`.
//! let list = Array([Number(1.0), Bool(true), Null, "hi".to_json()])
//! assert_eq(stringify(list), "[1,true,null,\"hi\"]")
//!
//! // parse is strict: no trailing commas, no comments, no single quotes.
//! assert(parse("[1, 2,]").is_err(), "a trailing comma is an error")
//! ```

// Nova standard library -- JSON.
//
// `nova-spec/20-STDLIB.md` section 7's type declaration compiles verbatim,
// including the variant names that shadow builtin type names. Measured, not
// assumed: `Bool(Bool)` and `String(String)` are accepted and `match` binds
// them unqualified.

/// A JSON value: null, a boolean, a number, a string, an array or an object.
pub type JsonValue =
    /// JSON's `null`.
    | Null
    /// `true` or `false`.
    | Bool(Bool)
    /// A number. JSON does not tell integers apart, so every number is a
    /// `Float`. `NaN` and the infinities have no JSON form: `stringify`
    /// writes them as `null`.
    | Number(Float)
    /// A string.
    | String(String)
    /// An array of values, in order.
    | Array([JsonValue])
    /// An object: its members' values by key. It holds one value per key,
    /// and keeps no order.
    | Object(Map<String, JsonValue>)

// `at` is a character index into the input, not a byte offset -- the parser
// walks `[Char]` -- but that is true only of an error `parse` returns.
// `FromJson::from_json`, below, constructs `JsonError` too, decoding an
// already-parsed `JsonValue` tree rather than input text: there is no
// cursor and no source position to report, so every error `from_json`
// returns carries a FIXED PLACEHOLDER, `at: 0`, never a real index.
/// The error `parse` and `FromJson::from_json` return.
pub record JsonError {
    /// What was wrong, such as `expected a value` or `trailing content`.
    pub msg: String,
    /// Where `parse` stopped in its input, counted in characters, not
    /// bytes, from 0. An error from `from_json` has no input to point into,
    /// and its `at` is always 0.
    pub at: Int,
}

// The deepest nesting `parse` accepts. RFC 8259 section 9 permits a limit --
// "An implementation may set limits on the maximum depth of nesting" -- so a
// cap conforms.
//
// 128 IS A DECLARED CONTRACT AND DELIBERATELY NOT DERIVED FROM THIS BUILD'S
// STACK. That distinction answers the objection this file used to raise against
// any cap: a stack-size threshold is not a portable budget, and a number taken
// from one machine's stack would be wrong on the next. 128 is instead the
// budget `serde_json` starts a deserializer with -- a bare `remaining_depth:
// 128` literal on a `u8` field in its `src/de.rs`, not a named constant, and
// defeatable there through `disable_recursion_limit`. Read at `master` on
// 2026-08-25 and cited as a precedent that makes 128 defensible, not as a
// standard: implementations that declare a limit pick different numbers.
//
// EACH COMPARISON NOW CARRIES A FILE AND A READ DATE, and one of them changed
// when it got one. This paragraph used to close "Jackson's
// `StreamReadConstraints.maxNestingDepth()` defaults to 1000, CPython has no
// JSON-specific constant and inherits the interpreter's recursion limit, and
// Go's `encoding/json` caps nothing." The Go half is FALSE and is retracted:
// `encoding/json` declares `const maxNestingDepth = 10000` in
// `src/encoding/json/scanner.go` and enforces it in `pushParseState`, which
// reports `exceeded max depth` (read at `master` on 2026-08-26). The others
// hold as read on 2026-08-26 -- Jackson's
// `StreamReadConstraints.DEFAULT_MAX_DEPTH` is `1000`
// (`src/main/java/com/fasterxml/jackson/core/StreamReadConstraints.java` on
// branch `2.x`), and CPython's C accelerator declares no JSON-specific
// constant and calls `_Py_EnterRecursiveCall` in `scan_once_unicode`
// (`Modules/_json.c` at `main`), so the interpreter's recursion limit is what
// bounds it. WHY THIS WAS THE ONE THAT WAS WRONG is worth keeping: the
// `serde_json` claim above carried a file, a field and a read date, the Go
// claim carried none of them, and it sat in the same sentence as if it had.
// The comparison the argument needs is unchanged, and stronger for holding a
// checked number where it used to hold an unchecked absence.
//
// For scale rather than for derivation: this build was measured parsing depth
// 5000 successfully and overflowing its stack at 6000, on an input of 12001
// characters. 128 sits far below that on the platform where it was measured,
// which is what makes it portable.
const MAX_DEPTH: Int = 128

// One pending item in `stringify`'s work list: either literal text to emit, or
// a value still to be rendered along with its nesting depth.
//
// The depth rides with the value because the guard needs one per item; a bare
// `Value(JsonValue)` cannot support it. `Pending` is a record rather than a
// second payload on the variant because Nova rejects tuples (E0900).
type Work = | Chunk(String) | Value(Pending)

record Pending { v: JsonValue, d: Int }

// The nesting depth past which `stringify` refuses to continue.
//
// This exists for CYCLES, but the guard is purely a depth cap and cannot
// tell a cyclic value from a legitimate deep one -- `stringify`'s own header
// discloses that the hard way, by counterexample. A cyclic `JsonValue` is
// constructible in ordinary Nova because arrays are heap references, and
// with the work list on the heap the pop-loop has no natural floor for one:
// it grows a net item per pop with nothing to stop it. What that ends in is
// REASONING, not a measurement here -- an unguarded run of
// `json_stringify_cycle.nova` was killed after 20 seconds still running, not
// run to completion, so "an allocator abort that prints neither a
// `nova: panic:` prefix nor a location" is the expected shape (the same
// shape `gc::alloc`'s `handle_alloc_error` produces elsewhere in this file),
// not an observed one. The recursive form this replaces died faster and just
// as fatally, with no message either -- THAT half is measured:
// `json_stringify_cycle.nova` aborted by stack overflow when it still ran
// through the recursive `stringify`, before this guard existed. A named
// panic is better than both, and it is the best available, because
// `stringify` returns `String` and has no error channel to report a cycle
// through.
//
// GUARDED ON DEPTH, NOT ON WORK-LIST LENGTH. A cycle grows nesting depth
// without bound; a wide, shallow document grows only length, and a length
// bound would refuse legitimate input. 100_000 is far above any depth a
// caller is likely to build deliberately, and far above anything `parse` can
// hand over, since `parse` stops at `MAX_DEPTH`. The bound itself is pinned,
// not merely approached, FOR ONE SHAPE: `json_render_guard_under.nova`
// renders successfully at exactly `MAX_RENDER_DEPTH` levels of arrays around
// a leaf and `json_render_guard.nova` panics one level past it -- superseding
// an earlier, unpinned "measured rendering depth 30000" this comment used to
// cite here. A chain whose innermost container is EMPTY reaches its boundary
// one level higher, and that shape is pinned by nothing; the counting rule
// and its measured boundaries are at the guard itself.
//
// DEPTH AND WIDTH ARE NOT INDEPENDENT, which the paragraph above reads as if
// they were. Choosing depth over length is still right for the reason given
// -- a length bound refuses legitimate wide input and a depth bound does not
// -- but "a wide, shallow document grows only length" says nothing about the
// case this guard exists for. A cyclic value grows the work list by ABOUT
// TWICE THE CONTAINER WIDTH PER LEVEL, so its cost is a product of the two
// and not a function of the bound alone.
//
// THE GUARD'S COST GROWS WITH THE PRODUCT OF ITS BOUND AND THE CYCLE'S
// WIDTH. This used to read "The guard's cost is proportional to its bound: a
// cycle runs one iteration per level before firing" -- RETRACTED: that is
// true only at width 1. `json_stringify_cycle.nova` builds a width-1 cycle and
// no fixture in the tree builds a wider one; that is a predicate to re-check by
// looking for a fixture that closes a cycle, not a tally to trust from here.
// (This sentence said "the only width either cycle fixture builds" until
// 2026-08-26, quantifying over two fixtures where only `json_stringify_cycle`
// closes a cycle at all: `json_render_guard.nova`'s own header calls its value
// acyclic, and so does `stringify`'s header here. A count over a set it had not
// checked, written in the same wave that removed one from `stringify`'s
// header.)
// Popping a container of width w pushes about `2w + 1` items (w children,
// w - 1 separators, two brackets) and removes one, so the list gains about
// 2w per level and nothing is reclaimed before the panic. Those pushes happen
// at every level whatever the cycle's position, so both the peak work-list
// size and the time to fire grow with `MAX_RENDER_DEPTH` TIMES the width; the
// position within the container moves the constant, not the growth.
//
// MEASURED against this build, min of 3 after a warm-up, cyclic arrays that
// fire the guard: width 1 (`json_stringify_cycle.nova`'s shape) 1.78 s;
// width 3 with the cycle at the last member 4.71 s; width 10 at the last
// member 14.24 s; width 10 at the FIRST member 7.14 s. Absolutes are
// machine-specific -- an earlier session measured the width-1 case at about
// 2 s -- and the width trend is the claim. This paragraph used to say the
// cost "is milliseconds with the buffer this file now uses", which stays
// RETRACTED as false by three orders of magnitude, and used to compare it
// against "more than two minutes when the same walk accumulated by
// interpolation", which stays retracted too: no such version of this walk
// was ever built in this tree to measure, so that number described a
// design-time scratch probe rather than a state this code ever shipped in.
//
// SO THE NAMED FAILURE IS REACHABLE FOR A NARROW CYCLE AND DEGRADES TOWARD
// THE UNNAMED ONE AS WIDTH GROWS. That degradation is REASONING past width
// 10, not a measurement: the work list's peak is the same allocator pressure
// this guard was introduced to replace, so for a wide enough cycle the
// process reaches `gc::alloc`'s `handle_alloc_error` -- printing neither a
// `nova: panic:` prefix nor a location -- before it reaches this panic.
// Nothing here measures where that crossover sits, and no fixture builds a
// wide cycle, so the guard's promise of an immediate named failure is a
// promise about narrow cycles.
const MAX_RENDER_DEPTH: Int = 100_000

// Render `v` as JSON text.
//
// This used to open "TOTAL OVER THE VALUES IT RETURNS FROM. For every
// acyclic `JsonValue` there is a rendering, and there is no error channel to
// report a failure with." That is FALSE, and the counterexample ships in
// this tree: `json_render_guard.nova` builds an ACYCLIC value -- a fresh
// array literal per level, no cycle -- at depth 100001, and it panics rather
// than rendering, because the guard below tests nesting depth alone and
// never inspects for a cycle. "Acyclic" does not rescue the claim.
//
// THE HONEST FORM: a rendering exists for every value whose nesting depth is
// at most `MAX_RENDER_DEPTH`, cyclic or not, and there is no error channel to
// report a refusal with. Depth is no longer a stack cost -- the arms below
// push pending work onto a heap list instead of recursing -- but nesting is
// bounded by that DECLARED CAP, not by memory: this also used to say
// "nesting is bounded by memory rather than by stack size", which is wrong
// the same way. `json_render_guard_under.nova` pins the cap itself rendering
// successfully at exactly `MAX_RENDER_DEPTH` levels, and `json_render_deep.nova`
// pins a shallower value, 20000 levels, rendering too -- both well past
// where the recursive form this replaces overflowed the stack, measured
// between 10000 and 16000.
//
// "NESTING DEPTH" IN THAT SENTENCE IS DEPTH AS THE GUARD COUNTS IT, and it
// does not count every shape the same way: a leaf costs a level and an empty
// innermost container does not, so a chain of `MAX_RENDER_DEPTH + 1`
// containers ending empty renders while one of `MAX_RENDER_DEPTH + 1` ending
// in a leaf panics. The rule, the measured boundaries and which of the two
// shapes any fixture pins are all at the guard itself, below.
//
// THE RESIDUALS ARE NAMED RATHER THAN LEFT TO INFERENCE, AND THE ROSTER IS
// WHAT TO READ. This sentence used to continue "and there are three, not
// two" -- a corrected count over a set a later increment can add to, sitting
// in shipped code against this file's own stated preference for a roster with
// no count. The count is dropped rather than corrected a second time, and the
// last member below is what a count would have gone stale on immediately:
//   - a cyclic value never terminates on its own and is refused by
//     `MAX_RENDER_DEPTH` with a panic;
//   - THE SAME CAP, by the same mechanism, refuses a legitimate ACYCLIC
//     value past `MAX_RENDER_DEPTH` too -- the guard cannot and does not tell
//     the two apart, which is exactly the counterexample the opening
//     paragraph retracts;
//   - heap exhaustion still aborts the process without a `JsonError`,
//     because `gc::alloc` calls `handle_alloc_error` and no alloc-error hook
//     is installed; there is no collect-and-retry on that path, and where
//     `gc.rs`'s `stack_base` has no implementation the collector is a no-op,
//     so nothing is reclaimed until the process exits;
//   - a WIDE cycle can reach that same allocator abort BEFORE it reaches the
//     named panic, because the work list grows with depth times container
//     width rather than with depth alone. `MAX_RENDER_DEPTH`'s own note
//     carries the measurements and marks where reasoning takes over from
//     them.
//
// `parse` is bounded differently, by its own declared `MAX_DEPTH` -- the two
// caps are independent numbers, chosen for independent reasons. `parse`
// consumes untrusted text, where a cap is a feature in its own right. This
// function's cap exists for the residual named above -- there is no error
// channel to report a cycle through, and nesting depth alone cannot tell a
// legitimate deep value from a cyclic one -- not because refusing a
// caller's own construction is a feature here too.
//
// This header used to close saying "The numbers for all four accumulators
// in this file, and why these two are the ones that matter, are at
// `scan_str`." That was false twice over: the roster at `scan_str` is by
// name, not by count, and this function's own accumulator numbers were
// deleted in the same change that retired its two `out = "${out}..."` arms,
// surviving nowhere in this file. The accumulator cost these arms used to
// carry is simply gone; the roster of what changed -- by name, not count --
// is at `scan_str`.
//
// A SEPARATE, LATER SAVING ON THIS SAME FUNCTION, PAST THAT MIGRATION. The
// migration above made the general path below LINEAR where interpolation
// had made it QUADRATIC; it did not change how many times a top-level
// scalar passes through that path on its way to a `String`. This increment
// does not make that path cheaper -- it SKIPS the path entirely for a
// top-level scalar, falling into it only for `Array`/`Object` or a scalar
// reached through either. `tests/runtime/json_stringify.nova` is what
// checks the fast path just below. Only the `String` arm's saving was
// measured; `Null`, `Bool` and `Number` got the same match arm with no
// separate measurement.
/// Returns `v` as compact JSON text, with no spaces or line breaks.
///
/// In a string, `"` and `\` take a backslash; backspace, form feed, line
/// feed, carriage return and tab are written `\b`, `\f`, `\n`, `\r` and
/// `\t`; every other character below U+0020 is written `\u00` and two
/// lowercase hex digits; and every other character is written as it is. A
/// number is written in plain decimal, never with an exponent, as the
/// shortest text that reads back as the same `Float`: `1.0` gives `1`, and
/// `1e21` gives a 1 and 21 zeros. `NaN` and the infinities are written as
/// `null`. An object's members come in an order that can change from one run
/// of a program to the next.
///
/// Panics if arrays and objects nest more than 100,000 deep around a value,
/// or more than 100,001 deep when the innermost one is empty. A value that
/// contains itself, such as an array placed inside itself, panics by the
/// same rule.
pub fn stringify(v: JsonValue) -> String {
    // A TOP-LEVEL SCALAR IS ALREADY FINISHED, so return it rather than
    // copying it twice. The general path below hands `buf_push_str` exactly
    // these values, and `buf_push_str` appends them character by character
    // into `out`, which `vec_chars_to_string` then drains back into a
    // `String` -- two round trips through a `Vec<Char>`, plus two `Vec`
    // allocations and the `Work`/`Pending` wrappers, for a value that needs
    // none of them.
    //
    // Byte-identical, argued in this increment's spec rather than assumed:
    // the depth guard cannot fire here because it tests
    // `d > MAX_RENDER_DEPTH` and a top-level value carries `d = 0`;
    // `buf_push_str` into an empty buffer is a pure copy; and draining those
    // characters back is lossless for anything already a `String`, which
    // `crates/nova-runtime/src/lib.rs`'s
    // `str_from_chars_round_trips_and_substitutes_invalid_scalars` pins.
    //
    // `tests/runtime/json_stringify.nova` pins each arm's output at TOP
    // LEVEL; `tests/runtime/json_round_trip.nova` pins `Bool` and `String`
    // INSIDE a container. A DELETED arm here is caught by NEITHER -- it
    // falls through the catch-all into the general path below and renders
    // IDENTICAL text, which is fine, because this path only shadows
    // behaviour the general path already has and adds none of its own. A
    // WRONG-RENDERING arm is different and IS caught: mutating the
    // `String` arm to return its argument unquoted failed six tests. Only
    // the `String` arm's saving was measured.
    match v {
        Null => return "null"
        Bool(b) => return if b { "true" } else { "false" }
        Number(n) => return number_to_json(n)
        String(s) => return quote(s)
        _ => {}
    }
    let mut out: Vec<Char> = Vec::new()
    let mut stack: Vec<Work> = Vec::new()
    stack.push(Value(Pending { v: v, d: 0 }))
    while stack.len() > 0 {
        match stack.pop() {
            None => out = out
            Some(w) => match w {
                Chunk(s) => buf_push_str(out, s)
                Value(p) => {
                    // HOISTED, out of the `Array` and `Object` arms below, where this
                    // used to live as two textually identical blocks (one per arm).
                    // Duplicated, deleting one left the other -- half a mutation the
                    // plan specified was invisible to every fixture, because no
                    // fixture exercised the `Object` arm's copy on its own. A single
                    // check here makes a half-deletion structurally impossible rather
                    // than merely detectable, and it prices the guard once per pop
                    // instead of once per container pop.
                    //
                    // EQUIVALENT TO THE OLD PER-ARM CHECK ON A CHAIN THAT ENDS IN A
                    // LEAF, AND NOT IN GENERAL. This heading used to read "EQUIVALENT
                    // TO THE OLD PER-ARM CHECK, `p.d + 1 > MAX_RENDER_DEPTH`, by
                    // derivation, not by assumption", unqualified, over a derivation
                    // that was already scoped to leaf-terminated chains in its own
                    // words. RETRACTED as an unscoped claim: the two forms differ, by
                    // exactly one level, on a chain whose innermost container is
                    // EMPTY.
                    //
                    // The derivation, for a chain of k containers ending in a LEAF.
                    // Children are pushed with `d = p.d + 1`, so the old check fired
                    // at the innermost container's OWN pop when k > MAX_RENDER_DEPTH
                    // -- the same k that chain's leaf carries as its `d` once pushed
                    // and later popped. Checking `p.d > MAX_RENDER_DEPTH` here, at
                    // every pop rather than only at a container's, fires at that same
                    // leaf for the same k, one pop later than before, and the
                    // threshold itself does not move.
                    //
                    // WHERE THE TWO DIVERGE. An empty innermost container pushes no
                    // child at all, so there is no "one pop later" to arrive at: the
                    // deepest `d` any pop sees is k - 1, and this check fires only
                    // once k - 1 > MAX_RENDER_DEPTH. The old per-arm check ran at that
                    // container's own pop and tested `p.d + 1`, so on the same chain
                    // it fired at k > MAX_RENDER_DEPTH, as it did for the leaf shape.
                    // The hoist therefore ADMITS one level more than the old form on
                    // an empty-innermost chain, and no fewer on any shape: it can
                    // newly accept a value and cannot newly refuse one.
                    //
                    // SO THIS CAP HAS THE SAME ASYMMETRIC COUNTING RULE `parse`'S
                    // CAP HAS. A leaf costs a level; an empty innermost container does
                    // not. MEASURED against this build, arrays one deep per level:
                    // 100000 containers around a leaf render, 200001 characters,
                    // exit 0, and 100001 panic; 100001 containers ending in an empty
                    // array render, 200002 characters, exit 0, and 100002 panic. The
                    // derivation applies to an empty innermost OBJECT identically,
                    // since either empty container pushes no `Value` -- that half is
                    // reasoning, and the measured pair above is arrays.
                    //
                    // THAT RULE IS PINNED IN ONE SHAPE ONLY, WHICH IS DISCLOSED
                    // RATHER THAN FIXED. `parse` states its rule at `parse_value` and
                    // pins both of its shapes, `json_depth_leaf.nova` and
                    // `json_depth_empty.nova`. Here `json_render_guard_under.nova`
                    // and `json_render_guard.nova` pin the leaf boundary and nothing
                    // pins the empty-innermost one, so a change that moved it by a
                    // level would pass the whole suite. No fixture is added for it:
                    // an earlier ruling declined render fixtures on suite cost, each
                    // of that pair already costing seconds, and that trade has not
                    // changed.
                    //
                    // Re-verified after hoisting rather than only re-derived: the
                    // leaf pair still renders at `MAX_RENDER_DEPTH` and panics one
                    // level past it, and the same pair of depths was independently
                    // confirmed for an OBJECT chain, built the same way one key deep
                    // per level, by a scratch run against this build rather than
                    // shipped as a third fixture.
                    if p.d > MAX_RENDER_DEPTH {
                        panic("stringify: nesting too deep or cyclic value")
                    }
                    match p.v {
                        Null => buf_push_str(out, "null")
                        Bool(b) => buf_push_str(out, if b { "true" } else { "false" })
                        Number(n) => buf_push_str(out, number_to_json(n))
                        String(s) => buf_push_str(out, quote(s))
                        Array(xs) => {
                            stack.push(Chunk("]"))
                            let mut i = xs.len() - 1
                            while i >= 0 {
                                stack.push(Value(Pending { v: xs[i], d: p.d + 1 }))
                                if i > 0 { stack.push(Chunk(",")) }
                                i = i - 1
                            }
                            stack.push(Chunk("["))
                        }
                        Object(m) => {
                            let ks = m.keys()
                            stack.push(Chunk("}"))
                            let mut i = ks.len() - 1
                            while i >= 0 {
                                // The `None` arm is unreachable THROUGH `Map`'s OWN API: `ks`
                                // is `m.keys()`, which returns exactly the occupied slots, and
                                // nothing mutates `m` between that call and this lookup.
                                //
                                // This used to read "The `None` arm is UNREACHABLE", flatly.
                                // That is wrong: `Map` exposes every field and Nova has no
                                // field privacy, so a forged map with a live slot off its own
                                // probe chain makes `keys()` return a key `get()` misses.
                                // Measured -- `json_object_forged_map.nova` builds one: a live
                                // member whose key sits at a `ks` position greater than 0 but is
                                // unreachable through `get`. Which key ends up there depends on
                                // the per-process hash seed, so this comment names the
                                // arrangement rather than the key. So the arm is a real path and
                                // the separator's position is load-bearing, not cosmetic:
                                // it is pushed OUTSIDE this match, so a missed member leaves a
                                // comma with no member after it, exactly as before this
                                // function stopped recursing.
                                //
                                // Moving the push inside the `Some` arm would emit valid JSON
                                // instead FOR THIS FIXTURE'S ARRANGEMENT, where the missed key
                                // sits at a `ks` position greater than 0 -- which key that is
                                // depends on the per-process hash seed, so naming one here would
                                // be true of most processes and false of some -- it is not
                                // true in general. A miss AT position 0 emits the same string
                                // either way: the separator there is gated on `i > 0`, which is
                                // false for position 0 regardless of which arm the push sits
                                // in, so that position contributes no comma under either version
                                // and the two forms coincide. `json_object_forged_map.nova`'s
                                // arrangement is what makes the two versions distinguishable at
                                // all, and it is refused by that fixture's golden rather than by
                                // this comment. Repairing the fallback is a separate change with
                                // its own record; the other fallbacks in this file that were
                                // written for the same reason degrade safely instead -- `bump`
                                // refuses to advance, `span` returns the empty string,
                                // `utf8_bytes` returns 0xFF precisely to force a downstream
                                // rejection -- which is the pattern this one departs from.
                                //
                                // KNOWN UNPINNED, NAMED HERE RATHER THAN FIXED WITH A
                                // FIXTURE. Changing the `d: p.d + 1` below to `d: p.d`
                                // leaves every `json_*` test passing -- goldens
                                // byte-identical and the expected-failure assertions
                                // still met -- measured, by applying that mutation,
                                // rebuilding, and re-running them. (This said "all
                                // fourteen `json_*` goldens" until 2026-08-26. That number
                                // counted the FIXTURES; the expected-failure ones carry no
                                // golden, so it named the wrong set. The substance held,
                                // and the count is dropped rather than corrected -- the
                                // predicate is every `json_*` test, which is re-runnable.)
                                // What it would break if wrong is
                                // `MAX_RENDER_DEPTH` itself for object-nested values:
                                // depth stops advancing through objects, so no object
                                // chain reaches the guard at any length. Measured on the
                                // mutant: an object chain of 100001 levels renders
                                // 600007 characters and exits 0, where this build panics
                                // on the same value. A cyclic value reached through
                                // objects would then run to the allocator abort with no
                                // message, which is what the guard exists to prevent.
                                // What would catch it is a fixture nesting through
                                // OBJECTS past the cap; the ruling that declined render
                                // fixtures on suite cost is why there is none, and a
                                // disclosed hole beats an undisclosed one.
                                match m.get(ks[i]) {
                                    Some(val) => {
                                        stack.push(Value(Pending { v: val, d: p.d + 1 }))
                                        stack.push(Chunk(":"))
                                        stack.push(Chunk(quote(ks[i])))
                                    }
                                    None => out = out
                                }
                                if i > 0 { stack.push(Chunk(",")) }
                                i = i - 1
                            }
                            stack.push(Chunk("{"))
                        }
                    }
                }
            }
        }
    }
    vec_chars_to_string(out)
}

// JSON has no NaN and no infinity, so a non-finite number cannot be rendered.
// This emits `null` instead, which is what most codecs do and which keeps
// `stringify` total over values -- depth is a separate matter, see its own
// note -- and its output always valid JSON. It is DELIBERATELY LOSSY:
// a round trip through a non-finite number does not preserve it.
//
// `n - n != 0.0` is the finiteness test. For any finite `n` the difference is
// exactly zero; for either infinity it is NaN, and for NaN it is NaN, and NaN
// compares unequal to everything including itself. This avoids needing an
// `is_nan` the standard library does not have.
fn number_to_json(n: Float) -> String { if n - n != 0.0 { "null" } else { "${n}" } }

// A growable character buffer, appended to and drained once.
//
// `Vec<Char>` plus `str_from_chars` IS the growable string buffer this file's
// cost note used to say the language did not have. `Vec` grows by doubling
// (`std/collections`) so appending is amortised O(1), and `str_from_chars`
// encodes a `[Char]` in one pass, so N appends plus one drain is linear where
// `out = "${out}..."` is quadratic. Measured, this compiler, debug build, with
// a 129 ms compile baseline included: pushing n characters took 142, 135, 132
// and 142 ms at n = 8000, 16000, 32000 and 64000 -- flat across an eightfold
// range -- against 164, 249, 447 and 752 ms for interpolation.
//
// The parameter is `mut b` and not `b`. A plain `b: Vec<Char>` fails
// `E0060: Vec_T.push mutates its receiver, but b is immutable`. Records are
// heap objects passed by pointer, so the mutation reaches the caller --
// including `Vec::push`'s reassignment of its backing array when it grows,
// which is the case that would break a value-semantics assumption. Measured,
// not assumed.
fn buf_push_str(mut b: Vec<Char>, s: String) {
    let cs = s.chars()
    let mut i = 0
    while i < cs.len() {
        b.push(cs[i])
        i = i + 1
    }
}

// The buffer's contents as a `String`. The same manoeuvre `vec_to_array` runs
// for `JsonValue`: allocate an exact-length array, copy, hand it on.
//
// THE EXACT-LENGTH COPY IS LOAD-BEARING and is not redundant with the `Vec`'s
// own backing array: that array is the buffer's CAPACITY, not its length, so
// encoding it directly would append the filler character of every unused slot.
// A mutation that returns the backing array PASSES at any length that happens
// to equal capacity, and capacity is always a power of two here, so exercise
// the drain at a length that is not one.
//
// Reads the backing array directly, not through `Vec::get`. `i` runs strictly
// below `b.len()`, so the `Option` that `get` returns could never be `None`
// here -- and every `Some` is a heap allocation. Going through `get` cost one
// 16-byte object per output character, which
// `examples/05-json-api/BENCHMARK.md` measured as all of `stringify`'s
// per-character object count. For any `Vec` changed only through its own
// methods the backing array is at least `len` long; the index is bounds
// checked regardless, exactly as `get`'s own read of it was.
fn vec_chars_to_string(b: Vec<Char>) -> String {
    let n = b.len()
    let mut out = [' '; n]
    let mut i = 0
    while i < n {
        out[i] = b.data[i]
        i = i + 1
    }
    str_from_chars(out)
}

// Escape `s` as a JSON string literal, quotes included: `"` and `\` take a
// backslash, the five control characters with a short escape take it, and
// every other control character below `0x20` becomes `\u00XX` in lowercase
// hex (RFC 8259). A runtime builtin does it: it counts the escaped length,
// then writes the result once. It used to scan a `[Char]` here and rebuild
// any string that needed escaping one character at a time.
fn quote(s: String) -> String { json_quote(s) }

// Parse `s` as JSON. `JsonError::at` is a character index -- a count of Unicode
// scalar values, matching the `[Char]` the parser walks.
//
// A JSON text is ONE value, so trailing content is an error rather than a
// stopping point: `parse` skips whitespace, parses a single value, skips
// whitespace again, and rejects anything left over.
//
// It is deliberate that the trailing-content check runs even when the value
// walk has already failed: it is the second caller of `fail` that makes
// `fail`'s first-failure-wins guard observable, and so the thing that keeps
// that guard tested rather than merely written. `err` is then inspected ONCE,
// here, which is the whole point of carrying the failure in a field.
/// Parses `s` as JSON text: one value, with nothing after it but whitespace.
///
/// Parsing is strict, as RFC 8259 asks. There are no comments, no trailing
/// commas, no single quotes, no unquoted keys, no `NaN` or `Infinity`, and no
/// leading `+` or leading zeros in numbers. Whitespace is only space, tab,
/// line feed and carriage return, so a leading byte order mark is an error.
/// A string must escape its control characters below U+0020; a `\u` escape
/// may name any character, one beyond U+FFFF as a surrogate pair, and an
/// unpaired surrogate is an error. A number too large in magnitude for a
/// `Float` becomes an infinity, and one too close to zero becomes 0. Of two
/// members of an object with the same key, the last is kept. Arrays and
/// objects may nest 128 deep around a value, or 129 deep when the innermost
/// one is empty.
///
/// Returns a `JsonError` if `s` is not such a value, saying what is wrong and
/// where.
///
/// Walking a parsed value, and where an error stopped:
///
/// ```
/// // Walk a parsed value by matching on its variants.
/// let doc = parse("{\"tags\": [\"fast\", \"small\"]}").unwrap()
/// let tags = match doc {
///     Object(fields) => fields.get("tags").unwrap()
///     _ => panic("expected an object")
/// }
/// let count = match tags {
///     Array(items) => items.len()
///     _ => 0
/// }
/// assert_eq(count, 2)
///
/// // An error says what is wrong, and where parse stopped, in characters.
/// match parse("[1, 2,]") {
///     Ok(_) => panic("a trailing comma is not JSON")
///     Err(e) => assert_eq(e.at, 6) // the `]` where a value should be
/// }
/// ```
pub fn parse(s: String) -> Result<JsonValue, JsonError> {
    let mut p = P { cs: s.chars(), pos: 0, err: None }
    p.skip_ws()
    let v = p.parse_value(0)
    p.skip_ws()
    if !p.at_end() { p.fail("trailing content") }
    match p.err {
        Some(e) => Err(e)
        None => Ok(v)
    }
}

// The parser carries its failure in a field instead of returning `Result` at
// every level. Nova has no `?` operator -- measured against this compiler, not
// assumed: `?` parses and then typeck rejects it with
// `error[E0900]: the `?` operator are not supported yet` -- so threading a
// `Result` through a recursive descent would cost a `match` at every call site
// and bury the grammar in plumbing.
//
// `err` is set at most once: the FIRST failure wins, so a downstream symptom
// cannot overwrite the real cause. Every step that could read past the end
// checks `err` first and no-ops, returning `Null` -- after a failure the
// remaining walk is inert rather than wrong.
record P { cs: [Char], pos: Int, err: Option<JsonError> }

impl P {
    // Record a failure at the cursor, unless one is already recorded.
    //
    // The guard IS "first failure wins". `parse` runs its trailing-content
    // check even after the value walk has failed, so without the guard that
    // check would overwrite the real cause with a consequence of it: `01` would
    // report `trailing content` at 1 rather than the leading zero that actually
    // stopped the scanner there.
    //
    // `01` and NOT `+1` for that example. `+1` no longer reaches `scan_number`
    // at all -- `parse_value`'s dispatch rejects a leading `+` as `expected a
    // value` before any digit is looked for -- so it no longer illustrates a
    // cause the guard protects, only one it renames. That correction was applied
    // to `parse`'s comment, `scan_number`'s comment and the fixture header in
    // the same change that made it necessary, and missed here.
    //
    // Measured, so the reach is not assumed: dropping this guard rewrites every
    // golden line whose rejection stops the cursor short of the end of the
    // input AND whose value walk had already failed. The predicate is the
    // durable part; the count is not, and this file's parse fixtures have grown
    // since it was taken. Re-measure by deleting the guard rather than citing a
    // number from here.
    //
    // Both halves of that predicate are load-bearing. The trailing-content
    // goldens in `json_parse_values.stdout` also stop short of the end of the
    // input, and they are UNCHANGED, because for them the trailing-content
    // error IS the first failure, so there is nothing for the guard to keep.
    // Reading the clause without its second half therefore OVER-PREDICTS the
    // rewrite, by exactly that set of goldens.
    //
    // The two numbers this sentence used to end on are dropped rather than
    // refreshed. It illustrated the over-prediction as one count against
    // another, and the smaller of the two was the count the paragraph above
    // replaced with a predicate -- so the larger had nothing left to be
    // larger than, and citing either would reintroduce exactly the staleness
    // that replacement removed. Delete the guard and re-measure both together.
    fn fail(mut self, m: String) {
        if self.err.is_none() { self.err = Some(JsonError { msg: m, at: self.pos }) }
    }

    fn at_end(self) -> Bool { self.pos >= self.cs.len() }

    // The character at the cursor, or `'\0'` past the end.
    //
    // `'\0'` is a sentinel, not a distinguished value: a real NUL in the input
    // reads identically here. That is harmless, because NUL appears in no JSON
    // token -- it is legal only inside a string literal, and RFC 8259 requires
    // even that one escaped -- so every caller that meets a NUL rejects it,
    // exactly as it rejects the end of the input. A caller that needs the two
    // told apart asks `at_end` instead.
    fn peek(self) -> Char { if self.at_end() { '\0' } else { self.cs[self.pos] } }

    // Advance one character, never past the end, so `pos` stays a valid bound
    // for `span` below. Every caller in this module bumps only after `peek`
    // returned a real character, so today the guard is defensive rather than
    // load-bearing; it is written so that a scanner looping on `bump` cannot
    // walk off the array.
    fn bump(mut self) { if !self.at_end() { self.pos = self.pos + 1 } }

    // Characters `start..end` as a `String`, `end` exclusive.
    //
    // `std/strings` runs the same algorithm over a plain `[Char]`
    // (`chars_to_string`), but it is private to that module and Nova has no
    // cross-module access to a private item, so these five lines are a
    // deliberate duplication rather than an oversight. `[' '; n]` needs a
    // filler and a repeat literal is the only way to allocate a runtime-length
    // array; every slot is overwritten immediately below, so the space never
    // survives.
    //
    // NEITHER half of the `n <= 0` guard is reachable today -- for a stronger
    // reason than in `chars_to_string`, whose note on exactly this guard the
    // duplication above had dropped. `scan_number` is this helper's only
    // caller, `pos` never moves backwards, and the call site sits past at least
    // one consumed digit, so `n` is always 1 or more. Kept for the reason
    // `chars_to_string` keeps it: a future caller of this module-private helper
    // may not bound its own span as carefully. At `n == 0` the general path
    // would return "" unaided anyway, so that half only skips an allocation --
    // measured, not assumed: `[' '; 0]` is a legal repeat literal of length 0,
    // the `0..0` loop is vacuous, and `nova_rt_str_from_chars` of an empty
    // array builds an empty `String`.
    fn span(self, start: Int, end: Int) -> String {
        let n = end - start
        if n <= 0 { return "" }
        let mut out = [' '; n]
        for i in 0..n { out[i] = self.cs[start + i] }
        str_from_chars(out)
    }

    // Skip JSON's whitespace: space, horizontal tab, line feed, carriage
    // return. THAT IS THE ENTIRE SET -- RFC 8259 permits no others, so a
    // vertical tab, a form feed or a non-breaking space is a syntax error and
    // must not be skipped here.
    fn skip_ws(mut self) {
        let mut going = true
        while going {
            let c = self.peek()
            if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
                self.bump()
            } else {
                going = false
            }
        }
    }

    // A JSON number: `-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?`.
    //
    // Validates by walking characters, then slices the matched span and hands
    // it to `str_to_float`. Validating FIRST is what makes that intrinsic's
    // `0.0`-on-rejection fallback unreachable: every span reaching it is a text
    // Rust's `f64` parser also accepts, since Rust's accepted grammar is a
    // superset of the one above.
    //
    // A magnitude no `Float` can hold is ACCEPTED, not rejected: JSON's grammar
    // sets no range, and Rust's parser saturates rather than failing. Measured
    // against this compiler: `1e999` parses to infinity, which `stringify` then
    // renders `null` via `number_to_json`'s non-finite branch, and `1e-999`
    // parses to `0`. Both are lossy round trips. No fixture pins either -- they
    // are recorded here as measured behaviour, not as tested behaviour.
    //
    // Each rejection is a shape JSON forbids that a laxer reader would take:
    // a leading zero followed by a digit (`01` is two tokens, not one number),
    // a `.` with no digit after it, and an exponent with no digits.
    //
    // `+1` and `.5` never arrive here. `parse_value` dispatches to this scanner
    // only on `-` or a digit, so neither a leading `+` (only `-` is a sign) nor
    // a bare `.` gets past the dispatch, and both are reported there as
    // `expected a value` instead. What still reaches the `expected a digit`
    // branch below is a `-` with no digit after it, which is the `-` case in
    // `json_parse_numbers.nova`.
    fn scan_number(mut self) -> JsonValue {
        if self.err.is_some() { return Null }
        let start = self.pos
        if self.peek() == '-' { self.bump() }
        if self.peek() == '0' {
            self.bump()
            if is_digit(self.peek()) {
                self.fail("a leading zero cannot be followed by another digit")
                return Null
            }
        } else if is_digit(self.peek()) {
            while is_digit(self.peek()) { self.bump() }
        } else {
            self.fail("expected a digit")
            return Null
        }
        if self.peek() == '.' {
            self.bump()
            if !is_digit(self.peek()) {
                self.fail("expected a digit after the decimal point")
                return Null
            }
            while is_digit(self.peek()) { self.bump() }
        }
        if self.peek() == 'e' || self.peek() == 'E' {
            self.bump()
            if self.peek() == '+' || self.peek() == '-' { self.bump() }
            if !is_digit(self.peek()) {
                self.fail("expected a digit in the exponent")
                return Null
            }
            while is_digit(self.peek()) { self.bump() }
        }
        Number(str_to_float(self.span(start, self.pos)))
    }

    // One JSON value, dispatched on the character at the cursor.
    //
    // The dispatch is unambiguous because JSON's six value kinds start on
    // disjoint character sets -- seven of them, since `true` and `false` are one
    // kind on two letters: `n`, `t`, `f`, `"`, `[`, `{`, and `-` or a digit.
    // Anything else is not the start of a value, which is where a leading `+`, a
    // bare `.`, a stray `]` and an empty input all land.
    //
    // `n`/`t`/`f` hand off to `keyword`, which matches the WHOLE spelling.
    // Dispatching on the first letter and then consuming a fixed count would
    // read `nxxx` as `null`.
    //
    // NESTING DEPTH IS CAPPED IN THIS DIRECTION. This recurses through
    // `scan_array` and `scan_object` back into itself, and the check above
    // bounds that recursion at `MAX_DEPTH`, reporting an ordinary `JsonError`
    // through the same channel as a syntax error. `stringify` is bounded
    // differently -- it does not recurse at all -- and says so at its own
    // header rather than here, so this file no longer states one property for
    // both directions.
    //
    // WHAT THE NUMBER COUNTS. A leaf costs a depth level and an empty
    // innermost container does not, because the empty-container fast paths in
    // `scan_array` and `scan_object` return without re-entering this function.
    // So 128 containers wrapping a leaf is accepted and 129 is not, while 129
    // containers ending empty is accepted and 130 is not. Both boundaries are
    // pinned: `json_depth_leaf.nova` and `json_depth_empty.nova`.
    //
    // RETRACTED, quoted so the retraction is legible. This paragraph used to
    // say a cap was declined deliberately because "a stack-size artefact is not
    // a budget a cap can be derived from", and that a cap "needs an API choice
    // this increment's scope did not include, namely which depth and whether
    // exceeding it is an ordinary `JsonError` or something a caller must
    // distinguish from bad syntax." The objection was right and is honoured
    // rather than overruled: `MAX_DEPTH` is a declared contract, not a number
    // read off this build's stack. Both deferred questions are answered -- the
    // counting rule is above, and the failure is an ordinary `JsonError`,
    // deliberately indistinguishable in shape from bad syntax, so a caller that
    // must tell them apart matches the message. That is a weak contract and is
    // named as one.
    //
    // The claim that no fixture could pin the threshold was true of a
    // STACK threshold -- a fixture that crashed the process would fail the
    // suite by construction -- and stops applying to a declared constant.
    fn parse_value(mut self, d: Int) -> JsonValue {
        if self.err.is_some() { return Null }
        if d > MAX_DEPTH {
            self.fail("maximum nesting depth exceeded")
            return Null
        }
        let c = self.peek()
        if c == 'n' { return self.keyword("null", Null) }
        if c == 't' { return self.keyword("true", Bool(true)) }
        if c == 'f' { return self.keyword("false", Bool(false)) }
        if c == '"' {
            let s = self.scan_str()
            return String(s)
        }
        if c == '[' { return self.scan_array(d) }
        if c == '{' { return self.scan_object(d) }
        if c == '-' || is_digit(c) { return self.scan_number() }
        self.fail("expected a value")
        Null
    }

    // A literal name -- `null`, `true` or `false` -- matched character by
    // character against `word`, returning `v` only if all of it is there.
    //
    // The mismatch is reported AT the offending character rather than at the
    // start of the name, so `nul` fails at 3 (where the fourth character should
    // be) and not at 0.
    fn keyword(mut self, word: String, v: JsonValue) -> JsonValue {
        let ws = word.chars()
        let mut i = 0
        while i < ws.len() {
            if self.peek() != ws[i] {
                self.fail("expected ${word}")
                return Null
            }
            self.bump()
            i = i + 1
        }
        v
    }

    // A JSON array, cursor on `[`.
    //
    // The empty array is checked BEFORE the element loop, which is what makes
    // `[]` legal while `[1,]` and `[,1]` are not: once the loop is entered, a
    // value is required, because RFC 8259's `array` repetition ends on a value
    // and never on a separator.
    fn scan_array(mut self, d: Int) -> JsonValue {
        self.bump()
        self.skip_ws()
        if self.peek() == ']' {
            self.bump()
            let empty: [JsonValue] = []
            return Array(empty)
        }
        let mut items: Vec<JsonValue> = Vec::new()
        let mut going = true
        while going {
            let v = self.parse_value(d + 1)
            if self.err.is_some() { return Null }
            items.push(v)
            self.skip_ws()
            let c = self.peek()
            if c == ',' {
                self.bump()
                self.skip_ws()
            } else if c == ']' {
                self.bump()
                going = false
            } else {
                self.fail("expected ',' or ']' in an array")
                return Null
            }
        }
        Array(vec_to_array(items))
    }

    // A JSON object, cursor on `{`.
    //
    // Mirrors `scan_array`, with a member in place of a value. The key check is
    // explicit here rather than left to `scan_str`, so a missing key reports
    // `expected an object key` -- RFC 8259 admits only a `string` as a member
    // name, so `{a:1}` is an error and not a laxer reader's bare word.
    //
    // A repeated key keeps the LAST value, because `Map::insert` replaces.
    // RFC 8259 section 4 sanctions no particular choice -- it calls the
    // behaviour of a non-unique name "unpredictable" and observes that many
    // implementations report the last pair only -- so this is a decision, not a
    // requirement, and `json_parse_values.nova`'s `duplicate key` case pins it.
    fn scan_object(mut self, d: Int) -> JsonValue {
        self.bump()
        self.skip_ws()
        let mut m: Map<String, JsonValue> = Map::new()
        if self.peek() == '}' {
            self.bump()
            return Object(m)
        }
        let mut going = true
        while going {
            if self.peek() != '"' {
                self.fail("expected an object key")
                return Null
            }
            let k = self.scan_str()
            if self.err.is_some() { return Null }
            self.skip_ws()
            if self.peek() != ':' {
                self.fail("expected ':' after an object key")
                return Null
            }
            self.bump()
            self.skip_ws()
            // KNOWN UNPINNED, NAMED HERE RATHER THAN FIXED WITH A FIXTURE.
            // Changing this `d + 1` to `d` leaves every `json_*` test passing,
            // goldens byte-identical and the expected-failure assertions still
            // met -- measured, by applying that mutation, rebuilding and
            // re-running them, not inferred from reading. (This said "all
            // fourteen `json_*` goldens" until 2026-08-26. That number counted
            // the fixtures, and the expected-failure ones carry no golden, so it
            // named the wrong set; the count is dropped rather than corrected.)
            // What it would
            // break if wrong is `MAX_DEPTH` in this direction: nesting through
            // OBJECTS would stop counting, so an object-nested input could
            // recurse arbitrarily deep and reach the stack overflow the cap
            // exists to replace. It survives because both depth fixtures nest
            // through ARRAYS -- `json_depth_leaf.nova` and
            // `json_depth_empty.nova` build their inputs from `[` and `]` --
            // which covers `scan_array`'s twin of this line and not this one.
            let v = self.parse_value(d + 1)
            if self.err.is_some() { return Null }
            m.insert(k, v)
            self.skip_ws()
            let c = self.peek()
            if c == ',' {
                self.bump()
                self.skip_ws()
            } else if c == '}' {
                self.bump()
                going = false
            } else {
                self.fail("expected ',' or '}' in an object")
                return Null
            }
        }
        Object(m)
    }

    // A JSON string literal, cursor on the opening `"`. Returns the DECODED
    // contents, with every escape resolved; on failure `err` is set and the
    // return value is `""` rather than meaningful.
    //
    // The three arms are exactly RFC 8259's `char`: a closing quote ends the
    // literal, a backslash starts an escape, and anything else is `unescaped`
    // -- which the grammar defines as `%x20-21 / %x23-5B / %x5D-10FFFF`. That
    // range starts at `0x20`, so a raw character below it is a SYNTAX ERROR and
    // not a character to pass through: tab and line feed are legal whitespace
    // BETWEEN tokens and still illegal unescaped inside a string. Note the
    // range runs to `10FFFF` with no upper exclusion, so `0x7f` (DEL) is
    // ordinary text here and needs no escape.
    //
    // The `peek() != '"'` guard is defensive rather than load-bearing today:
    // `parse_value` dispatches here only on `"`, and `scan_object` checks for
    // one itself. It is kept for the reason `bump`'s bounds guard is -- a
    // future caller may not position the cursor as carefully.
    //
    // `out` used to be described here as "rebuilt by interpolation on every
    // character, which is quadratic in the literal's length", with Nova
    // having "no growable string buffer to do better with". Both retracted by
    // the roster below: `out` is now a `Vec<Char>`, appended to and drained
    // once, and that buffer is exactly what the second clause said did not
    // exist. What did NOT change is why a plain slice cannot stand in for the
    // loop: a `span`-style slice still cannot replace it, because an escape
    // means the decoded text is not a contiguous run of the input, so the
    // characters still have to be assembled one at a time, only now into a
    // buffer rather than by interpolation.
    //
    // THE ACCUMULATOR ROSTER IN THIS FILE, by name rather than by count, and
    // what changed. `quote` and this function each built one string literal by
    // interpolation; `stringify`'s `Array` and `Object` arms built the whole
    // rendered document that way. This function and `stringify` now append
    // into a `Vec<Char>` (`out` here, and `stringify`'s own separate output
    // buffer, also named `out`, which is unrelated to its work list) drained
    // once through `vec_chars_to_string` instead. `quote` hands the whole
    // job to the runtime builtin `json_quote` (2026-10-03). So none is
    // quadratic in what it emits.
    //
    // This used to close saying `stringify`'s two arms "do not yet" -- that
    // migration was called "later work in this same increment, not this
    // task's". Both retracted: the migration is this task, `stringify` no
    // longer recurses or interpolates, and there is no accumulator left in
    // this file that still rebuilds a string by interpolation.
    //
    // Regrep before trusting that claim: `grep -n 'out = "${out}'` over this
    // file must match nothing except text a comment quotes for illustration.
    // This sentence is one such match, naming the pattern literally, and it
    // is not the only one -- `buf_push_str`'s own header and `stringify`'s
    // both quote the retired idiom too. Read every match before trusting the
    // claim; do not assume a fixed count, here or anywhere this comment is
    // cited from.
    //
    // RETRACTED, and quoted so the retraction is legible: this note used to
    // close "Neither is capped, and neither is fixable without a growable
    // string buffer the language does not have." That is true only of a
    // growable `String` TYPE, which Nova still lacks. It is false in the sense
    // the sentence was used: `Vec<Char>` plus `str_from_chars` composes into
    // exactly such a buffer, both already shipped, and both were already
    // called from this very file -- `Vec` at `vec_to_array`, `str_from_chars`
    // at `span`. The fix needed no language change.
    //
    // The measurements the old note carried described the interpolation form
    // and are superseded; `buf_push_str` above carries the buffer's numbers.
    // What did NOT change is `parse`'s cost, which was already effectively
    // flat: 279, 282 and 344 ms for an array of 4000, 8000 and 16000
    // one-character numbers.
    fn scan_str(mut self) -> String {
        if self.err.is_some() { return "" }
        if self.peek() != '"' {
            self.fail("expected a string")
            return ""
        }
        self.bump()
        let mut out: Vec<Char> = Vec::new()
        while true {
            if self.at_end() {
                self.fail("unterminated string")
                return ""
            }
            let c = self.peek()
            if c == '"' {
                self.bump()
                return vec_chars_to_string(out)
            }
            if c == '\\' {
                self.bump()
                let piece = self.scan_escape()
                if self.err.is_some() { return "" }
                buf_push_str(out, piece)
            } else if char_to_int(c) < 32 {
                self.fail("a control character must be escaped")
                return ""
            } else {
                self.bump()
                out.push(c)
            }
        }
        vec_chars_to_string(out)
    }

    // What one escape decodes to, cursor on the character AFTER the backslash.
    //
    // RFC 8259 allows exactly nine forms after a backslash, and a backslash
    // followed by anything else is an error -- there is no "unknown escapes
    // pass through" rule. The cursor is not advanced before the failure, so
    // `at` points at the offending character rather than past it.
    fn scan_escape(mut self) -> String {
        if self.at_end() {
            self.fail("unterminated string")
            return ""
        }
        let e = self.peek()
        if e == 'u' {
            self.bump()
            return self.scan_hex_escape()
        }
        match simple_escape(e) {
            Some(s) => {
                self.bump()
                s
            }
            None => {
                self.fail("unknown escape")
                ""
            }
        }
    }

    // `\uXXXX`, cursor just past the `u`, and the surrogate pairing RFC 8259
    // section 7 requires for anything above the BMP.
    //
    // There is NO Int-to-`Char` conversion in Nova, so this cannot build a
    // `Char` from a code point. It encodes the code point to UTF-8 bytes here
    // and hands them to `Bytes::to_string`, whose `Option` is the validation:
    // `to_string` checks UTF-8 and returns `None`, so a code point that is not
    // a Unicode scalar value becomes a `JsonError` instead of garbage. That is
    // why no second intrinsic was added for this. The runtime's own
    // `nova_rt_char_to_str` would have been strictly worse -- its body is
    // `char::from_u32(v as u32).unwrap_or(char::REPLACEMENT_CHARACTER)`, so it
    // silently substitutes U+FFFD for exactly the inputs that must be rejected.
    //
    // The `None` arm is therefore a SECOND line of defence, not a redundant
    // one: the explicit surrogate checks above it make every code point that
    // reaches the encoder a scalar value, so the arm cannot fire as written.
    // Deleting the LONE-SURROGATE rejection makes it fire -- measured, not
    // assumed: with that check gone, `"\udc00"` reports `an escape is not a
    // valid code point`, because a lone surrogate encodes to a three-byte
    // sequence Rust's `str::from_utf8` rejects. So the error survives the loss
    // of that check, and only its message changes.
    //
    // Deleting the LOW-SURROGATE RANGE check does NOT reach that arm, and the
    // asymmetry is why both checks are here rather than one. The
    // `high surrogate then a non-surrogate escape` case in
    // `json_parse_strings.nova` -- a high surrogate followed by an escape whose
    // value is `0x41` -- then combines to `0x11841`, a perfectly ordinary
    // character, so that input is silently ACCEPTED instead of rejected.
    // Measured on its own, with the check above this one left in place.
    fn scan_hex_escape(mut self) -> String {
        let hi = self.scan_hex4()
        if self.err.is_some() { return "" }
        if hi >= 0xDC00 && hi <= 0xDFFF {
            self.fail("a low surrogate must follow a high surrogate")
            return ""
        }
        let mut cp = hi
        if hi >= 0xD800 && hi <= 0xDBFF {
            if self.peek() != '\\' {
                self.fail("a high surrogate must be followed by a low surrogate")
                return ""
            }
            self.bump()
            if self.peek() != 'u' {
                self.fail("a high surrogate must be followed by a low surrogate")
                return ""
            }
            self.bump()
            let lo = self.scan_hex4()
            if self.err.is_some() { return "" }
            if lo < 0xDC00 || lo > 0xDFFF {
                self.fail("expected a low surrogate")
                return ""
            }
            cp = 0x10000 + (hi - 0xD800) * 0x400 + (lo - 0xDC00)
        }
        match bytes_from_ints(utf8_bytes(cp)).to_string() {
            Some(s) => s
            None => {
                self.fail("an escape is not a valid code point")
                ""
            }
        }
    }

    // Exactly four hexadecimal digits, as an `Int`. FOUR is the grammar's own
    // count (`%x75 4HEXDIG`), not a convenience: reading fewer would leave the
    // remaining digits to be taken as literal text, and reading more would
    // swallow the character after the escape.
    //
    // A non-hex character stops this, and so does the end of the input, since
    // `peek`'s past-the-end `'\0'` is not a hexadecimal digit.
    fn scan_hex4(mut self) -> Int {
        let mut v = 0
        let mut i = 0
        while i < 4 {
            let d = hex_value(self.peek())
            if d < 0 {
                self.fail("expected four hexadecimal digits")
                return 0
            }
            v = v * 16 + d
            self.bump()
            i = i + 1
        }
        v
    }
}

// An ASCII digit. `Char` admits `<=` and `>=` directly (`impl Ord for Char` in
// `std/core` is built on `<`), so this needs no conversion to `Int` -- which
// also keeps the past-the-end sentinel `'\0'` a non-digit for free.
fn is_digit(c: Char) -> Bool { c >= '0' && c <= '9' }

// One hexadecimal digit's value, or `-1` for anything that is not one. `-1`
// rather than `Option<Int>` because the single caller is a four-iteration loop
// and an `Option` there buys a `match` per digit and nothing else.
//
// The offsets are the ASCII arithmetic: `'0'` is 48, `'a'` is 97 and 97 - 87 is
// 10, `'A'` is 65 and 65 - 55 is 10. Both letter cases are accepted because RFC
// 8259's `HEXDIG` is `DIGIT / "A" / ... / "F"` under ABNF's case-insensitive
// rule for literals.
fn hex_value(c: Char) -> Int {
    if c >= '0' && c <= '9' { return char_to_int(c) - 48 }
    if c >= 'a' && c <= 'f' { return char_to_int(c) - 87 }
    if c >= 'A' && c <= 'F' { return char_to_int(c) - 55 }
    // `return`, not a bare `-1`: after the `if` block above, a statement
    // starting `-` parses as SUBTRACTION from that block's value. Measured.
    return -1
}

// `cp` encoded as UTF-8, one `Int` per byte. The four length classes are UTF-8's
// own: `0..0x80` in one byte, `0x80..0x800` in two, `0x800..0x10000` in three,
// and the rest in four.
//
// Nova has no `Char` built from an `Int`, so this is the only route from a
// `\uXXXX` code point to a `String`, via `bytes_from_ints` and
// `Bytes::to_string`.
//
// The out-of-range guard returns `0xFF`, which is not a legal UTF-8 byte in any
// position, so such a code point becomes a `to_string` failure at the call site
// rather than a mangled character. It is UNREACHABLE as written -- four
// hexadecimal digits cannot exceed `0xFFFF`, four of them cannot be negative,
// and the surrogate combine's own maximum is `0x10000 + 0x3FF * 0x400 + 0x3FF`,
// which is exactly `0x10FFFF`.
//
// It is here because the two arms it shields are the only expressions below
// whose result is not bounded to `0..=255` by construction: `[cp]` for a
// NEGATIVE `cp`, and the four-byte lead `0xF0 | (cp >> 18)` for an oversized
// one. Every other byte is masked or has a bounded input -- `0xC0 | (cp >> 6)`
// tops out at `0xDF`, `0xE0 | (cp >> 12)` at `0xEF`, and a continuation byte at
// `0xBF`. And the shielding matters because `bytes_from_ints` ABORTS the process
// on an element outside `0..=255` rather than returning an error.
fn utf8_bytes(cp: Int) -> [Int] {
    if cp < 0 || cp > 0x10FFFF { return [0xFF] }
    if cp < 0x80 { return [cp] }
    if cp < 0x800 { return [0xC0 | (cp >> 6), 0x80 | (cp & 0x3F)] }
    if cp < 0x10000 { return [0xE0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3F), 0x80 | (cp & 0x3F)] }
    // `return`, not a bare trailing expression: after the `if` block above, a
    // statement starting `[` parses as an INDEX into that block's value and
    // fails on the first comma. Measured, not stylistic.
    return [
        0xF0 | (cp >> 18),
        0x80 | ((cp >> 12) & 0x3F),
        0x80 | ((cp >> 6) & 0x3F),
        0x80 | (cp & 0x3F),
    ]
}

// A `Vec` copied into the plain `[JsonValue]` that `Array` holds.
//
// `scan_array` collects into a `Vec` because it cannot know the element count
// before it has parsed them. This used to be called "the one-way conversion
// out"; it no longer is one -- `vec_chars_to_string`, added above `quote` for
// the string buffer, drains a `Vec<Char>` the same way. `Null` is the repeat
// literal's filler -- every slot is overwritten below, so it never survives,
// and `JsonValue` having a nullary variant is why this needs no `Default`
// bound.
//
// Reads the backing array directly, for the reason `vec_chars_to_string`
// gives: `i` runs strictly below `v.len()`, so `Vec::get`'s `Option` could
// never be `None` here, and each `Some` would be a heap allocation per
// element. The index is bounds checked, as there.
fn vec_to_array(v: Vec<JsonValue>) -> [JsonValue] {
    let n = v.len()
    let mut out = [Null; n]
    let mut i = 0
    while i < n {
        out[i] = v.data[i]
        i = i + 1
    }
    out
}

// The eight one-character escapes of RFC 8259 section 7, or `None` for any other
// character after a backslash. `\u` is deliberately absent: it consumes four
// more characters and so needs the cursor, which a free function does not have.
//
// `\b` and `\f` are written as string `\u{...}` escapes because Nova's CHAR
// literal admits only `\n \t \r \\ \' \0` -- measured against this compiler:
// anything else falls through `lex_char`'s `_ => None`, so `'\u{0C}'` does not
// lex at all, while the string lexer's own `\u{...}` does.
fn simple_escape(c: Char) -> Option<String> {
    if c == '"' { return Some("\"") }
    if c == '\\' { return Some("\\") }
    if c == '/' { return Some("/") }
    if c == 'b' { return Some("\u{08}") }
    if c == 'f' { return Some("\u{0C}") }
    if c == 'n' { return Some("\n") }
    if c == 'r' { return Some("\r") }
    if c == 't' { return Some("\t") }
    None
}

// === ToJson / FromJson ===
//
// `nova-spec/20-STDLIB.md` section 7 writes both traits with `Self` in
// return position:
//
//     pub trait ToJson { fn to_json(self) -> JsonValue }
//     pub trait FromJson { fn from_json(v: JsonValue) -> Result<Self, JsonError> }
//
// An `impl`, unlike the trait declaration, writes the CONCRETE type in that
// position -- `impl Default for Int { fn default() -> Int { 0 } }` at
// `std/core/lib.nova` is what proves that compiles; every impl below follows
// that form, not `Self`.
/// A type that can be converted to a `JsonValue`.
///
/// std implements it for `Bool`, `String`, `Float` and `Int`. A type of your
/// own can implement it too, for instance by building an `Object` from its
/// fields.
///
/// A record of your own written as JSON:
///
/// ```
/// record Point { x: Int, y: Int }
///
/// impl ToJson for Point {
///     fn to_json(self) -> JsonValue {
///         let mut fields: Map<String, JsonValue> = Map::new()
///         fields.insert("x", self.x.to_json())
///         fields.insert("y", self.y.to_json())
///         Object(fields)
///     }
/// }
///
/// fn main() {
///     let text = stringify(Point { x: 1, y: 2 }.to_json())
///     // An object's members come out in an order that can change between runs.
///     assert(text == "{\"x\":1,\"y\":2}" || text == "{\"y\":2,\"x\":1}", text)
/// }
/// ```
pub trait ToJson {
    /// Returns `self` as a `JsonValue`.
    ///
    /// std's impls give a `Bool`, a `String` or a `Number`. An `Int` larger
    /// in magnitude than 2^53 (9,007,199,254,740,992) is rounded to the
    /// nearest `Float`, so it can lose its exact value.
    fn to_json(self) -> JsonValue
}

/// A type that can be built from a `JsonValue`.
///
/// std implements it for `Bool`, `String`, `Float` and `Int`.
pub trait FromJson {
    /// Returns the value of this type that `v` holds, or a `JsonError` if `v`
    /// does not hold one.
    ///
    /// std's impls convert nothing: each accepts only its own variant, so
    /// `Null` is not `false` and a `Number` is not a `String`. A wrong variant
    /// gives the error `expected a bool`, `expected a string` or
    /// `expected a number`. `Int` accepts only a whole number within its
    /// range: a fraction or `NaN` gives `expected an integer`, and a number
    /// out of range, an infinity included, `integer out of range`.
    fn from_json(v: JsonValue) -> Result<Self, JsonError>
}

// Every `from_json` below that returns `Err` uses `at: 0` -- see
// `JsonError`'s own doc comment above for why a fixed placeholder is
// correct here rather than an oversight.
//
// Scope stops at four primitives -- `Bool`, `String`, `Float`, `Int` -- on
// purpose. No container impl (`[T]`, `Map<String, T>`, `Option<T>`) is
// added: each one is more public API this module would then owe a fixture
// for `Array`, `Object`, `T: ToJson` would need a bound this module cannot
// express any better than by hand, and nothing this task's brief asks for
// needs one. That is exactly the "endless tail of instances" YAGNI is
// guarding against here.

// `Bool` and `JsonValue`'s `Bool` variant share a spelling; `Bool(self)`
// below constructs the variant, not the type. Proven the same way this
// module's own parser already relies on it: `parse_value` builds
// `Bool(true)`/`Bool(false)` as VALUES, not only as patterns, and that is
// measured against this compiler in this file's header comment.
impl ToJson for Bool {
    fn to_json(self) -> JsonValue { Bool(self) }
}

impl FromJson for Bool {
    // Anything but `Bool` is a decode error, never a guess -- `Number(0.0)`
    // and `Null` do NOT become `false`.
    fn from_json(v: JsonValue) -> Result<Bool, JsonError> {
        match v {
            Bool(b) => Ok(b)
            _ => Err(JsonError { msg: "expected a bool", at: 0 })
        }
    }
}

impl ToJson for String {
    fn to_json(self) -> JsonValue { String(self) }
}

impl FromJson for String {
    fn from_json(v: JsonValue) -> Result<String, JsonError> {
        match v {
            String(s) => Ok(s)
            _ => Err(JsonError { msg: "expected a string", at: 0 })
        }
    }
}

impl ToJson for Float {
    // Exact: `Number` already IS an `f64`, so nothing here can round.
    fn to_json(self) -> JsonValue { Number(self) }
}

impl FromJson for Float {
    // Every `Number` is a valid `Float` -- non-finite and fractional ones
    // included. `Float` has no narrower range to reject a `Number` into, so
    // unlike `Int` below this has nothing to decide.
    fn from_json(v: JsonValue) -> Result<Float, JsonError> {
        match v {
            Number(n) => Ok(n)
            _ => Err(JsonError { msg: "expected a number", at: 0 })
        }
    }
}

// RULE (encode): an `Int` too large for an `f64` to hold exactly converts
// anyway, rounding SILENTLY to the nearest value a `Float` can represent.
// `Int::to_json` returns `JsonValue`, not a `Result`, so there is no error
// channel here even if refusing were wanted.
//
// An `f64` mantissa carries 53 bits, so every `Int` in `-2^53..=2^53`
// survives this exactly; the first one that does not is `2^53 + 1 ==
// 9007199254740993`, which this rounds to `9007199254740992` (round half to
// even: `9007199254740992` and `9007199254740994` are its two representable
// neighbours, equally far away, and `9007199254740992` is the one with an
// even low mantissa bit). Measured against this compiler, not assumed:
// `json_traits.nova`'s "precision loss at the f64 mantissa boundary" case
// pins that exact pair colliding on one rendered JSON text.
//
// The conversion goes through `str_to_float`, the only PRIMITIVE route from
// text to `Float` this language has -- there is no `as` cast (`E0900`) and no
// Int-to-Float intrinsic anywhere in Nova today. `"${self}"` is `Int`'s own
// `Display`, exact for the full `Int` range, so all of the rounding above
// happens inside `str_to_float` and none of it in the formatting step.
impl ToJson for Int {
    fn to_json(self) -> JsonValue { Number(str_to_float("${self}")) }
}

// RULE (decode): a `Number` converts to `Int` only when its value is both
// IN RANGE for `Int` and carries no fraction. Two independent failures,
// checked in this order:
//
//   * a magnitude outside `i64::MIN..=i64::MAX`
//     (`-9223372036854775808..=9223372036854775807`) is an `Err`, never a
//     wrapped value. An earlier version of this function had no bound
//     check at all: a too-large `Number` reached the digit accumulation
//     below and wrapped silently, the same way `std/core`'s `mix64`
//     documents `Int`'s own `*` wrapping rather than trapping. That is
//     fine for `mix64`, which is mixing bits on purpose; it is not fine
//     here, decoding externally-supplied JSON, where "a fraction is a
//     decode error and never a silent truncation" (next bullet) and a
//     wrapped-away magnitude is the same failure mode applied to the
//     integer's size instead of its precision.
//   * a fraction is a decode error and never a silent truncation --
//     `Number(3.0)` decodes to `3`, `Number(3.5)` is an `Err`. This module
//     already marks `stringify`'s non-finite-to-`null` collapse
//     "DELIBERATELY LOSSY" in the encode direction, and letting
//     `from_json` throw away a fraction in the decode direction would make
//     a round trip fabricate a value instead of reporting that it could
//     not produce one.
//
// The bound check compares `n` itself, before any text exists, against
// `-9223372036854775808.0` (`i64::MIN`) and `9223372036854775808.0` (one
// past `i64::MAX`). Both are exact `Float` values -- both are powers of
// two -- which `i64::MAX` itself, `9223372036854775807`, is NOT (the
// nearest `Float` to it is `9223372036854775808.0`, the same value used
// as the rejected upper bound), so this never has to ask whether an
// inexact `Float` equals an exact `Int` edge. `NaN` fails both `<` and
// `>=` here. `<` and `==` are both `false` whenever either operand is
// `NaN`, which is what `std/core`'s own note on `impl Ord for Float`
// states -- that note names those two operators and no others. The same
// holds for `>`, `<=` and `>=` under IEEE 754, and of those the `<` and
// `>=` used right here are the ones measured below; `>` and `<=` are
// neither cited nor measured anywhere in this tree. `!=` is the
// exception and is `true`, which is exactly what `number_to_json`'s
// `n - n != 0.0` finiteness test above depends on, so "a comparison
// against `NaN` is always false" would contradict this file's own
// machinery. So `NaN` is not rejected by the range check; it is caught
// immediately after, by the fraction check,
// since `"NaN"` is not digit-shaped either. Measured against this
// compiler, not assumed: a throwaway `out_of_range(n)` returned `false`
// for `0.0 / 0.0`, and `true` for both `1.0 / 0.0` and its negation.
//
// There is no Float-to-Int intrinsic and no `as` cast (`E0900`) anywhere
// in Nova today, so getting to `Int` at all means walking decimal digits
// by hand, the way `scan_hex4` above walks hex ones -- over TWO different
// texts of `n`, not one, and this is the second decision this rule makes
// explicit rather than by accident:
//
//   * `"${n}"`, `Float`'s `Display`, checked ONLY for shape below (every
//     character but an optional leading `-` must be `0`-`9`) to decide
//     fraction-or-not. Its DIGITS are never used for the value: `Display`
//     is a SHORTEST-ROUND-TRIP rendering, not an exact one, and once a
//     `Float`'s neighbouring representable values are more than 1 apart,
//     the shortest text that round-trips can be a rounder nearby number
//     rather than the exact one. Measured, not assumed:
//     `"${-9223372036854775808.0}"` (`i64::MIN`, exactly representable,
//     being a power of two) is `"-9223372036854776000"` -- OFF BY 192 from
//     the value it names, and also outside `Int`'s own range -- exactly
//     the shape of bug this rule exists to refuse rather than launder
//     into a wrapped `Int`.
//   * `float_fixed(n, 0)` for the actual digits, which IS exact --
//     `std/fmt`'s own comment on `Float::fixed` gives the reason, that
//     Nova "cannot begin on it, having no `Float` to `Int` conversion at
//     all", and `nova_rt_float_fixed` is Rust's `format!("{:.0}", v)`, an
//     exact fixed-precision conversion rather than a shortest-round-trip
//     one. `float_fixed(-9223372036854775808.0, 0)` is
//     `"-9223372036854775808"`, the exact value. This is used ONLY for
//     the digits, never for the shape check above it -- `float_fixed`
//     ROUNDS a fraction rather than reporting one
//     (`float_fixed(3.5, 0)` is `"4"`), which would silently turn the
//     fraction rule above into the very truncation it rejects, if this
//     text were the one checked for a `.`.
impl FromJson for Int {
    fn from_json(v: JsonValue) -> Result<Int, JsonError> {
        match v {
            Number(n) => {
                if n < -9223372036854775808.0 || n >= 9223372036854775808.0 {
                    Err(JsonError { msg: "integer out of range", at: 0 })
                } else if parse_decimal_int("${n}").is_none() {
                    Err(JsonError { msg: "expected an integer", at: 0 })
                } else {
                    match parse_decimal_int(float_fixed(n, 0)) {
                        Some(i) => Ok(i)
                        None => Err(JsonError { msg: "expected an integer", at: 0 })
                    }
                }
            }
            _ => Err(JsonError { msg: "expected a number", at: 0 })
        }
    }
}

// `s`'s digits as an `Int`, or `None` if any character once an optional
// leading `-` is consumed is not `0`-`9`, or if there are no digits at all
// (an empty string, or a bare `"-"`). `from_json` above calls this twice,
// on two different texts of the same `Number` -- see its comment for why.
//
// Accumulates NEGATIVE throughout, even for a positive input, negating
// only once at the very end, rather than the more obvious positive
// accumulation negated only when `neg` is set.
//
// NOT because the positive form gives a wrong answer -- measured, not
// assumed: mutating this to positive accumulation changes not one output
// anywhere in the whole 44-target suite. The reason is `i64::MIN`'s own
// magnitude, `9223372036854775808`, being one past `i64::MAX`: a positive
// running total reaches exactly `2^63` on that value's last digit, wraps
// once there, and wraps a second time negating the wrapped result, and
// Nova's `Int` wraps CONSISTENTLY (mod `2^64`, the same arithmetic
// `std/core`'s `mix64` documents for `*`) -- so the two wraps cancel and
// the final value comes out correct either way.
//
// Negative accumulation is used anyway because its correctness does not
// need that cancellation argument at all: every partial sum stays
// DIRECTLY representable at every step, for `i64::MIN` and for everything
// smaller in magnitude, with no appeal to wraparound anywhere in the
// reasoning. "Everything smaller in magnitude" is the whole of what this
// function is ever asked to parse, but that is an invariant of its ONE
// caller, not of this function by itself: `from_json` above rejects any
// `n` outside `i64::MIN..=i64::MAX` by comparing the `Float` itself,
// before either text this function walks is ever built. This function
// does not repeat that check -- it has no way to, from a `String` alone,
// without re-deriving the same comparison from the digits it is still in
// the middle of reading -- so it stays correct for a magnitude its
// caller has already ruled out, by cancellation, for exactly the reason
// measured above; it would not stay correct for a magnitude past that,
// which is why the bound check in `from_json` is load-bearing and this
// accumulation direction, on its own, is not a substitute for it.
fn parse_decimal_int(s: String) -> Option<Int> {
    let cs = s.chars()
    let mut i = 0
    let mut neg = false
    if cs.len() > 0 && cs[0] == '-' {
        neg = true
        i = 1
    }
    if i >= cs.len() { return None }
    let mut v = 0
    while i < cs.len() {
        if !is_digit(cs[i]) { return None }
        v = v * 10 - (char_to_int(cs[i]) - 48)
        i = i + 1
    }
    if neg { Some(v) } else { Some(-v) }
}