diff --git a/.agents/docs/2026-09-18-kal-term-control-design.md b/.agents/docs/2026-09-18-kal-term-control-design.md new file mode 100644 index 0000000..573a30b --- /dev/null +++ b/.agents/docs/2026-09-18-kal-term-control-design.md @@ -0,0 +1,158 @@ +# openkal.terminal 增加 KAL_TERM_CONTROL 位:设计与跨仓库实施计划 + +- 日期:2026-09-18 +- 状态:设计稿,待 review(本文档不含实现,实现按 §5 任务图在各仓库另行提 PR) +- 起因:消费者报告 [mcpplibs/openkal-musl#36](https://github.com/mcpplibs/openkal-musl/issues/36)——`tcsetattr` 被端口拒绝(ENOTTY),程序无法进入 raw mode,也无法阻止 Ctrl+C 杀死进程 +- 涉及仓库:`mcpplibs/openkal`(规范),`mcpplibs/mcpp-index`(发布),`openkal-linux`、`openkal-windows`、`openkal-macos`(实现),`mcpplibs/openkal-musl`(C 库端口) +- 事实来源(规范文档地址,链接锚定 main 当前头 `86eb855`): + - 规范正文:[SPEC.md](https://github.com/mcpplibs/openkal/blob/86eb855/SPEC.md)——条款 6.2(未分配位读 0)、6.4(接口分解)、7.1(Naturalness)、7.11(问询的逆 / "a claim is a claim about what can be done") + - 声明头:[include/openkal/terminal.h](https://github.com/mcpplibs/openkal/blob/86eb855/include/openkal/terminal.h)(模式字 KAL_TERM_LINE_EDIT/ECHO,props 字 KAL_TERM_PROP_MODE/SIZE) + - 先例:SPEC 历史条目 7(文件锁——"What was missing was a word, not a capability")、7.11(`kal_fs_set_modified` 的准入判据——"three ordinary programs could not be written above the interface without it") + - 本地方程式测量:issue #36 的两组对照程序(musl 构建 vs glibc 构建,同一 tmux server) + +--- + +## 0. 一句话 + +**模式字加第三个位 `KAL_TERM_CONTROL`(环境是否消费控制键),配套 props 位 `KAL_TERM_PROP_CONTROL`;三平台本机开关一一对应(ISIG / ENABLE_PROCESSED_INPUT),缺的只是词汇不是能力;openkal 先发 0.14,index 注册后四个下游仓库按依赖序适配。** + +--- + +## 1. 问题与消费者需求 + +### 1.1 需求(与信号模型无关) + +全屏/逐键程序的真实需求只有一句:**"我读的每个字节就是用户按的每个键"**,包括 0x03。分三种场景: + +| 场景 | LINE_EDIT | ECHO | CONTROL | 例子 | +| --- | --- | --- | --- | --- | +| 全屏程序 | 0 | 0 | **0**(控制键也是数据) | vim、less、游戏 | +| 自带行编辑的程序 | 0 | 0 | **1**(^C 仍"打断") | bash/readline:关 ICANON 但保留 ISIG | +| 普通程序 | 1 | 1 | 1(默认 cooked) | 一切照旧 | + +第二种场景是硬约束:**它否决了"把中断语义折进 LINE_EDIT=0"的偷懒方案**,所以 CONTROL 必须是独立的正交位。 + +### 1.2 三平台本机语义 + +| 抽象 | Linux (termios) | macOS (termios) | Windows console | +| --- | --- | --- | --- | +| 行组装 | `ICANON` | `ICANON`(同 Linux,POSIX) | `ENABLE_LINE_INPUT` | +| 回显 | `ECHO` | `ECHO` | `ENABLE_ECHO_INPUT` | +| **环境消费控制键** | `ISIG`(VINTR/^C 等) | `ISIG` | `ENABLE_PROCESSED_INPUT`(^C→CTRL_C_EVENT,另含 ^S/^Q) | + +两个观察:macOS 与 Linux 完全同构(不是三种语义而是两种);Windows 恰好存在一一对应的开关。三个目标环境都有现成、几乎拼写一致的承载机制——这正是 SPEC 历史条目 7(文件锁)的准入形状:"every environment this specification targets locks a byte range, and spells it almost identically. What was missing was a word, not a capability." + +--- + +## 2. 设计方案 + +### 2.1 声明(将加入 `include/openkal/terminal.h`) + +```c +#define KAL_TERM_CONTROL ((kal_uintptr)1u << 2) /* 模式字位 2 */ +#define KAL_TERM_PROP_CONTROL ((kal_uintptr)1u << 2) /* props 字位 2 */ +``` + +语义: + +- **CONTROL=1(默认)**:环境可保留约定控制键用于自身动作。Linux/macOS 映射为 `ISIG`,Windows 映射为 `ENABLE_PROCESSED_INPUT`。openkal 不承诺"信号"机制(SPEC.md:284-304 明确排除),只承诺"环境会行动";在 musl 端口下对程序的可见效果就是默认处置(终止),与端口"无 handler"的既定立场自洽。 +- **CONTROL=0**:**保证**每个键以字节到达,包括 0x03。这是全屏程序在 openkal 上存活于 ^C 的唯一途径(openkal 装不了 signal handler)。 +- `kal_terminal_get_mode/set_mode` 做本机映射;未分配的位仍按条款 6.2 读 0、set 忽略——**但本位置是例外,见 §2.2**。 + +### 2.2 条款 6.2 的字面例外(本设计唯一需要规范性文字的地方) + +条款 6.2 承诺"未分配位读 0,因此对旧实现的向后兼容成立"。对绝大多数位成立(0 = 无此能力),但 CONTROL 位**不成立**:已发布的 openkal-linux 0.12/0.13 不认此位(get_mode 读 0 = "环境不行动"),而其实际行为是保留 ISIG(等效 CONTROL=1)。新规范程序对旧实现 set CONTROL=0 会被静默忽略,程序仍被 ^C 杀死——正是 6.2 承诺要防的事故形状。 + +规范的两条落笔: + +1. props 字加 `KAL_TERM_PROP_CONTROL`:实现是否区分该模式位。 +2. 规范性例外(写入条款 6.2 的相应处):**程序若依赖"键入皆数据"的保证,须先查 `kal_terminal_props`,不能只信 get_mode 的读 0**——因为恰好对这个位,未分配不等于行为等于 0。引用条款 7.11 的既有原则收口:"An implementation that claims the position is required to perform the operation: clause 6.2 exists so that a claim is a claim about what can be done." + +### 2.3 命名 + +模式位命名 `KAL_TERM_CONTROL`,不叫 INTERRUPT/SIGNAL:条款 7.5 已占用 "Interruption"(实现须重试被打断的操作且不得上报),含义完全不同;且 openkal 无信号模型,位名描述**环境行为**("消费控制键"),与 LINE_EDIT/ECHO 的命名风格一致。 + +### 2.4 逐条符合性核对(对照 SPEC) + +| 条款 | 结论 | +| --- | --- | +| 7.1 Naturalness | 符合。三实现直接映射本机开关,无需翻译表/注册层 | +| 6.4 接口分解 | 符合。语义只在交互流上存在,落在既有 `openkal.terminal`,不开新接口 | +| 7.11 问询的逆 / 准入判据 | 符合。全屏编辑器是"C 库之上预期托管的程序",无此位无法正确写出——正是 `kal_fs_set_modified` 的准入形状 | +| 6.2 版本化 | 基本符合,配 §2.2 的 props 位 + 规范性例外 | +| 3.2 核心集不扩张 | 不冲突。`openkal.terminal` 是可选层接口,加位属条款 8 的标准演化 | + +--- + +## 3. 对使用侧的效果(零改动) + +- musl 的 `cfmakeraw` 本来就清 ISIG。openkal-musl 把 ioctl 分支接到 `kal_terminal_*` 后,`tcgetattr` 读 CONTROL→ISIG、`tcsetattr` 写 ISIG→CONTROL,**现有全屏程序不改动即正确**。 +- readline 类程序"关 ICANON、留 ISIG"的写法映射为 CONTROL=1,语义不变。 +- 依赖链上唯一要改变行为的是 openkal-linux 的 `set_mode`:目前清 LINE_EDIT 时保留其余 lflag(read-modify-write),产生 issue #36 指出的"半 raw 被杀"状态。有了 CONTROL 位后按模式字逐位置写——这本来就是 get/set 对称性(terminal.h:16-20)的要求。 + +--- + +## 4. 非目标(out of scope) + +- **不改写 openkal 的"无信号"模型**(SPEC 排除信号接口是既定设计,KAL_TERM_CONTROL 不引入任何信号形状)。 +- **不在本设计中修 openkal-musl 的 SIG_IGN 静默 no-op**(okm_syscall.c 接受 SIG_IGN 却不安装)。它与本设计无关,是独立的端口 bug,遵循规范"接受⇒生效,否则拒绝"原则,应作为 openkal-musl 侧独立修复(可同批 PR 也可单独)。 +- **不要求 openkal-musl 支持 signal handler**(ENOSYS 是端口对"无内存保护"立场的既定实现)。 + +--- + +## 5. 任务分解与依赖关系 + +``` + ┌─────────────┐ + │ 1. openkal │ 规范 PR(terminal.h 两个宏 + 条款 6.2 例外 + │ spec 0.14 │ + SPEC 历史条目 + mcpp.toml/README 版本) + └──────┬───────┘ + │ 合并 + 打 tag 0.14.0 + ▼ + ┌─────────────┐ + │ 2. mcpp-index│ 注册 openkal 0.14.0 描述符 + └──────┬───────┘ + ┌───────────┼───────────────┐ + ▼ ▼ ▼ + ┌────────────┐ ┌────────────┐ ┌────────────┐ + │3a.openkal- │ │3b.openkal- │ │3c.openkal- │ 三实现并行,互不依赖; + │ linux │ │ windows │ │ macos │ 各自 bump 依赖 openkal=0.14.0 + └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ + └──────────────┼──────────────┘ + ▼ + ┌─────────────┐ + │4. openkal-musl│ bump 依赖 openkal-linux 0.14.x; + │ │ ioctl 分支路由到 kal_terminal_*, + │ │ ISIG↔CONTROL 映射;port 自测(pty 探针) + └─────────────┘ +``` + +**为什么是这个序:** + +1. **openkal 先行(唯一硬序)。** 下游四仓库的适配代码都要 `#include ` 里的新宏;mcpp 依赖解析也要求实现包的 `openkal = "0.14.0"` 依赖先存在于 index。规范 PR 合并且 tag 后才能动下游。 +2. **mcpp-index 第二。** 实现包 bump 依赖到 openkal 0.14.0 后,其 CI/构建需要 index 里已注册该版本描述符,否则依赖解析失败。 +3. **三个实现并行(无相互依赖)。** linux 与 macos 都是 termios(改动几乎同形),windows 是 console mode;三者只共同依赖规范,彼此独立,PR 可同时开。 +4. **openkal-musl 最后。** 它依赖 `openkal-linux`(及其 macos/windows 对应物)的新实现来跑端口级验证;且它的改动最大(ioctl 分发器重写 + 链接接线),放在实现稳定之后风险最低。 + +### 5.1 任务卡 + +| # | 仓库 | 内容 | 验收 | +| --- | --- | --- | --- | +| 1 | openkal | terminal.h 加 `KAL_TERM_CONTROL`/`KAL_TERM_PROP_CONTROL`(含房屋风格理由注释);src/terminal.cppm、src/macros.cppm 镜像;SPEC.md:版本 0.14、条款 6.2 规范性例外、历史条目;mcpp.toml/README 0.14.0;conformance terminal 段加观察(props 宣布则 roundtrip 该位并恢复) | tools/check-*.sh 全绿;CI 四声明矩阵绿 | +| 2 | mcpp-index | 注册 openkal 0.14.0 描述符(仿 register-openkal-0.15.0 分支既有流程) | index CI 绿 | +| 3a | openkal-linux | `kal_terminal_get_mode` 读 ISIG→CONTROL;`set_mode` 按模式字逐位置写 ISIG(不再"保留其余");`kal_terminal_props` 宣布 KAL_TERM_PROP_CONTROL | 端口自测 + openkal conformance 通过 | +| 3b | openkal-windows | get/set `ENABLE_PROCESSED_INPUT`;props 宣布 | 同 3a(Windows leg) | +| 3c | openkal-macos | 同 3a(termios) | 同 3a(macOS leg) | +| 4 | openkal-musl | okm_syscall.c ioctl 分支:TCGETS/TIOCGWINSZ 把真实 termios/winsize 拷回调用者(不再"成功但不写");TCSETS 族路由 `kal_terminal_set_mode`;tcgetattr 读回 ISIG↔CONTROL 映射使 `cfmakeraw` 免费正确;bump 依赖 openkal-linux 0.14.x | issue #36 的两支探针程序在 pty 下行为与 glibc 对照一致 | + +### 5.2 版本链 + +`openkal 0.14.0` → index 注册 → `openkal-linux/windows/macos` 各自 minor bump(依赖 `openkal = "0.14.0"`)→ index 注册 → `openkal-musl` minor bump(依赖新实现)。KAL_VERSION_MINOR 不动(仓库惯例:0.12/0.13 发布均未 bump version.h)。 + +--- + +## 6. 风险与开放问题 + +- **R1:已发布实现的语义债。** openkal-linux 0.12/0.13 的旧行为(清 LINE_EDIT 保留 ISIG)在新规范下应视为 bug:它的 `set_mode` 必须先升级才能诚实地声称 KAL_TERM_PROP_CONTROL。3a 的 PR 即还债。 +- **R2:conformance 的可见性。** "^C 以字节到达"这一核心保证需要 pty + 按键注入,CI 终端环境下难以自动观察;折衷是只观察 props 宣布 + 位 roundtrip,端到端保证留给 issue #36 的探针程序在 openkal-musl 侧人工/脚本验证(已列入 4 的验收)。 +- **O1(可选,留给规范作者)**:是否在历史条目里同时记录"SIG_IGN 静默 no-op"作为 openkal-musl 侧独立修复的锚点——本文 §4 将其排除在设计外,但两 PR 同批走可减少消费者等待。 diff --git a/.agents/docs/2026-09-20-kal-term-pass-control-design.md b/.agents/docs/2026-09-20-kal-term-pass-control-design.md new file mode 100644 index 0000000..094e4f3 --- /dev/null +++ b/.agents/docs/2026-09-20-kal-term-pass-control-design.md @@ -0,0 +1,293 @@ +# openkal.terminal 的第三个模式位:设计与跨仓库实施计划(v2) + +- 日期:2026-09-20 +- 状态:设计稿,待 review(不含实现;实现按 §6 任务图在各仓库另行提 PR) +- 关系:取代 `.agents/docs/2026-09-18-kal-term-control-design.md`(PR #37)的方案部分,保留其问题分析与依赖序;差异集中在 §2(位的极性)、§3(不动条款 6.2、不加 props 位)、§4(保证的范围与读的零)、§5(发现机制修复) +- 起因:消费者报告 [mcpplibs/openkal-musl#36](https://github.com/mcpplibs/openkal-musl/issues/36)——程序无法让"每个按键以字节到达",因而被 `^C` 杀死 +- 涉及仓库:`openkal`(规范)、`mcpp-index`(发布)、`openkal-linux`/`-macos`/`-windows`/`-emscripten`(**四个**实现,均宣告 `KAL_IFACE_TERMINAL`)、`openkal-musl`(C 环境之一,消费者侧适配) +- 事实来源(均已本地核对,锚定 main @ `86eb855`): + - SPEC.md 条款 6.1/6.2/6.4/7.1/7.4/7.11、§3.2、§11 历史条目 10(文件锁)与 8(fork) + - `include/openkal/terminal.h`(模式字两位、get/set 是逆)、`include/openkal/version.h:30-36`、`include/openkal/stream.h:26-28` + - `conformance/src/sections/terminal.cpp`(已有往返观察)、`tools/gen-macros.sh`、`tools/check-readme-versions.sh`、`SURFACE.txt` + - 四个实现的 `src/terminal.cpp`;`openkal-linux/src/version.cpp:16`(`kal_version` 返回 `KAL_VERSION`) + +--- + +## 0. 一句话 + +**模式字加一个位,极性取"放行"而不是"消费"——`KAL_TERM_PASS_CONTROL`:1 = 环境不为自己保留任何按键,每个按键以字节到达;0 = 环境可保留一组由自己定义的按键。** 极性这样取,条款 6.2 的"未分配读 0"对旧实现**是真话**,于是不需要规范例外、不需要强制 props 位、不产生语义债;发现机制用规范已有的 get/set 往返。附带两项与位同源的收尾:把"读到 0 即输入结束"(7.4)在关掉行组装时的后果写明,以及修好整个生态里已经失灵的版本发现机制(`KAL_VERSION_MINOR` 停在 11)。 + +--- + +## 1. 需求(与任何一种 C 环境无关) + +逐键程序的需求只有一句:**我读到的每个字节,就是用户按下的每个键**。三种场景: + +| 场景 | LINE_EDIT | ECHO | PASS_CONTROL | 例子 | +| --- | --- | --- | --- | --- | +| 全屏程序 | 0 | 0 | **1**(连 `^C` 也是数据) | 编辑器、分页器、游戏、TUI | +| 自带行编辑的程序 | 0 | 0 | **0**(`^C` 仍由环境处置) | shell 的行编辑、REPL | +| 普通程序 | 1 | 1 | 0(终端的常态) | 一切照旧 | + +第二行是硬约束:它否决了"把中断语义折进 `LINE_EDIT=0`"的省事方案,所以这必须是**正交的第三个位**——与 terminal.h 里 `LINE_EDIT` 不含 `ECHO` 的既有理由(密码提示符"关一个留一个")同构。 + +**这不是一个 C 库的需求,更不是 musl 的需求。** 需要它的是"一个程序 + 一个交互流",与程序用什么语言写、上面盖没盖 C 库无关:`openkal-musl` 会用它兑现 `cfmakeraw`,`std-freestanding` 的 TUI 程序直接用模块形式 `kal::terminal::pass_control`,一个不带 C 库的裸程序也一样。issue #36 是**证据**,不是设计的驱动方。 + +### 1.1 为什么不能在接口之上组合出来(必要性) + +- openkal 不定义信号接口,程序装不了 handler; +- 清掉 `LINE_EDIT` 不附带任何中断语义——四个实现的 `set_mode` 都只动自己命名的两个位并保留其余(linux `src/terminal.cpp:52-80` 的 read-modify-write 注释写明了为什么保留),这是**正确行为**,不是缺陷; +- 于是"每个按键都是数据"在当前规范下**无法表达**。缺的是一个词,不是一种能力:每个目标环境都有现成机制,拼写几乎一致——这正是 §11 历史条目 10(文件锁)记下的准入形状。 +- 条款 7.11 的准入判据("三个普通程序在接口之上写不出来")满足:编辑器、分页器、自带行编辑的 REPL。 + +--- + +## 2. 方案:一个位,极性取"放行" + +### 2.1 声明(`include/openkal/terminal.h`) + +```c +/* Positions in the mode word. + * + * A position that has not been assigned reads as zero, so a program compiled + * against a later revision of this specification behaves correctly against an + * earlier implementation (clause 6.2). A POSITION IS THEREFORE GIVEN THE SENSE + * IN WHICH ZERO IS THE WEAKER CLAIM: an implementation that has never heard of + * a position must not be made to state the stronger one by its silence. */ +#define KAL_TERM_LINE_EDIT ((kal_uintptr)1u << 0) /* the environment assembles lines */ +#define KAL_TERM_ECHO ((kal_uintptr)1u << 1) /* the environment shows what is typed */ +#define KAL_TERM_PASS_CONTROL ((kal_uintptr)1u << 2) /* the environment reserves no keystroke */ +``` + +语义,写在位的旁边(规范性文字在声明里,SPEC.md 从不重述模式位): + +- **`PASS_CONTROL = 1` 是一个保证**:环境不为自己保留任何按键,用户按下的每个键都以它产生的字节到达程序——包括 `0x03`。 +- **`PASS_CONTROL = 0` 是一个许可**:环境**可以**保留一组按键用于自身动作。**保留哪些由环境定义**,规范不列举、不要求一致。 +- 方向不对称是有意的:程序需要的是 1 方向的保证;0 方向若要求各环境一致,就是要求每个环境实现别人的键表,条款 7.1 拒绝这种形状。 + +### 2.2 为什么极性是"放行"而不是"消费"(本设计的核心) + +同一个位有两种取法,差别只在旧实现(0.12/0.13,不认识这个位)读回 0 时**说的是真话还是假话**: + +| 极性 | 旧实现读 0 的含义 | Linux/macOS/Windows 的真实行为 | 旧实现说的是 | +| --- | --- | --- | --- | +| 消费(PR #37) | "环境不消费控制键" | 保留 ISIG,**会消费** | **假话**,而且是危险方向的假话:程序以为拿到了保证,然后被 `^C` 杀死 | +| **放行(本稿)** | "环境不放行控制键" | 确实不放行 | **真话** | + +再看另一端,一个天然没有线路规程的环境(裸机控制台、将来的 UEFI/串口实现):旧实现读 0 = "不放行",而它其实一直放行——**这也是假话,但方向是保守的**:程序被告知"拿不到保证",于是降级或拒绝运行,没有人死掉。 + +**这就是条款 6.2 的"未分配读 0"一直以来的含义:0 是弱主张、是能力缺席。** PR #37 之所以需要给 6.2 开例外,不是因为这个能力特殊,而是因为极性取反了之后 0 变成了强主张。极性改回来,例外自动消失: + +- 条款 6.2 **一个字都不用改**,本设计回到条款 8 的纯增量演化; +- **不需要强制 props 位**(见 §3.1); +- **没有语义债**:已发布的 0.12/0.13 在新规范下读回 0,仍然是对自己行为的正确描述。PR #37 的 R1("旧行为应视为 bug、必须先还债才能诚实宣称")在本方案下不存在。 + +### 2.3 发现机制:用规范已有的那一个 + +程序要问"我能不能拿到保证",答案从 get/set 的往返里来,这是 terminal.h 已经承诺、conformance 已经在观察的东西: + +```c +kal_uintptr m; +kal_terminal_get_mode(s, &m); /* 先拿到可恢复的原样 */ +kal_terminal_set_mode(s, (m & ~KAL_TERM_LINE_EDIT & ~KAL_TERM_ECHO) + | KAL_TERM_PASS_CONTROL); +kal_uintptr got; +kal_terminal_get_mode(s, &got); +if (!(got & KAL_TERM_PASS_CONTROL)) { /* 这个实现或这个资源给不了,降级 */ } +``` + +- 旧实现:忽略未知位、读回 0 → 程序当场知道,且终端未被改坏; +- 新实现但资源给不了(串口、浏览器里的 emscripten):同上,**按资源回答**,这正是 `kal_terminal_props` 作为"取资源的问询"而非静态字的理由(条款 6.2 末段); +- 三行、无新概念、无新位、无调用顺序上的新规矩。 + +### 2.4 命名 + +`KAL_TERM_PASS_CONTROL`,注释 `the environment reserves no keystroke`。 + +- 不叫 `INTERRUPT`/`SIGNAL`:条款 7.5 已占用 "Interruption"(含义完全不同),且 openkal 不定义信号; +- 不叫单个 `CONTROL`:既可读成"控制键",也可读成"对终端的控制",而同一个头文件里另外两位都是明确的动作; +- 备选(供 review 定夺):`KAL_TERM_KEYS_AS_DATA`(最直白,但语态从"环境做什么"变成"程序得到什么")、`KAL_TERM_PASS_KEYS`(更宽,但"控制键"这个限定正是它要表达的)。 + +### 2.5 考虑过而未采纳 + +- **折进 `LINE_EDIT=0`。** 不采纳:自带行编辑的程序要"关行组装、留中断",折叠使其意图不可表达(§1 第二行)。 +- **配一个强制的 `KAL_TERM_PROP_CONTROL`。** 不采纳,见 §3.1。 +- **按键逐个命名(中断键、挂起键、流控键各一位)。** 不采纳:等于要求每个环境实现别人的键表,条款 7.1 的机械判据(翻译表 / 注册表)当场判否;而程序的需求是全有或全无。 +- **给 0 方向规定统一的键集合。** 不采纳,同上;0 方向定为许可而非保证。 +- **用 `kal_version` 作为主要发现机制。** 不采纳为**主要**机制:版本是"实现级"的答案,而终端的能力**按资源变化**(同一个实现,pty 能、串口不能)。版本仍需修好,但用途是 §5 的那一类消费者。 + +--- + +## 3. 与条款的逐条核对(结论与理由,不含客套) + +| 条款 | 结论 | 理由 | +| --- | --- | --- | +| 6.2 未分配读 0 | **符合,且不需例外** | 极性使 0 成为弱主张;见 §2.2 | +| 6.1 接口的存在 | 符合 | 不新增操作,无"存在但总是失败"的形状 | +| 6.4 不可一致存在的操作 | 符合 | 语义只存在于交互流,落在既有 `openkal.terminal`,不开新接口、不动 `openkal.stream` | +| 7.1 自然性 | 符合 | 四个实现各自 1-3 行本机映射,无翻译表、无注册层;位的措辞不提任何环境的机制名 | +| 7.4 传输 | **需补一句后果**,见 §4 | 关掉行组装后若读返回 0,就与"零即输入结束"冲突 | +| 7.11 问询的逆 | 符合,且被强化 | 发现机制就是那个逆;get 拿到的仍是可原样恢复的模式字 | +| 3.2 核心集不扩张 | 不冲突 | `openkal.terminal` 是可选接口;加位属条款 8 | +| 5.3 结构布局不可变 | 不涉及 | 不动任何结构 | +| 9 一致性程序 | 部分可观测,见 §7 R2 | `SURFACE.txt` 不变(不新增符号);宏经 `tools/gen-macros.sh` 自动进 `src/macros.cppm` | + +### 3.1 为什么不加 props 位 + +现行 `kal_terminal_props` 的两个位是**操作级**的:`PROP_MODE`(get/set 被回答)、`PROP_SIZE`(尺寸可知)。加一个**位级**的 `PROP_CONTROL` 会带来三件事,一件比一件贵: + +1. 粒度不一致:三个模式位里两个靠往返发现、一个靠 props 发现,规则从字面推不出来; +2. 没有停止点:按同一逻辑,此后每加一个模式位都要加一个 props 位,props 字沦为模式字的影子; +3. 它只在"极性取反"的前提下才**必须**存在——而那个前提本身是本设计要去掉的东西。 + +`openkal-emscripten/src/terminal.cpp` 的做法是这条判断的现成佐证:它的 props 是**问机器**得来的(浏览器里没有终端就不宣告),注释写着"a program that believes it turned echo off and did not is a program that prints a password"。既然能力本来就按资源问、按资源答,就不该把一个位的存在性再搬进静态字里。 + +--- + +## 4. 同源的第二件事:关掉行组装之后,读的零 + +条款 7.4 是规范性的:"A result of zero denotes end of input."(`stream.h:26-28` 同文)。而行组装一关,读的阻塞语义就由环境里**另一组旋钮**决定(termios 的 `VMIN`/`VTIME` 是其中一种形式),模式字不承载它,四个实现目前也都不碰它。后果是:终端若被上一个程序留在"可以立刻返回零字节"的状态,`kal_stream_read` 返回 0,**程序读到一个不是输入结束的输入结束**——这正是本设计让全屏程序终于可写之后,它们会立刻踩到的下一格。 + +**不新增位,也不新增概念**,只把既有条款的后果写在它该在的地方(terminal.h,`set_mode` 旁): + +> An implementation that turns line assembly off shall not thereby cause a read +> of the stream to report zero while input has not ended: zero denotes end of +> input (clause 7.4), and a mode change does not make it mean anything else. + +实现侧的代价:清行组装位时顺手把本环境的"至少等到一个字节"旋钮置好(termios 上是 `VMIN=1, VTIME=0`,一行)。 + +这一条可与位同批,也可拆成 0.14 的第二个提交;它**不依赖**位,但由位引出,分开走会让消费者拿到一个"raw mode 好了、read 偶尔假 EOF"的中间态。 + +--- + +## 5. 同源的第三件事:版本发现机制已经失灵 + +`include/openkal/version.h:36` 是 `KAL_VERSION_MINOR 11u`,包是 `0.13.0`;而每个实现的 `kal_version()` 就是 `return KAL_VERSION;`(openkal-linux/src/version.cpp:16,其余同)。于是**整个生态的实现都在报 0.11**,version.h 开头那段话所描述的比较—— + +> "A consumer compares it with what `kal_version' answers and refuses to proceed +> against an implementation older than the declarations it holds --- because an +> older implementation reports conditions this consumer distinguishes as +> conditions it does not, **which is a wrong answer rather than a refusal**." + +——恒等于"相等",形同虚设。对被链接的消费者无所谓(有链接器和往返可问);对条款 3.2 点名的那一类——**在 load 期绑定、或跨越一个用陷入实现的边界**,没有链接器可问的消费者——`kal_version` 是唯一入口。本设计是第一个需要它说真话的位。 + +两步,都很小: + +1. `version.h` 校正为 `0/14/0`,并在 PR 里写明这是漂移的修正而不是惯例的延续; +2. 加一条 CI 检查(新增 `tools/check-version.sh`,或并入 `tools/check-readme-versions.sh`):`KAL_VERSION_*` 必须与 `mcpp.toml` 的 `version` 一致。 + +第 2 步是这个仓库自己的体例:`check-readme-versions.sh` 的注释说得很清楚——"THE POINT IS NOT THE STALENESS, IT IS THAT IT WAS INVISIBLE... The one thing a reader actually types was checked by nobody." 把"读者要抄的那一行"换成"消费者要问的那一个数",同一句话成立。 + +--- + +## 6. 生态与任务图 + +### 6.1 实现是四条腿,不是三条 + +| 仓库 | 环境里的承载机制 | 改动量 | +| --- | --- | --- | +| `openkal-linux` | termios:`ISIG`、`IXON`、`IEXTEN`(**全集**,见下) | `mode_of` 与 `set_mode` 各 1-2 行 | +| `openkal-macos` | 同上(POSIX 同构) | 同上 | +| `openkal-windows` | console:`ENABLE_PROCESSED_INPUT` | 同上 | +| `openkal-emscripten` | 转发宿主 termios;浏览器里 props 本就不宣告 | 同上,且天然按资源作答 | + +**映射取全集而不是只取 `ISIG`。** 保证的措辞是"每个按键以字节到达",而 termios 上 `ISIG` 只挡住中断/退出/挂起三键;流控(`^S`/`^Q`,`IXON`)和 literal-next/discard(`^V`/`^O`,`IEXTEN`)仍会被线路规程吃掉。Windows 的 `ENABLE_PROCESSED_INPUT` 恰好**包含**流控。只映射 `ISIG` 的结果是:同一个模式字下 Linux 的 `^S` 冻住终端而 Windows 不会——由规范导致的跨平台分叉,正是条款 7.1 要避免的"规范取了某一个环境的形状"。位的措辞("环境保留的按键,集合由环境定义")已经把这件事说对了,任务卡只需与它一致。 + +将来一个没有线路规程的环境(裸机、UEFI 控制台)实现 `openkal.terminal` 时:`PASS_CONTROL` 恒为 1,`set(0)` 被忽略,读回 1——诚实,且不需要它去模拟任何人的键表。 + +### 6.2 消费者是多个,`openkal-musl` 只是其中之一 + +| 消费者 | 本设计对它意味着什么 | +| --- | --- | +| `openkal-musl` | `tcgetattr`/`tcsetattr` 的 `ISIG|IXON|IEXTEN` ↔ `PASS_CONTROL`;`cfmakeraw` 随之免费正确 | +| 任何其他 C 环境(picolibc 一类的移植) | 同一映射,各自决定;规范不提 termios,所以不与 musl 绑死 | +| `std-freestanding` / 不带 C 库的程序 | 直接用模块形式 `kal::terminal::pass_control`,不经过任何 C 语义 | +| 已有 TUI 类端口(imgui 等) | 现有全屏程序在**与模式字对应的那部分**自动正确;其余(输出后处理、字符尺寸/奇偶等)仍由各自环境决定,**不宣称"零改动即全对"** | + +### 6.3 任务图与顺序 + +``` +1. openkal 0.14.0(规范,唯一硬先行) + ├ terminal.h:KAL_TERM_PASS_CONTROL + 位的语义 + §4 的一句 + 极性规则一句 + ├ src/terminal.cppm:kal::terminal::pass_control + ├ tools/gen-macros.sh 重生成 src/macros.cppm(CI 会 diff) + ├ SPEC.md:§11 新条目(体例照条目 10);条款 6.2 加"零是弱主张"一句(规则,非例外) + ├ version.h 0.14.0 + tools/check-version.sh(§5) + ├ conformance:往返观察纳入新位;新增"关行组装后读不返回假零"的条件观察 + └ mcpp.toml / README 版本行(check-readme-versions.sh 会校) + │ 合并 + tag 0.14.0 + ▼ +2. mcpp-index 注册 openkal 0.14.0 + │ + ├──────┬──────────┬──────────────┐ + ▼ ▼ ▼ ▼ +3a. linux 3b. macos 3c. windows 3d. emscripten (四实现并行,互不依赖) + └──────┴──────────┴──────────────┘ + │ 各自 minor bump + index 注册 + ▼ +4. 消费者侧:openkal-musl(ioctl 分发器路由到 kal_terminal_*; + 并修掉三处"报告成功却什么都没做":TCGETS/TIOCGWINSZ 不回写、SIG_IGN 不安装) + │ + ▼ +5. 验证与回执:issue #36 的两支探针收进 openkal-musl 的 tests/(不留在 issue 里), + pty 下与宿主 C 库对照;回帖 #36 +``` + +**为什么是这个序**:下游都要 `#include ` 的新位,且 mcpp 依赖解析要求 `openkal = "0.14.0"` 先存在于 index;四个实现只共同依赖规范、彼此独立;消费者层依赖实现跑端到端验证,且改动最大,放最后风险最低。 + +### 6.4 验收 + +| # | 验收 | +| --- | --- | +| 1 | `tools/check-*.sh` 全绿(含新增的版本检查);CI 四声明矩阵绿;`macros.cppm` 重生成无 diff | +| 2 | index CI 绿 | +| 3a-3d | 各自端口自测 + openkal conformance;有 pty 的腿上人工确认 `^C` 以 `0x03` 到达、`^S` 不冻结 | +| 4 | 探针程序在 pty 下与宿主 C 库对照一致;三处 silent success 消失(哨兵字节被真实数据覆盖、`SigIgn` 位真的出现) | +| 5 | #36 上给出可复现的对照输出 | + +--- + +## 7. 风险与开放问题 + +- **R1 无语义债**(与 PR #37 最大的差别)。旧实现读回 0 在新规范下仍是真话,因此不存在"必须先升级才能诚实"的前置;升级顺序纯由依赖决定。 +- **R2 一致性套件看不见端到端保证。** CI 无 pty 且套件必须能在没有 C 库的环境跑,所以自动观察只到"往返"和"非交互流的拒绝"两半;"`^C` 以字节到达"由 §6.3 第 5 步的探针承担。探针进仓库,不留在 issue(链接会烂)。 +- **R3 名称待裁。** `PASS_CONTROL` / `KEYS_AS_DATA` / `PASS_KEYS`,见 §2.4。 +- **R4 §4 与 §5 是否同批。** 两者都可拆;建议同批,理由分别是"分开会留下中间态"和"本设计是第一个需要版本说真话的位"。 +- **R5 条款 6.2 的那一句是否写。** 不写也不影响本设计成立(极性已经把事办了);写了则把一次性判断变成下一个位也适用的规则——这是 openkal 一贯的落笔高度(条款 7.11 之于 `kal_fs_set_modified`,历史条目 8 之于 fork)。倾向写。 +- **O1 `openkal-musl` 的三处 silent success** 与本设计无关,属端口缺陷,但建议同批:issue #36 的四个缺陷里三个是"报告成功却什么都没做",分批修会让消费者拿到"raw mode 好了但 `SIG_IGN` 仍骗人"的中间态。 +- **O2 输出方向暂不处理。** 输出后处理(termios `OPOST` 一类)不在模式字里,本设计不动它;若将来有程序因此写不出来,按同一判据(7.11)再议,并按同一极性规则取位。 + +--- + +## 8. 附:待 review 的规范性文字草稿 + +**条款 6.2(在"a position that has not been assigned reads as zero"之后加一句):** + +> A position is assigned the sense in which zero is the weaker claim, so that an +> implementation which has never heard of the position is not made to assert the +> stronger one by its silence. + +**SPEC.md §11 新条目(编号 20,体例照条目 10):** + +> 20. **A keystroke the environment keeps for itself.** Settled in 0.14. +> `KAL_TERM_PASS_CONTROL` states whether the environment reserves any +> keystroke, and a program that needs every key as data asks for it and reads +> the mode back. Admitted on the grounds entry 10 records for locking: every +> environment this specification has been implemented on already reserves +> such keys and spells the switch almost identically --- `ISIG` and its +> neighbours, `ENABLE_PROCESSED_INPUT` --- so what was missing was a word and +> not a capability. +> +> **What its absence cost, and it was not a refusal.** A C environment above +> this interface answers `tcsetattr` with the mode word. With no position for +> the reserved keys, an editor that cleared line assembly and echo was still +> killed by the interrupt key, and nothing in the interface was wrong: the +> implementation preserved what the specification had not named. Measured +> against a host C library on one terminal, openkal-musl#36. +> +> **The position is spelled in the sense that zero is the weaker claim**, so +> an implementation predating it reports zero and thereby says something true +> about itself. The arrangement in which zero would have been the guarantee +> was considered and not adopted: it would have required an exception to +> clause 6.2 and a property position beside it, to repair a hazard that the +> spelling itself removes. diff --git a/.agents/docs/2026-09-20-pr37-term-control-review.md b/.agents/docs/2026-09-20-pr37-term-control-review.md new file mode 100644 index 0000000..7e5728d --- /dev/null +++ b/.agents/docs/2026-09-20-pr37-term-control-review.md @@ -0,0 +1,134 @@ +# PR #37(KAL_TERM_CONTROL 设计稿)深度 review + +- 日期:2026-09-20 +- 对象:mcpplibs/openkal#37 —— `.agents/docs/2026-09-18-kal-term-control-design.md`(158 行,纯设计,无实现) +- 参照:SPEC.md(main @ 86eb855)、include/openkal/{terminal,version,stream}.h、conformance/src/sections/terminal.cpp、openkal-linux/src/terminal.cpp、openkal-macos/src/terminal.cpp、openkal-windows/src/terminal.cpp、mcpp-index/pkgs/o/openkal.lua、消费者报告 mcpplibs/openkal-musl#36 +- 审查问题:**必要吗 / 通用吗 / 简洁优雅吗 / 合乎 openkal 的设计原则吗** + +--- + +## 0. 结论 + +| 维度 | 判断 | +| --- | --- | +| **必要性** | **成立。** 问题真实、无法在接口之上组合出来,且正好命中条款 7.11 的准入判据 | +| **位置与形状** | **正确。** 一个模式位、落在既有 `openkal.terminal`、正交于 LINE_EDIT,是这个问题唯一自然的形状 | +| **通用性** | **基本成立,但被"ISIG 一一对应"的叙述缩窄了**(见 P3),三平台会在 `^S`/`^V`/阻塞语义上分叉 | +| **简洁优雅** | **未达到。** 方案为了一个位,附带了 (a) 修改核心条款 6.2 的规范性例外、(b) 一个强制 props 位。二者都是**极性选错**的连带损害,不是问题本身要求的 | +| **原则符合** | 7.1 / 6.4 / 7.11 符合;6.2 **不是"基本符合",而是被本方案的极性逼成了例外**;且方案未用规范自己已有的两个发现机制(get/set 往返、`kal_version`) | + +**一句话建议:保留这个位,砍掉围绕它的两处规范手术。** 把位的极性反过来("环境放行控制键"而不是"环境消费控制键"),条款 6.2 的"未分配读 0"就自动是**真话**,强制 props 位和规范性例外一起消失,方案回到纯增量演化(条款 8)。 + +--- + +## 1. 必要性:通过 + +三条都查过了,不是转述设计稿: + +1. **不能在接口之上组合。** openkal 无信号接口,openkal-musl 的 `rt_sigaction` 对非 SIG_DFL/SIG_IGN 一律 ENOSYS(issue #36 实测 `errno=38`)。程序既不能装 handler,也不能靠清 ICANON 顺带躲开 `^C` —— openkal-linux 的 `set_mode` 按设计只动 `ICANON`/`ECHO` 并保留其余 lflag(src/terminal.cpp:63-66 的 read-modify-write,注释明确说明为什么保留)。**"清了行编辑仍被 ^C 杀死"是当前实现的正确行为**,不是 bug,这恰好证明缺的是规范词汇。 +2. **准入判据(7.11 / 历史条目 10)成立。** "三个普通程序写不出来":全屏编辑器、分页器、自带行编辑的 REPL,都是"C 库之上预期托管的程序"。三个目标环境都有现成承载机制(termios `ISIG`、console `ENABLE_PROCESSED_INPUT`),**缺的是一个词而不是一种能力** —— 与历史条目 10(文件锁)同形。 +3. **正交性论证成立。** readline 类程序"关 ICANON、保留 ISIG"是三平台的本机写法,把中断语义折进 `LINE_EDIT=0` 会让这类程序的意图不可表达 —— 与 terminal.h 里 `LINE_EDIT` 不含 `ECHO` 的既有理由("密码提示符关一个留一个")字面同构。设计稿 §1.1 的这一格是全文最强的一段。 + +> 设计稿把 issue #36 拆成"3/4 端口缺陷 + 1/4 规范缺口"是准确的分账。本 PR 只处理那 1/4,`§4 非目标`的划线干净。 + +--- + +## 2. 主要问题 + +### P1(首要)极性选反了,规范性例外是自找的 + +设计稿 §2.2 自己承认:"恰好对这个位,条款 6.2 的'未分配读 0'与已发布实现的真实行为不符"。这不是这个能力的固有性质,**是 `CONTROL=1 表示环境消费控制键` 这个极性选择的后果**: + +- 旧实现读回 0 → 按规范语义是"环境不消费控制键" → **假话**(它保留了 ISIG)。 +- 新程序 `set(CONTROL=0)` → 旧实现忽略 → 程序以为拿到了保证 → 正是 6.2 存在要防的事故形状。 + +把极性反过来(暂名 `KAL_TERM_PASS_CONTROL`:**1 = 环境把约定控制键原样交给程序**): + +| | 旧实现(0.12/0.13) | 新实现 | +| --- | --- | --- | +| `get_mode` 该位 | 读 0 = "不放行" | 真实状态 | +| 真实行为 | 不放行(保留 ISIG) | 一致 | +| **6.2 的"未分配读 0"** | **是真话** | 是真话 | +| 程序 `set(位)` 后读回 | 读回 0 → **程序当场知道没拿到** | 读回 1 | + +于是: + +- 条款 6.2 **一个字都不用改**,本方案变回纯粹的条款 8 演化; +- **强制 props 位不再必要**(能力发现由 get/set 往返承担,见 P2); +- 发现机制用的是规范自己已经写下并且 conformance 已经在观察的那一条 —— terminal.h:"A position this implementation does not distinguish is reported as zero by the first and ignored by the second",conformance/src/sections/terminal.cpp 已有 "the mode read back is the mode that was set"; +- 也符合 7.11 "问询的逆":程序通过逆运算得知环境的实际状态,而不是另开一条"先查 props"的特殊规矩。 + +**唯一代价**是位的语态与 `LINE_EDIT`/`ECHO`("环境做 X")不完全同向。但 `PASS_CONTROL` 仍是描述环境行为的("环境放行"而非"程序要求"),语态一致;而用语态上的整齐去换一条核心条款的例外 + 一条程序必须记住的新规矩,**这笔买卖是亏的**。openkal 的风格恰恰是宁可位名拗口也不让规范长出例外(见历史条目 8 对"一句 openkal 不会有 fork"的拒绝理由)。 + +### P2 强制 props 位改变了 props 字的粒度,且没有停止点 + +现行 `kal_terminal_props` 的两个位是**操作级**的:`KAL_TERM_PROP_MODE`("get/set 被回答")、`KAL_TERM_PROP_SIZE`("显示尺寸可知")。`KAL_TERM_PROP_CONTROL` 是第一个**位级**条目 —— 它回答的是"模式字里某一位是否被区分"。引入之后: + +- 为什么 `LINE_EDIT`/`ECHO` 没有对应的 props 位?三个模式位里两个靠 get/set 往返发现、一个靠 props 发现,读者无法从字面推出规则; +- 按同一逻辑,**今后每加一个模式位都要加一个 props 位**,props 字变成模式字的影子,且没有原则能在中途叫停。这与条款 6.2 划的"操作 / 属性"两分不是一个东西。 + +若采纳 P1,这个位可以整个不要;若要保留(作为可选的、便于一次性询问的冗余信息),也**不应写成"程序须先查 props"的规范义务**,否则等于承认模式字的 get/set 往返不可信 —— 而那是 terminal.h 与 conformance 已经承诺的东西。 + +### P3 "ISIG 一一对应"不足以兑现 §2.1 的保证,三平台会分叉 + +设计稿 §2.1 的承诺是"**保证每个键以字节到达**"。termios 上 `ISIG` 只挡住 `VINTR/VQUIT/VSUSP`,其余仍被线路规程吃掉: + +| 被环境吃掉的键 | termios 开关 | 设计稿是否覆盖 | Windows `ENABLE_PROCESSED_INPUT` | +| --- | --- | --- | --- | +| `^C` `^\` `^Z` | `ISIG` | 是 | 是 | +| `^S` `^Q`(流控) | `IXON` | **否** | 是(同一开关) | +| `^V` `^O`(literal-next / discard) | `IEXTEN` | **否** | n/a | +| `Enter` 变成 `0x0a` | `ICRNL` | 否(可另议) | n/a | + +后果:同一个模式字下,Linux/macOS 的 `^S` 仍会冻住终端而 Windows 不会 —— **由规范导致的跨平台行为分叉**,正是 7.1 要避免的"规范取了某一个环境的形状"。musl `cfmakeraw` 清的是 `ISIG|IEXTEN|ECHO|ECHONL|ICANON` 加 `IXON` 等,所以 §3 "现有全屏程序不改动即正确"也随之**过度宣称**。 + +修法很轻,且不损 7.1:把位定义成 **"环境是否保留*一组*约定控制键为己用,集合由环境定义"**,实现侧把它映射到本环境**全部**此类机制(termios: `ISIG|IXON|IEXTEN`;console: `ENABLE_PROCESSED_INPUT`)。方向性保证写成非对称的两句 —— **0 方向是承诺(所有按键都是数据),1 方向是许可(环境可保留,保留哪些由环境定)** —— 设计稿 §2.1 已经隐约这么写了("可保留约定控制键"),只是 §1.2 的表和任务卡 3a/3c 又把它收窄回了 `ISIG` 一个标志。把任务卡也改成全集即可。 + +**附带的硬问题:`VMIN`/`VTIME`。** `stream.h` 写死"Zero denotes end of input"。而 `ICANON` 清掉之后,读的阻塞语义由 `c_cc[VMIN]/[VTIME]` 决定,而模式字里没有位承载它,现行三个实现也都不碰它(openkal-linux src/terminal.cpp 的 read-modify-write 只动 lflag)。若终端被前一个程序留在 `VMIN=0`,`kal_stream_read` 会返回 0,**程序读到一个不是 EOF 的 EOF**。这是本设计使全屏程序真正可写之后立刻会撞上的下一格。建议至少二选一:任务卡 3a/3b/3c 要求"清 LINE_EDIT 时确保至少一字节才返回(`VMIN=1, VTIME=0`)";或按 §11 的体例记一条"未settle"条目。**不宜默默留白**——留白正是 issue #36 那份报告全程在批评的形状。 + +### P4 规范已有的发现机制之一是坏的:`KAL_VERSION_MINOR` 停在 11 + +`include/openkal/version.h:36` 是 `11u`,而 mcpp.toml 是 `0.13.0`。而 version.h 开头那段话,字面就是 §2.2 想解决的问题: + +> "A consumer compares it with what `kal_version' answers and refuses to proceed against an implementation older than the declarations it holds --- because an older implementation reports conditions this consumer distinguishes as conditions it does not, **which is a wrong answer rather than a refusal**." + +"旧实现把它不区分的状态报成 0,是错误答案而不是拒绝" —— 规范早就写下了这条规则和它的对策。设计稿 §5.2 却把版本停滞当成"仓库惯例"接受下来("0.12/0.13 发布均未 bump version.h")。**那不是惯例,是漂移**:对 linked consumer 无所谓(靠 get/set 往返与链接期符号),但对条款 3.2 点名的那类消费者 —— 在 load 期绑定或跨边界、没有链接器可问的 —— `kal_version` 是唯一入口,而它现在会说 0.11。 + +建议:0.14 这一波把 version.h 校正到 `0u.14u.0u`,并在 PR 里写明为什么(漂移、以及本设计正好是第一个依赖它的位)。这比新增一条 6.2 例外更贴合规范自身的结构。 + +### P5 三处引用与事实核对不符(都在"事实来源"里,需改) + +1. **§2.1 "SPEC.md:284-304 明确排除[信号]"** —— 该行区间是条款 4.2 的"Arrangements considered and not adopted",讲的是声明的组织方式,与信号无关。**全文 grep:SPEC.md 中没有任何排除信号接口的条款**("signal" 只出现在 §11 历史条目 9 讨论 job control 的语境里)。"openkal 无信号"目前只见于 README.md:115 和 terminal.h 的一句注释("openkal has no signals, so a program learns of a change by asking again")。 + → 本设计的整条论证都架在"无信号"之上("这是全屏程序在 openkal 上存活于 ^C 的唯一途径")。**既然 0.14 因为"没有信号"才必须加这个位,就应当在同一个 PR 里把这项排除写进 §3.4 或 §11** —— 正是历史条目 8 的体例("一句'openkal 不会有 fork'会把这个区分埋掉,此条因此存在")。这一条我认为不是可选润色,而是本设计缺的那块规范文字。 +2. **"SPEC 历史条目 7(文件锁)"** —— 条目 7 是 links;文件锁与那句 "What was missing was a word, not a capability" 在**条目 10**(SPEC.md:1105-1115)。论点没错,编号错了。 +3. **§5.1 "仿 register-openkal-0.15.0 分支既有流程"** —— mcpp-index 当前该分支名与 openkal 实际版本(index 里最高 0.13.0)不一致,容易被读成"0.15 已在路上"。引用流程即可,别引用这个分支名。 + +### P6 位名 `KAL_TERM_CONTROL` 语义含糊 + +`CONTROL` 单独一个词既可读成"控制键",也可读成"对终端的控制",而同一个头文件里 `LINE_EDIT`/`ECHO` 都是**动作**。设计稿 §2.3 排除 `INTERRUPT`/`SIGNAL` 的理由(7.5 已占用 "Interruption"、openkal 无信号模型)是对的,但结论落点不够。建议 `KAL_TERM_PASS_CONTROL`(配合 P1 的反相极性)或 `KAL_TERM_CONTROL_KEYS`(维持原极性)。 + +--- + +## 3. 次要 + +- **§1.1 表里 "CONTROL 1(默认)"** —— 模式字报告的是"当前生效的模式",没有"默认"一说;准确的话是"三个环境的终端初始状态中该位通常为 1"。 +- **R2(conformance 可观测性)的折衷是对的**,但可以再收一格:CI 无 pty 时观察不了"^C 以字节到达",那就把**非交互流的拒绝**与**位的 get/set 往返**当作可观测的两半(现有 section 已是这个骨架),端到端留给 issue #36 的探针。建议把探针程序收进 openkal-musl 仓库而不是只留在 issue 里 —— 链接会烂,验收会跑不了。 +- **任务卡 4 的验收**("两支探针在 pty 下与 glibc 对照一致")目前是人工步骤,且第二支探针测的是 SIG_IGN —— 而 SIG_IGN 已被 §4 划出范围。两处需对齐:要么验收只用第一支探针,要么把 SIG_IGN 修复并入同批(即 O1,我倾向并入:issue #36 的四个缺陷里三个是"报告成功却什么都没做",那是该端口文件自己的注释声明绝不产生的形状,分批修会让消费者拿到一个"raw mode 好了但 SIG_IGN 仍骗人"的中间态)。 +- **依赖序**(openkal → index → 三实现并行 → musl)**正确**,理由也站得住:三实现只共同依赖规范,musl 依赖实现跑端口级验证。无可改。 +- **零改动宣称**:请按 P3 降级为"`cfmakeraw` 中与模式字对应的部分自动正确",别写成"现有全屏程序不改动即正确"。 + +--- + +## 4. 如果采纳,0.14 的最小改动清单 + +1. `terminal.h`:加一个模式位,**反相极性**,含房屋风格的理由注释(为什么是第三个位而不是折进 LINE_EDIT;为什么极性与前两位相反 —— 因为"未分配读 0"必须是真话)。`src/terminal.cppm`、`src/macros.cppm` 镜像。 +2. `SPEC.md`:**不动条款 6.2**;加一条 §11 条目记录本位的admission(体例照条目 10),并在 §3.4 或 §11 补记"信号接口不被定义"这项既有立场(P5-1)。 +3. `include/openkal/version.h`:`KAL_VERSION_MINOR` 校正为 14(P4),PR 里说明漂移。 +4. 不加 props 位;若坚持加,则写成可选的冗余信息,不写"程序须先查"的义务(P2)。 +5. conformance:往返观察沿用现有骨架,新增位纳入同一条观察。 +6. 三实现任务卡:映射**全集**(`ISIG|IXON|IEXTEN` / `ENABLE_PROCESSED_INPUT`),并处理 `VMIN/VTIME`(P3)。 + +## 5. 如果坚持原极性,必须补的文字 + +- 条款 6.2 的例外必须写成**一般规则**而不是"恰好对这个位":即"当某位的 0 值同时是一个**真实状态**而非能力缺席时,该位须伴随 props 位,且程序须先查 props"。否则这条例外无法泛化,下一个同形的位会重开一次同样的讨论——而 openkal 的体例一贯是把规则写在能泛化的高度上(条款 7.11 之于 `kal_fs_set_modified` 就是范例)。 +- 并且仍需 P3、P4、P5 的修正 —— 这三条与极性选择无关。 diff --git a/.agents/docs/2026-09-20-term-pass-control-execution-plan.md b/.agents/docs/2026-09-20-term-pass-control-execution-plan.md new file mode 100644 index 0000000..e971c0e --- /dev/null +++ b/.agents/docs/2026-09-20-term-pass-control-execution-plan.md @@ -0,0 +1,88 @@ +# KAL_TERM_PASS_CONTROL:执行计划与生态闭环 + +- 日期:2026-09-20 +- 依据:`.agents/docs/2026-09-20-kal-term-pass-control-design.md`(v2 设计稿) +- 原则:每个仓库一个 PR,实现全部方案;分支名在各仓库一致(`openkal-0.14`),因为各仓库 CI 用 `github.head_ref` 去检出同名的规范分支;文档与注释不含表情符号。 + +--- + +## 0. 自我 review:设计稿在落地前需要补的四条 + +**S1 模式字压缩是有损的,损在哪里必须写下来。** `PASS_CONTROL` 一个位对应 termios 上三个机制(`ISIG`、`IXON`、`IEXTEN`)。一个终端若本来就处在"部分释放"的混合态(用户 `stty -ixon` 是常见配置),`get_mode` 只能报 0,程序 raw 一趟再恢复,`IXON` 会被重新打开——"用户回到的终端不是他原来那个"。三个位分列可以无损,但 Windows 只有一个开关(`ENABLE_PROCESSED_INPUT` 同时管中断与流控),分列会要求 Windows 实现它拿不出的区分,条款 6.4 判否。**结论:保留一个位,把这项代价写进 SPEC §11 条目与头文件注释**(体例照条目 9 的 "What this costs")。 + +**S2 为减小损面,补一条实现规则:请求值与当前值相同时,不得改动该位覆盖的任何机制。** 单机制的位(`LINE_EDIT`、`ECHO`)上这条自动成立;多机制的位上它决定了"只改回显的程序不会碰到流控"。写在 `set_mode` 旁。 + +**S3 `VMIN`/`VTIME` 的那一条是必须的,理由比设计稿说得更强。** openkal-musl 把 `tcsetattr` 映射到模式字之后,`cfmakeraw` 写下的 `VMIN=1/VTIME=0` **会被丢弃**——没有位承载它。若终端停在 `VMIN=0`,`kal_stream_read` 返回 0,而条款 7.4 说"零即输入结束"。所以这不是"顺带做的好事",是本设计不做就会**造出**的缺陷。 + +**S4 版本号变真本身是一次行为变化。** `conformance/src/sections/version.cpp` 已经在断言 `kal_version() >= kal::header_version`。今天整个生态的实现都报 0.11(`kal_version` 就是 `return KAL_VERSION;`,而 `version.h` 停在 11),断言恒真。把 `version.h` 改到 0.14.0 之后,**未随本波重建的实现会被 0.14 的套件判为过旧**——这正是该断言的本意。因此版本修正必须与六个实现在同一波里落地,且 `mcpp-index` 里旧描述符保持不动(旧消费者按旧版本对解析,仍是自洽的一对)。 + +--- + +## 1. 多角度判据 + +| 角度 | 本波的判据 | +| --- | --- | +| 架构 | 不新增接口、不新增操作、不改结构布局;语义落在既有 `openkal.terminal` 的模式字 | +| 稳定性 | 旧实现在新规范下说的仍是真话(极性选择的直接结果),无语义债、无强制升级 | +| 优雅简洁 | 规范净增:一个模式位、条款 6.2 一句规则、§11 一条记录;不加 props 位、不加例外 | +| 用户体验 | 消费者侧零改动:`tcgetattr`/`cfmakeraw`/`tcsetattr` 的既有写法自动正确;失败可被读回发现 | +| 兼容性 | 0.13 消费者不受影响;0.14 程序在 0.13 实现上读回 0 并降级;index 旧描述符保留 | +| 跨平台 | 四个实现各自映射本机机制;无线路规程的环境恒报 1 并忽略置 0,诚实且无需模拟别人的键表 | +| 一致性 | 位的措辞不提任何环境的机制名;四实现映射各自机制的**全集**,避免 `^S` 在 Linux 冻终端而 Windows 不冻 | +| 无感升级 | 版本链一次走完(规范 → index → 四实现 → C 环境 → 运行时);消费者只改一行版本号 | +| 测试覆盖 | 套件内:位的往返、未分配位不报错、props 与操作一致;套件外:pty 探针(`^C` 以 0x03 到达、`^S` 不冻结、读不返回假零);生态级:真实 TUI 程序在沙箱里跑 | + +--- + +## 2. 仓库任务卡 + +| # | 仓库 | 版本 | 内容 | 验收 | +| --- | --- | --- | --- | --- | +| 1 | `openkal` | 0.13.0 → **0.14.0** | `terminal.h` 新位与语义、`set_mode` 的两条实现规则(S2、S3);`src/terminal.cppm` 的 `pass_control`;`tools/gen-macros.sh` 重生成;SPEC 条款 6.2 一句 + §11 条目 20;`version.h` 0.14.0 + `tools/check-version.sh`;conformance 新观察;README/mcpp.toml 版本 | `tools/check-*.sh` 全绿;conformance 对 openkal-linux 全绿;CI 全绿 | +| 2 | `openkal-linux` | 0.13.0 → **0.14.0** | `mode_of` 读 `ISIG|IXON|IEXTEN`;`set_mode` 按位写全集 + `VMIN=1/VTIME=0`;依赖 0.14.0 | 自带测试 + pty 探针;conformance 全绿 | +| 3 | `openkal-macos` | 0.10.0 → **0.11.0** | 同 2(termios 同构,常量值不同) | 同 2(macOS leg) | +| 4 | `openkal-windows` | 0.8.0 → **0.9.0** | `ENABLE_PROCESSED_INPUT` 的读写;依赖 0.14.0 | 同 2(Windows leg) | +| 5 | `openkal-emscripten` | 0.2.0 → **0.3.0** | 转发宿主 termios 的同一映射;依赖 0.14.0 | 同 2(node leg) | +| 6 | `openkal-uefi` | 0.7.0 → **0.8.0** | 跟随规范 0.14.0(不提供 terminal,只改依赖与文档) | CI 全绿 | +| 7 | `openkal-opensbi` | 0.7.0 → **0.8.0** | 同 6 | CI 全绿 | +| 8 | `openkal-musl` | 0.15.0 → **0.16.0** | ioctl 分发器路由到 `kal_terminal_*`:`TCGETS`/`TCSETS` 族真实读写、`TIOCGWINSZ` 真实回填;`ISIG|IXON|IEXTEN` ↔ `PASS_CONTROL`;`rt_sigaction` 的 `SIG_IGN` 不再假成功 | issue #36 的两支探针与宿主 C 库对照一致 | +| 9 | `openkal-llvm-runtime` | 0.11.0 → **0.12.0** | 跟随 openkal-musl 0.16.0 | CI 全绿 | +| 10 | `mcpp-index` | — | 注册以上全部新版本描述符 | index CI 绿;沙箱能只写版本号解析 | + +--- + +## 3. 依赖与顺序 + +``` +1 openkal 0.14.0 ──tag──> mcpp-index 注册 + ├──> 2 linux ──┐ + ├──> 3 macos ├── 并行,互不依赖 ──> mcpp-index 注册 + ├──> 4 windows │ + ├──> 5 emscripten ┘ + ├──> 6 uefi / 7 opensbi(并行,跟随) + └──> 8 openkal-musl(依赖 2..5 的实现)──> 9 llvm-runtime ──> 10 index + └──> 生态验证:xlings 沙箱 + re-cloud-code 真实 TUI +``` + +唯一硬序是规范先行:下游都要 `#include ` 的新位,且依赖解析要求 `openkal = "0.14.0"` 先在 index 里。各仓库 PR 可同时开:CI 按分支名检出同名规范分支,因此在合并前就能看到真实结果。 + +--- + +## 4. 验证层次 + +1. **规范内**:`tools/check-declarations.sh`、`check-surface.sh`、`check-types.sh`、`check-readme-versions.sh`、新增 `check-version.sh`、`gen-macros.sh` 重生成无 diff。 +2. **一致性套件**:`tools/run-conformance.sh` 对每个实现跑;新位的往返观察在有终端时生效,在管道下按既有方式报告为未观察。 +3. **端口级**:pty 下的探针程序(进仓库,不留在 issue 里):`^C` 到达为 `0x03`、`^S` 不冻结、恢复后终端回到原样、读不返回假零。 +4. **生态级**:`xlings subos ... --sandbox --cmd`,只写版本号解析全链;再用 `/home/speak/workspace/scode/re-cloud-code`(真实 TUI,`packages/tui-kit/src/term.cppm` 正是 `tcgetattr`/`cfmakeraw`/`tcsetattr` 的调用方)在 musl 目标上构建并运行。 + +## 5. 发布 + +每个仓库:PR 合并 → 打 tag → GitHub release → `gtc` 补 GitCode 镜像资源 → `mcpp-index` 注册描述符(sha256 记录在本波的 record 文档)。顺序同 §3。 + +## 6. 风险 + +- **R1** 版本号变真使未重建的实现被套件判为过旧(S4):六个实现在本波内全部重建,index 旧描述符不动。 +- **R2** 套件看不到端到端保证:CI 无 pty,端到端由第 3 层探针与第 4 层真实程序承担。 +- **R3** 混合态的有损恢复(S1):记录为代价,不追加位。 +- **R4** openkal-musl 的 `SIG_IGN` 由"假成功"改为"按能否兑现回答"是行为变化:已在 issue #36 中被消费者明确要求,写入发布说明。 +- **R5** 输出方向(`OPOST` 一类)不在模式字内,本波不动;若真实程序因此可见异常,记录为下一波的问题而不是临时加位。 diff --git a/.agents/docs/2026-09-20-term-pass-control-record.md b/.agents/docs/2026-09-20-term-pass-control-record.md new file mode 100644 index 0000000..4150d28 --- /dev/null +++ b/.agents/docs/2026-09-20-term-pass-control-record.md @@ -0,0 +1,92 @@ +# KAL_TERM_PASS_CONTROL:本波的记录 + +- 日期:2026-09-20 +- 设计:`2026-09-20-kal-term-pass-control-design.md`;计划:`2026-09-20-term-pass-control-execution-plan.md`;对前一方案的 review:`2026-09-20-pr37-term-control-review.md` +- 起因:消费者报告 [mcpplibs/openkal-musl#36](https://github.com/mcpplibs/openkal-musl/issues/36) +- 分支名(各仓库一致,CI 据此检出同名规范分支):`openkal-0.14` + +--- + +## 1. PR 与版本 + +| 仓库 | PR | 版本 | 内容 | +| --- | --- | --- | --- | +| openkal | [#38](https://github.com/mcpplibs/openkal/pull/38) | 0.13.0 → 0.14.0 | `KAL_TERM_PASS_CONTROL`;条款 6.2 的极性规则;`set_mode` 两条规则;§11 条目 20;`version.h` 0.14.0 与 `tools/check-version.sh`;套件新观察 | +| openkal-linux | [#28](https://github.com/mcpplibs/openkal-linux/pull/28) | 0.13.0 → 0.14.0 | `ISIG|IXON|IEXTEN` 映射;`VMIN=1/VTIME=0`;自带测试新增观察 | +| openkal-macos | [#22](https://github.com/mcpplibs/openkal-macos/pull/22) | 0.10.0 → 0.11.0 | 同上,本内核常量 | +| openkal-windows | [#25](https://github.com/mcpplibs/openkal-windows/pull/25) | 0.8.0 → 0.9.0 | `ENABLE_PROCESSED_INPUT`,取反 | +| openkal-emscripten | [#3](https://github.com/mcpplibs/openkal-emscripten/pull/3) | 0.2.0 → 0.3.0 | 转发宿主 termios;absence 检查改为取工作树,engine pin 升到 index 下限 | +| openkal-uefi | [#14](https://github.com/mcpplibs/openkal-uefi/pull/14) | 0.7.0 → 0.8.0 | 跟随规范(不提供 terminal,跟的是 `kal_version`) | +| openkal-opensbi | [#17](https://github.com/mcpplibs/openkal-opensbi/pull/17) | 0.7.0 → 0.8.0 | 同上 | +| openkal-musl | [#38](https://github.com/mcpplibs/openkal-musl/pull/38) | 0.15.0 → 0.16.0 | ioctl 路由到 `openkal.terminal`;信号处置只接受已生效的;`examples/terminal` + `tools/pty-keys.py` | +| openkal-llvm-runtime | [#25](https://github.com/mcpplibs/openkal-llvm-runtime/pull/25) | 0.11.0 → 0.12.0 | 跟随 musl 0.16.0(并把停在 main 的 0.11.0 带回) | +| mcpp-index | 待填 | — | 注册以上全部描述符 | + +## 2. 设计落点(与设计稿的差异) + +1. **位名**定为 `KAL_TERM_PASS_CONTROL`(设计稿备选 `KEYS_AS_DATA`/`PASS_KEYS`)。 +2. **条款 6.2 的那一句写了**(设计稿 R5 留待裁定):位按「零是弱主张」的方向拼写,并说明这条规则是在指派这个位时被认出来的。 +3. **`VMIN`/`VTIME` 与版本修正同批**(设计稿 R4 留待裁定),理由见计划 §0 S3、S4。 +4. **一个位压缩三个机制的代价**写进了头文件、SPEC §11 条目 20 与实现注释(计划 §0 S1)。 +5. **端口的映射方向不对称**:读取时三机制全清才报告,写入时只看 `ISIG`。设计稿未区分这两个方向,这是落地时发现的:要求三者全清才转达,会让 `cfmakeraw` 能用而「只清 ISIG」的程序静默无效——正是本报告的缺陷形状。 + +## 3. 验证 + +### 3.1 规范内 + +- `tools/check-version.sh`(新增)通过,并且 CI 证明它会拒绝漂移;负向探针在三个系统上都能跑(`sed -i` 的不可移植写法已改)。 +- `check-declarations.sh` 104 names、`check-types.sh` 175 declarations、`gen-macros.sh` 重生成无 diff、`mcpp build` 通过。 + +### 3.2 一致性套件 + +对 openkal-linux(本波分支),在**带尺寸的伪终端**中运行: + +``` +observations: 199 held, 0 did not hold, 3 not observed +kal_version = 0xe0000 +openkal.terminal + held an interactive stream reports its mode + held setting the mode that was read succeeds + held the mode read back is the mode that was set + held an unassigned position in the mode word is not an error + held asking that every keystroke be passed on is not an error + held the position is either distinguished or reads as zero + held the mode the section found is the mode it leaves + held a reported display size is not zero in either dimension +``` + +在没有窗口尺寸的伪终端里,`a reported display size is not zero` 不成立——这是探测装置而不是实现:`pty.fork` 不设置窗口大小时,内核报告 0x0。 + +### 3.3 端口级(本波的核心证据) + +`examples/terminal` 在伪终端中,与同一份源码在宿主 C 库上的对照,去掉回车后逐行相同: + +``` +isatty 1 +tcgetattr rc=0 errno=0 +before lflag_icanon=1 lflag_echo=1 lflag_isig=1 iflag_ixon=1 vmin=1 vtime=0 +tcsetattr rc=0 errno=0 +tcgetattr(readback) rc=0 errno=0 +readback lflag_icanon=0 lflag_echo=0 lflag_isig=0 iflag_ixon=0 vmin=1 vtime=0 +reading +byte 0x61 +byte 0x62 +byte 0x03 +byte 0x71 +restored lflag_icanon=1 lflag_echo=1 lflag_isig=1 iflag_ixon=1 vmin=1 vtime=0 +done +``` + +对照 issue #36 里同一程序在本端口上的旧行为:`tcsetattr rc=-1 errno=25`,`before lflag=0x5a5a5a5a`(哨兵未被覆盖),以及 `ab^C` 之后 `Pane is dead (signal 2)`。 + +**比较时去掉回车,排除的是一件已记录的事**:openkal 的模式字管「键入」,不管「写出」,所以输出后处理(`OPOST`/`ONLCR`)在本端口的 `tcsetattr` 下不受影响,而宿主 C 库的 `cfmakeraw` 会清掉它。两份 transcript 因此每行差一个字节,其余完全一致。这条限制写进了 openkal-musl 的 README 限制表,并作为下一波的候选问题记在设计稿 §7 O2。 + +### 3.4 生态级 + +(待填:发布、index 注册、沙箱解析、真实 TUI 程序 `re-cloud-code` 的构建与运行。) + +## 4. 本波修掉的、与设计无关但同源的缺陷 + +- **`KAL_VERSION_MINOR` 停在 11**:整个生态的 `kal_version` 都答 0.11,条款 6.2 给 load 期绑定消费者的比较形同虚设。修正并加了检查。 +- **openkal-musl 的三处「报告成功却什么都没做」**:`TCGETS`、`TIOCGWINSZ` 不回写调用者结构,`SIG_IGN` 不安装。前两者由本波的 ioctl 路由一并解决,第三者改为「只接受已经生效的处置」。 +- **openkal-emscripten 的 absence 检查依赖 index**:它按本仓库清单里的版本号去 index 取规范,于是在提升版本的分支上必然失败,并把失败报成「链接没有提到 kal_process_spawn」。改为两个依赖都取工作树。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c94e952..27ad1f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -313,6 +313,39 @@ jobs: exit 1 } + # WHAT THE DECLARATIONS SAY THE VERSION IS, AND WHAT THE PACKAGE IS. + # + # `kal_version' is `return KAL_VERSION;' in every implementation there is, + # so the three constants in version.h are what the ecosystem states about + # itself and the only thing a consumer bound at load can ask. They had + # drifted through two releases, and the comparison clause 6.2 gives that + # consumer was between two equal numbers the whole time. + - name: The declarations state the version this package is + if: runner.os != 'Windows' + run: bash tools/check-version.sh + + # AND THE CHECK DETECTS WHAT IT EXISTS TO DETECT, for the reason the type + # check below is proved the same way: a check that reports success by + # finding nothing reports it identically when it has read nothing. + - name: The version check detects a drift + if: runner.os != 'Windows' + run: | + # `sed -i` IS NOT PORTABLE AND THIS MATRIX REACHES THE SYSTEM WHERE + # IT IS NOT: one of the three reads the next argument as the suffix + # for a backup file, so the edit is attempted upon a file named by + # the expression. Measured on this row, on the first run of this + # step. The edit is therefore written beside and moved over. + cp include/openkal/version.h /tmp/version.h.orig + sed 's/^#define KAL_VERSION_MINOR .*/#define KAL_VERSION_MINOR 11u/' \ + /tmp/version.h.orig > include/openkal/version.h + if bash tools/check-version.sh; then + cp /tmp/version.h.orig include/openkal/version.h + echo "the version check accepted a number the package is not" >&2 + exit 1 + fi + cp /tmp/version.h.orig include/openkal/version.h + echo "the version check rejects a drifted number, as it must" + - name: The module form compiles run: mcpp build diff --git a/README.md b/README.md index e358ea9..7420082 100644 --- a/README.md +++ b/README.md @@ -77,16 +77,16 @@ conditional on the target. ```toml [dependencies] -openkal = "0.13.0" +openkal = "0.14.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.13.0" +openkal-linux = "0.14.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.10.0" +openkal-macos = "0.11.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.8.0" +openkal-windows = "0.9.0" ``` The program imports the interface and names no implementation. diff --git a/SPEC.md b/SPEC.md index f005bc0..19e5683 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# openkal Specification, version 0.13 +# openkal Specification, version 0.14 ## 1. Scope @@ -50,7 +50,7 @@ provides an interface in whole or not at all. | `openkal.timeout` | a bound upon operations that would otherwise wait | optional | ✓ | ✓ | ✓ | | `openkal.event` | readiness of a set of resources | reserved | | | | -Version 0.13 specifies the core and optional interfaces. The reserved row is not +Version 0.14 specifies the core and optional interfaces. The reserved row is not specified, and its name shall not be used for other purposes. The S, L and X columns state which boundaries an interface's declarations can @@ -522,6 +522,16 @@ position that has not been assigned reads as zero, so that a program compiled against a later specification behaves correctly against an earlier implementation. +A position is therefore assigned the sense in which zero is the weaker claim. +An implementation that has never heard of a position answers with zero, and +that answer is a statement about the implementation which has to be true; a +position spelled the other way about would make the silence of every earlier +implementation assert the stronger thing. The rule is stated here because it +was recognised while assigning one: version 0.14 states whether an environment +reserves keystrokes for itself, and the spelling in which zero meant *reserves +none* would have had every implementation released before it claim a guarantee +that none of them provides. + A property that varies between the *resources* of an interface rather than between implementations cannot be a word, because there is no one answer to record in it. Such a property is reported by an enquiry taking the resource. @@ -1280,3 +1290,37 @@ The following are recorded so that they are not mistaken for oversights. with zero and does not require it; a caller that cannot tolerate the ambiguity reports the request as unsupported, which is what openkal-musl does. +20. **A keystroke the environment keeps for itself.** **Settled in 0.14.** + `KAL_TERM_PASS_CONTROL` states whether the environment reserves any + keystroke for an action of its own, and a program that reads what is typed + asks for the position and reads the mode back. Admitted on the grounds + entry 10 records for locking: every environment this specification has been + implemented on reserves such keystrokes and spells the switch almost + identically — `ISIG` and the flags beside it, `ENABLE_PROCESSED_INPUT` — so + what was missing was a word and not a capability. The admission test of + clause 7.11 is met by three ordinary programs that cannot be written above + the interface without it: an editor, a pager, and a prompt with line editing + of its own. + + **What its absence cost, and it was not a refusal.** A C environment above + this interface answers `tcsetattr` with the mode word. With no position for + the reserved keystrokes, a program that cleared line assembly and the echo + was still ended by the interrupt key, and no operation had misbehaved: the + implementation preserved what the specification had not named. Measured + against a host C library upon one terminal, and reported by the consumer + who met it. + + **The position is spelled in the sense clause 6.2 now states**, so an + implementation released before it reads as zero and thereby says something + true about itself. The arrangement in which zero would have been the + guarantee was considered and not adopted: it required an exception to + clause 6.2 and a property position beside the mode position, to repair a + hazard that the spelling itself removes. + + **What one position cannot record.** An environment may reserve several + classes of keystroke through separate mechanisms of its own, and a terminal + upon which some of them had been released is restored to the set the + environment ordinarily reserves. A position for each class was weighed and + declined: one of the three environments governs the classes with a single + switch, so the finer interface would have required of it a distinction it + does not have (clause 6.4). diff --git a/conformance/src/sections/terminal.cpp b/conformance/src/sections/terminal.cpp index 65ebb99..81ee749 100644 --- a/conformance/src/sections/terminal.cpp +++ b/conformance/src/sections/terminal.cpp @@ -85,6 +85,38 @@ void run() { kal_terminal_set_mode(out, original); } + // THE POSITION VERSION 0.14 ADDED, OBSERVED IN THE ONLY TERMS A SUITE HAS. + // + // Whether a keystroke arrives as a byte cannot be observed without a + // terminal somebody types at, and a run under continuous integration has + // none. What is observable is the pair: asking for the position is not an + // error, and what is read back is either the position or zero --- the two + // answers clause 6.2 permits, one from an implementation that distinguishes + // it and one from an implementation that does not. A third answer would + // mean a set that half took effect, which is the failure this interface can + // least afford. + { + const kal_uintptr pass = kal::terminal::pass_control.bits; + const int rc = kal_terminal_set_mode(out, original | pass); + observe(kind::behaviour, rc == kal_ok, + "asking that every keystroke be passed on is not an error"); + + kal_uintptr after = 0; + const int re = kal_terminal_get_mode(out, &after); + const kal_uintptr got = after & pass; + observe(kind::behaviour, re == kal_ok && (got == pass || got == 0), + "the position is either distinguished or reads as zero"); + + // AND THE TERMINAL IS PUT BACK, which is the other half of clause 7.11 + // and the half a reader of this suite depends upon: the position that + // was just asked for is the one that stops the interrupt key working. + kal_terminal_set_mode(out, original); + kal_uintptr back = 0; + observe(kind::behaviour, + kal_terminal_get_mode(out, &back) == kal_ok && back == original, + "the mode the section found is the mode it leaves"); + } + // The display size, where it is known. An environment that cannot ask // reports not_supported and leaves both outputs untouched, so the outputs // are pre-set to a value the operation would not produce. diff --git a/include/openkal/terminal.h b/include/openkal/terminal.h index e049759..bc24baf 100644 --- a/include/openkal/terminal.h +++ b/include/openkal/terminal.h @@ -27,9 +27,46 @@ * * A position that has not been assigned reads as zero, so a program compiled * against a later revision of this specification behaves correctly against an - * earlier implementation (clause 6.2). */ -#define KAL_TERM_LINE_EDIT ((kal_uintptr)1u << 0) /* the environment assembles lines */ -#define KAL_TERM_ECHO ((kal_uintptr)1u << 1) /* the environment shows what is typed */ + * earlier implementation (clause 6.2). A POSITION IS THEREFORE SPELLED IN THE + * SENSE IN WHICH ZERO IS THE WEAKER CLAIM: an implementation that has never + * heard of a position says something true about itself by reading as zero, and + * the stronger statement is one only an implementation that distinguishes the + * position can make. */ +#define KAL_TERM_LINE_EDIT ((kal_uintptr)1u << 0) /* the environment assembles lines */ +#define KAL_TERM_ECHO ((kal_uintptr)1u << 1) /* the environment shows what is typed */ +#define KAL_TERM_PASS_CONTROL ((kal_uintptr)1u << 2) /* the environment reserves no keystroke */ + +/* KAL_TERM_PASS_CONTROL IS A GUARANTEE IN ONE DIRECTION AND A PERMISSION IN THE + * OTHER. Set, every keystroke reaches the program as the bytes it produces, + * including the ones an environment ordinarily keeps for itself. Clear, the + * environment may reserve an agreed set of keystrokes for actions of its own. + * Which keystrokes those are belongs to the environment: an interface that + * required every environment to reserve the same ones would require of all of + * them what one of them happens to spell, which is the shape clause 7.1 + * excludes. + * + * A PROGRAM THAT READS KEYSTROKES CANNOT BE WRITTEN WITHOUT IT. openkal has no + * signals, so a program that has turned line assembly off and reads what is + * typed still cannot survive the keystroke its environment reserves for + * interruption --- the environment acts, and nothing above this interface can + * decline the action. Clearing KAL_TERM_LINE_EDIT does not imply this position: + * a program that performs line editing of its own turns line assembly off and + * keeps the interruption, and that combination is what every environment this + * specification has been implemented on spells natively. + * + * THE POSITION COVERS EVERY MECHANISM BY WHICH THE ENVIRONMENT KEEPS A + * KEYSTROKE, and not the interruption alone. An environment that released the + * interruption and kept the keystroke that stops output would leave the program + * unable to say what it needs, and would leave one mode word meaning two things + * on two environments. + * + * WHAT IT COSTS, RECORDED HERE SO THAT IT IS NOT DISCOVERED. One position + * cannot record which of several mechanisms an environment had already + * released. A terminal where some keystrokes were reserved and others were not + * reads as clear, and is restored to the set the environment ordinarily + * reserves. A position for each class of keystroke would record it, and would + * require of an environment whose single switch governs them together a + * distinction it does not have --- the defect clause 6.4 describes. */ /* Positions in the result of kal_terminal_props. */ #define KAL_TERM_PROP_MODE ((kal_uintptr)1u << 0) /* get_mode/set_mode are answered */ @@ -45,7 +82,23 @@ extern "C" { * the first and ignored by the second; neither is an error. An implementation * that distinguishes no position at all withholds the whole interface instead * (clause 6.1), so that a program discovers the absence at the link rather than - * at a call that reports success having done nothing. */ + * at a call that reports success having done nothing. + * + * A POSITION WHOSE REQUESTED VALUE IS THE ONE IN EFFECT SHALL NOT BE WRITTEN. + * Where a position stands for several of the environment's own mechanisms, + * establishing it again would settle mechanisms the caller did not ask about: + * a program turning the echo off would restore keystrokes its user had + * released. + * + * AND A MODE IS NOT A WAY TO END THE INPUT. An implementation that turns line + * assembly off shall not thereby cause a read of the stream to report zero + * while input has not ended: zero denotes end of input (clause 7.4), and a + * change of mode does not make it denote anything else. Where the environment + * states this as a least number of bytes a read waits for, the implementation + * establishes that number; the position says that the program reads + * keystrokes, not that it is willing to be told there are none. A program that + * wants a read which gives up asks for one: a bound upon waiting is stated by + * `openkal.timeout' and not by a mode. */ int kal_terminal_get_mode(struct kal_stream s, kal_uintptr* mode); int kal_terminal_set_mode(struct kal_stream s, kal_uintptr mode); diff --git a/include/openkal/version.h b/include/openkal/version.h index 4cb7529..d180e49 100644 --- a/include/openkal/version.h +++ b/include/openkal/version.h @@ -31,9 +31,18 @@ * consumer compares it with what `kal_version' answers and refuses to proceed * against an implementation older than the declarations it holds --- because an * older implementation reports conditions this consumer distinguishes as - * conditions it does not, which is a wrong answer rather than a refusal. */ + * conditions it does not, which is a wrong answer rather than a refusal. + * + * THE NUMBER IS CHECKED AGAINST THE PACKAGE, BECAUSE IT HAD DRIFTED. Every + * implementation answers `kal_version' with this constant, so a number left + * behind here is a number the whole ecosystem states: releases 0.12 and 0.13 + * each went out with 11 written above, and the comparison a consumer makes was + * therefore between two equal numbers on every implementation there is. + * tools/check-version.sh compares these three with the package's own manifest, + * for the reason tools/check-readme-versions.sh exists --- the one fact a + * consumer actually asks for was checked by nobody. */ #define KAL_VERSION_MAJOR 0u -#define KAL_VERSION_MINOR 11u +#define KAL_VERSION_MINOR 14u #define KAL_VERSION_PATCH 0u #define KAL_VERSION_MAKE(major, minor, patch) \ diff --git a/mcpp.toml b/mcpp.toml index 7b50344..b9e5bae 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal" -version = "0.13.0" +version = "0.14.0" description = "openkal: a portable kernel ABI specification. This package carries the normative declarations; implementations are separate packages." license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/src/macros.cppm b/src/macros.cppm index 99ff6a0..e8338ca 100644 --- a/src/macros.cppm +++ b/src/macros.cppm @@ -104,6 +104,7 @@ inline constexpr kal_uintptr KAL_TASK_PROP_THREAD_LOCAL_M = (kal_uintptr)( inline constexpr kal_uintptr KAL_TASK_PROP_WAIT_TIMEOUT_M = (kal_uintptr)(KAL_TASK_PROP_WAIT_TIMEOUT); inline constexpr kal_uintptr KAL_TERM_ECHO_M = (kal_uintptr)(KAL_TERM_ECHO); inline constexpr kal_uintptr KAL_TERM_LINE_EDIT_M = (kal_uintptr)(KAL_TERM_LINE_EDIT); +inline constexpr kal_uintptr KAL_TERM_PASS_CONTROL_M = (kal_uintptr)(KAL_TERM_PASS_CONTROL); inline constexpr kal_uintptr KAL_TERM_PROP_MODE_M = (kal_uintptr)(KAL_TERM_PROP_MODE); inline constexpr kal_uintptr KAL_TERM_PROP_SIZE_M = (kal_uintptr)(KAL_TERM_PROP_SIZE); inline constexpr kal_uintptr KAL_TIME_PROP_MONOTONIC_SUSPENDS_M = (kal_uintptr)(KAL_TIME_PROP_MONOTONIC_SUSPENDS); diff --git a/src/terminal.cppm b/src/terminal.cppm index 9cbd6ca..6ab6991 100644 --- a/src/terminal.cppm +++ b/src/terminal.cppm @@ -32,8 +32,13 @@ export namespace kal::terminal { struct mode_tag; using mode = kal::props; -inline constexpr mode line_edit{KAL_TERM_LINE_EDIT}; -inline constexpr mode echo {KAL_TERM_ECHO}; +inline constexpr mode line_edit {KAL_TERM_LINE_EDIT}; +inline constexpr mode echo {KAL_TERM_ECHO}; +// Set, every keystroke reaches the program, including the ones the environment +// would otherwise keep for itself; clear, the environment may reserve a set of +// its own choosing. The position is spelled so that zero is the weaker claim, +// which is what lets an implementation that predates it answer honestly. +inline constexpr mode pass_control{KAL_TERM_PASS_CONTROL}; struct props_tag; using props = kal::props; diff --git a/tools/check-version.sh b/tools/check-version.sh new file mode 100755 index 0000000..1cff5ca --- /dev/null +++ b/tools/check-version.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# The version the declarations state must be the version the package is. +# +# check-version.sh [] +# +# WHAT THIS CHECKS AND WHY NOTHING ELSE DOES. `KAL_VERSION_MAJOR`, `_MINOR` and +# `_PATCH` in include/openkal/version.h are not documentation. Every conforming +# implementation answers `kal_version' with the constant they compose --- each +# one is literally `return KAL_VERSION;' --- so the number written here is the +# number the whole ecosystem states about itself, and it is the only number a +# consumer bound at load or across a boundary can ask for. Clause 3.2 names that +# consumer; clause 6.2 gives it the comparison to make. +# +# IT HAD DRIFTED, AND THE DRIFT WAS INVISIBLE. Releases 0.12.0 and 0.13.0 both +# went out with 11 written above. The comparison a consumer makes --- refuse an +# implementation older than the declarations I hold --- was therefore between +# two equal numbers on every implementation there is, and passed for the reason +# that neither side had moved. Nothing reported it: the surface is checked +# against SURFACE.txt, the declarations against both forms, the README's pins +# against the manifests, and this one fact against nobody. +# +# THE RULE IS THE SAME ONE tools/check-readme-versions.sh STATES. A version +# written in one file and true in another is a fact with two places to be +# wrong, so the two places are compared. Here the manifest is the authority, +# because it is what the index publishes and what a consumer resolves. +set -euo pipefail + +here="$(cd "${1:-$(dirname "${BASH_SOURCE[0]}")/..}" && pwd)" +header="$here/include/openkal/version.h" +manifest="$here/mcpp.toml" +[ -f "$header" ] || { echo "no include/openkal/version.h at $here" >&2; exit 2; } +[ -f "$manifest" ] || { echo "no mcpp.toml at $here" >&2; exit 2; } + +field() { # field --- the integer a KAL_VERSION_ define carries + sed -n "s/^#define *KAL_VERSION_$1 *\([0-9][0-9]*\)u\{0,1\}.*/\1/p" "$header" | head -1 +} + +major="$(field MAJOR)"; minor="$(field MINOR)"; patch="$(field PATCH)" +# A CHECK THAT READS NOTHING MUST NOT REPORT SUCCESS, which is the rule clause 9 +# states for the declaration checks and applies here for the same reason: a +# renamed macro would leave all three empty and every comparison vacuous. +for f in major minor patch; do + [ -n "${!f}" ] || { echo " no KAL_VERSION_${f^^} is not declared in version.h"; exit 1; } +done + +declared="$major.$minor.$patch" +packaged="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' "$manifest" | head -1)" +[ -n "$packaged" ] || { echo " no the manifest declares no version"; exit 1; } + +if [ "$declared" = "$packaged" ]; then + echo " ok the declarations state $declared, which is what this package is" + exit 0 +fi + +echo " no version.h states $declared and the package is $packaged" +echo " every implementation answers kal_version with the first of those," +echo " so a consumer comparing them is told the wrong thing about all of" +echo " them. Edit include/openkal/version.h to $packaged." +exit 1 diff --git a/tools/run-conformance.sh b/tools/run-conformance.sh index d848254..e300fd7 100755 --- a/tools/run-conformance.sh +++ b/tools/run-conformance.sh @@ -221,13 +221,23 @@ fi # afterwards is what was just produced. Only the binaries are removed, so the # rebuild is a link and not a compile; and finding more than one afterwards is # now a condition rather than a choice. -find target -type f \( -name 'openkal-conformance' -o -name 'openkal-conformance.exe' \) \ +# +# AND THE NAME OF WHAT WAS BUILT IS NOT ONE NAME. Two of the targets this suite +# is run for produce a program with a suffix: `.exe' on one system, and `.js' +# beside a `.wasm' on the Web, where the file the runner is handed is the first +# of the two. Measured 2026-09-20 on mcpp 2026.9.18.3, where the Web row +# reported "expected exactly one suite to have been produced, found 0" for a +# build that had just succeeded --- which names neither the cause nor anything +# a reader can act upon. +find target -type f \( -name 'openkal-conformance' -o -name 'openkal-conformance.exe' \ + -o -name 'openkal-conformance.js' \) \ -delete 2> /dev/null || true mcpp build --features "$features" "$@" mkdir -p target && printf '%s' "$want_stamp" > "$stamp" -produced="$(find target -type f \( -name 'openkal-conformance' -o -name 'openkal-conformance.exe' \))" +produced="$(find target -type f \( -name 'openkal-conformance' -o -name 'openkal-conformance.exe' \ + -o -name 'openkal-conformance.js' \))" count="$(printf '%s\n' "$produced" | grep -c . || true)" [ "$count" = 1 ] || { echo "expected exactly one suite to have been produced, found $count:" >&2