// M2 engine (continued) — structure state machine, token scanners, finish and
// snapshot assembly. Same package as engine.mbt.
///|
/// Whether `u` may start a value.
fn is_value_start_char(u : UInt16) -> Bool {
let n = u.to_int()
n == 0x7B || n == 0x5B || is_quote_open(u) || is_ident_char(u)
}
///|
/// Whether the token that starts with `u` could also continue the string
/// literal that ended just before it. When it could, a comma inserted between
/// the two admits two readings (R006, FR-27).
fn can_extend_string(u : UInt16) -> Bool {
is_quote_open(u) || (is_ident_char(u) && !is_number_start(u))
}
///|
/// Dispatch on the document phase.
fn Engine::step_idle(
self : Engine,
u : UInt16,
p : Position,
) -> Unit raise MendError {
match self.phase {
PPrelude => self.step_prelude(u, p)
PBody => self.step_body(u, p)
PEpilogue => self.step_epilogue(u, p)
PFinished | PFailed =>
raise InvalidState("no input may be processed after finish")
}
}
///|
/// Prelude: skip whitespace and BOM, optionally strip one code fence and
/// optionally skip prose before the first `{` or `[` (D08 8.4).
fn Engine::step_prelude(
self : Engine,
u : UInt16,
p : Position,
) -> Unit raise MendError {
if is_ws(u) {
return
}
if !self.saw_content {
self.saw_content = true
self.nojson_pos = p
}
if self.opts.skip_prose {
let n = u.to_int()
if n == 0x7B || n == 0x5B {
if self.prose_skipped > 0 {
self.report(SkipProse, self.prose_start)
}
self.phase = PBody
self.step_body(u, p)
return
}
if self.prose_skipped == 0 {
self.prose_start = p
}
self.prose_skipped = self.prose_skipped + 1
return
}
if self.opts.strip_fence && u.to_int() == 0x60 {
self.tok = TFence(1)
self.fence_start = p
self.fence_body = true
return
}
self.phase = PBody
self.step_body(u, p)
}
///|
/// Trailing-junk policy after the first root value (D08 8.5).
fn Engine::epilogue_drop(self : Engine, p : Position) -> Unit raise MendError {
match self.opts.multi_root {
KeepFirst =>
if !self.epilogue_reported {
self.report(DropUnexpected, p, ambiguity=TrailingContent)
self.epilogue_reported = true
}
AsError =>
raise StrictViolation(
DropUnexpected,
p,
RepairCode::DropUnexpected.message(),
)
AsArray => ()
}
}
///|
/// Epilogue: whitespace, code fence, extra closers, otherwise the multi-root
/// policy decides.
fn Engine::step_epilogue(
self : Engine,
u : UInt16,
p : Position,
) -> Unit raise MendError {
if is_ws(u) {
return
}
if self.epilogue_reported {
return
}
let n = u.to_int()
if self.opts.strip_fence && n == 0x60 {
self.tok = TFence(1)
self.fence_start = p
self.fence_body = false
return
}
if n == 0x2F {
self.tok = TSlash
self.tok_start = p
return
}
if n == 0x2B && self.can_concat() {
self.concat_pending = true
self.concat_pos = p
return
}
if self.concat_pending && is_quote_open(u) {
self.start_string(u, p, false)
return
}
if self.opts.multi_root is AsError {
self.epilogue_drop(p)
return
}
if n == 0x7D || n == 0x5D {
self.report(DropExtraCloser, p)
return
}
self.epilogue_drop(p)
}
///|
/// Start whatever value `u` begins.
fn Engine::start_value_char(
self : Engine,
u : UInt16,
p : Position,
) -> Unit raise MendError {
let n = u.to_int()
if n == 0x7B {
self.enter(FObj, p)
} else if n == 0x5B {
self.enter(FArr, p)
} else if is_quote_open(u) {
self.start_string(u, p, false)
} else {
self.start_bare(u, p, BareValue)
}
}
///|
/// Start whatever key `u` begins.
fn Engine::start_key_char(
self : Engine,
u : UInt16,
p : Position,
) -> Unit raise MendError {
if is_quote_open(u) {
self.start_string(u, p, true)
} else {
self.start_bare(u, p, BareKey)
}
}
///|
/// Push a container frame, enforcing `max_depth` (E020). When delta tracking is
/// on, the new container is announced at the path of the slot it occupies
/// (FR-18).
fn Engine::enter(
self : Engine,
kind : FrameKind,
p : Position,
) -> Unit raise MendError {
if self.stack.length() >= self.opts.max_depth {
raise DepthLimit(self.opts.max_depth, p.offset)
}
let track = self.opts.collect_deltas
let path = if track { self.slot_path() } else { [] }
let f = new_frame(kind, p, false)
if track {
f.path = path
f.announced = true
}
self.stack.push(f)
if track {
self.delta_enter(path, kind)
}
}
///|
/// Body dispatch: root, array and object positions.
fn Engine::step_body(
self : Engine,
u : UInt16,
p : Position,
) -> Unit raise MendError {
if is_ws(u) {
return
}
let n = u.to_int()
if self.concat_pending && !is_quote_open(u) {
self.concat_pending = false
self.report(DropUnexpected, self.concat_pos)
}
if self.stack.length() == 0 {
if n == 0x7B {
self.enter(FObj, p)
return
}
if n == 0x5B {
self.enter(FArr, p)
return
}
if n == 0x2F {
self.tok = TSlash
self.tok_start = p
return
}
if is_quote_open(u) {
self.start_string(u, p, false)
return
}
if is_ident_char(u) {
self.start_bare(u, p, BareValue)
return
}
// Anything else at a still-empty root (`}`, `]`, `@`, a fence, prose …)
// means the document carries no JSON at all: E002 at the first
// non-whitespace code unit (G-113, G-250, G-290).
raise NoJsonFound(self.nojson_pos.offset)
}
let idx = self.stack.length() - 1
let kind = self.stack[idx].kind
let ctx = self.stack[idx].ctx
match kind {
FArr => self.step_in_array(idx, ctx, u, p)
FObj => self.step_in_object(idx, ctx, u, p)
}
}
///|
/// Structure transitions inside an array.
fn Engine::step_in_array(
self : Engine,
idx : Int32,
ctx : Ctx,
u : UInt16,
p : Position,
) -> Unit raise MendError {
let n = u.to_int()
if n == 0x2F {
self.tok = TSlash
self.tok_start = p
return
}
match ctx {
CArrValueOrEnd => {
if n == 0x5D {
self.close_container(p, false)
return
}
if n == 0x7D {
self.close_container(p, true)
return
}
if n == 0x2C {
self.report(RemoveExtraComma, p)
return
}
if is_value_start_char(u) {
self.start_value_char(u, p)
return
}
self.report(DropUnexpected, p)
}
CArrCommaOrEnd => {
if n == 0x2B && self.can_concat() {
self.concat_pending = true
self.concat_pos = p
return
}
if n == 0x2C {
self.stack[idx].ctx = CArrValueOrEnd
self.stack[idx].after_comma = true
self.stack[idx].last_comma = p
return
}
if n == 0x5D {
self.close_container(p, false)
return
}
if n == 0x7D {
self.close_container(p, true)
return
}
if is_value_start_char(u) {
if !self.stack[idx].synthetic && !self.concat_pending {
// `["a" "b"]` is the case §5.2 of the product document admits: the
// comma is a notation fix, but the structure is a guess.
let items = self.stack[idx].items
let prev_string = items.length() > 0 &&
items[items.length() - 1] is Json::String(_)
let amb : Ambiguity = if prev_string && can_extend_string(u) {
AdjacentStrings
} else {
Unambiguous
}
self.report(InsertComma, p, ambiguity=amb)
}
self.start_value_char(u, p)
return
}
self.report(DropUnexpected, p)
}
_ => self.report(DropUnexpected, p)
}
}
///|
/// Structure transitions inside an object.
fn Engine::step_in_object(
self : Engine,
idx : Int32,
ctx : Ctx,
u : UInt16,
p : Position,
) -> Unit raise MendError {
let n = u.to_int()
if n == 0x2F {
self.tok = TSlash
self.tok_start = p
return
}
match ctx {
CObjKeyOrEnd => {
if is_quote_open(u) {
self.start_string(u, p, true)
return
}
if is_ident_char(u) {
self.start_bare(u, p, BareKey)
return
}
if n == 0x7D {
self.close_container(p, false)
return
}
if n == 0x5D {
self.close_container(p, true)
return
}
if n == 0x2C {
self.report(RemoveExtraComma, p)
return
}
self.report(DropUnexpected, p)
}
CObjColon => {
if n == 0x3A {
self.stack[idx].ctx = CObjValue
return
}
if n == 0x7D || n == 0x5D || n == 0x2C {
self.fill_null_and_replay(idx, p, u)
return
}
if is_value_start_char(u) {
self.report(InsertColon, p)
self.start_value_char(u, p)
return
}
self.report(DropUnexpected, p)
}
CObjValue => {
if is_value_start_char(u) {
self.start_value_char(u, p)
return
}
if n == 0x7D || n == 0x5D || n == 0x2C {
self.fill_null_and_replay(idx, p, u)
return
}
self.report(DropUnexpected, p)
}
CObjCommaOrEnd => {
if n == 0x2B && self.can_concat() {
self.concat_pending = true
self.concat_pos = p
return
}
if n == 0x2C {
self.stack[idx].ctx = CObjKeyOrEnd
self.stack[idx].after_comma = true
self.stack[idx].last_comma = p
return
}
if n == 0x7D {
self.close_container(p, false)
return
}
if n == 0x5D {
self.close_container(p, true)
return
}
if is_quote_open(u) || is_ident_char(u) {
if self.concat_pending {
self.start_value_char(u, p)
return
}
self.report(InsertComma, p)
self.start_key_char(u, p)
return
}
self.report(DropUnexpected, p)
}
_ => self.report(DropUnexpected, p)
}
}
///|
/// Fill `null` for a key that never got a value (R008), then replay `u`.
fn Engine::fill_null_and_replay(
self : Engine,
idx : Int32,
p : Position,
u : UInt16,
) -> Unit raise MendError {
self.report(FillNull, p)
match self.stack[idx].last_key {
Some(k) => self.assign(idx, k, Json::null())
None => ()
}
self.stack[idx].last_key = None
self.stack[idx].ctx = CObjCommaOrEnd
self.step_body(u, p)
}
///|
/// Close the top container normally, reporting a mismatched closer and a
/// trailing comma when present.
fn Engine::close_container(
self : Engine,
p : Position,
mismatch : Bool,
) -> Unit raise MendError {
if mismatch {
self.report(MismatchedCloser, p)
}
let idx = self.stack.length() - 1
if self.stack[idx].after_comma {
self.report(RemoveExtraComma, self.stack[idx].last_comma)
}
let frame = self.stack.pop().unwrap()
let v = match frame.kind {
FObj => Json::object(frame.map)
FArr => Json::array(frame.items)
}
self.value_done(v)
}
///|
/// Whether the value delivered just before a `+` can absorb a string (BR-21).
fn Engine::can_concat(self : Engine) -> Bool {
if self.stack.length() == 0 {
match self.root {
Some(Json::String(_)) => true
_ => false
}
} else {
let f = self.stack[self.stack.length() - 1]
match f.kind {
FArr =>
if f.items.length() == 0 {
false
} else {
match f.items[f.items.length() - 1] {
Json::String(_) => true
_ => false
}
}
FObj =>
match f.last_assigned_key {
Some(k) =>
match f.map.get(k) {
Some(Json::String(_)) => true
_ => false
}
None => false
}
}
}
}
///|
/// Append `extra` to the previously delivered string value (R019).
fn Engine::concat_last(self : Engine, extra : String) -> Bool {
if self.stack.length() == 0 {
match self.root {
Some(Json::String(prev)) => {
self.root = Some(Json::string(prev + extra))
true
}
_ => false
}
} else {
let f = self.stack[self.stack.length() - 1]
match f.kind {
FArr => {
let n = f.items.length()
if n == 0 {
false
} else {
match f.items[n - 1] {
Json::String(prev) => {
f.items[n - 1] = Json::string(prev + extra)
true
}
_ => false
}
}
}
FObj =>
match f.last_assigned_key {
Some(k) =>
match f.map.get(k) {
Some(Json::String(prev)) => {
f.map[k] = Json::string(prev + extra)
true
}
_ => false
}
None => false
}
}
}
}
///|
/// Attach a completed value to the enclosing container, or finish the
/// document when there is none. A pending `+` first tries to merge a string
/// into the value delivered just before it (BR-21 / R019).
fn Engine::value_done(self : Engine, v : Json) -> Unit raise MendError {
if self.concat_pending {
self.concat_pending = false
match v {
Json::String(s) =>
if self.concat_last(s) {
self.report(ConcatStrings, self.concat_pos, ambiguity=ConcatOperator)
if self.opts.collect_deltas {
self.push_delta(AppendText, self.concat_target_path(), None, s)
}
return
}
_ => ()
}
self.report(DropUnexpected, self.concat_pos)
}
if self.stack.length() == 0 {
match self.root {
Some(_old) => ()
None => {
self.root = Some(v)
self.phase = PEpilogue
if self.opts.collect_deltas {
self.delta_land([], v)
}
}
}
return
}
let idx = self.stack.length() - 1
self.stack[idx].dirty = true
let track = self.opts.collect_deltas
let path = if track { self.child_path(idx) } else { [] }
if self.stack[idx].synthetic {
self.stack[idx].items.push(v)
self.stack[idx].ctx = CArrCommaOrEnd
if track {
self.delta_land(path, v)
}
return
}
match self.stack[idx].kind {
FArr => {
self.stack[idx].items.push(v)
self.stack[idx].ctx = CArrCommaOrEnd
self.stack[idx].after_comma = false
if track {
self.delta_land(path, v)
}
}
FObj =>
match self.stack[idx].last_key {
Some(k) => {
self.assign(idx, k, v)
self.stack[idx].ctx = CObjCommaOrEnd
self.stack[idx].after_comma = false
self.stack[idx].last_key = None
self.stack[idx].last_assigned_key = Some(k)
}
None => self.stack[idx].ctx = CObjCommaOrEnd
}
}
}
///|
/// Insert a value under `k`, applying the duplicate-key policy (BR-19).
fn Engine::assign(
self : Engine,
idx : Int32,
k : String,
v : Json,
) -> Unit raise MendError {
let track = self.opts.collect_deltas
if self.stack[idx].map.contains(k) {
// Merging two duplicates only loses information when the two values differ.
let kept = self.stack[idx].map[k]
let sev = if json_equal(kept, v) { Info } else { Warning }
match self.opts.duplicate_keys {
KeepLast => {
self.report(
MergeDuplicateKey,
self.stack[idx].key_pos,
severity=Some(sev),
)
self.stack[idx].map[k] = v
if track {
self.delta_land(self.child_path(idx), v)
}
}
KeepFirst =>
self.report(
MergeDuplicateKey,
self.stack[idx].key_pos,
severity=Some(sev),
)
AsError =>
raise StrictViolation(
MergeDuplicateKey,
self.stack[idx].key_pos,
RepairCode::MergeDuplicateKey.message(),
)
}
} else {
self.stack[idx].map[k] = v
if track {
self.delta_land(self.child_path(idx), v)
}
}
}
///|
/// Mark the current value slot as complete without producing a value (used
/// when an ellipsis placeholder is dropped).
fn Engine::mark_value_done_no_value(self : Engine) -> Unit {
if self.stack.length() == 0 {
return
}
let idx = self.stack.length() - 1
match self.stack[idx].kind {
FArr => self.stack[idx].ctx = CArrCommaOrEnd
FObj => {
self.stack[idx].ctx = CObjCommaOrEnd
self.stack[idx].last_key = None
}
}
self.stack[idx].after_comma = false
}
///|
/// Open a string token.
fn Engine::start_string(
self : Engine,
u : UInt16,
p : Position,
is_key : Bool,
) -> Unit raise MendError {
if u.to_int() != 0x22 {
self.report(NormalizeQuote, p)
}
self.tok = TStr(str_kind_of(u), is_key)
self.tok_start = p
self.parts.clear()
self.parts_len = 0
self.esc = 0
self.pending_high = 0
self.delta_str_reported = 0
if self.opts.collect_deltas {
self.delta_open_string(is_key)
}
}
///|
/// Append one code unit of string content, enforcing `max_token_len`.
fn Engine::push_cu(
self : Engine,
u : UInt16,
p : Position,
) -> Unit raise MendError {
if is_high_surrogate(u) {
if self.pending_high != 0 {
self.flush_lone_surrogate()
}
self.pending_high = u.to_int()
self.pending_high_pos = p
return
}
if is_low_surrogate(u) {
if self.pending_high != 0 {
let hi = self.pending_high
self.pending_high = 0
self.push_code_point(
0x10000 + ((hi - 0xD800) << 10) + (u.to_int() - 0xDC00),
)
} else {
self.report(FixEscape, p)
self.push_code_point(0xFFFD)
}
return
}
self.push_plain(u.to_int())
}
///|
/// Append one code unit produced by a `\uXXXX` escape. Diagnostics point at
/// the backslash that opened the escape (G-224).
fn Engine::push_escaped_cu(
self : Engine,
v : Int32,
p : Position,
) -> Unit raise MendError {
if v >= 0xD800 && v <= 0xDBFF {
if self.pending_high != 0 {
self.flush_lone_surrogate()
}
self.pending_high = v
self.pending_high_pos = p
return
}
if v >= 0xDC00 && v <= 0xDFFF {
if self.pending_high != 0 {
let hi = self.pending_high
self.pending_high = 0
self.push_code_point(0x10000 + ((hi - 0xD800) << 10) + (v - 0xDC00))
} else {
self.report(FixEscape, p)
self.push_code_point(0xFFFD)
}
return
}
self.push_plain(v)
}
///|
/// Append a scalar value.
fn Engine::push_code_point(self : Engine, cp : Int32) -> Unit raise MendError {
match cp.to_char() {
Some(c) => self.push_char(c)
None => ()
}
}
///|
/// Append an ASCII code unit.
fn Engine::push_char_code(self : Engine, code : Int32) -> Unit raise MendError {
match code.to_char() {
Some(c) => self.push_char(c)
None => ()
}
}
///|
/// Append plain (non-surrogate) content, flushing a pending half pair first.
fn Engine::push_plain(self : Engine, code : Int32) -> Unit raise MendError {
if self.pending_high != 0 {
self.flush_lone_surrogate()
}
self.push_char_code(code)
}
///|
/// Append a character and check the token length limit (E022).
fn Engine::push_char(self : Engine, c : Char) -> Unit raise MendError {
self.parts.push(c.to_string())
self.parts_len = self.parts_len + c.utf16_len()
if self.parts_len > self.opts.max_token_len {
raise TokenLimit(self.opts.max_token_len, self.tok_start.offset)
}
}
///|
/// A half surrogate pair never completed: replace it with U+FFFD (R014).
fn Engine::flush_lone_surrogate(self : Engine) -> Unit raise MendError {
let p = self.pending_high_pos
self.pending_high = 0
self.report(FixEscape, p)
self.push_code_point(0xFFFD)
}
///|
/// String scanning (D08 8.6).
fn Engine::step_str(
self : Engine,
u : UInt16,
p : Position,
kind : StrKind,
is_key : Bool,
) -> Unit raise MendError {
if self.esc == 1 {
self.esc = 0
let n = u.to_int()
if n == 0x75 {
self.esc = 2
self.esc_val = 0
self.esc_digits = 0
return
}
if n == 0x62 {
self.push_plain(0x08)
return
}
if n == 0x66 {
self.push_plain(0x0C)
return
}
if n == 0x6E {
self.push_plain(0x0A)
return
}
if n == 0x72 {
self.push_plain(0x0D)
return
}
if n == 0x74 {
self.push_plain(0x09)
return
}
if n == 0x22 {
self.push_plain(0x22)
return
}
if n == 0x2F {
self.push_plain(0x2F)
return
}
if n == 0x5C {
self.push_plain(0x5C)
return
}
self.report(FixEscape, self.esc_pos)
self.push_cu(u, p)
return
}
if self.esc >= 2 {
if is_hex_digit(u) {
self.esc_val = self.esc_val * 16 + hex_value(u)
self.esc_digits = self.esc_digits + 1
if self.esc_digits == 4 {
self.esc = 0
self.push_escaped_cu(self.esc_val, self.esc_pos)
}
return
}
self.esc = 0
self.report(FixEscape, self.esc_pos)
self.push_cu(u, p)
return
}
if closes_str(kind, u) {
if self.opts.quote_lookahead {
self.tok = TStrPending(kind, is_key)
self.pending_quote = u.to_int()
self.pending_quote_pos = p
self.pending_ws.reset()
return
}
// The closing quote is consumed by the string scanner; the next code unit
// is dispatched by the container state machine on the following step.
self.tok = TIdle
self.finish_string(is_key)
return
}
if u.to_int() == 0x5C {
self.esc = 1
self.esc_pos = p
return
}
if u.to_int() < 0x20 {
self.report(EscapeControl, p)
}
self.push_cu(u, p)
}
///|
/// Quote lookahead: a candidate closing quote is only confirmed when the next
/// non-whitespace character can follow a value (D08 8.6, FR-14).
fn Engine::step_str_pending(
self : Engine,
u : UInt16,
p : Position,
kind : StrKind,
is_key : Bool,
) -> Unit raise MendError {
if is_ws(u) {
match u.to_char() {
Some(c) => self.pending_ws.write_char(c)
None => ()
}
return
}
let n = u.to_int()
if n == 0x2C || n == 0x7D || n == 0x5D || n == 0x3A || n == 0x2F {
self.pending_ws.reset()
self.tok = TIdle
self.finish_string(is_key)
self.step_idle(u, p)
return
}
self.report(NormalizeQuote, self.pending_quote_pos)
match self.pending_quote.to_char() {
Some(q) => self.push_char(q)
None => ()
}
let ws = self.pending_ws.to_string()
self.pending_ws.reset()
if ws.length() > 0 {
self.parts.push(ws)
self.parts_len = self.parts_len + ws.length()
}
self.tok = TStr(kind, is_key)
self.step_str(u, p, kind, is_key)
}
///|
/// Complete the current string token, delivering it as a key or a value.
fn Engine::finish_string(self : Engine, is_key : Bool) -> Unit raise MendError {
if self.pending_high != 0 {
self.flush_lone_surrogate()
}
if self.esc != 0 {
self.report(FixEscape, self.esc_pos)
self.esc = 0
}
let text = self.parts_text()
self.parts.clear()
self.parts_len = 0
if is_key {
let idx = self.stack.length() - 1
self.stack[idx].last_key = Some(text)
self.stack[idx].key_pos = self.tok_start
self.stack[idx].ctx = CObjColon
if self.opts.collect_deltas {
self.delta_pending_key()
}
} else {
self.value_done(Json::string(text))
}
}
///|
/// Open a bare token.
fn Engine::start_bare(
self : Engine,
u : UInt16,
p : Position,
kind : BareKind,
) -> Unit raise MendError {
self.tok = TBare(kind)
self.tok_start = p
self.bare.reset()
self.bare_len = 0
self.push_bare_cu(u)
}
///|
/// Append a code unit to the bare token, enforcing `max_token_len`.
fn Engine::push_bare_cu(self : Engine, u : UInt16) -> Unit raise MendError {
match u.to_char() {
Some(c) => self.bare.write_char(c)
None => ()
}
self.bare_len = self.bare_len + 1
if self.bare_len > self.opts.max_token_len {
raise TokenLimit(self.opts.max_token_len, self.tok_start.offset)
}
}
///|
/// Bare token scanning.
fn Engine::step_bare(
self : Engine,
u : UInt16,
p : Position,
kind : BareKind,
) -> Unit raise MendError {
if is_ident_char(u) {
self.push_bare_cu(u)
return
}
self.tok = TIdle
self.classify_bare(kind)
self.step_idle(u, p)
}
///|
/// Classify a finished bare token (D08 8.7).
fn Engine::classify_bare(
self : Engine,
kind : BareKind,
) -> Unit raise MendError {
let text = self.bare.to_string()
let start = self.tok_start
self.bare.reset()
self.bare_len = 0
if text.length() == 0 {
return
}
if text == "..." {
self.report(DropEllipsis, start)
self.mark_value_done_no_value()
return
}
if kind is BareKey {
self.emit_bare_word(text, start, kind)
return
}
let lower = ascii_lower(text)
let lk = literal_kind(lower)
if !(lk is LNone) {
let canon = literal_canonical(lk)
let v = match lk {
LTrue => Json::boolean(true)
LFalse => Json::boolean(false)
LNull => Json::null()
LNone => Json::null()
}
// `cased` must mean "the source differs from the canonical spelling only
// by ASCII case", not "it has the same length": `None` and `none` are as
// long as `null` but are a different word, so they invent a value (R012).
self.emit_literal(v, text == canon, ascii_lower(text) == canon, start)
return
}
if is_non_finite(lower) {
self.emit_non_finite(text, start)
return
}
if is_number_start(text.at(0)) && is_number_shaped(text) {
match normalize_number(text) {
Some(txt) => {
if txt != text {
self.report(FixNumber, start)
}
self.value_done(build_number(txt))
}
None => self.emit_numberless(start)
}
return
}
self.emit_bare_word(text, start, kind)
}
///|
/// Literal or literal-prefix conversion (R012).
fn Engine::emit_literal(
self : Engine,
v : Json,
exact : Bool,
cased : Bool,
start : Position,
) -> Unit raise MendError {
if !exact {
// `True`/`FALSE` are re-cased and lose nothing (`NormalizeLiteral`, Info);
// `tru`/`nul` are completed and do invent a value, which stays a
// `Warning` under `ConvertLiteral` (BR-12).
if cased {
self.report(NormalizeLiteral, start)
} else {
self.report(ConvertLiteral, start)
}
}
self.value_done(v)
}
///|
/// Structural equality of two parsed values, used to decide whether merging a
/// duplicate key actually dropped information.
fn json_equal(a : Json, b : Json) -> Bool {
match (a, b) {
(Json::Null, Json::Null) => true
(Json::True, Json::True) => true
(Json::False, Json::False) => true
(Json::Number(x, ..), Json::Number(y, ..)) => x == y
(Json::String(x), Json::String(y)) => x == y
(Json::Array(x), Json::Array(y)) => {
let mut ok = x.length() == y.length()
let mut i = 0
while ok && i < x.length() {
if !json_equal(x[i], y[i]) {
ok = false
}
i = i + 1
}
ok
}
(Json::Object(x), Json::Object(y)) => {
let mut ok = x.length() == y.length()
if ok {
for k, v in x {
match y.get(k) {
Some(w) => if !json_equal(v, w) { ok = false }
None => ok = false
}
}
}
ok
}
_ => false
}
}
///|
/// `NaN` / `Infinity` handling (BR-12).
fn Engine::emit_non_finite(
self : Engine,
text : String,
start : Position,
) -> Unit raise MendError {
match self.opts.non_finite {
AsNull => {
self.report(ConvertLiteral, start)
self.value_done(Json::null())
}
AsString => {
self.report(ConvertLiteral, start)
self.value_done(Json::string(text))
}
AsError =>
raise StrictViolation(
ConvertLiteral,
start,
RepairCode::ConvertLiteral.message(),
)
}
}
///|
/// A number-shaped token with no usable prefix: `null` in an object, dropped
/// in an array, `E002` at the root (G-076, G-077, G-133).
fn Engine::emit_numberless(
self : Engine,
start : Position,
) -> Unit raise MendError {
if self.stack.length() == 0 {
raise NoJsonFound(start.offset)
}
let idx = self.stack.length() - 1
match self.stack[idx].kind {
FArr => {
self.report(FixNumber, start)
self.mark_value_done_no_value()
}
FObj => {
self.report(FillNull, start)
match self.stack[idx].last_key {
Some(k) => self.assign(idx, k, Json::null())
None => ()
}
self.stack[idx].last_key = None
self.stack[idx].ctx = CObjCommaOrEnd
}
}
}
///|
/// A word that is neither literal nor number: quoted as a key (R004, Info) or
/// as a string value (R004, Warning); `E002` at the root (BR-07).
fn Engine::emit_bare_word(
self : Engine,
text : String,
start : Position,
kind : BareKind,
) -> Unit raise MendError {
match kind {
BareKey => {
self.report(QuoteBareWord, start)
let idx = self.stack.length() - 1
self.stack[idx].last_key = Some(text)
self.stack[idx].key_pos = start
self.stack[idx].ctx = CObjColon
if self.opts.collect_deltas {
self.delta_pending_key()
}
}
BareValue => {
if self.stack.length() == 0 {
raise NoJsonFound(start.offset)
}
// A bare word in value position is a guess, not a notation fix: it is
// recorded as `Warning` so that `Exactness` becomes `Lossy` (FR-17).
self.report(QuoteBareWord, start, slot=ValueSlot, ambiguity=BareWord)
self.value_done(Json::string(text))
}
}
}
///|
/// `/` seen: decide between line comment, block comment and a dropped slash.
fn Engine::step_slash(
self : Engine,
u : UInt16,
p : Position,
) -> Unit raise MendError {
let n = u.to_int()
if n == 0x2F {
self.tok = TLineComment
self.report(StripComment, self.tok_start)
return
}
if n == 0x2A {
self.tok = TBlockComment
self.block_star = false
self.report(StripComment, self.tok_start)
return
}
self.tok = TIdle
self.report(DropUnexpected, self.tok_start)
self.step_idle(u, p)
}
///|
/// Block comment scanning, including the `*` / `/` pair.
fn Engine::step_block(self : Engine, u : UInt16, _p : Position) -> Unit {
let n = u.to_int()
if n == 0x2A {
self.block_star = true
return
}
if self.block_star && n == 0x2F {
self.block_star = false
self.tok = TIdle
return
}
self.block_star = false
}
///|
/// Code fence scanning: three backticks then the rest of the line.
fn Engine::step_fence(
self : Engine,
u : UInt16,
p : Position,
count : Int32,
) -> Unit raise MendError {
if u.to_int() == 0x60 {
if count + 1 >= 3 {
self.report(StripFence, self.fence_start)
self.tok = TFenceLine
} else {
self.tok = TFence(count + 1)
}
return
}
match self.phase {
PPrelude => raise NoJsonFound(self.fence_start.offset)
_ => {
self.tok = TIdle
self.epilogue_drop(p)
}
}
}
///|
/// End of a fence line.
fn Engine::finish_fence_line(self : Engine) -> Unit {
self.tok = TIdle
if self.fence_body {
self.fence_body = false
self.phase = PBody
}
}
///|
/// Flush the last unfinished token when the input ends (D08 8.11).
fn Engine::finalize_token(self : Engine) -> Unit raise MendError {
match self.tok {
TIdle => ()
TStr(_kind, is_key) => {
self.tok = TIdle
if self.pending_high != 0 {
self.flush_lone_surrogate()
}
if self.esc != 0 {
self.report(FixEscape, self.esc_pos)
self.esc = 0
}
self.report(CloseString, self.pos)
self.finish_string(is_key)
}
TStrPending(_kind, is_key) => {
self.tok = TIdle
self.finish_string(is_key)
}
TBare(kind) => {
self.tok = TIdle
self.classify_bare(kind)
}
TSlash => {
self.tok = TIdle
self.report(DropUnexpected, self.tok_start)
}
TLineComment | TBlockComment | TFence(_) | TFenceLine => self.tok = TIdle
}
}
///|
/// Close one still-open container at end of input (D08 8.11).
fn Engine::unwind_top(self : Engine) -> Bool raise MendError {
let idx = self.stack.length() - 1
let f = self.stack[idx]
if f.synthetic {
self.stack.pop() |> ignore
if f.dirty {
self.root = Some(Json::array(f.items))
self.phase = PEpilogue
if self.opts.collect_deltas && f.announced {
self.delta_land([], Json::array(f.items))
}
return true
}
return false
}
match f.kind {
FObj => {
if f.ctx is CObjColon || f.ctx is CObjValue {
match f.last_key {
Some(k) => {
self.report(FillNull, self.pos)
self.assign(idx, k, Json::null())
}
None => ()
}
} else if f.after_comma {
self.report(RemoveExtraComma, f.last_comma)
}
self.report(CloseContainer, self.pos)
self.stack.pop() |> ignore
self.value_done(Json::object(f.map))
true
}
FArr => {
if f.after_comma {
self.report(RemoveExtraComma, f.last_comma)
}
self.report(CloseContainer, self.pos)
self.stack.pop() |> ignore
self.value_done(Json::array(f.items))
true
}
}
}
///|
/// Finish the document: flush the token, close open containers, produce text.
fn Engine::finish_engine(self : Engine) -> FinalResult raise MendError {
match self.phase {
PFinished | PFailed =>
raise InvalidState("finish called on a finished parser")
_ => ()
}
if self.utf8_need != 0 {
self.utf8_need = 0
// The sequence was truncated by the end of the input: point at where it
// started, in absolute byte coordinates (BR-22).
raise InvalidUtf8(self.utf8_start)
}
self.snap_stamp = self.snap_stamp + 1
if self.concat_pending {
self.concat_pending = false
self.report(DropUnexpected, self.concat_pos)
}
self.finalize_token()
let mut unwound = false
while self.stack.length() > 0 {
if self.unwind_top() {
unwound = true
}
}
match self.root {
Some(value) => {
self.phase = PFinished
let text = if self.opts.want_text {
render(value, self.opts.indent)
} else {
""
}
{
text,
value,
repairs: self.reps,
completeness: Complete,
repairs_truncated: self.repairs_truncated,
exactness: self.exactness,
ambiguous: self.ambiguous,
}
}
None => {
self.phase = PFailed
if !self.saw_content && !unwound {
raise EmptyInput
}
raise NoJsonFound(self.nojson_pos.offset)
}
}
}
///|
/// Current string content as text.
fn Engine::parts_text(self : Engine) -> String {
let sb = StringBuilder()
for part in self.parts {
sb.write_string(part)
}
sb.to_string()
}
///|
/// Value of the unfinished scalar token, if it may appear in a snapshot.
fn Engine::pending_element(self : Engine) -> Json? {
match self.tok {
TStr(_kind, is_key) =>
if is_key {
None
} else {
Some(Json::string(self.parts_text()))
}
TStrPending(_kind, is_key) =>
if is_key {
None
} else {
Some(Json::string(self.parts_text()))
}
TBare(kind) =>
match kind {
BareKey => None
BareValue => self.pending_bare()
}
_ => None
}
}
///|
/// Whether an unfinished bare token can be shown as a number (FR-11).
fn Engine::pending_bare(self : Engine) -> Json? {
if self.opts.incomplete_number is Drop {
return None
}
let text = self.bare.to_string()
if text.length() == 0 {
return None
}
let lower = ascii_lower(text)
if !(literal_kind(lower) is LNone) || is_non_finite(lower) {
return None
}
if !is_number_shaped(text) {
return None
}
match normalize_number(text) {
Some(txt) => Some(build_number(txt))
None => None
}
}
///|
/// Partial value of the frame at `i`, with `elem` standing in for the value
/// currently being built inside it.
fn Engine::partial_frame(self : Engine, i : Int32, elem : Json?) -> Json {
let f = self.stack[i]
match f.kind {
FArr => {
let items = f.items.copy()
match elem {
Some(v) => items.push(v)
None => ()
}
Json::array(items)
}
FObj => {
let map = f.map.copy()
match f.last_key {
Some(k) =>
if f.ctx is CObjColon || f.ctx is CObjValue {
match elem {
Some(v) => map[k] = v
None =>
if self.opts.pending_key is NullKey {
map[k] = Json::null()
}
}
}
None => ()
}
Json::object(map)
}
}
}
///|
/// Assemble the partial value from the container stack.
fn Engine::snapshot_value(self : Engine) -> Json? {
if self.stack.length() == 0 {
match self.root {
Some(v) => Some(v)
None => self.pending_element()
}
} else {
let n = self.stack.length()
let mut acc : Json? = None
let mut i = n - 1
while i >= 0 {
let elem = if i == n - 1 { self.pending_element() } else { acc }
acc = Some(self.partial_frame(i, elem))
i = i - 1
}
acc
}
}
///|
/// Snapshot completeness (FR-09).
fn Engine::completeness(self : Engine) -> Completeness {
if self.stack.length() > 0 {
return Partial
}
match self.root {
Some(_v) => Complete
None =>
match self.pending_element() {
Some(_v) => Partial
None => Empty
}
}
}
///|
/// Path of the value currently being built.
fn Engine::open_path(self : Engine) -> Array[PathSegment] {
let out : Array[PathSegment] = []
for i = 0; i < self.stack.length(); i = i + 1 {
let f = self.stack[i]
if f.synthetic {
continue
}
match f.kind {
FObj =>
match f.last_key {
Some(k) => out.push(Key(k))
None => ()
}
FArr => out.push(Index(f.items.length()))
}
}
out
}
///|
/// Take a snapshot without mutating engine state (FR-10).
fn Engine::snapshot(self : Engine) -> Snapshot {
// P3-8: snapshots taken without any new input reuse the previous result
// instead of rebuilding the whole value. Values handed out this way are
// shared, so callers must treat snapshot values as immutable.
if self.snap_cache is Some(s) && self.snap_cache_stamp == self.snap_stamp {
return s
}
let s : Snapshot = {
value: self.snapshot_value(),
completeness: self.completeness(),
open_path: self.open_path(),
exactness: self.exactness,
ambiguous: self.ambiguous,
}
self.snap_cache = Some(s)
self.snap_cache_stamp = self.snap_stamp
s
}