tp/docs/API-SEMANTICS.md
Kinneyzhang 1195297011 feat(tp): isolate stylesheet rule domains
Give independent consumers their own rules, cascade layer ordering, and source-order counters so packages such as Ebox cannot pollute TP's default stylesheet or each other. Match generic class and state tokens by value and document caller-owned stylesheet lifecycle.

Verified: make clean; make test (728/728); make compile WERROR=t; checkdoc tp-style.el; git diff --check; Ebox make test against ../tp.
2026-08-06 13:59:08 +08:00

21 KiB
Raw Blame History

tp API 语义规范

本文档是 tp 核心 API 的当前行为契约。代码、测试、README 与 docstring 若与本文冲突,应以经过测试的代码为准并同步修正文档。

tp 当前定位是 Emacs 文本属性的高层操作工具箱,以及一套受管理的命名层、层栈和响应式渲染模型。它尚不是全部原生文本/字符属性语义的等价替代品;完整范围与路线见 REPOSITORY-AUDIT.md

TP 1.0 迁移的第一层纯计算合同已经落地在 tp-style.elnamespaced property schema、结构化 selector、确定性 cascade、逐属性继承、custom property、显式 tp-computed 和 Emacs 属性投影已经是当前 public behaviorretained object/surface/mount/transaction 尚未切换,仍以目标架构文档描述为未来合同。

Stage 2 canonical façade 已完成为内部模型:tp--native-rangetp--presencetp--requesttp--matchtp--result 是模块间传递的规范记录。公开入口和历史返回值保持兼容,不因内部模型收敛而改变。

Stage 3 text-only 原生语义 façade 已完成direct/effective/source-aware lookup、property change/any/not-all 与三种 mutation policy 组合均有明确公开边界。

Stage 4 managed lifecycle 已完成managed metadata、attach/detach、diagnostics、transaction、参数化 mounted layer args/version refresh 与 theme generation diagnostics 均有公开边界。

Stage 5 overlay-aware 字符属性查询已完成:tp-lookup :char / :char-source 报告 Emacs 选出的字符属性值、overlay 来源和获胜 overlay 身份。overlay 创建、移动、删除、priority 管理和生命周期不属于 tp 契约。

1. 对象、范围与坐标

  • 所有范围均采用半开区间 [START, END)
  • 字符串使用 Emacs 原生的 0-based 坐标,合法边界为 0(length STRING)
  • 缓冲区使用 Emacs 原生位置,通常从 point-min 开始;显式 BUFFER 和 nil当前缓冲区具有相同语义。
  • 范围结果原则上使用目标对象的原生坐标。当前公开兼容例外是 tp-intervals / tp-intervals-map:缓冲区默认返回相对 START 的 offset传入 ABSOLUTE 才返回可直接回传给写 API 的原生坐标。内部 canonical range 使用原生坐标,但该 relative 默认作为历史兼容例外保留。

2. 修改策略

调用族 字符串 缓冲区
tp-set / tp-reset / tp-add / tp-remove 整串形式 复制后返回新字符串 不适用
上述函数的 START/END 形式 原地修改显式字符串 OBJECT 原地修改
tp-match-* / tp-regexp-* 返回新字符串 原地修改,返回匹配范围
层栈修改函数 原地修改字符串 原地修改
tp-forward-do / tp-backward-do / tp-search-map 原地修改;替换文本必须等长 原地修改;替换文本可增减长度

普通缓冲区写入使用 Emacs 的属性修改原语,因此遵循 Emacs 的 modified、undo 和 read-only 行为tp 不把所有修改默认包装为 silent modification。响应式重渲染和 tp-text 文本替换为了恢复受管理输出会在内部允许修改 read-only 文本,这是当前实现边界,不代表公开 API 已提供统一的 read-only 策略。相同值刷新会尽量避免无意义地翻转 buffer-modified 状态。

3. presence、nil 与 wildcard

以下三种状态必须区分:

  1. 属性键不存在;
  2. 属性键存在,值为 nil
  3. 属性键存在,值为非 nil。

tp-member 用于判断直接属性键是否存在;tp-at / plist-get 单独使用时不能区分前两种状态。删除最后一个子属性会移除空的父属性键,不会偶然留下 (PROPERTY nil)

搜索 API 的规则是:

  • 省略 VALUE匹配该直接属性的任意已存在值
  • 显式传入 nil只匹配“键存在且值为 nil”
  • 需要继续提供 OBJECT、次数或范围等后续位置参数时传入公共唯一哨兵 tp-any-value 表示任意值;
  • 自定义 PREDICATE 总是优先执行,并接收请求 VALUE可能是 tp-any-value)和实际属性值。

属性缺失的范围不属于搜索结果,即使搜索值是 nil。

4. 搜索结果

  • tp-search 对字符串和缓冲区范围都返回 (START END VALUE) 列表。
  • tp-forward / tp-backward 的字符串路径返回前/后 N 个 (START END VALUE);缓冲区路径移动 point并返回第 N 次搜索的 prop-match
  • tp-forward-do / tp-backward-do 只在第 N 个匹配上调用函数;匹配不足 N 个时不调用函数,返回实际找到的数量。
  • tp-search-map 处理全部匹配并返回处理数量。

tp-forward / tp-backward 的对象相关结果差异是现存公开兼容契约。内部搜索路径已收敛到 canonical match/result 记录;公开返回结构暂不改变。

5. 原生文本查询与修改策略

Stage 3/5 查询 façade 已完成text modes 保持 text-onlychar modes 委托 Emacs 的 overlay-aware 字符属性查询。

tp-lookup 返回 tp-lookup-result 记录,字段为 property、value、present-p、source、mode、object、position、overlay。text modes 中 overlay 始终为 nil:char / :char-source 中 overlay 为 Emacs 选出的获胜 overlay若文本 fallback 获胜则为 nil。

MODE 语义
:text-direct 只读取直接文本属性;显式 nil 与缺失通过 present-p/source 区分
:text-effective 值使用 get-text-propertysource 使用 text-only 来源解释
:text-source 返回 direct/category/alias/default/absent 来源和值,不查看 overlay
:char 使用 get-char-property-and-overlay 的值,按 Emacs overlay/text 优先级解析
:char-source :char,并在 overlay 获胜时把 source 设为 :overlay、overlay 设为获胜 overlay

source 取值为 :text-direct:category:alias:default:overlay:absent。direct 显式 nil 的结果是 present-p 为 t、value 为 nil、source 为 :text-directalias nil 与 Emacs 原生语义一致,会继续寻找后续 alias/default缺失属性的结果是 present-p 为 nil、value 为 nil、source 为 :absent

tp-property-change 是 Emacs property change 原语的显式封装::direction :next / :previous 选择 next/previous传入 :property 时使用 single-property change省略时使用 all-property change。

tp-property-any / tp-property-not-alltext-property-any / text-property-not-all 的薄封装,保留 Emacs 对显式 nil、边界和对象的行为。

tp-with-mutation-policy 只接受三种有效组合:

POLICY 语义
(:modified :ordinary :read-only :respect) 普通修改,尊重 read-only
(:modified :ordinary :read-only :inhibit) 绑定 inhibit-read-only,普通 modified/undo 行为
(:modified :silent :read-only :inhibit) 绑定 inhibit-read-only 并使用 with-silent-modifications

(:modified :silent :read-only :respect) 明确拒绝,因为 silent modification 与尊重 read-only 不能同时满足。

insert、copy、yank、stickiness、narrowing 和 indirect buffer 行为直接委托 Emacstp 不为这些原生操作提供 wrapper。

6. 直接模板展开与 managed mount

层名有两种不同用途:

6.1 直接模板展开

把非响应式层名传给 tp-settp-resettp-addtp-match-*tp-regexp-* 时,层定义展开为普通属性,通常不保留 tp-name。结果不能依赖层名进行后续移动、隐藏、按名删除或静态重定义刷新。

匿名响应式属性和响应式层需要保留 tp-name 才能登记与刷新,这是直接路径中的受管理例外。

6.2 managed mount

tp-push-layer / tp-put-layer 明确保留 tp-name 和必要的 tp-layers 状态。mounted layer 可以被查询、移动、隐藏、显示、删除和响应式刷新。

非参数化层重新定义后,已挂载区域按 old/new 所有权协调:

  • 新定义写入其拥有的键;
  • 旧定义拥有、但新定义不再拥有的键,仅在当前值仍等于旧值时移除;
  • 外部已经改写的值不会被当作旧层残留删除。

所有 managed mount 都携带 lifecycle metadata。即使只有单个 managed layer只要存在 tp-meta,权威存储也使用 tp-layers;直接渲染属性和 tp-layer-stack-at 等 public stack query 不暴露 tp-meta

参数化层的已挂载 entry 保存调用实参、形参表和 definition version重新定义参数化层后既有 managed entry 会按保存的 args 刷新。历史无 metadata 的 entry 按 legacy entry 保守处理。

tp-attach-managed-layers 扫描已经进入缓冲区的 managed storage补齐/规范化 metadata登记发现的层并返回层名列表。tp-detach-managed-layers 移除 managed storageKEEP-RENDERED 非 nil 时保留当前可见渲染属性为普通文本属性。

tp-managed-layer-diagnosticstp-managed-buffer-diagnosticstp-managed-diagnostics 是只读诊断入口,报告 entries、args、registry、errors 与 theme diagnostics。tp-managed-diagnostics 的 theme 部分报告 generation、last hook source、refresh mode、refreshed ranges 和 errors这是 lifecycle 诊断,不是性能基准。

tp-layer-transaction 在给定范围内执行 managed stack 修改。成功返回结构化 plist其中 :statusok:ok 为 t:result 保留 FUNCTION 的返回值;失败时恢复事务前文本/属性快照并默认发出 tp-layer-transaction-errorNOERROR 非 nil 时返回结构化失败 plist。

7. tp-text 替换

  • 初次应用和响应式更新都按内嵌字符串的真实属性 interval 处理,不从位置 0 采样后扩散到整段。
  • 调用者显式属性覆盖内嵌属性;显式 nil 也是有效覆盖值。
  • 未被调用者覆盖的内嵌属性按各自 interval 保留。
  • 每个 interval 算出待写属性后,tp-set 只覆盖这些键并保留其他目标属性,tp-reset 替换目标的完整属性集合,tp-add 则对目标已有的 face-family 与嵌套 plist 继续合并;文本内容相同和发生替换时遵守同一规则。
  • 字符串和缓冲区路径遵守相同的 per-interval 属性计算;对象的复制/原地策略仍按第 2 节执行。

8. 错误边界

  • 未定义或无法解析的层使用 tp-unresolved-layer 表达。
  • 栈 API 的 NOERROR 只抑制 tp-unresolved-layer;参数化层 body、计算、属性结构和其他内部错误必须传播。
  • 隐藏层存储发生所有权冲突时使用 tp-layer-conflict
  • transform 失败或返回非字符串、compute 失败都属于业务输出失败,必须传播。
  • watcher 属于 observer单个 watcher 失败不会阻断 managed update失败会记录到 tp-reactive-observer-errorsnewest first并输出消息。
  • 公开边界不应把内部失败转换为默认值后继续写入。

9. 隐藏层与外部直接修改

存在隐藏层时,tp-layers 保存完整 managed stack其他直接属性是第一个可见层的渲染缓存。两类调用采用不同但明确的策略

  • definition/reactive refresh 知道正在刷新哪个定义,也知道直接属性对应哪个可见 entry它先把原生直接编辑协调进该可见 entry再执行 old/new 所有权刷新,因此外部改写值不会被当作旧定义残留删除;
  • 普通 stack decode/write 缺少这次 definition refresh 的所有权上下文,缓存不一致时会在写入前发出 tp-layer-conflict,且不修改层栈;
  • 所有层都隐藏时没有可接收直接属性的可见 entry此时出现直接属性始终发出 tp-layer-conflict

tp 不会把外部属性收编成新的匿名层,也不会静默覆盖它。

10. 返回值现状

核心写 API 的返回值仍保留历史差异:

  • buffer/region tp-set / tp-reset / tp-add(START . END)
  • 整串复制式写入:新字符串;
  • buffer tp-remove / tp-clearnil
  • stack mutator修改的 property-run 数量;
  • tp-put-layer / tp-push-layer:显式 OBJECT 或 (START . END)

这些返回值是当前兼容契约。property-run 数量会受无关 interval 边界影响,不应被当成稳定业务标识。内部 request/result 模型已建立,但不会改变这些历史 public returns。

11. 明确不在当前完整契约内

以下能力仍需设计或补齐,不能据现有 API 推断:

  • overlay lifecycle创建、移动、删除、priority 管理);
  • insert/copy/yank/stickiness 的 tp wrapper 或 managed workflow
  • mutation policy 三种组合之外的统一 read-only、silent modification、undo 策略;
  • 字符串/缓冲区完全一致的搜索结果结构;
  • observer 错误的清理、重试与汇总策略。

12. Schema-driven cascade

  • tp-define-property 只接受带 namespace 的 symbol id例如 text/faceebox/widthschema replacement 在完整验证后一次写入,失败不会破坏旧 definition。
  • structured selector 原生支持 type/id/class/attribute/state、compound、descendant/child/adjacent/general sibling以及 :is:where:nottp-selector-specificity 与 rule matching 使用同一 AST。
  • cascade 顺序固定为 importance、origin、layer、specificity、scope proximity、source ordernormal 与 important declaration 的 layer 顺序按 CSS 规则相反unlayered normal 高于 layered normal。
  • tp-stylesheet-create 建立相互隔离的 rule、layer order 与 source order domaintp-stylesheet-add-rule :stylesheet SHEET 只写入该实例,tp-compute-style :rules SHEET 只读取该实例,避免不同 consumer 通过默认全局 stylesheet 相互污染。
  • 独立 stylesheet 的生命周期归创建者所有;tp-style-reset 只清理 TP 的全局 schema、named style 与 default stylesheet调用方必须用 tp-style-reset-rules SHEET 显式清理自己的实例。
  • initialinheritunsetrevertrevert-layer 必须由 tp-wide-value 显式构造,普通同名 Elisp symbol 保持 literal。
  • 普通 function value 永远不执行;只有 tp-computed 包装的 function 在计算时执行一次,其返回值不二次调用。
  • --name custom property 默认继承;tp-var 支持 fallback 和 cycle invalidation。属性 schema 的 normalizer/validator 在变量与 wide value 求值后执行。
  • tp-compute-style 返回 tp-computed-style,保存 canonical values、resolved custom properties 和可选 provenancetp-project-style 是把 schema projector 汇总为最终 Emacs text properties 的唯一纯投影入口。
  • 静态 define-tp 和静态 define-tps 生成层会同步编译为同名 canonical style参数化或旧 $var 响应式定义不会冻结当前值为 style待新的 signal/binding runtime 接管其动态 source。

13. Signals、bindings 与 transaction

  • tp-signal-create 创建 global 或 buffer-scoped sourcetp-signal-read 仅在 binding compute context 中登记依赖,tp-signal-peek 永不登记依赖。buffer kill 会 dispose scoped signalglobal signal 可用 tp-signal-dispose 显式结束 lifecycle两者都会移除 subscriptions。
  • tp-bind 的 identity 是 owner object identity 加 caller-namespaced key重复安装复用同一 bindingcompute definition 变化才使它 dirty。tp-binding-read 读取 memoized value 并建立 binding→binding edge。
  • 每次成功 compute 以本次实际读取的依赖替换旧依赖;条件分支切换后旧 signal 不再触发。binding value 经其 equality comparator 判等,相等结果不 invalidates downstream。
  • signal write 先写入 transaction-local candidate state。最外层 tp-with-transaction 只遍历 exact dirty closure去重并按 binding dependency 拓扑惰性求值compute 内嵌 write 排队稳定,不递归执行。
  • tp-transaction-participate KEY PUBLISH ROLLBACK 只允许在 outer transaction 内登记。所有 surface candidate 发布并切换 client state 后participant 按登记顺序执行 PUBLISH;任一后续步骤失败时,已经进入 publication 的 participant 按逆序执行 ROLLBACK。KEY 在同一 transaction 内唯一两个函数均不得接收参数rollback 必须能够撤销 publish 已经开始后的部分副作用。
  • 任一 compute 或 cycle 失败会恢复 committed signal values、last successful binding values、dependencies、dirty state、owner registry 和 scheduler counters。cycle condition 携带 namespaced binding-key path。
  • tp-variable-signal 是 global/buffer-local Elisp variable包括后续 $var compiler的 source adapter它只转发精确 scope 的 write不调用 legacy layer renderer。
  • tp-reactive-counters 只公开 invalidated、recomputed、skipped、subscription-added、subscription-removed 五个工作量计数,不暴露 internal hash shape。

14. Retained surface、range ownership 与 publication

  • tp-surface-plan-create 只接受 key/kind/text/props/children/tags/capability 纯数据text 与 children 互斥,同一 parent 的显式 key 不可重复constructor 与每次 prepare 都做 defensive copy。普通 function property value 保持 literal identityplan 不携带 marker、position、binding 或 producer closure。
  • producer 在 active prepare context 中先用 tp-object-ensure 取得 candidate handle再返回 plan 或 tp-surface-result-create。成功 publication 才使新 handle live失败、materialize 返回或 orphan/cross-surface validation 失败都会释放 candidate bindings、anchors 与 subscriptions。tp-object-resolve 只读 live key path不创建 identity。
  • tp-object-retain 显式声明一个没有可见字符也应随 candidate 晋升的 logical object未进入 plan、未 retain、也未 attachment 的 touched object 仍以 tp-orphan-object 拒绝。content producer 可用 tp-object-attach-fragment 把同一 logical object 挂到多个 plan fragmentattachment 只存在 prepare/side state不进入 pure plan。tp-object-mounts 通过 object-keyed index 返回当前数值 start/end 与 opaque tags 的防御性快照,不暴露 live marker。
  • content capability 拥有一个 disjoint text span使用 character common-prefix/suffix 与 direct-property run diff外部字符编辑使其 stale。properties capability 不能携带 text只能通过 tp-object-attach-range 写 attached anchor。host edit 位于 anchor 前方时 marker 正常移动;跨入 owned range 时按 boundary policy shorten/remove/stale。
  • tp-surface-update-scoped 接受同 surface 的 live object handles 和一个完整 candidate。scope 只在当前 transaction 内有效TP 通过 object→mount index 得到授权范围,支持一个 logical object 的多个离散 mounts并在 prepare 阶段证明 candidate 没有改变范围外输出。默认 mismatch 发出 tp-scope-mismatch 且零发布;只有显式 (:on-mismatch root) 才允许 full-root fallback。后续普通 reactive recompute 或 tp-surface-update 不继承这次 scope。
  • properties ledger 保存 baseline、last-published value 与 contribution anchors。prepare compare-before-write外部 property override 触发 tp-property-conflict 且保持旧 revision。tp-range-rebase 显式接受当前 host runs 为新 baselineunmount 只恢复仍等于 TP last-published value 的子区间,冲突子区间保持外部值并进入 report。
  • 同一 surface 的 overlapping properties contributions 按 plan order 和 native property schema merge独立 surfaces 当前不得重叠字符 ownership。这个限制把 journal owner 保持为唯一 surface避免两套 baseline 静默覆盖。
  • 最外层 transaction 先完成所有 producer/plan/conflict validation再按 surface id publish随后执行 transaction participants最后提交 source values。multi-buffer change group 负责 text rollbackTP 的精确 property journal 覆盖 with-silent-modifications;失败先逆序撤销已经进入 publication 的 participants再恢复 buffer direct properties、markers/index、objects、plan、client state、revision、bindings 与 signal values。publish 中 killed buffer 不复活,其他 surfaces 与 sources 回滚。
  • tp-surface-report 的字段只使用 transaction/surface/source/binding/object/text/property/conflict/observer/timing 通用词汇。observer 在成功 commit 且 publishing transaction 的动态范围退出后执行,因此 observer 中的 signal write 会开启新 transactionobserver error 只写入 report不回滚。

15. 简单与响应式便利入口

  • tp-propertize STRING DECLARATIONS 接受 Emacs 原生 property plist把它转换为 canonical text/ declarations经 schema/cascade/projector 后应用到 STRING 的防御性副本;它不建立 object、binding、anchor 或 surface。
  • tp-apply BUFFER START END DECLARATIONS 使用同一 projection 与现有 range mutation primitive只修改声明过的 direct properties保留文本和未声明 property并返回 (START . END)
  • tp-watch BUFFER START END COMPUTE 创建一个 properties-only surface。COMPUTE 是返回原生 property plist 的零参数函数;其 signal/binding dependencies 由 exact graph 收集。返回值就是可传给 tp-surface-inspecttp-surface-reporttp-surface-unmount 的 opaque surface。首次 publication 失败会释放 convenience 层创建的 anchor。