tp/docs/API-SEMANTICS.md
Kinneyzhang 07b84b0685 feat(tp): add scoped retained surface updates
Allow callers to authorize one atomic surface update through generic retained object handles while TP remains the sole owner of mount resolution, buffer publication, and rollback.

Verified: byte compilation with warnings as errors; 726 ERT tests; 92 doctests; targeted scoped-update tests; checkdoc; git diff --check.
2026-08-06 06:32:58 +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。
  • 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。