// M6 delta — the incremental change stream (FR-18).
//
// Delta tracking is off unless `Options::collect_deltas` is set, so every hook
// in this file is guarded by that flag and costs one branch otherwise.
//
// The contract, checked by `delta_wbtest.mbt`:
//
// applying the deltas returned by `drain_deltas()` to the value obtained
// from the previous drain yields exactly `snapshot().value`
//
// Three design consequences follow, and all three are enforced or documented:
//
// * text is flushed at drain time, not per code unit, so a long string costs
// one path computation per drain rather than one per character;
// * `duplicate_keys KeepFirst` is rejected by `Options::validate`, because a
// dropped duplicate still announces its container; the default `KeepLast`
// needs no retraction — a later `Set` simply overwrites;
// * `incomplete_number TrimToValid` is rejected as well, because an
// unfinished number has no delta representation.
///|
/// Record one delta. Only called when `collect_deltas` is on.
fn Engine::push_delta(
self : Engine,
op : DeltaOp,
path : Array[PathSegment],
value : Json?,
text : String,
) -> Unit {
self.deltas.push({ path, op, value, text, })
}
///|
/// Path of the container frame at `idx`.
fn Engine::frame_path(self : Engine, idx : Int32) -> Array[PathSegment] {
self.stack[idx].path.copy()
}
///|
/// Path of the slot the next child of the frame at `idx` would occupy.
fn Engine::child_path(self : Engine, idx : Int32) -> Array[PathSegment] {
let out = self.frame_path(idx)
let f = self.stack[idx]
match f.kind {
FArr => out.push(Index(f.items.length()))
FObj =>
match f.last_key {
Some(k) => out.push(Key(k))
None => ()
}
}
out
}
///|
/// Path of the slot currently being filled: the root, or the next child of the
/// innermost frame.
fn Engine::slot_path(self : Engine) -> Array[PathSegment] {
if self.stack.length() == 0 {
[]
} else {
self.child_path(self.stack.length() - 1)
}
}
///|
/// Whether the value currently being built is already visible in a snapshot.
///
/// At the root the pending value is hidden once a root value exists. Inside an
/// array the pending element is always visible. Inside an object it is visible
/// only once a key is known and a colon may follow.
fn Engine::slot_visible(self : Engine) -> Bool {
if self.stack.length() == 0 {
return self.root is None
}
let f = self.stack[self.stack.length() - 1]
match f.kind {
FArr => true
FObj =>
match f.last_key {
Some(_) => f.ctx is CObjColon || f.ctx is CObjValue
None => false
}
}
}
///|
/// Path of the string value that a pending `+` would extend (BR-21).
fn Engine::concat_target_path(self : Engine) -> Array[PathSegment] {
if self.stack.length() == 0 {
return []
}
let idx = self.stack.length() - 1
let out = self.frame_path(idx)
let f = self.stack[idx]
match f.kind {
FArr => if f.items.length() > 0 { out.push(Index(f.items.length() - 1)) }
FObj =>
match f.last_assigned_key {
Some(k) => out.push(Key(k))
None => ()
}
}
out
}
///|
/// Emit the delta that announces a freshly opened container.
fn Engine::delta_enter(
self : Engine,
path : Array[PathSegment],
kind : FrameKind,
) -> Unit {
let v = match kind {
FObj => Json::object(Map([]))
FArr => Json::array([])
}
self.push_delta(Set, path, Some(v), "")
}
///|
/// Emit the delta for a value that just landed in a slot. A container lands as
/// `Close`, which carries its final content; the earlier `Set` from
/// `delta_enter` announced the empty container.
fn Engine::delta_land(
self : Engine,
path : Array[PathSegment],
v : Json,
) -> Unit {
let op = match v {
Json::Array(_) | Json::Object(_) => Close
_ => Set
}
self.push_delta(op, path, Some(v), "")
}
///|
/// Emit the `Set` that opens a string value slot, so that a drain taken while
/// the string is still growing sees the element.
fn Engine::delta_open_string(self : Engine, is_key : Bool) -> Unit {
if is_key || self.concat_pending || !self.slot_visible() {
return
}
self.push_delta(Set, self.slot_path(), Some(Json::string("")), "")
}
///|
/// Emit the `Set` for a key that is visible before its value arrives, which is
/// what `pending_key NullKey` promises (FR-09).
fn Engine::delta_pending_key(self : Engine) -> Unit {
if self.opts.pending_key is OmitKey || self.stack.length() == 0 {
return
}
let idx = self.stack.length() - 1
let f = self.stack[idx]
if f.kind is FArr {
return
}
match f.last_key {
Some(_) =>
if f.ctx is CObjColon || f.ctx is CObjValue {
self.push_delta(Set, self.child_path(idx), Some(Json::null()), "")
}
None => ()
}
}
///|
/// Text of the string being scanned that has not been reported yet.
fn Engine::delta_text_tail(self : Engine) -> String {
let sb = StringBuilder()
let mut acc = 0
for part in self.parts {
let n = part.length()
if acc + n <= self.delta_str_reported {
acc = acc + n
continue
}
let start = if self.delta_str_reported > acc {
self.delta_str_reported - acc
} else {
0
}
sb.write_string(part[start:n].to_owned())
acc = acc + n
}
sb.to_string()
}
///|
/// Flush the growth of the string currently being scanned (FR-18). Called from
/// `drain_deltas`, so a long string costs one path computation per drain.
fn Engine::flush_delta_text(self : Engine) -> Unit {
if self.concat_pending || self.parts_len <= self.delta_str_reported {
return
}
match self.tok {
TStr(_k, is_key) | TStrPending(_k, is_key) => if is_key { return }
_ => return
}
if !self.slot_visible() {
return
}
let tail = self.delta_text_tail()
self.delta_str_reported = self.parts_len
if tail.length() > 0 {
self.push_delta(AppendText, self.slot_path(), None, tail)
}
}
///|
/// Return and clear the deltas recorded since the previous call, including the
/// text of a string that is still growing.
fn Engine::drain_deltas(self : Engine) -> Array[Delta] {
self.flush_delta_text()
let out = self.deltas
self.deltas = []
out
}