Skip to main content

JSON

Constant JSON 

Source
pub const JSON: &str = "//! JSON: text parsed into a `JsonValue`, and a `JsonValue` written as text.\n//!\n//! `parse` reads JSON text strictly, as RFC 8259 defines it, and `stringify`\n//! writes a `JsonValue` as compact JSON text. `ToJson` and `FromJson` convert\n//! between a `JsonValue` and another type; std implements both for `Bool`,\n//! `String`, `Float` and `Int`. Every module sees these names without an\n//! import, and a module\'s own definition of one of them shadows the one here.\n//!\n//! Every JSON number is a `Float`, so an integer larger in magnitude than\n//! 2^53 (9,007,199,254,740,992) may not keep its exact value. An object is a\n//! `Map`, which holds one value per key and keeps no order: `stringify`\n//! writes an object\'s members in an order that can change from one run of a\n//! program to the next.\n//!\n//! Parsing JSON text, reading values out of an object, and writing a value\n//! back as compact text:\n//!\n//! ```\n//! let doc = parse(\"{\\\"name\\\": \\\"nova\\\", \\\"stars\\\": 42}\").unwrap()\n//! let fields = match doc {\n//!     Object(m) => m\n//!     _ => panic(\"expected an object\")\n//! }\n//! assert_eq(String::from_json(fields.get(\"name\").unwrap()).unwrap(), \"nova\")\n//! assert_eq(Int::from_json(fields.get(\"stars\").unwrap()).unwrap(), 42)\n//!\n//! // stringify writes compact text; every number is a Float, and 1.0 is `1`.\n//! let list = Array([Number(1.0), Bool(true), Null, \"hi\".to_json()])\n//! assert_eq(stringify(list), \"[1,true,null,\\\"hi\\\"]\")\n//!\n//! // parse is strict: no trailing commas, no comments, no single quotes.\n//! assert(parse(\"[1, 2,]\").is_err(), \"a trailing comma is an error\")\n//! ```\n\n// Nova standard library -- JSON.\n//\n// `nova-spec/20-STDLIB.md` section 7\'s type declaration compiles verbatim,\n// including the variant names that shadow builtin type names. Measured, not\n// assumed: `Bool(Bool)` and `String(String)` are accepted and `match` binds\n// them unqualified.\n\n/// A JSON value: null, a boolean, a number, a string, an array or an object.\npub type JsonValue =\n    /// JSON\'s `null`.\n    | Null\n    /// `true` or `false`.\n    | Bool(Bool)\n    /// A number. JSON does not tell integers apart, so every number is a\n    /// `Float`. `NaN` and the infinities have no JSON form: `stringify`\n    /// writes them as `null`.\n    | Number(Float)\n    /// A string.\n    | String(String)\n    /// An array of values, in order.\n    | Array([JsonValue])\n    /// An object: its members\' values by key. It holds one value per key,\n    /// and keeps no order.\n    | Object(Map<String, JsonValue>)\n\n// `at` is a character index into the input, not a byte offset -- the parser\n// walks `[Char]` -- but that is true only of an error `parse` returns.\n// `FromJson::from_json`, below, constructs `JsonError` too, decoding an\n// already-parsed `JsonValue` tree rather than input text: there is no\n// cursor and no source position to report, so every error `from_json`\n// returns carries a FIXED PLACEHOLDER, `at: 0`, never a real index.\n/// The error `parse` and `FromJson::from_json` return.\npub record JsonError {\n    /// What was wrong, such as `expected a value` or `trailing content`.\n    pub msg: String,\n    /// Where `parse` stopped in its input, counted in characters, not\n    /// bytes, from 0. An error from `from_json` has no input to point into,\n    /// and its `at` is always 0.\n    pub at: Int,\n}\n\n// The deepest nesting `parse` accepts. RFC 8259 section 9 permits a limit --\n// \"An implementation may set limits on the maximum depth of nesting\" -- so a\n// cap conforms.\n//\n// 128 IS A DECLARED CONTRACT AND DELIBERATELY NOT DERIVED FROM THIS BUILD\'S\n// STACK. That distinction answers the objection this file used to raise against\n// any cap: a stack-size threshold is not a portable budget, and a number taken\n// from one machine\'s stack would be wrong on the next. 128 is instead the\n// budget `serde_json` starts a deserializer with -- a bare `remaining_depth:\n// 128` literal on a `u8` field in its `src/de.rs`, not a named constant, and\n// defeatable there through `disable_recursion_limit`. Read at `master` on\n// 2026-08-25 and cited as a precedent that makes 128 defensible, not as a\n// standard: implementations that declare a limit pick different numbers.\n//\n// EACH COMPARISON NOW CARRIES A FILE AND A READ DATE, and one of them changed\n// when it got one. This paragraph used to close \"Jackson\'s\n// `StreamReadConstraints.maxNestingDepth()` defaults to 1000, CPython has no\n// JSON-specific constant and inherits the interpreter\'s recursion limit, and\n// Go\'s `encoding/json` caps nothing.\" The Go half is FALSE and is retracted:\n// `encoding/json` declares `const maxNestingDepth = 10000` in\n// `src/encoding/json/scanner.go` and enforces it in `pushParseState`, which\n// reports `exceeded max depth` (read at `master` on 2026-08-26). The others\n// hold as read on 2026-08-26 -- Jackson\'s\n// `StreamReadConstraints.DEFAULT_MAX_DEPTH` is `1000`\n// (`src/main/java/com/fasterxml/jackson/core/StreamReadConstraints.java` on\n// branch `2.x`), and CPython\'s C accelerator declares no JSON-specific\n// constant and calls `_Py_EnterRecursiveCall` in `scan_once_unicode`\n// (`Modules/_json.c` at `main`), so the interpreter\'s recursion limit is what\n// bounds it. WHY THIS WAS THE ONE THAT WAS WRONG is worth keeping: the\n// `serde_json` claim above carried a file, a field and a read date, the Go\n// claim carried none of them, and it sat in the same sentence as if it had.\n// The comparison the argument needs is unchanged, and stronger for holding a\n// checked number where it used to hold an unchecked absence.\n//\n// For scale rather than for derivation: this build was measured parsing depth\n// 5000 successfully and overflowing its stack at 6000, on an input of 12001\n// characters. 128 sits far below that on the platform where it was measured,\n// which is what makes it portable.\nconst MAX_DEPTH: Int = 128\n\n// One pending item in `stringify`\'s work list: either literal text to emit, or\n// a value still to be rendered along with its nesting depth.\n//\n// The depth rides with the value because the guard needs one per item; a bare\n// `Value(JsonValue)` cannot support it. `Pending` is a record rather than a\n// second payload on the variant because Nova rejects tuples (E0900).\ntype Work = | Chunk(String) | Value(Pending)\n\nrecord Pending { v: JsonValue, d: Int }\n\n// The nesting depth past which `stringify` refuses to continue.\n//\n// This exists for CYCLES, but the guard is purely a depth cap and cannot\n// tell a cyclic value from a legitimate deep one -- `stringify`\'s own header\n// discloses that the hard way, by counterexample. A cyclic `JsonValue` is\n// constructible in ordinary Nova because arrays are heap references, and\n// with the work list on the heap the pop-loop has no natural floor for one:\n// it grows a net item per pop with nothing to stop it. What that ends in is\n// REASONING, not a measurement here -- an unguarded run of\n// `json_stringify_cycle.nova` was killed after 20 seconds still running, not\n// run to completion, so \"an allocator abort that prints neither a\n// `nova: panic:` prefix nor a location\" is the expected shape (the same\n// shape `gc::alloc`\'s `handle_alloc_error` produces elsewhere in this file),\n// not an observed one. The recursive form this replaces died faster and just\n// as fatally, with no message either -- THAT half is measured:\n// `json_stringify_cycle.nova` aborted by stack overflow when it still ran\n// through the recursive `stringify`, before this guard existed. A named\n// panic is better than both, and it is the best available, because\n// `stringify` returns `String` and has no error channel to report a cycle\n// through.\n//\n// GUARDED ON DEPTH, NOT ON WORK-LIST LENGTH. A cycle grows nesting depth\n// without bound; a wide, shallow document grows only length, and a length\n// bound would refuse legitimate input. 100_000 is far above any depth a\n// caller is likely to build deliberately, and far above anything `parse` can\n// hand over, since `parse` stops at `MAX_DEPTH`. The bound itself is pinned,\n// not merely approached, FOR ONE SHAPE: `json_render_guard_under.nova`\n// renders successfully at exactly `MAX_RENDER_DEPTH` levels of arrays around\n// a leaf and `json_render_guard.nova` panics one level past it -- superseding\n// an earlier, unpinned \"measured rendering depth 30000\" this comment used to\n// cite here. A chain whose innermost container is EMPTY reaches its boundary\n// one level higher, and that shape is pinned by nothing; the counting rule\n// and its measured boundaries are at the guard itself.\n//\n// DEPTH AND WIDTH ARE NOT INDEPENDENT, which the paragraph above reads as if\n// they were. Choosing depth over length is still right for the reason given\n// -- a length bound refuses legitimate wide input and a depth bound does not\n// -- but \"a wide, shallow document grows only length\" says nothing about the\n// case this guard exists for. A cyclic value grows the work list by ABOUT\n// TWICE THE CONTAINER WIDTH PER LEVEL, so its cost is a product of the two\n// and not a function of the bound alone.\n//\n// THE GUARD\'S COST GROWS WITH THE PRODUCT OF ITS BOUND AND THE CYCLE\'S\n// WIDTH. This used to read \"The guard\'s cost is proportional to its bound: a\n// cycle runs one iteration per level before firing\" -- RETRACTED: that is\n// true only at width 1. `json_stringify_cycle.nova` builds a width-1 cycle and\n// no fixture in the tree builds a wider one; that is a predicate to re-check by\n// looking for a fixture that closes a cycle, not a tally to trust from here.\n// (This sentence said \"the only width either cycle fixture builds\" until\n// 2026-08-26, quantifying over two fixtures where only `json_stringify_cycle`\n// closes a cycle at all: `json_render_guard.nova`\'s own header calls its value\n// acyclic, and so does `stringify`\'s header here. A count over a set it had not\n// checked, written in the same wave that removed one from `stringify`\'s\n// header.)\n// Popping a container of width w pushes about `2w + 1` items (w children,\n// w - 1 separators, two brackets) and removes one, so the list gains about\n// 2w per level and nothing is reclaimed before the panic. Those pushes happen\n// at every level whatever the cycle\'s position, so both the peak work-list\n// size and the time to fire grow with `MAX_RENDER_DEPTH` TIMES the width; the\n// position within the container moves the constant, not the growth.\n//\n// MEASURED against this build, min of 3 after a warm-up, cyclic arrays that\n// fire the guard: width 1 (`json_stringify_cycle.nova`\'s shape) 1.78 s;\n// width 3 with the cycle at the last member 4.71 s; width 10 at the last\n// member 14.24 s; width 10 at the FIRST member 7.14 s. Absolutes are\n// machine-specific -- an earlier session measured the width-1 case at about\n// 2 s -- and the width trend is the claim. This paragraph used to say the\n// cost \"is milliseconds with the buffer this file now uses\", which stays\n// RETRACTED as false by three orders of magnitude, and used to compare it\n// against \"more than two minutes when the same walk accumulated by\n// interpolation\", which stays retracted too: no such version of this walk\n// was ever built in this tree to measure, so that number described a\n// design-time scratch probe rather than a state this code ever shipped in.\n//\n// SO THE NAMED FAILURE IS REACHABLE FOR A NARROW CYCLE AND DEGRADES TOWARD\n// THE UNNAMED ONE AS WIDTH GROWS. That degradation is REASONING past width\n// 10, not a measurement: the work list\'s peak is the same allocator pressure\n// this guard was introduced to replace, so for a wide enough cycle the\n// process reaches `gc::alloc`\'s `handle_alloc_error` -- printing neither a\n// `nova: panic:` prefix nor a location -- before it reaches this panic.\n// Nothing here measures where that crossover sits, and no fixture builds a\n// wide cycle, so the guard\'s promise of an immediate named failure is a\n// promise about narrow cycles.\nconst MAX_RENDER_DEPTH: Int = 100_000\n\n// Render `v` as JSON text.\n//\n// This used to open \"TOTAL OVER THE VALUES IT RETURNS FROM. For every\n// acyclic `JsonValue` there is a rendering, and there is no error channel to\n// report a failure with.\" That is FALSE, and the counterexample ships in\n// this tree: `json_render_guard.nova` builds an ACYCLIC value -- a fresh\n// array literal per level, no cycle -- at depth 100001, and it panics rather\n// than rendering, because the guard below tests nesting depth alone and\n// never inspects for a cycle. \"Acyclic\" does not rescue the claim.\n//\n// THE HONEST FORM: a rendering exists for every value whose nesting depth is\n// at most `MAX_RENDER_DEPTH`, cyclic or not, and there is no error channel to\n// report a refusal with. Depth is no longer a stack cost -- the arms below\n// push pending work onto a heap list instead of recursing -- but nesting is\n// bounded by that DECLARED CAP, not by memory: this also used to say\n// \"nesting is bounded by memory rather than by stack size\", which is wrong\n// the same way. `json_render_guard_under.nova` pins the cap itself rendering\n// successfully at exactly `MAX_RENDER_DEPTH` levels, and `json_render_deep.nova`\n// pins a shallower value, 20000 levels, rendering too -- both well past\n// where the recursive form this replaces overflowed the stack, measured\n// between 10000 and 16000.\n//\n// \"NESTING DEPTH\" IN THAT SENTENCE IS DEPTH AS THE GUARD COUNTS IT, and it\n// does not count every shape the same way: a leaf costs a level and an empty\n// innermost container does not, so a chain of `MAX_RENDER_DEPTH + 1`\n// containers ending empty renders while one of `MAX_RENDER_DEPTH + 1` ending\n// in a leaf panics. The rule, the measured boundaries and which of the two\n// shapes any fixture pins are all at the guard itself, below.\n//\n// THE RESIDUALS ARE NAMED RATHER THAN LEFT TO INFERENCE, AND THE ROSTER IS\n// WHAT TO READ. This sentence used to continue \"and there are three, not\n// two\" -- a corrected count over a set a later increment can add to, sitting\n// in shipped code against this file\'s own stated preference for a roster with\n// no count. The count is dropped rather than corrected a second time, and the\n// last member below is what a count would have gone stale on immediately:\n//   - a cyclic value never terminates on its own and is refused by\n//     `MAX_RENDER_DEPTH` with a panic;\n//   - THE SAME CAP, by the same mechanism, refuses a legitimate ACYCLIC\n//     value past `MAX_RENDER_DEPTH` too -- the guard cannot and does not tell\n//     the two apart, which is exactly the counterexample the opening\n//     paragraph retracts;\n//   - heap exhaustion still aborts the process without a `JsonError`,\n//     because `gc::alloc` calls `handle_alloc_error` and no alloc-error hook\n//     is installed; there is no collect-and-retry on that path, and where\n//     `gc.rs`\'s `stack_base` has no implementation the collector is a no-op,\n//     so nothing is reclaimed until the process exits;\n//   - a WIDE cycle can reach that same allocator abort BEFORE it reaches the\n//     named panic, because the work list grows with depth times container\n//     width rather than with depth alone. `MAX_RENDER_DEPTH`\'s own note\n//     carries the measurements and marks where reasoning takes over from\n//     them.\n//\n// `parse` is bounded differently, by its own declared `MAX_DEPTH` -- the two\n// caps are independent numbers, chosen for independent reasons. `parse`\n// consumes untrusted text, where a cap is a feature in its own right. This\n// function\'s cap exists for the residual named above -- there is no error\n// channel to report a cycle through, and nesting depth alone cannot tell a\n// legitimate deep value from a cyclic one -- not because refusing a\n// caller\'s own construction is a feature here too.\n//\n// This header used to close saying \"The numbers for all four accumulators\n// in this file, and why these two are the ones that matter, are at\n// `scan_str`.\" That was false twice over: the roster at `scan_str` is by\n// name, not by count, and this function\'s own accumulator numbers were\n// deleted in the same change that retired its two `out = \"${out}...\"` arms,\n// surviving nowhere in this file. The accumulator cost these arms used to\n// carry is simply gone; the roster of what changed -- by name, not count --\n// is at `scan_str`.\n//\n// A SEPARATE, LATER SAVING ON THIS SAME FUNCTION, PAST THAT MIGRATION. The\n// migration above made the general path below LINEAR where interpolation\n// had made it QUADRATIC; it did not change how many times a top-level\n// scalar passes through that path on its way to a `String`. This increment\n// does not make that path cheaper -- it SKIPS the path entirely for a\n// top-level scalar, falling into it only for `Array`/`Object` or a scalar\n// reached through either. `tests/runtime/json_stringify.nova` is what\n// checks the fast path just below. Only the `String` arm\'s saving was\n// measured; `Null`, `Bool` and `Number` got the same match arm with no\n// separate measurement.\n/// Returns `v` as compact JSON text, with no spaces or line breaks.\n///\n/// In a string, `\"` and `\\` take a backslash; backspace, form feed, line\n/// feed, carriage return and tab are written `\\b`, `\\f`, `\\n`, `\\r` and\n/// `\\t`; every other character below U+0020 is written `\\u00` and two\n/// lowercase hex digits; and every other character is written as it is. A\n/// number is written in plain decimal, never with an exponent, as the\n/// shortest text that reads back as the same `Float`: `1.0` gives `1`, and\n/// `1e21` gives a 1 and 21 zeros. `NaN` and the infinities are written as\n/// `null`. An object\'s members come in an order that can change from one run\n/// of a program to the next.\n///\n/// Panics if arrays and objects nest more than 100,000 deep around a value,\n/// or more than 100,001 deep when the innermost one is empty. A value that\n/// contains itself, such as an array placed inside itself, panics by the\n/// same rule.\npub fn stringify(v: JsonValue) -> String {\n    // A TOP-LEVEL SCALAR IS ALREADY FINISHED, so return it rather than\n    // copying it twice. The general path below hands `buf_push_str` exactly\n    // these values, and `buf_push_str` appends them character by character\n    // into `out`, which `vec_chars_to_string` then drains back into a\n    // `String` -- two round trips through a `Vec<Char>`, plus two `Vec`\n    // allocations and the `Work`/`Pending` wrappers, for a value that needs\n    // none of them.\n    //\n    // Byte-identical, argued in this increment\'s spec rather than assumed:\n    // the depth guard cannot fire here because it tests\n    // `d > MAX_RENDER_DEPTH` and a top-level value carries `d = 0`;\n    // `buf_push_str` into an empty buffer is a pure copy; and draining those\n    // characters back is lossless for anything already a `String`, which\n    // `crates/nova-runtime/src/lib.rs`\'s\n    // `str_from_chars_round_trips_and_substitutes_invalid_scalars` pins.\n    //\n    // `tests/runtime/json_stringify.nova` pins each arm\'s output at TOP\n    // LEVEL; `tests/runtime/json_round_trip.nova` pins `Bool` and `String`\n    // INSIDE a container. A DELETED arm here is caught by NEITHER -- it\n    // falls through the catch-all into the general path below and renders\n    // IDENTICAL text, which is fine, because this path only shadows\n    // behaviour the general path already has and adds none of its own. A\n    // WRONG-RENDERING arm is different and IS caught: mutating the\n    // `String` arm to return its argument unquoted failed six tests. Only\n    // the `String` arm\'s saving was measured.\n    match v {\n        Null => return \"null\"\n        Bool(b) => return if b { \"true\" } else { \"false\" }\n        Number(n) => return number_to_json(n)\n        String(s) => return quote(s)\n        _ => {}\n    }\n    let mut out: Vec<Char> = Vec::new()\n    let mut stack: Vec<Work> = Vec::new()\n    stack.push(Value(Pending { v: v, d: 0 }))\n    while stack.len() > 0 {\n        match stack.pop() {\n            None => out = out\n            Some(w) => match w {\n                Chunk(s) => buf_push_str(out, s)\n                Value(p) => {\n                    // HOISTED, out of the `Array` and `Object` arms below, where this\n                    // used to live as two textually identical blocks (one per arm).\n                    // Duplicated, deleting one left the other -- half a mutation the\n                    // plan specified was invisible to every fixture, because no\n                    // fixture exercised the `Object` arm\'s copy on its own. A single\n                    // check here makes a half-deletion structurally impossible rather\n                    // than merely detectable, and it prices the guard once per pop\n                    // instead of once per container pop.\n                    //\n                    // EQUIVALENT TO THE OLD PER-ARM CHECK ON A CHAIN THAT ENDS IN A\n                    // LEAF, AND NOT IN GENERAL. This heading used to read \"EQUIVALENT\n                    // TO THE OLD PER-ARM CHECK, `p.d + 1 > MAX_RENDER_DEPTH`, by\n                    // derivation, not by assumption\", unqualified, over a derivation\n                    // that was already scoped to leaf-terminated chains in its own\n                    // words. RETRACTED as an unscoped claim: the two forms differ, by\n                    // exactly one level, on a chain whose innermost container is\n                    // EMPTY.\n                    //\n                    // The derivation, for a chain of k containers ending in a LEAF.\n                    // Children are pushed with `d = p.d + 1`, so the old check fired\n                    // at the innermost container\'s OWN pop when k > MAX_RENDER_DEPTH\n                    // -- the same k that chain\'s leaf carries as its `d` once pushed\n                    // and later popped. Checking `p.d > MAX_RENDER_DEPTH` here, at\n                    // every pop rather than only at a container\'s, fires at that same\n                    // leaf for the same k, one pop later than before, and the\n                    // threshold itself does not move.\n                    //\n                    // WHERE THE TWO DIVERGE. An empty innermost container pushes no\n                    // child at all, so there is no \"one pop later\" to arrive at: the\n                    // deepest `d` any pop sees is k - 1, and this check fires only\n                    // once k - 1 > MAX_RENDER_DEPTH. The old per-arm check ran at that\n                    // container\'s own pop and tested `p.d + 1`, so on the same chain\n                    // it fired at k > MAX_RENDER_DEPTH, as it did for the leaf shape.\n                    // The hoist therefore ADMITS one level more than the old form on\n                    // an empty-innermost chain, and no fewer on any shape: it can\n                    // newly accept a value and cannot newly refuse one.\n                    //\n                    // SO THIS CAP HAS THE SAME ASYMMETRIC COUNTING RULE `parse`\'S\n                    // CAP HAS. A leaf costs a level; an empty innermost container does\n                    // not. MEASURED against this build, arrays one deep per level:\n                    // 100000 containers around a leaf render, 200001 characters,\n                    // exit 0, and 100001 panic; 100001 containers ending in an empty\n                    // array render, 200002 characters, exit 0, and 100002 panic. The\n                    // derivation applies to an empty innermost OBJECT identically,\n                    // since either empty container pushes no `Value` -- that half is\n                    // reasoning, and the measured pair above is arrays.\n                    //\n                    // THAT RULE IS PINNED IN ONE SHAPE ONLY, WHICH IS DISCLOSED\n                    // RATHER THAN FIXED. `parse` states its rule at `parse_value` and\n                    // pins both of its shapes, `json_depth_leaf.nova` and\n                    // `json_depth_empty.nova`. Here `json_render_guard_under.nova`\n                    // and `json_render_guard.nova` pin the leaf boundary and nothing\n                    // pins the empty-innermost one, so a change that moved it by a\n                    // level would pass the whole suite. No fixture is added for it:\n                    // an earlier ruling declined render fixtures on suite cost, each\n                    // of that pair already costing seconds, and that trade has not\n                    // changed.\n                    //\n                    // Re-verified after hoisting rather than only re-derived: the\n                    // leaf pair still renders at `MAX_RENDER_DEPTH` and panics one\n                    // level past it, and the same pair of depths was independently\n                    // confirmed for an OBJECT chain, built the same way one key deep\n                    // per level, by a scratch run against this build rather than\n                    // shipped as a third fixture.\n                    if p.d > MAX_RENDER_DEPTH {\n                        panic(\"stringify: nesting too deep or cyclic value\")\n                    }\n                    match p.v {\n                        Null => buf_push_str(out, \"null\")\n                        Bool(b) => buf_push_str(out, if b { \"true\" } else { \"false\" })\n                        Number(n) => buf_push_str(out, number_to_json(n))\n                        String(s) => buf_push_str(out, quote(s))\n                        Array(xs) => {\n                            stack.push(Chunk(\"]\"))\n                            let mut i = xs.len() - 1\n                            while i >= 0 {\n                                stack.push(Value(Pending { v: xs[i], d: p.d + 1 }))\n                                if i > 0 { stack.push(Chunk(\",\")) }\n                                i = i - 1\n                            }\n                            stack.push(Chunk(\"[\"))\n                        }\n                        Object(m) => {\n                            let ks = m.keys()\n                            stack.push(Chunk(\"}\"))\n                            let mut i = ks.len() - 1\n                            while i >= 0 {\n                                // The `None` arm is unreachable THROUGH `Map`\'s OWN API: `ks`\n                                // is `m.keys()`, which returns exactly the occupied slots, and\n                                // nothing mutates `m` between that call and this lookup.\n                                //\n                                // This used to read \"The `None` arm is UNREACHABLE\", flatly.\n                                // That is wrong: `Map` exposes every field and Nova has no\n                                // field privacy, so a forged map with a live slot off its own\n                                // probe chain makes `keys()` return a key `get()` misses.\n                                // Measured -- `json_object_forged_map.nova` builds one: a live\n                                // member whose key sits at a `ks` position greater than 0 but is\n                                // unreachable through `get`. Which key ends up there depends on\n                                // the per-process hash seed, so this comment names the\n                                // arrangement rather than the key. So the arm is a real path and\n                                // the separator\'s position is load-bearing, not cosmetic:\n                                // it is pushed OUTSIDE this match, so a missed member leaves a\n                                // comma with no member after it, exactly as before this\n                                // function stopped recursing.\n                                //\n                                // Moving the push inside the `Some` arm would emit valid JSON\n                                // instead FOR THIS FIXTURE\'S ARRANGEMENT, where the missed key\n                                // sits at a `ks` position greater than 0 -- which key that is\n                                // depends on the per-process hash seed, so naming one here would\n                                // be true of most processes and false of some -- it is not\n                                // true in general. A miss AT position 0 emits the same string\n                                // either way: the separator there is gated on `i > 0`, which is\n                                // false for position 0 regardless of which arm the push sits\n                                // in, so that position contributes no comma under either version\n                                // and the two forms coincide. `json_object_forged_map.nova`\'s\n                                // arrangement is what makes the two versions distinguishable at\n                                // all, and it is refused by that fixture\'s golden rather than by\n                                // this comment. Repairing the fallback is a separate change with\n                                // its own record; the other fallbacks in this file that were\n                                // written for the same reason degrade safely instead -- `bump`\n                                // refuses to advance, `span` returns the empty string,\n                                // `utf8_bytes` returns 0xFF precisely to force a downstream\n                                // rejection -- which is the pattern this one departs from.\n                                //\n                                // KNOWN UNPINNED, NAMED HERE RATHER THAN FIXED WITH A\n                                // FIXTURE. Changing the `d: p.d + 1` below to `d: p.d`\n                                // leaves every `json_*` test passing -- goldens\n                                // byte-identical and the expected-failure assertions\n                                // still met -- measured, by applying that mutation,\n                                // rebuilding, and re-running them. (This said \"all\n                                // fourteen `json_*` goldens\" until 2026-08-26. That number\n                                // counted the FIXTURES; the expected-failure ones carry no\n                                // golden, so it named the wrong set. The substance held,\n                                // and the count is dropped rather than corrected -- the\n                                // predicate is every `json_*` test, which is re-runnable.)\n                                // What it would break if wrong is\n                                // `MAX_RENDER_DEPTH` itself for object-nested values:\n                                // depth stops advancing through objects, so no object\n                                // chain reaches the guard at any length. Measured on the\n                                // mutant: an object chain of 100001 levels renders\n                                // 600007 characters and exits 0, where this build panics\n                                // on the same value. A cyclic value reached through\n                                // objects would then run to the allocator abort with no\n                                // message, which is what the guard exists to prevent.\n                                // What would catch it is a fixture nesting through\n                                // OBJECTS past the cap; the ruling that declined render\n                                // fixtures on suite cost is why there is none, and a\n                                // disclosed hole beats an undisclosed one.\n                                match m.get(ks[i]) {\n                                    Some(val) => {\n                                        stack.push(Value(Pending { v: val, d: p.d + 1 }))\n                                        stack.push(Chunk(\":\"))\n                                        stack.push(Chunk(quote(ks[i])))\n                                    }\n                                    None => out = out\n                                }\n                                if i > 0 { stack.push(Chunk(\",\")) }\n                                i = i - 1\n                            }\n                            stack.push(Chunk(\"{\"))\n                        }\n                    }\n                }\n            }\n        }\n    }\n    vec_chars_to_string(out)\n}\n\n// JSON has no NaN and no infinity, so a non-finite number cannot be rendered.\n// This emits `null` instead, which is what most codecs do and which keeps\n// `stringify` total over values -- depth is a separate matter, see its own\n// note -- and its output always valid JSON. It is DELIBERATELY LOSSY:\n// a round trip through a non-finite number does not preserve it.\n//\n// `n - n != 0.0` is the finiteness test. For any finite `n` the difference is\n// exactly zero; for either infinity it is NaN, and for NaN it is NaN, and NaN\n// compares unequal to everything including itself. This avoids needing an\n// `is_nan` the standard library does not have.\nfn number_to_json(n: Float) -> String { if n - n != 0.0 { \"null\" } else { \"${n}\" } }\n\n// A growable character buffer, appended to and drained once.\n//\n// `Vec<Char>` plus `str_from_chars` IS the growable string buffer this file\'s\n// cost note used to say the language did not have. `Vec` grows by doubling\n// (`std/collections`) so appending is amortised O(1), and `str_from_chars`\n// encodes a `[Char]` in one pass, so N appends plus one drain is linear where\n// `out = \"${out}...\"` is quadratic. Measured, this compiler, debug build, with\n// a 129 ms compile baseline included: pushing n characters took 142, 135, 132\n// and 142 ms at n = 8000, 16000, 32000 and 64000 -- flat across an eightfold\n// range -- against 164, 249, 447 and 752 ms for interpolation.\n//\n// The parameter is `mut b` and not `b`. A plain `b: Vec<Char>` fails\n// `E0060: Vec_T.push mutates its receiver, but b is immutable`. Records are\n// heap objects passed by pointer, so the mutation reaches the caller --\n// including `Vec::push`\'s reassignment of its backing array when it grows,\n// which is the case that would break a value-semantics assumption. Measured,\n// not assumed.\nfn buf_push_str(mut b: Vec<Char>, s: String) {\n    let cs = s.chars()\n    let mut i = 0\n    while i < cs.len() {\n        b.push(cs[i])\n        i = i + 1\n    }\n}\n\n// The buffer\'s contents as a `String`. The same manoeuvre `vec_to_array` runs\n// for `JsonValue`: allocate an exact-length array, copy, hand it on.\n//\n// THE EXACT-LENGTH COPY IS LOAD-BEARING and is not redundant with the `Vec`\'s\n// own backing array: that array is the buffer\'s CAPACITY, not its length, so\n// encoding it directly would append the filler character of every unused slot.\n// A mutation that returns the backing array PASSES at any length that happens\n// to equal capacity, and capacity is always a power of two here, so exercise\n// the drain at a length that is not one.\n//\n// Reads the backing array directly, not through `Vec::get`. `i` runs strictly\n// below `b.len()`, so the `Option` that `get` returns could never be `None`\n// here -- and every `Some` is a heap allocation. Going through `get` cost one\n// 16-byte object per output character, which\n// `examples/05-json-api/BENCHMARK.md` measured as all of `stringify`\'s\n// per-character object count. For any `Vec` changed only through its own\n// methods the backing array is at least `len` long; the index is bounds\n// checked regardless, exactly as `get`\'s own read of it was.\nfn vec_chars_to_string(b: Vec<Char>) -> String {\n    let n = b.len()\n    let mut out = [\' \'; n]\n    let mut i = 0\n    while i < n {\n        out[i] = b.data[i]\n        i = i + 1\n    }\n    str_from_chars(out)\n}\n\n// Escape `s` as a JSON string literal, quotes included: `\"` and `\\` take a\n// backslash, the five control characters with a short escape take it, and\n// every other control character below `0x20` becomes `\\u00XX` in lowercase\n// hex (RFC 8259). A runtime builtin does it: it counts the escaped length,\n// then writes the result once. It used to scan a `[Char]` here and rebuild\n// any string that needed escaping one character at a time.\nfn quote(s: String) -> String { json_quote(s) }\n\n// Parse `s` as JSON. `JsonError::at` is a character index -- a count of Unicode\n// scalar values, matching the `[Char]` the parser walks.\n//\n// A JSON text is ONE value, so trailing content is an error rather than a\n// stopping point: `parse` skips whitespace, parses a single value, skips\n// whitespace again, and rejects anything left over.\n//\n// It is deliberate that the trailing-content check runs even when the value\n// walk has already failed: it is the second caller of `fail` that makes\n// `fail`\'s first-failure-wins guard observable, and so the thing that keeps\n// that guard tested rather than merely written. `err` is then inspected ONCE,\n// here, which is the whole point of carrying the failure in a field.\n/// Parses `s` as JSON text: one value, with nothing after it but whitespace.\n///\n/// Parsing is strict, as RFC 8259 asks. There are no comments, no trailing\n/// commas, no single quotes, no unquoted keys, no `NaN` or `Infinity`, and no\n/// leading `+` or leading zeros in numbers. Whitespace is only space, tab,\n/// line feed and carriage return, so a leading byte order mark is an error.\n/// A string must escape its control characters below U+0020; a `\\u` escape\n/// may name any character, one beyond U+FFFF as a surrogate pair, and an\n/// unpaired surrogate is an error. A number too large in magnitude for a\n/// `Float` becomes an infinity, and one too close to zero becomes 0. Of two\n/// members of an object with the same key, the last is kept. Arrays and\n/// objects may nest 128 deep around a value, or 129 deep when the innermost\n/// one is empty.\n///\n/// Returns a `JsonError` if `s` is not such a value, saying what is wrong and\n/// where.\n///\n/// Walking a parsed value, and where an error stopped:\n///\n/// ```\n/// // Walk a parsed value by matching on its variants.\n/// let doc = parse(\"{\\\"tags\\\": [\\\"fast\\\", \\\"small\\\"]}\").unwrap()\n/// let tags = match doc {\n///     Object(fields) => fields.get(\"tags\").unwrap()\n///     _ => panic(\"expected an object\")\n/// }\n/// let count = match tags {\n///     Array(items) => items.len()\n///     _ => 0\n/// }\n/// assert_eq(count, 2)\n///\n/// // An error says what is wrong, and where parse stopped, in characters.\n/// match parse(\"[1, 2,]\") {\n///     Ok(_) => panic(\"a trailing comma is not JSON\")\n///     Err(e) => assert_eq(e.at, 6) // the `]` where a value should be\n/// }\n/// ```\npub fn parse(s: String) -> Result<JsonValue, JsonError> {\n    let mut p = P { cs: s.chars(), pos: 0, err: None }\n    p.skip_ws()\n    let v = p.parse_value(0)\n    p.skip_ws()\n    if !p.at_end() { p.fail(\"trailing content\") }\n    match p.err {\n        Some(e) => Err(e)\n        None => Ok(v)\n    }\n}\n\n// The parser carries its failure in a field instead of returning `Result` at\n// every level. Nova has no `?` operator -- measured against this compiler, not\n// assumed: `?` parses and then typeck rejects it with\n// `error[E0900]: the `?` operator are not supported yet` -- so threading a\n// `Result` through a recursive descent would cost a `match` at every call site\n// and bury the grammar in plumbing.\n//\n// `err` is set at most once: the FIRST failure wins, so a downstream symptom\n// cannot overwrite the real cause. Every step that could read past the end\n// checks `err` first and no-ops, returning `Null` -- after a failure the\n// remaining walk is inert rather than wrong.\nrecord P { cs: [Char], pos: Int, err: Option<JsonError> }\n\nimpl P {\n    // Record a failure at the cursor, unless one is already recorded.\n    //\n    // The guard IS \"first failure wins\". `parse` runs its trailing-content\n    // check even after the value walk has failed, so without the guard that\n    // check would overwrite the real cause with a consequence of it: `01` would\n    // report `trailing content` at 1 rather than the leading zero that actually\n    // stopped the scanner there.\n    //\n    // `01` and NOT `+1` for that example. `+1` no longer reaches `scan_number`\n    // at all -- `parse_value`\'s dispatch rejects a leading `+` as `expected a\n    // value` before any digit is looked for -- so it no longer illustrates a\n    // cause the guard protects, only one it renames. That correction was applied\n    // to `parse`\'s comment, `scan_number`\'s comment and the fixture header in\n    // the same change that made it necessary, and missed here.\n    //\n    // Measured, so the reach is not assumed: dropping this guard rewrites every\n    // golden line whose rejection stops the cursor short of the end of the\n    // input AND whose value walk had already failed. The predicate is the\n    // durable part; the count is not, and this file\'s parse fixtures have grown\n    // since it was taken. Re-measure by deleting the guard rather than citing a\n    // number from here.\n    //\n    // Both halves of that predicate are load-bearing. The trailing-content\n    // goldens in `json_parse_values.stdout` also stop short of the end of the\n    // input, and they are UNCHANGED, because for them the trailing-content\n    // error IS the first failure, so there is nothing for the guard to keep.\n    // Reading the clause without its second half therefore OVER-PREDICTS the\n    // rewrite, by exactly that set of goldens.\n    //\n    // The two numbers this sentence used to end on are dropped rather than\n    // refreshed. It illustrated the over-prediction as one count against\n    // another, and the smaller of the two was the count the paragraph above\n    // replaced with a predicate -- so the larger had nothing left to be\n    // larger than, and citing either would reintroduce exactly the staleness\n    // that replacement removed. Delete the guard and re-measure both together.\n    fn fail(mut self, m: String) {\n        if self.err.is_none() { self.err = Some(JsonError { msg: m, at: self.pos }) }\n    }\n\n    fn at_end(self) -> Bool { self.pos >= self.cs.len() }\n\n    // The character at the cursor, or `\'\\0\'` past the end.\n    //\n    // `\'\\0\'` is a sentinel, not a distinguished value: a real NUL in the input\n    // reads identically here. That is harmless, because NUL appears in no JSON\n    // token -- it is legal only inside a string literal, and RFC 8259 requires\n    // even that one escaped -- so every caller that meets a NUL rejects it,\n    // exactly as it rejects the end of the input. A caller that needs the two\n    // told apart asks `at_end` instead.\n    fn peek(self) -> Char { if self.at_end() { \'\\0\' } else { self.cs[self.pos] } }\n\n    // Advance one character, never past the end, so `pos` stays a valid bound\n    // for `span` below. Every caller in this module bumps only after `peek`\n    // returned a real character, so today the guard is defensive rather than\n    // load-bearing; it is written so that a scanner looping on `bump` cannot\n    // walk off the array.\n    fn bump(mut self) { if !self.at_end() { self.pos = self.pos + 1 } }\n\n    // Characters `start..end` as a `String`, `end` exclusive.\n    //\n    // `std/strings` runs the same algorithm over a plain `[Char]`\n    // (`chars_to_string`), but it is private to that module and Nova has no\n    // cross-module access to a private item, so these five lines are a\n    // deliberate duplication rather than an oversight. `[\' \'; n]` needs a\n    // filler and a repeat literal is the only way to allocate a runtime-length\n    // array; every slot is overwritten immediately below, so the space never\n    // survives.\n    //\n    // NEITHER half of the `n <= 0` guard is reachable today -- for a stronger\n    // reason than in `chars_to_string`, whose note on exactly this guard the\n    // duplication above had dropped. `scan_number` is this helper\'s only\n    // caller, `pos` never moves backwards, and the call site sits past at least\n    // one consumed digit, so `n` is always 1 or more. Kept for the reason\n    // `chars_to_string` keeps it: a future caller of this module-private helper\n    // may not bound its own span as carefully. At `n == 0` the general path\n    // would return \"\" unaided anyway, so that half only skips an allocation --\n    // measured, not assumed: `[\' \'; 0]` is a legal repeat literal of length 0,\n    // the `0..0` loop is vacuous, and `nova_rt_str_from_chars` of an empty\n    // array builds an empty `String`.\n    fn span(self, start: Int, end: Int) -> String {\n        let n = end - start\n        if n <= 0 { return \"\" }\n        let mut out = [\' \'; n]\n        for i in 0..n { out[i] = self.cs[start + i] }\n        str_from_chars(out)\n    }\n\n    // Skip JSON\'s whitespace: space, horizontal tab, line feed, carriage\n    // return. THAT IS THE ENTIRE SET -- RFC 8259 permits no others, so a\n    // vertical tab, a form feed or a non-breaking space is a syntax error and\n    // must not be skipped here.\n    fn skip_ws(mut self) {\n        let mut going = true\n        while going {\n            let c = self.peek()\n            if c == \' \' || c == \'\\t\' || c == \'\\n\' || c == \'\\r\' {\n                self.bump()\n            } else {\n                going = false\n            }\n        }\n    }\n\n    // A JSON number: `-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?`.\n    //\n    // Validates by walking characters, then slices the matched span and hands\n    // it to `str_to_float`. Validating FIRST is what makes that intrinsic\'s\n    // `0.0`-on-rejection fallback unreachable: every span reaching it is a text\n    // Rust\'s `f64` parser also accepts, since Rust\'s accepted grammar is a\n    // superset of the one above.\n    //\n    // A magnitude no `Float` can hold is ACCEPTED, not rejected: JSON\'s grammar\n    // sets no range, and Rust\'s parser saturates rather than failing. Measured\n    // against this compiler: `1e999` parses to infinity, which `stringify` then\n    // renders `null` via `number_to_json`\'s non-finite branch, and `1e-999`\n    // parses to `0`. Both are lossy round trips. No fixture pins either -- they\n    // are recorded here as measured behaviour, not as tested behaviour.\n    //\n    // Each rejection is a shape JSON forbids that a laxer reader would take:\n    // a leading zero followed by a digit (`01` is two tokens, not one number),\n    // a `.` with no digit after it, and an exponent with no digits.\n    //\n    // `+1` and `.5` never arrive here. `parse_value` dispatches to this scanner\n    // only on `-` or a digit, so neither a leading `+` (only `-` is a sign) nor\n    // a bare `.` gets past the dispatch, and both are reported there as\n    // `expected a value` instead. What still reaches the `expected a digit`\n    // branch below is a `-` with no digit after it, which is the `-` case in\n    // `json_parse_numbers.nova`.\n    fn scan_number(mut self) -> JsonValue {\n        if self.err.is_some() { return Null }\n        let start = self.pos\n        if self.peek() == \'-\' { self.bump() }\n        if self.peek() == \'0\' {\n            self.bump()\n            if is_digit(self.peek()) {\n                self.fail(\"a leading zero cannot be followed by another digit\")\n                return Null\n            }\n        } else if is_digit(self.peek()) {\n            while is_digit(self.peek()) { self.bump() }\n        } else {\n            self.fail(\"expected a digit\")\n            return Null\n        }\n        if self.peek() == \'.\' {\n            self.bump()\n            if !is_digit(self.peek()) {\n                self.fail(\"expected a digit after the decimal point\")\n                return Null\n            }\n            while is_digit(self.peek()) { self.bump() }\n        }\n        if self.peek() == \'e\' || self.peek() == \'E\' {\n            self.bump()\n            if self.peek() == \'+\' || self.peek() == \'-\' { self.bump() }\n            if !is_digit(self.peek()) {\n                self.fail(\"expected a digit in the exponent\")\n                return Null\n            }\n            while is_digit(self.peek()) { self.bump() }\n        }\n        Number(str_to_float(self.span(start, self.pos)))\n    }\n\n    // One JSON value, dispatched on the character at the cursor.\n    //\n    // The dispatch is unambiguous because JSON\'s six value kinds start on\n    // disjoint character sets -- seven of them, since `true` and `false` are one\n    // kind on two letters: `n`, `t`, `f`, `\"`, `[`, `{`, and `-` or a digit.\n    // Anything else is not the start of a value, which is where a leading `+`, a\n    // bare `.`, a stray `]` and an empty input all land.\n    //\n    // `n`/`t`/`f` hand off to `keyword`, which matches the WHOLE spelling.\n    // Dispatching on the first letter and then consuming a fixed count would\n    // read `nxxx` as `null`.\n    //\n    // NESTING DEPTH IS CAPPED IN THIS DIRECTION. This recurses through\n    // `scan_array` and `scan_object` back into itself, and the check above\n    // bounds that recursion at `MAX_DEPTH`, reporting an ordinary `JsonError`\n    // through the same channel as a syntax error. `stringify` is bounded\n    // differently -- it does not recurse at all -- and says so at its own\n    // header rather than here, so this file no longer states one property for\n    // both directions.\n    //\n    // WHAT THE NUMBER COUNTS. A leaf costs a depth level and an empty\n    // innermost container does not, because the empty-container fast paths in\n    // `scan_array` and `scan_object` return without re-entering this function.\n    // So 128 containers wrapping a leaf is accepted and 129 is not, while 129\n    // containers ending empty is accepted and 130 is not. Both boundaries are\n    // pinned: `json_depth_leaf.nova` and `json_depth_empty.nova`.\n    //\n    // RETRACTED, quoted so the retraction is legible. This paragraph used to\n    // say a cap was declined deliberately because \"a stack-size artefact is not\n    // a budget a cap can be derived from\", and that a cap \"needs an API choice\n    // this increment\'s scope did not include, namely which depth and whether\n    // exceeding it is an ordinary `JsonError` or something a caller must\n    // distinguish from bad syntax.\" The objection was right and is honoured\n    // rather than overruled: `MAX_DEPTH` is a declared contract, not a number\n    // read off this build\'s stack. Both deferred questions are answered -- the\n    // counting rule is above, and the failure is an ordinary `JsonError`,\n    // deliberately indistinguishable in shape from bad syntax, so a caller that\n    // must tell them apart matches the message. That is a weak contract and is\n    // named as one.\n    //\n    // The claim that no fixture could pin the threshold was true of a\n    // STACK threshold -- a fixture that crashed the process would fail the\n    // suite by construction -- and stops applying to a declared constant.\n    fn parse_value(mut self, d: Int) -> JsonValue {\n        if self.err.is_some() { return Null }\n        if d > MAX_DEPTH {\n            self.fail(\"maximum nesting depth exceeded\")\n            return Null\n        }\n        let c = self.peek()\n        if c == \'n\' { return self.keyword(\"null\", Null) }\n        if c == \'t\' { return self.keyword(\"true\", Bool(true)) }\n        if c == \'f\' { return self.keyword(\"false\", Bool(false)) }\n        if c == \'\"\' {\n            let s = self.scan_str()\n            return String(s)\n        }\n        if c == \'[\' { return self.scan_array(d) }\n        if c == \'{\' { return self.scan_object(d) }\n        if c == \'-\' || is_digit(c) { return self.scan_number() }\n        self.fail(\"expected a value\")\n        Null\n    }\n\n    // A literal name -- `null`, `true` or `false` -- matched character by\n    // character against `word`, returning `v` only if all of it is there.\n    //\n    // The mismatch is reported AT the offending character rather than at the\n    // start of the name, so `nul` fails at 3 (where the fourth character should\n    // be) and not at 0.\n    fn keyword(mut self, word: String, v: JsonValue) -> JsonValue {\n        let ws = word.chars()\n        let mut i = 0\n        while i < ws.len() {\n            if self.peek() != ws[i] {\n                self.fail(\"expected ${word}\")\n                return Null\n            }\n            self.bump()\n            i = i + 1\n        }\n        v\n    }\n\n    // A JSON array, cursor on `[`.\n    //\n    // The empty array is checked BEFORE the element loop, which is what makes\n    // `[]` legal while `[1,]` and `[,1]` are not: once the loop is entered, a\n    // value is required, because RFC 8259\'s `array` repetition ends on a value\n    // and never on a separator.\n    fn scan_array(mut self, d: Int) -> JsonValue {\n        self.bump()\n        self.skip_ws()\n        if self.peek() == \']\' {\n            self.bump()\n            let empty: [JsonValue] = []\n            return Array(empty)\n        }\n        let mut items: Vec<JsonValue> = Vec::new()\n        let mut going = true\n        while going {\n            let v = self.parse_value(d + 1)\n            if self.err.is_some() { return Null }\n            items.push(v)\n            self.skip_ws()\n            let c = self.peek()\n            if c == \',\' {\n                self.bump()\n                self.skip_ws()\n            } else if c == \']\' {\n                self.bump()\n                going = false\n            } else {\n                self.fail(\"expected \',\' or \']\' in an array\")\n                return Null\n            }\n        }\n        Array(vec_to_array(items))\n    }\n\n    // A JSON object, cursor on `{`.\n    //\n    // Mirrors `scan_array`, with a member in place of a value. The key check is\n    // explicit here rather than left to `scan_str`, so a missing key reports\n    // `expected an object key` -- RFC 8259 admits only a `string` as a member\n    // name, so `{a:1}` is an error and not a laxer reader\'s bare word.\n    //\n    // A repeated key keeps the LAST value, because `Map::insert` replaces.\n    // RFC 8259 section 4 sanctions no particular choice -- it calls the\n    // behaviour of a non-unique name \"unpredictable\" and observes that many\n    // implementations report the last pair only -- so this is a decision, not a\n    // requirement, and `json_parse_values.nova`\'s `duplicate key` case pins it.\n    fn scan_object(mut self, d: Int) -> JsonValue {\n        self.bump()\n        self.skip_ws()\n        let mut m: Map<String, JsonValue> = Map::new()\n        if self.peek() == \'}\' {\n            self.bump()\n            return Object(m)\n        }\n        let mut going = true\n        while going {\n            if self.peek() != \'\"\' {\n                self.fail(\"expected an object key\")\n                return Null\n            }\n            let k = self.scan_str()\n            if self.err.is_some() { return Null }\n            self.skip_ws()\n            if self.peek() != \':\' {\n                self.fail(\"expected \':\' after an object key\")\n                return Null\n            }\n            self.bump()\n            self.skip_ws()\n            // KNOWN UNPINNED, NAMED HERE RATHER THAN FIXED WITH A FIXTURE.\n            // Changing this `d + 1` to `d` leaves every `json_*` test passing,\n            // goldens byte-identical and the expected-failure assertions still\n            // met -- measured, by applying that mutation, rebuilding and\n            // re-running them, not inferred from reading. (This said \"all\n            // fourteen `json_*` goldens\" until 2026-08-26. That number counted\n            // the fixtures, and the expected-failure ones carry no golden, so it\n            // named the wrong set; the count is dropped rather than corrected.)\n            // What it would\n            // break if wrong is `MAX_DEPTH` in this direction: nesting through\n            // OBJECTS would stop counting, so an object-nested input could\n            // recurse arbitrarily deep and reach the stack overflow the cap\n            // exists to replace. It survives because both depth fixtures nest\n            // through ARRAYS -- `json_depth_leaf.nova` and\n            // `json_depth_empty.nova` build their inputs from `[` and `]` --\n            // which covers `scan_array`\'s twin of this line and not this one.\n            let v = self.parse_value(d + 1)\n            if self.err.is_some() { return Null }\n            m.insert(k, v)\n            self.skip_ws()\n            let c = self.peek()\n            if c == \',\' {\n                self.bump()\n                self.skip_ws()\n            } else if c == \'}\' {\n                self.bump()\n                going = false\n            } else {\n                self.fail(\"expected \',\' or \'}\' in an object\")\n                return Null\n            }\n        }\n        Object(m)\n    }\n\n    // A JSON string literal, cursor on the opening `\"`. Returns the DECODED\n    // contents, with every escape resolved; on failure `err` is set and the\n    // return value is `\"\"` rather than meaningful.\n    //\n    // The three arms are exactly RFC 8259\'s `char`: a closing quote ends the\n    // literal, a backslash starts an escape, and anything else is `unescaped`\n    // -- which the grammar defines as `%x20-21 / %x23-5B / %x5D-10FFFF`. That\n    // range starts at `0x20`, so a raw character below it is a SYNTAX ERROR and\n    // not a character to pass through: tab and line feed are legal whitespace\n    // BETWEEN tokens and still illegal unescaped inside a string. Note the\n    // range runs to `10FFFF` with no upper exclusion, so `0x7f` (DEL) is\n    // ordinary text here and needs no escape.\n    //\n    // The `peek() != \'\"\'` guard is defensive rather than load-bearing today:\n    // `parse_value` dispatches here only on `\"`, and `scan_object` checks for\n    // one itself. It is kept for the reason `bump`\'s bounds guard is -- a\n    // future caller may not position the cursor as carefully.\n    //\n    // `out` used to be described here as \"rebuilt by interpolation on every\n    // character, which is quadratic in the literal\'s length\", with Nova\n    // having \"no growable string buffer to do better with\". Both retracted by\n    // the roster below: `out` is now a `Vec<Char>`, appended to and drained\n    // once, and that buffer is exactly what the second clause said did not\n    // exist. What did NOT change is why a plain slice cannot stand in for the\n    // loop: a `span`-style slice still cannot replace it, because an escape\n    // means the decoded text is not a contiguous run of the input, so the\n    // characters still have to be assembled one at a time, only now into a\n    // buffer rather than by interpolation.\n    //\n    // THE ACCUMULATOR ROSTER IN THIS FILE, by name rather than by count, and\n    // what changed. `quote` and this function each built one string literal by\n    // interpolation; `stringify`\'s `Array` and `Object` arms built the whole\n    // rendered document that way. This function and `stringify` now append\n    // into a `Vec<Char>` (`out` here, and `stringify`\'s own separate output\n    // buffer, also named `out`, which is unrelated to its work list) drained\n    // once through `vec_chars_to_string` instead. `quote` hands the whole\n    // job to the runtime builtin `json_quote` (2026-10-03). So none is\n    // quadratic in what it emits.\n    //\n    // This used to close saying `stringify`\'s two arms \"do not yet\" -- that\n    // migration was called \"later work in this same increment, not this\n    // task\'s\". Both retracted: the migration is this task, `stringify` no\n    // longer recurses or interpolates, and there is no accumulator left in\n    // this file that still rebuilds a string by interpolation.\n    //\n    // Regrep before trusting that claim: `grep -n \'out = \"${out}\'` over this\n    // file must match nothing except text a comment quotes for illustration.\n    // This sentence is one such match, naming the pattern literally, and it\n    // is not the only one -- `buf_push_str`\'s own header and `stringify`\'s\n    // both quote the retired idiom too. Read every match before trusting the\n    // claim; do not assume a fixed count, here or anywhere this comment is\n    // cited from.\n    //\n    // RETRACTED, and quoted so the retraction is legible: this note used to\n    // close \"Neither is capped, and neither is fixable without a growable\n    // string buffer the language does not have.\" That is true only of a\n    // growable `String` TYPE, which Nova still lacks. It is false in the sense\n    // the sentence was used: `Vec<Char>` plus `str_from_chars` composes into\n    // exactly such a buffer, both already shipped, and both were already\n    // called from this very file -- `Vec` at `vec_to_array`, `str_from_chars`\n    // at `span`. The fix needed no language change.\n    //\n    // The measurements the old note carried described the interpolation form\n    // and are superseded; `buf_push_str` above carries the buffer\'s numbers.\n    // What did NOT change is `parse`\'s cost, which was already effectively\n    // flat: 279, 282 and 344 ms for an array of 4000, 8000 and 16000\n    // one-character numbers.\n    fn scan_str(mut self) -> String {\n        if self.err.is_some() { return \"\" }\n        if self.peek() != \'\"\' {\n            self.fail(\"expected a string\")\n            return \"\"\n        }\n        self.bump()\n        let mut out: Vec<Char> = Vec::new()\n        while true {\n            if self.at_end() {\n                self.fail(\"unterminated string\")\n                return \"\"\n            }\n            let c = self.peek()\n            if c == \'\"\' {\n                self.bump()\n                return vec_chars_to_string(out)\n            }\n            if c == \'\\\\\' {\n                self.bump()\n                let piece = self.scan_escape()\n                if self.err.is_some() { return \"\" }\n                buf_push_str(out, piece)\n            } else if char_to_int(c) < 32 {\n                self.fail(\"a control character must be escaped\")\n                return \"\"\n            } else {\n                self.bump()\n                out.push(c)\n            }\n        }\n        vec_chars_to_string(out)\n    }\n\n    // What one escape decodes to, cursor on the character AFTER the backslash.\n    //\n    // RFC 8259 allows exactly nine forms after a backslash, and a backslash\n    // followed by anything else is an error -- there is no \"unknown escapes\n    // pass through\" rule. The cursor is not advanced before the failure, so\n    // `at` points at the offending character rather than past it.\n    fn scan_escape(mut self) -> String {\n        if self.at_end() {\n            self.fail(\"unterminated string\")\n            return \"\"\n        }\n        let e = self.peek()\n        if e == \'u\' {\n            self.bump()\n            return self.scan_hex_escape()\n        }\n        match simple_escape(e) {\n            Some(s) => {\n                self.bump()\n                s\n            }\n            None => {\n                self.fail(\"unknown escape\")\n                \"\"\n            }\n        }\n    }\n\n    // `\\uXXXX`, cursor just past the `u`, and the surrogate pairing RFC 8259\n    // section 7 requires for anything above the BMP.\n    //\n    // There is NO Int-to-`Char` conversion in Nova, so this cannot build a\n    // `Char` from a code point. It encodes the code point to UTF-8 bytes here\n    // and hands them to `Bytes::to_string`, whose `Option` is the validation:\n    // `to_string` checks UTF-8 and returns `None`, so a code point that is not\n    // a Unicode scalar value becomes a `JsonError` instead of garbage. That is\n    // why no second intrinsic was added for this. The runtime\'s own\n    // `nova_rt_char_to_str` would have been strictly worse -- its body is\n    // `char::from_u32(v as u32).unwrap_or(char::REPLACEMENT_CHARACTER)`, so it\n    // silently substitutes U+FFFD for exactly the inputs that must be rejected.\n    //\n    // The `None` arm is therefore a SECOND line of defence, not a redundant\n    // one: the explicit surrogate checks above it make every code point that\n    // reaches the encoder a scalar value, so the arm cannot fire as written.\n    // Deleting the LONE-SURROGATE rejection makes it fire -- measured, not\n    // assumed: with that check gone, `\"\\udc00\"` reports `an escape is not a\n    // valid code point`, because a lone surrogate encodes to a three-byte\n    // sequence Rust\'s `str::from_utf8` rejects. So the error survives the loss\n    // of that check, and only its message changes.\n    //\n    // Deleting the LOW-SURROGATE RANGE check does NOT reach that arm, and the\n    // asymmetry is why both checks are here rather than one. The\n    // `high surrogate then a non-surrogate escape` case in\n    // `json_parse_strings.nova` -- a high surrogate followed by an escape whose\n    // value is `0x41` -- then combines to `0x11841`, a perfectly ordinary\n    // character, so that input is silently ACCEPTED instead of rejected.\n    // Measured on its own, with the check above this one left in place.\n    fn scan_hex_escape(mut self) -> String {\n        let hi = self.scan_hex4()\n        if self.err.is_some() { return \"\" }\n        if hi >= 0xDC00 && hi <= 0xDFFF {\n            self.fail(\"a low surrogate must follow a high surrogate\")\n            return \"\"\n        }\n        let mut cp = hi\n        if hi >= 0xD800 && hi <= 0xDBFF {\n            if self.peek() != \'\\\\\' {\n                self.fail(\"a high surrogate must be followed by a low surrogate\")\n                return \"\"\n            }\n            self.bump()\n            if self.peek() != \'u\' {\n                self.fail(\"a high surrogate must be followed by a low surrogate\")\n                return \"\"\n            }\n            self.bump()\n            let lo = self.scan_hex4()\n            if self.err.is_some() { return \"\" }\n            if lo < 0xDC00 || lo > 0xDFFF {\n                self.fail(\"expected a low surrogate\")\n                return \"\"\n            }\n            cp = 0x10000 + (hi - 0xD800) * 0x400 + (lo - 0xDC00)\n        }\n        match bytes_from_ints(utf8_bytes(cp)).to_string() {\n            Some(s) => s\n            None => {\n                self.fail(\"an escape is not a valid code point\")\n                \"\"\n            }\n        }\n    }\n\n    // Exactly four hexadecimal digits, as an `Int`. FOUR is the grammar\'s own\n    // count (`%x75 4HEXDIG`), not a convenience: reading fewer would leave the\n    // remaining digits to be taken as literal text, and reading more would\n    // swallow the character after the escape.\n    //\n    // A non-hex character stops this, and so does the end of the input, since\n    // `peek`\'s past-the-end `\'\\0\'` is not a hexadecimal digit.\n    fn scan_hex4(mut self) -> Int {\n        let mut v = 0\n        let mut i = 0\n        while i < 4 {\n            let d = hex_value(self.peek())\n            if d < 0 {\n                self.fail(\"expected four hexadecimal digits\")\n                return 0\n            }\n            v = v * 16 + d\n            self.bump()\n            i = i + 1\n        }\n        v\n    }\n}\n\n// An ASCII digit. `Char` admits `<=` and `>=` directly (`impl Ord for Char` in\n// `std/core` is built on `<`), so this needs no conversion to `Int` -- which\n// also keeps the past-the-end sentinel `\'\\0\'` a non-digit for free.\nfn is_digit(c: Char) -> Bool { c >= \'0\' && c <= \'9\' }\n\n// One hexadecimal digit\'s value, or `-1` for anything that is not one. `-1`\n// rather than `Option<Int>` because the single caller is a four-iteration loop\n// and an `Option` there buys a `match` per digit and nothing else.\n//\n// The offsets are the ASCII arithmetic: `\'0\'` is 48, `\'a\'` is 97 and 97 - 87 is\n// 10, `\'A\'` is 65 and 65 - 55 is 10. Both letter cases are accepted because RFC\n// 8259\'s `HEXDIG` is `DIGIT / \"A\" / ... / \"F\"` under ABNF\'s case-insensitive\n// rule for literals.\nfn hex_value(c: Char) -> Int {\n    if c >= \'0\' && c <= \'9\' { return char_to_int(c) - 48 }\n    if c >= \'a\' && c <= \'f\' { return char_to_int(c) - 87 }\n    if c >= \'A\' && c <= \'F\' { return char_to_int(c) - 55 }\n    // `return`, not a bare `-1`: after the `if` block above, a statement\n    // starting `-` parses as SUBTRACTION from that block\'s value. Measured.\n    return -1\n}\n\n// `cp` encoded as UTF-8, one `Int` per byte. The four length classes are UTF-8\'s\n// own: `0..0x80` in one byte, `0x80..0x800` in two, `0x800..0x10000` in three,\n// and the rest in four.\n//\n// Nova has no `Char` built from an `Int`, so this is the only route from a\n// `\\uXXXX` code point to a `String`, via `bytes_from_ints` and\n// `Bytes::to_string`.\n//\n// The out-of-range guard returns `0xFF`, which is not a legal UTF-8 byte in any\n// position, so such a code point becomes a `to_string` failure at the call site\n// rather than a mangled character. It is UNREACHABLE as written -- four\n// hexadecimal digits cannot exceed `0xFFFF`, four of them cannot be negative,\n// and the surrogate combine\'s own maximum is `0x10000 + 0x3FF * 0x400 + 0x3FF`,\n// which is exactly `0x10FFFF`.\n//\n// It is here because the two arms it shields are the only expressions below\n// whose result is not bounded to `0..=255` by construction: `[cp]` for a\n// NEGATIVE `cp`, and the four-byte lead `0xF0 | (cp >> 18)` for an oversized\n// one. Every other byte is masked or has a bounded input -- `0xC0 | (cp >> 6)`\n// tops out at `0xDF`, `0xE0 | (cp >> 12)` at `0xEF`, and a continuation byte at\n// `0xBF`. And the shielding matters because `bytes_from_ints` ABORTS the process\n// on an element outside `0..=255` rather than returning an error.\nfn utf8_bytes(cp: Int) -> [Int] {\n    if cp < 0 || cp > 0x10FFFF { return [0xFF] }\n    if cp < 0x80 { return [cp] }\n    if cp < 0x800 { return [0xC0 | (cp >> 6), 0x80 | (cp & 0x3F)] }\n    if cp < 0x10000 { return [0xE0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3F), 0x80 | (cp & 0x3F)] }\n    // `return`, not a bare trailing expression: after the `if` block above, a\n    // statement starting `[` parses as an INDEX into that block\'s value and\n    // fails on the first comma. Measured, not stylistic.\n    return [\n        0xF0 | (cp >> 18),\n        0x80 | ((cp >> 12) & 0x3F),\n        0x80 | ((cp >> 6) & 0x3F),\n        0x80 | (cp & 0x3F),\n    ]\n}\n\n// A `Vec` copied into the plain `[JsonValue]` that `Array` holds.\n//\n// `scan_array` collects into a `Vec` because it cannot know the element count\n// before it has parsed them. This used to be called \"the one-way conversion\n// out\"; it no longer is one -- `vec_chars_to_string`, added above `quote` for\n// the string buffer, drains a `Vec<Char>` the same way. `Null` is the repeat\n// literal\'s filler -- every slot is overwritten below, so it never survives,\n// and `JsonValue` having a nullary variant is why this needs no `Default`\n// bound.\n//\n// Reads the backing array directly, for the reason `vec_chars_to_string`\n// gives: `i` runs strictly below `v.len()`, so `Vec::get`\'s `Option` could\n// never be `None` here, and each `Some` would be a heap allocation per\n// element. The index is bounds checked, as there.\nfn vec_to_array(v: Vec<JsonValue>) -> [JsonValue] {\n    let n = v.len()\n    let mut out = [Null; n]\n    let mut i = 0\n    while i < n {\n        out[i] = v.data[i]\n        i = i + 1\n    }\n    out\n}\n\n// The eight one-character escapes of RFC 8259 section 7, or `None` for any other\n// character after a backslash. `\\u` is deliberately absent: it consumes four\n// more characters and so needs the cursor, which a free function does not have.\n//\n// `\\b` and `\\f` are written as string `\\u{...}` escapes because Nova\'s CHAR\n// literal admits only `\\n \\t \\r \\\\ \\\' \\0` -- measured against this compiler:\n// anything else falls through `lex_char`\'s `_ => None`, so `\'\\u{0C}\'` does not\n// lex at all, while the string lexer\'s own `\\u{...}` does.\nfn simple_escape(c: Char) -> Option<String> {\n    if c == \'\"\' { return Some(\"\\\"\") }\n    if c == \'\\\\\' { return Some(\"\\\\\") }\n    if c == \'/\' { return Some(\"/\") }\n    if c == \'b\' { return Some(\"\\u{08}\") }\n    if c == \'f\' { return Some(\"\\u{0C}\") }\n    if c == \'n\' { return Some(\"\\n\") }\n    if c == \'r\' { return Some(\"\\r\") }\n    if c == \'t\' { return Some(\"\\t\") }\n    None\n}\n\n// === ToJson / FromJson ===\n//\n// `nova-spec/20-STDLIB.md` section 7 writes both traits with `Self` in\n// return position:\n//\n//     pub trait ToJson { fn to_json(self) -> JsonValue }\n//     pub trait FromJson { fn from_json(v: JsonValue) -> Result<Self, JsonError> }\n//\n// An `impl`, unlike the trait declaration, writes the CONCRETE type in that\n// position -- `impl Default for Int { fn default() -> Int { 0 } }` at\n// `std/core/lib.nova` is what proves that compiles; every impl below follows\n// that form, not `Self`.\n/// A type that can be converted to a `JsonValue`.\n///\n/// std implements it for `Bool`, `String`, `Float` and `Int`. A type of your\n/// own can implement it too, for instance by building an `Object` from its\n/// fields.\n///\n/// A record of your own written as JSON:\n///\n/// ```\n/// record Point { x: Int, y: Int }\n///\n/// impl ToJson for Point {\n///     fn to_json(self) -> JsonValue {\n///         let mut fields: Map<String, JsonValue> = Map::new()\n///         fields.insert(\"x\", self.x.to_json())\n///         fields.insert(\"y\", self.y.to_json())\n///         Object(fields)\n///     }\n/// }\n///\n/// fn main() {\n///     let text = stringify(Point { x: 1, y: 2 }.to_json())\n///     // An object\'s members come out in an order that can change between runs.\n///     assert(text == \"{\\\"x\\\":1,\\\"y\\\":2}\" || text == \"{\\\"y\\\":2,\\\"x\\\":1}\", text)\n/// }\n/// ```\npub trait ToJson {\n    /// Returns `self` as a `JsonValue`.\n    ///\n    /// std\'s impls give a `Bool`, a `String` or a `Number`. An `Int` larger\n    /// in magnitude than 2^53 (9,007,199,254,740,992) is rounded to the\n    /// nearest `Float`, so it can lose its exact value.\n    fn to_json(self) -> JsonValue\n}\n\n/// A type that can be built from a `JsonValue`.\n///\n/// std implements it for `Bool`, `String`, `Float` and `Int`.\npub trait FromJson {\n    /// Returns the value of this type that `v` holds, or a `JsonError` if `v`\n    /// does not hold one.\n    ///\n    /// std\'s impls convert nothing: each accepts only its own variant, so\n    /// `Null` is not `false` and a `Number` is not a `String`. A wrong variant\n    /// gives the error `expected a bool`, `expected a string` or\n    /// `expected a number`. `Int` accepts only a whole number within its\n    /// range: a fraction or `NaN` gives `expected an integer`, and a number\n    /// out of range, an infinity included, `integer out of range`.\n    fn from_json(v: JsonValue) -> Result<Self, JsonError>\n}\n\n// Every `from_json` below that returns `Err` uses `at: 0` -- see\n// `JsonError`\'s own doc comment above for why a fixed placeholder is\n// correct here rather than an oversight.\n//\n// Scope stops at four primitives -- `Bool`, `String`, `Float`, `Int` -- on\n// purpose. No container impl (`[T]`, `Map<String, T>`, `Option<T>`) is\n// added: each one is more public API this module would then owe a fixture\n// for `Array`, `Object`, `T: ToJson` would need a bound this module cannot\n// express any better than by hand, and nothing this task\'s brief asks for\n// needs one. That is exactly the \"endless tail of instances\" YAGNI is\n// guarding against here.\n\n// `Bool` and `JsonValue`\'s `Bool` variant share a spelling; `Bool(self)`\n// below constructs the variant, not the type. Proven the same way this\n// module\'s own parser already relies on it: `parse_value` builds\n// `Bool(true)`/`Bool(false)` as VALUES, not only as patterns, and that is\n// measured against this compiler in this file\'s header comment.\nimpl ToJson for Bool {\n    fn to_json(self) -> JsonValue { Bool(self) }\n}\n\nimpl FromJson for Bool {\n    // Anything but `Bool` is a decode error, never a guess -- `Number(0.0)`\n    // and `Null` do NOT become `false`.\n    fn from_json(v: JsonValue) -> Result<Bool, JsonError> {\n        match v {\n            Bool(b) => Ok(b)\n            _ => Err(JsonError { msg: \"expected a bool\", at: 0 })\n        }\n    }\n}\n\nimpl ToJson for String {\n    fn to_json(self) -> JsonValue { String(self) }\n}\n\nimpl FromJson for String {\n    fn from_json(v: JsonValue) -> Result<String, JsonError> {\n        match v {\n            String(s) => Ok(s)\n            _ => Err(JsonError { msg: \"expected a string\", at: 0 })\n        }\n    }\n}\n\nimpl ToJson for Float {\n    // Exact: `Number` already IS an `f64`, so nothing here can round.\n    fn to_json(self) -> JsonValue { Number(self) }\n}\n\nimpl FromJson for Float {\n    // Every `Number` is a valid `Float` -- non-finite and fractional ones\n    // included. `Float` has no narrower range to reject a `Number` into, so\n    // unlike `Int` below this has nothing to decide.\n    fn from_json(v: JsonValue) -> Result<Float, JsonError> {\n        match v {\n            Number(n) => Ok(n)\n            _ => Err(JsonError { msg: \"expected a number\", at: 0 })\n        }\n    }\n}\n\n// RULE (encode): an `Int` too large for an `f64` to hold exactly converts\n// anyway, rounding SILENTLY to the nearest value a `Float` can represent.\n// `Int::to_json` returns `JsonValue`, not a `Result`, so there is no error\n// channel here even if refusing were wanted.\n//\n// An `f64` mantissa carries 53 bits, so every `Int` in `-2^53..=2^53`\n// survives this exactly; the first one that does not is `2^53 + 1 ==\n// 9007199254740993`, which this rounds to `9007199254740992` (round half to\n// even: `9007199254740992` and `9007199254740994` are its two representable\n// neighbours, equally far away, and `9007199254740992` is the one with an\n// even low mantissa bit). Measured against this compiler, not assumed:\n// `json_traits.nova`\'s \"precision loss at the f64 mantissa boundary\" case\n// pins that exact pair colliding on one rendered JSON text.\n//\n// The conversion goes through `str_to_float`, the only PRIMITIVE route from\n// text to `Float` this language has -- there is no `as` cast (`E0900`) and no\n// Int-to-Float intrinsic anywhere in Nova today. `\"${self}\"` is `Int`\'s own\n// `Display`, exact for the full `Int` range, so all of the rounding above\n// happens inside `str_to_float` and none of it in the formatting step.\nimpl ToJson for Int {\n    fn to_json(self) -> JsonValue { Number(str_to_float(\"${self}\")) }\n}\n\n// RULE (decode): a `Number` converts to `Int` only when its value is both\n// IN RANGE for `Int` and carries no fraction. Two independent failures,\n// checked in this order:\n//\n//   * a magnitude outside `i64::MIN..=i64::MAX`\n//     (`-9223372036854775808..=9223372036854775807`) is an `Err`, never a\n//     wrapped value. An earlier version of this function had no bound\n//     check at all: a too-large `Number` reached the digit accumulation\n//     below and wrapped silently, the same way `std/core`\'s `mix64`\n//     documents `Int`\'s own `*` wrapping rather than trapping. That is\n//     fine for `mix64`, which is mixing bits on purpose; it is not fine\n//     here, decoding externally-supplied JSON, where \"a fraction is a\n//     decode error and never a silent truncation\" (next bullet) and a\n//     wrapped-away magnitude is the same failure mode applied to the\n//     integer\'s size instead of its precision.\n//   * a fraction is a decode error and never a silent truncation --\n//     `Number(3.0)` decodes to `3`, `Number(3.5)` is an `Err`. This module\n//     already marks `stringify`\'s non-finite-to-`null` collapse\n//     \"DELIBERATELY LOSSY\" in the encode direction, and letting\n//     `from_json` throw away a fraction in the decode direction would make\n//     a round trip fabricate a value instead of reporting that it could\n//     not produce one.\n//\n// The bound check compares `n` itself, before any text exists, against\n// `-9223372036854775808.0` (`i64::MIN`) and `9223372036854775808.0` (one\n// past `i64::MAX`). Both are exact `Float` values -- both are powers of\n// two -- which `i64::MAX` itself, `9223372036854775807`, is NOT (the\n// nearest `Float` to it is `9223372036854775808.0`, the same value used\n// as the rejected upper bound), so this never has to ask whether an\n// inexact `Float` equals an exact `Int` edge. `NaN` fails both `<` and\n// `>=` here. `<` and `==` are both `false` whenever either operand is\n// `NaN`, which is what `std/core`\'s own note on `impl Ord for Float`\n// states -- that note names those two operators and no others. The same\n// holds for `>`, `<=` and `>=` under IEEE 754, and of those the `<` and\n// `>=` used right here are the ones measured below; `>` and `<=` are\n// neither cited nor measured anywhere in this tree. `!=` is the\n// exception and is `true`, which is exactly what `number_to_json`\'s\n// `n - n != 0.0` finiteness test above depends on, so \"a comparison\n// against `NaN` is always false\" would contradict this file\'s own\n// machinery. So `NaN` is not rejected by the range check; it is caught\n// immediately after, by the fraction check,\n// since `\"NaN\"` is not digit-shaped either. Measured against this\n// compiler, not assumed: a throwaway `out_of_range(n)` returned `false`\n// for `0.0 / 0.0`, and `true` for both `1.0 / 0.0` and its negation.\n//\n// There is no Float-to-Int intrinsic and no `as` cast (`E0900`) anywhere\n// in Nova today, so getting to `Int` at all means walking decimal digits\n// by hand, the way `scan_hex4` above walks hex ones -- over TWO different\n// texts of `n`, not one, and this is the second decision this rule makes\n// explicit rather than by accident:\n//\n//   * `\"${n}\"`, `Float`\'s `Display`, checked ONLY for shape below (every\n//     character but an optional leading `-` must be `0`-`9`) to decide\n//     fraction-or-not. Its DIGITS are never used for the value: `Display`\n//     is a SHORTEST-ROUND-TRIP rendering, not an exact one, and once a\n//     `Float`\'s neighbouring representable values are more than 1 apart,\n//     the shortest text that round-trips can be a rounder nearby number\n//     rather than the exact one. Measured, not assumed:\n//     `\"${-9223372036854775808.0}\"` (`i64::MIN`, exactly representable,\n//     being a power of two) is `\"-9223372036854776000\"` -- OFF BY 192 from\n//     the value it names, and also outside `Int`\'s own range -- exactly\n//     the shape of bug this rule exists to refuse rather than launder\n//     into a wrapped `Int`.\n//   * `float_fixed(n, 0)` for the actual digits, which IS exact --\n//     `std/fmt`\'s own comment on `Float::fixed` gives the reason, that\n//     Nova \"cannot begin on it, having no `Float` to `Int` conversion at\n//     all\", and `nova_rt_float_fixed` is Rust\'s `format!(\"{:.0}\", v)`, an\n//     exact fixed-precision conversion rather than a shortest-round-trip\n//     one. `float_fixed(-9223372036854775808.0, 0)` is\n//     `\"-9223372036854775808\"`, the exact value. This is used ONLY for\n//     the digits, never for the shape check above it -- `float_fixed`\n//     ROUNDS a fraction rather than reporting one\n//     (`float_fixed(3.5, 0)` is `\"4\"`), which would silently turn the\n//     fraction rule above into the very truncation it rejects, if this\n//     text were the one checked for a `.`.\nimpl FromJson for Int {\n    fn from_json(v: JsonValue) -> Result<Int, JsonError> {\n        match v {\n            Number(n) => {\n                if n < -9223372036854775808.0 || n >= 9223372036854775808.0 {\n                    Err(JsonError { msg: \"integer out of range\", at: 0 })\n                } else if parse_decimal_int(\"${n}\").is_none() {\n                    Err(JsonError { msg: \"expected an integer\", at: 0 })\n                } else {\n                    match parse_decimal_int(float_fixed(n, 0)) {\n                        Some(i) => Ok(i)\n                        None => Err(JsonError { msg: \"expected an integer\", at: 0 })\n                    }\n                }\n            }\n            _ => Err(JsonError { msg: \"expected a number\", at: 0 })\n        }\n    }\n}\n\n// `s`\'s digits as an `Int`, or `None` if any character once an optional\n// leading `-` is consumed is not `0`-`9`, or if there are no digits at all\n// (an empty string, or a bare `\"-\"`). `from_json` above calls this twice,\n// on two different texts of the same `Number` -- see its comment for why.\n//\n// Accumulates NEGATIVE throughout, even for a positive input, negating\n// only once at the very end, rather than the more obvious positive\n// accumulation negated only when `neg` is set.\n//\n// NOT because the positive form gives a wrong answer -- measured, not\n// assumed: mutating this to positive accumulation changes not one output\n// anywhere in the whole 44-target suite. The reason is `i64::MIN`\'s own\n// magnitude, `9223372036854775808`, being one past `i64::MAX`: a positive\n// running total reaches exactly `2^63` on that value\'s last digit, wraps\n// once there, and wraps a second time negating the wrapped result, and\n// Nova\'s `Int` wraps CONSISTENTLY (mod `2^64`, the same arithmetic\n// `std/core`\'s `mix64` documents for `*`) -- so the two wraps cancel and\n// the final value comes out correct either way.\n//\n// Negative accumulation is used anyway because its correctness does not\n// need that cancellation argument at all: every partial sum stays\n// DIRECTLY representable at every step, for `i64::MIN` and for everything\n// smaller in magnitude, with no appeal to wraparound anywhere in the\n// reasoning. \"Everything smaller in magnitude\" is the whole of what this\n// function is ever asked to parse, but that is an invariant of its ONE\n// caller, not of this function by itself: `from_json` above rejects any\n// `n` outside `i64::MIN..=i64::MAX` by comparing the `Float` itself,\n// before either text this function walks is ever built. This function\n// does not repeat that check -- it has no way to, from a `String` alone,\n// without re-deriving the same comparison from the digits it is still in\n// the middle of reading -- so it stays correct for a magnitude its\n// caller has already ruled out, by cancellation, for exactly the reason\n// measured above; it would not stay correct for a magnitude past that,\n// which is why the bound check in `from_json` is load-bearing and this\n// accumulation direction, on its own, is not a substitute for it.\nfn parse_decimal_int(s: String) -> Option<Int> {\n    let cs = s.chars()\n    let mut i = 0\n    let mut neg = false\n    if cs.len() > 0 && cs[0] == \'-\' {\n        neg = true\n        i = 1\n    }\n    if i >= cs.len() { return None }\n    let mut v = 0\n    while i < cs.len() {\n        if !is_digit(cs[i]) { return None }\n        v = v * 10 - (char_to_int(cs[i]) - 48)\n        i = i + 1\n    }\n    if neg { Some(v) } else { Some(-v) }\n}\n";
Expand description

std/json/lib.nova.