Retain native node deltas across confirmed sessions
Some checks are pending
CI / test (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run

This commit is contained in:
Kinneyzhang 2026-09-05 08:44:29 +08:00
parent bc4516e171
commit b824467791
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-put overrides :native-inherited-dirty-node-ids
(plist-get candidate-state (plist-get candidate-state
:native-inherited-dirty-node-ids))) :native-inherited-dirty-node-ids)))
(setq overrides
(plist-put overrides :native-local-dirty-entries
(plist-get candidate-state
:native-local-dirty-entries)))
(setq overrides (setq overrides
(plist-put (plist-put
overrides :native-topology-stable-p overrides :native-topology-stable-p
@ -10963,9 +10967,10 @@ fresh by the mixed projection."
((eq projection-kind 'native-frame) ((eq projection-kind 'native-frame)
(append (append
(ebox-incremental--viewport-state-overrides (ebox-incremental--viewport-state-overrides
candidate-state t) candidate-state t)
(list :native-topology-stable-p t (list :native-topology-stable-p t
:native-touched-node-ids nil :native-touched-node-ids nil
:native-local-dirty-entries nil
:native-removed-node-ids nil))) :native-removed-node-ids nil)))
((memq projection-kind ((memq projection-kind
'(viewport-reflow viewport-reflow-mixed-scroll)) '(viewport-reflow viewport-reflow-mixed-scroll))

View File

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

View File

@ -95,7 +95,7 @@
(defvar ebox--flex-content-min-width-table) (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.") "Version tuple shared by the native module, layout IR, and render tape.")
(defconst ebox-native-reflow--minimum-rust-version "1.82.0" (defconst ebox-native-reflow--minimum-rust-version "1.82.0"
@ -222,7 +222,10 @@ module while loading the package."
styles styles
layout-package layout-package
layout-fragment-cache layout-fragment-cache
layout-fragment-index
layout-fragment-revision layout-fragment-revision
layout-style-index
layout-property-template-index
readiness-process readiness-process
readiness-preparation readiness-preparation
released-p) released-p)
@ -286,9 +289,74 @@ module while loading the package."
(defvar ebox-native-reflow--compile-styles nil (defvar ebox-native-reflow--compile-styles nil
"Dynamically bound ordered style registry for one Layout IR compile.") "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 (defvar ebox-native-reflow--compile-property-templates nil
"Dynamically bound ordered opaque text-property registry.") "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 (defvar ebox-native-reflow--compile-property-template-ids nil
"Dynamically bound opaque property-template lookup table.") "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 (condition-case err
(progn (progn
(setf (ebox-native-reflow-session-styles fork) (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 fork)
(ebox-native-reflow-session-layout-package session) (ebox-native-reflow-session-layout-package session)
(ebox-native-reflow-session-layout-fragment-cache fork) (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 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) fork)
(error (error
(ebox-native-reflow-release-session fork) (ebox-native-reflow-release-session fork)
@ -1409,7 +1483,11 @@ copied separately; no pending frame or registered Rust document is inherited."
:properties owned-properties)) :properties owned-properties))
(missing (make-symbol "missing-property-template")) (missing (make-symbol "missing-property-template"))
(existing (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) ebox-native-reflow--compile-property-template-ids)
(gethash (gethash
entry ebox-native-reflow--compile-property-template-ids 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 (or (cl-position
entry ebox-native-reflow--compile-property-templates entry ebox-native-reflow--compile-property-templates
:test #'equal) :test #'equal)
missing)))) missing)))))
(if (not (eq existing missing)) (if (not (eq existing missing))
existing 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 (setq ebox-native-reflow--compile-property-templates
(append ebox-native-reflow--compile-property-templates (append ebox-native-reflow--compile-property-templates
(list entry))) (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 (when (hash-table-p
ebox-native-reflow--compile-property-template-ids) ebox-native-reflow--compile-property-template-ids)
(puthash (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) (defun ebox-native-reflow--register-style (mode face)
"Return the stable numeric id for a MODE and FACE operation." "Return the stable numeric id for a MODE and FACE operation."
(let* ((entry (list :mode mode :face face)) (let* ((entry (list :mode mode :face face))
(existing (cl-position entry ebox-native-reflow--compile-styles (missing (make-symbol "missing-native-style"))
:test #'equal))) (existing
(or existing (if ebox-native-reflow--compile-retained-registry-p
(prog1 (length ebox-native-reflow--compile-styles) (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 (setq ebox-native-reflow--compile-styles
(append 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) (defun ebox-native-reflow--compile-add-face-style (face)
"Register additive FACE and return its id, or nil." "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 (plist-put replacement :document-revision
(1+ (or (plist-get old :document-revision) 0))))))) (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) (defun ebox-native-reflow--buffer-display-signature (buffer)
"Return BUFFER's live canonical Surface display capability." "Return BUFFER's live canonical Surface display capability."
(with-current-buffer buffer (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))) (gethash child flex-content-min-widths)))
(ebox-native-reflow--retained-layout-children node)))))) (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) (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))) (let ((postorder (plist-get state :native-node-postorder)))
(unless (and (vectorp postorder) (> (length postorder) 0)) (unless (and (vectorp postorder) (> (length postorder) 0))
(setq postorder (setq postorder
@ -2632,7 +2740,9 @@ per call, and no call recursively visits the captured Ebox tree."
(setq fragment (setq fragment
(ebox-native-reflow--compile-scene-node (ebox-native-reflow--compile-scene-node
current fragments flex-content-min-widths) current fragments flex-content-min-widths)
fragment (plist-put fragment :node-id node-id)
revision (1+ revision) revision (1+ revision)
fragment (plist-put fragment :node-revision revision)
entry (list :signature (copy-tree signature) entry (list :signature (copy-tree signature)
:fragment fragment :fragment fragment
:revision revision))) :revision revision)))
@ -2658,10 +2768,354 @@ per call, and no call recursively visits the captured Ebox tree."
candidate))) candidate)))
(setf (ebox-native-reflow-session-layout-fragment-cache session) (setf (ebox-native-reflow-session-layout-fragment-cache session)
new-cache 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) (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))))) 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) (defun ebox-native-reflow-compile-layout-ir (node)
"Compile NODE to one complete versioned native layout document." "Compile NODE to one complete versioned native layout document."
(plist-get (ebox-native-reflow--compile-layout-package node) :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 (defun ebox-native-reflow--layout-control-json
(document frames &optional document-base-revision (document frames &optional document-base-revision
document-target-revision) document-target-revision document-delta)
"Return strict control JSON for optional layout DOCUMENT and FRAMES. "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) (unless (and (listp frames) frames)
(error "Native reflow requires at least one layout frame")) (error "Native reflow requires at least one layout frame"))
(let ((control (let ((control
@ -2792,6 +3247,8 @@ DOCUMENT-BASE-REVISION and DOCUMENT-TARGET-REVISION bind retained input."
document-target-revision))) document-target-revision)))
(when document (when document
(setq control (plist-put control :document 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))) (ebox-native-reflow--serialize-layout-control control)))
(defun ebox-native-reflow-submit-layout (defun ebox-native-reflow-submit-layout
@ -2807,9 +3264,12 @@ LAYOUT-PACKAGE may reuse a caller-validated document for the same NODE."
node node
(ebox-native-reflow-session-styles session) (ebox-native-reflow-session-styles session)
(plist-get old-package :property-templates)))) (plist-get old-package :property-templates))))
(document-delta (plist-get candidate-package :document-delta))
(package (package
(ebox-native-reflow--reuse-exact-layout-package (if document-delta
old-package candidate-package)) candidate-package
(ebox-native-reflow--reuse-exact-layout-package
old-package candidate-package)))
(register-layout-p (register-layout-p
(not (eq package (not (eq package
old-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) (ebox-native-reflow--live-handle session)
generation generation
(ebox-native-reflow--layout-control-json (ebox-native-reflow--layout-control-json
(and register-layout-p (plist-get package :document)) frames (and register-layout-p (not document-delta)
base-revision target-revision)))) (plist-get package :document))
frames base-revision target-revision document-delta))))
(setf (ebox-native-reflow-session-generation session) generation) (setf (ebox-native-reflow-session-generation session) generation)
(setf (ebox-native-reflow-session-styles session) (setf (ebox-native-reflow-session-styles session)
(plist-get package :styles)) (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)) accepted))
(defun ebox-native-reflow--tape-cursor (payload &optional position) (defun ebox-native-reflow--tape-cursor (payload &optional position)
@ -3968,13 +4430,16 @@ root effect metadata required by a thin host commit."
(plist-get (plist-get
(ebox-native-reflow-session-layout-package session) (ebox-native-reflow-session-layout-package session)
:property-templates))))) :property-templates)))))
(document-delta (plist-get candidate-package :document-delta))
(package (package
(ebox-native-reflow--reuse-exact-layout-package (if document-delta
old-package candidate-package)) candidate-package
(ebox-native-reflow--reuse-exact-layout-package
old-package candidate-package)))
(generation (1+ (ebox-native-reflow-session-generation session))) (generation (1+ (ebox-native-reflow-session-generation session)))
(control-frame (copy-sequence frame)) (control-frame (copy-sequence frame))
(native-key (or (plist-get control-frame :key) 1)) (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)) (base-revision (or (plist-get old-package :document-revision) 0))
(target-revision (or (plist-get package :document-revision) (target-revision (or (plist-get package :document-revision)
(1+ base-revision)))) (1+ base-revision))))
@ -3986,12 +4451,22 @@ root effect metadata required by a thin host commit."
generation generation
(ebox-native-reflow--layout-control-json (ebox-native-reflow--layout-control-json
(unless reuse-document-p (plist-get package :document)) (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))) package control-frame generation)))
(setf (ebox-native-reflow-session-generation session) 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) (ebox-native-reflow-session-styles session)
(plist-get package :styles)) (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))) result)))
(defun ebox-native-reflow-render-proof-sync (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 (:symbol ebox-native-reflow--compile-property-template-ids
:reason dynamically-bound-compile-local-scratch :reason dynamically-bound-compile-local-scratch
:evidence let-bound-per-compile-and-never-committed) :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 (:symbol ebox--render-source-generations
:reason dynamically-bound-render-proof-input :reason dynamically-bound-render-proof-input
:evidence let-bound-to-candidate-and-prior-generations-for-one-plan) :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) nargs;
(void) args; (void) args;
(void) data; (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)); 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; mod layout;
use layout::{ use layout::{
encode_error_tape, LayoutContext, LayoutDocument, LayoutTape, TapeIdentity, TapeOutputOptions, encode_error_tape, DocumentDelta, LayoutContext, LayoutDocument, LayoutTape, RetainedDocument,
MAX_LAYOUT_DIMENSION, MIN_TAPE_BYTES, TapeIdentity, TapeOutputOptions, MAX_LAYOUT_DIMENSION, MIN_TAPE_BYTES,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::{HashMap, HashSet, VecDeque};
@ -324,6 +324,8 @@ struct ControlBatch {
version: u32, version: u32,
#[serde(default)] #[serde(default)]
document: Option<LayoutDocument>, document: Option<LayoutDocument>,
#[serde(default, rename = "document-delta")]
document_delta: Option<DocumentDelta>,
#[serde(default, rename = "document-base-revision")] #[serde(default, rename = "document-base-revision")]
document_base_revision: Option<u64>, document_base_revision: Option<u64>,
#[serde(default, rename = "document-target-revision")] #[serde(default, rename = "document-target-revision")]
@ -375,7 +377,7 @@ struct ControlFrame {
enum JobPayload { enum JobPayload {
Echo(Vec<u8>), Echo(Vec<u8>),
Layout { Layout {
document: Arc<LayoutDocument>, document: LayoutSource,
context: LayoutContext, context: LayoutContext,
root_width: i64, root_width: i64,
root_width_override: bool, root_width_override: bool,
@ -388,9 +390,43 @@ enum JobPayload {
root_metadata: bool, root_metadata: bool,
document_base_revision: u64, document_base_revision: u64,
document_target_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)] #[derive(Clone, Debug)]
struct BaselineIdentity { struct BaselineIdentity {
context: LayoutContext, context: LayoutContext,
@ -404,7 +440,7 @@ struct BaselineIdentity {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct ConfirmedBaseline { struct ConfirmedBaseline {
identity: BaselineIdentity, identity: BaselineIdentity,
document: Arc<LayoutDocument>, document: LayoutSource,
document_revision: u64, document_revision: u64,
tape: LayoutTape, tape: LayoutTape,
styles: Vec<layout::StyleTemplate>, styles: Vec<layout::StyleTemplate>,
@ -413,7 +449,7 @@ struct ConfirmedBaseline {
#[derive(Debug)] #[derive(Debug)]
struct PendingBaseline { struct PendingBaseline {
confirmed_identity: BaselineIdentity, confirmed_identity: BaselineIdentity,
document: Arc<LayoutDocument>, document: LayoutSource,
document_revision: u64, document_revision: u64,
tape: LayoutTape, tape: LayoutTape,
styles: Vec<layout::StyleTemplate>, styles: Vec<layout::StyleTemplate>,
@ -431,6 +467,20 @@ struct RenderedJob {
baseline_hit: bool, baseline_hit: bool,
base_renders: u64, base_renders: u64,
target_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)] #[derive(Debug)]
@ -463,6 +513,13 @@ struct RuntimeState {
document_parses: u64, document_parses: u64,
document_validations: u64, document_validations: u64,
document_reuses: 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>>, confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
} }
@ -491,7 +548,7 @@ struct Shared {
struct Session { struct Session {
shared: Arc<Shared>, shared: Arc<Shared>,
workers: Mutex<Option<Vec<JoinHandle<()>>>>, workers: Mutex<Option<Vec<JoinHandle<()>>>>,
layout_document: Mutex<Option<Arc<LayoutDocument>>>, layout_document: Mutex<Option<LayoutSource>>,
worker_count: usize, worker_count: usize,
} }
@ -516,6 +573,13 @@ struct SessionStats {
document_parses: u64, document_parses: u64,
document_validations: u64, document_validations: u64,
document_reuses: 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, pending_baselines: usize,
confirmed_baseline: bool, confirmed_baseline: bool,
confirmed_baseline_bytes: usize, confirmed_baseline_bytes: usize,
@ -581,6 +645,13 @@ impl Session {
document_parses: 0, document_parses: 0,
document_validations: 0, document_validations: 0,
document_reuses: 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, confirmed_baseline,
}), }),
readiness_channel: Mutex::new(None), readiness_channel: Mutex::new(None),
@ -611,7 +682,7 @@ impl Session {
.unwrap_or_else(|poison| poison.into_inner()) .unwrap_or_else(|poison| poison.into_inner())
.confirmed_baseline .confirmed_baseline
.as_ref() .as_ref()
.map(|baseline| Arc::clone(&baseline.document)); .map(|baseline| baseline.document.clone());
Ok(Box::new(Self { Ok(Box::new(Self {
shared, shared,
workers: Mutex::new(Some(handles)), workers: Mutex::new(Some(handles)),
@ -643,6 +714,9 @@ impl Session {
fn submit(&self, generation: u64, payload: &[u8]) -> Result<usize, String> { fn submit(&self, generation: u64, payload: &[u8]) -> Result<usize, String> {
let batch = parse_control_batch(payload)?; 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) { if !self.shared.alive.load(Ordering::Acquire) {
return Err("Native reflow session is closed".to_owned()); return Err("Native reflow session is closed".to_owned());
} }
@ -672,12 +746,12 @@ impl Session {
let document = match batch.document { let document = match batch.document {
Some(document) => { Some(document) => {
document.validate()?; document.validate()?;
Some(Arc::new(document)) Some(LayoutSource::Full(Arc::new(document)))
} }
None if layout_requested => Some( None if layout_requested => Some(
confirmed_baseline confirmed_baseline
.as_ref() .as_ref()
.map(|baseline| Arc::clone(&baseline.document)) .map(|baseline| baseline.document.clone())
.or_else(|| { .or_else(|| {
self.layout_document self.layout_document
.lock() .lock()
@ -852,10 +926,13 @@ impl Session {
.unwrap_or_else(|poison| poison.into_inner()) .unwrap_or_else(|poison| poison.into_inner())
.confirmed_baseline .confirmed_baseline
.clone(); .clone();
let (document, document_parses, document_validations, document_reuses) = match batch let (document, input_stats) = match (batch.document, batch.document_delta) {
.document (Some(_), Some(_)) => {
{ return Err(
Some(document) => { "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) { if document_base_revision.checked_add(1) != Some(document_target_revision) {
return Err( return Err(
"Native retained document replacement must advance one revision".to_owned(), "Native retained document replacement must advance one revision".to_owned(),
@ -876,10 +953,60 @@ impl Session {
} }
_ => {} _ => {}
} }
document.validate()?; let retained = document.retained_root_p();
(Arc::new(document), 1, 1, 0) 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 { if document_base_revision != document_target_revision {
return Err( return Err(
"Native retained document reuse requires equal base and target revisions" "Native retained document reuse requires equal base and target revisions"
@ -902,7 +1029,13 @@ impl Session {
.to_owned(), .to_owned(),
); );
} }
(Arc::clone(&baseline.document), 0, 0, 1) (
baseline.document.clone(),
DocumentInputStats {
reuses: 1,
..DocumentInputStats::default()
},
)
} }
}; };
let frame = batch let frame = batch
@ -943,9 +1076,16 @@ impl Session {
} }
state.base_renders += output.base_renders; state.base_renders += output.base_renders;
state.target_renders += output.target_renders; state.target_renders += output.target_renders;
state.document_parses += document_parses; state.document_resolver_lookups += output.resolver_lookups;
state.document_validations += document_validations; state.document_parses += input_stats.parses;
state.document_reuses += document_reuses; 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(); state.pending_baselines.clear();
if let Some(pending) = output.pending { if let Some(pending) = output.pending {
state state
@ -1078,6 +1218,13 @@ impl Session {
document_parses: state.document_parses, document_parses: state.document_parses,
document_validations: state.document_validations, document_validations: state.document_validations,
document_reuses: state.document_reuses, 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, pending_baselines,
confirmed_baseline: state.confirmed_baseline.is_some(), confirmed_baseline: state.confirmed_baseline.is_some(),
confirmed_baseline_bytes, confirmed_baseline_bytes,
@ -1129,7 +1276,7 @@ fn parse_control_batch(payload: &[u8]) -> Result<ControlBatch, String> {
} }
fn checked_layout_context( fn checked_layout_context(
document: &LayoutDocument, document: &LayoutSource,
frame_key: i64, frame_key: i64,
viewport_width: i64, viewport_width: i64,
viewport_width_known: bool, viewport_width_known: bool,
@ -1179,11 +1326,12 @@ fn confirmed_baseline_matches(
} }
fn prepare_layout_job( fn prepare_layout_job(
document: &Arc<LayoutDocument>, document: &LayoutSource,
frame: ControlFrame, frame: ControlFrame,
document_base_revision: u64, document_base_revision: u64,
document_target_revision: u64, document_target_revision: u64,
) -> Result<PreparedJob, String> { ) -> Result<PreparedJob, String> {
layout::reset_resolver_lookups();
if frame.payload.is_some() { if frame.payload.is_some() {
return Err(format!( return Err(format!(
"Native layout frame {} cannot contain an echo payload", "Native layout frame {} cannot contain an echo payload",
@ -1197,7 +1345,7 @@ fn prepare_layout_job(
.viewport_height .viewport_height
.ok_or_else(|| format!("Native layout frame {} requires viewport-height", frame.key))?; .ok_or_else(|| format!("Native layout frame {} requires viewport-height", frame.key))?;
let context = checked_layout_context( let context = checked_layout_context(
document.as_ref(), document,
frame.key, frame.key,
viewport_width, viewport_width,
frame.viewport_width_known, frame.viewport_width_known,
@ -1223,7 +1371,7 @@ fn prepare_layout_job(
) )
})?; })?;
let base_context = checked_layout_context( let base_context = checked_layout_context(
document.as_ref(), document,
frame.key, frame.key,
base_viewport_width, base_viewport_width,
frame.base_viewport_width_known, frame.base_viewport_width_known,
@ -1254,7 +1402,7 @@ fn prepare_layout_job(
key: frame.key, key: frame.key,
delay_ms: frame.delay_ms, delay_ms: frame.delay_ms,
payload: JobPayload::Layout { payload: JobPayload::Layout {
document: Arc::clone(document), document: document.clone(),
context, context,
root_width, root_width,
root_width_override: frame.root_width_override, root_width_override: frame.root_width_override,
@ -1267,6 +1415,7 @@ fn prepare_layout_job(
root_metadata: frame.root_metadata, root_metadata: frame.root_metadata,
document_base_revision, document_base_revision,
document_target_revision, document_target_revision,
validation_resolver_lookups: layout::resolver_lookups(),
}, },
}) })
} }
@ -1287,6 +1436,7 @@ fn render_layout_payload(
baseline_hit: false, baseline_hit: false,
base_renders: 0, base_renders: 0,
target_renders: 0, target_renders: 0,
resolver_lookups: 0,
}, },
JobPayload::Layout { JobPayload::Layout {
document, document,
@ -1302,6 +1452,7 @@ fn render_layout_payload(
root_metadata, root_metadata,
document_base_revision, document_base_revision,
document_target_revision, document_target_revision,
validation_resolver_lookups,
} => { } => {
let identity = TapeIdentity { let identity = TapeIdentity {
session_id, session_id,
@ -1331,8 +1482,20 @@ fn render_layout_payload(
// job with neither result nor error tape, so the Elisp ready // job with neither result nor error tape, so the Elisp ready
// watcher would poll forever. Convert panics into the same // watcher would poll forever. Convert panics into the same
// error-tape channel ordinary layout failures use. // 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 result = catch_unwind(AssertUnwindSafe(|| -> LayoutRenderOutcome {
let target_styles = document.styles()?;
if let Some(base_context) = base_context { if let Some(base_context) = base_context {
let base_identity = BaselineIdentity { let base_identity = BaselineIdentity {
context: base_context, context: base_context,
@ -1351,7 +1514,7 @@ fn render_layout_payload(
&base_identity, &base_identity,
) && layout::style_registry_extends_exact_prefix( ) && layout::style_registry_extends_exact_prefix(
&baseline.styles, &baseline.styles,
&document.styles, &target_styles,
) )
}) })
.cloned(); .cloned();
@ -1360,12 +1523,12 @@ fn render_layout_payload(
.layout_tape(context, root_width_override.then_some(root_width))?; .layout_tape(context, root_width_override.then_some(root_width))?;
let bytes = layout::encode_layout_tape( let bytes = layout::encode_layout_tape(
target.clone(), target.clone(),
&document.styles, &target_styles,
identity, identity,
output.root_metadata, output.root_metadata,
output.max_bytes, 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 { let (old, baseline_hit, base_renders) = if let Some(baseline) = base_hit {
(baseline.tape.clone(), true, 0) (baseline.tape.clone(), true, 0)
@ -1384,45 +1547,51 @@ fn render_layout_payload(
let bytes = layout::encode_layout_patch_tape( let bytes = layout::encode_layout_patch_tape(
old, old,
target.clone(), target.clone(),
&document.styles, &target_styles,
identity, identity,
output.root_metadata, output.root_metadata,
output.max_bytes, output.max_bytes,
)?; )?;
Ok((bytes, target, baseline_hit, base_renders, 1)) Ok((bytes, target, target_styles, baseline_hit, base_renders, 1))
} else { } else {
let target = let target =
document.layout_tape(context, root_width_override.then_some(root_width))?; document.layout_tape(context, root_width_override.then_some(root_width))?;
let bytes = layout::encode_layout_tape( let bytes = layout::encode_layout_tape(
target.clone(), target.clone(),
&document.styles, &target_styles,
identity, identity,
output.root_metadata, output.root_metadata,
output.max_bytes, 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 { match result {
Ok(Ok((bytes, tape, baseline_hit, base_renders, target_renders))) => RenderedJob { Ok(Ok((bytes, tape, styles, baseline_hit, base_renders, target_renders))) => {
bytes, RenderedJob {
pending: Some(PendingBaseline { bytes,
confirmed_identity: pending_identity, pending: Some(PendingBaseline {
document: Arc::clone(&document), confirmed_identity: pending_identity,
document_revision: document_target_revision, document: document.clone(),
tape, document_revision: document_target_revision,
styles: document.styles.clone(), tape,
}), styles,
baseline_hit, }),
base_renders, baseline_hit,
target_renders, base_renders,
}, target_renders,
resolver_lookups,
}
}
Ok(Err(error)) => RenderedJob { Ok(Err(error)) => RenderedJob {
bytes: encode_error_tape(identity, &error, max_result_bytes), bytes: encode_error_tape(identity, &error, max_result_bytes),
pending: None, pending: None,
baseline_hit: false, baseline_hit: false,
base_renders: 0, base_renders: 0,
target_renders: 0, target_renders: 0,
resolver_lookups,
}, },
Err(_) => RenderedJob { Err(_) => RenderedJob {
bytes: encode_error_tape(identity, "native layout panicked", max_result_bytes), bytes: encode_error_tape(identity, "native layout panicked", max_result_bytes),
@ -1430,6 +1599,7 @@ fn render_layout_payload(
baseline_hit: false, baseline_hit: false,
base_renders: 0, base_renders: 0,
target_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 { if frame.delay_ms != 0 {
return Err("Native proof render cannot contain delay-ms".to_owned()); 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( let output = render_layout_payload(
prepared.payload, prepared.payload,
SYNC_RENDER_SESSION_ID, SYNC_RENDER_SESSION_ID,
@ -1577,6 +1748,7 @@ fn worker_loop(shared: Arc<Shared>) {
} }
state.base_renders += output.base_renders; state.base_renders += output.base_renders;
state.target_renders += output.target_renders; state.target_renders += output.target_renders;
state.document_resolver_lookups += output.resolver_lookups;
state.results.insert( state.results.insert(
(job.generation, job.key), (job.generation, job.key),
ResultEntry { ResultEntry {
@ -1994,6 +2166,14 @@ mod tests {
.into_bytes() .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( fn replacement_layout_payload(
document_base_revision: u64, document_base_revision: u64,
document_target_revision: u64, document_target_revision: u64,
@ -2336,6 +2516,79 @@ mod tests {
grandchild.stop(true); 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] #[test]
fn retained_document_reuse_rejects_wrong_or_ambiguous_revision() { fn retained_document_reuse_rejects_wrong_or_ambiguous_revision() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap(); let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();

View File

@ -2372,6 +2372,14 @@ remain retained identities."
session initial-state root)) session initial-state root))
(next (copy-tree root)) (next (copy-tree root))
(next-child (car (ebox-tree-node-children next)))) (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) (setf (ebox-native-reflow-session-styles session)
(plist-get initial-package :styles) (plist-get initial-package :styles)
(ebox-native-reflow-session-layout-package session) (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-base-revision) 10))
(should (= (plist-get (car controls) :document-target-revision) 11))))) (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) (provide 'ebox-commit-tests)
;;; ebox-commit-tests.el ends here ;;; ebox-commit-tests.el ends here

View File

@ -411,6 +411,55 @@
:authority ebox :lifetime candidate-until-final-accept :authority ebox :lifetime candidate-until-final-accept
:rollback private-session-discard :rebuild native-frame-stage :rollback private-session-discard :rebuild native-frame-stage
:cleanup confirm-or-rollback) :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 (:id process/scroll-global-state
:storage (:global ebox--scroll-global-state) :storage (:global ebox--scroll-global-state)
:proposed-category generation-bound-mutable :owner ebox-incremental :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)))) 'generation-bound-mutable-authority))))
(ert-deftest ebox-state-contract-excludes-native-compile-scratch-from-authority () (ert-deftest ebox-state-contract-excludes-native-compile-scratch-from-authority ()
"Compile-local template ids are not mislabeled as native session authority." "Compile-local indexes are not mislabeled as native session authority."
(should-not (let ((authority-storage
(memq 'ebox-native-reflow--compile-property-template-ids
(plist-get (ebox-state-contract-record 'native-runtime-authority) (plist-get (ebox-state-contract-record 'native-runtime-authority)
:storage))) :storage)))
(let ((exclusion (dolist (symbol '(ebox-native-reflow--compile-property-template-ids
(cl-find 'ebox-native-reflow--compile-property-template-ids ebox-native-reflow--compile-property-template-index
ebox-state-contract-source-scan-exclusions ebox-native-reflow--compile-style-index))
:key (lambda (entry) (plist-get entry :symbol))))) (should-not (memq symbol authority-storage))
(should exclusion) (let ((exclusion
(should (eq (plist-get exclusion :reason) (cl-find symbol ebox-state-contract-source-scan-exclusions
'dynamically-bound-compile-local-scratch)))) :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 () (ert-deftest ebox-state-contract-tp-custody-is-opaque ()
"The inventory separates current whole-state custody from its M2a target." "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) (plist-get (tp-surface-client-state surface)
:native-sync-session)) :native-sync-session))
(should (eq (plist-get report :projection-kind) 'native-frame)) (should (eq (plist-get report :projection-kind) 'native-frame))
;; A style-table change cannot consume the prior confirmed ;; A root paint delta and appended styles preserve the old
;; document and therefore remains a truthful full frame. ;; registry prefix and publish against the confirmed tape.
(should (eq (plist-get report :native-frame-kind) 'full)) (should (eq (plist-get report :native-frame-kind) 'patch))
(should next-session) (should next-session)
(should-not (eq next-session winning-session)) (should-not (eq next-session winning-session))
(should (ebox-native-reflow-session-released-p winning-session)) (should (ebox-native-reflow-session-released-p winning-session))
(should-not (ebox-native-reflow-session-released-p (should-not (ebox-native-reflow-session-released-p
next-session)) 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))) (let ((stats (ebox-native-reflow-stats next-session)))
(should (= (plist-get stats :document-parses) 1)) (should (= (plist-get stats :document-parses) 0))
(should (= (plist-get stats :document-validations) 1)) (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)))))) (should (= (plist-get stats :document-reuses) 0))))))
(when (buffer-live-p buffer) (when (buffer-live-p buffer)
(kill-buffer buffer)) (kill-buffer buffer))
@ -1725,6 +1733,108 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(when (buffer-live-p buffer) (when (buffer-live-p buffer)
(kill-buffer 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 () (ert-deftest ebox-viewport-reflow-retains-final-sized-flex-child-fragments ()
"Viewport reflow should reuse final-sized Flex child fragments exactly." "Viewport reflow should reuse final-sized Flex child fragments exactly."
(ebox-surface-test--reset-render-state) (ebox-surface-test--reset-render-state)