diff --git a/.runtime-index-plan.md b/.runtime-index-plan.md new file mode 100644 index 0000000..7308694 --- /dev/null +++ b/.runtime-index-plan.md @@ -0,0 +1,51 @@ +# Runtime persistent index cleanup plan + +1. Lock the persistent index contract: exact `equal` keys, nil values, + collision buckets, immutable put/delete, count/map/keys, and legacy hash + adapters. +2. Add one fixed-depth radix index module and load it before tree, patch-plan, + incremental, surface, and native commit consumers. +3. Bootstrap node, parent, and region-owner indexes directly into immutable + indexes; store native postorder as node IDs. Bind the generated ID format + to the exact read-only vector; convert unmarked legacy entries with complete + candidate membership validation before retaining that format. +4. Change local candidate preparation to path-copy only removed/touched node and + affected-region keys. Keep all other state tables on their current path. +5. Migrate readers and compatibility entry points to the common adapters. + Preserve hash-table inputs at package boundaries. +6. Keep the complete topology comparison, including untouched surface objects + and source keys. Key-set identity tokens prove only persistent index + membership: value updates retain the token, membership changes replace it, + and unrelated index lineages use the exact key comparison. +7. Verify focused index and candidate regressions, then compile/checkdoc/full + ERT serially after coordinating CPU use with the root agent. + +Stop when all production consumers of the three runtime indexes use adapters, +local candidates perform no whole-table copy/remap, and the focused plus full +validation gates pass. Report remaining full work in region-box/count/source +and other state tables. + +## Compressed radix follow-up + +The compiled fixed-depth version passed behavior checks but regressed measured +row/theme/filter/pagination latency. It is not accepted for integration. + +1. Preserve the measured fixed-depth module, compiled bytecode, tests, and + reviewed patch outside the checkout before editing. +2. Add regressions for compressed branching depth and exact key semantics: + prefix divergence above an existing branch; colliding 64-bit integer codes; + colliding noninteger keys; nil values; deletion collapse and fork isolation; + and deletion of the key that supplied a branch's representative code. +3. Replace mandatory-depth roots with nil, exact collision leaves, and + 16-way branch vectors carrying their actual nibble shift and representative + code. Branches have at least two children and increasing child shifts. + Put returns its membership-change flag without a separate lookup. Delete + replaces unary branches with their surviving child. +4. Keep key-set tokens, hash-table adapters, native postorder representation, + full topology validation, and all publication/rollback paths unchanged. +5. After the CPU owner releases its slot, run focused regressions, compiled + old/new index cost comparisons, then required package checks serially. + Report actual costs to the root agent for full workload measurement. + +Accept this follow-up only after semantic checks pass and measured costs +improve; compressed structure alone does not establish end-to-end latency. diff --git a/Makefile b/Makefile index a60f46d..9f10384 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ NATIVE_MANIFEST = native/Cargo.toml NATIVE_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))') NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release -.PHONY: all check ci load compile test checkdoc source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests m0a-tests state-contract-tests layout-boundary-tests layout-boundary-performance patch-plan-tests patch-plan-performance style-schema-tests style-schema-performance spi-tests spi-performance c1b-contract-tests docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install +.PHONY: all check ci load compile test checkdoc runtime-index-tests source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests m0a-tests state-contract-tests layout-boundary-tests layout-boundary-performance patch-plan-tests patch-plan-performance style-schema-tests style-schema-performance spi-tests spi-performance c1b-contract-tests docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install all: check @@ -25,7 +25,10 @@ compile: rm -f *.elc tests/*.elc scripts/*.elc $(EMACS_BATCH) --eval '(setq byte-compile-error-on-warn t byte-compile-warnings (quote (not obsolete)))' -l ebox.el --eval '(ebox-byte-compile)' -test: source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests m0a-tests state-contract-tests layout-boundary-tests patch-plan-tests style-schema-tests spi-tests docs-contract-tests ci-contract-tests +test: runtime-index-tests source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests m0a-tests state-contract-tests layout-boundary-tests patch-plan-tests style-schema-tests spi-tests docs-contract-tests ci-contract-tests + +runtime-index-tests: + $(EMACS_BATCH) -l tests/ebox-runtime-index-tests.el -f ert-run-tests-batch-and-exit source-tests: $(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit diff --git a/docs/maintainer/ebox-current-implementation-reference.en.md b/docs/maintainer/ebox-current-implementation-reference.en.md index 9f7b909..7f8b89a 100644 --- a/docs/maintainer/ebox-current-implementation-reference.en.md +++ b/docs/maintainer/ebox-current-implementation-reference.en.md @@ -19,6 +19,7 @@ This is the maintainer entry point for the standalone Ebox repository. It descri | `ebox.el` | Public facade, construction helpers, rendering, TP-backed buffer entry points, scrolling, commit, and byte compilation. | | `ebox-cache.el` | Measurement/render cache records, invalidation, and cache reports. | | `ebox-source.el` | Opaque source handles, immutable author source records, and candidate source indexes. | +| `ebox-runtime-index.el` | Compressed persistent radix maps and legacy hash-table adapters. | | `ebox-state-contract.el` | Closed retained-state ownership inventory and read-only compatibility-mirror rebuild probes. | | `ebox-style.el` | ECSS property schemas, declarations and cascade, shorthand expansion, computed style, colors, borders, and dirty effects. | | `ebox-tree.el` | Node traversal, logical child access, ECSS subject adaptation, identity, parent paths, keys, and tree snapshots. | @@ -42,6 +43,8 @@ The package intentionally does not include application Components, UI controls, The active contract also covers `Makefile`, `.github/workflows/ci.yml`, `tests/ebox-core-render-tests.el`, `tests/ebox-state-contract-tests.el`, `tests/ebox-layout-boundary-tests.el`, `tests/ebox-patch-plan-tests.el`, `tests/ebox-style-schema-tests.el`, `tests/ebox-spi-tests.el`, `tests/ebox-child-range-tests.el`, `tests/ebox-grid-tests.el`, `tests/ebox-commit-tests.el`, `tests/ebox-surface-tests.el`, `tests/ebox-dsl-tests.el`, `tests/ebox-flex-tests.el`, `tests/ebox-selector-tests.el`, `tests/ebox-package-tests.el`, `tests/ebox-visual-check-tests.el`, `tests/ebox-docs-contract-tests.el`, `tests/ebox-ci-contract-tests.el`, `native/Cargo.toml`, `native/Cargo.lock`, `native/build.rs`, `native/vendor/emacs-30/emacs-module.h`, `native/src/lib.rs`, `native/src/layout.rs`, `native/c/ebox_module.c`, `scripts/ebox-package-lint.el`, `scripts/ebox-visual-check.el`, and `scripts/ebox-performance-evaluator.el`. +Runtime index behavior is covered by `tests/ebox-runtime-index-tests.el`. + ## Runtime model The normal data flow is: @@ -137,6 +140,7 @@ make load make compile make check make core-tests +make runtime-index-tests make child-range-tests make grid-tests make ebox-commit-tests diff --git a/docs/maintainer/ebox-current-implementation-reference.zh.md b/docs/maintainer/ebox-current-implementation-reference.zh.md index 5e1a47b..35e89f3 100644 --- a/docs/maintainer/ebox-current-implementation-reference.zh.md +++ b/docs/maintainer/ebox-current-implementation-reference.zh.md @@ -19,6 +19,7 @@ | `ebox.el` | 公共门面、构造辅助函数、渲染、基于 TP 的 buffer 入口、滚动、commit 与 byte compile。 | | `ebox-cache.el` | 测量/渲染缓存记录、失效和缓存报告。 | | `ebox-source.el` | opaque source handle、不可变 author source record 与 candidate source index。 | +| `ebox-runtime-index.el` | 压缩持久 radix map 与旧 hash-table adapter。 | | `ebox-state-contract.el` | 封闭的 retained-state 所有权清单与只读 compatibility-mirror 重建探针。 | | `ebox-style.el` | ECSS property schema、declaration 与 cascade、shorthand 展开、computed style、颜色、border 和 dirty effect。 | | `ebox-tree.el` | 节点遍历、逻辑子节点访问、ECSS subject 适配、identity、父路径、key 和树 snapshot。 | @@ -42,6 +43,8 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-core-render-tests.el`、`tests/ebox-state-contract-tests.el`、`tests/ebox-layout-boundary-tests.el`、`tests/ebox-patch-plan-tests.el`、`tests/ebox-style-schema-tests.el`、`tests/ebox-spi-tests.el`、`tests/ebox-child-range-tests.el`、`tests/ebox-grid-tests.el`、`tests/ebox-commit-tests.el`、`tests/ebox-surface-tests.el`、`tests/ebox-dsl-tests.el`、`tests/ebox-flex-tests.el`、`tests/ebox-selector-tests.el`、`tests/ebox-package-tests.el`、`tests/ebox-visual-check-tests.el`、`tests/ebox-docs-contract-tests.el`、`tests/ebox-ci-contract-tests.el`、`native/Cargo.toml`、`native/Cargo.lock`、`native/build.rs`、`native/vendor/emacs-30/emacs-module.h`、`native/src/lib.rs`、`native/src/layout.rs`、`native/c/ebox_module.c`、`scripts/ebox-package-lint.el`、`scripts/ebox-visual-check.el` 和 `scripts/ebox-performance-evaluator.el`。 +Runtime index 行为由 `tests/ebox-runtime-index-tests.el` 覆盖。 + ## 运行时模型 正常数据流是: @@ -130,6 +133,7 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor ```sh make check make core-tests +make runtime-index-tests make child-range-tests make grid-tests make ebox-commit-tests diff --git a/ebox-incremental.el b/ebox-incremental.el index 7910bb9..539a500 100644 --- a/ebox-incremental.el +++ b/ebox-incremental.el @@ -14,6 +14,7 @@ (require 'ebox-buffer-backend) (require 'ebox-render-context) (require 'ebox-patch-plan) +(require 'ebox-runtime-index) (defvar ebox-region-types) (declare-function ebox--scroll-state-region-ids-containing-node-ids @@ -440,8 +441,8 @@ unless root reflow and idle prefetch also avoid crossing several such rows.") (or (plist-get state :selector-index-stale-p) (and (hash-table-p type-counts) (> (or (gethash 'flex type-counts) 0) 0)) - (and (hash-table-p node-table) - (>= (hash-table-count node-table) + (and (ebox-runtime-index-like-p node-table) + (>= (ebox-runtime-index-size node-table) ebox--latency-sensitive-scroll-node-limit))))) (defun ebox--root-reflow-scroll-lookahead-lines-for-buffer (buffer) @@ -1148,10 +1149,10 @@ Keys are region-id, values are plists with :scroll-offset, :content-lines, etc." When COLLECT-REGION-BOXES is non-nil, collect `:region-box-table' during the same traversal in complete-render overwrite order. SOURCE-INDEX, when supplied, is the candidate-owned source snapshot for ROOT-NODE." - (let ((node-table (make-hash-table :test 'equal)) - (parent-table (make-hash-table :test 'equal)) + (let ((node-table (ebox-runtime-index-empty)) + (parent-table (ebox-runtime-index-empty)) (region-id-set (make-hash-table :test 'equal)) - (region-node-table (make-hash-table :test 'equal)) + (region-node-table (ebox-runtime-index-empty)) (region-box-table (and collect-region-boxes (make-hash-table :test 'equal))) (region-box-count-table (make-hash-table :test 'equal)) @@ -1181,17 +1182,21 @@ supplied, is the candidate-owned source snapshot for ROOT-NODE." ;; A flex wrapper is visited after its flex owner. Keep ;; that earlier render-owner mapping instead of ;; replacing it with the non-renderable source box. - (unless (gethash region-id region-node-table) - (puthash region-id node-id region-node-table)))) + (unless (ebox-runtime-index-get region-id region-node-table) + (setq region-node-table + (ebox-runtime-index-put + region-id node-id region-node-table))))) ('flex (when-let* ((box (plist-get node :box))) (let ((region-id (ebox--ensure-region-id box))) (puthash region-id t region-id-set) ;; Preserve the recursive lookup's first-match ;; semantics when malformed trees reuse a region id. - (unless (gethash region-id region-node-table) - (puthash region-id node-id - region-node-table))))))) + (unless (ebox-runtime-index-get region-id region-node-table) + (setq region-node-table + (ebox-runtime-index-put + region-id node-id + region-node-table)))))))) (index-region-box (node) ;; Box rendering records region ownership after rendering ;; child content. Preserve that postorder overwrite order @@ -1211,11 +1216,14 @@ supplied, is the candidate-owned source snapshot for ROOT-NODE." (not (stringp node))) (let ((node-id (ebox--ensure-node-id node)) (type (plist-get node :ebox-type))) - (puthash node-id node node-table) + (setq node-table + (ebox-runtime-index-put node-id node node-table)) (ebox-incremental--adjust-runtime-type-count runtime-type-count-table node 1) (when parent-id - (puthash node-id parent-id parent-table)) + (setq parent-table + (ebox-runtime-index-put + node-id parent-id parent-table))) (index-region-id node node-id) (index-host-ref node node-id) (when-let* ((sequence (plist-get node :ebox-child-sequence))) @@ -1257,12 +1265,12 @@ supplied, is the candidate-owned source snapshot for ROOT-NODE." ;; source boxes are indexed for ownership without adding ;; duplicate compiler work. (when native-layout-p - (push node native-node-postorder)))))) + (push node-id native-node-postorder)))))) (visit root-node nil t)) (setq source-index (or source-index (ebox-tree-source-index root-node))) (setq source-index (ebox-source--with-runtime-hosts source-index host-ref-table)) - (let () + (let ((postorder (vconcat (nreverse native-node-postorder)))) (list :node-table node-table :parent-table parent-table :region-id-set region-id-set @@ -1278,8 +1286,8 @@ supplied, is the candidate-owned source snapshot for ROOT-NODE." :selector-type-table (ebox-source--index-selector-table source-index 'type) :runtime-type-count-table runtime-type-count-table - :native-node-postorder - (vconcat (nreverse native-node-postorder)))))) + :native-node-postorder postorder + :native-node-postorder-ids postorder)))) (defun ebox-incremental--ensure-buffer-selector-indexes (buffer) "Return BUFFER's state after materializing deferred selector indexes." @@ -1337,7 +1345,7 @@ supplied, is the candidate-owned source snapshot for ROOT-NODE." :range-ref-table :source-index :selector-id-table :selector-class-table :selector-type-table :runtime-type-count-table - :native-node-postorder)) + :native-node-postorder :native-node-postorder-ids)) (setq state (plist-put state key (plist-get index key)))) state) @@ -1386,7 +1394,7 @@ supplied, is the candidate-owned source snapshot for ROOT-NODE." (defun ebox-incremental--range-ref-resolve (state ref) "Resolve REF in STATE to its parent, sequence, segment index, and rank." (when-let* ((record (gethash ref (plist-get state :range-ref-table))) - (parent (gethash (plist-get record :parent-node-id) + (parent (ebox-runtime-index-get (plist-get record :parent-node-id) (plist-get state :node-table))) (sequence (plist-get parent :ebox-child-sequence))) (list :parent parent :sequence sequence @@ -1399,7 +1407,7 @@ supplied, is the candidate-owned source snapshot for ROOT-NODE." Use the persistent runtime index when available. Legacy runtime states that lack the index, or an indexed entry, fall back to a recursive tree lookup." (or (when-let* ((table (ebox--buffer-region-node-table buffer))) - (gethash region-id table)) + (ebox-runtime-index-get region-id table)) (when-let* ((root (ebox--buffer-root-node buffer)) (path (ebox--node-path-to-region root region-id))) (ebox--ensure-node-id (car path))))) @@ -2187,7 +2195,7 @@ the planning pass has one for this buffer." (or (when-let* ((base ebox-incremental--candidate-base-state) ((eq buffer (car base))) (node-table (plist-get (cdr base) :node-table))) - (gethash node-id node-table)) + (ebox-runtime-index-get node-id node-table)) (ebox--buffer-runtime-node buffer node-id))) (defun ebox--ensure-layout-snapshot-spans (buffer node-id) @@ -2328,17 +2336,17 @@ When DETAILS is non-nil, include expensive per-line detail fields." (or (and ebox-incremental--candidate-proof-node-table (eq buffer (car ebox-incremental--candidate-proof-node-table)) - (gethash node-id + (ebox-runtime-index-get node-id (cdr ebox-incremental--candidate-proof-node-table))) (when-let* ((node-table (ebox--buffer-node-table buffer))) - (gethash node-id node-table)) + (ebox-runtime-index-get node-id node-table)) (when-let* ((root (ebox--buffer-root-node buffer))) (ebox--runtime-node-by-id root node-id)))) (defun ebox--runtime-parent-id (buffer node-id) "Return NODE-ID's parent id in BUFFER runtime state." (when-let* ((parent-table (ebox--buffer-parent-table buffer))) - (gethash node-id parent-table))) + (ebox-runtime-index-get node-id parent-table))) (defun ebox--invalidate-runtime-render-signature-path (node-id node-table parent-table signature-cache height-cache) @@ -2348,10 +2356,10 @@ When DETAILS is non-nil, include expensive per-line detail fields." (when signature-cache (clrhash signature-cache)) (when height-cache (clrhash height-cache))) (while node-id - (when-let* ((node (gethash node-id node-table))) + (when-let* ((node (ebox-runtime-index-get node-id node-table))) (when signature-cache (remhash node signature-cache)) (when height-cache (remhash node height-cache))) - (setq node-id (gethash node-id parent-table))))) + (setq node-id (ebox-runtime-index-get node-id parent-table))))) (defun ebox--runtime-ancestor-id-p (buffer ancestor-id node-id) "Return non-nil when ANCESTOR-ID is NODE-ID's ancestor in BUFFER." @@ -4184,7 +4192,7 @@ during `ebox-incremental-flush'." (ebox-incremental--candidate-range-items-retainable-p candidate next-items ebox-incremental--source-base-index reuse-map))) - (parent (gethash (plist-get record :parent-node-id) + (parent (ebox-runtime-index-get (plist-get record :parent-node-id) (plist-get base-state :node-table))) (sequence (and parent (plist-get parent :ebox-child-sequence))) (segment (and sequence (ebox-child-range--lookup-ref sequence ref))) @@ -4285,12 +4293,12 @@ during `ebox-incremental-flush'." "Return non-nil when ANCESTOR-ID contains DESCENDANT-ID in CANDIDATE's base." (let* ((state (ebox-candidate--base-state candidate)) (parent-table (plist-get state :parent-table)) - (current (gethash descendant-id parent-table)) + (current (ebox-runtime-index-get descendant-id parent-table)) found) (while (and current (not found)) (if (equal current ancestor-id) (setq found t) - (setq current (gethash current parent-table)))) + (setq current (ebox-runtime-index-get current parent-table)))) found)) (defun ebox-incremental-candidate-replace @@ -4311,7 +4319,7 @@ detached semantic variants of this stable anchor for bounded identity reuse." (let* ((state (ebox-candidate--base-state candidate)) (node-table (plist-get state :node-table)) retained) - (unless (gethash node-id node-table) + (unless (ebox-runtime-index-get node-id node-table) (error "Ebox candidate anchor does not exist: %S" node-id)) (unless (and (listp next-subtree) (not (stringp next-subtree))) (error "Ebox candidate replacement must be an Ebox node")) @@ -4428,7 +4436,7 @@ the patched Host's ancestor chain." (when (eq node-id missing) (error "Ebox candidate host ref does not exist: %S" host-ref)) (catch 'unsupported - (let* ((base-node (gethash node-id (plist-get state :node-table))) + (let* ((base-node (ebox-runtime-index-get node-id (plist-get state :node-table))) (previous-node (ebox-incremental--find-host-ref-node previous-subtree host-ref)) @@ -4565,9 +4573,9 @@ logical tree crosses that boundary." "Return ROOT path-copied with NODE-ID replaced by REPLACEMENT." (let* ((node-table (plist-get index :node-table)) (parent-table (plist-get index :parent-table)) - (old-child (gethash node-id node-table)) + (old-child (ebox-runtime-index-get node-id node-table)) (new-child replacement) - (parent-id (gethash node-id parent-table))) + (parent-id (ebox-runtime-index-get node-id parent-table))) (unless old-child (error "Ebox candidate anchor is absent after an ancestor replacement: %S" node-id)) @@ -4577,12 +4585,12 @@ logical tree crosses that boundary." (plist-get replacement :node-id)))) (while parent-id (let* ((current-parent-id parent-id) - (old-parent (gethash current-parent-id node-table)) + (old-parent (ebox-runtime-index-get current-parent-id node-table)) (new-parent (ebox-tree-copy-with-direct-child-replacements old-parent (list (cons old-child new-child)))) (grandparent-id - (gethash current-parent-id parent-table))) + (ebox-runtime-index-get current-parent-id parent-table))) (ebox-incremental--candidate-record-path-copy current-parent-id new-parent grandparent-id nil (not (equal (plist-get old-child :node-id) @@ -4607,16 +4615,16 @@ copies the owner's descendants before mutation while retaining their ids." (node-table (plist-get state :node-table)) (parent-table (plist-get state :parent-table)) (region-node-table (plist-get state :region-node-table)) - (owner-id (and (hash-table-p region-node-table) - (gethash region-id region-node-table))) - (owner (and (hash-table-p node-table) - (gethash owner-id node-table))) + (owner-id (and (ebox-runtime-index-like-p region-node-table) + (ebox-runtime-index-get region-id region-node-table))) + (owner (and (ebox-runtime-index-like-p node-table) + (ebox-runtime-index-get owner-id node-table))) (box (pcase (and owner (plist-get owner :ebox-type)) ('box owner) ('flex (plist-get owner :box))))) (when (and root owner box - (hash-table-p parent-table) + (ebox-runtime-index-like-p parent-table) (equal (plist-get box :region-id) region-id)) (let* ((candidate-box (if isolate-owner-subtree-p @@ -4647,14 +4655,14 @@ shared with the published generation." affected-ids) (maphash (lambda (node-id _replacement) - (unless (gethash node-id node-table) + (unless (ebox-runtime-index-get node-id node-table) (error "Ebox candidate path-copy node is absent: %S" node-id)) (let ((current node-id)) (while current (unless (gethash current affected) (puthash current t affected) (push current affected-ids)) - (setq current (gethash current parent-table))))) + (setq current (ebox-runtime-index-get current parent-table))))) replacements) (let ((depths (make-hash-table :test 'equal))) (cl-labels @@ -4664,7 +4672,7 @@ shared with the published generation." (let ((current node-id) path (result -1)) (while (and current (not (gethash current depths))) (push current path) - (setq current (gethash current parent-table))) + (setq current (ebox-runtime-index-get current parent-table))) (when current (setq result (gethash current depths))) (dolist (path-id path) @@ -4676,7 +4684,7 @@ shared with the published generation." (> (depth left) (depth right))))))) (let (result) (dolist (node-id affected-ids) - (let* ((old-node (gethash node-id node-table)) + (let* ((old-node (ebox-runtime-index-get node-id node-table)) (local (or (gethash node-id replacements) old-node)) (child-replacements (gethash node-id children)) (effective-child-replacements @@ -4704,7 +4712,7 @@ shared with the published generation." (ebox-tree-copy-with-direct-child-replacements local effective-child-replacements) local)) - (parent-id (gethash node-id parent-table))) + (parent-id (ebox-runtime-index-get node-id parent-table))) (ebox-incremental--candidate-record-path-copy node-id candidate parent-id (gethash node-id replacements) nil) (if parent-id @@ -4729,15 +4737,15 @@ be resolved through the retained runtime indexes." (region-node-table (plist-get state :region-node-table)) (replacements (make-hash-table :test 'equal)) valid) - (setq valid (and root (hash-table-p node-table) - (hash-table-p parent-table) - (hash-table-p region-node-table))) + (setq valid (and root (ebox-runtime-index-like-p node-table) + (ebox-runtime-index-like-p parent-table) + (ebox-runtime-index-like-p region-node-table))) (dolist (entry region-offsets) (when valid (let* ((region-id (car entry)) (offset (cdr entry)) - (owner-id (gethash region-id region-node-table)) - (owner (and owner-id (gethash owner-id node-table))) + (owner-id (ebox-runtime-index-get region-id region-node-table)) + (owner (and owner-id (ebox-runtime-index-get owner-id node-table))) (box (pcase (and owner (plist-get owner :ebox-type)) ('box owner) @@ -4772,8 +4780,8 @@ be resolved through the retained runtime indexes." affected-ids) (maphash (lambda (node-id replacement) - (let ((old-node (gethash node-id node-table)) - (parent-id (gethash node-id parent-table))) + (let ((old-node (ebox-runtime-index-get node-id node-table)) + (parent-id (ebox-runtime-index-get node-id parent-table))) (unless old-node (error "Ebox candidate path-copy node is absent: %S" node-id)) (ebox-incremental--candidate-record-path-copy @@ -4790,7 +4798,7 @@ be resolved through the retained runtime indexes." (unless (gethash parent-id affected-table) (puthash parent-id t affected-table) (push parent-id affected-ids)) - (setq parent-id (gethash parent-id parent-table))))) + (setq parent-id (ebox-runtime-index-get parent-id parent-table))))) replacements) (let ((depth-table (make-hash-table :test 'equal))) (cl-labels @@ -4803,7 +4811,7 @@ be resolved through the retained runtime indexes." (while (and current (not (gethash current depth-table))) (push current path) - (setq current (gethash current parent-table))) + (setq current (ebox-runtime-index-get current parent-table))) (when current (setq result (gethash current depth-table))) (dolist (path-id path) @@ -4816,11 +4824,11 @@ be resolved through the retained runtime indexes." (let ((result (gethash (plist-get root :node-id) replacements))) (dolist (parent-id affected-ids) (let* ((child-replacements (gethash parent-id direct)) - (old-parent (gethash parent-id node-table)) + (old-parent (ebox-runtime-index-get parent-id node-table)) (copy (ebox-tree-copy-with-direct-child-replacements old-parent child-replacements)) - (grandparent-id (gethash parent-id parent-table))) + (grandparent-id (ebox-runtime-index-get parent-id parent-table))) (ebox-incremental--candidate-record-path-copy parent-id copy grandparent-id nil (cl-some @@ -4846,12 +4854,12 @@ building an index for an intermediate path-copied root." (cl-labels ((ancestor-p (ancestor-id descendant-id) - (let ((current (gethash descendant-id parent-table)) + (let ((current (ebox-runtime-index-get descendant-id parent-table)) found) (while (and current (not found)) (if (equal current ancestor-id) (setq found t) - (setq current (gethash current parent-table)))) + (setq current (ebox-runtime-index-get current parent-table)))) found))) (cl-loop for tail on replacements @@ -4900,12 +4908,12 @@ Cost is proportional to the replacements, never to the page." (let ((walk descendant) found) (while (and walk (not found)) (setq found (equal walk ancestor) - walk (and (not found) (gethash walk parent-table)))) + walk (and (not found) (ebox-runtime-index-get walk parent-table)))) found)) (inside-replaced-anchor-p (node-id) (let ((walk node-id)) (while (and walk (not (member walk anchor-ids))) - (setq walk (gethash walk parent-table))) + (setq walk (ebox-runtime-index-get walk parent-table))) walk)) (collect-host-refs (node acc) (if (or (not (listp node)) (stringp node)) @@ -4929,8 +4937,8 @@ Cost is proportional to the replacements, never to the page." (replacement-key (ebox-tree-node-author-key replacement-source-index replacement)) - (parent-id (gethash node-id parent-table)) - (parent (and parent-id (gethash parent-id node-table)))) + (parent-id (ebox-runtime-index-get node-id parent-table)) + (parent (and parent-id (ebox-runtime-index-get parent-id node-table)))) ;; Sibling keys stay unique under the anchor's parent. (when (and replacement-key parent) (dolist (sibling (ebox-tree--children-raw parent)) @@ -5033,7 +5041,7 @@ CURRENT-SNAPSHOT identifies the subtree identities being replaced." conflict) (maphash (lambda (node-id _present) - (when (and (gethash node-id node-table) + (when (and (ebox-runtime-index-get node-id node-table) (not (gethash node-id current-node-ids))) (setq conflict t))) (plist-get historical-snapshot :node-id-set)) @@ -5118,7 +5126,7 @@ DETACHED-HISTORY receives identity snapshots for semantic replacements." (let* ((node-id (ebox-incremental--candidate-replacement-anchor-id entry)) (old-anchor - (gethash node-id (plist-get base-index :node-table)))) + (ebox-runtime-index-get node-id (plist-get base-index :node-table)))) (unless old-anchor (error "Ebox candidate anchor is absent: %S" node-id)) ;; Replacements were validated when they were recorded @@ -5147,7 +5155,7 @@ DETACHED-HISTORY receives identity snapshots for semantic replacements." (let* ((node-id (ebox-incremental--candidate-replacement-anchor-id entry)) (old-anchor - (gethash node-id (plist-get base-index :node-table)))) + (ebox-runtime-index-get node-id (plist-get base-index :node-table)))) (unless old-anchor (error "Ebox candidate anchor is absent: %S" @@ -5251,7 +5259,7 @@ but must re-enter the candidate indexes as a new object." "Return current candidate NODE-ID from path trace or base STATE." (or (plist-get (gethash node-id ebox-incremental--candidate-path-copy-trace) :node) - (gethash node-id (plist-get state :node-table)))) + (ebox-runtime-index-get node-id (plist-get state :node-table)))) (defun ebox-incremental--candidate-range-ref-overlay (old-state path-trace) "Return Range ref table updated only for node anchors in PATH-TRACE." @@ -5268,10 +5276,10 @@ but must re-enter the candidate indexes as a new object." (setq anchors (cl-delete-if (lambda (anchor) - (let ((walk (gethash (car anchor) parent-table)) found) + (let ((walk (ebox-runtime-index-get (car anchor) parent-table)) found) (while (and walk (not found)) (setq found (gethash walk anchor-set) - walk (and (not found) (gethash walk parent-table)))) + walk (and (not found) (ebox-runtime-index-get walk parent-table)))) found)) anchors))) (if (null anchors) @@ -5285,7 +5293,7 @@ but must re-enter the candidate indexes as a new object." (dolist (child (ebox-tree--children-raw node)) (walk child function)))) (dolist (anchor anchors) - (when-let* ((old (gethash (car anchor) + (when-let* ((old (ebox-runtime-index-get (car anchor) (plist-get old-state :node-table)))) (walk old (lambda (_parent record) @@ -5309,7 +5317,7 @@ but must re-enter the candidate indexes as a new object." (let* ((parent-table (plist-get state :parent-table)) (old-child (ebox-incremental--candidate-current-node state node-id)) (new-child replacement) - (parent-id (gethash node-id parent-table))) + (parent-id (ebox-runtime-index-get node-id parent-table))) (ebox-incremental--candidate-record-path-copy node-id replacement parent-id t structure-p) (while parent-id @@ -5348,7 +5356,7 @@ but must re-enter the candidate indexes as a new object." copy) (ebox-tree-copy-with-direct-child-replacements old-parent (list (cons old-child new-child))))) - (grandparent-id (gethash current-parent-id parent-table))) + (grandparent-id (ebox-runtime-index-get current-parent-id parent-table))) (ebox-incremental--candidate-record-path-copy current-parent-id new-parent grandparent-id nil (not (equal (plist-get old-child :node-id) @@ -5391,7 +5399,7 @@ but must re-enter the candidate indexes as a new object." (dolist (parent-id (nreverse order)) (let* ((parent (ebox-incremental--candidate-current-node state parent-id)) - (base-parent (gethash parent-id (plist-get state :node-table))) + (base-parent (ebox-runtime-index-get parent-id (plist-get state :node-table))) (base-sequence (and base-parent (plist-get base-parent :ebox-child-sequence))) @@ -5561,10 +5569,10 @@ but must re-enter the candidate indexes as a new object." "Return PATCH-IDS without a patched ancestor in STATE." (let ((parents (plist-get state :parent-table)) result) (dolist (node-id patch-ids (nreverse result)) - (let ((parent-id (gethash node-id parents)) found) + (let ((parent-id (ebox-runtime-index-get node-id parents)) found) (while (and parent-id (not found)) (setq found (gethash parent-id patch-table) - parent-id (and (not found) (gethash parent-id parents)))) + parent-id (and (not found) (ebox-runtime-index-get parent-id parents)))) (unless found (push node-id result)))))) (defun ebox-incremental--candidate-apply-paint-patches (candidate root) @@ -5587,7 +5595,7 @@ style consumers; the Style Adapter computes their values exactly once later." (let ((current node-id)) (while current (puthash current t patch-paths) - (setq current (gethash current parents))))) + (setq current (ebox-runtime-index-get current parents))))) (cl-labels ((rewrite (node inherited-input-changed-p) @@ -5691,7 +5699,7 @@ style consumers; the Style Adapter computes their values exactly once later." (gethash (ebox-incremental--candidate-range-replacement-ref range) (plist-get (ebox-candidate--base-state candidate) :range-ref-table))) - (parent (gethash (plist-get record :parent-node-id) + (parent (ebox-runtime-index-get (plist-get record :parent-node-id) (plist-get (ebox-candidate--base-state candidate) :node-table))) (segment (and parent @@ -5762,10 +5770,11 @@ Return the same pure surface-input shape as buffer old-state next-root)))) (defun ebox-incremental--hash-keys (table) - "Return the keys currently present in hash TABLE." + "Return the keys currently present in supported TABLE." (let (keys) - (when (hash-table-p table) - (maphash (lambda (key _value) (push key keys)) table)) + (when (ebox-runtime-index-like-p table) + (ebox-runtime-index-map + (lambda (key _value) (push key keys)) table)) (nreverse keys))) (defun ebox-incremental--hash-snapshot (table keys) @@ -5812,7 +5821,7 @@ Return the same pure surface-input shape as (candidate-node-table (plist-get candidate-index :node-table)) (snapshots (make-hash-table :test 'equal))) (when (hash-table-p old-snapshots) - (maphash + (ebox-runtime-index-map (lambda (node-id _node) (when-let* ((snapshot (gethash node-id old-snapshots))) (puthash node-id (copy-sequence snapshot) snapshots))) @@ -6009,14 +6018,17 @@ Return the same pure surface-input shape as (plist-get state :runtime-type-count-table) :selector-index-stale-p (plist-get state :selector-index-stale-p) - :native-node-postorder (plist-get state :native-node-postorder))) + :native-node-postorder (plist-get state :native-node-postorder) + :native-node-postorder-ids + (plist-get state :native-node-postorder-ids))) (defun ebox-incremental--candidate-copy-index-table (state key test) - "Return a materialized copy of STATE's hash table under KEY." + "Return STATE's persistent index under KEY or a fresh legacy table." (let ((table (plist-get state key))) - (if (hash-table-p table) - (copy-hash-table table) - (make-hash-table :test test)))) + (cond + ((ebox-runtime-index-p table) table) + ((hash-table-p table) (copy-hash-table table)) + (t (make-hash-table :test test))))) (defun ebox-incremental--candidate-node-region-record (node) "Return NODE's `(REGION-ID OWNER-ID BOX)' record, or nil." @@ -6064,7 +6076,8 @@ complete preorder first-owner and postorder last-box semantics." (maphash (lambda (region-id _present) (remhash region-id region-id-set) - (remhash region-id region-node-table) + (setq region-node-table + (ebox-runtime-index-delete region-id region-node-table)) (remhash region-id region-box-table)) region-ids) (cl-labels @@ -6075,8 +6088,10 @@ complete preorder first-owner and postorder last-box semantics." (pcase-let ((`(,region-id ,owner-id ,_box) record)) (when (gethash region-id region-ids) (puthash region-id t region-id-set) - (unless (gethash region-id region-node-table) - (puthash region-id owner-id region-node-table))))) + (unless (ebox-runtime-index-get region-id region-node-table) + (setq region-node-table + (ebox-runtime-index-put + region-id owner-id region-node-table)))))) (dolist (child (ebox--node-children node)) (visit child)) (when-let* ((record @@ -6084,7 +6099,8 @@ complete preorder first-owner and postorder last-box semantics." (pcase-let ((`(,region-id ,_owner-id ,box) record)) (when (gethash region-id region-ids) (puthash region-id box region-box-table))))))) - (visit root))) + (visit root)) + (list region-id-set region-node-table region-box-table)) (defun ebox-incremental--candidate-native-postorder (root) "Return ROOT's native compiler postorder without building other indexes." @@ -6092,7 +6108,8 @@ complete preorder first-owner and postorder last-box semantics." (cl-labels ((visit (node native-layout-p) (when (and (listp node) (not (stringp node))) - (let ((type (plist-get node :ebox-type))) + (let ((type (plist-get node :ebox-type)) + (node-id (plist-get node :node-id))) (ebox-tree-for-each-direct-child node (lambda (child) @@ -6102,23 +6119,33 @@ complete preorder first-owner and postorder last-box semantics." (eq child (plist-get node :box)))))))) (when native-layout-p - (push node postorder)))))) + (push node-id postorder)))))) (visit root t)) (vconcat (nreverse postorder)))) -(defun ebox-incremental--candidate-map-native-postorder +(defun ebox-incremental--candidate-retain-native-postorder (old-state candidate-node-table) - "Re-key OLD-STATE's structurally stable native postorder to candidate nodes." + "Return OLD-STATE's stable node-id postorder for CANDIDATE-NODE-TABLE. +The `:native-node-postorder-ids' slot binds the exact read-only vector built +from indexed node IDs. Reuse that vector without enumeration. Unmarked +vectors may contain legacy node plists or IDs; resolve every entry against +the candidate index before converting them. This format marker supplies no +topology proof, and neither producers nor consumers may mutate its vector." (let ((old-postorder (plist-get old-state :native-node-postorder))) - (if (vectorp old-postorder) + (when (vectorp old-postorder) + (if (eq old-postorder (plist-get old-state :native-node-postorder-ids)) + old-postorder (vconcat (mapcar - (lambda (old-node) - (or (gethash (plist-get old-node :node-id) - candidate-node-table) - (error "Ebox candidate lost a structurally retained native node"))) - (append old-postorder nil))) - nil))) + (lambda (entry) + (let ((node-id (if (and (listp entry) + (plist-member entry :node-id)) + (plist-get entry :node-id) + entry))) + (unless (ebox-runtime-index-get node-id candidate-node-table) + (error "Ebox candidate lost a structurally retained native node")) + node-id)) + old-postorder)))))) (defun ebox-incremental--candidate-range-created-node-ids (range-delta-table) "Return new node ids covered by structural RANGE-DELTA-TABLE entries." @@ -6236,7 +6263,7 @@ complete preorder first-owner and postorder last-box semantics." (defun ebox-incremental--candidate-full-index-p (old-state touched) "Return non-nil when TOUCHED justifies rebuilding OLD-STATE's index." (let ((touched-count (length touched)) - (old-count (hash-table-count (plist-get old-state :node-table)))) + (old-count (ebox-runtime-index-size (plist-get old-state :node-table)))) (and (>= touched-count ebox-incremental--candidate-full-index-min-touched) (>= (* 2 touched-count) old-count)))) @@ -6286,9 +6313,9 @@ complete preorder first-owner and postorder last-box semantics." Traversal stops at every node that is `eq' to OLD-STATE's indexed node. The work is therefore bounded by copied ancestor paths plus added, removed, and -replacement subtrees. Core tables are materialized in this stage so existing -scalar `gethash' consumers remain unchanged; publication journaling can later -replace those O(n) table copies without changing this delta contract." +replacement subtrees. Node, parent, and region-owner changes path-copy their +immutable radix indexes; unrelated runtime side tables retain their existing +candidate isolation contracts." (let ((old-root (plist-get old-state :root-node))) (if (eq candidate-root old-root) (list :index (ebox-incremental--candidate-base-index old-state) @@ -6317,7 +6344,7 @@ replace those O(n) table copies without changing this delta contract." touched))) (visit-new (node parent-id) (let* ((node-id (ebox--ensure-node-id node)) - (old-node (gethash node-id old-node-table missing))) + (old-node (ebox-runtime-index-get node-id old-node-table missing))) (unless (or (gethash node-id visited-new) (eq node old-node)) (puthash node-id t visited-new) @@ -6347,7 +6374,7 @@ replace those O(n) table copies without changing this delta contract." (parent-id (plist-get delta :parent-id))) (puthash parent-id t visited-new) (record-touched old-parent new-parent - (gethash parent-id + (ebox-runtime-index-get parent-id (plist-get old-state :parent-table)) (plist-get delta :children-changed)) (dolist (payload (plist-get delta :old-payloads)) @@ -6362,7 +6389,7 @@ replace those O(n) table copies without changing this delta contract." (maphash (lambda (base-node-id trace) (when (plist-get trace :anchor-p) - (let* ((old-node (gethash base-node-id old-node-table)) + (let* ((old-node (ebox-runtime-index-get base-node-id old-node-table)) (new-node (plist-get trace :node)) (parent-id (plist-get trace :parent-id))) (when (and old-node @@ -6381,7 +6408,7 @@ replace those O(n) table copies without changing this delta contract." (unless (gethash node-id visited-new) (puthash node-id t visited-new) (record-touched - (gethash base-node-id old-node-table) + (ebox-runtime-index-get base-node-id old-node-table) new-node (plist-get trace :parent-id) (plist-get trace :structure-p)))))) @@ -6418,8 +6445,8 @@ replace those O(n) table copies without changing this delta contract." ((parent-id (node-id) (if (gethash node-id visited-new) - (gethash node-id touched-parent-table) - (gethash node-id old-parent-table))) + (ebox-runtime-index-get node-id touched-parent-table) + (ebox-runtime-index-get node-id old-parent-table))) (depth (node-id) (or (gethash node-id depth-table) @@ -6475,8 +6502,10 @@ replace those O(n) table copies without changing this delta contract." (let ((node-id (plist-get node :node-id))) (ebox-incremental--adjust-runtime-type-count runtime-type-count-table node -1) - (remhash node-id node-table) - (remhash node-id parent-table) + (setq node-table + (ebox-runtime-index-delete node-id node-table)) + (setq parent-table + (ebox-runtime-index-delete node-id parent-table)) (when-let* ((host-ref (ebox-tree-node-source-identity node))) (when (equal (gethash host-ref host-ref-table) node-id) @@ -6531,13 +6560,17 @@ replace those O(n) table copies without changing this delta contract." (when (hash-table-p old-region-box-count-table) (ebox-incremental--candidate-adjust-region-box-count region-box-count-table old-node -1))) - (puthash node-id new-node node-table) + (setq node-table + (ebox-runtime-index-put node-id new-node node-table)) (when (or (null old-node) (not (eq old-type new-type))) (ebox-incremental--adjust-runtime-type-count runtime-type-count-table new-node 1)) (if parent-id - (puthash node-id parent-id parent-table) - (remhash node-id parent-table)) + (setq parent-table + (ebox-runtime-index-put + node-id parent-id parent-table)) + (setq parent-table + (ebox-runtime-index-delete node-id parent-table))) ;; Replacement subtrees were validated before recording and ;; checked against untouched Host refs before path copying. (when-let* ((new-host-ref @@ -6568,7 +6601,9 @@ replace those O(n) table copies without changing this delta contract." (maphash (lambda (region-id _present) (remhash region-id region-id-set) - (remhash region-id region-node-table) + (setq region-node-table + (ebox-runtime-index-delete + region-id region-node-table)) (remhash region-id region-box-table)) affected-region-ids) ;; TOUCHED is preorder, matching `ebox--runtime-index' ownership: @@ -6579,13 +6614,21 @@ replace those O(n) table copies without changing this delta contract." (nth 1 entry)))) (pcase-let ((`(,region-id ,owner-id ,box) record)) (puthash region-id t region-id-set) - (unless (gethash region-id region-node-table) - (puthash region-id owner-id region-node-table)) + (unless (ebox-runtime-index-get region-id region-node-table) + (setq region-node-table + (ebox-runtime-index-put + region-id owner-id region-node-table))) (puthash region-id box region-box-table)))) (when (> (hash-table-count ambiguous-region-ids) 0) - (ebox-incremental--candidate-recompute-regions - candidate-root ambiguous-region-ids - region-id-set region-node-table region-box-table)) + (pcase-let + ((`(,new-region-id-set ,new-region-node-table + ,new-region-box-table) + (ebox-incremental--candidate-recompute-regions + candidate-root ambiguous-region-ids + region-id-set region-node-table region-box-table))) + (setq region-id-set new-region-id-set + region-node-table new-region-node-table + region-box-table new-region-box-table))) (let* ((range-delta-table (make-hash-table :test #'equal)) (_range-deltas (dolist (delta range-index-deltas) @@ -6613,7 +6656,7 @@ replace those O(n) table copies without changing this delta contract." (if (or range-index-deltas structure-p) (ebox-incremental--candidate-native-postorder candidate-root) - (ebox-incremental--candidate-map-native-postorder + (ebox-incremental--candidate-retain-native-postorder old-state node-table))) (index (list :node-table node-table @@ -6637,7 +6680,8 @@ replace those O(n) table copies without changing this delta contract." candidate-source-index 'type) :runtime-type-count-table runtime-type-count-table :selector-index-stale-p t - :native-node-postorder native-node-postorder))) + :native-node-postorder native-node-postorder + :native-node-postorder-ids native-node-postorder))) (list :index index :dirty-set dirty-set :participation-node-ids @@ -6651,6 +6695,8 @@ replace those O(n) table copies without changing this delta contract." (mapcar (lambda (node) (plist-get node :node-id)) removed) + :touched-region-ids + (ebox-incremental--hash-keys affected-region-ids) :touched-count (length touched) :removed-count (length removed)))))))))) @@ -6661,7 +6707,7 @@ The search stops at the first shared node, so a copy-on-write candidate is recognized from its first untouched sibling without walking the whole tree." (let ((old-root (plist-get old-state :root-node)) (old-node-table (plist-get old-state :node-table))) - (and (hash-table-p old-node-table) + (and (ebox-runtime-index-like-p old-node-table) (catch 'shared (cl-labels ((visit (node) @@ -6669,7 +6715,7 @@ recognized from its first untouched sibling without walking the whole tree." (let ((node-id (plist-get node :node-id))) (when (and node-id (not (eq node old-root)) - (eq node (gethash node-id old-node-table))) + (eq node (ebox-runtime-index-get node-id old-node-table))) (throw 'shared t))) (dolist (child (ebox-tree--children-raw node)) (visit child))))) @@ -6795,9 +6841,10 @@ CANDIDATE-INDEX is the prepared runtime index for CANDIDATE-ROOT." :new-region-ids (ebox--node-all-region-ids candidate-root) :old old-root :new candidate-root) dirty) - (maphash - (lambda (node-id new-node) - (when-let* ((old-node (gethash node-id old-table))) + (dolist (node-id (ebox--runtime-node-ids candidate-root)) + (let ((new-node (ebox-runtime-index-get node-id new-table))) + (when-let* ((old-node + (ebox-runtime-index-get node-id old-table))) (let* ((changed-keys (ebox-incremental--candidate-local-changed-keys old-node (plist-get old-state :source-index) @@ -6832,8 +6879,7 @@ CANDIDATE-INDEX is the prepared runtime index for CANDIDATE-ROOT." (ebox-tree-node-local-source-signature new-node)) (ebox-incremental--child-change-props old-node new-node children-changed)) - dirty))))) - new-table)) + dirty))))))) (nreverse dirty))) (defun ebox-incremental--isolate-candidate-caches (state) @@ -6933,7 +6979,9 @@ candidate therefore owns its cache tables until TP commits the generation." (:selector-index-stale-p ,(plist-get candidate-index :selector-index-stale-p)) (:native-node-postorder - ,(plist-get candidate-index :native-node-postorder)))) + ,(plist-get candidate-index :native-node-postorder)) + (:native-node-postorder-ids + ,(plist-get candidate-index :native-node-postorder-ids)))) (setq state (plist-put state (car entry) (cadr entry)))) (ebox-incremental--isolate-candidate-caches state))) @@ -6944,11 +6992,11 @@ candidate therefore owns its cache tables until TP commits the generation." (let ((candidate-cache (make-hash-table :test 'eq)) (missing (make-symbol "ebox-candidate-cache-missing"))) (when (and (hash-table-p old-cache) - (hash-table-p old-node-table) - (hash-table-p candidate-node-table)) - (maphash + (ebox-runtime-index-like-p old-node-table) + (ebox-runtime-index-like-p candidate-node-table)) + (ebox-runtime-index-map (lambda (node-id candidate-node) - (when-let* ((old-node (gethash node-id old-node-table))) + (when-let* ((old-node (ebox-runtime-index-get node-id old-node-table))) (let ((cached (gethash old-node old-cache missing))) (unless (eq cached missing) (puthash candidate-node cached candidate-cache))))) @@ -6972,7 +7020,7 @@ must be recomputed in the next publication." old-node-table old-parent-table candidate-cache nil)) (dolist (node-id (append touched-node-ids removed-node-ids)) - (when-let* ((old-node (gethash node-id old-node-table))) + (when-let* ((old-node (ebox-runtime-index-get node-id old-node-table))) (remhash old-node candidate-cache))) candidate-cache)) @@ -7026,8 +7074,8 @@ derive render changes from the computed fragment style signatures." (mapcar (lambda (entry) (let* ((node-id (plist-get entry :node-id)) - (old-node (and node-id (gethash node-id old-nodes))) - (new-node (and node-id (gethash node-id new-nodes)))) + (old-node (and node-id (ebox-runtime-index-get node-id old-nodes))) + (new-node (and node-id (ebox-runtime-index-get node-id new-nodes)))) (if (or (eq (plist-get entry :dirty-kind) 'structure) (not old-node) (not new-node) @@ -7156,7 +7204,7 @@ certificate." (with-current-buffer buffer (dolist (entry dirty-set) (when (memq (plist-get entry :dirty-kind) '(geometry structure)) - (when (gethash (plist-get entry :node-id) + (when (ebox-runtime-index-get (plist-get entry :node-id) (plist-get old-state :node-table)) (ebox--ensure-layout-snapshot-spans buffer (plist-get entry :node-id))))) @@ -7454,7 +7502,7 @@ the expensive parent-context mount and role scan first." (node-id candidate-id) eligible) (while (and node-id (not (equal node-id root-id)) (not eligible)) - (let* ((node (gethash node-id (plist-get old-state :node-table))) + (let* ((node (ebox-runtime-index-get node-id (plist-get old-state :node-table))) (formatting-p (and node (ebox-incremental--formatting-context-reflow-node-p node))) @@ -7475,7 +7523,7 @@ the expensive parent-context mount and role scan first." (ebox--line-spans-cover-whole-lines-p spans))) (setq eligible t)) (unless eligible - (setq node-id (gethash node-id parents))))) + (setq node-id (ebox-runtime-index-get node-id parents))))) ) eligible)) @@ -7521,11 +7569,11 @@ line count; its outside siblings are shifted by the one scoped replacement." dirty-set)) (while (and candidate-id (not proof) (not (equal candidate-id root-id))) - (let* ((old-node (gethash candidate-id + (let* ((old-node (ebox-runtime-index-get candidate-id (plist-get old-state :node-table))) - (new-node (gethash candidate-id + (new-node (ebox-runtime-index-get candidate-id (plist-get candidate-state :node-table))) - (parent-id (gethash candidate-id parents)) + (parent-id (ebox-runtime-index-get candidate-id parents)) (snapshot (and old-node (ebox--ensure-layout-snapshot-details @@ -7544,25 +7592,25 @@ line count; its outside siblings are shifted by the one scoped replacement." (plist-get mount :end)) mounts))))) (parent (and parent-id - (gethash parent-id + (ebox-runtime-index-get parent-id (plist-get old-state :node-table)))) (parent-new (and parent-id - (gethash parent-id + (ebox-runtime-index-get parent-id (plist-get candidate-state :node-table)))) (ancestors-stable-p t) (walk parent-id)) (while (and walk ancestors-stable-p (not (equal walk root-id))) - (let ((ancestor (gethash walk (plist-get old-state :node-table)))) + (let ((ancestor (ebox-runtime-index-get walk (plist-get old-state :node-table)))) (when (or (not ancestor) (memq (ebox-tree-display-inner ancestor) '(row flex grid)) (ebox-tree-node-visible-overflow-p ancestor)) (setq ancestors-stable-p nil))) - (setq walk (gethash walk parents))) + (setq walk (ebox-runtime-index-get walk parents))) (when (and old-node new-node parent parent-new (ebox-incremental--formatting-context-reflow-node-p old-node) - (equal (gethash candidate-id parents) - (gethash candidate-id candidate-parents)) + (equal (ebox-runtime-index-get candidate-id parents) + (ebox-runtime-index-get candidate-id candidate-parents)) (ebox-incremental--node-child-ids-equal-p old-node new-node) (equal (ebox-incremental--layout-slot-style-signature @@ -7598,7 +7646,7 @@ line count; its outside siblings are shifted by the one scoped replacement." (plist-get old-state :layout-snapshot-detail-generation)))) (unless proof - (setq candidate-id (gethash candidate-id parents)))))) + (setq candidate-id (ebox-runtime-index-get candidate-id parents)))))) proof)) (defun ebox-incremental--two-owner-allocation-proof @@ -7852,14 +7900,14 @@ suppresses the ordinary fallback when any fact is missing." :border-bottom-color))) (plist-get entry :changed-keys)) (let* ((node-id (plist-get entry :node-id)) - (old-node (gethash node-id old-nodes)) - (new-node (gethash node-id new-nodes))) + (old-node (ebox-runtime-index-get node-id old-nodes)) + (new-node (ebox-runtime-index-get node-id new-nodes))) (and old-node new-node (ebox-incremental--node-child-ids-equal-p old-node new-node) - (equal (gethash node-id + (equal (ebox-runtime-index-get node-id (plist-get old-state :parent-table)) - (gethash node-id + (ebox-runtime-index-get node-id (plist-get candidate-state :parent-table))) (equal (ebox-incremental--layout-slot-style-signature @@ -7907,7 +7955,7 @@ suppresses the ordinary fallback when any fact is missing." (not (plist-get prepared :scroll-patch-fast-p)) root-id (not (equal geometry-owner-id root-id)) - (> (hash-table-count old-nodes) + (> (ebox-runtime-index-size old-nodes) (length (delete-dups (append geometry-owner-ids paint-owner-ids))))) (list :geometry-proof geometry-proof @@ -7919,7 +7967,7 @@ suppresses the ordinary fallback when any fact is missing." :dirty-set (copy-sequence dirty-set) :owner-plan (copy-tree owner-plan) :outside-complement-count - (- (hash-table-count old-nodes) + (- (ebox-runtime-index-size old-nodes) (length (delete-dups (append geometry-owner-ids paint-owner-ids)))))))) @@ -7938,7 +7986,7 @@ because it could address a run whose topology the closure just replaced." (let ((walk node-id)) (while (and valid walk) (when (gethash walk geometry-set) (setq valid nil)) - (setq walk (and valid (gethash walk parents)))))) + (setq walk (and valid (ebox-runtime-index-get walk parents)))))) valid)) (defun ebox-incremental--allocation-closure-ancestor-stable-p @@ -7951,12 +7999,12 @@ because it could address a run whose topology the closure just replaced." (node-id owner-id) (valid t)) (while (and valid node-id) - (let* ((old-node (gethash node-id old-nodes)) - (new-node (gethash node-id new-nodes)) - (old-parent (gethash node-id old-parents))) + (let* ((old-node (ebox-runtime-index-get node-id old-nodes)) + (new-node (ebox-runtime-index-get node-id new-nodes)) + (old-parent (ebox-runtime-index-get node-id old-parents))) (setq valid (and old-node new-node - (equal old-parent (gethash node-id new-parents)) + (equal old-parent (ebox-runtime-index-get node-id new-parents)) (ebox-incremental--node-child-ids-equal-p old-node new-node) (equal @@ -8016,15 +8064,15 @@ because it could address a run whose topology the closure just replaced." old-state candidate-state dirty-set))) (while (and candidate-id path-valid-p (not proof) (not (equal candidate-id root-id))) - (let* ((old-node (gethash candidate-id old-nodes)) - (new-node (gethash candidate-id new-nodes)) - (parent-id (gethash candidate-id parents)) + (let* ((old-node (ebox-runtime-index-get candidate-id old-nodes)) + (new-node (ebox-runtime-index-get candidate-id new-nodes)) + (parent-id (ebox-runtime-index-get candidate-id parents)) (display (and old-node (ebox-tree-display-inner old-node)))) (setq saw-flex-p (or saw-flex-p (eq display 'flex))) (setq path-valid-p (and old-node new-node (equal parent-id - (gethash candidate-id candidate-parents)) + (ebox-runtime-index-get candidate-id candidate-parents)) (ebox-incremental--node-child-ids-equal-p old-node new-node) (equal @@ -8288,7 +8336,7 @@ role, overflow, and outside-complement validation." (parent-table (plist-get (ebox--buffer-render-state buffer) :parent-table)) ops) - (unless (hash-table-p parent-table) + (unless (ebox-runtime-index-like-p parent-table) (signal 'ebox-patch-plan-error (list :missing-parent-table buffer))) (ebox--with-layout-snapshot-detail-context buffer @@ -8468,7 +8516,7 @@ closure may still plan one for a later owner-scoped surface projection." (mapcar (lambda (op) (plist-get op :op)) owner-plan) :owner-ids owner-ids :owner-type - (plist-get (gethash single-owner node-table) :ebox-type) + (plist-get (ebox-runtime-index-get single-owner node-table) :ebox-type) :span-count (cl-loop for owner-id in owner-ids for snapshot = (and snapshots @@ -8492,18 +8540,8 @@ closure may still plan one for a later owner-scoped surface projection." dirty-set))) (defun ebox-incremental--hash-key-set-equal-p (left right) - "Return non-nil when hash tables LEFT and RIGHT have the same keys." - (and (hash-table-p left) - (hash-table-p right) - (= (hash-table-count left) (hash-table-count right)) - (let ((equal-p t) - (missing (make-symbol "ebox-missing-key"))) - (maphash - (lambda (key _value) - (when (eq (gethash key right missing) missing) - (setq equal-p nil))) - left) - equal-p))) + "Return non-nil when supported indexes LEFT and RIGHT have the same keys." + (ebox-runtime-index-key-set-equal-p left right)) (defun ebox-incremental--span-patch-identity-stable-p (buffer old-state candidate-state prepared owner-id) @@ -8524,7 +8562,7 @@ the surface is allowed to reuse retained TP objects." (plist-get prepared :dirty-set)))) (while walk (push walk ids) - (setq walk (gethash walk old-parent-table))) + (setq walk (ebox-runtime-index-get walk old-parent-table))) (delete-dups ids)) (delete-dups (append (plist-get prepared :touched-node-ids) @@ -8538,8 +8576,8 @@ the surface is allowed to reuse retained TP objects." (plist-get candidate-state :region-id-set))) (cl-every (lambda (node-id) - (let ((old-node (gethash node-id old-node-table)) - (new-node (gethash node-id new-node-table))) + (let ((old-node (ebox-runtime-index-get node-id old-node-table)) + (new-node (ebox-runtime-index-get node-id new-node-table))) (and old-node new-node (eq (plist-get old-node :ebox-type) (plist-get new-node :ebox-type)) @@ -8548,8 +8586,8 @@ the surface is allowed to reuse retained TP objects." (plist-get old-state :source-index) old-node) (ebox-tree-node-author-key (plist-get candidate-state :source-index) new-node)) - (equal (gethash node-id old-parent-table) - (gethash node-id new-parent-table))))) + (equal (ebox-runtime-index-get node-id old-parent-table) + (ebox-runtime-index-get node-id new-parent-table))))) (delq nil touched))))) (defun ebox-incremental--path-span-line-count-stable-p @@ -8710,10 +8748,10 @@ the complete proof." (node-id (plist-get entry :node-id)) (next-id (and (cdr entries) (plist-get (cadr entries) :node-id))) - (old-node (and node-id (gethash node-id old-nodes))) - (new-node (and node-id (gethash node-id new-nodes))) - (old-parent (and node-id (gethash node-id old-parents))) - (new-parent (and node-id (gethash node-id new-parents))) + (old-node (and node-id (ebox-runtime-index-get node-id old-nodes))) + (new-node (and node-id (ebox-runtime-index-get node-id new-nodes))) + (old-parent (and node-id (ebox-runtime-index-get node-id old-parents))) + (new-parent (and node-id (ebox-runtime-index-get node-id new-parents))) (allocated-style (plist-get entry :allocated-style))) (setq valid (and old-node new-node allocated-style @@ -8762,10 +8800,10 @@ candidate's unrendered caches." signature valid) (setq valid t) (while (and valid node-id) - (let* ((old-node (gethash node-id old-nodes)) - (new-node (gethash node-id new-nodes)) - (old-parent (gethash node-id old-parents)) - (new-parent (gethash node-id new-parents)) + (let* ((old-node (ebox-runtime-index-get node-id old-nodes)) + (new-node (ebox-runtime-index-get node-id new-nodes)) + (old-parent (ebox-runtime-index-get node-id old-parents)) + (new-parent (ebox-runtime-index-get node-id new-parents)) (snapshot (and old-node (ebox-incremental--cached-allocation-snapshot buffer node-id))) @@ -8859,28 +8897,28 @@ complete published ancestor slot chain remains identical." (let* ((old-state (ebox-incremental--proof-base-state buffer)) (node-id (plist-get entry :node-id)) (old-node (and node-id - (gethash node-id (plist-get old-state :node-table)))) + (ebox-runtime-index-get node-id (plist-get old-state :node-table)))) (new-node (and node-id - (gethash node-id + (ebox-runtime-index-get node-id (plist-get candidate-state :node-table)))) (parent-table (plist-get old-state :parent-table)) (candidate-parents (plist-get candidate-state :parent-table)) - (ancestor-id (and node-id (gethash node-id parent-table))) + (ancestor-id (and node-id (ebox-runtime-index-get node-id parent-table))) grid-id old-grid new-grid) (while (and ancestor-id (not grid-id)) (let ((old-ancestor - (gethash ancestor-id (plist-get old-state :node-table))) + (ebox-runtime-index-get ancestor-id (plist-get old-state :node-table))) (new-ancestor - (gethash ancestor-id (plist-get candidate-state :node-table)))) + (ebox-runtime-index-get ancestor-id (plist-get candidate-state :node-table)))) (if (and old-ancestor new-ancestor - (equal (gethash ancestor-id parent-table) - (gethash ancestor-id candidate-parents)) + (equal (ebox-runtime-index-get ancestor-id parent-table) + (ebox-runtime-index-get ancestor-id candidate-parents)) (eq (ebox-tree-display-inner old-ancestor) 'grid) (eq (ebox-tree-display-inner new-ancestor) 'grid)) (setq grid-id ancestor-id old-grid old-ancestor new-grid new-ancestor) - (setq ancestor-id (gethash ancestor-id parent-table))))) + (setq ancestor-id (ebox-runtime-index-get ancestor-id parent-table))))) (let* ((old-props (and old-grid (ebox-tree-layout-props old-grid))) (new-props (and new-grid (ebox-tree-layout-props new-grid))) (tracks (and old-props @@ -8922,16 +8960,16 @@ following sibling. The surface footprint validator remains the final gate." (let* ((old-state (ebox-incremental--proof-base-state buffer)) (node-id (plist-get entry :node-id)) (old-node (and node-id - (gethash node-id (plist-get old-state :node-table)))) + (ebox-runtime-index-get node-id (plist-get old-state :node-table)))) (new-node (and node-id - (gethash node-id (plist-get candidate-state :node-table)))) + (ebox-runtime-index-get node-id (plist-get candidate-state :node-table)))) (parents (plist-get old-state :parent-table)) (candidate-parents (plist-get candidate-state :parent-table)) - (parent-id (and node-id (gethash node-id parents))) + (parent-id (and node-id (ebox-runtime-index-get node-id parents))) (old-parent (and parent-id - (gethash parent-id (plist-get old-state :node-table)))) + (ebox-runtime-index-get parent-id (plist-get old-state :node-table)))) (new-parent (and parent-id - (gethash parent-id (plist-get candidate-state :node-table)))) + (ebox-runtime-index-get parent-id (plist-get candidate-state :node-table)))) (old-children (and old-parent (ebox-tree--children-raw old-parent))) (position (and old-children (cl-position-if @@ -8949,8 +8987,8 @@ following sibling. The surface footprint validator remains the final gate." (ebox-incremental--ancestor-slot-signature buffer old-state candidate-state node-id)))) (and old-state old-node new-node old-parent new-parent - (equal (gethash parent-id parents) - (gethash parent-id candidate-parents)) + (equal (ebox-runtime-index-get parent-id parents) + (ebox-runtime-index-get parent-id candidate-parents)) (ebox-incremental--node-child-ids-equal-p old-parent new-parent) (equal (ebox-incremental--layout-slot-style-signature old-parent) (ebox-incremental--layout-slot-style-signature new-parent)) @@ -9007,17 +9045,17 @@ proving that changed descendants still fit this slot exactly." (new-nodes (plist-get candidate-state :node-table)) (old-parents (plist-get old-state :parent-table)) (new-parents (plist-get candidate-state :parent-table)) - (old-node (and owner-id (gethash owner-id old-nodes))) - (new-node (and owner-id (gethash owner-id new-nodes))) - (parent-id (and owner-id (gethash owner-id old-parents))) - (old-parent (and parent-id (gethash parent-id old-nodes))) - (new-parent (and parent-id (gethash parent-id new-nodes))) + (old-node (and owner-id (ebox-runtime-index-get owner-id old-nodes))) + (new-node (and owner-id (ebox-runtime-index-get owner-id new-nodes))) + (parent-id (and owner-id (ebox-runtime-index-get owner-id old-parents))) + (old-parent (and parent-id (ebox-runtime-index-get parent-id old-nodes))) + (new-parent (and parent-id (ebox-runtime-index-get parent-id new-nodes))) (snapshot (and old-node (ebox-incremental--cached-allocation-snapshot buffer owner-id)))) (and old-node new-node old-parent new-parent snapshot - (equal parent-id (gethash owner-id new-parents)) + (equal parent-id (ebox-runtime-index-get owner-id new-parents)) (eq (ebox-tree-display-inner old-parent) 'column) (eq (ebox-tree-display-inner new-parent) 'column) (ebox-incremental--node-child-ids-equal-p old-node new-node) @@ -9058,17 +9096,17 @@ style, and the published ancestor slot chain remain mandatory." (let* ((old-state (ebox-incremental--proof-base-state buffer)) (node-id (plist-get entry :node-id)) (old-node (and node-id - (gethash node-id (plist-get old-state :node-table)))) + (ebox-runtime-index-get node-id (plist-get old-state :node-table)))) (new-node (and node-id - (gethash node-id (plist-get candidate-state + (ebox-runtime-index-get node-id (plist-get candidate-state :node-table)))) (parents (plist-get old-state :parent-table)) (candidate-parents (plist-get candidate-state :parent-table)) - (parent-id (and node-id (gethash node-id parents))) + (parent-id (and node-id (ebox-runtime-index-get node-id parents))) (old-parent (and parent-id - (gethash parent-id (plist-get old-state :node-table)))) + (ebox-runtime-index-get parent-id (plist-get old-state :node-table)))) (new-parent (and parent-id - (gethash parent-id + (ebox-runtime-index-get parent-id (plist-get candidate-state :node-table)))) (snapshot (and old-node (ebox-incremental--cached-allocation-snapshot @@ -9079,8 +9117,8 @@ style, and the published ancestor slot chain remain mandatory." (eq (ebox-tree-display-inner old-parent) (ebox-tree-display-inner new-parent)) (memq (ebox-tree-display-inner old-parent) '(row flex)) - (equal (gethash parent-id parents) - (gethash parent-id candidate-parents)) + (equal (ebox-runtime-index-get parent-id parents) + (ebox-runtime-index-get parent-id candidate-parents)) (ebox-incremental--node-child-ids-equal-p old-parent new-parent) (equal (ebox-incremental--layout-slot-style-signature old-parent) (ebox-incremental--layout-slot-style-signature new-parent)) @@ -9113,17 +9151,17 @@ the complete snapshot proof." (new-nodes (plist-get candidate-state :node-table)) (old-parents (plist-get old-state :parent-table)) (new-parents (plist-get candidate-state :parent-table)) - (node-id (gethash grid-id old-parents)) + (node-id (ebox-runtime-index-get grid-id old-parents)) (seen nil) (valid (and node-id t))) (while (and valid node-id) (if (memq node-id seen) (setq valid nil) (push node-id seen) - (let* ((old-node (gethash node-id old-nodes)) - (new-node (gethash node-id new-nodes)) - (old-parent (gethash node-id old-parents)) - (new-parent (gethash node-id new-parents)) + (let* ((old-node (ebox-runtime-index-get node-id old-nodes)) + (new-node (ebox-runtime-index-get node-id new-nodes)) + (old-parent (ebox-runtime-index-get node-id old-parents)) + (new-parent (ebox-runtime-index-get node-id new-parents)) (display (and old-node (ebox-tree-display-inner old-node))) (old-width (and old-node (ebox-get old-node :width))) @@ -9156,22 +9194,22 @@ projection kinds and the surface output validator remains authoritative." (let* ((old-state (ebox-incremental--proof-base-state buffer)) (node-id (plist-get entry :node-id)) (old-node (and node-id - (gethash node-id (plist-get old-state :node-table)))) + (ebox-runtime-index-get node-id (plist-get old-state :node-table)))) (new-node (and node-id - (gethash node-id + (ebox-runtime-index-get node-id (plist-get candidate-state :node-table)))) (parents (plist-get old-state :parent-table)) (candidate-parents (plist-get candidate-state :parent-table)) - (ancestor-id (and node-id (gethash node-id parents))) + (ancestor-id (and node-id (ebox-runtime-index-get node-id parents))) (child-id node-id) grid-id old-grid new-grid) (while (and ancestor-id (not grid-id)) - (let* ((old-ancestor (gethash ancestor-id + (let* ((old-ancestor (ebox-runtime-index-get ancestor-id (plist-get old-state :node-table))) - (new-ancestor (gethash ancestor-id + (new-ancestor (ebox-runtime-index-get ancestor-id (plist-get candidate-state :node-table))) - (parent-id (gethash ancestor-id parents)) - (new-parent-id (gethash ancestor-id candidate-parents))) + (parent-id (ebox-runtime-index-get ancestor-id parents)) + (new-parent-id (ebox-runtime-index-get ancestor-id candidate-parents))) (if (and old-ancestor new-ancestor (equal parent-id new-parent-id) (not (ebox-tree-node-visible-overflow-p old-ancestor))) @@ -9295,8 +9333,8 @@ metadata; this predicate only authorizes the local attempt." (dirty (car (plist-get prepared :dirty-set))) (owner-id (plist-get operation :owner-id)) (changed-keys (plist-get dirty :changed-keys)) - (old-node (gethash owner-id (plist-get old-state :node-table))) - (new-node (gethash owner-id (plist-get candidate-state :node-table))) + (old-node (ebox-runtime-index-get owner-id (plist-get old-state :node-table))) + (new-node (ebox-runtime-index-get owner-id (plist-get candidate-state :node-table))) (slot-dirty (plist-put (copy-sequence dirty) :node-id owner-id)) (snapshot @@ -9549,8 +9587,8 @@ role, and outside-complement compatibility before publication." (owner-id (plist-get operation :owner-id)) (dirty-set (plist-get prepared :dirty-set)) (root-id (ebox--buffer-root-node-id buffer)) - (old-node (gethash owner-id (plist-get old-state :node-table))) - (new-node (gethash owner-id + (old-node (ebox-runtime-index-get owner-id (plist-get old-state :node-table))) + (new-node (ebox-runtime-index-get owner-id (plist-get candidate-state :node-table))) (fixed-flex-slot-p (ebox--flex-item-fixed-basis-content-allocation-stable-p @@ -9662,9 +9700,9 @@ role, and outside-complement compatibility before publication." ;; a second proof render here would only duplicate work. (or owner-slot-proof fixed-flex-ancestor-proof content-slot-proof) - (equal (gethash owner-id + (equal (ebox-runtime-index-get owner-id (plist-get old-state :parent-table)) - (gethash owner-id + (ebox-runtime-index-get owner-id (plist-get candidate-state :parent-table))) (ebox-incremental--node-child-ids-equal-p old-node new-node) @@ -9719,8 +9757,8 @@ role, and outside-complement compatibility before publication." (dirty (car (plist-get prepared :dirty-set))) (delta (plist-get dirty :range-delta)) (owner-id (plist-get operation :owner-id)) - (old-node (gethash owner-id (plist-get old-state :node-table))) - (new-node (gethash owner-id + (old-node (ebox-runtime-index-get owner-id (plist-get old-state :node-table))) + (new-node (ebox-runtime-index-get owner-id (plist-get candidate-state :node-table))) (old-items (and delta (apply #'append (plist-get delta :old-payloads)))) @@ -9751,8 +9789,8 @@ role, and outside-complement compatibility before publication." (cl-every (lambda (item) (eq (plist-get item :ebox-kind) 'box)) new-items) - (equal (gethash owner-id (plist-get old-state :parent-table)) - (gethash owner-id + (equal (ebox-runtime-index-get owner-id (plist-get old-state :parent-table)) + (ebox-runtime-index-get owner-id (plist-get candidate-state :parent-table))) (equal (ebox-incremental--layout-slot-style-signature old-node) (ebox-incremental--layout-slot-style-signature new-node)) @@ -10046,6 +10084,8 @@ role, and outside-complement compatibility before publication." (plist-get candidate-state :selector-index-stale-p) :native-node-postorder (plist-get candidate-state :native-node-postorder) + :native-node-postorder-ids + (plist-get candidate-state :native-node-postorder-ids) :surface-node-object-table (and (memq projection-kind '(span-patch owner-scoped scroll-patch @@ -10627,6 +10667,7 @@ When RETAIN-NATIVE-P is non-nil, preserve its committed native frame bases." :selector-type-table (plist-get state :selector-type-table) :runtime-type-count-table (plist-get state :runtime-type-count-table) :native-node-postorder (plist-get state :native-node-postorder) + :native-node-postorder-ids (plist-get state :native-node-postorder-ids) :runtime-index-prepared-p t :scroll-state-table (plist-get state :scroll-state-table) :detached-identity-history @@ -10695,7 +10736,7 @@ topology changes; those continue to use the conservative full projection." (region-id (and (= (length scroll-ids) 1) (car scroll-ids))) (region-node-id (and region-id region-node-table - (gethash region-id region-node-table))) + (ebox-runtime-index-get region-id region-node-table))) (candidate-scroll-table (plist-get candidate-state :scroll-state-table))) (and old-root candidate-root @@ -10734,13 +10775,13 @@ the producer's content geometry is the retained boundary." (node-table (plist-get state :node-table)) (scroll-state-table (plist-get state :scroll-state-table))) (or (null scroll-ids) - (and (hash-table-p region-node-table) - (hash-table-p node-table) + (and (ebox-runtime-index-like-p region-node-table) + (ebox-runtime-index-like-p node-table) (hash-table-p scroll-state-table) (cl-every (lambda (region-id) - (let* ((node-id (gethash region-id region-node-table)) - (node (and node-id (gethash node-id node-table)))) + (let* ((node-id (ebox-runtime-index-get region-id region-node-table)) + (node (and node-id (ebox-runtime-index-get node-id node-table)))) (and node (gethash region-id scroll-state-table) (not (ebox--viewport-dependent-subtree-p node)) @@ -10758,8 +10799,8 @@ fresh by the mixed projection." (scroll-state-table (plist-get state :scroll-state-table)) (stable nil) (affected nil)) - (when (and (hash-table-p region-node-table) - (hash-table-p node-table) + (when (and (ebox-runtime-index-like-p region-node-table) + (ebox-runtime-index-like-p node-table) (hash-table-p scroll-state-table)) (let ((ebox--viewport-dependent-node-ids-cache (make-hash-table :test #'eq)) @@ -10768,8 +10809,8 @@ fresh by the mixed projection." (ebox--viewport-height-dependent-subtree-cache (make-hash-table :test #'eq))) (dolist (region-id (plist-get state :scroll-region-ids)) - (let* ((node-id (gethash region-id region-node-table)) - (node (and node-id (gethash node-id node-table))) + (let* ((node-id (ebox-runtime-index-get region-id region-node-table)) + (node (and node-id (ebox-runtime-index-get node-id node-table))) (scroll-state (gethash region-id scroll-state-table)) (stable-p (and node scroll-state @@ -10821,6 +10862,8 @@ fresh by the mixed projection." (plist-get candidate-state :selector-index-stale-p) :native-node-postorder (plist-get candidate-state :native-node-postorder) + :native-node-postorder-ids + (plist-get candidate-state :native-node-postorder-ids) :runtime-index-prepared-p t :surface-node-object-table (plist-get old-state :surface-node-object-table)))) diff --git a/ebox-layout.el b/ebox-layout.el index ee85509..553f601 100644 --- a/ebox-layout.el +++ b/ebox-layout.el @@ -15,6 +15,7 @@ (require 'ebox-fragment) (require 'ebox-render-context) (require 'ebox-buffer-backend) +(require 'ebox-runtime-index) (declare-function ebox--render-grid "ebox-grid" (node)) (declare-function ebox--render-grid-box-children @@ -1982,8 +1983,9 @@ also pass a pre-rendered string while an active surface owns runtime facts." (cl-loop for state in ebox--render-source-states for table = (plist-get state :node-table) - when (and (hash-table-p table) - (eq node (gethash node-id table))) + when (and (ebox-runtime-index-like-p table) + (eq node + (ebox-runtime-index-get node-id table))) return (plist-get state :source-index))) (and handle (cl-find-if diff --git a/ebox-native-commit.el b/ebox-native-commit.el index 92dd302..f1e76c4 100644 --- a/ebox-native-commit.el +++ b/ebox-native-commit.el @@ -11,6 +11,7 @@ ;;; Code: (require 'cl-lib) +(require 'ebox-runtime-index) (declare-function ebox-native-reflow-layout-ready-p "ebox-native-reflow" ()) (declare-function ebox-native-reflow-execute-sync @@ -45,7 +46,7 @@ "ebox-incremental" (node)) (defun ebox-native-commit-topology-stable-p - (old-state candidate-state prepared) + (old-state candidate-state prepared) "Return non-nil when PREPARED preserves OLD-STATE's object topology." (let ((old-nodes (plist-get old-state :node-table)) (new-nodes (plist-get candidate-state :node-table)) @@ -62,22 +63,22 @@ (lambda (entry) (eq (plist-get entry :dirty-kind) 'structure)) (plist-get prepared :dirty-set))) - (hash-table-p old-nodes) (hash-table-p new-nodes) - (hash-table-p old-parents) (hash-table-p new-parents) - (hash-table-p old-regions) (hash-table-p new-regions) + (ebox-runtime-index-like-p old-nodes) (ebox-runtime-index-like-p new-nodes) + (ebox-runtime-index-like-p old-parents) (ebox-runtime-index-like-p new-parents) + (ebox-runtime-index-like-p old-regions) (ebox-runtime-index-like-p new-regions) (hash-table-p old-objects) - (= (hash-table-count old-nodes) - (hash-table-count new-nodes)) - (= (hash-table-count old-regions) - (hash-table-count new-regions)))) + (= (ebox-runtime-index-size old-nodes) + (ebox-runtime-index-size new-nodes)) + (= (ebox-runtime-index-size old-regions) + (ebox-runtime-index-size new-regions)))) (when stable - (maphash + (ebox-runtime-index-map (lambda (node-id node) - (let ((old-node (gethash node-id old-nodes missing))) + (let ((old-node (ebox-runtime-index-get node-id old-nodes missing))) (unless (and (not (eq old-node missing)) (gethash node-id old-objects) - (equal (gethash node-id old-parents missing) - (gethash node-id new-parents missing)) + (equal (ebox-runtime-index-get node-id old-parents missing) + (ebox-runtime-index-get node-id new-parents missing)) (eq (plist-get old-node :ebox-type) (plist-get node :ebox-type)) (equal @@ -88,9 +89,9 @@ (setq stable nil)))) new-nodes)) (when stable - (maphash + (ebox-runtime-index-map (lambda (region-id node-id) - (unless (equal (gethash region-id old-regions missing) node-id) + (unless (equal (ebox-runtime-index-get region-id old-regions missing) node-id) (setq stable nil))) new-regions)) stable)) @@ -105,7 +106,7 @@ (while (and node-id (not (gethash node-id seen))) (puthash node-id t seen) (push node-id result) - (setq node-id (and parents (gethash node-id parents)))))) + (setq node-id (and parents (ebox-runtime-index-get node-id parents)))))) (nreverse result))) (defun ebox-native-commit-inherited-dirty-node-ids (prepared) @@ -123,8 +124,8 @@ "Return NODE-ID to `(PARENT-ID . POSITION)' table for STATE." (let ((nodes (plist-get state :node-table)) (table (make-hash-table :test 'equal))) - (when (hash-table-p nodes) - (maphash + (when (ebox-runtime-index-like-p nodes) + (ebox-runtime-index-map (lambda (parent-id node) (cl-loop for child in (ebox-tree--children-raw node) for position from 0 @@ -166,22 +167,22 @@ type, key, parent, and sibling position are identical." (plist-get old-state :source-index) old-root) (ebox-tree-node-key (plist-get candidate-state :source-index) new-root)) - (hash-table-p old-nodes) (hash-table-p new-nodes) + (ebox-runtime-index-like-p old-nodes) (ebox-runtime-index-like-p new-nodes) (hash-table-p old-objects))) result) (dolist (node-id (plist-get prepared :touched-node-ids)) - (if (gethash node-id new-nodes) + (if (ebox-runtime-index-get node-id new-nodes) (puthash node-id t requested) (setq valid nil))) (dolist (node-id (plist-get prepared :removed-node-ids)) - (if (and (gethash node-id old-nodes) - (not (gethash node-id new-nodes))) + (if (and (ebox-runtime-index-get node-id old-nodes) + (not (ebox-runtime-index-get node-id new-nodes))) (puthash node-id t removed) (setq valid nil))) (when valid - (maphash + (ebox-runtime-index-map (lambda (node-id _node) - (when (and (not (gethash node-id new-nodes)) + (when (and (not (ebox-runtime-index-get node-id new-nodes)) (not (gethash node-id removed))) (setq valid nil))) old-nodes)) @@ -189,7 +190,7 @@ type, key, parent, and sibling position are identical." (cl-labels ((visit (node) (let* ((node-id (plist-get node :node-id)) - (old-node (gethash node-id old-nodes missing)) + (old-node (ebox-runtime-index-get node-id old-nodes missing)) (old-object (gethash node-id old-objects)) (position (gethash node-id new-positions)) (parent-id (car-safe position)) @@ -229,12 +230,12 @@ type, key, parent, and sibling position are identical." (setq stable (and (plist-get old-state :viewport-dependent-node-ids-ready) (plist-member old-state :viewport-dependent-node-id-axes) - (hash-table-p old-nodes) - (hash-table-p new-nodes))) + (ebox-runtime-index-like-p old-nodes) + (ebox-runtime-index-like-p new-nodes))) (dolist (entry (plist-get prepared :dirty-set) stable) (let* ((node-id (plist-get entry :node-id)) - (old (and stable (gethash node-id old-nodes))) - (new (and stable (gethash node-id new-nodes)))) + (old (and stable (ebox-runtime-index-get node-id old-nodes))) + (new (and stable (ebox-runtime-index-get node-id new-nodes)))) (unless (and old new (eq (ebox--node-direct-viewport-width-dependent-p old) (ebox--node-direct-viewport-width-dependent-p new)) @@ -398,9 +399,9 @@ new session with the same bounded configuration used by retained frames." (defun ebox-native-commit--region-box (state region-id) "Resolve REGION-ID's candidate box directly from STATE indexes." - (when-let* ((node-id (gethash region-id + (when-let* ((node-id (ebox-runtime-index-get region-id (plist-get state :region-node-table))) - (node (gethash node-id (plist-get state :node-table)))) + (node (ebox-runtime-index-get node-id (plist-get state :node-table)))) (pcase (plist-get node :ebox-type) ('box node) ('flex (plist-get node :box))))) @@ -429,8 +430,8 @@ new session with the same bounded configuration used by retained frames." "Return non-nil when STATE contains only executable native node types." (let ((nodes (plist-get state :node-table)) (supported t)) - (when (hash-table-p nodes) - (maphash + (when (ebox-runtime-index-like-p nodes) + (ebox-runtime-index-map (lambda (_node-id node) (unless (memq (plist-get node :ebox-type) '(box concat stack flex)) diff --git a/ebox-native-reflow.el b/ebox-native-reflow.el index 5e73466..ee441be 100644 --- a/ebox-native-reflow.el +++ b/ebox-native-reflow.el @@ -15,6 +15,7 @@ (require 'subr-x) (require 'ebox-measure) (require 'ebox-render-context) +(require 'ebox-runtime-index) (declare-function ebox-native--module-version "ebox_native_reflow" ()) (declare-function ebox-native--module-layout-ready-p "ebox_native_reflow" ()) @@ -2411,7 +2412,13 @@ Otherwise return CANDIDATE with the next document revision." (root (and state (plist-get state :root-node))) (postorder (and state (plist-get state :native-node-postorder)))) (unless (and root (vectorp postorder) (> (length postorder) 0) - (eq root (aref postorder (1- (length postorder))))) + (equal (plist-get root :node-id) + (let ((entry (aref postorder + (1- (length postorder))))) + (if (and (listp entry) + (plist-member entry :node-id)) + (plist-get entry :node-id) + entry)))) (error "Native layout scene is absent from the render transaction")) (with-current-buffer buffer (ebox-native-reflow--make-layout-scene @@ -2496,7 +2503,12 @@ per call, and no call recursively visits the captured Ebox tree." (< (- (float-time) started) budget-seconds))) (let* ((index (ebox-native-reflow-layout-builder-index builder)) - (node (aref nodes index)) + (node + (ebox-native-reflow--postorder-node + (aref nodes index) + (plist-get + (ebox-native-reflow-layout-scene-render-state scene) + :node-table))) (fragment (ebox-native-reflow--compile-scene-node node fragments @@ -2645,10 +2657,21 @@ per call, and no call recursively visits the captured Ebox tree." (plist-get node :content-min-width))) local))) -(defun ebox-native-reflow--retained-edge-index (postorder cache) - "Build exact child-edge facts from full POSTORDER fragments in CACHE." +(defun ebox-native-reflow--postorder-node (entry node-table) + "Resolve POSTORDER ENTRY through NODE-TABLE when it is a node id." + (if (and (listp entry) (plist-member entry :ebox-type)) + entry + (or (ebox-runtime-index-get entry node-table) + (error "Native postorder references an absent node: %S" entry)))) + +(defun ebox-native-reflow--retained-edge-index + (postorder cache &optional node-table) + "Build exact child-edge facts from full POSTORDER fragments in CACHE. +Resolve node-ID entries through NODE-TABLE; legacy node plists need no index." (let (index) - (cl-loop for parent across postorder + (cl-loop for entry across postorder + for parent = (ebox-native-reflow--postorder-node + entry node-table) for parent-id = (plist-get parent :node-id) for rule = (ebox-native-reflow--retained-edge-rule parent) when rule do @@ -2685,7 +2708,8 @@ per call, and no call recursively visits the captured Ebox tree." (defun ebox-native-reflow--compile-retained-layout-package-full (session state node) "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)) + (node-table (plist-get state :node-table))) (unless (and (vectorp postorder) (> (length postorder) 0)) (setq postorder (ebox-native-reflow--retained-layout-postorder node))) @@ -2728,7 +2752,11 @@ per call, and no call recursively visits the captured Ebox tree." (postorder-id-set (let ((set (make-hash-table :test 'equal))) (dotimes (index (length postorder)) - (puthash (plist-get (aref postorder index) :node-id) + (puthash + (plist-get + (ebox-native-reflow--postorder-node + (aref postorder index) node-table) + :node-id) t set)) set)) (ebox-native-reflow--compile-styles @@ -2832,7 +2860,9 @@ per call, and no call recursively visits the captured Ebox tree." (puthash current fragment fragments) (puthash node-id entry new-cache)))) (dotimes (index (length postorder)) - (let* ((current (aref postorder index)) + (let* ((current + (ebox-native-reflow--postorder-node + (aref postorder index) node-table)) (node-id (plist-get current :node-id))) (if (and retained-fast-p (not (gethash node-id touched-set))) @@ -2871,7 +2901,7 @@ per call, and no call recursively visits the captured Ebox tree." (plist-get package :property-templates)) (ebox-native-reflow-session-layout-edge-index session) (ebox-native-reflow--retained-edge-index - postorder new-cache)) + postorder new-cache node-table)) package))))) (defconst ebox-native-reflow--delta-edge-fields @@ -2924,9 +2954,9 @@ RULE identifies the compiler lowering, and RETAINED supplies its stable slot." (owner-id (and nodes parents (ebox-native-reflow--delta-owner-id state dirty-id index))) - (owner (and owner-id (gethash owner-id nodes))) - (parent-id (and owner-id (gethash owner-id parents))) - (parent (and parent-id (gethash parent-id nodes))) + (owner (and owner-id (ebox-runtime-index-get owner-id nodes))) + (parent-id (and owner-id (ebox-runtime-index-get owner-id parents))) + (parent (and parent-id (ebox-runtime-index-get parent-id nodes))) (current-parent-rule (and parent (ebox-native-reflow--retained-edge-rule parent))) (retained-parent-rule @@ -2994,9 +3024,9 @@ RULE identifies the compiler lowering, and RETAINED supplies its stable slot." (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-let* ((source (and nodes (ebox-runtime-index-get current nodes))) + (parent-id (and parents (ebox-runtime-index-get current parents))) + (parent (and nodes (ebox-runtime-index-get parent-id nodes)))) (when (and (eq (plist-get source :ebox-kind) 'text) (eq (plist-get parent :ebox-kind) 'box) (eq (ebox-layout-config-kind @@ -3006,7 +3036,7 @@ RULE identifies the compiler lowering, and RETAINED supplies its stable slot." (while (and current (null (ebox-native-reflow--persistent-index-get index current))) - (setq current (and parents (gethash current parents)))) + (setq current (and parents (ebox-runtime-index-get current parents)))) current)) (defun ebox-native-reflow--compile-delta-slots (node old-fragment) @@ -3073,7 +3103,7 @@ RULE identifies the compiler lowering, and RETAINED supplies its stable slot." (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) + (when (and old-package old-index (ebox-runtime-index-like-p node-table) (plist-get state :native-topology-stable-p) (plist-member state :native-local-dirty-entries) (not (ebox-native-reflow--delta-edge-change-p @@ -3126,7 +3156,7 @@ RULE identifies the compiler lowering, and RETAINED supplies its stable slot." (let* ((cached (ebox-native-reflow--persistent-index-get old-index owner-id)) (old-fragment (plist-get cached :fragment)) - (source (gethash owner-id node-table)) + (source (ebox-runtime-index-get owner-id node-table)) (expected (plist-get cached :revision)) patches new-fragment) (unless (and cached source (integerp expected)) diff --git a/ebox-patch-plan.el b/ebox-patch-plan.el index a5df05a..fe00411 100644 --- a/ebox-patch-plan.el +++ b/ebox-patch-plan.el @@ -12,6 +12,7 @@ (require 'cl-lib) (require 'subr-x) +(require 'ebox-runtime-index) (define-error 'ebox-patch-plan-error "Invalid Ebox patch plan input") @@ -115,14 +116,14 @@ "Return OWNER-ID's strict ancestors from immutable PARENT-TABLE." (let ((ancestors (make-hash-table :test #'equal)) (seen (make-hash-table :test #'equal)) - (walk (gethash owner-id parent-table))) + (walk (ebox-runtime-index-get owner-id parent-table))) (while walk (when (gethash walk seen) (signal 'ebox-patch-plan-error (list :cyclic-parent-table :node-id walk))) (puthash walk t seen) (puthash walk t ancestors) - (setq walk (gethash walk parent-table))) + (setq walk (ebox-runtime-index-get walk parent-table))) ancestors)) (defun ebox-patch-plan--make-index () @@ -216,7 +217,7 @@ PARENT-TABLE maps a node id to its parent id and is treated as immutable. OPERATIONS are patch artifact plists. This function mutates neither input and has no publication capability." - (unless (hash-table-p parent-table) + (unless (ebox-runtime-index-like-p parent-table) (signal 'ebox-patch-plan-error (list :malformed-parent-table parent-table))) (unless (proper-list-p operations) diff --git a/ebox-runtime-index.el b/ebox-runtime-index.el new file mode 100644 index 0000000..b09302c --- /dev/null +++ b/ebox-runtime-index.el @@ -0,0 +1,237 @@ +;;; ebox-runtime-index.el --- Immutable runtime indexes for Ebox -*- lexical-binding: t; -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: + +;; Compressed persistent radix maps used by runtime state. Branches retain +;; only nibble positions where codes diverge; leaves hold exact equal-key +;; collision buckets. Adapters continue to accept legacy hash tables. + +;;; Code: + +(require 'cl-lib) + +(cl-defstruct (ebox-runtime-index + (:constructor ebox-runtime-index--create + (root count key-set-token))) + "Immutable compressed radix index." + root + count + key-set-token) + +(defun ebox-runtime-index-empty () + "Return an empty immutable runtime index." + (ebox-runtime-index--create nil 0 nil)) + +(defun ebox-runtime-index-like-p (object) + "Return non-nil when OBJECT is a supported runtime index." + (or (ebox-runtime-index-p object) (hash-table-p object))) + +(defun ebox-runtime-index--code (key) + "Return a stable 64-bit radix code for KEY." + (logand (if (and (integerp key) (>= key 0)) key (sxhash-equal key)) + #xffffffffffffffff)) + +(defun ebox-runtime-index--branch (shift code &optional original) + "Return a fresh branch for nibble SHIFT and representative CODE. +Copy ORIGINAL when supplied; its metadata already carries SHIFT and CODE. +All branch allocations, including path copies, pass through this function." + (if original + (copy-sequence original) + (let ((branch (make-vector 18 nil))) + (aset branch 0 shift) + (aset branch 1 code) + branch))) + +(defun ebox-runtime-index--join (left left-code right right-code) + "Join LEFT and RIGHT above the first differing nibble of their codes. +LEFT-CODE and RIGHT-CODE are distinct 64-bit representative codes." + (let* ((difference (logxor left-code right-code)) + (shift (logand (logcount (1- (logand difference (- difference)))) + -4)) + (branch (ebox-runtime-index--branch shift left-code))) + (aset branch (+ 2 (logand (ash left-code (- shift)) 15)) left) + (aset branch (+ 2 (logand (ash right-code (- shift)) 15)) right) + branch)) + +(defun ebox-runtime-index--root-get (root key missing) + "Return KEY from persistent ROOT, or MISSING." + (let ((node root) + (code (ebox-runtime-index--code key))) + (while (vectorp node) + (setq node (aref node (+ 2 (logand (ash code (- (aref node 0))) 15))))) + (if (and node (= code (car node))) + (let ((pair (assoc key (cdr node)))) + (if pair (cdr pair) missing)) + missing))) + +(defun ebox-runtime-index-get (key index &optional missing) + "Return KEY's value from INDEX, or MISSING. +INDEX may be an immutable runtime index or a legacy hash table." + (cond + ((ebox-runtime-index-p index) + (ebox-runtime-index--root-get (ebox-runtime-index-root index) key missing)) + ((hash-table-p index) (gethash key index missing)) + (t missing))) + +(defun ebox-runtime-index-contains-p (key index) + "Return non-nil when INDEX contains KEY, including a nil value." + (let ((missing (make-symbol "ebox-runtime-index-missing"))) + (not (eq (ebox-runtime-index-get key index missing) missing)))) + +(defun ebox-runtime-index--root-put (root key value code) + "Return `(ROOT . ADDED)' after mapping KEY to VALUE using CODE." + (cond + ((null root) (cons (cons code (list (cons key value))) t)) + ((vectorp root) + (let ((shift (aref root 0)) + (representative (aref root 1))) + (if (/= (logand (logxor code representative) (1- (ash 1 shift))) 0) + (cons (ebox-runtime-index--join + root representative (cons code (list (cons key value))) code) + t) + (let* ((slot (+ 2 (logand (ash code (- shift)) 15))) + (result (ebox-runtime-index--root-put + (aref root slot) key value code)) + (copy (ebox-runtime-index--branch shift representative root))) + (aset copy slot (car result)) + (cons copy (cdr result)))))) + ((= code (car root)) + (let* ((bucket (cdr root)) + (present (assoc key bucket))) + (cons (cons code + (cons (cons key value) + (if present + (cl-remove key bucket :key #'car :test #'equal) + bucket))) + (not present)))) + (t (cons (ebox-runtime-index--join + root (car root) (cons code (list (cons key value))) code) + t)))) + +(defun ebox-runtime-index-put (key value index) + "Return INDEX with KEY mapped to VALUE. +Immutable indexes return a new value. Legacy hash tables are mutated and +returned for compatibility." + (cond + ((ebox-runtime-index-p index) + (let ((result + (ebox-runtime-index--root-put + (ebox-runtime-index-root index) key value + (ebox-runtime-index--code key)))) + (ebox-runtime-index--create + (car result) + (+ (ebox-runtime-index-count index) (if (cdr result) 1 0)) + (if (cdr result) + (make-symbol "ebox-runtime-index-key-set") + (ebox-runtime-index-key-set-token index))))) + ((hash-table-p index) (puthash key value index) index) + (t (signal 'wrong-type-argument (list 'ebox-runtime-index-like-p index))))) + +(defun ebox-runtime-index--root-delete (root key code) + "Return `(ROOT . REMOVED)' after deleting KEY using CODE." + (cond + ((null root) (cons nil nil)) + ((vectorp root) + (let* ((shift (aref root 0)) + (slot (+ 2 (logand (ash code (- shift)) 15))) + (result (ebox-runtime-index--root-delete (aref root slot) key code))) + (if (not (cdr result)) + (cons root nil) + (let ((children 0) survivor) + (unless (car result) + (dotimes (offset 16) + (let ((position (+ offset 2))) + (when (and (/= position slot) (aref root position)) + (cl-incf children) + (setq survivor (aref root position)))))) + (if (= children 1) + (cons survivor t) + (let ((copy (ebox-runtime-index--branch shift (aref root 1) root))) + (aset copy slot (car result)) + (cons copy t))))))) + ((and (= code (car root)) (assoc key (cdr root))) + (let ((bucket (cl-remove key (cdr root) :key #'car :test #'equal))) + (cons (and bucket (cons code bucket)) t))) + (t (cons root nil)))) + +(defun ebox-runtime-index-delete (key index) + "Return INDEX without KEY. +Immutable indexes return a new value. Legacy hash tables are mutated and +returned for compatibility." + (cond + ((ebox-runtime-index-p index) + (let ((result + (ebox-runtime-index--root-delete + (ebox-runtime-index-root index) key + (ebox-runtime-index--code key)))) + (if (cdr result) + (let ((count (1- (ebox-runtime-index-count index)))) + (ebox-runtime-index--create + (car result) count + (and (> count 0) + (make-symbol "ebox-runtime-index-key-set")))) + index))) + ((hash-table-p index) (remhash key index) index) + (t (signal 'wrong-type-argument (list 'ebox-runtime-index-like-p index))))) + +(defun ebox-runtime-index-size (index) + "Return the number of entries in INDEX." + (cond + ((ebox-runtime-index-p index) (ebox-runtime-index-count index)) + ((hash-table-p index) (hash-table-count index)) + (t 0))) + +(defun ebox-runtime-index-key-set-equal-p (left right) + "Return non-nil when supported indexes LEFT and RIGHT have equal keys." + (let ((persistent-shared-keys-p + (and (ebox-runtime-index-p left) + (ebox-runtime-index-p right) + (= (ebox-runtime-index-count left) + (ebox-runtime-index-count right)) + (eq (ebox-runtime-index-key-set-token left) + (ebox-runtime-index-key-set-token right))))) + (cond + (persistent-shared-keys-p t) + ((and (ebox-runtime-index-like-p left) + (ebox-runtime-index-like-p right) + (= (ebox-runtime-index-size left) (ebox-runtime-index-size right))) + (let ((equal-p t) + (missing (make-symbol "ebox-runtime-index-missing"))) + (ebox-runtime-index-map + (lambda (key _value) + (when (eq (ebox-runtime-index-get key right missing) missing) + (setq equal-p nil))) + left) + equal-p)) + (t nil)))) + +(defun ebox-runtime-index--map-root (function root) + "Call FUNCTION for entries below ROOT, visiting only live trie nodes." + (when root + (if (vectorp root) + (dotimes (branch 16) + (let ((child (aref root (+ 2 branch)))) + (when child (ebox-runtime-index--map-root function child)))) + (dolist (pair (cdr root)) (funcall function (car pair) (cdr pair)))))) + +(defun ebox-runtime-index-map (function index) + "Call FUNCTION with every key and value in INDEX." + (cond + ((ebox-runtime-index-p index) + (ebox-runtime-index--map-root + function (ebox-runtime-index-root index))) + ((hash-table-p index) (maphash function index)) + (t (signal 'wrong-type-argument (list 'ebox-runtime-index-like-p index)))) + nil) + +(defun ebox-runtime-index-keys (index) + "Return a list of keys in INDEX." + (let (keys) + (ebox-runtime-index-map (lambda (key _value) (push key keys)) index) + keys)) + +(provide 'ebox-runtime-index) + +;;; ebox-runtime-index.el ends here diff --git a/ebox-surface.el b/ebox-surface.el index 0da7d3f..63b34d2 100644 --- a/ebox-surface.el +++ b/ebox-surface.el @@ -18,6 +18,7 @@ (require 'ebox-tree) (require 'ebox-layout) (require 'ebox-incremental) +(require 'ebox-runtime-index) (require 'tp-reactive) (require 'tp-surface) (require 'tp) @@ -1521,7 +1522,7 @@ PREVIOUS-STATE's node-object table." (context nodes node-root old-objects objects-by-node) "Reuse native NODES whose object topology is proven unchanged." (tp-object-reuse-subtree context node-root) - (maphash + (ebox-runtime-index-map (lambda (node-id node) (let ((object (gethash node-id old-objects))) (unless object @@ -1544,7 +1545,7 @@ PREVIOUS-STATE's node-object table." (old-objects (plist-get previous-state :surface-node-object-table)) (touched (make-hash-table :test 'equal)) (objects-by-id (make-hash-table :test 'equal))) - (unless (and (hash-table-p nodes) (hash-table-p parents) + (unless (and (ebox-runtime-index-like-p nodes) (ebox-runtime-index-like-p parents) (hash-table-p old-objects) (or (plist-get state-overrides :native-topology-stable-p) touched-ids)) @@ -1554,7 +1555,7 @@ PREVIOUS-STATE's node-object table." context nodes node-root old-objects objects-by-node) (tp-object-retain context node-root) (dolist (node-id touched-ids) (puthash node-id t touched)) - (maphash + (ebox-runtime-index-map (lambda (node-id node) (when subject-table (when-let* ((subject @@ -1574,10 +1575,10 @@ PREVIOUS-STATE's node-object table." (puthash node-id object objects-by-id)))) nodes) (dolist (node-id touched-ids) - (let* ((node (gethash node-id nodes)) - (parent-id (and node (gethash node-id parents))) + (let* ((node (ebox-runtime-index-get node-id nodes)) + (parent-id (and node (ebox-runtime-index-get node-id parents))) (old-object (and node (gethash node-id old-objects))) - (parent-node (and parent-id (gethash parent-id nodes))) + (parent-node (and parent-id (ebox-runtime-index-get parent-id nodes))) (position (if parent-node (or (cl-position @@ -2009,7 +2010,7 @@ batch without asking TP to rediscover the same diff from a full plan." (progn (setq object (and objects (gethash node-id objects))) (or (not object) (not (tp-object-mounted-p object))))) - (setq node-id (and parents (gethash node-id parents)))) + (setq node-id (and parents (ebox-runtime-index-get node-id parents)))) (unless (and object (tp-object-live-p object) (tp-object-mounted-p object)) (error "Ebox scoped owner has no mounted TP object")) @@ -2457,19 +2458,19 @@ source-handle-to-subject table when the requested local preparation completed." (when (gethash node-id visiting) (error "Ebox candidate style parent cycle at %S" node-id)) (puthash node-id t visiting) - (let* ((node (gethash node-id new-nodes)) - (old-node (gethash node-id old-nodes)) - (parent-id (gethash node-id parents)) + (let* ((node (ebox-runtime-index-get node-id new-nodes)) + (old-node (ebox-runtime-index-get node-id old-nodes)) + (parent-id (ebox-runtime-index-get node-id parents)) (parent-style (and parent-id (resolve parent-id))) (parent-subject (and parent-id (gethash parent-id subjects-by-id))) (old-parent-id (and old-node - (gethash node-id + (ebox-runtime-index-get node-id (plist-get old-state :parent-table)))) (old-parent (and old-parent-id - (gethash old-parent-id old-nodes))) + (ebox-runtime-index-get old-parent-id old-nodes))) (old-parent-style (and old-parent (plist-get old-parent :ebox-computed-style))) @@ -2534,7 +2535,7 @@ source-handle-to-subject table when the requested local preparation completed." (ebox-surface--project-font node style (and parent-id - (plist-get (gethash parent-id new-nodes) + (plist-get (ebox-runtime-index-get parent-id new-nodes) :ebox-font-fact)) snapshot) (cond @@ -2556,7 +2557,7 @@ source-handle-to-subject table when the requested local preparation completed." (puthash node-id style resolved) style)))))) (dolist (node-id node-ids) - (when (gethash node-id new-nodes) + (when (ebox-runtime-index-get node-id new-nodes) (resolve node-id)))) subjects-by-handle)))) @@ -2671,7 +2672,7 @@ dependencies, or any malformed index conservatively preserves root planning." signature))))) (and (hash-table-p objects) (hash-table-p style-states) - (hash-table-p candidate-nodes) + (ebox-runtime-index-like-p candidate-nodes) dirty-set (cl-every (lambda (entry) @@ -2681,7 +2682,8 @@ dependencies, or any malformed index conservatively preserves root planning." (object (and node-id (gethash node-id objects))) (style-state (and object (gethash object style-states))) (candidate-node (and node-id - (gethash node-id candidate-nodes))) + (ebox-runtime-index-get + node-id candidate-nodes))) (subject (and style-state (plist-get style-state :subject))) (candidate-declarations @@ -2790,7 +2792,7 @@ dependencies, or any malformed index conservatively preserves root planning." (defun ebox-surface--region-object-table (state node-objects) "Return region-to-object table from candidate STATE and NODE-OBJECTS." (let ((table (make-hash-table :test 'equal))) - (maphash + (ebox-runtime-index-map (lambda (region-id node-id) (let ((object (gethash node-id node-objects))) (unless object @@ -2975,10 +2977,10 @@ valid because their declarative layout and viewport did not change." "Attach retained TP objects to candidate nodes in STATE." (let ((nodes (plist-get state :node-table)) (objects (plist-get state :surface-node-object-table))) - (when (and (hash-table-p nodes) (hash-table-p objects)) + (when (and (ebox-runtime-index-like-p nodes) (hash-table-p objects)) (maphash (lambda (node-id object) - (when-let* ((node (gethash node-id nodes))) + (when-let* ((node (ebox-runtime-index-get node-id nodes))) (plist-put node :surface-object object))) objects)) state)) @@ -2994,7 +2996,7 @@ valid because their declarative layout and viewport did not change." (objects (plist-get state :surface-node-object-table)) (style-states (plist-get state :style-binding-states))) (dolist (owner-id owner-ids) - (when-let* ((node (and nodes (gethash owner-id nodes))) + (when-let* ((node (and nodes (ebox-runtime-index-get owner-id nodes))) (object (and objects (gethash owner-id objects))) (style-state (and style-states (gethash object style-states))) @@ -3255,11 +3257,11 @@ old allocated width while the owner changes its natural content length." (old-snapshot (and owner-id (ebox--ensure-layout-snapshot-details buffer owner-id))) (spans (and old-snapshot (plist-get old-snapshot :buffer-spans))) - (node (and owner-id (gethash owner-id (plist-get state :node-table)))) + (node (and owner-id (ebox-runtime-index-get owner-id (plist-get state :node-table)))) (parent-id (and owner-id - (gethash owner-id (plist-get state :parent-table)))) + (ebox-runtime-index-get owner-id (plist-get state :parent-table)))) (parent (and parent-id - (gethash parent-id (plist-get state :node-table)))) + (ebox-runtime-index-get parent-id (plist-get state :node-table)))) (slot-sized-render-p (and role-owned-lines-p parent (eq (ebox-tree-display-inner parent) 'flex))) @@ -3343,7 +3345,7 @@ old allocated width while the owner changes its natural content length." (let* ((owner-id (plist-get proof :owner-id)) (old-snapshot (plist-get proof :snapshot)) (spans (plist-get old-snapshot :buffer-spans)) - (node (gethash owner-id (plist-get state :node-table))) + (node (ebox-runtime-index-get owner-id (plist-get state :node-table))) (render-node (if (and node (numberp (plist-get proof :allocated-width))) (plist-put (copy-sequence node) @@ -3566,7 +3568,7 @@ untouched." (snapshot (and proof (plist-get proof :snapshot))) (old-spans (and proof (plist-get proof :old-spans))) (node (and owner-id - (gethash owner-id (plist-get state :node-table)))) + (ebox-runtime-index-get owner-id (plist-get state :node-table)))) (allocated-width (and proof (plist-get proof :allocated-width)))) (when (and proof snapshot old-spans node @@ -3577,16 +3579,16 @@ untouched." (parent-table (plist-get state :parent-table)) (ancestor-owner-ids (let ((walk (and parent-table - (gethash owner-id parent-table))) + (ebox-runtime-index-get owner-id parent-table))) owners) (while walk (when-let* ((ancestor - (gethash walk (plist-get state :node-table))) + (ebox-runtime-index-get walk (plist-get state :node-table))) (region-id (ebox-surface--node-editable-region-id ancestor))) (setq owners (append owners (list region-id)))) - (setq walk (gethash walk parent-table))) + (setq walk (ebox-runtime-index-get walk parent-table))) owners)) (rendered (ebox--maplines @@ -3965,11 +3967,11 @@ box with wrapper chrome falls back to the retained wrapper renderer." (nodes (plist-get state :node-table))) (and (plist-get state :runtime-index-prepared-p) root-id - (hash-table-p nodes) - (eq root (gethash root-id nodes)) - (hash-table-p (plist-get state :parent-table)) + (ebox-runtime-index-like-p nodes) + (eq root (ebox-runtime-index-get root-id nodes)) + (ebox-runtime-index-like-p (plist-get state :parent-table)) (hash-table-p (plist-get state :region-id-set)) - (hash-table-p (plist-get state :region-node-table)) + (ebox-runtime-index-like-p (plist-get state :region-node-table)) (hash-table-p (plist-get state :region-box-count-table)) (hash-table-p (plist-get state :region-box-table)) (hash-table-p @@ -4075,7 +4077,7 @@ Each owner appears at most once for one rendered fragment." owners) (dolist (region-id (delete-dups (mapcar #'cdr role-ids))) (let ((object (gethash region-id region-objects)) - (node-id (gethash region-id region-node-table))) + (node-id (ebox-runtime-index-get region-id region-node-table))) (unless (and object node-id) (error "Ebox output references unknown region %S" region-id)) (unless (gethash object attached) @@ -4083,7 +4085,7 @@ Each owner appears at most once for one rendered fragment." (ebox-surface--region-role-tags region-id role-ids)) owners) (puthash object t attached)) - (setq node-id (gethash node-id parent-table)) + (setq node-id (ebox-runtime-index-get node-id parent-table)) (while node-id (let ((ancestor (gethash node-id node-objects))) (unless ancestor @@ -4091,7 +4093,7 @@ Each owner appears at most once for one rendered fragment." (unless (gethash ancestor attached) (push (list ancestor (list :ebox/descendant-output t)) owners) (puthash ancestor t attached))) - (setq node-id (gethash node-id parent-table))))) + (setq node-id (ebox-runtime-index-get node-id parent-table))))) (nreverse owners))) (defun ebox-surface--rendered-fragments (rendered) @@ -4514,7 +4516,7 @@ all descendant ranges remain unchanged." (let* ((proof (plist-get state :formatting-context-reflow-proof)) (owner-id (plist-get proof :owner-id)) (owner (and owner-id (gethash owner-id node-objects))) - (node (and owner-id (gethash owner-id (plist-get state :node-table)))) + (node (and owner-id (ebox-runtime-index-get owner-id (plist-get state :node-table)))) (region-id (and node (plist-get node :region-id))) (block-start (max 0 @@ -4720,7 +4722,7 @@ NODE-OBJECTS and REGION-OBJECTS supply retained ownership ranges." (let ((key node-id) (parents (plist-get state :parent-table)) (depth 0)) - (while (setq node-id (and node-id (gethash node-id parents))) + (while (setq node-id (and node-id (ebox-runtime-index-get node-id parents))) (cl-incf depth)) (when ebox-surface--paint-node-depth-cache (puthash key depth ebox-surface--paint-node-depth-cache)) @@ -4737,14 +4739,16 @@ NODE-OBJECTS and REGION-OBJECTS supply retained ownership ranges." (let ((region-nodes (plist-get state :region-node-table)) (parents (plist-get state :parent-table)) deepest deepest-depth) (dolist (entry role-ids) - (when-let* ((node-id (gethash (cdr entry) region-nodes))) + (when-let* ((node-id + (ebox-runtime-index-get + (cdr entry) region-nodes))) (let ((depth (ebox-surface--node-depth state node-id))) (when (or (null deepest-depth) (> depth deepest-depth)) (setq deepest node-id deepest-depth depth))))) (let (chain) (while deepest (push deepest chain) - (setq deepest (gethash deepest parents))) + (setq deepest (ebox-runtime-index-get deepest parents))) (setq chain (nreverse chain)) (when ebox-surface--paint-node-chain-cache (puthash role-ids chain ebox-surface--paint-node-chain-cache)) @@ -4816,7 +4820,7 @@ ANCESTOR-P means BOX wraps a more specific rendered surface." (let ((nodes (plist-get state :node-table)) result first) (setq first t) (dolist (node-id (ebox-surface--paint-node-chain state role-ids)) - (when-let* ((node (gethash node-id nodes)) + (when-let* ((node (ebox-runtime-index-get node-id nodes)) (box (ebox-fragment-style-source-node node))) (let* ((region-id (plist-get box :region-id)) (roles @@ -5003,7 +5007,7 @@ relative face contributions and performs the final merge during prepare." "Return FRAGMENTS with horizontal border roles updated from STATE." (let ((nodes (plist-get state :node-table))) (dolist (dirty dirty-set fragments) - (when-let* ((node (gethash (plist-get dirty :node-id) nodes)) + (when-let* ((node (ebox-runtime-index-get (plist-get dirty :node-id) nodes)) (box (ebox-fragment-style-source-node node)) (region-id (plist-get box :region-id))) (dolist (entry '(((:border-top-width :border-top-style) @@ -5084,7 +5088,7 @@ OLD-TEXT is a defensive snapshot of the published TP content." (when (hash-table-p snapshots) (dolist (dirty dirty-set) (when-let* ((node-id (plist-get dirty :node-id)) - (node (gethash node-id nodes)) + (node (ebox-runtime-index-get node-id nodes)) (snapshot (gethash node-id snapshots))) (plist-put snapshot :style-signature (ebox-fragment-node-style-signature node))))) diff --git a/ebox-tree.el b/ebox-tree.el index 28bd633..684b695 100644 --- a/ebox-tree.el +++ b/ebox-tree.el @@ -15,6 +15,7 @@ (require 'ebox-style) (require 'ebox-node-factory) (require 'ebox-child-range) +(require 'ebox-runtime-index) (defvar ebox-tree--incoming-source-indexes nil "Transaction-local handle to incoming source-index map for one delta.") @@ -739,8 +740,8 @@ handles, reused node objects, and cycles without consulting node metadata." (defun ebox-tree--indexed-parent-context (node-id node-table parent-table) "Return the direct parent layout kind for NODE-ID in indexed tree." - (let* ((parent-id (gethash node-id parent-table)) - (parent (and parent-id (gethash parent-id node-table)))) + (let* ((parent-id (ebox-runtime-index-get node-id parent-table)) + (parent (and parent-id (ebox-runtime-index-get parent-id node-table)))) (and parent (ebox-tree--child-layout-kind parent nil)))) (defun ebox-tree-validate-indexed-participation @@ -748,7 +749,7 @@ handles, reused node objects, and cycles without consulting node metadata." "Validate NODE-IDS in indexed final tree using AUTHOR or COMPUTED SOURCE." (setq source (or source 'author)) (dolist (node-id (delete-dups (copy-sequence node-ids))) - (when-let* ((node (gethash node-id node-table))) + (when-let* ((node (ebox-runtime-index-get node-id node-table))) (ebox-tree--validate-node-parent-participation node (ebox-tree--indexed-parent-context node-id node-table parent-table) @@ -1379,12 +1380,12 @@ stale until a query or full projection materializes them." (parent-id (nth 2 entry)) parent-subject) (while (and parent-id (null parent-subject)) - (when-let* ((parent (gethash parent-id node-table))) + (when-let* ((parent (ebox-runtime-index-get parent-id node-table))) (when-let* ((parent-handle (ebox-tree-node-source-handle parent))) (setq parent-subject (node-subject parent-handle)))) (unless parent-subject - (setq parent-id (gethash parent-id parent-table)))) + (setq parent-id (ebox-runtime-index-get parent-id parent-table)))) (setf (ecss-subject-parent subject) parent-subject) (puthash (ebox-source--handle-id-view handle) subject added-subjects) diff --git a/ebox.el b/ebox.el index a0eff1f..b368212 100644 --- a/ebox.el +++ b/ebox.el @@ -23,7 +23,8 @@ "Directory containing the active Ebox Lisp sources.") (defconst ebox--compile-sources - '("ebox-cache.el" "ebox-source.el" "ebox-state-contract.el" + '("ebox-cache.el" "ebox-source.el" "ebox-runtime-index.el" + "ebox-state-contract.el" "ebox-font.el" "ebox-style.el" "ebox-layout-config.el" "ebox-node-factory.el" @@ -44,6 +45,7 @@ (require 'subr-x) (require 'ebox-cache) (require 'ebox-source) +(require 'ebox-runtime-index) (require 'ebox-state-contract) (require 'ebox-font) (require 'ebox-style) @@ -1141,7 +1143,7 @@ remain transaction-local." (let* ((template-box (plist-get template :box)) (node-id (and template-box (ebox--ensure-node-id template-box))) (node-table (ebox--buffer-node-table (current-buffer)))) - (or (and node-table node-id (gethash node-id node-table)) + (or (and node-table node-id (ebox-runtime-index-get node-id node-table)) (plist-get (ebox--scroll-get-state region-id) :box) (gethash region-id ebox--region-box-table)))) @@ -1164,7 +1166,7 @@ remain transaction-local." (or (ebox-get effective-box :scroll-offset) 0)) (or portable (null node-table) - (eq box (gethash node-id node-table))) + (eq box (ebox-runtime-index-get node-id node-table))) (or (null live-state) (eq effective-box (plist-get live-state :box))) (not (plist-get live-state :lazy-scroll-prefix-dirty)) @@ -2931,7 +2933,9 @@ Scroll routing uses semantic ancestry rather than mount span length because nested owners may render equal-sized clipped spans." (let* ((state (ebox--buffer-render-state (current-buffer))) (region-nodes (plist-get state :region-node-table)) - (node-id (and region-nodes (gethash region-id region-nodes)))) + (node-id + (and region-nodes + (ebox-runtime-index-get region-id region-nodes)))) (if node-id (ebox-surface--node-depth state node-id) -1))) (defun ebox--scroll-region-ids-in-buffer-outer-first () @@ -4110,7 +4114,7 @@ cannot be changed through the unpublished batch root." entries) (let ((parents (ebox--buffer-parent-table buffer)) ancestors) (dolist (node-id node-ids) - (while (setq node-id (gethash node-id parents)) + (while (setq node-id (ebox-runtime-index-get node-id parents)) (push node-id ancestors))) (delete-dups ancestors)))) @@ -4122,7 +4126,7 @@ cannot be changed through the unpublished batch root." (let ((parents (ebox--buffer-parent-table buffer))) (delete-dups (delq nil - (mapcar (lambda (node-id) (gethash node-id parents)) + (mapcar (lambda (node-id) (ebox-runtime-index-get node-id parents)) node-ids)))))) (defun ebox--publish-scoped-region-candidate diff --git a/tests/ebox-child-range-tests.el b/tests/ebox-child-range-tests.el index 313c052..e118fa4 100644 --- a/tests/ebox-child-range-tests.el +++ b/tests/ebox-child-range-tests.el @@ -355,8 +355,8 @@ (should-not (plist-member empty-record :sequence)) (should (= (plist-get empty-resolved :rank) 1)) (should (= (plist-get items-resolved :rank) 1)) - (should (= (hash-table-count (plist-get state :node-table)) - (hash-table-count (plist-get direct-state :node-table)))) + (should (= (ebox-runtime-index-size (plist-get state :node-table)) + (ebox-runtime-index-size (plist-get direct-state :node-table)))) (should (= (hash-table-count (plist-get state :region-id-set)) (hash-table-count (plist-get direct-state :region-id-set)))) @@ -482,7 +482,9 @@ (node-id (gethash ref (ebox-child-range-test--host-ref-table state))) - (node (gethash node-id (plist-get state :node-table)))) + (node + (ebox-runtime-index-get + node-id (plist-get state :node-table)))) (should node-id) (should (equal (ebox-child-range-test--node-key @@ -668,7 +670,7 @@ (ebox-commit buffer candidate)) (let* ((state (ebox--buffer-render-state buffer)) (record (gethash 'items (plist-get state :range-ref-table))) - (parent (gethash (plist-get record :parent-node-id) + (parent (ebox-runtime-index-get (plist-get record :parent-node-id) (plist-get state :node-table))) (segment (ebox-child-range--lookup-ref @@ -742,7 +744,7 @@ (ebox-commit buffer candidate)) (let* ((state (ebox--buffer-render-state buffer)) (record (gethash 'items (plist-get state :range-ref-table))) - (parent (gethash (plist-get record :parent-node-id) + (parent (ebox-runtime-index-get (plist-get record :parent-node-id) (plist-get state :node-table))) (segment (ebox-child-range--lookup-ref @@ -1208,7 +1210,7 @@ node-id (plist-get state :surface-node-object-table)))) - (should (eq node (gethash node-id + (should (eq node (ebox-runtime-index-get node-id (plist-get state :node-table)))) (should object) (should (eq object (plist-get node :surface-object)))))))) @@ -1301,7 +1303,9 @@ (current-id (gethash 'child (ebox-child-range-test--host-ref-table state))) - (node (gethash current-id (plist-get state :node-table)))) + (node + (ebox-runtime-index-get + current-id (plist-get state :node-table)))) (when node-id (should (equal node-id current-id))) (setq node-id current-id) (should (plist-get node :ebox-sequence-location)))) diff --git a/tests/ebox-commit-tests.el b/tests/ebox-commit-tests.el index 991d806..ae6bfb5 100644 --- a/tests/ebox-commit-tests.el +++ b/tests/ebox-commit-tests.el @@ -43,9 +43,10 @@ (defun ebox-commit-test--hash-facts (table &optional values) "Return sorted TABLE keys, or key/value pairs when VALUES is non-nil." (let (facts) - (maphash (lambda (key value) - (push (if values (cons key value) key) facts)) - table) + (ebox-runtime-index-map + (lambda (key value) + (push (if values (cons key value) key) facts)) + table) (sort facts (lambda (left right) (string< (prin1-to-string left) @@ -653,7 +654,7 @@ new-root new-source-index)) (new-index (ebox--runtime-index new-root t new-source-index)) (root-id (plist-get old-root :node-id))) - (maphash (lambda (node-id _node) + (ebox-runtime-index-map (lambda (node-id _node) (puthash node-id (list 'object node-id) old-objects)) (plist-get old-index :node-table)) (let* ((old-state (append (list :root-node old-root @@ -700,7 +701,7 @@ new-root new-source-index)) (new-index (ebox--runtime-index new-root t new-source-index)) (root-id (plist-get old-root :node-id))) - (maphash (lambda (node-id _node) + (ebox-runtime-index-map (lambda (node-id _node) (puthash node-id (list 'object node-id) old-objects)) (plist-get old-index :node-table)) (let ((old-state (append (list :root-node old-root @@ -1342,20 +1343,24 @@ remain retained identities." (node-table (plist-get state :node-table)) (parent-table (plist-get state :parent-table)) (node-id (and region-node-table - (gethash value region-node-table))) + (ebox-runtime-index-get + value region-node-table))) (source-node-id node-id) (root-node-id (plist-get (plist-get state :root-node) :node-id)) key) (while (and node-id (not key)) - (when-let* ((node (gethash node-id node-table))) + (when-let* ((node + (ebox-runtime-index-get + node-id node-table))) (setq key (ebox-tree-node-key (plist-get state :source-index) node))) (setq node-id (and (not key) - (gethash node-id parent-table)))) + (ebox-runtime-index-get + node-id parent-table)))) (or key (and (equal source-node-id root-node-id) 'root) value)) @@ -2587,6 +2592,187 @@ remain retained identities." (ebox-native-reflow--persistent-index-get next 17))) (should-not (eq base next)))) +(ert-deftest ebox-logical-candidate-path-copies-runtime-indexes-only () + "A local logical commit shares its base and never copies a complete core map." + (let* ((input + (ebox-test-column + (ebox-test-box :key 'message :source-identity 'message + (ebox-test-text "old")) + (ebox-test-box :key 'tail (ebox-test-text "tail")))) + (buffer + (ebox-render-to-buffer + (generate-new-buffer-name " *ebox-runtime-index-local*") input)) + (old-state (ebox--buffer-render-state buffer)) + (old-nodes (plist-get old-state :node-table)) + (old-parents (plist-get old-state :parent-table)) + (old-regions (plist-get old-state :region-node-table)) + (old-postorder (plist-get old-state :native-node-postorder)) + (old-message (ebox--host-ref-node buffer 'message)) + (original-copy (symbol-function 'copy-hash-table)) + copied-core) + (unwind-protect + (let ((candidate (ebox-candidate-begin buffer))) + (ebox-candidate-replace-host-ref + candidate 'message + (ebox-test-box :key 'message :source-identity 'message + (ebox-test-text "new"))) + (cl-letf (((symbol-function 'copy-hash-table) + (lambda (table) + (when (memq table + (list old-nodes old-parents old-regions)) + (setq copied-core t)) + (funcall original-copy table)))) + (ebox-commit buffer candidate)) + (let* ((state (ebox--buffer-render-state buffer)) + (nodes (plist-get state :node-table)) + (parents (plist-get state :parent-table)) + (regions (plist-get state :region-node-table))) + (should-not copied-core) + (should (ebox-runtime-index-p nodes)) + (should (ebox-runtime-index-p parents)) + (should (ebox-runtime-index-p regions)) + (should-not (eq nodes old-nodes)) + (should (eq regions old-regions)) + (should (eq (plist-get state :native-node-postorder) + old-postorder)) + (should (eq (plist-get state :native-node-postorder-ids) + old-postorder)) + (should (eq (ebox-runtime-index-get + (plist-get old-message :node-id) old-nodes) + old-message)) + (should + (cl-every + (lambda (node-id) + (ebox-runtime-index-get node-id nodes)) + old-postorder)))) + (when (buffer-live-p buffer) (kill-buffer buffer))))) + +(ert-deftest ebox-logical-candidate-converts-legacy-native-postorder () + "Legacy and mixed vectors resolve every entry against candidate nodes." + (let* ((old-leaf '(:ebox-type text :node-id 1 :content "old")) + (old-root (list :ebox-type 'column :node-id 2 + :children (list old-leaf))) + (new-leaf '(:ebox-type text :node-id 1 :content "new")) + (new-root (list :ebox-type 'column :node-id 2 + :children (list new-leaf)))) + (dolist (table (list (ebox-runtime-index-empty) + (make-hash-table :test 'equal))) + (setq table (ebox-runtime-index-put 1 new-leaf table) + table (ebox-runtime-index-put 2 new-root table)) + (dolist (legacy (list (vector old-leaf old-root) + (vector 1 old-root) + (vector old-leaf 2))) + (let* ((before (copy-sequence legacy)) + (state (list :native-node-postorder legacy + :native-node-postorder-ids (vector 1 2))) + (postorder + (ebox-incremental--candidate-retain-native-postorder + state table))) + (should (equal postorder [1 2])) + (should-not (eq postorder legacy)) + (should (equal legacy before)) + (should (eq (ebox-native-reflow--postorder-node + (aref postorder 0) table) + new-leaf)) + (should (eq (ebox-native-reflow--postorder-node + (aref postorder 1) table) + new-root))))))) + +(ert-deftest ebox-logical-candidate-native-postorder-validates-legacy-membership () + "Unmarked retained entries reject missing candidate nodes; empty is valid." + (let* ((node '(:ebox-type text :node-id 1 :content "old")) + (table (ebox-runtime-index-empty))) + (should-error + (ebox-incremental--candidate-retain-native-postorder + (list :native-node-postorder (vector node)) table) + :type 'error) + (should (equal + (ebox-incremental--candidate-retain-native-postorder + (list :native-node-postorder []) table) + [])) + (should-not + (ebox-incremental--candidate-retain-native-postorder nil table)))) + +(ert-deftest ebox-logical-candidate-retains-native-id-postorder-without-lookups () + "Generated ID vectors retain their exact identity without walking nodes." + (let* ((input (ebox-test-column (ebox-test-text "retained"))) + (root (ebox-test-root input)) + (index (ebox--runtime-index root nil (ebox-test-source-index input))) + (postorder (plist-get index :native-node-postorder))) + (should (eq postorder (plist-get index :native-node-postorder-ids))) + (cl-letf (((symbol-function 'ebox-runtime-index-get) + (lambda (&rest _arguments) + (ert-fail "Generated native ID postorder was remapped")))) + (should (eq postorder + (ebox-incremental--candidate-retain-native-postorder + index (plist-get index :node-table))))))) + +(defun ebox-test--native-topology-full-gate-fixture () + "Return `(BUFFER OLD-STATE CANDIDATE-STATE PREPARED TAIL)' for a paint edit." + (let* ((previous-message + (ebox-test-box :key 'message :source-identity 'message + :background-color "#111111" + (ebox-test-text "message"))) + (previous + (ebox-test-column + previous-message + (ebox-test-box :key 'tail :source-identity 'tail + (ebox-test-text "tail")))) + (next + (ebox-test-box :key 'message :source-identity 'message + :background-color "#222222" + (ebox-test-text "message"))) + (buffer + (ebox-render-to-buffer + (generate-new-buffer-name " *ebox-topology-authority*") previous)) + (old-state (ebox--buffer-render-state buffer)) + (candidate (ebox-candidate-begin buffer))) + (unless (ebox-candidate-patch-host-paint + candidate 'message + previous-message next) + (error "Topology authority fixture paint patch was rejected")) + (let* ((prepared + (ebox-incremental--prepare-logical-candidate + buffer old-state candidate)) + (candidate-state + (ebox-incremental--candidate-state + old-state (plist-get prepared :root) + (plist-get prepared :index) prepared))) + (list buffer old-state candidate-state prepared + (ebox--host-ref-node buffer 'tail))))) + +(ert-deftest ebox-native-topology-full-gate-rejects-untouched-object-loss () + "The full topology gate rejects a missing untouched surface object." + (pcase-let ((`(,buffer ,old-state ,candidate-state ,prepared ,tail) + (ebox-test--native-topology-full-gate-fixture))) + (unwind-protect + (progn + (should (ebox-native-commit-topology-stable-p + old-state candidate-state prepared)) + (remhash (plist-get tail :node-id) + (plist-get old-state :surface-node-object-table)) + (should-not (ebox-native-commit-topology-stable-p + old-state candidate-state prepared))) + (when (buffer-live-p buffer) (kill-buffer buffer))))) + +(ert-deftest ebox-native-topology-full-gate-rejects-untouched-source-key-change () + "The full topology gate rejects an untouched source key change." + (pcase-let ((`(,buffer ,old-state ,candidate-state ,prepared ,tail) + (ebox-test--native-topology-full-gate-fixture))) + (unwind-protect + (progn + (should (ebox-native-commit-topology-stable-p + old-state candidate-state prepared)) + (let* ((source-index (plist-get candidate-state :source-index)) + (binding + (ebox-source-index-rebind + source-index (ebox-tree-node-source-handle tail) + :key 'tampered-tail))) + (plist-put candidate-state :source-index (car binding))) + (should-not (ebox-native-commit-topology-stable-p + old-state candidate-state prepared))) + (when (buffer-live-p buffer) (kill-buffer buffer))))) + (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) diff --git a/tests/ebox-core-render-tests.el b/tests/ebox-core-render-tests.el index a815933..9ab6f0a 100644 --- a/tests/ebox-core-render-tests.el +++ b/tests/ebox-core-render-tests.el @@ -875,6 +875,31 @@ (when (and buffer (buffer-live-p buffer)) (kill-buffer buffer))))) +(ert-deftest ebox-runtime-bootstrap-uses-persistent-core-indexes-and-id-postorder () + "Runtime bootstrap stores persistent core indexes and stable node ids." + (ebox-test--reset-runtime-state) + (let* ((input + (ebox-test-column + (ebox-test-box :key 'left (ebox-test-text "Left")) + (ebox-test-box :key 'right (ebox-test-text "Right")))) + (root (ebox-test-root input)) + (index (ebox--runtime-index root t (ebox-test-source-index input))) + (nodes (plist-get index :node-table)) + (postorder (plist-get index :native-node-postorder))) + (should (ebox-runtime-index-p nodes)) + (should (ebox-runtime-index-p (plist-get index :parent-table))) + (should (ebox-runtime-index-p (plist-get index :region-node-table))) + (should (vectorp postorder)) + (should (> (length postorder) 0)) + (should + (cl-every + (lambda (node-id) + (and (not (listp node-id)) + (ebox-runtime-index-get node-id nodes))) + postorder)) + (should (equal (aref postorder (1- (length postorder))) + (plist-get root :node-id))))) + (ert-deftest ebox-region-render-owner-index-keeps-earlier-duplicate-box () "A later flex wrapper should not replace an earlier duplicate region owner." (ebox-test--reset-runtime-state) @@ -894,7 +919,7 @@ (expected (car (ebox--node-path-to-region layout-node region-id)))) (should (eq expected plain-node)) - (should (equal (gethash region-id table) + (should (equal (ebox-runtime-index-get region-id table) (ebox--ensure-node-id expected)))))) (ert-deftest ebox-region-render-owner-index-falls-back-for-legacy-state () @@ -2440,7 +2465,7 @@ (should (eq (plist-get report :constraint-owner-type) 'box)) (should (equal (plist-get report :constraint-owner-id) - (gethash left-id + (ebox-runtime-index-get left-id (ebox--buffer-region-node-table (current-buffer))))) (should (equal (plist-get report :dirty-kinds) diff --git a/tests/ebox-docs-contract-tests.el b/tests/ebox-docs-contract-tests.el index 1b90c38..d0d0cf3 100644 --- a/tests/ebox-docs-contract-tests.el +++ b/tests/ebox-docs-contract-tests.el @@ -24,14 +24,16 @@ (defconst ebox-docs-test--active-files '("Makefile" ".github/workflows/ci.yml" - "ebox.el" "ebox-cache.el" "ebox-state-contract.el" "ebox-style.el" + "ebox.el" "ebox-cache.el" "ebox-runtime-index.el" + "ebox-state-contract.el" "ebox-style.el" "ebox-child-range.el" "ebox-tree.el" "ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el" "ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-patch-plan.el" "ebox-incremental.el" "ebox-surface.el" "ebox-dsl.el" "ebox-spi.el" "ebox-selector.el" "ebox-native-reflow.el" - "tests/ebox-core-render-tests.el" "tests/ebox-state-contract-tests.el" + "tests/ebox-core-render-tests.el" "tests/ebox-runtime-index-tests.el" + "tests/ebox-state-contract-tests.el" "tests/ebox-layout-boundary-tests.el" "tests/ebox-patch-plan-tests.el" "tests/ebox-style-schema-tests.el" @@ -50,7 +52,8 @@ "Active Ebox files that the maintainer map must cover.") (defconst ebox-docs-test--targets - '("check" "ci" "load" "compile" "core-tests" "child-range-tests" "grid-tests" + '("check" "ci" "load" "compile" "core-tests" "runtime-index-tests" + "child-range-tests" "grid-tests" "ebox-commit-tests" "surface-tests" "visual-check-tests" "package-tests" "selector-tests" "dsl-tests" "flex-tests" "state-contract-tests" "layout-boundary-tests" "layout-boundary-performance" diff --git a/tests/ebox-package-tests.el b/tests/ebox-package-tests.el index 016c30b..e4dca9b 100644 --- a/tests/ebox-package-tests.el +++ b/tests/ebox-package-tests.el @@ -156,7 +156,8 @@ (should (commandp 'ebox-byte-compile)) (should (equal ebox--compile-sources - '("ebox-cache.el" "ebox-source.el" "ebox-state-contract.el" + '("ebox-cache.el" "ebox-source.el" "ebox-runtime-index.el" + "ebox-state-contract.el" "ebox-font.el" "ebox-style.el" "ebox-layout-config.el" "ebox-node-factory.el" diff --git a/tests/ebox-runtime-index-tests.el b/tests/ebox-runtime-index-tests.el new file mode 100644 index 0000000..7753e8a --- /dev/null +++ b/tests/ebox-runtime-index-tests.el @@ -0,0 +1,295 @@ +;;; ebox-runtime-index-tests.el --- Runtime index tests -*- lexical-binding: t; -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'ebox-runtime-index) + +(defun ebox-runtime-index-test--shape (index) + "Validate compressed INDEX and return `(BRANCHES LEAVES MAX-DEPTH)'." + (let ((branches 0) (leaves 0) (max-depth 0)) + (cl-labels + ((visit (node depth parent-shift mask prefix) + (setq max-depth (max max-depth depth)) + (cond + ((vectorp node) + (should (= (length node) 18)) + (let ((shift (aref node 0)) + (code (aref node 1)) + (children 0)) + (should (and (integerp shift) (<= 0 shift 60) + (= (% shift 4) 0) (> shift parent-shift))) + (should (= (logand code mask) prefix)) + (cl-incf branches) + (dotimes (slot 16) + (when-let* ((child (aref node (+ slot 2)))) + (cl-incf children) + (visit child (1+ depth) shift + (1- (ash 1 (+ shift 4))) + (logior (logand code (1- (ash 1 shift))) + (ash slot shift))))) + (should (>= children 2)))) + (node + (should (and (consp node) (integerp (car node)) (cdr node))) + (should (= (logand (car node) mask) prefix)) + (dolist (entry (cdr node)) + (should (= (ebox-runtime-index--code (car entry)) + (car node)))) + (cl-incf leaves))))) + (visit (ebox-runtime-index-root index) 0 -1 0 0)) + (list branches leaves max-depth))) + +(ert-deftest ebox-runtime-index-persistent-update-isolates-base () + (let* ((base (ebox-runtime-index-put 1 'old (ebox-runtime-index-empty))) + (changed (ebox-runtime-index-put 1 'new base)) + (added (ebox-runtime-index-put 17 nil changed)) + (deleted (ebox-runtime-index-delete 1 added))) + (should (eq (ebox-runtime-index-get 1 base) 'old)) + (should (eq (ebox-runtime-index-get 1 changed) 'new)) + (should (ebox-runtime-index-contains-p 17 added)) + (should-not (ebox-runtime-index-get 17 added 'missing)) + (should-not (ebox-runtime-index-contains-p 1 deleted)) + (should (ebox-runtime-index-key-set-equal-p base changed)) + (should-not (ebox-runtime-index-key-set-equal-p base added)) + (should-not (ebox-runtime-index-key-set-equal-p base deleted)) + (should (= (ebox-runtime-index-size base) 1)) + (should (= (ebox-runtime-index-size added) 2)) + (should (= (ebox-runtime-index-size deleted) 1)))) + +(ert-deftest ebox-runtime-index-key-set-proof-does-not-enumerate-persistent-indexes () + (let* ((base (ebox-runtime-index-put 1 'old (ebox-runtime-index-empty))) + (changed (ebox-runtime-index-put 1 'new base))) + (cl-letf (((symbol-function 'ebox-runtime-index-map) + (lambda (&rest _args) + (ert-fail "persistent key-set proof enumerated its input")))) + (should (ebox-runtime-index-key-set-equal-p base changed))))) + +(ert-deftest ebox-runtime-index-key-set-equality-supports-independent-indexes () + (let ((left (ebox-runtime-index-empty)) + (right (ebox-runtime-index-empty))) + (dolist (key '(1 17 33)) + (setq left (ebox-runtime-index-put key key left))) + (dolist (key '(33 17 1)) + (setq right (ebox-runtime-index-put key nil right))) + (should (ebox-runtime-index-key-set-equal-p left right)))) + +(ert-deftest ebox-runtime-index-preserves-equal-key-collisions () + (cl-letf (((symbol-function 'ebox-runtime-index--code) + (lambda (_key) 0))) + (let* ((base (ebox-runtime-index-empty)) + (one (ebox-runtime-index-put '(same code one) 1 base)) + (two (ebox-runtime-index-put '(same code two) 2 one))) + (should (= (ebox-runtime-index-get '(same code one) two) 1)) + (should (= (ebox-runtime-index-get '(same code two) two) 2)) + (should (= (ebox-runtime-index-size two) 2))))) + +(ert-deftest ebox-runtime-index-map-and-keys-cover-exact-entries () + (let ((index (ebox-runtime-index-empty)) + entries) + (dolist (pair '((5 . five) (1 . one) (9 . nine))) + (setq index (ebox-runtime-index-put (car pair) (cdr pair) index))) + (ebox-runtime-index-map + (lambda (key value) (push (cons key value) entries)) index) + (should (equal (sort entries (lambda (a b) (< (car a) (car b)))) + '((1 . one) (5 . five) (9 . nine)))) + (should (equal (sort (ebox-runtime-index-keys index) #'<) '(1 5 9))))) + +(ert-deftest ebox-runtime-index-adapters-accept-legacy-hash-tables () + (let ((table (make-hash-table :test #'equal))) + (should (eq (ebox-runtime-index-put 'key nil table) table)) + (should (ebox-runtime-index-contains-p 'key table)) + (should (= (ebox-runtime-index-size table) 1)) + (should (equal (ebox-runtime-index-keys table) '(key))) + (should (eq (ebox-runtime-index-delete 'key table) table)) + (should (= (ebox-runtime-index-size table) 0)))) + +(ert-deftest ebox-runtime-index-update-work-is-bounded-by-changed-keys () + "Path-copy work grows with changed keys rather than retained index size." + (dolist (case '((32 . 2) (128 . 2) (512 . 3))) + (let ((size (car case)) + (max-depth (cdr case)) + (base (ebox-runtime-index-empty))) + (dotimes (id size) + (setq base (ebox-runtime-index-put id id base))) + (dolist (changed-count '(1 4 16)) + (let ((next base) + (vector-copies 0) + (original-copy (symbol-function 'copy-sequence))) + (cl-letf (((symbol-function 'copy-sequence) + (lambda (sequence) + (when (vectorp sequence) + (cl-incf vector-copies)) + (funcall original-copy sequence)))) + (dotimes (id changed-count) + (setq next (ebox-runtime-index-put id (- id) next)))) + (should (<= vector-copies (* changed-count max-depth))) + (should (= (ebox-runtime-index-size next) size)) + (should (= (ebox-runtime-index-get (1- size) base) (1- size))) + (should (= (ebox-runtime-index-get 0 base) 0))))))) + +(ert-deftest ebox-runtime-index-compresses-prefixes-for-generic-keys () + "Noninteger keys with long shared codes keep only real branching levels." + (cl-letf (((symbol-function 'ebox-runtime-index--code) + (lambda (key) (logior #x123456789a (ash (cadr key) 40))))) + (let ((index (ebox-runtime-index-empty))) + (dotimes (id 128) + (setq index (ebox-runtime-index-put (list 'key id) id index))) + (pcase-let ((`(,branches ,leaves ,depth) + (ebox-runtime-index-test--shape index))) + (should (= branches 17)) + (should (= leaves 128)) + (should (= depth 2))) + (dotimes (id 128) + (should (= (ebox-runtime-index-get (list 'key id) index) id)))))) + +(ert-deftest ebox-runtime-index-splits-prefixes-and-collapses-deleted-branches () + "Splitting above retained branches and deleting representatives isolate forks." + (let ((index (ebox-runtime-index-empty)) snapshots) + (dolist (key '(#x1100 #x2100 #x0101 #x1110 #x3110)) + (push index snapshots) + (setq index (ebox-runtime-index-put key key index)) + (ebox-runtime-index-test--shape index)) + (let ((base index)) + ;; The first key supplied representative codes to several branch nodes. + (setq index (ebox-runtime-index-delete #x1100 index)) + (ebox-runtime-index-test--shape index) + (setq index (ebox-runtime-index-put #x4110 'later index)) + (ebox-runtime-index-test--shape index) + (should (= (ebox-runtime-index-get #x1100 base) #x1100)) + (should-not (ebox-runtime-index-contains-p #x4110 base)) + (should (eq (ebox-runtime-index-get #x4110 index) 'later)) + (dolist (key '(#x2100 #x0101 #x1110 #x3110)) + (setq index (ebox-runtime-index-delete key index)) + (ebox-runtime-index-test--shape index)) + (should (equal (ebox-runtime-index-test--shape index) '(0 1 0))) + (should (eq index (ebox-runtime-index-delete 'absent index))) + (setq index (ebox-runtime-index-delete #x4110 index)) + (should (= (ebox-runtime-index-size index) 0)) + (should-not (ebox-runtime-index-root index))) + (cl-loop for snapshot in snapshots + for count downfrom 4 + do (should (= (ebox-runtime-index-size snapshot) count)) + do (ebox-runtime-index-test--shape snapshot)))) + +(ert-deftest ebox-runtime-index-preserves-truncated-large-integer-collisions () + "Keys differing above bit 63 retain independent exact values and membership." + (let* ((small 7) + (large (+ 7 (ash 1 80))) + (larger (+ 7 (ash 1 100))) + (base (ebox-runtime-index-put small 'small (ebox-runtime-index-empty))) + (base (ebox-runtime-index-put large nil base)) + (base (ebox-runtime-index-put larger 'larger base)) + (changed (ebox-runtime-index-put large 'large base)) + (deleted (ebox-runtime-index-delete small changed))) + (should (= (ebox-runtime-index--code small) + (ebox-runtime-index--code larger))) + (should (= (ebox-runtime-index-size base) 3)) + (should (ebox-runtime-index-contains-p large base)) + (should-not (ebox-runtime-index-get large base 'missing)) + (should (eq (ebox-runtime-index-get large deleted) 'large)) + (should (eq (ebox-runtime-index-get larger deleted) 'larger)) + (should-not (ebox-runtime-index-contains-p small deleted)) + (should (ebox-runtime-index-key-set-equal-p base changed)) + (should (equal (ebox-runtime-index-test--shape deleted) '(0 1 0))))) + +(ert-deftest ebox-runtime-index-branches-at-the-highest-code-nibble () + "The compressed prefix arithmetic covers all 64 code bits." + (let ((index (ebox-runtime-index-empty))) + (dolist (key (list 0 (ash 1 60) (ash 1 63))) + (setq index (ebox-runtime-index-put key key index))) + (should (equal (ebox-runtime-index-test--shape index) '(1 3 1))) + (should (= (aref (ebox-runtime-index-root index) 0) 60)) + (setq index (ebox-runtime-index-delete 0 index)) + (setq index (ebox-runtime-index-put (ash 1 62) 'later index)) + (ebox-runtime-index-test--shape index) + (should (= (ebox-runtime-index-get (ash 1 63) index) (ash 1 63))) + (should (eq (ebox-runtime-index-get (ash 1 62) index) 'later)))) + +(ert-deftest ebox-runtime-index-put-discovers-membership-in-one-traversal () + "Insertion and replacement do not perform a separate membership lookup." + (let ((base (ebox-runtime-index-put 'one nil (ebox-runtime-index-empty)))) + (cl-letf (((symbol-function 'ebox-runtime-index--root-get) + (lambda (&rest _arguments) + (ert-fail "Put performed a preliminary membership lookup")))) + (should (= (ebox-runtime-index-size + (ebox-runtime-index-put 'one 'changed base)) + 1)) + (should (= (ebox-runtime-index-size + (ebox-runtime-index-put 'two nil base)) + 2))))) + +(ert-deftest ebox-runtime-index-collision-updates-and-deletes-preserve-equal-keys () + "Colliding noninteger keys compare with equal across immutable forks." + (cl-letf (((symbol-function 'ebox-runtime-index--code) + (lambda (_key) 23))) + (let* ((base (ebox-runtime-index-put + (list "key" 1) nil (ebox-runtime-index-empty))) + (base (ebox-runtime-index-put ["key" 2] 'two base)) + (changed (ebox-runtime-index-put (list "key" 1) 'one base)) + (deleted (ebox-runtime-index-delete (vector "key" 2) changed))) + (should (= (ebox-runtime-index-size changed) 2)) + (should (ebox-runtime-index-contains-p (list "key" 1) base)) + (should-not (ebox-runtime-index-get (list "key" 1) base 'missing)) + (should (eq (ebox-runtime-index-get (list "key" 1) deleted) 'one)) + (should-not (ebox-runtime-index-contains-p ["key" 2] deleted)) + (should (eq (ebox-runtime-index-get ["key" 2] base) 'two)) + (ebox-runtime-index-test--shape deleted)))) + +(ert-deftest ebox-runtime-index-map-visits-only-live-compressed-nodes () + "Enumeration visits at most one leaf and one branch per distinct code." + (let ((index (ebox-runtime-index-empty)) + (visits 0) + (entries 0) + (original (symbol-function 'ebox-runtime-index--map-root))) + (dotimes (id 512) + (setq index (ebox-runtime-index-put id id index))) + (cl-letf (((symbol-function 'ebox-runtime-index--map-root) + (lambda (&rest arguments) + (cl-incf visits) + (apply original arguments)))) + (ebox-runtime-index-map + (lambda (_key _value) (cl-incf entries)) index)) + (should (= entries 512)) + (should (<= visits (1- (* entries 2)))))) + +(ert-deftest ebox-runtime-index-mixed-key-updates-match-hash-table () + "Generic insertion/deletion sequences preserve exact facts and old snapshots." + (let ((keys (vector nil t -1 0 1 17 256 (ash 1 80) (1+ (ash 1 80)) + "key" '(key . 1) '[key 2])) + (index (ebox-runtime-index-empty)) + (expected (make-hash-table :test 'equal)) + (random-state 1729) + snapshots) + (dotimes (step 256) + (setq random-state (mod (+ (* random-state 1103515245) 12345) + 2147483648)) + (let ((key (aref keys (% (/ random-state 16) (length keys)))) + (value (and (/= (% step 7) 0) step))) + (if (= (% random-state 3) 0) + (progn + (setq index (ebox-runtime-index-delete key index)) + (remhash key expected)) + (setq index (ebox-runtime-index-put key value index)) + (puthash key value expected))) + (should (= (ebox-runtime-index-size index) (hash-table-count expected))) + (ebox-runtime-index-test--shape index) + (cl-loop for key across keys do + (should (equal (ebox-runtime-index-get key index 'missing) + (gethash key expected 'missing)))) + (when (= (% step 16) 0) + (push (cons index (copy-hash-table expected)) snapshots))) + (dolist (snapshot snapshots) + (ebox-runtime-index-test--shape (car snapshot)) + (should (= (ebox-runtime-index-size (car snapshot)) + (hash-table-count (cdr snapshot)))) + (cl-loop for key across keys do + (should (equal + (ebox-runtime-index-get key (car snapshot) 'missing) + (gethash key (cdr snapshot) 'missing))))))) + +(provide 'ebox-runtime-index-tests) + +;;; ebox-runtime-index-tests.el ends here diff --git a/tests/ebox-source-tests.el b/tests/ebox-source-tests.el index 4edfe14..f3c5aa3 100644 --- a/tests/ebox-source-tests.el +++ b/tests/ebox-source-tests.el @@ -396,7 +396,7 @@ (ebox-source-record-classes (ebox-source-index-record old-index old-handle)))) - (maphash + (ebox-runtime-index-map (lambda (_node-id node) (when (ebox-node-kind node) (should (ebox-source-index-record diff --git a/tests/ebox-surface-tests.el b/tests/ebox-surface-tests.el index 40eb1a8..3989dd9 100644 --- a/tests/ebox-surface-tests.el +++ b/tests/ebox-surface-tests.el @@ -501,7 +501,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer." (defun ebox-surface-test--object-by-key (state key) "Return the candidate surface object for Ebox node KEY in STATE." (let (object) - (maphash + (ebox-runtime-index-map (lambda (_node-id node) (when (equal (plist-get node :key) key) (setq object (plist-get node :surface-object)))) @@ -511,7 +511,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer." (defun ebox-surface-test--node-by-key (state key) "Return the runtime Ebox node for KEY in STATE." (let (match) - (maphash + (ebox-runtime-index-map (lambda (_node-id node) (when (equal (plist-get node :key) key) (setq match node))) @@ -2099,7 +2099,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer." (style-states (plist-get state :style-binding-states)) (source-index (plist-get state :source-index)) text-id text-node) - (maphash + (ebox-runtime-index-map (lambda (node-id node) (when (and (eq (plist-get node :ebox-kind) 'text) (equal (plist-get node :content) "Inherited")) @@ -2107,13 +2107,14 @@ candidate cannot hide mutations by restoring the old hash-table pointer." nodes) (should text-id) (let* ((parent-id - (gethash text-id (plist-get state :parent-table))) + (ebox-runtime-index-get + text-id (plist-get state :parent-table))) (object (gethash text-id objects)) (parent-object (gethash parent-id objects)) (style-state (gethash object style-states)) (parent-state (gethash parent-object style-states)) (candidate-node (copy-sequence text-node)) - (candidate-nodes (copy-hash-table nodes)) + (candidate-nodes nodes) (candidate-state (copy-sequence state))) (should style-state) (should parent-state) @@ -2126,7 +2127,9 @@ candidate cannot hide mutations by restoring the old hash-table pointer." (ecss-subject-id (plist-get style-state :subject)))) (should (ebox-surface--static-style-state-p style-state)) (plist-put candidate-node :ebox-candidate-computed-style-p t) - (puthash text-id candidate-node candidate-nodes) + (setq candidate-nodes + (ebox-runtime-index-put + text-id candidate-node candidate-nodes)) (plist-put candidate-state :node-table candidate-nodes) (let ((ebox-incremental--allocated-slot-proof-cache (make-hash-table :test 'equal))) @@ -3207,7 +3210,7 @@ changes. Geometry and non-inherited computed values must remain identical." (maphash (lambda (id node) (should (eq node - (gethash (gethash id + (ebox-runtime-index-get (ebox-runtime-index-get id (plist-get state :region-node-table)) (plist-get state :node-table)))))