458 lines
25 KiB
Markdown
458 lines
25 KiB
Markdown
# Emacs-KP 开发者文档
|
||
|
||
本文档描述 `emacs-kp` 的实际内部架构、算法与 API,面向贡献者和高级用户。
|
||
|
||
当前仓库健康度与后续工作的优先级见
|
||
[2026-07-28 系统审计](./Docs/REPOSITORY_AUDIT_20260728.md)。
|
||
|
||
## 1. 处理管线
|
||
|
||
一次排版调用经过五个阶段:
|
||
|
||
```
|
||
字符串
|
||
│
|
||
▼
|
||
① 分词 ekp-split-to-boxes (ekp-utils.el)
|
||
│ 拉丁词 / CJK 单字 / 空格串 → 盒子(box);
|
||
│ CJK 标点按避头尾规则附着
|
||
▼
|
||
② 断词 ekp--split-with-hyphen (ekp.el + ekp-hyphen.el)
|
||
│ 拉丁词盒子 → 音节盒子(Liang 模式)
|
||
▼
|
||
③ 测量与索引 ekp--make-para (ekp.el)
|
||
│ 像素宽度、glue 类型、前缀和数组
|
||
│ → 缓存为 `ekp-para` 结构
|
||
▼
|
||
④ 断行(DP) ekp--dp-run-1d / C 模块 (ekp.el / ekp_c/)
|
||
│ Knuth-Plass 动态规划 → 断点序列
|
||
▼
|
||
⑤ 渲染 ekp-line-glues, ekp--pixel-justify
|
||
分配 glue 像素、剥离行首尾空格盒、附加连字符
|
||
→ 以 "\n" 连接的行
|
||
```
|
||
|
||
`ekp-pixel-justify` 按 `"\n"` 拆分输入,每个非空段独立走这条管线
|
||
(C 模块可用时通过 batch API 并行处理)。
|
||
|
||
## 2. 数据结构
|
||
|
||
### `ekp-para`(段落缓存条目)
|
||
|
||
DP 和渲染需要的一切,每段只算一次:
|
||
|
||
| 字段 | 内容 |
|
||
|:-----|:-----|
|
||
| `string`, `latin-font`, `cjk-font` | 原文与检测到的字体 |
|
||
| `boxes` | 盒子字符串向量 |
|
||
| `boxes-widths` | 每个盒子的像素宽(带去重测量) |
|
||
| `boxes-types` | 每盒 `(首类型 . 尾类型)`:`latin`/`cjk`/`cjk-punct`/`space` |
|
||
| `glues-types` | 每个盒子*之前*的 glue 类别:`lws`/`mws`/`cws`/`nws` |
|
||
| `hyphen-pixel`, `hyphen-positions` | 连字符宽度;可断词盒索引的有序向量 |
|
||
| `ideal/min/max-prefixs` | 盒+glue 宽度在理想/最收/最伸状态下的前缀和(n+1 个元素) |
|
||
| `glue-ideals/shrinks/stretches` | 按盒索引的前导 glue 值(n 个)——原样传给 C |
|
||
| `lws/mws/cws-prefixs` | 各可伸缩 glue 类别的前缀**计数** → 每候选行 O(1) 数间隙 |
|
||
| `lead-spaces` | `lead-spaces[i]` = 从盒 i 开始的连续空格盒总宽;下标 0 强制为 0(首行缩进保留) |
|
||
| `trail-spaces` | `trail-spaces[k]` = 到盒 k−1 结束的连续空格盒总宽 |
|
||
| `glue-params` | 创建时九个间距值的 plist 快照 |
|
||
| `dp-cache` | 以 `equal` 比较的哈希:完整 DP 签名 → dp-result plist |
|
||
|
||
段落缓存(`ekp--para-cache`)以 `equal` 比较结构化 key——字符串内容、
|
||
文本属性区间的打印形式、检测字体、断词语言(`ekp-latin-lang`)、九个
|
||
显式间距值或自动 CJK stretch 默认值。其他自动值由字体测量派生,已
|
||
由字体与显示上下文字段表达。结构化 key 使哈希碰撞无害(旧的
|
||
`sxhash` 整数方案理论上可能串段)。超过 `ekp-para-cache-limit` 时
|
||
整体清空。单条快路径(`ekp--last-para`)只绕过哈希 lookup,复用前仍
|
||
比较同一个完整结构 key。因此排版相关 text property 的原地修改会让
|
||
两条路径都 miss,并得到与 fresh paragraph 相同的结果。
|
||
|
||
DP 签名独立于段落 key,包含行宽、looseness 与全部六个运行时代价参数。
|
||
参数变化会选择新结果,无需丢弃与宽度无关的段落数据;结构化 `equal`
|
||
比较也让非零 looseness 签名能够正常命中缓存。
|
||
|
||
### dp-result
|
||
|
||
`(:rests R :gaps G :breaks B :cost C :line-count N)`。`breaks` 为每行
|
||
的排他终点索引;`rests[i]` = 行宽 − 行理想宽(glue 需要吸收的像素);
|
||
`gaps[i]` = `(lws数 mws数 cws数)` 用于 glue 分配(单盒行和末行为 nil)。
|
||
|
||
## 3. 行度量
|
||
|
||
候选行覆盖盒子 `[i, k)` 时:
|
||
|
||
```
|
||
raw = prefix[k] − prefix[i] − 前导glue(i)
|
||
space-w = min(raw, lead-spaces[i] + trail-spaces[k])
|
||
width = raw − space-w (若盒 k−1 处断词,再加连字符宽)
|
||
```
|
||
|
||
理想/最小/最大三个值均 O(1) 得出。行边缘的空格盒串被排除,因为渲染层
|
||
会剥离它们;DP 与渲染层因此严格一致,每一行的渲染宽度精确等于目标宽
|
||
(测试 `ekp-test-justify-line-width-invariant`)。
|
||
`ekp--line-stripped-space-pixel` 统一拥有 1D/loose DP、C 结果重建与
|
||
渲染层使用的这一排除规则。
|
||
|
||
## 4. Knuth-Plass 动态规划
|
||
|
||
`ekp--dp-run-1d` 从左到右松弛位置。对每个可达起点 `i` 扫描终点 `k`,
|
||
直到行的最小宽度超过目标。断点合法条件:`min ≤ 目标 ≤ max`,或末行
|
||
`ideal ≤ 目标`。
|
||
|
||
**Demerits**(每行,与 `ekp_c/ekp_kp.c` 完全一致):
|
||
|
||
```
|
||
demerits = (line-penalty + badness)²
|
||
+ penalty² ; 断词处为 hyphen-penalty
|
||
+ adjacent-fitness-penalty ; 当 |fitness − 前行fitness| > 1
|
||
+ consecutive-hyphen-penalty × 连续次数²
|
||
badness = min(10000, 100·|adjustment/flexibility|³)
|
||
```
|
||
|
||
松紧等级(tight/decent/loose/very-loose)沿用 TeX 的比例阈值。特殊情
|
||
况:严格遍的单盒行 flexibility 固定为 1;最终遍使用与普通欠宽行相同
|
||
的有限 emergency stretch。末行代价为
|
||
`(line-penalty + 短行badness)²`,填充率低于 `ekp-last-line-min-ratio`
|
||
时 `短行badness = last-line-short-penalty × (1 − 填充率)`。
|
||
|
||
与 1981 论文的差异(有意为之):penalty 一律以 `+p²` 计入(无负
|
||
penalty/flagged 断点),主流程无 `q`/looseness(见 §6),相邻松紧惩
|
||
罚为平坦常数。
|
||
|
||
### 两遍紧急策略
|
||
|
||
某些输入不存在合法排版:比行宽更宽的不可断盒子,或无法伸展到目标宽
|
||
的刚性(全 `nws`)区段。先跑严格遍;若段尾不可达,第二遍额外允许
|
||
为普通欠宽候选加入有限 background emergency stretch,并继续走同一套
|
||
adjustment ratio、badness、fitness 和 demerits。这样最终遍的欠宽选择
|
||
仍由全局 K-P DP 比较,不会被塞进另一条固定代价路径。
|
||
|
||
另外,最终遍实现 TeX 的 `artificial_demerits` 可达性保护。如果某个
|
||
超宽候选将使一个断点的最后活动路径消失,且该断点没有任何未超宽候选
|
||
存活,就以 tight fitness 和零增量 demerits 安装最佳暂存路径。这不是
|
||
hard atom 专用的评分捷径:只要正常活动路径还在,它就不参与竞争,也不
|
||
检查中文、单位或 token 类别。因此任何输入仍会得到覆盖完整源文本的
|
||
plan(回归:窄栏 CJK 曾整段返回空结果),而普通欠宽行仍按正常 K-P 代价
|
||
竞争。Elisp 1D、looseness/parshape 与 C 引擎实现相同语义。
|
||
|
||
## 5. 渲染
|
||
|
||
`ekp-line-glues` 把每行的 `rest` 转成各 glue 的像素值:
|
||
|
||
- rest > 0 → 拉伸,按 拉丁 → 中西 → CJK 优先级分配;CJK 间隙可吸收超
|
||
出名义容量的剩余(紧急摊布)。
|
||
- rest < 0 → 收缩,同样的优先级,不低于各类收缩下限;glue 宽度钳制
|
||
≥ 0。
|
||
- 末行右侧不齐(理想 glue + 尾部填充);单盒行的尾部填充钳制 ≥ 0。
|
||
|
||
`ekp-layout-plan` 是布局决策与渲染方式之间的表示边界。它把段落盒子
|
||
及其原文 offset、DP 断点、逐行 glue 目标、缩进、边缘剥离和可选断词
|
||
决策组合成 `ekp-layout-plan`、`ekp-layout-line` 和 `ekp-layout-gap`
|
||
记录。plan 不包含 buffer 位置或具体显示机制。
|
||
`ekp-render-layout-string` 用它服务现有字符串 API;buffer 集成可复用
|
||
完全相同的决策,无需重新运行或重新解释 KP 算法。
|
||
|
||
缓存的 semantic plan 对缓存 owner 不可变。缓存命中会返回 consumer
|
||
自有副本,覆盖所有 plan 自有的可变 payload:源字符串、context 树、盒
|
||
向量与盒字符串、源 offset、line 记录、line glue、gap 记录和 line
|
||
signature。`ekp-layout-plan-para` 是有意例外:段落缓存所有权早于
|
||
semantic-plan cache,append planning 依赖稳定的段落身份。
|
||
layout context snapshot 与返回 plan context 使用同一个递归 copier,
|
||
覆盖 cons、vector 与 string,因此可变 policy 输入不会别名到 cache key
|
||
或后续 consumer plan。
|
||
|
||
两个消费方拥有不同且明确的表示权限。
|
||
|
||
#### 字符串渲染器
|
||
|
||
`ekp-render-layout-string` 保持公开字符串 API 兼容。它剥离首尾空格
|
||
盒、合成 display 空格与视觉换行,并在选中断词点附加继承样式的连字符。
|
||
返回字符串通过四类私有标记保持无损:
|
||
|
||
| 属性 | 位置 | 值 / 含义 |
|
||
|-------------------|------------------|---------------------------|
|
||
| `ekp-glue` | 合成的 glue 空格 | 它所替换的原文 |
|
||
| `ekp-soft-break` | 合成的 `\n` | 断点处吞掉的边界空白 |
|
||
| `ekp-soft-hyphen` | 合成的连字符 | 仅作标记 |
|
||
| `ekp-hidden` | 段落边缘文本 | 保留源文本、显示为空 |
|
||
|
||
这份物理表示只存在于返回字符串中,用于兼容既有 API,绝不会安装到源
|
||
buffer。`ekp--layout-marker-properties` 统一拥有其标记词汇表与不继承
|
||
契约。
|
||
|
||
#### Buffer 渲染器
|
||
|
||
`ekp-buffer.el` 保持 buffer 字符序列不变,只在现有源字符上用文本属性
|
||
投影同一份 plan:
|
||
|
||
- 源 ASCII 空格:
|
||
`((space-width FACTOR) (min-width ((TARGET-PIXELS))))`;
|
||
- 无源空格的 CJK/混排 glue:把 `min-width` 加在前一个完整字素上,
|
||
目标为其自然 advance 加 glue;
|
||
- 缩进:`line-prefix`;
|
||
- 源空白断点:第一个边界字符显示成换行,其余显示为空;
|
||
- CJK 或拉丁断词断点:replacing display string 重现已有完整字素,
|
||
接上可选连字符和视觉换行。
|
||
|
||
`ekp-buffer--display` 与 `ekp-buffer--line-prefix` 记录精确 owner。
|
||
移除时只有公开属性的值仍与 EKP owner 值相同时才清除,因此后来的外部
|
||
修改不会被误删。带外部 `display`、`line-prefix`、`wrap-prefix`、
|
||
`composition` 或 `invisible` owner 的段落保持 verbatim。若精确排版
|
||
要求缩窄 tab/非 ASCII 空白也会拒绝,因为 `space-width` 只影响 ASCII
|
||
空格。
|
||
|
||
所有安装/移除都在 `with-silent-modifications` 内完成,自有属性设为
|
||
nonsticky。因此 buffer 字符、point/mark、modified 状态、undo、字符
|
||
修改 tick 与外部 change hook 仍完全由源编辑拥有。任何 EKP buffer
|
||
路径都不会创建 overlay。重投影直接恢复 mark marker,不会调用会激活
|
||
选区的 `set-mark`,随后独立恢复 `mark-active`;因此布局不能把旧 mark
|
||
变成选区。
|
||
|
||
保存、普通搜索、语法和直接 Elisp 字符 API 不再需要逻辑文本 adapter:
|
||
真实 buffer 本来就是逻辑文本。复制过滤仍有必要,因为
|
||
`buffer-substring` 按设计保留文本属性。EKP 先组合原有
|
||
`filter-buffer-substring-function`,再只移除自己在复制字符串中的投影
|
||
元数据。
|
||
|
||
#### 实时流式排版
|
||
|
||
实时编辑直接消费正常的整段 Knuth-Plass plan,不会把编辑状态交给
|
||
核心规划器:
|
||
|
||
1. live state 按最窄窗口权威宽度持有最后一次提交的硬行源文本、正常
|
||
`ekp-layout-plan`、语义签名、投影 span 与稳定断行锚点。
|
||
2. 第一次真实变更打开一个编辑事务。事务先快照提交态,再只移除包含该
|
||
编辑的最小投影 span 范围中的 EKP 属性;span 对象与无关锚点仍保持
|
||
注册。
|
||
3. 脏岛内的后续变更交给原生软折行,不规划整条硬行。若逻辑源文本精确
|
||
回到快照,EKP 直接恢复保存的自有属性区间与 marker 偏移;state、plan、
|
||
signatures 和 spans 都保持对象身份。
|
||
4. 跨过原生视觉行边界才提交。EKP 计算或复用一次整条硬行 plan,得到
|
||
全部已完成语义行,并在同一个 command-loop 转换中静默发布真正变化的
|
||
后缀;新的当前行保持自然。
|
||
5. 其他提交事件只有硬换行/段落完成、下一次真实编辑发生在脏岛之外、
|
||
显式 refill,以及宽度/字体/face/主题或布局上下文变化。point 移动永远
|
||
不是提交,即使跨越硬段落也一样;live 路径没有 `post-command-hook`。
|
||
6. 稳定行签名只负责在提交时减少属性写入,buffer-local 16 项 LRU 只负责
|
||
复用近期文本/上下文 plan。何时允许布局变化由事务控制,不是二者控制。
|
||
7. IME preedit、外部显示所有权、不支持的收缩、超长硬行、过期
|
||
generation 或发布错误都会 fail closed 到原生显示。半成品投影会
|
||
回滚,原始错误继续向外报告。
|
||
|
||
原生软折行是状态机的前提,不能只希望用户碰巧开启它。mode 启用时先
|
||
保存 `truncate-lines` 和 `truncate-partial-width-windows` 的值与
|
||
buffer-local 所有权,再把二者设为 buffer-local `nil`;teardown 时恢复
|
||
原局部值,或移除临时局部绑定让全局值重新接管。这样 Emacs 默认的
|
||
50 列分栏阈值就不会悄悄把窄分栏变成横向滚动。
|
||
|
||
这个状态模型不需要 live lookahead、push/pull 收敛、逐键整行规划器或
|
||
idle formatter。point 移动本身严格零副作用;之后若在别处发生真实编辑,
|
||
则允许提交先前活动硬行,即使 narrowing 让该行已不在可访问区内。
|
||
|
||
不存在编辑空闲后整段 formatter。resize/后台工作都带 generation。
|
||
大 buffer 按可见优先的硬段落 chunk 处理;单个硬段落超过
|
||
`ekp-auto-justify-paragraph-limit` 时,自动路径保持自然显示,只有显式
|
||
`ekp-refill-paragraph` 才执行无界完整质量 pass。文本属性属于 buffer,
|
||
所以以最窄活动窗口作为唯一权威宽度。
|
||
|
||
### 5.1 断行许可、对齐、悬挂、段形
|
||
|
||
- **断行许可**:每个 CJK 字符(含标点)独立成盒;
|
||
`ekp-para-breaks-allowed` 按禁则(全角与半角)、`ekp-no-break`
|
||
区间及 NBSP 族连接符禁止相应间隙,被禁间隙不携带 glue。DP 跳过
|
||
被禁候选但继续延伸行。最终遍中的合法欠宽候选获得有限 emergency
|
||
stretch 并按正常 badness/demerits 评分;若超宽候选将消灭最后活动
|
||
路径,TeX 风格 artificial demerits 以零增量代价保留该路径。C 侧接收
|
||
稀疏 `forbidden-positions` 向量。显式 hard atom 只禁止其区间内部
|
||
断行;atom 前后本来合法的边界仍然合法。atom 邻接不获得特殊评分,
|
||
也不会额外禁止断点。
|
||
- **可配置 policy 编译**:buffer 与 core policy 变量在 tokenization
|
||
之前解析为私有结构区间。区域 `ekp-break-policy` 优先,显式
|
||
buffer/file/dir local 值优先于 mode profile,profile 优先于全局默认。
|
||
只有 token policy 是按类别合并的 map;标量与 face 列表都直接替换
|
||
低优先级值。core 可以在 analysis copy 上使用
|
||
`ekp--face-break-policy`、`ekp--no-hyphen`、`ekp--literal-spacing`
|
||
等私有属性,但 cache key、plan string、box、rendered string 与
|
||
`ekp--last-para` 只能保留公开源属性。自动 face no-break 按连续的
|
||
私有 face-policy 区间整体测宽,超宽时降级为 no-hyphen;显式
|
||
`ekp-no-break` 永不降级。
|
||
- **行内字面空格**:face 派生的 `no-hyphen` 在行内保留源空格为字面
|
||
box,禁止会把源空格 box 移到行首的断点,并允许互补的"空格后到内容"
|
||
断点。当这些空白成为选中的视觉断点时,源归属属于 break gap metadata,
|
||
而不是尾随可见行 box。该规则来自 policy,不能依赖源上是否另有公开
|
||
`face` 属性。
|
||
- **对齐**(`ekp-alignment`):非两端对齐把 glue 伸缩数组与类参数
|
||
置零,DP 给 `max_w` 加每行额外伸展 R(`ekp-c-set-penalties` 第 7
|
||
参),badness = 100·(欠宽/R)³;渲染层按模式分派剩余(尾部/对半/
|
||
头部)。
|
||
- **悬挂**(`ekp-protrusion`):逐间隙 `tail-protrudes[k]`(穿透尾
|
||
随空格盒取最后内容盒)加 `hyphen-protrude` 标量,在 DP、
|
||
`ekp-line-glues`、C 结果重建三处同步放宽每个候选的有效目标宽
|
||
(`lw = width + release`)——三处必须保持一致。
|
||
- **每行宽度**(`ekp-parshape` / `ekp-first-line-indent`):由
|
||
`ekp--line-spec`(行号 → 缩进 . 宽度)解析。纯首行缩进只改第 0 行,
|
||
而"以盒 0 开头的行"恰好对应 DP 起点 i = 0,故 1D 遍(以及 C 引擎,
|
||
经 `FIRST-LINE-WIDTH` 参数)无需额外状态即可处理;只有完整的
|
||
`ekp-parshape` 和 `ekp-looseness` 才需要(位置×行数)DP 并旁路 C。
|
||
缩进渲染为行首 `ekp-glue` 垫片。
|
||
|
||
C 模块 1.6:`ekp-c-break-with-arrays` 15 参(…、forbidden-positions、
|
||
tail-protrudes、hyphen-protrude、first-line-width);batch 向量 15 元;
|
||
`ekp-c-set-penalties` 4–8 参。policy 编译只进入现有断词位置与禁断
|
||
向量;没有新的架构决策时,不得增加第 16 个 C 参数或 batch 字段。
|
||
|
||
特性完成后的性能(字节编译 + C,Apple Silicon,batch):justify zh
|
||
w=200 ≈ 54 ms、range zh ≈ 117 ms——justify 与特性前持平,range 因盒
|
||
数增加约 +55%。热路径缓存:`ekp--str-type` 按字符记忆化、glue 字符
|
||
串驻留、(段落, 宽度) 渲染结果缓存进 dp-cache(上限 64 个宽度)。连
|
||
续变宽实测(60 段 2.6 万字文章,含 region 层全链路):每次变宽约
|
||
73 ms,重访宽度更快;编辑后单段增量重排约 17 ms。
|
||
|
||
`ekp-bench-adversarial-builders` 单独测量源码解释模式下的退化
|
||
builder。输入从 1,000 增到 8,000 字符时,基于片段的 tokenizer 增长
|
||
6.3×,密集插入增长 7.7×,接近输入 8× 的线性增长。8,000 字符分别从
|
||
3.133 s 降到 1.100 s、从 0.945 s 降到 0.013 s。断词位置缓存使用
|
||
显式 miss sentinel,因此合法的 nil 结果也能复用。
|
||
|
||
## 6. Looseness
|
||
|
||
`ekp-looseness` ≠ 0 时切换到 `ekp--dp-run-loose`:完整的
|
||
(位置 × 行数)DP,为每个行数保留最优路径,最终选取与
|
||
(最优行数 + looseness)最接近的行数,平局取 demerits 更小者。该路径
|
||
比 1D 重,仅有 Elisp 实现;looseness 激活期间 `ekp--c-available-p`
|
||
返回 nil,两引擎永不分歧。
|
||
|
||
## 7. C 模块集成
|
||
|
||
C 模块(`ekp_c/`,版本 1.6)只执行阶段 ④。所有字体相关数据以 Elisp
|
||
为唯一事实来源。
|
||
|
||
- `ekp-c-break-with-arrays`(15 参数):para 的前缀数组、glue 数组、
|
||
断词数据、行宽、两个空格串数组、禁则/悬挂数组和首行宽度。返回
|
||
`(breaks . cost)`。
|
||
- `ekp-c-break-batch`:15 元素向量的向量,由 pthread 线程池并行处理
|
||
——每段一个任务(这是正确的并行粒度;DP 本身天然串行)。线程池在
|
||
首次多段落 batch 时惰性创建,按机器核心数定大小;队列满时提交方
|
||
阻塞等待而非丢弃任务。
|
||
- `ekp-c-set-penalties`(4–8 参数):`ekp--c-sync-params` 在**每次**
|
||
进入 C 之前调用,保证 `ekp-line-penalty` 等变量始终生效(回归:此
|
||
前从未同步)。
|
||
- `ekp-c-module-load` 拒绝低于 `ekp-c-module-required-version` 的模块
|
||
并回落到 Elisp,避免升级后的参数数量不匹配。
|
||
|
||
模块不可用、ABI 版本不兼容、整批 C 结果为 nil 或单项/断点结果为
|
||
nil 时回落到 Elisp。直接 API 的非法输入 signal
|
||
`ekp-c-invalid-input`。任何非 nil 的畸形后端结果 signal
|
||
`ekp-backend-contract-error`:结果 cons 形状错误、breaks 不是 list、
|
||
break 非整数/越界/非递增/未覆盖段尾、cost 非数字,或 batch 结果形状
|
||
错误。已启用后端发出的任何 signal 都会穿过公共 formatter,dispatcher
|
||
不捕获或隐藏。模块不会在部分失败时静默产出不同的排版。两引擎输出
|
||
逐字节一致,由
|
||
`ekp-test-c-parity-simple` / `ekp-test-c-parity-files` 及 300 例性质
|
||
fuzz 验证。
|
||
|
||
### 未来方向:段落句柄 API
|
||
|
||
每次 `ekp-c-break-with-arrays` 调用都会重新编组段落的宽度无关数组
|
||
(约 18·n 次 `env` 提取)。单次 justify 时这无关紧要,但
|
||
`ekp-pixel-range-justify` 会对每个候选宽度重新编组同一批数组:即便
|
||
段落缓存已暖,C 路径每个宽度仍约 12 ms,其中大部分是编组而非 DP
|
||
(整篇样本的 DP 约 2.5 ms)。
|
||
|
||
解法是 `make_user_ptr` 句柄:`ekp-c-para-upload` 把数组一次性拷进 C
|
||
结构体并返回带 GC finalizer 的句柄,`ekp-c-break (handle, width)` 之后
|
||
只传两个宽度相关标量。它**有意**不纳入本次发布——这是引入 C 端对象
|
||
生命周期的破坏性(2.0)ABI 变更,而常见交互路径(单次 justify、
|
||
`ekp-auto-justify-mode`)本就命中 dp-cache、避开了重复编组。当宽度
|
||
搜索或超大批处理成为瓶颈时,这是明确的下一步。
|
||
|
||
## 8. 断词(ekp-hyphen.el)
|
||
|
||
普通 Liang 模式算法:
|
||
|
||
- `dictionaries/hyph_*.dic` 首次使用时编译为模式哈希并按路径缓存。
|
||
文件可为 UTF-8 或 ISO-8859(Emacs 自动检测;由
|
||
`ekp-test-hyphen-de-iso8859-dict` 验证)。
|
||
- `ekp-hyphen-create LANG` 先精确匹配,再逐级缩短(`"de_CH" → "de"`)。
|
||
- 断点两侧默认至少保留 2 个字符。
|
||
- 非注释 pattern 出现斜杠时失败关闭。libhyphen 替换规则只在断点
|
||
胜出时改变可见文字与宽度,当前固定宽度 box 无法诚实表达;编译器
|
||
计数后 signal `ekp-hyphen-unsupported-pattern`,公共排版入口保留该
|
||
错误。
|
||
- `dictionaries/MANIFEST.tsv` 将 49 个条目固定到 LibreOffice 提交
|
||
(另明确标记一个 legacy Basque 字节),记录 SHA-256、语法标记和许可
|
||
证据。`tests/check-dictionaries.sh` 做离线门禁,
|
||
`dictionaries/update.sh check` 在 macOS/Linux 对照固定上游字节。
|
||
|
||
词盒按 `^[左标点]* (拉丁词) [右标点]*$` 匹配,因此被标点包裹的词
|
||
(`(word)`、`word!`、`»word«`)仍可断词;标点粘在首/末音节盒上。
|
||
|
||
## 9. 测试与基准
|
||
|
||
```bash
|
||
tests/run-tests.sh [emacs] # batch 可跑的 ERT 测试集
|
||
tests/run-tests.sh [emacs] --random-order
|
||
tests/run-tests-isolated.sh [emacs] # 每个 ERT 使用全新进程
|
||
tests/check-dictionaries.sh # 离线清单/校验值门禁
|
||
dictionaries/update.sh check # 核对固定上游字节
|
||
make -C ekp_c PROFILE=portable # 默认可移植发布构建
|
||
make -C ekp_c PROFILE=native # 仅本机基准
|
||
make -C ekp_c PROFILE=debug # 调试符号,不优化
|
||
make -C ekp_c PROFILE=sanitize # ASan + UBSan
|
||
emacs -Q --batch -L . --eval '(setq ekp-use-c-module nil)' -l tests/ekp-bench.el
|
||
emacs -Q --batch -L . --eval '(progn (require (quote ekp)) (ekp-c-module-load))' \
|
||
-l tests/ekp-bench.el
|
||
```
|
||
|
||
可用 `EKP_TEST_SEED` 复现或改变乱序。测试 fixture 会动态恢复其隔离的
|
||
全部 EKP 配置;分派类测试必须经过公开排版入口,不能只断言内部资格
|
||
谓词。
|
||
|
||
GUI 矩阵需显式加载 `tests/ekp-gui-verify.el`。任一行失败时,它先打印
|
||
完整表格,再以状态码 1 退出;ERT 套件包含该边界的强制失败负控。
|
||
|
||
`M-x ekp-c-module-build` 使用同一组四种 profile,并在 `ekp_c/` 中以
|
||
argv 直接启动 make,不再构造 shell `cd` 命令。发布/CI 使用
|
||
`portable`;`native` 仅用于将在同一机器运行的基准。
|
||
|
||
核心被测不变式:渲染行宽 == 目标宽(像素级对齐)、任意宽度下不丢内
|
||
容、O(1) 前缀机制与暴力算法交叉验证、内置文本上的 Elisp/C 一致性、
|
||
参数持久化/同步回归。
|
||
|
||
基准结果(batch Emacs 30.2、Apple Silicon、`tests/text-zh.txt` ≈
|
||
3.6KB 中文及各示例;3 次冷缓存取最小值)——"改造前"为重写前的实现
|
||
(解释执行):
|
||
|
||
| 场景 | 改造前 (Elisp) | 改造后 (Elisp 解释) | 改造后 (Elisp 编译) | 改造后 (C) |
|
||
|:-----------------------|---------------:|--------------------:|--------------------:|-----------:|
|
||
| justify 中文 w=200 | 7547 ms | 1780 ms | 96 ms | 57 ms |
|
||
| justify 中文 w=400 | 2928 ms | 815 ms | 71 ms | 57 ms |
|
||
| justify 混排 w=300 | 5540 ms | 1275 ms | 53 ms | 23 ms |
|
||
| range 中文 340–380 | 29696 ms | 8937 ms | 294 ms | 75 ms |
|
||
| range 混排 280–320 | 68534 ms | 14552 ms | 480 ms | 34 ms |
|
||
| 仅 DP,中文 w=400 | 2382 ms | 591 ms | 15 ms | 1.3 ms |
|
||
|
||
("改造后 (C)" 列在字节编译的 Elisp 环境下测得。作为参照,重写前的
|
||
C 模块在 justify-中文-200 / range-中文 / 仅-DP 上分别为 197 ms /
|
||
430 ms / 25 ms——重写通过 para 级预建 glue 数组、para 缓存的 `eq'
|
||
快路径和 O(1) 重建 rest/gap,把 C 路径也提速了 3–19 倍。)
|
||
|
||
主要收益来源:前缀数组带来的 O(1) 行度量(旧内层每候选分配 O(n) 子
|
||
序列,总计 O(n³))、两遍紧急策略(保持 DP 稀疏)、盒宽测量去重。
|
||
|
||
## 10. 文件地图
|
||
|
||
```
|
||
ekp.el 核心:para 结构、缓存、DP(1D + looseness)、
|
||
glue 分配、渲染、公共 API
|
||
ekp-utils.el 分词器(盒子、避头尾)、带 batch/tty 回退的字体
|
||
检测、C 模块加载
|
||
ekp-hyphen.el Liang 断词 + 词典注册
|
||
ekp-buffer.el 纯文本属性 buffer/region 投影、同步实时流动、窗口
|
||
lifecycle、复制过滤与诊断
|
||
ekp_c/ C 动态模块(见 ekp_c/README.md)
|
||
dictionaries/ Hunspell 断词模式(来自 LibreOffice)
|
||
tests/ ekp-tests.el、ekp-buffer-tests.el(ERT)、
|
||
ekp-fuzz.el(一致性 fuzz)、ekp-bench.el、
|
||
ekp-demo.el、ekp-showcase.el、示例文本、run-tests.sh
|
||
```
|