Retain native node deltas across confirmed sessions

This commit is contained in:
Kinneyzhang 2026-09-05 08:44:29 +08:00
parent 13e360a1a5
commit c9131401ea
11 changed files with 3040 additions and 202 deletions

View File

@ -9974,6 +9974,10 @@ role, and outside-complement compatibility before publication."
(plist-put overrides :native-inherited-dirty-node-ids
(plist-get candidate-state
:native-inherited-dirty-node-ids)))
(setq overrides
(plist-put overrides :native-local-dirty-entries
(plist-get candidate-state
:native-local-dirty-entries)))
(setq overrides
(plist-put
overrides :native-topology-stable-p
@ -10963,9 +10967,10 @@ fresh by the mixed projection."
((eq projection-kind 'native-frame)
(append
(ebox-incremental--viewport-state-overrides
candidate-state t)
candidate-state t)
(list :native-topology-stable-p t
:native-touched-node-ids nil
:native-local-dirty-entries nil
:native-removed-node-ids nil)))
((memq projection-kind
'(viewport-reflow viewport-reflow-mixed-scroll))

View File

@ -339,6 +339,7 @@ new session with the same bounded configuration used by retained frames."
(progn
(plist-put candidate-state :native-topology-stable-p t)
(plist-put candidate-state :native-touched-node-ids nil)
(plist-put candidate-state :native-local-dirty-entries nil)
(plist-put candidate-state :native-removed-node-ids nil)
t)))
@ -375,6 +376,8 @@ new session with the same bounded configuration used by retained frames."
old-state candidate-state))
(plist-put candidate-state :native-topology-stable-p stable)
(plist-put candidate-state :native-touched-node-ids layout-touched)
(plist-put candidate-state :native-local-dirty-entries
(copy-tree (plist-get prepared :dirty-set)))
(plist-put candidate-state :native-inherited-dirty-node-ids
(ebox-native-commit-inherited-dirty-node-ids prepared))
(when context-axes-stable
@ -612,6 +615,7 @@ publication transaction can still roll back."
(1+ (or (plist-get state :runtime-revision) 0))))
(plist-put state :native-frame-kind
(if (plist-get frame :native-patch) 'patch 'full))
(cl-remf state :native-local-dirty-entries)
frame)))
(defconst ebox-native-commit--failed-render-state-keys
@ -632,6 +636,7 @@ publication transaction can still roll back."
:native-reuse-mount-projection-p
:native-reuse-ownership-p
:native-inherited-dirty-node-ids
:native-local-dirty-entries
:native-topology-stable-p
:native-touched-node-ids
:native-removed-node-ids)

View File

@ -95,7 +95,7 @@
(defvar ebox--flex-content-min-width-table)
(defconst ebox-native-reflow-abi-version "11:7:12"
(defconst ebox-native-reflow-abi-version "12:7:12"
"Version tuple shared by the native module, layout IR, and render tape.")
(defconst ebox-native-reflow--minimum-rust-version "1.82.0"
@ -222,7 +222,10 @@ module while loading the package."
styles
layout-package
layout-fragment-cache
layout-fragment-index
layout-fragment-revision
layout-style-index
layout-property-template-index
readiness-process
readiness-preparation
released-p)
@ -286,9 +289,74 @@ module while loading the package."
(defvar ebox-native-reflow--compile-styles nil
"Dynamically bound ordered style registry for one Layout IR compile.")
(defvar ebox-native-reflow--compile-style-base-count 0
"Number of retained styles preceding the current append batch.")
(defvar ebox-native-reflow--compile-style-index nil
"Persistent retained style-to-id index for incremental compilation.")
(defvar ebox-native-reflow--compile-property-templates nil
"Dynamically bound ordered opaque text-property registry.")
(defvar ebox-native-reflow--compile-property-template-base-count 0
"Number of retained property templates preceding current additions.")
(defvar ebox-native-reflow--compile-property-template-index nil
"Persistent retained property-template-to-id index.")
(defvar ebox-native-reflow--compile-retained-registry-p nil
"Non-nil while style and property registries append incrementally.")
(defconst ebox-native-reflow--persistent-index-depth 16
"Fixed number of four-bit branches in a retained native index path.")
(defun ebox-native-reflow--persistent-index-code (key)
"Return a stable nonnegative retained-index code for KEY."
(if (and (integerp key) (>= key 0))
key
(logand (sxhash-equal key) #xffffffffffffffff)))
(defun ebox-native-reflow--persistent-index-get (root key &optional missing)
"Return KEY's value from persistent ROOT, or MISSING."
(let ((node root)
(code (ebox-native-reflow--persistent-index-code key))
(depth 0))
(while (and node (< depth ebox-native-reflow--persistent-index-depth))
(setq node (and (vectorp node)
(aref node (logand (ash code (* -4 depth)) 15)))
depth (1+ depth)))
(if (= depth ebox-native-reflow--persistent-index-depth)
(let ((pair (cl-assoc key node :test #'equal)))
(if pair (cdr pair) missing))
missing)))
(defun ebox-native-reflow--persistent-index-put (root key value &optional depth)
"Return persistent ROOT updated to map KEY to VALUE.
Only the fixed-depth path for KEY is copied. DEPTH is for recursion."
(let ((depth (or depth 0)))
(if (= depth ebox-native-reflow--persistent-index-depth)
(cons (cons key value)
(cl-remove key root :key #'car :test #'equal))
(let* ((code (ebox-native-reflow--persistent-index-code key))
(branch (logand (ash code (* -4 depth)) 15))
(copy (if (vectorp root)
(copy-sequence root)
(make-vector 16 nil))))
(aset copy branch
(ebox-native-reflow--persistent-index-put
(aref copy branch) key value (1+ depth)))
copy))))
(defun ebox-native-reflow--persistent-index-from-sequence (sequence)
"Return an immutable persistent index for ordered SEQUENCE values."
(let (root)
(cl-loop for value across (vconcat sequence)
for id from 0
do (setq root
(ebox-native-reflow--persistent-index-put
root value id)))
root))
(defvar ebox-native-reflow--compile-property-template-ids nil
"Dynamically bound opaque property-template lookup table.")
@ -1338,14 +1406,20 @@ copied separately; no pending frame or registered Rust document is inherited."
(condition-case err
(progn
(setf (ebox-native-reflow-session-styles fork)
(copy-sequence (ebox-native-reflow-session-styles session))
(ebox-native-reflow-session-styles session)
(ebox-native-reflow-session-layout-package fork)
(ebox-native-reflow-session-layout-package session)
(ebox-native-reflow-session-layout-fragment-cache fork)
(copy-hash-table
(ebox-native-reflow-session-layout-fragment-cache session))
(ebox-native-reflow-session-layout-fragment-cache session)
(ebox-native-reflow-session-layout-fragment-index fork)
(ebox-native-reflow-session-layout-fragment-index session)
(ebox-native-reflow-session-layout-fragment-revision fork)
(ebox-native-reflow-session-layout-fragment-revision session))
(ebox-native-reflow-session-layout-fragment-revision session)
(ebox-native-reflow-session-layout-style-index fork)
(ebox-native-reflow-session-layout-style-index session)
(ebox-native-reflow-session-layout-property-template-index fork)
(ebox-native-reflow-session-layout-property-template-index
session))
fork)
(error
(ebox-native-reflow-release-session fork)
@ -1409,7 +1483,11 @@ copied separately; no pending frame or registered Rust document is inherited."
:properties owned-properties))
(missing (make-symbol "missing-property-template"))
(existing
(if (hash-table-p
(if ebox-native-reflow--compile-retained-registry-p
(ebox-native-reflow--persistent-index-get
ebox-native-reflow--compile-property-template-index
entry missing)
(if (hash-table-p
ebox-native-reflow--compile-property-template-ids)
(gethash
entry ebox-native-reflow--compile-property-template-ids
@ -1417,13 +1495,19 @@ copied separately; no pending frame or registered Rust document is inherited."
(or (cl-position
entry ebox-native-reflow--compile-property-templates
:test #'equal)
missing))))
missing)))))
(if (not (eq existing missing))
existing
(let ((id (length ebox-native-reflow--compile-property-templates)))
(let ((id (+ ebox-native-reflow--compile-property-template-base-count
(length ebox-native-reflow--compile-property-templates))))
(setq ebox-native-reflow--compile-property-templates
(append ebox-native-reflow--compile-property-templates
(list entry)))
(when ebox-native-reflow--compile-retained-registry-p
(setq ebox-native-reflow--compile-property-template-index
(ebox-native-reflow--persistent-index-put
ebox-native-reflow--compile-property-template-index
entry id)))
(when (hash-table-p
ebox-native-reflow--compile-property-template-ids)
(puthash
@ -1592,13 +1676,27 @@ When FORMATTED-P is non-nil, preserve exact display spaces produced by KP."
(defun ebox-native-reflow--register-style (mode face)
"Return the stable numeric id for a MODE and FACE operation."
(let* ((entry (list :mode mode :face face))
(existing (cl-position entry ebox-native-reflow--compile-styles
:test #'equal)))
(or existing
(prog1 (length ebox-native-reflow--compile-styles)
(missing (make-symbol "missing-native-style"))
(existing
(if ebox-native-reflow--compile-retained-registry-p
(ebox-native-reflow--persistent-index-get
ebox-native-reflow--compile-style-index entry missing)
(or (cl-position entry ebox-native-reflow--compile-styles
:test #'equal)
missing))))
(if (not (eq existing missing))
existing
(prog1 (+ ebox-native-reflow--compile-style-base-count
(length ebox-native-reflow--compile-styles))
(setq ebox-native-reflow--compile-styles
(append ebox-native-reflow--compile-styles
(list entry)))))))
(list entry)))
(when ebox-native-reflow--compile-retained-registry-p
(setq ebox-native-reflow--compile-style-index
(ebox-native-reflow--persistent-index-put
ebox-native-reflow--compile-style-index entry
(+ ebox-native-reflow--compile-style-base-count
(1- (length ebox-native-reflow--compile-styles))))))))))
(defun ebox-native-reflow--compile-add-face-style (face)
"Register additive FACE and return its id, or nil."
@ -2286,6 +2384,16 @@ Otherwise return CANDIDATE with the next document revision."
(plist-put replacement :document-revision
(1+ (or (plist-get old :document-revision) 0)))))))
(defun ebox-native-reflow--confirmed-layout-package (package)
"Return PACKAGE without one-shot retained input transport fields."
(if (plist-get package :document-delta)
(let ((confirmed (copy-sequence package)))
(dolist (key '(:document-delta :native-fragment-index
:native-fragment-revision :native-style-index))
(cl-remf confirmed key))
confirmed)
package))
(defun ebox-native-reflow--buffer-display-signature (buffer)
"Return BUFFER's live canonical Surface display capability."
(with-current-buffer buffer
@ -2491,9 +2599,9 @@ per call, and no call recursively visits the captured Ebox tree."
(gethash child flex-content-min-widths)))
(ebox-native-reflow--retained-layout-children node))))))
(defun ebox-native-reflow--compile-retained-layout-package
(defun ebox-native-reflow--compile-retained-layout-package-full
(session state node)
"Compile NODE incrementally into retained native SESSION from STATE."
"Compile a complete retained package for NODE in SESSION from STATE."
(let ((postorder (plist-get state :native-node-postorder)))
(unless (and (vectorp postorder) (> (length postorder) 0))
(setq postorder
@ -2632,7 +2740,9 @@ per call, and no call recursively visits the captured Ebox tree."
(setq fragment
(ebox-native-reflow--compile-scene-node
current fragments flex-content-min-widths)
fragment (plist-put fragment :node-id node-id)
revision (1+ revision)
fragment (plist-put fragment :node-revision revision)
entry (list :signature (copy-tree signature)
:fragment fragment
:revision revision)))
@ -2658,10 +2768,354 @@ per call, and no call recursively visits the captured Ebox tree."
candidate)))
(setf (ebox-native-reflow-session-layout-fragment-cache session)
new-cache
(ebox-native-reflow-session-layout-fragment-index session)
(let (index)
(maphash
(lambda (node-id entry)
(setq index
(ebox-native-reflow--persistent-index-put
index node-id entry)))
new-cache)
index)
(ebox-native-reflow-session-layout-fragment-revision session)
revision)
revision
(ebox-native-reflow-session-layout-style-index session)
(ebox-native-reflow--persistent-index-from-sequence
(plist-get package :styles))
(ebox-native-reflow-session-layout-property-template-index
session)
(ebox-native-reflow--persistent-index-from-sequence
(plist-get package :property-templates)))
package)))))
(defconst ebox-native-reflow--delta-edge-fields
'(:type :child :children :items :node-id :node-revision)
"Fragment fields that a stable local node delta cannot replace.")
(defconst ebox-native-reflow--delta-flex-edge-properties
'(:order :flex-grow :flex-shrink :flex-basis :align-self
:flex-direction :cross-align)
"Changed properties that alter retained Flex item metadata in N1.")
(defun ebox-native-reflow--delta-flex-edge-change-p (state)
"Return non-nil when STATE changes unsupported Flex edge metadata."
(cl-some
(lambda (dirty)
(let* ((keys (plist-get dirty :changed-keys))
(parents (plist-get state :parent-table))
(nodes (plist-get state :node-table))
(dirty-id (plist-get dirty :node-id))
(dirty-node (and nodes (gethash dirty-id nodes)))
(raw-parent-id (and parents (gethash dirty-id parents)))
(raw-parent (and nodes (gethash raw-parent-id nodes)))
(owner-id
(if (and (eq (plist-get dirty-node :ebox-kind) 'text)
(eq (plist-get raw-parent :ebox-kind) 'box)
(when-let* ((layout
(plist-get raw-parent
:ebox-layout-config)))
(eq (ebox-layout-config-kind layout) 'normal)))
raw-parent-id
dirty-id))
(edge-parent-id (and parents (gethash owner-id parents)))
(edge-parent (and nodes (gethash edge-parent-id nodes)))
(config
(and edge-parent
(plist-get edge-parent :ebox-layout-config))))
(or
(cl-some (lambda (key)
(memq key ebox-native-reflow--delta-flex-edge-properties))
keys)
;; A child-local edit can change content-min-width in a Flex item or
;; derived align-self in typed axis lowering. N1 has no edge patch,
;; so retain correctness through full input until exact edge facts are
;; independently addressable.
(or (eq (plist-get edge-parent :ebox-type) 'flex)
(and config
(let ((kind (ebox-layout-config-kind config))
(props (ebox-layout-config-props config)))
(or (eq kind 'flex)
(and (memq kind '(row column))
(not (and (equal (plist-get props :item-gap) 0)
(eq (plist-get props :cross-align)
'stretch)))))))))))
(plist-get state :native-local-dirty-entries)))
(defun ebox-native-reflow--fragment-local (fragment)
"Return FRAGMENT's scalar and local-content fields."
(let ((tail fragment) local)
(while tail
(let ((key (pop tail))
(value (pop tail)))
(unless (memq key ebox-native-reflow--delta-edge-fields)
(setq local (nconc local (list key value))))))
local))
(defun ebox-native-reflow--local-difference (old new)
"Return NEW local fields whose values differ from OLD."
(let ((tail new) difference)
(while tail
(let ((key (pop tail))
(value (pop tail)))
(unless (equal value (plist-get old key))
(setq difference (nconc difference (list key value))))))
difference))
(defun ebox-native-reflow--apply-local-difference (fragment difference)
"Return FRAGMENT with scalar fields from DIFFERENCE replaced."
(let ((copy (copy-sequence fragment))
(tail difference))
(while tail
(setq copy (plist-put copy (pop tail) (pop tail))))
copy))
(defun ebox-native-reflow--delta-owner-id (state node-id index)
"Return addressable owner for NODE-ID in STATE and retained INDEX."
(let ((parents (plist-get state :parent-table))
(nodes (plist-get state :node-table))
(current node-id))
(when-let* ((source (and nodes (gethash current nodes)))
(parent-id (and parents (gethash current parents)))
(parent (and nodes (gethash parent-id nodes))))
(when (and (eq (plist-get source :ebox-kind) 'text)
(eq (plist-get parent :ebox-kind) 'box)
(eq (ebox-layout-config-kind
(plist-get parent :ebox-layout-config))
'normal))
(setq current parent-id)))
(while (and current
(null (ebox-native-reflow--persistent-index-get
index current)))
(setq current (and parents (gethash current parents))))
current))
(defun ebox-native-reflow--compile-delta-slots (node old-fragment)
"Compile NODE local slots while retaining OLD-FRAGMENT edges."
(pcase (plist-get node :ebox-type)
('box
(if (eq (plist-get node :ebox-kind) 'text)
(vector (ebox-native-reflow--compile-text-node node))
(let* ((config (and (eq (plist-get node :ebox-kind) 'box)
(plist-get node :ebox-layout-config)))
(kind (and config (ebox-layout-config-kind config)))
(old-child (plist-get old-fragment :child))
(fused-p (and (eq kind 'normal)
(let ((child (car (ebox-tree-node-children node))))
(and child
(eq (plist-get child :ebox-kind) 'text)))))
(inner
(pcase kind
((or 'row 'column)
(ebox-native-reflow--compile-typed-axis-children
kind config nil))
('flex
(ebox-native-reflow--compile-flex-inner
(copy-sequence (ebox-layout-config-props config)) nil))
(_ nil)))
(child (cond (fused-p
(ebox-native-reflow--compile-text-node
(car (ebox-tree-node-children node))))
(inner inner)
(t old-child)))
(outer (ebox-native-reflow--compile-box node child nil t)))
(if inner (vector outer inner) (vector outer)))))
('flex
(let* ((wrapper (plist-get node :box))
(props (ebox--flex-container-content-props
(plist-get node :props) (plist-get node :raw-props)
wrapper)))
(when wrapper
(plist-put props :width nil)
(when (plist-member (plist-get node :raw-props) :height)
(plist-put props :height 'viewport-height)))
(let ((inner (ebox-native-reflow--compile-flex-inner props nil)))
(if wrapper
(vector (ebox-native-reflow--compile-box wrapper inner t) inner)
(vector inner)))))
(_ (vector old-fragment))))
(defun ebox-native-reflow--delta-direct-owner-ids (state index)
"Return owners for STATE's direct dirty entries found in INDEX."
(let ((seen (make-hash-table :test 'equal)) result)
(dolist (dirty (plist-get state :native-local-dirty-entries))
(when-let* ((owner
(ebox-native-reflow--delta-owner-id
state (plist-get dirty :node-id) index)))
(unless (gethash owner seen)
(puthash owner t seen)
(push owner result))))
(nreverse result)))
(defun ebox-native-reflow--compile-retained-layout-delta
(session state node)
"Return SESSION's local delta for NODE under STATE, or nil for fallback."
(let* ((old-package (ebox-native-reflow-session-layout-package session))
(old-index (ebox-native-reflow-session-layout-fragment-index session))
(node-table (plist-get state :node-table))
(touched (plist-get state :native-touched-node-ids)))
(when (and old-package old-index (hash-table-p node-table)
(plist-get state :native-topology-stable-p)
(plist-member state :native-local-dirty-entries)
(not (ebox-native-reflow--delta-flex-edge-change-p state))
;; Descendant inherited-style expansion needs its own bounded
;; affected-node index. Until then it is an explicit full
;; input fallback rather than a partial semantic update.
(null (plist-get state :native-inherited-dirty-node-ids)))
(if (null touched)
old-package
(let* ((style-base (length (plist-get old-package :styles)))
(template-base
(length (plist-get old-package :property-templates)))
(ebox-native-reflow--compile-styles nil)
(ebox-native-reflow--compile-style-base-count style-base)
(ebox-native-reflow--compile-style-index
(ebox-native-reflow-session-layout-style-index session))
(ebox-native-reflow--compile-property-templates nil)
(ebox-native-reflow--compile-property-template-base-count
template-base)
(ebox-native-reflow--compile-property-template-index
(ebox-native-reflow-session-layout-property-template-index
session))
(ebox-native-reflow--compile-retained-registry-p t)
(ebox-native-reflow--compile-display-signature
(ebox--current-display-signature))
(ebox-native-reflow--compile-root-node node)
(ebox--render-string-pixel-width-cache
(make-hash-table :test 'equal))
(ebox--render-string-max-pixel-width-cache
(make-hash-table :test 'eq))
(direct (ebox-native-reflow--delta-direct-owner-ids
state old-index))
(direct-set (make-hash-table :test 'equal))
(entry-ids nil)
(seen (make-hash-table :test 'equal))
(new-index old-index)
(revision
(or (ebox-native-reflow-session-layout-fragment-revision
session) 0))
entries unsupported)
(dolist (id direct) (puthash id t direct-set))
(dolist (id touched)
(when-let* ((owner
(ebox-native-reflow--delta-owner-id state id old-index)))
(unless (gethash owner seen)
(puthash owner t seen)
(push owner entry-ids))))
(dolist (owner-id (nreverse entry-ids))
(let* ((cached (ebox-native-reflow--persistent-index-get
old-index owner-id))
(old-fragment (plist-get cached :fragment))
(source (gethash owner-id node-table))
(expected (plist-get cached :revision))
patches new-fragment)
(unless (and cached source (integerp expected))
(setq unsupported t))
(when (and (not unsupported) (gethash owner-id direct-set))
(let ((slots
(ebox-native-reflow--compile-delta-slots
source old-fragment)))
(unless (equal (plist-get (aref slots 0) :type)
(plist-get old-fragment :type))
(setq unsupported t))
(unless unsupported
(let ((local
(ebox-native-reflow--local-difference
(ebox-native-reflow--fragment-local old-fragment)
(ebox-native-reflow--fragment-local
(aref slots 0)))))
(when local
(push (list :slot 0 :local local) patches)
(setq new-fragment
(ebox-native-reflow--apply-local-difference
old-fragment local))))
(when (> (length slots) 1)
(let ((old-inner (plist-get old-fragment :child))
(new-inner (aref slots 1)))
(unless (equal (plist-get old-inner :type)
(plist-get new-inner :type))
(setq unsupported t))
(unless unsupported
(let ((local
(ebox-native-reflow--local-difference
(ebox-native-reflow--fragment-local old-inner)
(ebox-native-reflow--fragment-local new-inner))))
(when local
(push (list :slot 1 :local local) patches)
(let ((outer (or new-fragment
(copy-sequence old-fragment))))
(plist-put
outer :child
(ebox-native-reflow--apply-local-difference
old-inner local))
(setq new-fragment outer))))))))))
(unless unsupported
(setq revision (1+ revision))
(unless new-fragment
(setq new-fragment (copy-sequence old-fragment)))
(plist-put new-fragment :node-id owner-id)
(plist-put new-fragment :node-revision revision)
(let* ((entry
(append
(list :node-id owner-id
:expected-revision expected
:target-revision revision)
(when patches
(list :slot-patches (vconcat (nreverse patches))))))
(new-cache-entry
(list :signature nil
:fragment (or new-fragment old-fragment)
:revision revision)))
(push entry entries)
(setq new-index
(ebox-native-reflow--persistent-index-put
new-index owner-id new-cache-entry))))))
(when (and (not unsupported)
;; N1 can authenticate retained templates, but cannot
;; append their Emacs-owned values to Rust atomically.
(= template-base
(+ template-base
(length ebox-native-reflow--compile-property-templates))))
(let* ((styles-append ebox-native-reflow--compile-styles)
(styles
(if styles-append
;; Materializing the global style output is explicitly
;; outside retained input work; the common no-append
;; path preserves the old vector by identity.
(vconcat (plist-get old-package :styles) styles-append)
(plist-get old-package :styles)))
(base (or (plist-get old-package :document-revision) 0))
(package (copy-sequence old-package)))
(plist-put package :document-revision (1+ base))
(plist-put package :styles styles)
(plist-put
package :document-delta
(list :style-base-count style-base
:styles-append
(vconcat
(mapcar
(lambda (descriptor)
(list :mode (symbol-name (plist-get descriptor :mode))
:face
(list :lisp
(prin1-to-string
(plist-get descriptor :face)))))
styles-append))
:property-template-base-count template-base
:property-template-target-count template-base
:entries (vconcat (nreverse entries))))
(plist-put package :native-fragment-index new-index)
(plist-put package :native-fragment-revision revision)
(plist-put package :native-style-index
ebox-native-reflow--compile-style-index)
package)))))))
(defun ebox-native-reflow--compile-retained-layout-package
(session state node)
"Compile NODE for SESSION using a delta when STATE proves it supported."
(or (ebox-native-reflow--compile-retained-layout-delta session state node)
(ebox-native-reflow--compile-retained-layout-package-full
session state node)))
(defun ebox-native-reflow-compile-layout-ir (node)
"Compile NODE to one complete versioned native layout document."
(plist-get (ebox-native-reflow--compile-layout-package node) :document))
@ -2682,9 +3136,10 @@ per call, and no call recursively visits the captured Ebox tree."
(defun ebox-native-reflow--layout-control-json
(document frames &optional document-base-revision
document-target-revision)
document-target-revision document-delta)
"Return strict control JSON for optional layout DOCUMENT and FRAMES.
DOCUMENT-BASE-REVISION and DOCUMENT-TARGET-REVISION bind retained input."
DOCUMENT-BASE-REVISION and DOCUMENT-TARGET-REVISION bind retained input.
DOCUMENT-DELTA is the stable-topology local replacement batch."
(unless (and (listp frames) frames)
(error "Native reflow requires at least one layout frame"))
(let ((control
@ -2792,6 +3247,8 @@ DOCUMENT-BASE-REVISION and DOCUMENT-TARGET-REVISION bind retained input."
document-target-revision)))
(when document
(setq control (plist-put control :document document)))
(when document-delta
(setq control (plist-put control :document-delta document-delta)))
(ebox-native-reflow--serialize-layout-control control)))
(defun ebox-native-reflow-submit-layout
@ -2807,9 +3264,12 @@ LAYOUT-PACKAGE may reuse a caller-validated document for the same NODE."
node
(ebox-native-reflow-session-styles session)
(plist-get old-package :property-templates))))
(document-delta (plist-get candidate-package :document-delta))
(package
(ebox-native-reflow--reuse-exact-layout-package
old-package candidate-package))
(if document-delta
candidate-package
(ebox-native-reflow--reuse-exact-layout-package
old-package candidate-package)))
(register-layout-p
(not (eq package
old-package)))
@ -2821,12 +3281,14 @@ LAYOUT-PACKAGE may reuse a caller-validated document for the same NODE."
(ebox-native-reflow--live-handle session)
generation
(ebox-native-reflow--layout-control-json
(and register-layout-p (plist-get package :document)) frames
base-revision target-revision))))
(and register-layout-p (not document-delta)
(plist-get package :document))
frames base-revision target-revision document-delta))))
(setf (ebox-native-reflow-session-generation session) generation)
(setf (ebox-native-reflow-session-styles session)
(plist-get package :styles))
(setf (ebox-native-reflow-session-layout-package session) package)
(setf (ebox-native-reflow-session-layout-package session)
(ebox-native-reflow--confirmed-layout-package package))
accepted))
(defun ebox-native-reflow--tape-cursor (payload &optional position)
@ -3968,13 +4430,16 @@ root effect metadata required by a thin host commit."
(plist-get
(ebox-native-reflow-session-layout-package session)
:property-templates)))))
(document-delta (plist-get candidate-package :document-delta))
(package
(ebox-native-reflow--reuse-exact-layout-package
old-package candidate-package))
(if document-delta
candidate-package
(ebox-native-reflow--reuse-exact-layout-package
old-package candidate-package)))
(generation (1+ (ebox-native-reflow-session-generation session)))
(control-frame (copy-sequence frame))
(native-key (or (plist-get control-frame :key) 1))
(reuse-document-p (eq package old-package))
(reuse-document-p (or document-delta (eq package old-package)))
(base-revision (or (plist-get old-package :document-revision) 0))
(target-revision (or (plist-get package :document-revision)
(1+ base-revision))))
@ -3986,12 +4451,22 @@ root effect metadata required by a thin host commit."
generation
(ebox-native-reflow--layout-control-json
(unless reuse-document-p (plist-get package :document))
(list control-frame) base-revision target-revision))
(list control-frame) base-revision target-revision
document-delta))
package control-frame generation)))
(setf (ebox-native-reflow-session-generation session) generation
(ebox-native-reflow-session-layout-package session) package
(ebox-native-reflow-session-layout-package session)
(ebox-native-reflow--confirmed-layout-package package)
(ebox-native-reflow-session-styles session)
(plist-get package :styles))
(when document-delta
(setf (ebox-native-reflow-session-layout-fragment-cache session) nil
(ebox-native-reflow-session-layout-fragment-index session)
(plist-get package :native-fragment-index)
(ebox-native-reflow-session-layout-fragment-revision session)
(plist-get package :native-fragment-revision)
(ebox-native-reflow-session-layout-style-index session)
(plist-get package :native-style-index)))
result)))
(defun ebox-native-reflow-render-proof-sync

View File

@ -278,6 +278,12 @@ rather than relying on an implicit default.")
(:symbol ebox-native-reflow--compile-property-template-ids
:reason dynamically-bound-compile-local-scratch
:evidence let-bound-per-compile-and-never-committed)
(:symbol ebox-native-reflow--compile-property-template-index
:reason dynamically-bound-compile-local-scratch
:evidence let-bound-per-compile-and-published-only-through-session-root)
(:symbol ebox-native-reflow--compile-style-index
:reason dynamically-bound-compile-local-scratch
:evidence let-bound-per-compile-and-published-only-through-session-root)
(:symbol ebox--render-source-generations
:reason dynamically-bound-render-proof-input
:evidence let-bound-to-candidate-and-prior-generations-for-one-plan)

View File

@ -164,7 +164,7 @@ ebox_module_version(emacs_env *env, ptrdiff_t nargs, emacs_value *args,
(void) nargs;
(void) args;
(void) data;
static const char version[] = "11:7:12";
static const char version[] = "12:7:12";
return env->make_string(env, version, (ptrdiff_t) (sizeof version - 1));
}

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
mod layout;
use layout::{
encode_error_tape, LayoutContext, LayoutDocument, LayoutTape, TapeIdentity, TapeOutputOptions,
MAX_LAYOUT_DIMENSION, MIN_TAPE_BYTES,
encode_error_tape, DocumentDelta, LayoutContext, LayoutDocument, LayoutTape, RetainedDocument,
TapeIdentity, TapeOutputOptions, MAX_LAYOUT_DIMENSION, MIN_TAPE_BYTES,
};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
@ -324,6 +324,8 @@ struct ControlBatch {
version: u32,
#[serde(default)]
document: Option<LayoutDocument>,
#[serde(default, rename = "document-delta")]
document_delta: Option<DocumentDelta>,
#[serde(default, rename = "document-base-revision")]
document_base_revision: Option<u64>,
#[serde(default, rename = "document-target-revision")]
@ -375,7 +377,7 @@ struct ControlFrame {
enum JobPayload {
Echo(Vec<u8>),
Layout {
document: Arc<LayoutDocument>,
document: LayoutSource,
context: LayoutContext,
root_width: i64,
root_width_override: bool,
@ -388,9 +390,43 @@ enum JobPayload {
root_metadata: bool,
document_base_revision: u64,
document_target_revision: u64,
validation_resolver_lookups: u64,
},
}
#[derive(Clone, Debug)]
enum LayoutSource {
Full(Arc<LayoutDocument>),
Retained(Arc<RetainedDocument>),
}
impl LayoutSource {
fn validate_context(&self, context: LayoutContext) -> Result<(), String> {
match self {
Self::Full(document) => document.validate_context(context),
Self::Retained(document) => document.validate_context(context),
}
}
fn layout_tape(
&self,
context: LayoutContext,
root_width: Option<i64>,
) -> Result<LayoutTape, String> {
match self {
Self::Full(document) => document.layout_tape(context, root_width),
Self::Retained(document) => document.layout_tape(context, root_width),
}
}
fn styles(&self) -> Result<Vec<layout::StyleTemplate>, String> {
match self {
Self::Full(document) => Ok(document.styles.clone()),
Self::Retained(document) => document.styles(),
}
}
}
#[derive(Clone, Debug)]
struct BaselineIdentity {
context: LayoutContext,
@ -404,7 +440,7 @@ struct BaselineIdentity {
#[derive(Clone, Debug)]
struct ConfirmedBaseline {
identity: BaselineIdentity,
document: Arc<LayoutDocument>,
document: LayoutSource,
document_revision: u64,
tape: LayoutTape,
styles: Vec<layout::StyleTemplate>,
@ -413,7 +449,7 @@ struct ConfirmedBaseline {
#[derive(Debug)]
struct PendingBaseline {
confirmed_identity: BaselineIdentity,
document: Arc<LayoutDocument>,
document: LayoutSource,
document_revision: u64,
tape: LayoutTape,
styles: Vec<layout::StyleTemplate>,
@ -431,6 +467,20 @@ struct RenderedJob {
baseline_hit: bool,
base_renders: u64,
target_renders: u64,
resolver_lookups: u64,
}
#[derive(Default)]
struct DocumentInputStats {
parses: u64,
validations: u64,
reuses: u64,
full_input_bytes: u64,
delta_input_bytes: u64,
full_nodes_parsed: u64,
delta_entries_parsed: u64,
delta_entries_validated: u64,
trie_path_nodes_copied: u64,
}
#[derive(Debug)]
@ -463,6 +513,13 @@ struct RuntimeState {
document_parses: u64,
document_validations: u64,
document_reuses: u64,
document_full_input_bytes: u64,
document_delta_input_bytes: u64,
document_full_nodes_parsed: u64,
document_delta_entries_parsed: u64,
document_delta_entries_validated: u64,
document_trie_path_nodes_copied: u64,
document_resolver_lookups: u64,
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
}
@ -491,7 +548,7 @@ struct Shared {
struct Session {
shared: Arc<Shared>,
workers: Mutex<Option<Vec<JoinHandle<()>>>>,
layout_document: Mutex<Option<Arc<LayoutDocument>>>,
layout_document: Mutex<Option<LayoutSource>>,
worker_count: usize,
}
@ -516,6 +573,13 @@ struct SessionStats {
document_parses: u64,
document_validations: u64,
document_reuses: u64,
document_full_input_bytes: u64,
document_delta_input_bytes: u64,
document_full_nodes_parsed: u64,
document_delta_entries_parsed: u64,
document_delta_entries_validated: u64,
document_trie_path_nodes_copied: u64,
document_resolver_lookups: u64,
pending_baselines: usize,
confirmed_baseline: bool,
confirmed_baseline_bytes: usize,
@ -581,6 +645,13 @@ impl Session {
document_parses: 0,
document_validations: 0,
document_reuses: 0,
document_full_input_bytes: 0,
document_delta_input_bytes: 0,
document_full_nodes_parsed: 0,
document_delta_entries_parsed: 0,
document_delta_entries_validated: 0,
document_trie_path_nodes_copied: 0,
document_resolver_lookups: 0,
confirmed_baseline,
}),
readiness_channel: Mutex::new(None),
@ -611,7 +682,7 @@ impl Session {
.unwrap_or_else(|poison| poison.into_inner())
.confirmed_baseline
.as_ref()
.map(|baseline| Arc::clone(&baseline.document));
.map(|baseline| baseline.document.clone());
Ok(Box::new(Self {
shared,
workers: Mutex::new(Some(handles)),
@ -643,6 +714,9 @@ impl Session {
fn submit(&self, generation: u64, payload: &[u8]) -> Result<usize, String> {
let batch = parse_control_batch(payload)?;
if batch.document_delta.is_some() {
return Err("Native async reflow does not accept retained document deltas".to_owned());
}
if !self.shared.alive.load(Ordering::Acquire) {
return Err("Native reflow session is closed".to_owned());
}
@ -672,12 +746,12 @@ impl Session {
let document = match batch.document {
Some(document) => {
document.validate()?;
Some(Arc::new(document))
Some(LayoutSource::Full(Arc::new(document)))
}
None if layout_requested => Some(
confirmed_baseline
.as_ref()
.map(|baseline| Arc::clone(&baseline.document))
.map(|baseline| baseline.document.clone())
.or_else(|| {
self.layout_document
.lock()
@ -852,10 +926,13 @@ impl Session {
.unwrap_or_else(|poison| poison.into_inner())
.confirmed_baseline
.clone();
let (document, document_parses, document_validations, document_reuses) = match batch
.document
{
Some(document) => {
let (document, input_stats) = match (batch.document, batch.document_delta) {
(Some(_), Some(_)) => {
return Err(
"Native retained render cannot combine document and document-delta".to_owned(),
);
}
(Some(document), None) => {
if document_base_revision.checked_add(1) != Some(document_target_revision) {
return Err(
"Native retained document replacement must advance one revision".to_owned(),
@ -876,10 +953,60 @@ impl Session {
}
_ => {}
}
document.validate()?;
(Arc::new(document), 1, 1, 0)
let retained = document.retained_root_p();
let (source, node_count) = if retained {
let (document, node_count) = RetainedDocument::bootstrap(document)?;
(LayoutSource::Retained(document), node_count)
} else {
document.validate()?;
let node_count = document.input_node_count();
(LayoutSource::Full(Arc::new(document)), node_count)
};
(
source,
DocumentInputStats {
parses: 1,
validations: 1,
full_input_bytes: payload.len() as u64,
full_nodes_parsed: node_count,
..DocumentInputStats::default()
},
)
}
None => {
(None, Some(delta)) => {
if document_base_revision.checked_add(1) != Some(document_target_revision) {
return Err(
"Native retained document delta must advance one revision".to_owned()
);
}
let baseline = confirmed.as_ref().ok_or_else(|| {
"Native retained document delta requires a confirmed baseline".to_owned()
})?;
if baseline.document_revision != document_base_revision {
return Err(
"Native retained document delta base revision does not match confirmed state"
.to_owned(),
);
}
let LayoutSource::Retained(document) = &baseline.document else {
return Err(
"Native retained document delta requires an identified bootstrap"
.to_owned(),
);
};
let (document, parsed, copied) = document.apply_delta(delta)?;
(
LayoutSource::Retained(document),
DocumentInputStats {
delta_input_bytes: payload.len() as u64,
delta_entries_parsed: parsed,
delta_entries_validated: parsed,
trie_path_nodes_copied: copied,
..DocumentInputStats::default()
},
)
}
(None, None) => {
if document_base_revision != document_target_revision {
return Err(
"Native retained document reuse requires equal base and target revisions"
@ -902,7 +1029,13 @@ impl Session {
.to_owned(),
);
}
(Arc::clone(&baseline.document), 0, 0, 1)
(
baseline.document.clone(),
DocumentInputStats {
reuses: 1,
..DocumentInputStats::default()
},
)
}
};
let frame = batch
@ -943,9 +1076,16 @@ impl Session {
}
state.base_renders += output.base_renders;
state.target_renders += output.target_renders;
state.document_parses += document_parses;
state.document_validations += document_validations;
state.document_reuses += document_reuses;
state.document_resolver_lookups += output.resolver_lookups;
state.document_parses += input_stats.parses;
state.document_validations += input_stats.validations;
state.document_reuses += input_stats.reuses;
state.document_full_input_bytes += input_stats.full_input_bytes;
state.document_delta_input_bytes += input_stats.delta_input_bytes;
state.document_full_nodes_parsed += input_stats.full_nodes_parsed;
state.document_delta_entries_parsed += input_stats.delta_entries_parsed;
state.document_delta_entries_validated += input_stats.delta_entries_validated;
state.document_trie_path_nodes_copied += input_stats.trie_path_nodes_copied;
state.pending_baselines.clear();
if let Some(pending) = output.pending {
state
@ -1078,6 +1218,13 @@ impl Session {
document_parses: state.document_parses,
document_validations: state.document_validations,
document_reuses: state.document_reuses,
document_full_input_bytes: state.document_full_input_bytes,
document_delta_input_bytes: state.document_delta_input_bytes,
document_full_nodes_parsed: state.document_full_nodes_parsed,
document_delta_entries_parsed: state.document_delta_entries_parsed,
document_delta_entries_validated: state.document_delta_entries_validated,
document_trie_path_nodes_copied: state.document_trie_path_nodes_copied,
document_resolver_lookups: state.document_resolver_lookups,
pending_baselines,
confirmed_baseline: state.confirmed_baseline.is_some(),
confirmed_baseline_bytes,
@ -1129,7 +1276,7 @@ fn parse_control_batch(payload: &[u8]) -> Result<ControlBatch, String> {
}
fn checked_layout_context(
document: &LayoutDocument,
document: &LayoutSource,
frame_key: i64,
viewport_width: i64,
viewport_width_known: bool,
@ -1179,11 +1326,12 @@ fn confirmed_baseline_matches(
}
fn prepare_layout_job(
document: &Arc<LayoutDocument>,
document: &LayoutSource,
frame: ControlFrame,
document_base_revision: u64,
document_target_revision: u64,
) -> Result<PreparedJob, String> {
layout::reset_resolver_lookups();
if frame.payload.is_some() {
return Err(format!(
"Native layout frame {} cannot contain an echo payload",
@ -1197,7 +1345,7 @@ fn prepare_layout_job(
.viewport_height
.ok_or_else(|| format!("Native layout frame {} requires viewport-height", frame.key))?;
let context = checked_layout_context(
document.as_ref(),
document,
frame.key,
viewport_width,
frame.viewport_width_known,
@ -1223,7 +1371,7 @@ fn prepare_layout_job(
)
})?;
let base_context = checked_layout_context(
document.as_ref(),
document,
frame.key,
base_viewport_width,
frame.base_viewport_width_known,
@ -1254,7 +1402,7 @@ fn prepare_layout_job(
key: frame.key,
delay_ms: frame.delay_ms,
payload: JobPayload::Layout {
document: Arc::clone(document),
document: document.clone(),
context,
root_width,
root_width_override: frame.root_width_override,
@ -1267,6 +1415,7 @@ fn prepare_layout_job(
root_metadata: frame.root_metadata,
document_base_revision,
document_target_revision,
validation_resolver_lookups: layout::resolver_lookups(),
},
})
}
@ -1287,6 +1436,7 @@ fn render_layout_payload(
baseline_hit: false,
base_renders: 0,
target_renders: 0,
resolver_lookups: 0,
},
JobPayload::Layout {
document,
@ -1302,6 +1452,7 @@ fn render_layout_payload(
root_metadata,
document_base_revision,
document_target_revision,
validation_resolver_lookups,
} => {
let identity = TapeIdentity {
session_id,
@ -1331,8 +1482,20 @@ fn render_layout_payload(
// job with neither result nor error tape, so the Elisp ready
// watcher would poll forever. Convert panics into the same
// error-tape channel ordinary layout failures use.
type LayoutRenderOutcome = Result<(Vec<u8>, LayoutTape, bool, u64, u64), String>;
type LayoutRenderOutcome = Result<
(
Vec<u8>,
LayoutTape,
Vec<layout::StyleTemplate>,
bool,
u64,
u64,
),
String,
>;
layout::reset_resolver_lookups();
let result = catch_unwind(AssertUnwindSafe(|| -> LayoutRenderOutcome {
let target_styles = document.styles()?;
if let Some(base_context) = base_context {
let base_identity = BaselineIdentity {
context: base_context,
@ -1351,7 +1514,7 @@ fn render_layout_payload(
&base_identity,
) && layout::style_registry_extends_exact_prefix(
&baseline.styles,
&document.styles,
&target_styles,
)
})
.cloned();
@ -1360,12 +1523,12 @@ fn render_layout_payload(
.layout_tape(context, root_width_override.then_some(root_width))?;
let bytes = layout::encode_layout_tape(
target.clone(),
&document.styles,
&target_styles,
identity,
output.root_metadata,
output.max_bytes,
)?;
return Ok((bytes, target, false, 0, 1));
return Ok((bytes, target, target_styles, false, 0, 1));
}
let (old, baseline_hit, base_renders) = if let Some(baseline) = base_hit {
(baseline.tape.clone(), true, 0)
@ -1384,45 +1547,51 @@ fn render_layout_payload(
let bytes = layout::encode_layout_patch_tape(
old,
target.clone(),
&document.styles,
&target_styles,
identity,
output.root_metadata,
output.max_bytes,
)?;
Ok((bytes, target, baseline_hit, base_renders, 1))
Ok((bytes, target, target_styles, baseline_hit, base_renders, 1))
} else {
let target =
document.layout_tape(context, root_width_override.then_some(root_width))?;
let bytes = layout::encode_layout_tape(
target.clone(),
&document.styles,
&target_styles,
identity,
output.root_metadata,
output.max_bytes,
)?;
Ok((bytes, target, false, 0, 1))
Ok((bytes, target, target_styles, false, 0, 1))
}
}));
let resolver_lookups =
validation_resolver_lookups.saturating_add(layout::resolver_lookups());
match result {
Ok(Ok((bytes, tape, baseline_hit, base_renders, target_renders))) => RenderedJob {
bytes,
pending: Some(PendingBaseline {
confirmed_identity: pending_identity,
document: Arc::clone(&document),
document_revision: document_target_revision,
tape,
styles: document.styles.clone(),
}),
baseline_hit,
base_renders,
target_renders,
},
Ok(Ok((bytes, tape, styles, baseline_hit, base_renders, target_renders))) => {
RenderedJob {
bytes,
pending: Some(PendingBaseline {
confirmed_identity: pending_identity,
document: document.clone(),
document_revision: document_target_revision,
tape,
styles,
}),
baseline_hit,
base_renders,
target_renders,
resolver_lookups,
}
}
Ok(Err(error)) => RenderedJob {
bytes: encode_error_tape(identity, &error, max_result_bytes),
pending: None,
baseline_hit: false,
base_renders: 0,
target_renders: 0,
resolver_lookups,
},
Err(_) => RenderedJob {
bytes: encode_error_tape(identity, "native layout panicked", max_result_bytes),
@ -1430,6 +1599,7 @@ fn render_layout_payload(
baseline_hit: false,
base_renders: 0,
target_renders: 0,
resolver_lookups,
},
}
}
@ -1464,7 +1634,8 @@ fn render_proof(payload: &[u8]) -> Result<Vec<u8>, String> {
if frame.delay_ms != 0 {
return Err("Native proof render cannot contain delay-ms".to_owned());
}
let prepared = prepare_layout_job(&Arc::new(document), frame, 0, 1)?;
let document = LayoutSource::Full(Arc::new(document));
let prepared = prepare_layout_job(&document, frame, 0, 1)?;
let output = render_layout_payload(
prepared.payload,
SYNC_RENDER_SESSION_ID,
@ -1577,6 +1748,7 @@ fn worker_loop(shared: Arc<Shared>) {
}
state.base_renders += output.base_renders;
state.target_renders += output.target_renders;
state.document_resolver_lookups += output.resolver_lookups;
state.results.insert(
(job.generation, job.key),
ResultEntry {
@ -1994,6 +2166,14 @@ mod tests {
.into_bytes()
}
fn identified_proof_layout_payload(frames: &str) -> Vec<u8> {
let mut payload: serde_json::Value =
serde_json::from_slice(&proof_layout_payload(frames)).unwrap();
payload["document"]["root"]["node-id"] = serde_json::json!(1);
payload["document"]["root"]["node-revision"] = serde_json::json!(7);
serde_json::to_vec(&payload).unwrap()
}
fn replacement_layout_payload(
document_base_revision: u64,
document_target_revision: u64,
@ -2336,6 +2516,79 @@ mod tests {
grandchild.stop(true);
}
#[test]
fn nonzero_document_delta_renders_and_promotes_without_full_reparse() {
let session = Session::new(1, 4, 4, 64 * 1024).unwrap();
let first = identified_proof_layout_payload(
r#"[{"key":1,"viewport-width":80,"viewport-height":10,"root-width":80,"runtime-revision":0,"context-hash":77}]"#,
);
let first_tape = session.render_sync(1, &first).unwrap();
assert!(!tape_patch_p(&first_tape));
assert!(session.confirm(1, 1, 1).unwrap());
let delta = serde_json::to_vec(&serde_json::json!({
"version": 1,
"document-base-revision": 1,
"document-target-revision": 2,
"document-delta": {
"style-base-count": 0,
"styles-append": [],
"property-template-base-count": 0,
"property-template-target-count": 0,
"entries": [{
"node-id": 1,
"expected-revision": 7,
"target-revision": 21,
"slot-patches": [{
"slot": 0,
"local": {
"content": {"lines": [{"clusters": [{
"text": "y", "width": 8, "cjk": false, "space": false
}]}]}
}
}]
}]
},
"frames": [{
"key": 2,
"viewport-width": 80,
"viewport-height": 10,
"root-width": 80,
"patch": true,
"base-viewport-width": 80,
"base-viewport-height": 10,
"base-root-width": 80,
"runtime-revision": 1,
"context-hash": 77
}]
}))
.unwrap();
let delta_tape = session.render_sync(2, &delta).unwrap();
assert!(tape_patch_p(&delta_tape));
let stats = session.stats();
assert_eq!(stats.document_parses, 1);
assert_eq!(stats.document_validations, 1);
assert_eq!(stats.document_full_nodes_parsed, 1);
assert_eq!(stats.document_delta_entries_parsed, 1);
assert_eq!(stats.document_delta_entries_validated, 1);
assert_eq!(stats.document_trie_path_nodes_copied, 17);
assert_eq!(stats.document_delta_input_bytes, delta.len() as u64);
assert!(
stats.document_resolver_lookups >= 3,
"target/base validation and target render lookups must all remain visible"
);
assert!(session.confirm(2, 2, 2).unwrap());
let child = session.fork_confirmed().unwrap();
let retained = retained_layout_payload(
2,
r#"[{"key":3,"viewport-width":80,"viewport-height":10,"root-width":80,"runtime-revision":2,"context-hash":77}]"#,
);
assert!(!child.render_sync(1, &retained).unwrap().is_empty());
session.stop(true);
child.stop(true);
}
#[test]
fn retained_document_reuse_rejects_wrong_or_ambiguous_revision() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();

View File

@ -2372,6 +2372,14 @@ remain retained identities."
session initial-state root))
(next (copy-tree root))
(next-child (car (ebox-tree-node-children next))))
(let ((bootstrap-root
(plist-get (plist-get initial-package :document) :root)))
(should (= (plist-get root :node-id)
(plist-get bootstrap-root :node-id)))
(should (integerp (plist-get bootstrap-root :node-revision)))
;; The Text is fused into its owner and therefore has no addressable
;; child entry in the full wire document.
(should (eq :null (plist-get bootstrap-root :child))))
(setf (ebox-native-reflow-session-styles session)
(plist-get initial-package :styles)
(ebox-native-reflow-session-layout-package session)
@ -2567,6 +2575,434 @@ remain retained identities."
(should (= (plist-get (car controls) :document-base-revision) 10))
(should (= (plist-get (car controls) :document-target-revision) 11)))))
(ert-deftest ebox-native-persistent-index-path-copies-without-changing-base ()
"A retained index update shares the base and leaves its values immutable."
(require 'ebox-native-reflow)
(let* ((base (ebox-native-reflow--persistent-index-put nil 1 'one))
(next (ebox-native-reflow--persistent-index-put base 17 'seventeen)))
(should (eq 'one (ebox-native-reflow--persistent-index-get base 1)))
(should-not (ebox-native-reflow--persistent-index-get base 17))
(should (eq 'one (ebox-native-reflow--persistent-index-get next 1)))
(should (eq 'seventeen
(ebox-native-reflow--persistent-index-get next 17)))
(should-not (eq base next))))
(ert-deftest ebox-native-session-fork-shares-immutable-compiler-roots ()
"Forking does not clone the retained fragment map or persistent indexes."
(require 'ebox-native-reflow)
(let* ((cache (make-hash-table :test 'equal))
(index (ebox-native-reflow--persistent-index-put nil 1 'entry))
(styles (vector 'style))
(session
(ebox-native-reflow--make-session
:handle 'parent :generation 4 :styles styles
:layout-package 'package :layout-fragment-cache cache
:layout-fragment-index index :layout-fragment-revision 8
:layout-style-index index :layout-property-template-index index)))
(cl-letf (((symbol-function 'ebox-native--module-fork-confirmed)
(lambda (_handle) 'child)))
(let ((fork (ebox-native-reflow-fork-session session)))
(should (eq cache
(ebox-native-reflow-session-layout-fragment-cache fork)))
(should (eq index
(ebox-native-reflow-session-layout-fragment-index fork)))
(should (eq styles (ebox-native-reflow-session-styles fork)))
(should (= 8
(ebox-native-reflow-session-layout-fragment-revision
fork)))))))
(ert-deftest ebox-native-failed-delta-keeps-fork-and-parent-indexes-unchanged ()
"A rejected candidate cannot publish its path-copied compiler index."
(require 'ebox-native-reflow)
(let* ((cache (make-hash-table :test 'equal))
(base-index (ebox-native-reflow--persistent-index-put nil 1 'old))
(next-index
(ebox-native-reflow--persistent-index-put base-index 1 'new))
(old (list :document '(:version 2) :document-revision 4
:styles [] :property-templates []))
(next (copy-sequence old))
(parent
(ebox-native-reflow--make-session
:handle 'parent :layout-package old :styles []
:layout-fragment-cache cache :layout-fragment-index base-index)))
(plist-put next :document-revision 5)
(plist-put next :document-delta
'(:style-base-count 0 :styles-append []
:property-template-base-count 0
:property-template-target-count 0 :entries []))
(plist-put next :native-fragment-index next-index)
(cl-letf (((symbol-function 'ebox-native--module-fork-confirmed)
(lambda (_handle) 'child)))
(let ((fork (ebox-native-reflow-fork-session parent)))
(cl-letf (((symbol-function
'ebox-native-reflow--compile-retained-layout-package)
(lambda (&rest _) next))
((symbol-function 'ebox-native--module-render-session-frame)
(lambda (&rest _) (error "reject delta"))))
(should-error
(ebox-native-reflow-execute-session-sync
fork 'node '(:key 1 :viewport-width 80 :viewport-height 10)
nil 'state))
(should (eq base-index
(ebox-native-reflow-session-layout-fragment-index fork)))
(should (eq base-index
(ebox-native-reflow-session-layout-fragment-index parent)))
(should (eq cache
(ebox-native-reflow-session-layout-fragment-cache fork))))))))
(ert-deftest ebox-native-accepted-delta-invalidates-stale-full-cache ()
"A later full fallback cannot resurrect pre-delta legacy fragments."
(require 'ebox-native-reflow)
(let* ((cache (make-hash-table :test 'equal))
(base-index (ebox-native-reflow--persistent-index-put nil 1 'old))
(next-index
(ebox-native-reflow--persistent-index-put base-index 1 'new))
(old (list :document '(:version 2) :document-revision 4
:styles [] :property-templates []))
(next (copy-sequence old))
(session
(ebox-native-reflow--make-session
:handle 'test :generation 0 :layout-package old :styles []
:layout-fragment-cache cache :layout-fragment-index base-index)))
(puthash 1 'stale-fragment cache)
(plist-put next :document-revision 5)
(plist-put next :document-delta
'(:style-base-count 0 :styles-append []
:property-template-base-count 0
:property-template-target-count 0 :entries []))
(plist-put next :native-fragment-index next-index)
(plist-put next :native-fragment-revision 9)
(cl-letf (((symbol-function
'ebox-native-reflow--compile-retained-layout-package)
(lambda (&rest _) next))
((symbol-function 'ebox-native--module-render-session-frame)
(lambda (&rest _) 'native-frame))
((symbol-function 'ebox-native-reflow--materialize-module-frame)
(lambda (&rest _) '(:rendered "ok"))))
(ebox-native-reflow-execute-session-sync
session 'node '(:key 1 :viewport-width 80 :viewport-height 10)
nil 'state)
(should-not
(ebox-native-reflow-session-layout-fragment-cache session))
(should (eq next-index
(ebox-native-reflow-session-layout-fragment-index session)))
(should (= 9
(ebox-native-reflow-session-layout-fragment-revision
session)))
;; Model the next unsupported B update. The current source contains A's
;; accepted value while the persistent index is deliberately stale; the
;; invalidated legacy cache forces a complete compile from current state.
(let* ((input
(ebox-test-box (ebox-test-text "A") :bgcolor "#00ff00"))
(root (ebox-test-root input))
(_ids (ebox--runtime-node-ids root))
(full
(ebox-native-reflow--compile-retained-layout-package-full
session
(list :native-node-postorder
(ebox-native-reflow--retained-layout-postorder root)
:native-topology-stable-p nil
:source-index (ebox-test-source-index input))
root))
(document-root (plist-get (plist-get full :document) :root))
(style-id (plist-get document-root :background-style)))
(should
(equal '(:background "#00ff00")
(plist-get (aref (plist-get full :styles) style-id)
:face)))))))
(ert-deftest ebox-native-node-delta-is-local-and-bumps-ancestors ()
"A local change patches one owner and only revises its retained ancestor."
(require 'ebox-native-reflow)
(let* ((leaf-fragment
'(:type "box" :background-style :null :child :null
:node-id 2 :node-revision 3))
(root-fragment
(list :type "box" :background-style :null :child leaf-fragment
:node-id 1 :node-revision 4))
(leaf-entry (list :fragment leaf-fragment :revision 3))
(root-entry (list :fragment root-fragment :revision 4))
(index (ebox-native-reflow--persistent-index-put nil 1 root-entry))
(_index (setq index
(ebox-native-reflow--persistent-index-put
index 2 leaf-entry)))
(style-index
(ebox-native-reflow--persistent-index-put
nil '(:mode add :face (:background "red")) 0))
(package
(list :document '(:version 2) :document-revision 7
:styles [] :property-templates []))
(session
(ebox-native-reflow--make-session
:handle 'test :generation 0 :styles [] :layout-package package
:layout-fragment-index index :layout-fragment-revision 4
:layout-style-index style-index))
(nodes (make-hash-table :test 'equal))
(parents (make-hash-table :test 'equal))
(state
(list :node-table nodes :parent-table parents
:native-topology-stable-p t
:native-touched-node-ids '(2 1)
:native-local-dirty-entries
'((:node-id 2 :dirty-kind paint
:changed-keys (:background-color))))))
(puthash 1 '(:node-id 1) nodes)
(puthash 2 '(:node-id 2) nodes)
(puthash 2 1 parents)
(cl-letf (((symbol-function 'ebox--current-display-signature)
(lambda () 'display))
((symbol-function 'ebox-native-reflow--compile-delta-slots)
(lambda (_node _old)
(vector
'(:type "box" :background-style 0 :child :null)))))
(let* ((next
(ebox-native-reflow--compile-retained-layout-delta
session state 'root))
(delta (plist-get next :document-delta))
(entries (plist-get delta :entries))
(leaf (aref entries 0))
(root (aref entries 1)))
(should (= 8 (plist-get next :document-revision)))
(should (= 0 (plist-get delta :style-base-count)))
(should (= 2 (length entries)))
(should (equal [(:slot 0 :local (:background-style 0))]
(plist-get leaf :slot-patches)))
(should-not (plist-member root :slot-patches))
(should (= 5 (plist-get leaf :target-revision)))
(should (= 6 (plist-get root :target-revision)))))))
(ert-deftest ebox-native-node-delta-deduplicates-multiple-leaf-closures ()
"Multiple changed leaves produce one entry each and one shared ancestor."
(require 'ebox-native-reflow)
(let ((index nil)
(nodes (make-hash-table :test 'equal))
(parents (make-hash-table :test 'equal)))
(dolist (pair '((1 . 3) (2 . 1) (3 . 2)))
(setq index
(ebox-native-reflow--persistent-index-put
index (car pair)
(list :fragment
'(:type "box" :background-style :null :child :null)
:revision (cdr pair))))
(puthash (car pair) (list :node-id (car pair)) nodes))
(puthash 2 1 parents)
(puthash 3 1 parents)
(let* ((package (list :document '(:version 2) :document-revision 2
:styles [] :property-templates []))
(session
(ebox-native-reflow--make-session
:handle 'test :layout-package package
:layout-fragment-index index :layout-fragment-revision 3))
(state
(list :node-table nodes :parent-table parents
:native-topology-stable-p t
:native-touched-node-ids '(2 1 3 1)
:native-local-dirty-entries
'((:node-id 2 :changed-keys (:background-color))
(:node-id 3 :changed-keys (:background-color))))))
(cl-letf (((symbol-function 'ebox--current-display-signature)
(lambda () 'display))
((symbol-function 'ebox-native-reflow--compile-delta-slots)
(lambda (node _old)
(vector
(list :type "box" :background-style
(plist-get node :node-id) :child :null)))))
(let* ((next
(ebox-native-reflow--compile-retained-layout-delta
session state 'root))
(entries
(plist-get (plist-get next :document-delta) :entries)))
(should (= 3 (length entries)))
(should (equal '(2 1 3)
(mapcar (lambda (entry)
(plist-get entry :node-id))
(append entries nil))))
(should (plist-member (aref entries 0) :slot-patches))
(should-not (plist-member (aref entries 1) :slot-patches))
(should (plist-member (aref entries 2) :slot-patches)))))))
(ert-deftest ebox-native-fused-text-delta-resolves-to-box-owner ()
"A fused text id addresses its containing Box rather than a hidden node."
(require 'ebox-native-reflow)
(let ((nodes (make-hash-table :test 'equal))
(parents (make-hash-table :test 'equal))
(index (ebox-native-reflow--persistent-index-put nil 10 'owner)))
(puthash 10
(list :node-id 10 :ebox-kind 'box
:ebox-layout-config
(ebox-normal-layout-create))
nodes)
(puthash 11 '(:node-id 11 :ebox-kind text) nodes)
(puthash 11 10 parents)
(should (= 10
(ebox-native-reflow--delta-owner-id
(list :node-table nodes :parent-table parents) 11 index)))))
(ert-deftest ebox-native-flex-item-metadata-change-uses-full-input ()
"N1 does not misrepresent Flex item metadata as a local scalar patch."
(require 'ebox-native-reflow)
(let ((session
(ebox-native-reflow--make-session
:handle 'test :layout-package 'old
:layout-fragment-index
(ebox-native-reflow--persistent-index-put
nil 1 '(:fragment (:type "box" :child :null) :revision 1))))
(state
'(:native-topology-stable-p t :native-touched-node-ids (1)
:native-local-dirty-entries
((:node-id 1 :dirty-kind geometry
:changed-keys (:flex-grow)))))
full-called)
(cl-letf (((symbol-function
'ebox-native-reflow--compile-retained-layout-package-full)
(lambda (&rest _)
(setq full-called t)
'full))
((symbol-function 'ebox-native-reflow--compile-delta-slots)
(lambda (&rest _)
(ert-fail "Flex item metadata reached local delta"))))
(should (eq 'full
(ebox-native-reflow--compile-retained-layout-package
session state 'node)))
(should full-called))))
(ert-deftest ebox-native-flex-child-content-change-uses-full-input ()
"Child content may alter retained Flex edge measurement in N1."
(require 'ebox-native-reflow)
(let ((nodes (make-hash-table :test 'equal))
(parents (make-hash-table :test 'equal))
(index
(ebox-native-reflow--persistent-index-put
nil 2 '(:fragment (:type "box" :content [] :child :null)
:revision 3)))
full-called)
(puthash 1 '(:node-id 1 :ebox-type flex) nodes)
(puthash 2
(list :node-id 2 :ebox-type 'box :ebox-kind 'box
:ebox-layout-config (ebox-normal-layout-create))
nodes)
(puthash 3 '(:node-id 3 :ebox-type box :ebox-kind text) nodes)
(puthash 2 1 parents)
(puthash 3 2 parents)
(let ((session
(ebox-native-reflow--make-session
:handle 'test :layout-package 'old
:layout-fragment-index index))
(state
(list :node-table nodes :parent-table parents
:native-topology-stable-p t
:native-touched-node-ids '(3 2 1)
:native-local-dirty-entries
'((:node-id 3 :dirty-kind content
:changed-keys (:content))))))
(cl-letf (((symbol-function
'ebox-native-reflow--compile-retained-layout-package-full)
(lambda (&rest _)
(setq full-called t)
'full))
((symbol-function 'ebox-native-reflow--compile-delta-slots)
(lambda (&rest _)
(ert-fail "Flex child content reached local delta"))))
(should (eq 'full
(ebox-native-reflow--compile-retained-layout-package
session state 'node)))
(should full-called)))))
(ert-deftest ebox-native-default-axis-child-change-keeps-local-input ()
"Default Row/Column edges carry no derived Flex item metadata."
(require 'ebox-native-reflow)
(let ((nodes (make-hash-table :test 'equal))
(parents (make-hash-table :test 'equal)))
(puthash 1
(list :node-id 1 :ebox-type 'box :ebox-kind 'box
:ebox-layout-config (ebox-column-layout-create))
nodes)
(puthash 2
(list :node-id 2 :ebox-type 'box :ebox-kind 'box
:ebox-layout-config (ebox-normal-layout-create))
nodes)
(puthash 3 '(:node-id 3 :ebox-type box :ebox-kind text) nodes)
(puthash 2 1 parents)
(puthash 3 2 parents)
(should-not
(ebox-native-reflow--delta-flex-edge-change-p
(list :node-table nodes :parent-table parents
:native-local-dirty-entries
'((:node-id 3 :dirty-kind geometry
:changed-keys (:content))))))))
(ert-deftest ebox-native-surface-overrides-carry-local-dirty-entries ()
"The incremental producer preserves exact local dirtiness to native input."
(let* ((dirty '((:node-id 7 :dirty-kind paint
:changed-keys (:background-color))))
(candidate (list :runtime-revision 3
:native-topology-stable-p t
:native-touched-node-ids '(7 1)
:native-local-dirty-entries dirty))
(overrides
(ebox-incremental--surface-state-overrides
nil '(:display-signature display) candidate 'native-frame)))
(should (equal dirty
(plist-get overrides :native-local-dirty-entries)))))
(ert-deftest ebox-native-wide-owner-local-compile-does-not-enumerate-children ()
"A scalar slot update never walks a stable owner's unchanged children."
(require 'ebox-native-reflow)
(let ((node (list :ebox-type 'box :ebox-kind 'box
:ebox-layout-config (ebox-flex-layout-create)))
(old '(:type "box" :child (:type "flex" :items [a b c]))))
(cl-letf (((symbol-function 'ebox-tree-node-children)
(lambda (&rest _)
(ert-fail "stable delta enumerated unchanged children")))
((symbol-function 'ebox-native-reflow--compile-flex-inner)
(lambda (_props items &rest _)
(should-not items)
'(:type "flex" :items [])))
((symbol-function 'ebox-native-reflow--compile-box)
(lambda (_box child &rest _)
(list :type "box" :child child))))
(let ((slots (ebox-native-reflow--compile-delta-slots node old)))
(should (= 2 (length slots)))
(should (equal [] (plist-get (aref slots 1) :items)))))))
(ert-deftest ebox-native-retained-sync-sends-document-delta-without-document ()
"A supported retained compile sends only its node delta and revisions."
(require 'ebox-native-reflow)
(let* ((old (list :document '(:version 2) :document-revision 4
:styles [] :property-templates []))
(delta '(:style-base-count 0 :styles-append []
:property-template-base-count 0
:property-template-target-count 0 :entries []))
(next (copy-sequence old))
(session
(ebox-native-reflow--make-session
:handle 'test :generation 0 :styles [] :layout-package old))
control)
(plist-put next :document-revision 5)
(plist-put next :document-delta delta)
(cl-letf (((symbol-function
'ebox-native-reflow--compile-retained-layout-package)
(lambda (&rest _) next))
((symbol-function 'ebox-native--module-render-session-frame)
(lambda (_handle _generation payload)
(setq control
(json-parse-string payload :object-type 'plist
:array-type 'array))
'native-frame))
((symbol-function 'ebox-native-reflow--materialize-module-frame)
(lambda (&rest _) '(:rendered "ok"))))
(ebox-native-reflow-execute-session-sync
session 'node
'(:key 1 :viewport-width 80 :viewport-height 10) nil 'state)
(should-not (plist-member control :document))
(should (equal delta (plist-get control :document-delta)))
(should (= 4 (plist-get control :document-base-revision)))
(should (= 5 (plist-get control :document-target-revision)))
(should-not
(plist-member
(ebox-native-reflow-session-layout-package session)
:document-delta)))))
(provide 'ebox-commit-tests)
;;; ebox-commit-tests.el ends here

View File

@ -411,6 +411,55 @@
:authority ebox :lifetime candidate-until-final-accept
:rollback private-session-discard :rebuild native-frame-stage
:cleanup confirm-or-rollback)
(:id candidate/native-local-dirty-entries
:storage (:state-key :native-local-dirty-entries)
:proposed-category generation-fact :owner ebox-native-commit
:authority ebox :lifetime candidate-until-native-input
:rollback candidate-discard :rebuild prepared-dirty-set
:cleanup native-frame-consume-or-downgrade)
(:id native-session/layout-fragment-index
:storage (:struct-slot ebox-native-reflow-session layout-fragment-index)
:proposed-category generation-bound-mutable :owner ebox-native-reflow
:authority ebox :lifetime private-native-session
:rollback private-session-discard :rebuild full-bootstrap-or-path-copy
:cleanup native-session-release)
(:id native-session/layout-style-index
:storage (:struct-slot ebox-native-reflow-session layout-style-index)
:proposed-category generation-bound-mutable :owner ebox-native-reflow
:authority ebox :lifetime private-native-session
:rollback private-session-discard :rebuild full-bootstrap-or-style-append
:cleanup native-session-release)
(:id native-session/layout-property-template-index
:storage
(:struct-slot ebox-native-reflow-session layout-property-template-index)
:proposed-category generation-bound-mutable :owner ebox-native-reflow
:authority ebox :lifetime private-native-session
:rollback private-session-discard :rebuild full-bootstrap
:cleanup native-session-release)
(:id native-compile/style-index
:storage (:global ebox-native-reflow--compile-style-index)
:proposed-category generation-fact :owner ebox-native-reflow
:authority ebox :lifetime dynamic-native-compile
:rollback dynamic-binding-unwind :rebuild retained-session-root
:cleanup dynamic-binding-unwind)
(:id native-compile/property-template-index
:storage (:global ebox-native-reflow--compile-property-template-index)
:proposed-category generation-fact :owner ebox-native-reflow
:authority ebox :lifetime dynamic-native-compile
:rollback dynamic-binding-unwind :rebuild retained-session-root
:cleanup dynamic-binding-unwind)
(:id native-package/fragment-index
:storage (:state-key :native-fragment-index)
:proposed-category generation-fact :owner ebox-native-reflow
:authority ebox :lifetime native-call-until-success
:rollback package-discard :rebuild path-copied-session-root
:cleanup strip-from-confirmed-package)
(:id native-package/style-index
:storage (:state-key :native-style-index)
:proposed-category generation-fact :owner ebox-native-reflow
:authority ebox :lifetime native-call-until-success
:rollback package-discard :rebuild style-append-path-copy
:cleanup strip-from-confirmed-package)
(:id process/scroll-global-state
:storage (:global ebox--scroll-global-state)
:proposed-category generation-bound-mutable :owner ebox-incremental

View File

@ -466,18 +466,20 @@ exclusion freshness, but only retained/container evidence is coverage-gated."
'generation-bound-mutable-authority))))
(ert-deftest ebox-state-contract-excludes-native-compile-scratch-from-authority ()
"Compile-local template ids are not mislabeled as native session authority."
(should-not
(memq 'ebox-native-reflow--compile-property-template-ids
"Compile-local indexes are not mislabeled as native session authority."
(let ((authority-storage
(plist-get (ebox-state-contract-record 'native-runtime-authority)
:storage)))
(let ((exclusion
(cl-find 'ebox-native-reflow--compile-property-template-ids
ebox-state-contract-source-scan-exclusions
:key (lambda (entry) (plist-get entry :symbol)))))
(should exclusion)
(should (eq (plist-get exclusion :reason)
'dynamically-bound-compile-local-scratch))))
(dolist (symbol '(ebox-native-reflow--compile-property-template-ids
ebox-native-reflow--compile-property-template-index
ebox-native-reflow--compile-style-index))
(should-not (memq symbol authority-storage))
(let ((exclusion
(cl-find symbol ebox-state-contract-source-scan-exclusions
:key (lambda (entry) (plist-get entry :symbol)))))
(should exclusion)
(should (eq (plist-get exclusion :reason)
'dynamically-bound-compile-local-scratch))))))
(ert-deftest ebox-state-contract-tp-custody-is-opaque ()
"The inventory separates current whole-state custody from its M2a target."

View File

@ -1656,17 +1656,25 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(plist-get (tp-surface-client-state surface)
:native-sync-session))
(should (eq (plist-get report :projection-kind) 'native-frame))
;; A style-table change cannot consume the prior confirmed
;; document and therefore remains a truthful full frame.
(should (eq (plist-get report :native-frame-kind) 'full))
;; A root paint delta and appended styles preserve the old
;; registry prefix and publish against the confirmed tape.
(should (eq (plist-get report :native-frame-kind) 'patch))
(should next-session)
(should-not (eq next-session winning-session))
(should (ebox-native-reflow-session-released-p winning-session))
(should-not (ebox-native-reflow-session-released-p
next-session))
(should
(equal-including-properties
(with-current-buffer buffer
(buffer-substring (point-min) (point-max)))
(ebox-surface-test--render-runtime
(tp-surface-client-state surface))))
(let ((stats (ebox-native-reflow-stats next-session)))
(should (= (plist-get stats :document-parses) 1))
(should (= (plist-get stats :document-validations) 1))
(should (= (plist-get stats :document-parses) 0))
(should (= (plist-get stats :document-validations) 0))
(should (= (plist-get stats :document-delta-entries-parsed) 1))
(should (= (plist-get stats :document-delta-entries-validated) 1))
(should (= (plist-get stats :document-reuses) 0))))))
(when (buffer-live-p buffer)
(kill-buffer buffer))
@ -1725,6 +1733,108 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-native-node-delta-survives-viewport-and-repeated-commits ()
"Real local commits preserve earlier edits across viewport document reuse."
(skip-unless (ebox-native-reflow-layout-ready-p))
(ebox-surface-test--reset-render-state)
(let ((buffer (generate-new-buffer " *ebox-native-delta-history*"))
(ebox-viewport-width 320)
(ebox-viewport-height 40)
(ebox-runtime-idle-prewarm nil)
(ebox-runtime-idle-reflow-cache-prewarm nil))
(unwind-protect
(cl-labels
((item
(index text)
(ebox-test-box
:key index :source-identity index :width '(8) :height 1
(ebox-test-text text :key (+ 100 index)
:source-identity (+ 100 index))))
(source
(count)
(apply #'ebox-test-column
(append (list :key 'delta-history
:source-identity 'delta-history
:width '(viewport))
(cl-loop for index below count
collect (item index "A")))))
(ids
(state)
(let (result)
(ebox-surface-test--walk-runtime
(plist-get state :root-node)
(lambda (node) (push (plist-get node :node-id) result)))
(nreverse result))))
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
(lambda () nil)))
(ebox-render-to-buffer buffer (source 15)))
(ebox-commit buffer (source 16))
(let* ((surface
(with-current-buffer buffer ebox-surface--buffer-surface))
(baseline-ids (ids (tp-surface-client-state surface)))
(original-send
(symbol-function 'ebox-native--module-render-session-frame))
(expected-texts (make-vector 16 "A"))
previous-document-revision events)
(should (plist-get (tp-surface-client-state surface)
:native-sync-confirmed-p))
(cl-letf
(((symbol-function 'ebox-native--module-render-session-frame)
(lambda (handle generation control)
(push (json-parse-string control :object-type 'plist
:array-type 'array)
events)
(funcall original-send handle generation control))))
(dolist (step '((4 "B") viewport (12 "C") (4 "D")))
(setq events nil)
(if (eq step 'viewport)
(progn
(setq ebox-viewport-width 360)
(ebox-rerender-buffer-with-context buffer 360 40))
(let ((candidate (ebox-candidate-begin buffer)))
(ebox-candidate-replace-host-ref
candidate (car step) (item (car step) (cadr step)))
(ebox-commit buffer candidate)
(aset expected-texts (car step) (cadr step))))
(ert-info ((format "Native delta history step %S" step))
(should (= 1 (length events))))
(let* ((control (car events))
(base (plist-get control :document-base-revision))
(target (plist-get control :document-target-revision))
(state (tp-surface-client-state surface))
(session (plist-get state :native-sync-session)))
(should-not (plist-get control :document))
(when previous-document-revision
(should (= base previous-document-revision)))
(if (eq step 'viewport)
(progn
(should-not (plist-get control :document-delta))
(should (= base target)))
(should (plist-get control :document-delta))
(should (= (1+ base) target)))
(setq previous-document-revision target)
(should (equal baseline-ids (ids state)))
(should
(equal (mapconcat #'identity expected-texts "")
(with-current-buffer buffer
(replace-regexp-in-string
"[[:space:]]" ""
(buffer-substring-no-properties
(point-min) (point-max))))))
(should (plist-get state :native-sync-confirmed-p))
(should-not (plist-get state :native-sync-pending))
(should-not
(plist-get
(ebox-native-reflow-session-layout-package session)
:document-delta))
(should
(equal-including-properties
(with-current-buffer buffer
(buffer-substring (point-min) (point-max)))
(ebox-surface-test--render-runtime state))))))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-viewport-reflow-retains-final-sized-flex-child-fragments ()
"Viewport reflow should reuse final-sized Flex child fragments exactly."
(ebox-surface-test--reset-render-state)