tp/docs/API-SEMANTICS.md
Kinneyzhang 183f49ed3e feat(tp): replace scan reactivity with binding graph
Introduce exact signal and binding dependencies, transactional scheduling, rollback, cycle detection, scoped variable adapters, owner disposal, and scheduler counters. Keep the legacy scanner isolated until retained surfaces complete the cutover.\n\nVerified: 681 ERT tests, 92 doctests, shuffled ERT seed 20260806, and WERROR byte compilation.
2026-08-06 02:36:11 +08:00

16 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 排队稳定,不递归执行。
  • 任一 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。