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.