refactor: inject surface-free layout context for M2a E2
Some checks are pending
CI / test (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run

This commit is contained in:
Kinneyzhang 2026-08-31 20:37:08 +08:00
parent e55798e0e9
commit 12ce14d43a
13 changed files with 339 additions and 33 deletions

View File

@ -22,7 +22,7 @@ node tree
-> pure TP surface plan -> pure TP surface plan
``` ```
The owners are `ebox-child-range.el` for immutable child-sequence and key indexes, `ebox-tree.el` for Ebox identity and traversal, `ebox-style.el` for style semantics, `ebox-measure.el` for display-sensitive measurement, `ebox-layout.el`/`ebox-flex.el`/`ebox-grid.el` for geometry, `ebox-fragment.el` for fragment facts, `ebox-surface.el` for pure TP plan projection and retained publication, and `ebox-incremental.el` for dirty-owner planning. `ebox-buffer-backend.el` only builds and reshapes propertized render strings; all public live buffer publication routes through the TP surface boundary. The owners are `ebox-child-range.el` for immutable child-sequence and key indexes, `ebox-tree.el` for Ebox identity and traversal, `ebox-style.el` for style semantics, `ebox-measure.el` for display-sensitive measurement, `ebox-layout.el`/`ebox-flex.el`/`ebox-grid.el` for geometry, `ebox-fragment.el` for fragment facts, `ebox-render-context.el` for the candidate/materialization input port, `ebox-surface.el` for pure TP plan projection and retained publication, and `ebox-incremental.el` for dirty-owner planning. `ebox-layout.el` never loads or calls the surface/TP layer; the facade wires surface operations into its validated render-context port. `ebox-buffer-backend.el` only builds and reshapes propertized render strings; all public live buffer publication routes through the TP surface boundary.
## Public boundary ## Public boundary

View File

@ -22,7 +22,7 @@ Ebox 是底层空间渲染引擎。它负责把声明式节点树转换为经过
-> 纯 TP surface plan -> 纯 TP surface plan
``` ```
各层 owner 是:`ebox-child-range.el` 负责不可变 child sequence 与 key index`ebox-tree.el` 负责 Ebox identity 与遍历,`ebox-style.el` 负责样式语义,`ebox-measure.el` 负责 display 敏感测量,`ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` 负责几何,`ebox-fragment.el` 负责 fragment 事实,`ebox-surface.el` 负责纯 TP plan 投影与 retained 发布,`ebox-incremental.el` 负责 dirty owner 规划。`ebox-buffer-backend.el` 只构造和整形带文本属性的 render string所有公共 live buffer 发布路径都经过 TP surface 边界。 各层 owner 是:`ebox-child-range.el` 负责不可变 child sequence 与 key index`ebox-tree.el` 负责 Ebox identity 与遍历,`ebox-style.el` 负责样式语义,`ebox-measure.el` 负责 display 敏感测量,`ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` 负责几何,`ebox-fragment.el` 负责 fragment 事实,`ebox-render-context.el` 负责 candidate/materialization 输入 port`ebox-surface.el` 负责纯 TP plan 投影与 retained 发布,`ebox-incremental.el` 负责 dirty owner 规划。`ebox-layout.el` 不再 load 或调用 surface/TP 层;门面把 surface operation 接入已校验的 render-context port。`ebox-buffer-backend.el` 只构造和整形带文本属性的 render string所有公共 live buffer 发布路径都经过 TP surface 边界。
## 公共边界 ## 公共边界

View File

@ -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_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))')
NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release 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 state-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 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 state-contract-tests layout-boundary-tests layout-boundary-performance 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 all: check
@ -25,7 +25,7 @@ compile:
rm -f *.elc tests/*.elc scripts/*.elc 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)' $(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 state-contract-tests docs-contract-tests ci-contract-tests 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 state-contract-tests layout-boundary-tests docs-contract-tests ci-contract-tests
source-tests: source-tests:
$(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit $(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit
@ -66,6 +66,12 @@ flex-tests:
state-contract-tests: state-contract-tests:
$(EMACS_TEST) -l tests/ebox-state-contract-tests.el -f ert-run-tests-batch-and-exit $(EMACS_TEST) -l tests/ebox-state-contract-tests.el -f ert-run-tests-batch-and-exit
layout-boundary-tests:
$(EMACS_TEST) -l tests/ebox-layout-boundary-tests.el -f ert-run-tests-batch-and-exit
layout-boundary-performance:
$(EMACS_BATCH) -l scripts/ebox-performance-evaluator.el -f ebox-performance-evaluator-layout-boundary-batch
performance-evaluator: surface-tests ebox-commit-tests performance-evaluator: surface-tests ebox-commit-tests
$(EMACS_BATCH) -l scripts/ebox-performance-evaluator.el -f ebox-performance-evaluator-batch $(EMACS_BATCH) -l scripts/ebox-performance-evaluator.el -f ebox-performance-evaluator-batch

View File

@ -25,7 +25,7 @@ This is the maintainer entry point for the standalone Ebox repository. It descri
| `ebox-child-range.el` | Immutable weighted segment trie, sparse persistent key trie, Range replacement, and Gate A metrics. | | `ebox-child-range.el` | Immutable weighted segment trie, sparse persistent key trie, Range replacement, and Gate A metrics. |
| `ebox-measure.el` | Display-sensitive character/face/pixel measurement and measurement caches. | | `ebox-measure.el` | Display-sensitive character/face/pixel measurement and measurement caches. |
| `ebox-fragment.el` | Layout fragments, signatures, snapshots, spans, and dirty-kind facts. | | `ebox-fragment.el` | Layout fragments, signatures, snapshots, spans, and dirty-kind facts. |
| `ebox-render-context.el` | Render-local context and publication inputs. | | `ebox-render-context.el` | Render-local values and the injected candidate/materialization input port. |
| `ebox-layout.el` | Box, row, column, stack, concatenation, spacer, wrapping, and common formatting context. | | `ebox-layout.el` | Box, row, column, stack, concatenation, spacer, wrapping, and common formatting context. |
| `ebox-flex.el` | Flex normalization, lines, free-space distribution, and flex rendering. | | `ebox-flex.el` | Flex normalization, lines, free-space distribution, and flex rendering. |
| `ebox-grid.el` | Tracks, implicit tracks, fractions, minmax/repeat, gap, placement, span, and alignment. | | `ebox-grid.el` | Tracks, implicit tracks, fractions, minmax/repeat, gap, placement, span, and alignment. |
@ -38,7 +38,7 @@ This is the maintainer entry point for the standalone Ebox repository. It descri
The package intentionally does not include application Components, UI controls, reactive data, or a playground implementation. Those are sibling-package responsibilities. The package intentionally does not include application Components, UI controls, reactive data, or a playground implementation. Those are sibling-package responsibilities.
The active contract also covers `Makefile`, `.github/workflows/ci.yml`, `tests/ebox-core-render-tests.el`, `tests/ebox-state-contract-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`. 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-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 model ## Runtime model
@ -84,6 +84,7 @@ Caller-owned Source Tree
- Ebox owns logical node-to-subject adaptation and id/class/type candidate indexes; ECSS's public structured matcher is the only selector truth source. Subjects expose built-in id/key plus explicit `:selector-attributes`, never visible content, layout state, or runtime containers. - Ebox owns logical node-to-subject adaptation and id/class/type candidate indexes; ECSS's public structured matcher is the only selector truth source. Subjects expose built-in id/key plus explicit `:selector-attributes`, never visible content, layout state, or runtime containers.
- `ebox-style--property-definitions` is the single Ebox author-property source. Load time derives Ebox's read-only lookup index and one immutable ECSS package schema from it; the surface schema is composed once, and node construction or updates never re-register or copy the whole schema domain. - `ebox-style--property-definitions` is the single Ebox author-property source. Load time derives Ebox's read-only lookup index and one immutable ECSS package schema from it; the surface schema is composed once, and node construction or updates never re-register or copy the whole schema domain.
- E1 freezes the closed M2a target classification for retained state: immutable generation facts; generation-bound scroll/native authority; opaque TP client-state custody; one-way compatibility mirrors; and disposable caches. Every inventory row separately records the current v1 storage shape plus its target owner, mutation API, generation binding, rollback, rebuild proof, and cleanup. Current v1 TP client state still holds the whole Ebox state plist until a later checkpoint changes that storage shape. The E1 mirror probes only rebuild and compare projections; they never mutate live state. - E1 freezes the closed M2a target classification for retained state: immutable generation facts; generation-bound scroll/native authority; opaque TP client-state custody; one-way compatibility mirrors; and disposable caches. Every inventory row separately records the current v1 storage shape plus its target owner, mutation API, generation binding, rollback, rebuild proof, and cleanup. Current v1 TP client state still holds the whole Ebox state plist until a later checkpoint changes that storage shape. The E1 mirror probes only rebuild and compare projections; they never mutate live state.
- `ebox-layout.el` has no load or call edge to `ebox-surface.el` or TP. Candidate isolation, inherited-cascade detection, and one-shot materialization enter layout only through the validated port owned by `ebox-render-context.el`; `ebox.el` wires the surface implementation after both sides load.
- `owner-rerender` is broader than `span-patch`, which is broader than `paint-patch`. - `owner-rerender` is broader than `span-patch`, which is broader than `paint-patch`.
- Buffer coordinates belong to the generation that produced them and must be refreshed after mutation. - Buffer coordinates belong to the generation that produced them and must be refreshed after mutation.
- Grid uses the normal measurement and rendering pipeline. Native reflow may reject an ineligible tree and must fall back to Elisp without changing correctness. - Grid uses the normal measurement and rendering pipeline. Native reflow may reject an ineligible tree and must fall back to Elisp without changing correctness.
@ -116,6 +117,8 @@ make selector-tests
make dsl-tests make dsl-tests
make flex-tests make flex-tests
make state-contract-tests make state-contract-tests
make layout-boundary-tests
make layout-boundary-performance
make docs-contract-tests make docs-contract-tests
make ci-contract-tests make ci-contract-tests
make performance-evaluator make performance-evaluator

View File

@ -25,7 +25,7 @@
| `ebox-child-range.el` | 不可变 weighted segment trie、稀疏持久 key trie、Range replacement 与 Gate A metrics。 | | `ebox-child-range.el` | 不可变 weighted segment trie、稀疏持久 key trie、Range replacement 与 Gate A metrics。 |
| `ebox-measure.el` | display 敏感的字符、face、像素测量与测量缓存。 | | `ebox-measure.el` | display 敏感的字符、face、像素测量与测量缓存。 |
| `ebox-fragment.el` | 布局 fragment、signature、snapshot、span 和 dirty kind 事实。 | | `ebox-fragment.el` | 布局 fragment、signature、snapshot、span 和 dirty kind 事实。 |
| `ebox-render-context.el` | render-local context 与发布输入。 | | `ebox-render-context.el` | render-local 值与注入的 candidate/materialization 输入 port。 |
| `ebox-layout.el` | box、row、column、stack、concat、spacer、换行和通用 formatting context。 | | `ebox-layout.el` | box、row、column、stack、concat、spacer、换行和通用 formatting context。 |
| `ebox-flex.el` | flex 归一化、line、剩余空间分配和 flex 渲染。 | | `ebox-flex.el` | flex 归一化、line、剩余空间分配和 flex 渲染。 |
| `ebox-grid.el` | 轨道、隐式轨道、分数、minmax/repeat、gap、placement、span 和对齐。 | | `ebox-grid.el` | 轨道、隐式轨道、分数、minmax/repeat、gap、placement、span 和对齐。 |
@ -38,7 +38,7 @@
本包有意不包含应用 Component、UI control、响应式 data 或 playground 实现;它们属于同级包。历史应用性能记录器和 native reflow 评估器也不属于独立 Ebox 的发布边界Ebox 只保留 native 模块本身、Rust 构建输入和可重复的构建检查。 本包有意不包含应用 Component、UI control、响应式 data 或 playground 实现;它们属于同级包。历史应用性能记录器和 native reflow 评估器也不属于独立 Ebox 的发布边界Ebox 只保留 native 模块本身、Rust 构建输入和可重复的构建检查。
active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-core-render-tests.el`、`tests/ebox-state-contract-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` 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-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`
## 运行时模型 ## 运行时模型
@ -84,6 +84,7 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
- Ebox 只拥有逻辑 node-to-subject 适配与 id/class/type 候选索引ECSS 的公共结构化 matcher 是唯一 selector 真相源。subject 只暴露内建 id/key 和显式 `:selector-attributes`,绝不暴露可见 content、布局状态或 runtime 容器。 - Ebox 只拥有逻辑 node-to-subject 适配与 id/class/type 候选索引ECSS 的公共结构化 matcher 是唯一 selector 真相源。subject 只暴露内建 id/key 和显式 `:selector-attributes`,绝不暴露可见 content、布局状态或 runtime 容器。
- `ebox-style--property-definitions` 是 Ebox 作者属性的唯一来源:加载时一次生成 Ebox 自己的只读查询索引与一个不可变 ECSS package schemasurface schema 只组合一次node 构造和更新不重复注册或复制整份 schema。 - `ebox-style--property-definitions` 是 Ebox 作者属性的唯一来源:加载时一次生成 Ebox 自己的只读查询索引与一个不可变 ECSS package schemasurface schema 只组合一次node 构造和更新不重复注册或复制整份 schema。
- E1 固化封闭的 M2a 目标分类:不可变 generation fact、绑定 generation 的 scroll/native authority、opaque TP client-state custody、单向 compatibility mirror以及 disposable cache。每一行分别记录当前 v1 存储形态,以及目标 owner、mutation API、generation binding、rollback、rebuild proof 与 cleanup。在后续 checkpoint 改变存储形态前,当前 v1 TP client state 仍保存完整 Ebox state plist。E1 mirror 探针只重建并比较投影,不修改 live state。 - E1 固化封闭的 M2a 目标分类:不可变 generation fact、绑定 generation 的 scroll/native authority、opaque TP client-state custody、单向 compatibility mirror以及 disposable cache。每一行分别记录当前 v1 存储形态,以及目标 owner、mutation API、generation binding、rollback、rebuild proof 与 cleanup。在后续 checkpoint 改变存储形态前,当前 v1 TP client state 仍保存完整 Ebox state plist。E1 mirror 探针只重建并比较投影,不修改 live state。
- `ebox-layout.el` 不再 load 或调用 `ebox-surface.el`/TP。candidate isolation、继承 cascade 判定和一次性 materialization 只能通过 `ebox-render-context.el` 拥有的已校验 port 进入 layout`ebox.el` 在两侧加载后接线 surface 实现。
- `owner-rerender` 范围大于 `span-patch``span-patch` 大于 `paint-patch` - `owner-rerender` 范围大于 `span-patch``span-patch` 大于 `paint-patch`
- Buffer 坐标属于生成它的 generation变更后必须重新获取。 - Buffer 坐标属于生成它的 generation变更后必须重新获取。
- Grid 使用普通测量与渲染流水线native reflow 可以拒绝不适合的树并回退到 Elisp正确性不变。 - Grid 使用普通测量与渲染流水线native reflow 可以拒绝不适合的树并回退到 Elisp正确性不变。
@ -114,6 +115,8 @@ make selector-tests
make dsl-tests make dsl-tests
make flex-tests make flex-tests
make state-contract-tests make state-contract-tests
make layout-boundary-tests
make layout-boundary-performance
make docs-contract-tests make docs-contract-tests
make ci-contract-tests make ci-contract-tests
make performance-evaluator make performance-evaluator

View File

@ -372,10 +372,6 @@ and its formatted content exactly fills the used height."
(defvar ebox--render-cache-scroll-state-retained-cost-cache) (defvar ebox--render-cache-scroll-state-retained-cost-cache)
(declare-function ebox-style-cascade-active-p "ebox-style" ()) (declare-function ebox-style-cascade-active-p "ebox-style" ())
(declare-function ebox-surface--candidate-root "ebox-surface"
(source preserve-identities-p))
(declare-function ebox-surface--inline-inheritance-required-p "ebox-surface"
(root source-index))
(declare-function ebox--add-render-face! (declare-function ebox--add-render-face!
"ebox-render-context" (string start end face &optional append)) "ebox-render-context" (string start end face &optional append))
@ -1941,18 +1937,11 @@ This function owns spatial layout only; it never creates a live TP surface."
((eq type 'grid) (ebox--render-grid node)) ((eq type 'grid) (ebox--render-grid node))
(t (error "ebox-render: unknown node type %S" type))))))) (t (error "ebox-render: unknown node type %S" type)))))))
(declare-function ebox-surface-producer
"ebox-surface"
(source &optional previous-state preserve-identities-p
state-overrides signals projection-kind
source-isolated-p source-path-copied-p))
(declare-function tp-surface-materialize-string "tp-surface" (plan-or-producer))
(defun ebox--render-ephemeral-static (node) (defun ebox--render-ephemeral-static (node)
"Render static NODE without constructing a TP object tree. "Render static NODE without constructing a TP object tree.
The source remains caller-owned; the candidate copy carries all ephemeral The source remains caller-owned; the candidate copy carries all ephemeral
region/node identities and render side tables for this one materialization." region/node identities and render side tables for this one materialization."
(let ((candidate (ebox-surface--candidate-root node t))) (let ((candidate (ebox-render-context-candidate-root node t)))
(let ((ebox--surface-materialization-active t) (let ((ebox--surface-materialization-active t)
(ebox--region-box-table (make-hash-table :test 'equal)) (ebox--region-box-table (make-hash-table :test 'equal))
(ebox--scroll-global-state (make-hash-table :test 'equal)) (ebox--scroll-global-state (make-hash-table :test 'equal))
@ -2012,23 +2001,17 @@ also pass a pre-rendered string while an active surface owns runtime facts."
(if (or ebox--surface-materialization-active (if (or ebox--surface-materialization-active
ebox--render-runtime-revision) ebox--render-runtime-revision)
(ebox--render-layout node) (ebox--render-layout node)
(require 'ebox-surface)
(if (and (not (stringp node)) (if (and (not (stringp node))
(not (ebox-style-cascade-active-p)) (not (ebox-style-cascade-active-p))
(zerop (ebox-tree-author-style-count node)) (zerop (ebox-tree-author-style-count node))
(not (ebox-surface--inline-inheritance-required-p (not
node source-index))) (ebox-render-context-inline-inheritance-required-p
node source-index)))
(let ((ebox--region-id-counter ebox--region-id-counter) (let ((ebox--region-id-counter ebox--region-id-counter)
(ebox--runtime-node-id-counter ebox--runtime-node-id-counter)) (ebox--runtime-node-id-counter ebox--runtime-node-id-counter))
(ebox--render-ephemeral-static node)) (ebox--render-ephemeral-static node))
(let ((ebox--surface-materialization-active t) (ebox-render-context-materialize-string
(ebox--region-id-counter ebox--region-id-counter) node base-source-index)))))
(ebox--runtime-node-id-counter ebox--runtime-node-id-counter))
(tp-surface-materialize-string
(ebox-surface-producer
node nil t
(and base-source-index
(list :source-base-index base-source-index)))))))))
(unless ebox--paint-origin-capture-p (unless ebox--paint-origin-capture-p
(ebox--strip-paint-origins! rendered)) (ebox--strip-paint-origins! rendered))
rendered)) rendered))

View File

@ -7,6 +7,64 @@
;;; Code: ;;; Code:
(require 'cl-lib)
(define-error 'ebox-layout-context-error
"Ebox layout context is unavailable")
(cl-defstruct
(ebox-layout-context-port
(:constructor ebox-render-context--make-layout-port))
"Injected operations used when layout needs candidate materialization."
candidate-root inline-inheritance-required-p materialize-string)
(defvar ebox--layout-context-port nil
"Current surface-independent input port for candidate materialization.")
(cl-defun ebox-render-context-create-layout-port
(&key candidate-root inline-inheritance-required-p materialize-string)
"Create a validated layout context port from three operation functions."
(dolist (entry
`((candidate-root . ,candidate-root)
(inline-inheritance-required-p
. ,inline-inheritance-required-p)
(materialize-string . ,materialize-string)))
(unless (functionp (cdr entry))
(signal 'ebox-layout-context-error
(list :invalid-operation (car entry) (cdr entry)))))
(ebox-render-context--make-layout-port
:candidate-root candidate-root
:inline-inheritance-required-p inline-inheritance-required-p
:materialize-string materialize-string))
(defun ebox-render-context--require-layout-port ()
"Return the current validated layout context port."
(unless (ebox-layout-context-port-p ebox--layout-context-port)
(signal 'ebox-layout-context-error (list :missing-layout-port)))
ebox--layout-context-port)
(defun ebox-render-context-candidate-root (source preserve-identities-p)
"Return an isolated candidate for SOURCE through the current layout port."
(funcall
(ebox-layout-context-port-candidate-root
(ebox-render-context--require-layout-port))
source preserve-identities-p))
(defun ebox-render-context-inline-inheritance-required-p
(root source-index)
"Ask the current layout port whether ROOT requires inherited cascade."
(funcall
(ebox-layout-context-port-inline-inheritance-required-p
(ebox-render-context--require-layout-port))
root source-index))
(defun ebox-render-context-materialize-string (source source-index)
"Materialize SOURCE with SOURCE-INDEX through the current layout port."
(funcall
(ebox-layout-context-port-materialize-string
(ebox-render-context--require-layout-port))
source source-index))
(defvar ebox-viewport-width nil) (defvar ebox-viewport-width nil)
(defvar ebox-viewport-height nil) (defvar ebox-viewport-height nil)
(defvar ebox--render-runtime-revision nil) (defvar ebox--render-runtime-revision nil)

View File

@ -5324,6 +5324,16 @@ also bypass candidate copying and reconciliation without clearing those nodes."
context source previous-state preserve-identities-p state-overrides context source previous-state preserve-identities-p state-overrides
signals projection-kind source-isolated-p source-path-copied-p))))) signals projection-kind source-isolated-p source-path-copied-p)))))
(defun ebox-surface-materialize-ephemeral-string (source source-index)
"Materialize SOURCE with optional SOURCE-INDEX through one ephemeral TP plan."
(let ((ebox--surface-materialization-active t)
(ebox--region-id-counter ebox--region-id-counter)
(ebox--runtime-node-id-counter ebox--runtime-node-id-counter))
(tp-surface-materialize-string
(ebox-surface-producer
source nil t
(and source-index (list :source-base-index source-index))))))
(provide 'ebox-surface) (provide 'ebox-surface)
;;; ebox-surface.el ends here ;;; ebox-surface.el ends here

10
ebox.el
View File

@ -65,6 +65,16 @@
(require 'ebox-dsl) (require 'ebox-dsl)
(require 'ebox-selector) (require 'ebox-selector)
(defconst ebox--default-layout-context-port
(ebox-render-context-create-layout-port
:candidate-root #'ebox-surface--candidate-root
:inline-inheritance-required-p
#'ebox-surface--inline-inheritance-required-p
:materialize-string #'ebox-surface-materialize-ephemeral-string)
"Default Ebox layout input port backed by surface-owned materialization.")
(setq ebox--layout-context-port ebox--default-layout-context-port)
(declare-function ebox--buffer-update-report "ebox-incremental" (buffer)) (declare-function ebox--buffer-update-report "ebox-incremental" (buffer))
(declare-function ebox--set-buffer-update-report (declare-function ebox--set-buffer-update-report
"ebox-incremental" (buffer report)) "ebox-incremental" (buffer report))

View File

@ -298,7 +298,7 @@
(push (push
(append (append
(list 'box (list 'box
:key (list 'cell row column) :key (format "cell-%d-%d" row column)
:width '(52) :width '(52)
:padding '(0 1) :padding '(0 1)
:color "#0F172A" :color "#0F172A"
@ -319,7 +319,7 @@
(dotimes (index 12) (dotimes (index 12)
(push (push
(list 'box (list 'box
:key (list 'resize-item index) :key (format "resize-item-%d" index)
:width '(70) :width '(70)
:padding '(0 1) :padding '(0 1)
:color "#172554" :color "#172554"
@ -568,6 +568,30 @@
(plist-get report :checks))) (plist-get report :checks)))
reports)) reports))
;;;###autoload
(defun ebox-performance-evaluator-layout-boundary-report ()
"Run the pure layout/materialization scenarios used by the M2a E2 gate."
(let ((reports (list (ebox-performance-evaluator--pure-static)
(ebox-performance-evaluator--stylesheet-pure))))
(dolist (report reports)
(ebox-performance-evaluator--print-report report))
(ebox-performance-evaluator--assert
(ebox-performance-evaluator--all-passed-p reports)
"layout boundary scenarios failed")
(princ "LAYOUT-BOUNDARY-PERFORMANCE PASS\n")
reports))
;;;###autoload
(defun ebox-performance-evaluator-layout-boundary-batch ()
"Run the M2a E2 performance gate as a batch command."
(condition-case error-data
(progn
(ebox-performance-evaluator-layout-boundary-report)
(kill-emacs 0))
(error
(princ (format "LAYOUT-BOUNDARY-PERFORMANCE FAIL: %S\n" error-data))
(kill-emacs 1))))
;;;###autoload ;;;###autoload
(defun ebox-performance-evaluator-report () (defun ebox-performance-evaluator-report ()
"Run all declared Ebox performance scenarios and print their reports." "Run all declared Ebox performance scenarios and print their reports."

View File

@ -19,6 +19,8 @@
"core-tests:" "child-range-tests:" "grid-tests:" "ebox-commit-tests:" "core-tests:" "child-range-tests:" "grid-tests:" "ebox-commit-tests:"
"surface-tests:" "surface-tests:"
"state-contract-tests:" "state-contract-tests:"
"layout-boundary-tests:"
"layout-boundary-performance:"
"docs-contract-tests:" "docs-contract-tests:"
"ci-contract-tests:" "native-rust-tests:" "ci-contract-tests:" "native-rust-tests:"
"native-build:" "performance-evaluator:" "diff-check:")) "native-build:" "performance-evaluator:" "diff-check:"))

View File

@ -31,6 +31,7 @@
"ebox-buffer-backend.el" "ebox-incremental.el" "ebox-surface.el" "ebox-dsl.el" "ebox-buffer-backend.el" "ebox-incremental.el" "ebox-surface.el" "ebox-dsl.el"
"ebox-selector.el" "ebox-native-reflow.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-state-contract-tests.el"
"tests/ebox-layout-boundary-tests.el"
"tests/ebox-child-range-tests.el" "tests/ebox-grid-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-commit-tests.el" "tests/ebox-surface-tests.el"
"tests/ebox-dsl-tests.el" "tests/ebox-flex-tests.el" "tests/ebox-dsl-tests.el" "tests/ebox-flex-tests.el"
@ -48,6 +49,7 @@
'("check" "ci" "load" "compile" "core-tests" "child-range-tests" "grid-tests" '("check" "ci" "load" "compile" "core-tests" "child-range-tests" "grid-tests"
"ebox-commit-tests" "surface-tests" "visual-check-tests" "package-tests" "ebox-commit-tests" "surface-tests" "visual-check-tests" "package-tests"
"selector-tests" "dsl-tests" "flex-tests" "state-contract-tests" "selector-tests" "dsl-tests" "flex-tests" "state-contract-tests"
"layout-boundary-tests" "layout-boundary-performance"
"docs-contract-tests" "ci-contract-tests" "performance-evaluator" "docs-contract-tests" "ci-contract-tests" "performance-evaluator"
"visual-check" "visual-check"
"native-rust-tests" "native-build" "package-lint" "diff-check") "native-rust-tests" "native-build" "package-lint" "diff-check")

View File

@ -0,0 +1,205 @@
;;; ebox-layout-boundary-tests.el --- M2a layout dependency gates -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'cl-lib)
(require 'ebox)
(require 'ebox-fixtures)
(defconst ebox-layout-boundary-test--root
(expand-file-name ".." (file-name-directory (or load-file-name buffer-file-name)))
"Repository root used by layout boundary tests.")
(defun ebox-layout-boundary-test--source (file)
"Return repository FILE as text."
(with-temp-buffer
(insert-file-contents
(expand-file-name file ebox-layout-boundary-test--root))
(buffer-string)))
(defun ebox-layout-boundary-test--legacy-static-render (node)
"Render NODE through the pre-E2 static materialization edge."
(let ((candidate (ebox-surface--candidate-root node t)))
(let ((ebox--surface-materialization-active t)
(ebox--region-box-table (make-hash-table :test #'equal))
(ebox--scroll-global-state (make-hash-table :test #'equal))
(ebox--scroll-idle-prefetch-timers (make-hash-table :test #'equal))
(ebox--smooth-scroll-state-table (make-hash-table :test #'equal))
(ebox--render-cache-table (make-hash-table :test #'equal))
(ebox--render-cache-signature-cache (make-hash-table :test #'eq))
(ebox--box-content-render-cache (make-hash-table :test #'eq))
(ebox--viewport-dependent-node-ids-cache (make-hash-table :test #'eq))
(ebox--viewport-dependent-subtree-cache (make-hash-table :test #'eq))
(ebox--viewport-height-dependent-subtree-cache
(make-hash-table :test #'eq))
(ebox--flex-content-min-width-table (make-hash-table :test #'eq))
(ebox--render-runtime-revision nil)
(ebox--render-cache-allow-viewport-dependent nil)
(ebox--render-cache-scroll-state-region-ids nil)
(ebox--render-cache-scroll-state-restorable-p nil)
(ebox--render-cache-scroll-state-retained-cost-cache nil))
(ebox--render-layout candidate))))
(defun ebox-layout-boundary-test--legacy-render (input)
"Render canonical INPUT through the exact pre-E2 direct call edge."
(let* ((node
(ebox-canonical-input--single-root input "legacy E2 artifact"))
(base-source-index (ebox-canonical-input--source-index input))
(ebox--render-source-index base-source-index)
(source-index
(ebox-tree-source-index
node t ebox--render-root-parent-kind base-source-index nil))
(rendered
(if (and (not (ebox-style-cascade-active-p))
(zerop (ebox-tree-author-style-count node))
(not (ebox-surface--inline-inheritance-required-p
node source-index)))
(let ((ebox--region-id-counter ebox--region-id-counter)
(ebox--runtime-node-id-counter
ebox--runtime-node-id-counter))
(ebox-layout-boundary-test--legacy-static-render node))
(let ((ebox--surface-materialization-active t)
(ebox--region-id-counter ebox--region-id-counter)
(ebox--runtime-node-id-counter ebox--runtime-node-id-counter))
(tp-surface-materialize-string
(ebox-surface-producer
node nil t (list :source-base-index base-source-index)))))))
(ebox--strip-paint-origins! rendered)))
(defun ebox-layout-boundary-test--fixtures ()
"Return representative static and materialized layout inputs."
(list
(ebox-test-column
(ebox-test-row
(ebox-test-box (ebox-test-text "A") :width '(48))
(ebox-test-box (ebox-test-text "B") :width '(48)))
(ebox-test-box (ebox-test-text "Tail") :width '(96)))
(ebox-test-flex
:flex-flow '(row wrap) :column-gap '(2)
(ebox-test-box (ebox-test-text "One") :width '(52))
(ebox-test-box (ebox-test-text "Two") :width '(52)))
(ebox-test-grid
:grid-template-columns '((54) (fr 1))
(ebox-test-box (ebox-test-text "Left"))
(ebox-test-box (ebox-test-text "Right")))
(ebox-test-box
:font-size 16
(ebox-test-box (ebox-test-text "Inherited")))))
(ert-deftest ebox-layout-boundary-has-no-surface-or-publication-edge ()
"The layout module depends only on its injected render context."
(let ((source (ebox-layout-boundary-test--source "ebox-layout.el")))
(should-not (string-match-p "(require[[:space:]]+'ebox-surface)" source))
(should-not (string-match-p "\\_<ebox-surface-" source))
(should-not (string-match-p "\\_<tp-surface-" source))
(should (string-match-p "ebox-render-context-candidate-root" source))
(should (string-match-p
"ebox-render-context-materialize-string" source))))
(ert-deftest ebox-layout-boundary-preserves-pre-e2-layout-artifacts ()
"The injected route preserves every old character and property interval."
(dolist (input (ebox-layout-boundary-test--fixtures))
(let ((legacy (ebox-layout-boundary-test--legacy-render input))
(current (ebox-render input)))
(should (equal-including-properties current legacy))
(should
(equal (mapcar #'ebox--string-pixel-width (ebox-string-lines current))
(mapcar #'ebox--string-pixel-width
(ebox-string-lines legacy)))))))
(ert-deftest ebox-layout-boundary-static-route-is-a-strict-subset ()
"Static layout uses candidate and inheritance inputs without TP materialize."
(let ((candidate-calls 0)
(inheritance-calls 0)
(materialize-calls 0)
(default ebox--default-layout-context-port))
(let ((ebox--layout-context-port
(ebox-render-context-create-layout-port
:candidate-root
(lambda (&rest arguments)
(cl-incf candidate-calls)
(apply (ebox-layout-context-port-candidate-root default)
arguments))
:inline-inheritance-required-p
(lambda (&rest arguments)
(cl-incf inheritance-calls)
(apply
(ebox-layout-context-port-inline-inheritance-required-p default)
arguments))
:materialize-string
(lambda (&rest arguments)
(cl-incf materialize-calls)
(apply (ebox-layout-context-port-materialize-string default)
arguments)))))
(ebox-render
(ebox-test-box (ebox-test-text "Static") :width '(96))))
(should (= candidate-calls 1))
(should (= inheritance-calls 1))
(should (= materialize-calls 0))))
(ert-deftest ebox-layout-boundary-materialized-route-is-exactly-once ()
"Inherited layout invokes its context materializer exactly once."
(let ((candidate-calls 0)
(inheritance-calls 0)
(materialize-calls 0)
(default ebox--default-layout-context-port))
(let ((ebox--layout-context-port
(ebox-render-context-create-layout-port
:candidate-root
(lambda (&rest arguments)
(cl-incf candidate-calls)
(apply (ebox-layout-context-port-candidate-root default)
arguments))
:inline-inheritance-required-p
(lambda (&rest arguments)
(cl-incf inheritance-calls)
(apply
(ebox-layout-context-port-inline-inheritance-required-p default)
arguments))
:materialize-string
(lambda (&rest arguments)
(cl-incf materialize-calls)
(apply (ebox-layout-context-port-materialize-string default)
arguments)))))
(ebox-render
(ebox-test-box
:font-size 16
(ebox-test-box (ebox-test-text "Inherited")))))
(should (= candidate-calls 0))
;; Author-style pending is sufficient to choose the materialized route,
;; so the inherited-cascade predicate is correctly short-circuited here.
(should (= inheritance-calls 0))
(should (= materialize-calls 1))))
(ert-deftest ebox-layout-boundary-port-fault-leaves-source-unmodified ()
"A materialization-port fault cannot publish or consume source identity."
(let* ((input
(ebox-test-box
:font-size 16
(ebox-test-box (ebox-test-text "Fault"))))
(root (ebox-test-root input))
(source-index (ebox-test-source-index input))
(region-counter ebox--region-id-counter)
(node-counter ebox--runtime-node-id-counter)
(default ebox--default-layout-context-port)
(ebox--layout-context-port
(ebox-render-context-create-layout-port
:candidate-root
(ebox-layout-context-port-candidate-root default)
:inline-inheritance-required-p (lambda (&rest _arguments) t)
:materialize-string
(lambda (&rest _arguments)
(error "Injected E2 materialization fault")))))
(should-error (ebox-render input) :type 'error)
(should (eq (ebox-test-root input) root))
(should (eq (ebox-test-source-index input) source-index))
(should (= ebox--region-id-counter region-counter))
(should (= ebox--runtime-node-id-counter node-counter))
(should-not (plist-member root :node-id))
(should-not (plist-member root :region-id))
(should-not (plist-member root :surface-object))))
(provide 'ebox-layout-boundary-tests)
;;; ebox-layout-boundary-tests.el ends here