// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Read one document in the given dialect.
///
/// The whole input must be one value: anything after it, other than whitespace
/// and — where the dialect allows them — comments, is [`Trailing`], not silently
/// ignored. A parser that stops at the first value turns a truncated file into a
/// plausible one.
pub fn loads(input : StringView, flavor : Flavor) -> Json raise Malformed {
let r = { src: input, flavor, i: 0, }
r.space()
let value = r.value(0)
r.space()
if r.i < r.src.length() {
raise Trailing(r.spot(r.i))
}
value
}
///|
/// Read one document from bytes, which is how one arrives over a wire.
/// A leading byte-order mark is skipped by default. RFC 8259 §8.1 forbids
/// writing one and says nothing about refusing to read one, and refusing would
/// mean rejecting half the files Windows tooling produces.
pub fn load(
input : BytesView,
flavor : Flavor,
bom? : Bool = true,
) -> Json raise Malformed {
let text = @utf8.decode(input, ignore_bom=bom) catch {
_ => raise BadUtf8({ at: 0, line: 1, column: 1, })
}
loads(text[:], flavor)
}
///|
priv struct Reader {
src : StringView
flavor : Flavor
mut i : Int
}
///|
/// Line and column are counted only once something has gone wrong, so the
/// reader carries no bookkeeping through the common case.
fn Reader::spot(self : Reader, at : Int) -> At {
let mut line = 1
let mut column = 1
for k in 0.. Int {
if k < self.src.length() {
self.src.at(k).to_int()
} else {
-1
}
}
///|
fn Reader::here(self : Reader) -> Int {
self.peek(self.i)
}
///|
fn Reader::fail(self : Reader) -> Malformed {
let c = self.here()
if c < 0 {
Truncated(self.spot(self.i))
} else {
Unexpected(self.spot(self.i), c.unsafe_to_char())
}
}
///|
fn Reader::space(self : Reader) -> Unit raise Malformed {
let mut going = true
while going {
let c = self.here()
if c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0D {
self.i += 1
} else if self.flavor.extra_space && is_extra_space(c) {
self.i += 1
} else if self.flavor.comments && c == 0x2F && self.peek(self.i + 1) == 0x2F {
self.i += 2
while self.here() >= 0 && !ends_line(self.here()) {
self.i += 1
}
} else if self.flavor.comments && c == 0x2F && self.peek(self.i + 1) == 0x2A {
let start = self.i
self.i += 2
let mut closed = false
while !closed {
if self.here() < 0 {
raise Truncated(self.spot(start))
} else if self.here() == 0x2A && self.peek(self.i + 1) == 0x2F {
self.i += 2
closed = true
} else {
self.i += 1
}
}
} else {
going = false
}
}
}
///|
fn ends_line(c : Int) -> Bool {
c == 0x0A || c == 0x0D || c == 0x2028 || c == 0x2029
}
///|
/// The whitespace ECMAScript 5 recognises (JSON5 §2): the four JSON allows, plus
/// vertical tab, form feed, no-break space, the byte-order mark, the line and
/// paragraph separators, and the Unicode space separators.
fn is_extra_space(c : Int) -> Bool {
c == 0x0B ||
c == 0x0C ||
c == 0xA0 ||
c == 0xFEFF ||
c == 0x2028 ||
c == 0x2029 ||
c == 0x1680 ||
(c >= 0x2000 && c <= 0x200A) ||
c == 0x202F ||
c == 0x205F ||
c == 0x3000
}
///|
fn Reader::value(self : Reader, depth : Int) -> Json raise Malformed {
if depth > self.flavor.depth {
raise TooDeep(self.spot(self.i))
}
match self.here() {
0x7B => self.object(depth)
0x5B => self.array(depth)
0x22 => Json::string(self.text())
0x27 =>
if self.flavor.single_quotes {
Json::string(self.text())
} else {
raise self.fail()
}
0x74 => {
self.word("true")
Json::boolean(true)
}
0x66 => {
self.word("false")
Json::boolean(false)
}
0x6E => {
self.word("null")
Json::null()
}
_ => self.number()
}
}
///|
fn Reader::word(self : Reader, want : String) -> Unit raise Malformed {
let start = self.i
for k in 0.. Json raise Malformed {
let out : Map[String, Json] = Map([])
self.i += 1
self.space()
if self.here() == 0x7D {
self.i += 1
return Json::object(out)
}
let mut open = true
while open {
self.space()
let key = if self.here() == 0x22 ||
(self.flavor.single_quotes && self.here() == 0x27) {
self.text()
} else if self.flavor.unquoted_keys {
self.name()
} else {
raise self.fail()
}
self.space()
if self.here() != 0x3A {
raise self.fail()
}
self.i += 1
self.space()
let value = self.value(depth + 1)
match (out.get(key), self.flavor.duplicates) {
(Some(_), First) => ()
(Some(_), Reject) => raise Repeated(self.spot(self.i), key)
_ => out[key] = value
}
self.space()
match self.here() {
0x2C => {
self.i += 1
self.space()
if self.here() == 0x7D {
if !self.flavor.trailing_commas {
raise self.fail()
}
self.i += 1
open = false
}
}
0x7D => {
self.i += 1
open = false
}
_ => raise self.fail()
}
}
Json::object(out)
}
///|
fn Reader::array(self : Reader, depth : Int) -> Json raise Malformed {
let out : Array[Json] = []
self.i += 1
self.space()
if self.here() == 0x5D {
self.i += 1
return Json::array(out)
}
let mut open = true
while open {
self.space()
out.push(self.value(depth + 1))
self.space()
match self.here() {
0x2C => {
self.i += 1
self.space()
if self.here() == 0x5D {
if !self.flavor.trailing_commas {
raise self.fail()
}
self.i += 1
open = false
}
}
0x5D => {
self.i += 1
open = false
}
_ => raise self.fail()
}
}
Json::array(out)
}
///|
/// An unquoted member name (JSON5 §4): an ECMAScript identifier. `$` and `_`
/// start one, as does any letter; digits may follow.
fn Reader::name(self : Reader) -> String raise Malformed {
let start = self.i
while is_name_part(self.here(), self.i == start) {
self.i += 1
}
if self.i == start {
raise self.fail()
}
self.src[start:self.i].to_owned()
}
///|
fn is_name_part(c : Int, first : Bool) -> Bool {
c == 0x24 ||
c == 0x5F ||
(c >= 0x41 && c <= 0x5A) ||
(c >= 0x61 && c <= 0x7A) ||
c > 0x7F ||
(!first && c >= 0x30 && c <= 0x39)
}
///|
fn Reader::text(self : Reader) -> String raise Malformed {
let quote = self.here()
let open = self.i
self.i += 1
let out = StringBuilder()
let mut run = self.i
while self.here() >= 0 {
let c = self.here()
if c == quote {
out.write_view(self.src[run:self.i])
self.i += 1
return out.to_string()
}
if c == 0x5C {
out.write_view(self.src[run:self.i])
self.escape(out)
run = self.i
continue
}
// RFC 8259 §7: a control character must be written as an escape. JSON5
// keeps that rule for everything but the line terminators it allows after
// a backslash, which `escape` handles.
if c < 0x20 {
raise self.fail()
}
self.i += 1
}
raise Truncated(self.spot(open))
}
///|
fn Reader::escape(self : Reader, out : StringBuilder) -> Unit raise Malformed {
let start = self.i
self.i += 1
let c = self.here()
self.i += 1
match c {
0x22 => out.write_char('"')
0x5C => out.write_char('\\')
0x2F => out.write_char('/')
0x62 => out.write_char('\u{08}')
0x66 => out.write_char('\u{0C}')
0x6E => out.write_char('\n')
0x72 => out.write_char('\r')
0x74 => out.write_char('\t')
0x75 => out.write_char(self.unicode(start))
_ => self.loose_escape(out, c, start)
}
}
///|
fn Reader::loose_escape(
self : Reader,
out : StringBuilder,
c : Int,
start : Int,
) -> Unit raise Malformed {
if c < 0 {
raise Truncated(self.spot(start))
}
if !self.flavor.extra_escapes {
raise BadEscape(self.spot(start))
}
match c {
// A backslash before a line terminator continues the string and adds
// nothing (JSON5 §5); a CRLF pair counts once.
0x0A | 0x2028 | 0x2029 => ()
0x0D => if self.here() == 0x0A { self.i += 1 }
0x76 => out.write_char('\u{0B}')
0x78 => out.write_char(self.hex_byte(start))
0x30 =>
if self.here() >= 0x30 && self.here() <= 0x39 {
raise BadEscape(self.spot(start))
} else {
out.write_char('\u{00}')
}
_ =>
// A digit after a backslash is a legacy octal escape, which JSON5 does
// not adopt.
if c >= 0x31 && c <= 0x39 {
raise BadEscape(self.spot(start))
} else {
out.write_char(c.unsafe_to_char())
}
}
}
///|
/// `\uXXXX`, joining a surrogate pair into the one character it stands for.
///
/// A lone half is refused. It is not a character, it cannot be written back
/// out, and every value that could be put in its place is a guess about what
/// the writer meant.
fn Reader::unicode(self : Reader, start : Int) -> Char raise Malformed {
let hi = self.quad(start)
if hi < 0xD800 || hi > 0xDFFF {
return hi.unsafe_to_char()
}
if hi >= 0xDC00 {
if self.flavor.surrogates {
return replacement
}
raise BadEscape(self.spot(start))
}
if self.here() != 0x5C || self.peek(self.i + 1) != 0x75 {
if self.flavor.surrogates {
return replacement
}
raise BadEscape(self.spot(start))
}
let pair = self.i
self.i += 2
let lo = self.quad(pair)
if lo < 0xDC00 || lo > 0xDFFF {
if self.flavor.surrogates {
return replacement
}
raise BadEscape(self.spot(pair))
}
(0x10000 + (hi - 0xD800) * 0x400 + (lo - 0xDC00)).unsafe_to_char()
}
///|
fn Reader::quad(self : Reader, start : Int) -> Int raise Malformed {
let mut v = 0
for _ in 0..<4 {
let d = nibble(self.here())
if d < 0 {
raise BadEscape(self.spot(start))
}
v = (v << 4) | d
self.i += 1
}
v
}
///|
fn Reader::hex_byte(self : Reader, start : Int) -> Char raise Malformed {
let hi = nibble(self.here())
let lo = nibble(self.peek(self.i + 1))
if hi < 0 || lo < 0 {
raise BadEscape(self.spot(start))
}
self.i += 2
((hi << 4) | lo).unsafe_to_char()
}
///|
fn nibble(c : Int) -> Int {
if c >= 0x30 && c <= 0x39 {
c - 0x30
} else if c >= 0x61 && c <= 0x66 {
c - 0x61 + 10
} else if c >= 0x41 && c <= 0x46 {
c - 0x41 + 10
} else {
-1
}
}
///|
/// A number, keeping the text it was written as when that text is also valid
/// RFC 8259 — so a twenty-digit integer comes back out unchanged instead of
/// through a double.
fn Reader::number(self : Reader) -> Json raise Malformed {
let start = self.i
let mut signed = false
let mut plain = true
if self.here() == 0x2D {
signed = true
self.i += 1
} else if self.here() == 0x2B {
if !self.flavor.extra_numbers {
raise self.fail()
}
self.i += 1
plain = false
}
if self.flavor.extra_numbers {
if self.here() == 0x49 {
self.word("Infinity")
return Json::number(
if signed {
@double.neg_infinity
} else {
@double.infinity
},
)
}
if self.here() == 0x4E {
self.word("NaN")
return Json::number(@double.not_a_number)
}
if self.here() == 0x30 &&
(self.peek(self.i + 1) == 0x78 || self.peek(self.i + 1) == 0x58) {
return self.hex_number(start, signed)
}
}
let int_start = self.i
while self.here() >= 0x30 && self.here() <= 0x39 {
self.i += 1
}
let digits = self.i - int_start
if digits == 0 {
// `.5` is a JSON5 number; a bare sign is not a number at all.
if !(self.flavor.extra_numbers && self.here() == 0x2E) {
raise self.fail()
}
plain = false
} else if digits > 1 && self.src.at(int_start).to_int() == 0x30 {
raise BadNumber(self.spot(start))
}
let mut fraction = 0
if self.here() == 0x2E {
self.i += 1
let frac = self.i
while self.here() >= 0x30 && self.here() <= 0x39 {
self.i += 1
}
fraction = self.i - frac
if fraction == 0 {
// `5.` is a JSON5 number, and nothing else ends in a bare point.
if !self.flavor.extra_numbers {
raise BadNumber(self.spot(start))
}
plain = false
}
}
// A lone point, or a sign with nothing after it, is not a number however
// relaxed the dialect.
if digits + fraction == 0 {
raise BadNumber(self.spot(start))
}
if self.here() == 0x65 || self.here() == 0x45 {
self.i += 1
if self.here() == 0x2B || self.here() == 0x2D {
self.i += 1
}
let exp = self.i
while self.here() >= 0x30 && self.here() <= 0x39 {
self.i += 1
}
if self.i == exp {
raise BadNumber(self.spot(start))
}
}
let text = self.src[start:self.i]
let value = @string.parse_double(text) catch { _ => out_of_range(text) }
if plain {
Json::number(value, repr=text.to_owned())
} else {
Json::number(value)
}
}
///|
/// A number the grammar admits but a double cannot hold.
///
/// RFC 8259 §6 sets no limit on a number's magnitude and expects an
/// implementation to say what it does at the edges. This one does what every
/// JavaScript engine does: too large becomes an infinity, too small becomes
/// zero. The text is kept either way, so writing the document back out loses
/// nothing.
fn out_of_range(text : StringView) -> Double {
let negative = text.at(0).to_int() == 0x2D
let mut huge = true
for i in 0.. Json raise Malformed {
self.i += 2
let from = self.i
let mut value = 0.0
while nibble(self.here()) >= 0 {
value = value * 16.0 + nibble(self.here()).to_double()
self.i += 1
}
if self.i == from {
raise BadNumber(self.spot(start))
}
Json::number(if signed { -value } else { value })
}
///|
/// What a lone half of a surrogate pair becomes when the dialect tolerates one:
/// U+FFFD, the replacement character, which is what Go's reader substitutes.
let replacement : Char = '\u{FFFD}'