// M2 engine — one incremental engine behind both one-shot repair and streaming
// partial parsing (ADR-001, ADR-007).
//
// The engine is a pushdown automaton over UTF-16 code units: an explicit
// container stack plus a token state, no recursion and no regular expressions.
// Every mutation of the input is recorded as an `R` repair; fatal conditions
// raise `MendError` (`E` codes).
///|
/// Container kind of an open frame.
priv enum FrameKind {
FObj
FArr
}
///|
/// Grammar context inside an open container (D08 8.2).
priv enum Ctx {
CObjKeyOrEnd
CObjColon
CObjValue
CObjCommaOrEnd
CArrValueOrEnd
CArrCommaOrEnd
}
///|
/// One open container.
priv struct Frame {
kind : FrameKind
mut ctx : Ctx
items : Array[Json]
map : Map[String, Json]
mut last_key : String?
mut last_assigned_key : String?
mut key_pos : Position
mut after_comma : Bool
mut last_comma : Position
synthetic : Bool
mut dirty : Bool
/// Full delta path of this container (FR-18); empty unless delta tracking is
/// on, in which case it is the path at which the container was announced.
mut path : Array[PathSegment]
/// Whether a `Set` delta was emitted for this container.
mut announced : Bool
}
///|
/// Whether a bare token is in key or value position.
priv enum BareKind {
BareKey
BareValue
}
///|
/// Current unfinished token.
priv enum Tok {
TIdle
TStr(StrKind, Bool)
TStrPending(StrKind, Bool)
TBare(BareKind)
TSlash
TLineComment
TBlockComment
TFence(Int32)
TFenceLine
}
///|
/// Document lifecycle.
priv enum Phase {
PPrelude
PBody
PEpilogue
PFinished
PFailed
}
///|
/// Literal classification of a bare word.
priv enum LitKind {
LNone
LTrue
LFalse
LNull
}
///|
/// The incremental engine.
priv struct Engine {
opts : Options
mut phase : Phase
stack : Array[Frame]
parts : Array[String]
mut parts_len : Int32
mut esc : Int32
mut esc_val : Int32
mut esc_digits : Int32
mut esc_pos : Position
mut pending_high : Int32
mut pending_high_pos : Position
bare : StringBuilder
mut bare_len : Int32
pending_ws : StringBuilder
mut pending_quote : Int32
mut pending_quote_pos : Position
mut fence_start : Position
mut fence_body : Bool
mut pos : Position
mut tok : Tok
mut tok_start : Position
mut root : Json?
reps : Array[Repair]
mut repairs_truncated : Bool
mut concat_pending : Bool
mut concat_pos : Position
mut snap_stamp : Int32
mut snap_cache : Snapshot?
mut snap_cache_stamp : Int32
mut prose_start : Position
mut prose_skipped : Int32
mut saw_content : Bool
mut nojson_pos : Position
mut epilogue_reported : Bool
mut block_star : Bool
mut utf8_need : Int32
mut utf8_cp : Int32
mut utf8_min : Int32
mut utf8_start : Int32
mut fed_bytes : Int32
/// Worst exactness seen so far (FR-17), updated even when records are not
/// collected.
mut exactness : Exactness
/// Whether an ambiguous repair was seen (FR-27), updated even when records
/// are not collected.
mut ambiguous : Bool
/// Deltas recorded since the last `drain_deltas` (FR-18).
mut deltas : Array[Delta]
/// Code units of the string being scanned already reported as `AppendText`.
mut delta_str_reported : Int32
}
///|
/// Advance a position by one code unit. Only LF starts a new line (BR-20).
fn advance(p : Position, u : UInt16) -> Position {
if u.to_int() == 0x0A {
{ offset: p.offset + 1, line: p.line + 1, column: 1, }
} else {
{ offset: p.offset + 1, line: p.line, column: p.column + 1, }
}
}
///|
/// Position at the very start of the input.
fn pos0() -> Position {
{ offset: 0, line: 1, column: 1, }
}
///|
/// A fresh frame.
fn new_frame(kind : FrameKind, start : Position, synthetic : Bool) -> Frame {
{
kind,
ctx: if kind is FObj {
CObjKeyOrEnd
} else {
CArrValueOrEnd
},
items: [],
map: Map([]),
last_key: None,
last_assigned_key: None,
key_pos: start,
after_comma: false,
last_comma: start,
synthetic,
dirty: false,
path: [],
announced: false,
}
}
///|
/// Whether `u` is the first code unit of a number.
fn is_number_start(u : UInt16) -> Bool {
let n = u.to_int()
n == 0x2B || n == 0x2D || n == 0x2E || (n >= 0x30 && n <= 0x39)
}
///|
/// Build a JSON number, keeping the source text verbatim as `repr` so that
/// integers beyond `Double` precision survive (ADR-004).
fn build_number(text : String) -> Json {
let d = @string.parse_double(text) catch { _ => 0.0 }
Json::number(d, repr=text)
}
///|
/// Literal classification, accepting case variants and Python / JavaScript
/// spellings (`None`, `undefined`) as R012 material (D08 8.7).
fn literal_kind(lower : String) -> LitKind {
let n = lower.length()
if n == 0 || n > 9 {
return LNone
}
if lower == "true" || "true".has_prefix(lower) {
return LTrue
}
if lower == "false" || "false".has_prefix(lower) {
return LFalse
}
if lower == "null" ||
lower == "none" ||
lower == "undefined" ||
"null".has_prefix(lower) ||
"none".has_prefix(lower) ||
"undefined".has_prefix(lower) {
return LNull
}
LNone
}
///|
/// Canonical spelling of a literal.
fn literal_canonical(k : LitKind) -> String {
match k {
LTrue => "true"
LFalse => "false"
LNull => "null"
LNone => ""
}
}
///|
/// Non-finite literal spellings (BR-12).
fn is_non_finite(lower : String) -> Bool {
lower == "nan" || lower == "infinity" || lower == "-infinity"
}
///|
/// Serialise a value, honouring the indent option.
fn render(v : Json, indent : Int32) -> String {
if indent > 0 {
v.stringify(indent~)
} else {
v.stringify()
}
}
///|
/// Create an engine for the given options.
fn Engine::make(opts : Options) -> Engine {
let start = pos0()
let e : Engine = {
opts,
phase: PPrelude,
stack: [],
parts: [],
parts_len: 0,
esc: 0,
esc_val: 0,
esc_digits: 0,
esc_pos: start,
pending_high: 0,
pending_high_pos: start,
bare: StringBuilder(),
bare_len: 0,
pending_ws: StringBuilder(),
pending_quote: 0,
pending_quote_pos: start,
fence_start: start,
fence_body: false,
pos: start,
tok: TIdle,
tok_start: start,
root: None,
reps: [],
repairs_truncated: false,
concat_pending: false,
concat_pos: pos0(),
snap_stamp: 0,
snap_cache: None,
snap_cache_stamp: -1,
prose_start: start,
prose_skipped: 0,
saw_content: false,
nojson_pos: start,
epilogue_reported: false,
block_star: false,
utf8_need: 0,
utf8_cp: 0,
utf8_min: 0,
utf8_start: 0,
fed_bytes: 0,
exactness: Exact,
ambiguous: false,
deltas: [],
delta_str_reported: 0,
}
if opts.multi_root is AsArray {
let f = new_frame(FArr, start, true)
if opts.collect_deltas {
f.path = []
f.announced = true
e.push_delta(Set, [], Some(Json::array([])), "")
}
e.stack.push(f)
}
e
}
///|
/// Record a repair, or raise `E010` when the rule is disabled, the parser runs
/// in strict mode, or the repair would make the result riskier than
/// `Options::max_loss` (D09 9.5, FR-17).
fn Engine::report(
self : Engine,
code : RepairCode,
p : Position,
slot~ : Slot = KeySlot,
severity~ : Severity? = None,
ambiguity~ : Ambiguity = Unambiguous,
) -> Unit raise MendError {
// A few codes are only notation-level in some situations: quoting a key,
// re-casing a literal, merging two identical duplicate keys. Those sites
// pass an explicit severity; everything else derives it from the code.
let severity = match severity {
Some(s) => s
None => code.severity_in(slot)
}
if self.opts.mode is Strict || !self.opts.rules.is_enabled(code) {
raise StrictViolation(code, p, code.message())
}
// The ambiguity gate comes before the exactness gate: when both apply, the
// ambiguity is the more specific diagnosis and the one a retry can act on.
if ambiguity.is_ambiguous() && self.opts.reject_ambiguous {
raise AmbiguousRepair(code, p, ambiguity)
}
let level = if severity is Warning { Lossy } else { Cosmetic }
if !level.within(self.opts.max_loss) {
raise StrictViolation(
code,
p,
"exactness " +
level.name() +
" exceeds max_loss " +
self.opts.max_loss.name(),
)
}
if level.rank() > self.exactness.rank() {
self.exactness = level
}
if ambiguity.is_ambiguous() {
self.ambiguous = true
}
if self.opts.collect_repairs {
if self.reps.length() < self.opts.max_repairs {
self.reps.push({
code,
position: p,
severity,
message: code.message(),
ambiguity,
})
} else {
self.repairs_truncated = true
}
}
}
///|
/// Feed a whole string chunk.
fn Engine::feed_str(self : Engine, s : String) -> Unit raise MendError {
match self.phase {
PFinished | PFailed => raise InvalidState("cannot feed a finished parser")
_ => ()
}
let n = s.length()
let mut i = 0
while i < n {
let mut batched = false
// Fast path (P3-9): inside an open string, a run of ordinary characters is
// appended in one piece and the position advances in one step. Only code
// units that `step_str` would pass straight to `push_cu` qualify, so the
// observable behaviour is identical.
match self.tok {
TStr(kind, _is_key) =>
if self.esc == 0 && self.pending_high == 0 {
let mut j = i
while j < n && is_plain_str_cu(s.at(j), kind) {
j = j + 1
}
// never consume code units beyond max_input_len (E021)
while j > i && self.pos.offset + (j - i) > self.opts.max_input_len {
j = j - 1
}
if j > i {
self.push_run(s, i, j)
i = j
batched = true
}
}
_ => ()
}
if !batched {
self.step(s.at(i))
i = i + 1
}
}
}
///|
/// Whether a code unit is plain string content for the given quote style:
/// it cannot close the string, cannot start an escape, is not a control
/// character and is not a surrogate (those need the pair logic).
fn is_plain_str_cu(u : UInt16, kind : StrKind) -> Bool {
let n = u.to_int()
n >= 0x20 && n != 0x5C && !closes_str(kind, u) && (n < 0xD800 || n > 0xDFFF)
}
///|
/// Append a run of plain string content as a single part (P3-9). The run
/// contains no line break, so the column advances by the run length.
fn Engine::push_run(
self : Engine,
s : String,
from : Int32,
to : Int32,
) -> Unit raise MendError {
let len = to - from
self.parts.push(s[from:to].to_owned())
self.parts_len = self.parts_len + len
if self.parts_len > self.opts.max_token_len {
raise TokenLimit(self.opts.max_token_len, self.tok_start.offset)
}
self.pos = {
offset: self.pos.offset + len,
line: self.pos.line,
column: self.pos.column + len,
}
self.snap_stamp = self.snap_stamp + 1
}
///|
/// Feed one UTF-16 code unit.
fn Engine::step(self : Engine, u : UInt16) -> Unit raise MendError {
if self.pos.offset >= self.opts.max_input_len {
raise SizeLimit(self.opts.max_input_len, self.pos.offset)
}
let p = self.pos
match self.tok {
TIdle => self.step_idle(u, p)
TStr(kind, is_key) => self.step_str(u, p, kind, is_key)
TStrPending(kind, is_key) => self.step_str_pending(u, p, kind, is_key)
TBare(kind) => self.step_bare(u, p, kind)
TSlash => self.step_slash(u, p)
TLineComment => if u.to_int() == 0x0A { self.tok = TIdle }
TBlockComment => self.step_block(u, p)
TFence(count) => self.step_fence(u, p, count)
TFenceLine => if u.to_int() == 0x0A { self.finish_fence_line() }
}
self.pos = advance(p, u)
self.snap_stamp = self.snap_stamp + 1
}
///|
/// Feed raw UTF-8 bytes (FR-12). Invalid bytes raise `E040`.
///
/// The payload of `E040` is an absolute byte offset from the start of the whole
/// input, counted across every `feed_bytes` call, so the diagnostic does not
/// depend on how the caller split the stream into chunks (BR-22).
fn Engine::feed_bytes(self : Engine, bs : Bytes) -> Unit raise MendError {
for i = 0; i < bs.length(); i = i + 1 {
self.feed_byte(bs[i].to_char().to_int(), i)
}
self.fed_bytes = self.fed_bytes + bs.length()
}
///|
/// Feed one UTF-8 byte. `idx` is the offset inside the current chunk; the
/// absolute offset is `fed_bytes + idx` (BR-22).
fn Engine::feed_byte(
self : Engine,
b : Int32,
idx : Int32,
) -> Unit raise MendError {
let abs = self.fed_bytes + idx
if self.utf8_need == 0 {
if b < 0x80 {
self.step(b.to_uint16())
return
}
if b >= 0xC2 && b <= 0xDF {
self.utf8_need = 1
self.utf8_cp = b & 0x1F
self.utf8_min = 0x80
self.utf8_start = abs
return
}
if b >= 0xE0 && b <= 0xEF {
self.utf8_need = 2
self.utf8_cp = b & 0x0F
self.utf8_min = 0x800
self.utf8_start = abs
return
}
if b >= 0xF0 && b <= 0xF4 {
self.utf8_need = 3
self.utf8_cp = b & 0x07
self.utf8_min = 0x10000
self.utf8_start = abs
return
}
raise InvalidUtf8(abs)
}
if b < 0x80 || b > 0xBF {
self.utf8_need = 0
raise InvalidUtf8(abs)
}
self.utf8_cp = (self.utf8_cp << 6) | (b & 0x3F)
self.utf8_need = self.utf8_need - 1
if self.utf8_need == 0 {
let cp = self.utf8_cp
if cp < self.utf8_min || cp > 0x10FFFF {
raise InvalidUtf8(self.utf8_start)
}
if cp <= 0xFFFF {
self.step(cp.to_uint16())
} else {
let v = cp - 0x10000
self.step((0xD800 + (v >> 10)).to_uint16())
self.step((0xDC00 + (v & 0x3FF)).to_uint16())
}
}
}