13 KiB
TP 1.0 Current Architecture
本文描述 TP 1.0 当前实现的模块边界、权威状态、数据流和事务模型。公共行为合同见 API semantics,设计背景见 retained runtime architecture。
按功能查找公共入口和用法时,使用 API reference。
1. 定位
TP 是通用 retained/reactive text runtime:
application state
→ signals/bindings
→ prepare context + stable objects
→ pure surface plan
→ reconcile/diff
→ atomic Buffer publication
TP 负责文本属性 contribution、响应式依赖、身份、位置、变化和提交。调用者负责业务含义以及期望显示结果。
TP 不依赖 Ebox 或 ECSS,不包含 selector、stylesheet、CSS cascade、Box/Flex/Grid、measurement、layout owner 或 viewport dirty semantics。
2. 模块图
tp-core
├─ tp-style
│ └─ tp-reactive
│ └─ tp-surface
├─ tp-layer
├─ tp-ops
├─ tp-search
├─ tp-query
├─ tp-palette
└─ tp-builtins
tp.el loads the public package surface
真实 require 关系按源码为准;上图表达责任层次,不要求每个 consumer 经过所有中间模块。
| Module | Owns | Must not own |
|---|---|---|
tp-core.el |
canonical ranges/requests/results、interval traversal、plist/face merge、native property facts | runtime identity、reactivity、publication |
tp-style.el |
native property policies、direct declarations、explicit computed source、projection | selector、stylesheet、specificity、CSS winner |
tp-reactive.el |
signals、bindings、dynamic dependency graph、scheduler、candidate source state、transaction participants | buffer scans、mount positions、layout impact |
tp-surface.el |
prepare context、objects、plans、anchors、mount/index、contribution ledger、diff、publication、rollback、reports | stylesheet/cascade、consumer layout decisions |
tp-layer.el |
define-tp/define-tps declaration recipes and registry |
live layer stack、inline runtime metadata、watcher engine |
tp-ops.el |
direct set/reset/add/get/at/remove/clear plus one-shot tp-propertize/tp-apply |
retained identity、scan refresh |
tp-search.el |
match/regexp application、property search/navigation | runtime identity |
tp-query.el |
native lookup/change wrappers and mutation policy | retained publication |
tp-palette.el |
theme-aware palette data | runtime scheduling |
tp-builtins.el |
built-in direct recipes and display helpers | managed refresh hooks |
tp.el |
package metadata and public module loading | business logic |
There is no tp-render.el or tp-stack.el. The 0.3 scan renderer and managed stack runtime were deleted rather than wrapped.
3. Property data flow
Direct declarations are native property/value pairs. tp-style.el resolves them through one policy pipeline:
native declarations
→ canonical text/PROPERTY ids
→ explicit computed-source resolution
→ normalize
→ validate
→ merge contributions
→ project to final Emacs properties
Ordinary functions are literal. Only a tagged tp-computed source runs. Explicit nil remains distinguishable from absence throughout projection and retained contribution ownership.
The policy registry is generic. It knows how final Emacs properties compose; it does not decide which stylesheet declaration wins. CSS selection belongs to ECSS outside TP.
4. Static façade and declaration recipes
tp-propertize and tp-apply use the same direct projection core but do not create live state. tp-set/reset/add/remove and the search/query families share canonical range, presence, and mutation primitives from tp-core.el.
define-tp and define-tps store recipe arglists and body forms. Application expands a recipe into ordinary direct properties. Static recipes are also compiled into the named style registry. Parameterized recipes stay evaluable recipes rather than frozen declarations.
Recipe and group redefinition uses candidate registry state and commits only after body expansion, generated element creation, and named-style compilation succeed. Failure restores the previous registry state.
No recipe application writes tp-name, tp-layers, or tp-meta to text. No $variable parser remains.
5. Reactive graph
The authoritative graph lives in tp-reactive.el:
signal ──subscribers──> binding ──subscribers──> binding
│
└── owner object/surface
A binding is identified by owner plus caller-namespaced key. While its compute function runs, tp-signal-read and tp-binding-read record the exact dependencies used in that execution. On success, the new dependency set replaces the old set. A conditional branch therefore removes obsolete subscriptions automatically.
Signal writes enter transaction-local candidate state. Dirty bindings are deduplicated and evaluated by dependency order. Equal signal writes and equal binding results stop propagation. Nested writes queue another stabilization pass rather than recursively mutating output. Cycle detection reports the path.
The graph contains no layer-to-buffer registry. A source reaches surfaces through binding owners, not by scanning buffer-list or searching text properties.
6. Prepare context and identity
Every materialize/mount/update creates a short-lived prepare context. A producer calls tp-object-ensure before producing the corresponding plan node.
Object identity is scoped to one surface and derived from:
- parent object identity;
- sibling-local explicit key, or unkeyed position;
- opaque kind.
Candidate objects exist only inside the context. Successful publication promotes them to live objects; failed contexts dispose them and their bindings/anchors. tp-object-resolve queries live identity by key path without creating state.
The context records touched objects/bindings. Omitted objects are removed. Omitted bindings default to deletion unless an explicit lifecycle says retain. A logical object with no direct plan node must call tp-object-retain; disjoint physical output is attached through tp-object-attach-fragment. Content ranges normally use tp-object-attach-content-ranges, which defensively snapshots opaque tags. A producer that freshly allocates candidate-local tags and never mutates or exposes them may use tp-object-attach-content-ranges-owned to transfer those snapshots without another copy. The same candidate-local ownership rule applies to tp-surface-plan-create-owned and tp-surface-result-create-owned; an owned result is bound to the active prepare context and consumed once, while normal plan/result constructors remain defensive boundaries.
7. Pure surface plans
A plan is a defensive immutable-semantics tree of key/kind/text/props/children/tags/capability. It contains desired output only.
It deliberately excludes:
- buffer/position/marker;
- patch operation or inverse journal;
- producer/binding closure;
- client continuation;
- consumer-specific layout identity.
TP validates sibling keys, legal text/children combinations, property shape, and capability before publication. Tags remain opaque; they are indexed for callers but never interpreted by TP.
tp-surface-materialize-string creates an ephemeral surface/context, renders the plan, then releases all candidate runtime state. tp-surface-mount creates a live surface and stores the producer or plan for later reactive preparation.
8. Mounts and side indexes
Every live surface owns:
- key path to live object table;
- object to bindings;
- object to marker-backed mounts;
- position/tag query index;
- retained plan and producer;
- properties contribution ledger;
- opaque client state;
- revision and last report.
The displayed text contains only properties needed by Emacs display or interaction. Identity, provenance, dependencies, marker metadata, revisions, and client state stay in side state.
content mounts own their text and properties. properties mounts attach objects to opaque range anchors and can only contribute properties to host-owned text.
One object may have multiple disjoint mounts. Public queries expose numeric range/tag snapshots, never live markers.
9. Properties contribution ledger
For every relevant anchor/property interval, the surface keeps:
- host baseline presence/value;
- ordered TP contributions;
- last published presence/value;
- contributing anchors.
Candidate preparation collects interval boundaries from old ledger entries, current mounts, and current host property runs. It verifies that a previously published value has not been replaced externally, composes the baseline with current contributions through the property policy, and emits an operation only when the resulting presence/value changes.
An external mismatch raises tp-property-conflict. tp-range-rebase replaces the baseline with current host state. Unmount restores a baseline only when the current value is still TP's last published value; otherwise it preserves the host edit and reports the conflict.
10. Reconcile and diff
TP reconciles object identity by the prepare tree and compares old/new plans for:
- created, removed, retained, and moved keyed objects;
- minimal character replacement using common prefix/suffix;
- exact property-run differences;
- mount/index changes;
- scoped output authorization.
tp-surface-update-scoped maps requested objects directly through the object-to-mount index. For content surfaces it proves old/new changes stay within those mounted ranges; properties surfaces perform the equivalent contribution-range proof. A mismatch is an error unless root fallback is explicitly selected. Callers that only need scalar commit metrics may request no report snapshot and read tp-surface-report-summary after publication.
An equal candidate produces no prepared publication. It preserves revision, report, buffer modified state, markers, and client state.
11. Transaction and publication
The outer transaction owns candidate source values, dirty bindings, prepared surfaces, participants, inverse journals, view state, and final observer scheduling.
freeze candidate writes
→ recompute exact dependency closure
→ prepare every affected surface
→ validate all candidates
→ capture inverse journals
→ publish surfaces in stable id order
→ publish transaction participants
→ commit signals/bindings/surface state/revisions
→ run observers
Content publication edits the minimal text span and then exact property runs. Properties publication writes only prepared contribution operations. Marker mounts, indexes, plans, producer, bindings, opaque client state and report switch with the same revision.
Rollback restores text, properties, marker/index state, plans, producer, client state, signal values, binding values/dependencies, dirty queues, revisions and reports. Property journals are explicit because atomic-change-group alone does not cover every silent property mutation path.
tp-transaction-participate lets a consumer promote rollback-capable opaque state inside this boundary. Observers are different: they run only after the transaction commits, and observer failure is recorded rather than rolled back.
If publication kills a target buffer, kill teardown is authoritative. Other surfaces and source state roll back; TP never recreates the killed buffer.
12. Lifecycle
Surfaces are buffer-local lifecycle owners. A weak global registry supports lookup without keeping dead buffers alive. Mount installs local change/kill hooks; unmount and kill remove hooks, markers, ledger entries, objects, bindings, subscriptions, indexes, client state and weak registrations.
Global signals are explicitly disposable. Buffer-scoped signals are disposed by their buffer kill hook. Owner disposal detaches both dependency directions so no downstream subscriber keeps a dead object alive.
13. Diagnostics
Public diagnostics are defensive snapshots:
tp-reactive-countersreports graph work;tp-surface-reportreports the last publication;tp-surface-inspectreports surface lifecycle/state counts;tp-surface-at-pointqueries side indexes;tp-object-mountsreturns numeric range/tag snapshots.
Reports use generic terms such as bindings, objects, text/property operations, touched characters, scope and rollback. They contain no Ebox paint/layout vocabulary.
14. Architectural invariants
- TP source/tests/examples/package metadata do not require or name Ebox/ECSS runtime APIs.
- TP contains no CSS selector/stylesheet/specificity/origin/winner engine.
- There is one signal/binding/surface/mount/diff/transaction runtime; no embedded mode exists.
- TP is the only writer for live TP surfaces.
- Normal source-to-output flow is signal to binding to object to mount; it does not scan buffers or displayed text for identity.
tp-name,tp-layers, andtp-metaare not runtime storage.- Plans contain no raw positions or lifecycle closures.
- Ordinary functions are literal; only
tp-computedexecutes. - Candidate failure leaks no object, binding, anchor, subscription or revision.
- Every successful publication advances Buffer state and side state together; every failure preserves the previous committed revision.