@calcit/procs 0.12.19 → 0.12.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/editing-history/202507161633-wasm-data-structures.md +61 -0
  3. package/editing-history/20260416-1936-predicate-narrowing-expansion.md +31 -0
  4. package/editing-history/202604161507-wasm-data-structures-and-rfc-rename.md +27 -0
  5. package/editing-history/202604161520-wasm-bitwise-and-match.md +21 -0
  6. package/editing-history/202604161542-wasm-cross-ns-host-imports.md +38 -0
  7. package/editing-history/20260417-0026-wasm-rest-args.md +62 -0
  8. package/editing-history/202604170048-wasm-type-of.md +70 -0
  9. package/editing-history/202604170051-wasm-derived-predicates.md +34 -0
  10. package/editing-history/202604170132-monomorphize-map-filter.md +50 -0
  11. package/editing-history/202604170135-monomorphize-includes-reverse.md +31 -0
  12. package/editing-history/202604170140-fold-type-predicates.md +44 -0
  13. package/editing-history/202604170154-generic-dispatch-records-tuples.md +52 -0
  14. package/lib/calcit.procs.mjs +39 -6
  15. package/lib/package.json +1 -1
  16. package/package.json +1 -1
  17. package/rfc/02-04-runtime-traits-plan.md +613 -0
  18. package/rfc/02-14-project-modernization-roadmap.md +229 -0
  19. package/rfc/02-17-register-platform-api-rfc.md +115 -0
  20. package/rfc/02-18-language-theory-evolution-plan.md +367 -0
  21. package/rfc/02-23-optional-record-macro-plan.md +30 -0
  22. package/rfc/03-05-function-schema-dual-track-rfc.md +162 -0
  23. package/rfc/03-16-runtime-boundary-refactor-plan.md +546 -0
  24. package/rfc/03-18-query-def-tree-show-chunked-display-plan.md +301 -0
  25. package/rfc/04-13-call-arg-literal-rewrite-rfc.md +205 -0
  26. package/rfc/04-13-type-slot-mechanism-rfc.md +194 -0
  27. package/rfc/04-15-match-syntax-rfc.md +175 -0
  28. package/rfc/04-15-type-directed-optimization-catalog.md +170 -0
  29. package/rfc/04-15-wasm-compilation-feasibility.md +236 -0
  30. package/rfc/04-16-wasm-data-structures.md +192 -0
  31. package/rfc/README.md +40 -0
  32. package/ts-src/calcit.procs.mts +33 -6
@@ -0,0 +1,301 @@
1
+ # `cr query def` / `cr tree show` 分片展示方案
2
+
3
+ ## 目标
4
+
5
+ 当定义或子树比较小时,继续保持当前直接输出的体验。
6
+
7
+ 当表达式足够大时,切换到“分片展示”模式,满足下面几点:
8
+
9
+ - 保留一个可快速阅读的根视图;
10
+ - 用占位符替换体积过大的子树;
11
+ - 按坐标顺序输出被拆出的片段;
12
+ - 允许通过参数调整每个片段期望容纳的节点数;
13
+ - 输出与文档优先使用点号路径,同时兼容逗号路径输入。
14
+
15
+ ## 选定案例
16
+
17
+ 本次以 `find-children-diffs` 作为主案例。
18
+
19
+ 原因:
20
+
21
+ - 它已经足够大,能明显暴露当前整块输出的可读性问题;
22
+ - 它内部天然存在 `cond`、嵌套 `let`、局部分支组等适合作为切分边界的结构;
23
+ - 它贴近真实工作流,既适合人阅读,也适合作为 LLM 分治理解的目标。
24
+
25
+ ## 对比结论
26
+
27
+ ### 基线:直接整块输出
28
+
29
+ 当前的 `cr query def` 会一次性打印完整定义。
30
+
31
+ 当前的 `cr tree show` 会打印目标子树,然后列出直接子节点路径。
32
+
33
+ 这个方案在小表达式上没有问题,但面对大表达式时会出现这些缺点:
34
+
35
+ - 主流程和局部细节混在一起,不容易先抓到整体结构;
36
+ - 重复性的局部绑定会占掉大量屏幕;
37
+ - 用户必须反复在 `search`、`show`、原始代码块之间切换;
38
+ - LLM 容易被附近细节吸住,反而丢掉更高层的分支关系。
39
+
40
+ ### 1 倍预算
41
+
42
+ 较早实验里,`find-children-diffs` 的目标片段大小大约是 `24` 个节点。
43
+
44
+ 这一版会被拆成 `14` 个片段,很多片段落在 `19-39` 节点之间。结构虽然有效,但整体仍然偏碎,不利于连续阅读。
45
+
46
+ ### 2 倍预算
47
+
48
+ 把目标片段大小提高到大约 `48` 个节点后,同一个案例会变成 `9` 个片段:
49
+
50
+ - `75, 57, 55, 53, 53, 42, 41, 39, 35`
51
+
52
+ 这一版更适合作为默认方案,原因是:
53
+
54
+ - 顶层控制流可以一眼看清;
55
+ - 局部初始化逻辑更容易整体保留;
56
+ - 片段数足够少,可以顺序浏览;
57
+ - 切分边界看起来更接近语义单元,而不是机械按大小切开。
58
+
59
+ ## 表达式拆分示例
60
+
61
+ 下面用一个简化的表达式说明“整块输出”和“分片输出”的区别。
62
+
63
+ 原始表达式:
64
+
65
+ ```cirru
66
+ let
67
+ a $ compute-a x
68
+ b $ compute-b x
69
+ cond
70
+ ready? a b
71
+ handle-ready a b
72
+ fallback?
73
+ let
74
+ details $ build-details a b
75
+ view $ render-view details
76
+ emit! view
77
+ track! details
78
+ true
79
+ let
80
+ err $ build-error a b
81
+ warn err
82
+ recover err
83
+ ```
84
+
85
+ 分片展示后,根片段会更像这样:
86
+
87
+ ```cirru
88
+ ROOT at root
89
+ let
90
+ a $ compute-a x
91
+ b $ compute-b x
92
+ cond
93
+ ready? a b
94
+ handle-ready a b
95
+ fallback?
96
+ {{let_emit_track_01}}@2.2
97
+ true
98
+ {{let_warn_recover_02}}@2.3
99
+ ```
100
+
101
+ 再继续列出拆出的片段:
102
+
103
+ ```cirru
104
+ {{let_emit_track_01}} at 2.2
105
+ let
106
+ details $ build-details a b
107
+ view $ render-view details
108
+ emit! view
109
+ track! details
110
+
111
+ {{let_warn_recover_02}} at 2.3
112
+ let
113
+ err $ build-error a b
114
+ warn err
115
+ recover err
116
+ ```
117
+
118
+ 这样做的好处是:
119
+
120
+ - 根片段只保留主流程;
121
+ - 细节块被移动到后面,阅读顺序更清晰;
122
+ - 每个占位符都带精确坐标,仍然能回到原树上继续定位或编辑。
123
+
124
+ ## 决策
125
+
126
+ 默认采用“递归语义切分”作为大表达式的展示策略。
127
+
128
+ 更具体地说:
129
+
130
+ - 以现有递归语义启发式为基础;
131
+ - 只有当表达式超过某个可配置阈值时才启用分片;
132
+ - 默认预算采用比早期实验更宽松的配置;
133
+ - 保留 `--raw` 作为脚本和精确检查时的逃生口。
134
+
135
+ ## 预期交互
136
+
137
+ ### `cr query def`
138
+
139
+ 对于较小定义:
140
+
141
+ - 保持现在的直接输出。
142
+
143
+ 对于较大定义:
144
+
145
+ - 元信息照常输出;
146
+ - 用 `Chunked Cirru:` 替代一整块超长 Cirru;
147
+ - 先打印根片段,再按坐标顺序打印拆出的片段;
148
+ - 每个片段展示:坐标、节点数、最大深度、Cirru 内容。
149
+
150
+ ### `cr tree show`
151
+
152
+ 对于较小子树:
153
+
154
+ - 保持现在的输出形状。
155
+
156
+ 对于较大子树:
157
+
158
+ - 先展示节点类型和子树统计;
159
+ - 再展示分片后的根预览;
160
+ - 后续继续列出拆出的片段;
161
+ - 子节点导航提示仍然基于原始树路径;
162
+ - 用户可以直接根据片段坐标继续向下钻取。
163
+
164
+ ## CLI 参数设计
165
+
166
+ 这些参数应同时提供给 `cr query def` 和 `cr tree show`:
167
+
168
+ - `--chunk-target-nodes <n>`:每个片段理想节点数;
169
+ - `--chunk-max-nodes <n>`:停止继续细分前允许的软上限;
170
+ - `--chunk-trigger-nodes <n>`:总节点数低于该值时不启用分片;
171
+ - `--raw`:强制回退到旧的整块输出。
172
+
173
+ 当前建议默认值:
174
+
175
+ - `chunk-target-nodes = 56`
176
+ - `chunk-max-nodes = 68`
177
+ - `chunk-trigger-nodes = 88`
178
+
179
+ 原因:
180
+
181
+ - `target` 需要足够大,避免过度切碎;
182
+ - `max` 略高于 `target`,避免为了几颗节点继续无意义细分;
183
+ - `trigger` 需要过滤掉本来还能整体阅读的中等表达式。
184
+
185
+ ## 路径方案
186
+
187
+ ### 输出格式
188
+
189
+ 所有新的展示输出统一优先使用点号路径,例如:
190
+
191
+ - `3.2.4.1.1`
192
+
193
+ 这样和前面的分片研究保持一致,也更方便人和 LLM 直接复制。
194
+
195
+ ### 输入兼容
196
+
197
+ 路径输入保持两种写法都支持:
198
+
199
+ - 逗号:`3,2,4,1,1`
200
+ - 点号:`3.2.4.1.1`
201
+
202
+ 规则:
203
+
204
+ - 空字符串仍表示根节点;
205
+ - 同一路径里混用点号和逗号时直接报错,避免静默歧义;
206
+ - 文档只主推点号写法,逗号作为兼容行为说明一次即可。
207
+
208
+ ## 内部实现计划
209
+
210
+ ### 1. 提取可复用的分片模块
211
+
212
+ 把展示侧的切分逻辑从实验脚本迁移到 Rust,形成独立的 CLI 辅助模块。
213
+
214
+ 职责包括:
215
+
216
+ - 计算节点统计信息;
217
+ - 收集候选子树;
218
+ - 基于递归语义策略选择切点;
219
+ - 生成占位符;
220
+ - 输出有序分解结果;
221
+ - 统一格式化路径。
222
+
223
+ ### 2. 第一阶段只做展示层改造
224
+
225
+ 第一阶段不修改 snapshot 存储、不修改 AST 语义、不修改编辑行为。
226
+
227
+ 仅让 `query def` 与 `tree show` 使用新的分片渲染。
228
+
229
+ ### 3. 集中处理路径兼容
230
+
231
+ 扩展公共路径解析逻辑,让各 CLI handler 都能接受点号路径,而不是每个命令各自手写兼容。
232
+
233
+ 同时增加统一的路径格式化函数,避免到处手写 `join(",")` 或 `join(".")`。
234
+
235
+ ### 4. 更新命令处理逻辑
236
+
237
+ `handle_def`:
238
+
239
+ - 先计算定义总节点数;
240
+ - 根据阈值决定直接输出还是分片输出;
241
+ - 保持 `--json` 行为不变。
242
+
243
+ `handle_show`:
244
+
245
+ - 先计算目标子树节点数;
246
+ - 根据阈值决定直接输出还是分片输出;
247
+ - 保持子节点导航提示和替换提示。
248
+
249
+ ### 5. 文档迁移
250
+
251
+ 把文档示例统一调整为优先使用点号路径,例如:
252
+
253
+ ```bash
254
+ cr tree show ns/def -p '3.2.1'
255
+ ```
256
+
257
+ 逗号兼容只在合适位置说明一次,不重复铺满全文。
258
+
259
+ ## 风险与应对
260
+
261
+ ### 风险:展示过于“魔法化”
262
+
263
+ 应对:
264
+
265
+ - 保留 `--raw`;
266
+ - 明确打印分片统计信息;
267
+ - 每个片段都保留精确坐标。
268
+
269
+ ### 风险:点号路径与命名空间里的点冲突
270
+
271
+ 应对:
272
+
273
+ - 点号/逗号解析只作用于路径参数;
274
+ - 不把这套解析逻辑用于 `ns/def` 目标字符串。
275
+
276
+ ### 风险:分片展示影响编辑工作流
277
+
278
+ 应对:
279
+
280
+ - 分片只影响展示,不影响数据结构;
281
+ - 真正的编辑命令仍然对原始树坐标生效。
282
+
283
+ ## 推进顺序
284
+
285
+ 1. 先写这份方案文档。
286
+ 2. 在 Rust 中加入可复用的分片与路径辅助逻辑。
287
+ 3. 把分片渲染接入 `cr query def`。
288
+ 4. 把分片渲染接入 `cr tree show`。
289
+ 5. 加入点号路径兼容。
290
+ 6. 更新文档,主推点号路径。
291
+ 7. 用 `find-children-diffs` 做真实案例验证,并微调默认值。
292
+
293
+ ## 验收标准
294
+
295
+ - `cr query def` 在大定义上默认输出分片结果;
296
+ - `cr tree show` 在大子树上默认输出分片结果;
297
+ - 两个命令都支持 `--chunk-target-nodes`;
298
+ - 两个命令都接受点号路径;
299
+ - 逗号路径保持兼容;
300
+ - 文档示例优先使用点号路径;
301
+ - `--raw` 可以恢复旧的整块输出行为。
@@ -0,0 +1,205 @@
1
+ # RFC: 函数参数字面量自动改写 — Map-to-Record & Tuple-to-Enum
2
+
3
+ 状态:Implemented
4
+ 日期:2026-04-13
5
+
6
+ ---
7
+
8
+ ## 1. 概要
9
+
10
+ 在预处理阶段,当函数参数的 schema 标注引用了具体类型(struct 或 enum)时,允许调用方使用更简洁的字面量语法(hashmap `{}` 或 untyped tuple `::`),由预处理器自动改写为对应的类型化构造表达式(record `%{}` 或 enum tuple `%::`),以便后续类型检查正常工作。
11
+
12
+ ## 2. 动机
13
+
14
+ ### 问题
15
+
16
+ Calcit 区分"无类型字面量"和"有类型构造":
17
+
18
+ - `{} (:x 1) (:y 2)` 创建普通 hashmap,而 `%{} Point (:x 1) (:y 2)` 创建 record。
19
+ - `:: :ok` 创建普通 tuple(无 class 引用),而 `%:: Result0 :ok` 创建 enum tuple(有 class)。
20
+
21
+ 当函数签名明确标注参数类型时,调用者仍需手写完整的类型化构造:
22
+
23
+ ```cirru
24
+ ;; 参数标注为 Point record
25
+ defn sum-point (p) ...
26
+ :: :fn $ {} (:return :number)
27
+ :args $ [] 'app.main/Point
28
+
29
+ ;; 调用者必须写完整形式
30
+ sum-point $ %{} Point (:x 10) (:y 20)
31
+ ```
32
+
33
+ 这在大量调用点重复书写时既冗余又容易出错(特别是忘记加 `%` 前缀)。
34
+
35
+ ### 解法
36
+
37
+ 预处理器根据函数参数的 schema 类型标注,自动将简写形式改写为完整形式:
38
+
39
+ | 简写 | 改写为 | 触发条件 |
40
+ |---|---|---|
41
+ | `{} (:x 1) (:y 2)` | `%{} Point (:x 1) (:y 2)` | 参数类型为 struct/record |
42
+ | `:: :ok` | `%:: Result0 :ok` | 参数类型为 enum |
43
+
44
+ 改写后的 AST 能正常参与:
45
+ - 运行时类型验证(field 校验、variant 校验)
46
+ - 预处理阶段类型检查(`check_user_fn_arg_types`)
47
+ - JS codegen(通过 Import 引用而非内联 Struct/Enum 值)
48
+
49
+ ## 3. 设计
50
+
51
+ ### 3.1 Map-to-Record 改写
52
+
53
+ **触发条件:**
54
+
55
+ 1. 函数 schema 的 `:args` 中某位置引用了 struct 类型(`TypeRef`、`Struct` 或 `Record`)
56
+ 2. 调用处该位置是 hashmap 字面量(以 `NativeMap` proc 开头的 list)
57
+ 3. hashmap 所有 key 都是 tag,且都是目标 struct 的合法字段
58
+
59
+ **改写规则:**
60
+
61
+ - `[NativeMap, :k1, v1, :k2, v2]` → `[NativeRecord, struct_ref, :field1, v1_or_nil, :field2, v2_or_nil, ...]`
62
+ - struct 中未提供的字段自动填充 `nil`
63
+ - 字段顺序按 struct 定义排列(非 map 出现顺序)
64
+ - `struct_ref` 优先使用 `Calcit::Import`(带 ns/def 路径),以兼容 JS codegen
65
+
66
+ **跳过条件(不报错,保持原样):**
67
+
68
+ - map 中含有非 tag 的 key
69
+ - map 中含有 struct 不存在的字段名
70
+ - 无法从 schema 解析出 struct 定义
71
+
72
+ **示例:**
73
+
74
+ ```cirru
75
+ defstruct Point (:x :number) (:y :number)
76
+
77
+ defn sum-point (p)
78
+ :: :fn $ {} (:return :number)
79
+ :args $ [] 'app.main/Point
80
+ &+ (:x p) (:y p)
81
+
82
+ ;; 简写
83
+ sum-point $ {} (:x 10) (:y 20)
84
+ ;; 预处理改写为
85
+ sum-point $ %{} Point (:x 10) (:y 20)
86
+ ```
87
+
88
+ ### 3.2 Tuple-to-Enum 改写
89
+
90
+ **触发条件:**
91
+
92
+ 1. 函数 schema 的 `:args` 中某位置引用了 enum 类型(`TypeRef`、`Enum` 或 `Tuple`)
93
+ 2. 调用处该位置是 untyped tuple 字面量(以 `NativeTuple` proc 开头的 list)
94
+
95
+ **改写规则:**
96
+
97
+ - `[NativeTuple, :tag, payload...]` → `[NativeEnumTupleNew, enum_ref, :tag, payload...]`
98
+ - `enum_ref` 优先使用 `Calcit::Import`(带 ns/def 路径),以兼容 JS codegen
99
+ - 不验证 tag 或 payload(由后续 `check_enum_tuple_construction` 完成)
100
+
101
+ **跳过条件(不报错,保持原样):**
102
+
103
+ - tuple 字面量没有 tag(空 `::` 表达式)
104
+ - 无法从 schema 解析出 enum 定义
105
+
106
+ **示例:**
107
+
108
+ ```cirru
109
+ defenum Result0 (:err :string) (:ok)
110
+
111
+ defn takes-result (r)
112
+ :: :fn $ {} (:return :dynamic)
113
+ :args $ [] 'app.main/Result0
114
+ tag-match r ((:ok) :ok) ((:err msg) msg) $ _ :unknown
115
+
116
+ ;; 简写
117
+ takes-result $ :: :ok
118
+ ;; 预处理改写为
119
+ takes-result $ %:: Result0 :ok
120
+
121
+ ;; 带 payload
122
+ takes-result $ :: :err |error-msg
123
+ ;; 预处理改写为
124
+ takes-result $ %:: Result0 :err |error-msg
125
+ ```
126
+
127
+ ## 4. 实现
128
+
129
+ ### 4.1 类型解析
130
+
131
+ 在 `CalcitTypeAnnotation` 上新增方法:
132
+
133
+ | 方法 | 用途 |
134
+ |---|---|
135
+ | `resolve_to_struct_with_ref()` | 解析 struct + 可选 (ns, def) 路径(已有) |
136
+ | `resolve_to_enum_with_ref()` | 解析 enum + 可选 (ns, def) 路径(新增) |
137
+
138
+ 两者都处理 `Struct/Record`↔`Enum/Tuple` 直接值、`TypeRef("ns/def")` 程序查找、以及 `Optional(inner)` 解包。
139
+
140
+ 底层依赖:
141
+
142
+ | 函数 | 用途 |
143
+ |---|---|
144
+ | `resolve_struct_from_program(ns, def)` | 从 program registry 查找 struct 定义(已有) |
145
+ | `resolve_enum_from_program(ns, def)` | 从 program registry 查找 enum 定义(新增) |
146
+
147
+ ### 4.2 改写函数
148
+
149
+ | 函数 | 用途 |
150
+ |---|---|
151
+ | `try_rewrite_map_args_to_records()` | 遍历参数列表,对 map 字面量尝试改写(已有) |
152
+ | `try_rewrite_single_map_to_record()` | 单个参数的 map→record 改写(已有) |
153
+ | `try_rewrite_tuple_args_to_enum_tuples()` | 遍历参数列表,对 tuple 字面量尝试改写(新增) |
154
+ | `try_rewrite_single_tuple_to_enum_tuple()` | 单个参数的 tuple→enum-tuple 改写(新增) |
155
+
156
+ ### 4.3 集成点
157
+
158
+ 在 `preprocess_list_call()` 的 `Fn` 分支中,按顺序调用:
159
+
160
+ 1. `try_rewrite_map_args_to_records()` — map → record
161
+ 2. `try_rewrite_tuple_args_to_enum_tuples()` — tuple → enum tuple
162
+ 3. `check_core_fn_arg_types()` — 内建函数参数类型检查
163
+ 4. `check_user_fn_arg_types()` — 用户函数参数类型检查
164
+
165
+ 两次改写串联:第一次的输出作为第二次的输入。
166
+
167
+ ### 4.4 JS Codegen 兼容
168
+
169
+ 改写时若从 `TypeRef` 解析出 (ns, def) 路径,会构造 `Calcit::Import` 而非内联的 `Calcit::Struct`/`Calcit::Enum`。这是因为 JS codegen 不支持直接 emit `Struct`/`Enum` 字面量——它需要一个变量引用。
170
+
171
+ Import 策略:
172
+ - 同 namespace → `ImportInfo::SameFile`
173
+ - 跨 namespace → `ImportInfo::NsReferDef`
174
+
175
+ ### 修改的文件
176
+
177
+ | 文件 | 变更 |
178
+ |---|---|
179
+ | `src/calcit/type_annotation.rs` | `resolve_to_enum_with_ref()`, `resolve_enum_from_program()` |
180
+ | `src/runner/preprocess.rs` | `try_rewrite_tuple_args_to_enum_tuples()`, `try_rewrite_single_tuple_to_enum_tuple()`,集成到 Fn 分支 |
181
+ | `docs/features/enums.md` | 新增 "Automatic Tuple-to-Enum Rewrite" 章节 |
182
+ | `docs/features/records.md` | "Automatic Map-to-Record Rewrite" 章节(已有) |
183
+ | `calcit/test-enum.cirru` | 新增 `test-tuple-to-enum` 测试 |
184
+
185
+ ## 5. 测试覆盖
186
+
187
+ ### Map-to-Record(已有)
188
+
189
+ - `calcit/test-record.cirru` 中 `test-map-to-record`
190
+ - `sum-point $ {} (:x 10) (:y 20)` → 验证返回值正确
191
+ - `check-point-type $ {} (:x 10) (:y 20)` → 验证改写后值为 record 类型
192
+
193
+ ### Tuple-to-Enum(新增)
194
+
195
+ - `calcit/test-enum.cirru` 中 `test-tuple-to-enum`
196
+ - `takes-result $ :: :ok` → 验证 tag-match 匹配
197
+ - `takes-result $ :: :err |error-msg` → 验证 payload 传递
198
+ - `check-result-type $ :: :ok` → 验证改写后值有 enum origin
199
+
200
+ ## 6. 局限性
201
+
202
+ - **仅作用于直接字面量**:传递变量(即使值是 map/tuple)不会触发改写,因为预处理阶段无法确定变量的运行时值。
203
+ - **不验证内容**:map-to-record 验证 key 是否为合法字段,tuple-to-enum 不验证 tag 和 payload(由后续的 `check_enum_tuple_construction` 完成)。
204
+ - **无法逆向**:改写后的 AST 无法区分是用户手写的 `%{}` / `%::` 还是改写生成的。调试时看到的都是改写后的形式。
205
+ - **JS codegen 依赖**:如果类型注解是直接的 `Struct(def)` / `Enum(def)` 而非 `TypeRef("ns/def")`,改写会内联结构体/枚举值,在 JS codegen 中可能 panic(目前通过优先使用 TypeRef 规避)。
@@ -0,0 +1,194 @@
1
+ # RFC: Type Slot 机制 — 库-应用间的类型注入
2
+
3
+ 状态:Draft
4
+ 日期:2025-07-13
5
+
6
+ ---
7
+
8
+ ## 1. 概要
9
+
10
+ 实现 `deftype-slot` / `bind-type` proc 对,允许库代码声明类型占位符(slot),应用代码在启动时绑定具体类型(enum/struct/record)。预处理阶段自动解析 slot 引用,使跨包边界的类型检查成为可能。
11
+
12
+ ## 2. 动机
13
+
14
+ ### 问题
15
+
16
+ Calcit 的静态类型分析在单包内工作良好:`defenum` 定义的变体名、载荷数量和类型都能在预处理阶段被检查。但当**库**(如 Respo)定义回调签名时,它无法知道**应用**层会使用哪个 enum 作为 dispatch 操作类型。
17
+
18
+ 以 Respo 的 `EventHandler` 为例:
19
+
20
+ ```cirru
21
+ ;; respo.schema — 库代码
22
+ ;; dispatch 回调的签名原先只能标注为 :tuple(即 Dynamic)
23
+ :: :fn $ {} (:return :unit)
24
+ :args $ [] '*dispatch-op
25
+ ```
26
+
27
+ 应用层定义了自己的 Op enum:
28
+
29
+ ```cirru
30
+ ;; app.schema
31
+ defenum Op (:add :string) (:remove :tag) (:toggle :map) (:clear) ...
32
+ ```
33
+
34
+ 没有 type-slot 机制时,`d! (: add ...)` 这类调用无法被类型检查,因为预处理器不知道 dispatch 参数应该是 `Op` 类型。
35
+
36
+ ### 为什么不用泛型
37
+
38
+ Calcit 目前没有完整的泛型(type parameter)系统。引入泛型会大幅增加语言复杂性,而 type-slot 解决的是一个更窄的问题:**跨编译单元的单一类型注入**。它更像 dependency injection 而非 parametric polymorphism。
39
+
40
+ ## 3. 设计
41
+
42
+ ### 核心概念
43
+
44
+ | 概念 | 说明 |
45
+ |---|---|
46
+ | **Type Slot** | 一个命名占位符,声明时值为 `None`,绑定后值为具体类型注解 |
47
+ | `deftype-slot :name` | 在库代码中声明 slot(通常放在 schema 命名空间) |
48
+ | `bind-type :name ConcreteType` | 在应用入口绑定具体类型(通常放在 `main!` 函数体) |
49
+ | `*name` | 在 schema 类型标注中引用 slot(解析为 `TypeSlot(name)`) |
50
+
51
+ ### 生命周期
52
+
53
+ ```
54
+ 声明 (deftype-slot) → 绑定 (bind-type) → 解析 (*name 引用) → 类型检查
55
+ ↑ 库代码 ↑ 应用入口 ↑ 预处理阶段 ↑ 预处理阶段
56
+ ```
57
+
58
+ 1. **声明**:`deftype-slot :dispatch-op` 在 `TYPE_SLOTS` 注册表中插入 `("dispatch-op", None)`。
59
+ 2. **绑定**:`bind-type :dispatch-op Op` 将 slot 值设为 `Some(Enum(Op, []))`。绑定发生在预处理阶段(通过 `resolve_program_value_for_preprocess` 提前求值),确保后续同一编译 pass 的类型检查能立即使用。
60
+ 3. **解析**:当类型标注遇到 `*dispatch-op` 时,`resolve_type_slot("dispatch-op")` 返回绑定的 `Enum(Op, [])`。
61
+ 4. **检查**:解析后的类型委托给标准的 `matches_with_bindings` / `value_matches_type_annotation` 进行检查。
62
+
63
+ ### 约束
64
+
65
+ - 每个 slot 只能声明一次(重复声明报错)。
66
+ - 每个 slot 只能绑定一次(重复绑定报错)。预处理阶段已完成绑定后,运行时再次执行 `bind-type` 会静默跳过(no-op),不会重复报错。
67
+ - 未绑定的 slot 在类型检查时等同于 `:dynamic`(不报错但不检查)。
68
+ - 绑定必须是 enum、struct 或 record 类型。
69
+
70
+ ## 4. API 参考
71
+
72
+ ### `deftype-slot`
73
+
74
+ 声明一个类型占位符。
75
+
76
+ ```cirru
77
+ deftype-slot :dispatch-op
78
+ ```
79
+
80
+ - **参数**:1 个 tag 或 string,作为 slot 名称。
81
+ - **返回**:`nil`
82
+ - **副作用**:在全局 `TYPE_SLOTS` 注册表中注册 slot。
83
+
84
+ ### `bind-type`
85
+
86
+ 将具体类型绑定到已声明的 slot。
87
+
88
+ ```cirru
89
+ bind-type :dispatch-op Op
90
+ ```
91
+
92
+ - **参数**:
93
+ 1. tag 或 string — slot 名称(必须已通过 `deftype-slot` 声明)。
94
+ 2. enum / struct / record 定义值。
95
+ - **返回**:`nil`
96
+ - **副作用**:将类型绑定写入 `TYPE_SLOTS`。
97
+ - **错误**:slot 未声明、slot 已绑定、第二参数类型不对。
98
+
99
+ ### `*name` 类型引用语法
100
+
101
+ 在 schema 类型标注中使用 `*name` 引用 slot:
102
+
103
+ ```cirru
104
+ ;; 在 EventHandler 的 schema 中
105
+ :args $ [] '*dispatch-op
106
+ ```
107
+
108
+ Cirru EDN 序列化为 `'*dispatch-op`(`'` 是 EDN symbol 前缀,`*` 是 type-slot 标记)。
109
+
110
+ ## 5. 使用示例
111
+
112
+ ### Respo EventHandler 场景
113
+
114
+ **库端(respo.schema):**
115
+
116
+ ```cirru
117
+ ;; 声明 slot
118
+ deftype-slot :dispatch-op
119
+
120
+ ;; EventHandler schema 引用 slot
121
+ :: :fn $ {} (:return :unit)
122
+ :args $ [] 'respo.schema/RespoEvent
123
+ :: :fn $ {} (:return :unit)
124
+ :args $ [] '*dispatch-op
125
+ ```
126
+
127
+ **应用端(app.main):**
128
+
129
+ ```cirru
130
+ ;; app.schema 定义 Op enum
131
+ defenum Op
132
+ :add :string
133
+ :remove :tag
134
+ :toggle :map
135
+ :update :tag :string
136
+ :clear
137
+ :states-merge :any :any :any
138
+
139
+ ;; main! 中绑定
140
+ defn main! ()
141
+ bind-type :dispatch-op Op
142
+ ;; ... 后续代码
143
+ ```
144
+
145
+ **效果**:
146
+
147
+ ```cirru
148
+ ;; ✅ 正确 — 编译通过
149
+ d! $ %:: Op :toggle (:id task)
150
+
151
+ ;; ❌ 错误变体名 — 预处理警告 "does not have variant :delete"
152
+ d! $ %:: Op :delete (:id task)
153
+
154
+ ;; ❌ 载荷数量错 — 预处理警告 "expects 1 payload(s), got 2"
155
+ d! $ %:: Op :clear 42
156
+
157
+ ;; ❌ 载荷类型错 — 预处理警告 "expects :string, got :number"
158
+ d! $ %:: Op :add 42
159
+ ```
160
+
161
+ ## 6. 实现细节
162
+
163
+ ### 修改的文件
164
+
165
+ | 文件 | 变更 |
166
+ |---|---|
167
+ | `src/calcit/type_annotation.rs` | `TYPE_SLOTS` 注册表, `TypeSlot` 变体, slot 解析/匹配/序列化 |
168
+ | `src/calcit/proc_name.rs` | `DeftypeSlot` / `BindType` proc 名称 |
169
+ | `src/builtins.rs` | dispatch arms |
170
+ | `src/builtins/meta.rs` | `deftype_slot()` / `bind_type()` 实现 |
171
+ | `src/calcit.rs` | re-export `register_type_slot`, `bind_type_slot` |
172
+ | `src/runner.rs` | `clear_type_slots()` 在程序启动时调用(避免跨次运行残留) |
173
+ | `src/runner/preprocess.rs` | 预处理阶段提前执行 `deftype-slot` / `bind-type` |
174
+
175
+ ### 关键实现点
176
+
177
+ 1. **Thread-local 注册表**:`TYPE_SLOTS` 使用 `thread_local! { RefCell<HashMap<...>> }`,因为 `HashMap::new()` 不是 const fn,不能用 `const { ... }` 初始化。
178
+
179
+ 2. **预处理时绑定**:`bind-type` 在预处理阶段通过 `resolve_program_value_for_preprocess()` 提前求值,确保同一编译 pass 内类型检查可以立即使用绑定结果。这是整个机制的关键——如果在运行时才绑定,预处理阶段的类型检查无法看到具体类型。
180
+
181
+ 3. **类型匹配委托**:`TypeSlot` 在 `matches_with_bindings` 和 `value_matches_type_annotation` 中解析后直接委托给标准匹配逻辑,不引入新的匹配分支。
182
+
183
+ 4. **序列化**:`TypeSlot(name)` 序列化为 `Edn::Symbol("*name")`,反序列化时 `*` 前缀触发 `TypeSlot` 解析。
184
+
185
+ ## 7. 局限性与未来方向
186
+
187
+ - **单绑定约束**:每个 slot 只能绑定一次。如果需要一个库支持多个不同 dispatch 类型或一个项目中有多个不同的 EventHandler,需要声明多个 slot。
188
+ - **无运行时效果**:`deftype-slot` 和 `bind-type` 在运行时是无操作的(返回 nil),它们的作用完全在预处理阶段。
189
+ - **仅支持 enum/struct/record**:不能绑定基础类型(如 `:number`)到 slot,因为 slot 的主要场景是复合类型注入。
190
+ - **未来可扩展**:如果未来 Calcit 引入泛型或 trait 约束,type-slot 可以作为特化机制的基础。
191
+
192
+ ## 8. 从 editing-history 迁移说明
193
+
194
+ 本提案内容源自 `editing-history/202507131553-type-slot-mechanism.md`,已扩充为完整 RFC 格式。原始文件可在合并后删除。