@calcit/procs 0.12.35 → 0.12.37
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.
- package/.yarn/install-state.gz +0 -0
- package/README.md +44 -40
- package/{rfc → RFCs}/03-05-function-schema-dual-track-rfc.md +11 -12
- package/RFCs/05-31-generic-where-bounds-mfs.md +124 -0
- package/RFCs/06-01-generic-binding-unification-rfc.md +608 -0
- package/{rfc → RFCs}/README.md +1 -0
- package/build.rs +74 -1
- package/editing-history/2026-0526-0957-match-docs-check-md.md +24 -0
- package/editing-history/2026-0601-0003-update-callback-field-specialization.md +19 -0
- package/editing-history/2026-0601-0005-release-0.12.36.md +16 -0
- package/editing-history/2026-0601-2230-enum-struct-payload-docs.md +4 -0
- package/editing-history/2026-0601-2317-data-definition-where-bounds.md +6 -0
- package/editing-history/2026-0602-0114-data-definition-where-macro-followup.md +21 -0
- package/editing-history/202605210055-tree-show-cr-config-and-edit-cleanup.md +81 -0
- package/editing-history/202605312052-macro-schema-roundtrip-and-weak-types.md +6 -0
- package/editing-history/202605312059-ci-docs-test-and-fn-helper-tightening.md +3 -0
- package/editing-history/202605312105-map-generic-weak-types-followup.md +15 -0
- package/lib/calcit.procs.mjs +60 -3
- package/lib/js-record.mjs +1 -1
- package/lib/package.json +1 -1
- package/package.json +1 -1
- package/ts-src/calcit.procs.mts +65 -4
- package/ts-src/js-record.mts +1 -1
- /package/{rfc → RFCs}/02-04-runtime-traits-plan.md +0 -0
- /package/{rfc → RFCs}/02-14-project-modernization-roadmap.md +0 -0
- /package/{rfc → RFCs}/02-17-register-platform-api-rfc.md +0 -0
- /package/{rfc → RFCs}/02-18-language-theory-evolution-plan.md +0 -0
- /package/{rfc → RFCs}/02-23-optional-record-macro-plan.md +0 -0
- /package/{rfc → RFCs}/03-16-runtime-boundary-refactor-plan.md +0 -0
- /package/{rfc → RFCs}/03-18-query-def-tree-show-chunked-display-plan.md +0 -0
- /package/{rfc → RFCs}/04-13-call-arg-literal-rewrite-rfc.md +0 -0
- /package/{rfc → RFCs}/04-13-type-slot-mechanism-rfc.md +0 -0
- /package/{rfc → RFCs}/04-15-match-syntax-rfc.md +0 -0
- /package/{rfc → RFCs}/04-15-type-directed-optimization-catalog.md +0 -0
- /package/{rfc → RFCs}/04-15-wasm-compilation-feasibility.md +0 -0
- /package/{rfc → RFCs}/04-16-wasm-data-structures.md +0 -0
- /package/{rfcs → RFCs}/05-12-program-diff-rfc.md +0 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
# 命名类型泛型绑定统一 RFC
|
|
2
|
+
|
|
3
|
+
日期:2026-06-01
|
|
4
|
+
|
|
5
|
+
状态:Draft
|
|
6
|
+
|
|
7
|
+
## 背景
|
|
8
|
+
|
|
9
|
+
当前类型系统已经支持:
|
|
10
|
+
|
|
11
|
+
- 函数级泛型变量 `TypeVar`
|
|
12
|
+
- `:where` trait 约束
|
|
13
|
+
- `Struct` / `Enum` / `TypeRef` 三类命名类型表示
|
|
14
|
+
- 在 `matches_with_bindings` 中边匹配边收集泛型绑定
|
|
15
|
+
|
|
16
|
+
但是这条链路还不够统一。
|
|
17
|
+
|
|
18
|
+
基于当前版本,底层状态已经比最初起草这份 RFC 时更进一步:
|
|
19
|
+
|
|
20
|
+
- `CalcitStruct.generics` 已参与 `Struct <-> Struct` 与 `Struct <-> TypeRef` 的单边 applied 绑定
|
|
21
|
+
- `CalcitEnum` 现在也已经保存 `generics` 元数据
|
|
22
|
+
- `&enum::new` 已支持泛型参数列表,enum payload 里的类型变量可以稳定保留下来
|
|
23
|
+
- `defstruct Box ([] 'T) (:value 'T)` 这一类表层 struct 泛型声明已经能直接运行
|
|
24
|
+
|
|
25
|
+
这意味着 RFC 的关注点可以收窄成两部分:
|
|
26
|
+
|
|
27
|
+
- 先把已经具备元数据的路径补成对称行为
|
|
28
|
+
- 再处理确实仍需 schema lookup 的 `TypeRef <-> TypeRef`
|
|
29
|
+
|
|
30
|
+
最明显的缺口是:当两个命名类型本质上表示同一个定义,但只有一侧携带了已应用的泛型参数时,绑定行为并不一致。
|
|
31
|
+
|
|
32
|
+
- `Struct(Applied)` 对 `Struct(Bare)`:当前已经会把已应用参数绑定回声明的泛型变量。
|
|
33
|
+
- `TypeRef(Applied)` 对 `TypeRef(Bare)`:当前基本仍是宽松通过,不一定留下可用绑定。
|
|
34
|
+
- `Struct(Applied)` 对 `TypeRef(Bare)`:本轮已经补齐绑定。
|
|
35
|
+
- `Enum(Applied)` 对 `TypeRef(Bare)`:在新版之前缺失;现在底层元数据已经具备,可以直接补齐为与 struct 对称的绑定。
|
|
36
|
+
|
|
37
|
+
这会导致一个问题:调用点表面上“类型匹配成功”,但后续依赖绑定结果的能力并没有拿到足够信息,例如:
|
|
38
|
+
|
|
39
|
+
- 泛型返回类型特化不够稳定
|
|
40
|
+
- `:where` 约束检查可能看不到完整实参
|
|
41
|
+
- 不同命名类型组合的行为不一致,用户难以建立心智模型
|
|
42
|
+
|
|
43
|
+
## 目标
|
|
44
|
+
|
|
45
|
+
把“命名类型匹配时如何产生泛型绑定”统一成一条简单规则,尽量贴近 Rust:
|
|
46
|
+
|
|
47
|
+
- 先确认两侧是否是同一个命名类型
|
|
48
|
+
- 如果两侧都带泛型实参,则逐项统一
|
|
49
|
+
- 如果只有一侧带泛型实参,则把实参绑定回类型定义声明的泛型变量
|
|
50
|
+
- 匹配成功不应只是 `true`,还应尽可能留下后续阶段可复用的 bindings
|
|
51
|
+
|
|
52
|
+
这里的“贴近 Rust”不是复制 Rust 的完整 trait solver,而是借鉴它的一致性原则:
|
|
53
|
+
|
|
54
|
+
- 同一个类型构造器,在不同语法表面下应走同一套统一规则
|
|
55
|
+
- 泛型参数一旦可从已知实参恢复,就应该恢复,而不是跳过
|
|
56
|
+
|
|
57
|
+
## 非目标
|
|
58
|
+
|
|
59
|
+
本 RFC 不覆盖:
|
|
60
|
+
|
|
61
|
+
- 完整 trait 求解器
|
|
62
|
+
- 高阶类型或 higher-kinded types
|
|
63
|
+
- 复杂 `where` 传递闭包推导
|
|
64
|
+
- monomorphization 策略变更
|
|
65
|
+
- 运行时表示改造
|
|
66
|
+
|
|
67
|
+
## 当前问题拆解
|
|
68
|
+
|
|
69
|
+
### 问题 1:单边已应用泛型时,命名类型匹配过于宽松
|
|
70
|
+
|
|
71
|
+
当前若一侧是 bare type,另一侧是 applied type,经常直接返回 `true`,等价于“你们名字一样,那先算匹配”。
|
|
72
|
+
|
|
73
|
+
这在弱检查阶段很方便,但它牺牲了后续信息。
|
|
74
|
+
|
|
75
|
+
例如:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
actual: Struct(Pair, [number, string])
|
|
79
|
+
expected: TypeRef("Pair", [])
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
如果这里只返回 `true` 而不写入:
|
|
83
|
+
|
|
84
|
+
- `A -> number`
|
|
85
|
+
- `B -> string`
|
|
86
|
+
|
|
87
|
+
那么后续如果某个返回值、字段访问或 `:where` 约束依赖 `A` / `B`,就只能退化到更弱的判断。
|
|
88
|
+
|
|
89
|
+
### 问题 2:不同命名类型组合的统一规则不一致
|
|
90
|
+
|
|
91
|
+
当前已经存在这样的不对称:
|
|
92
|
+
|
|
93
|
+
- `Struct` 对 `Struct` 有“单边 applied 时绑定泛型”的逻辑
|
|
94
|
+
- `Struct` 对 `TypeRef` 过去没有同等级逻辑
|
|
95
|
+
- `Enum` 对 `TypeRef` 目前也没有
|
|
96
|
+
|
|
97
|
+
这意味着用户只是换了一层命名表示,行为就变了。
|
|
98
|
+
|
|
99
|
+
从工程上看,这种不一致比“暂时保守”更难维护,因为:
|
|
100
|
+
|
|
101
|
+
- bug 不稳定复现
|
|
102
|
+
- 某些路径上 `:where` 警告会出现,另一些路径不会
|
|
103
|
+
- 推理链条难以复用
|
|
104
|
+
|
|
105
|
+
## 当前已落地能力
|
|
106
|
+
|
|
107
|
+
截至当前版本,已经确认可用的基础能力包括:
|
|
108
|
+
|
|
109
|
+
- `Struct <-> TypeRef` 在单边 applied 时会把实参绑定回 `CalcitStruct.generics`
|
|
110
|
+
- `Enum` 定义已经持有 `generics` 元数据,不再需要借助旧 record 结构旁敲侧击恢复泛型名
|
|
111
|
+
- struct 泛型的表层声明、enum 泛型的运行时构造与 applied named type annotation 都已经能通过文档和 `eval` 验证
|
|
112
|
+
|
|
113
|
+
其中,`Struct <-> TypeRef` 已经落地的统一行为是:
|
|
114
|
+
|
|
115
|
+
- 两边都 bare:只检查命名是否一致
|
|
116
|
+
- 两边都 applied:逐项匹配参数
|
|
117
|
+
- 一边 bare、一边 applied:把 applied 参数绑定回 `CalcitStruct.generics`
|
|
118
|
+
|
|
119
|
+
也就是从“宽松通过但不留痕”改成“通过且留下绑定”。
|
|
120
|
+
|
|
121
|
+
enum 侧现在也具备做同等级修复的前提,不再属于“缺底层表示”的阶段。
|
|
122
|
+
|
|
123
|
+
## 先看 Calcit 里的实际写法
|
|
124
|
+
|
|
125
|
+
为了避免一直停留在内部 Rust 表示,先把这个问题翻回 Calcit 代码。
|
|
126
|
+
|
|
127
|
+
当前你最熟悉的两类表面写法大致是:
|
|
128
|
+
|
|
129
|
+
```cirru
|
|
130
|
+
defn id2 (x)
|
|
131
|
+
hint-fn $ {}
|
|
132
|
+
:generics $ [] 'T
|
|
133
|
+
:args $ [] 'T
|
|
134
|
+
:return 'T
|
|
135
|
+
x
|
|
136
|
+
|
|
137
|
+
defn show-id (x)
|
|
138
|
+
hint-fn $ {}
|
|
139
|
+
:generics $ [] 'T
|
|
140
|
+
:where $ {} ('T Show)
|
|
141
|
+
:args $ [] 'T
|
|
142
|
+
:return :string
|
|
143
|
+
.show x
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
这一层已经很像 Rust:
|
|
147
|
+
|
|
148
|
+
- `'T` 是泛型变量
|
|
149
|
+
- `:where` 约束表示 `'T` 必须满足某个 trait
|
|
150
|
+
- 调用点先绑定 `'T`,再拿绑定结果检查 `:where`
|
|
151
|
+
|
|
152
|
+
而命名类型这边,用户实际写的是:
|
|
153
|
+
|
|
154
|
+
```cirru
|
|
155
|
+
defstruct Pair
|
|
156
|
+
:left :number
|
|
157
|
+
:right :string
|
|
158
|
+
|
|
159
|
+
defstruct Holder
|
|
160
|
+
:box Pair
|
|
161
|
+
|
|
162
|
+
defenum Wrapped
|
|
163
|
+
:pair Pair
|
|
164
|
+
:none
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
这里的问题不是 Calcit 没有泛型,而是“命名类型在不同写法之间切换时,绑定信息没有总是被保留下来”。
|
|
168
|
+
|
|
169
|
+
## 用 Calcit 代码看这个问题
|
|
170
|
+
|
|
171
|
+
下面几段代码故意把内部 `Struct(...)` / `TypeRef(...)` 还原成用户更关心的表面写法。
|
|
172
|
+
|
|
173
|
+
### 场景 0:先看一个正常的泛型绑定
|
|
174
|
+
|
|
175
|
+
```cirru
|
|
176
|
+
defn echo-box (x)
|
|
177
|
+
hint-fn $ {}
|
|
178
|
+
:generics $ [] 'T
|
|
179
|
+
:args $ [] (:: Box 'T)
|
|
180
|
+
:return 'T
|
|
181
|
+
get x :value
|
|
182
|
+
|
|
183
|
+
defstruct Box $ :value 'T
|
|
184
|
+
|
|
185
|
+
defn demo-ok ()
|
|
186
|
+
let
|
|
187
|
+
b $ %{} Box (:value 1)
|
|
188
|
+
n $ echo-box b
|
|
189
|
+
assert-type n :number
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
你希望编译器在调用 `echo-box b` 时做的事情其实很简单:
|
|
193
|
+
|
|
194
|
+
1. 从参数 `b` 看出它是 `Box<number>`
|
|
195
|
+
2. 把 schema 里的 `'T` 绑定成 `:number`
|
|
196
|
+
3. 再把返回类型 `'T` 专门化成 `:number`
|
|
197
|
+
|
|
198
|
+
这条链路本身没有争议。
|
|
199
|
+
|
|
200
|
+
真正的分歧出在:如果 `Box 'T` 不是直接出现在同一种内部表示里,而是有时被保留成命名引用,有时已经解成结构定义,那还要不要留下 `'T -> :number` 这组绑定?
|
|
201
|
+
|
|
202
|
+
### 场景 1:`Struct(Applied)` 对 `TypeRef(Bare)`
|
|
203
|
+
|
|
204
|
+
对应的用户代码可以想成:
|
|
205
|
+
|
|
206
|
+
```cirru
|
|
207
|
+
defstruct Pair
|
|
208
|
+
:left 'A
|
|
209
|
+
:right 'B
|
|
210
|
+
|
|
211
|
+
defn keep-pair (p)
|
|
212
|
+
hint-fn $ {}
|
|
213
|
+
:generics $ [] 'A 'B
|
|
214
|
+
:args $ [] Pair
|
|
215
|
+
:return Pair
|
|
216
|
+
p
|
|
217
|
+
|
|
218
|
+
defn demo-struct-to-named ()
|
|
219
|
+
let
|
|
220
|
+
p $ %{} Pair (:left 1) (:right |hi)
|
|
221
|
+
out $ keep-pair p
|
|
222
|
+
assert-type out Pair
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
这段表面上看不出问题,因为 `assert-type out Pair` 太粗了,只要求它还是 `Pair`。
|
|
226
|
+
|
|
227
|
+
但内部其实还有一个更细的问题:
|
|
228
|
+
|
|
229
|
+
- `p` 这一侧已经知道是 `Pair<number, string>`
|
|
230
|
+
- `keep-pair` 的参数 schema 另一侧可能只保留成名字 `Pair`
|
|
231
|
+
|
|
232
|
+
如果这一步只判断“都是 Pair,所以 ok”,那 `'A` / `'B` 实际上没有被绑定下来。
|
|
233
|
+
|
|
234
|
+
这就会影响后续更依赖精确信息的场景,比如:
|
|
235
|
+
|
|
236
|
+
```cirru
|
|
237
|
+
defn pair-left (p)
|
|
238
|
+
hint-fn $ {}
|
|
239
|
+
:generics $ [] 'A 'B
|
|
240
|
+
:args $ [] Pair
|
|
241
|
+
:return 'A
|
|
242
|
+
get p :left
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
如果前一步没有留下 `'A -> :number`,这里的 `:return 'A` 就更容易退回弱类型。
|
|
246
|
+
|
|
247
|
+
### 场景 2:`TypeRef(Applied)` 对 `Struct(Bare)`
|
|
248
|
+
|
|
249
|
+
这个场景在表面代码里更像“类型信息从引用侧来,而不是从结构定义侧来”。
|
|
250
|
+
|
|
251
|
+
```cirru
|
|
252
|
+
defstruct Pair
|
|
253
|
+
:left 'A
|
|
254
|
+
:right 'B
|
|
255
|
+
|
|
256
|
+
defn pass-through (p)
|
|
257
|
+
hint-fn $ {}
|
|
258
|
+
:generics $ [] 'A 'B
|
|
259
|
+
:args $ [] (:: Pair 'A 'B)
|
|
260
|
+
:return (:: Pair 'A 'B)
|
|
261
|
+
p
|
|
262
|
+
|
|
263
|
+
defn takes-pair (p)
|
|
264
|
+
hint-fn $ {}
|
|
265
|
+
:args $ [] Pair
|
|
266
|
+
:return Pair
|
|
267
|
+
p
|
|
268
|
+
|
|
269
|
+
defn demo-named-to-struct ()
|
|
270
|
+
let
|
|
271
|
+
p $ pass-through $ %{} Pair (:left 1) (:right |hi)
|
|
272
|
+
out $ takes-pair p
|
|
273
|
+
assert-type out Pair
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
这里你可以把 `pass-through` 想成“把具体参数挂在名字上”,把 `takes-pair` 想成“只认这个结构定义”。
|
|
277
|
+
|
|
278
|
+
理论上它们之间没有本质差异,都该把:
|
|
279
|
+
|
|
280
|
+
- `'A -> :number`
|
|
281
|
+
- `'B -> :string`
|
|
282
|
+
|
|
283
|
+
留给后续链路。
|
|
284
|
+
|
|
285
|
+
这也是为什么本轮已经先补 `Struct <-> TypeRef`,因为它是最小、最安全、又最接近 Rust 统一行为的一段。
|
|
286
|
+
|
|
287
|
+
### 场景 3:`TypeRef(Applied)` 对 `TypeRef(Bare)`
|
|
288
|
+
|
|
289
|
+
这是下一步最值得讨论的点,因为它表面上最“正常”,但内部最容易宽松放过。
|
|
290
|
+
|
|
291
|
+
```cirru
|
|
292
|
+
defstruct Pair
|
|
293
|
+
:left 'A
|
|
294
|
+
:right 'B
|
|
295
|
+
|
|
296
|
+
defn id-pair (p)
|
|
297
|
+
hint-fn $ {}
|
|
298
|
+
:generics $ [] 'A 'B
|
|
299
|
+
:args $ [] (:: Pair 'A 'B)
|
|
300
|
+
:return (:: Pair 'A 'B)
|
|
301
|
+
p
|
|
302
|
+
|
|
303
|
+
defn erase-pair (p)
|
|
304
|
+
hint-fn $ {}
|
|
305
|
+
:args $ [] Pair
|
|
306
|
+
:return Pair
|
|
307
|
+
p
|
|
308
|
+
|
|
309
|
+
defn demo-named-to-named ()
|
|
310
|
+
let
|
|
311
|
+
p $ id-pair $ %{} Pair (:left 1) (:right |hi)
|
|
312
|
+
out $ erase-pair p
|
|
313
|
+
assert-type out Pair
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
这段为什么难判断?
|
|
317
|
+
|
|
318
|
+
- 用户只看到 `Pair` 和 `(:: Pair 'A 'B)` 都是“同一个名字”
|
|
319
|
+
- 但实现上 `TypeRef` 自己并不知道 `Pair` 的第 0 个参数叫 `'A`,第 1 个参数叫 `'B`
|
|
320
|
+
- 只有再去 resolve schema,才知道参数位和变量名的对应关系
|
|
321
|
+
|
|
322
|
+
所以这里的核心不是“要不要更严格”,而是:
|
|
323
|
+
|
|
324
|
+
- 要不要在这一层做受控 schema lookup
|
|
325
|
+
- 做 lookup 后,是不是能稳定拿到 `A/B` 这组声明名
|
|
326
|
+
|
|
327
|
+
这也是 RFC 里把它单独列成阶段 2,而不是直接和 struct 一起改掉的原因。
|
|
328
|
+
|
|
329
|
+
### 场景 4:`Enum(Applied)` 对 `TypeRef(Bare)`
|
|
330
|
+
|
|
331
|
+
enum 侧更容易读懂这个结构性缺口:
|
|
332
|
+
|
|
333
|
+
```cirru
|
|
334
|
+
defenum Result
|
|
335
|
+
:ok 'T
|
|
336
|
+
:err 'E
|
|
337
|
+
|
|
338
|
+
defn pass-result (x)
|
|
339
|
+
hint-fn $ {}
|
|
340
|
+
:generics $ [] 'T 'E
|
|
341
|
+
:args $ [] Result
|
|
342
|
+
:return Result
|
|
343
|
+
, x
|
|
344
|
+
|
|
345
|
+
defn demo-result ()
|
|
346
|
+
let
|
|
347
|
+
v $ %:: Result :ok 1
|
|
348
|
+
out $ pass-result v
|
|
349
|
+
assert-type out Result
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
在当前版本里,你期待的绑定已经变成一个可以直接实现的小步,而不是纯设计目标:
|
|
353
|
+
|
|
354
|
+
- `'T -> :number`
|
|
355
|
+
- `'E -> :string`(如果调用点给出的 applied 参数完整)
|
|
356
|
+
|
|
357
|
+
或者至少在仅一侧 applied 的情况下,把已有那一侧参数回填到 enum 声明的泛型名上。
|
|
358
|
+
|
|
359
|
+
新版里这块底层元数据已经补齐:`CalcitEnum` 本身就保存 `generics`。因此这里的剩余工作不再是“先改数据结构”,而是把 `matches_with_bindings` 里的 enum 分支改成与 struct 一样的单边绑定策略。
|
|
360
|
+
|
|
361
|
+
## 为什么这些 Calcit 片段今天还“不够显眼”
|
|
362
|
+
|
|
363
|
+
如果你只看这些代码,可能会觉得:
|
|
364
|
+
|
|
365
|
+
- 反正 `assert-type out Pair` 也过了
|
|
366
|
+
- 反正 `pass-through` 和 `erase-pair` 都只是原样返回
|
|
367
|
+
- 那到底哪里有问题?
|
|
368
|
+
|
|
369
|
+
关键在于:这里要观察的不是“会不会立刻报错”,而是“后续还能不能继续做精确判断”。
|
|
370
|
+
|
|
371
|
+
比如把上面的例子继续推进一步:
|
|
372
|
+
|
|
373
|
+
```cirru
|
|
374
|
+
defn pair-left (p)
|
|
375
|
+
hint-fn $ {}
|
|
376
|
+
:generics $ [] 'A 'B
|
|
377
|
+
:args $ [] Pair
|
|
378
|
+
:return 'A
|
|
379
|
+
get p :left
|
|
380
|
+
|
|
381
|
+
defn show-left (p)
|
|
382
|
+
hint-fn $ {}
|
|
383
|
+
:generics $ [] 'A 'B
|
|
384
|
+
:where $ {} ('A Show)
|
|
385
|
+
:args $ [] Pair
|
|
386
|
+
:return :string
|
|
387
|
+
.show $ pair-left p
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
如果 `Pair<number, string>` 在更早一层只是“名字匹配成功”但没有留下:
|
|
391
|
+
|
|
392
|
+
- `'A -> :number`
|
|
393
|
+
- `'B -> :string`
|
|
394
|
+
|
|
395
|
+
那么:
|
|
396
|
+
|
|
397
|
+
- `pair-left` 的返回类型就更容易变弱
|
|
398
|
+
- `show-left` 的 `:where ('A Show)` 也更容易看不到真实绑定
|
|
399
|
+
|
|
400
|
+
所以这个 RFC 讨论的不是“让更多代码报错”,而是“让后续推断不要过早丢信息”。
|
|
401
|
+
|
|
402
|
+
## 详细案例
|
|
403
|
+
|
|
404
|
+
### 案例 A:`Struct(Applied)` 对 `TypeRef(Bare)`
|
|
405
|
+
|
|
406
|
+
输入:
|
|
407
|
+
|
|
408
|
+
```text
|
|
409
|
+
actual = Struct(Pair, [number, string])
|
|
410
|
+
expected = TypeRef("Pair", [])
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
期望行为:
|
|
414
|
+
|
|
415
|
+
- 匹配成功
|
|
416
|
+
- bindings 写入 `A -> number`, `B -> string`
|
|
417
|
+
|
|
418
|
+
原因:
|
|
419
|
+
|
|
420
|
+
- `Pair` 已经由结构定义声明了泛型变量顺序
|
|
421
|
+
- 已应用实参信息就在 `Struct` 上,跳过绑定没有收益
|
|
422
|
+
|
|
423
|
+
收益:
|
|
424
|
+
|
|
425
|
+
- 后续返回类型、字段类型或 `:where` 检查可以继续消费这组绑定
|
|
426
|
+
|
|
427
|
+
### 案例 B:`TypeRef(Applied)` 对 `Struct(Bare)`
|
|
428
|
+
|
|
429
|
+
输入:
|
|
430
|
+
|
|
431
|
+
```text
|
|
432
|
+
actual = TypeRef("Pair", [number, string])
|
|
433
|
+
expected = Struct(Pair, [])
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
期望行为:
|
|
437
|
+
|
|
438
|
+
- 匹配成功
|
|
439
|
+
- bindings 写入 `A -> number`, `B -> string`
|
|
440
|
+
|
|
441
|
+
原因:
|
|
442
|
+
|
|
443
|
+
- 这和案例 A 在类型论上没有本质区别
|
|
444
|
+
- 只是 applied 参数出现在另一侧
|
|
445
|
+
|
|
446
|
+
### 案例 C:`TypeRef(Applied)` 对 `TypeRef(Bare)`
|
|
447
|
+
|
|
448
|
+
输入:
|
|
449
|
+
|
|
450
|
+
```text
|
|
451
|
+
actual = TypeRef("app/Pair", [number, string])
|
|
452
|
+
expected = TypeRef("app/Pair", [])
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
建议行为:
|
|
456
|
+
|
|
457
|
+
- 如果 `app/Pair` 可 resolve 到带泛型定义的 schema,则应绑定 `A -> number`, `B -> string`
|
|
458
|
+
- 如果无法 resolve,则保留当前宽松行为或显式降级策略
|
|
459
|
+
|
|
460
|
+
难点:
|
|
461
|
+
|
|
462
|
+
- `TypeRef` 自身只保存名字和参数,不直接携带定义处的泛型变量名
|
|
463
|
+
- 因此需要借助 schema lookup 才能知道“第 0 个参数其实是 `A`”
|
|
464
|
+
|
|
465
|
+
这是下一步值得推进的点。
|
|
466
|
+
|
|
467
|
+
### 案例 D:`Enum(Applied)` 对 `TypeRef(Bare)`
|
|
468
|
+
|
|
469
|
+
输入:
|
|
470
|
+
|
|
471
|
+
```text
|
|
472
|
+
actual = Enum(Result, [number, string])
|
|
473
|
+
expected = TypeRef("Result", [])
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
建议行为:
|
|
477
|
+
|
|
478
|
+
- 匹配成功
|
|
479
|
+
- bindings 写入 `T -> number`, `E -> string`
|
|
480
|
+
|
|
481
|
+
当前版本下的实现前提已经具备:
|
|
482
|
+
|
|
483
|
+
- `CalcitEnum.generics()` 可以提供 `T/E/...` 这些声明名
|
|
484
|
+
- 因此这一步已经下降为“补一段与 struct 对称的匹配代码和测试”
|
|
485
|
+
|
|
486
|
+
所以 enum 侧现在不再是“缺底层元数据”,而是一个适合直接试做的小步增量。
|
|
487
|
+
|
|
488
|
+
## 为什么这更像 Rust
|
|
489
|
+
|
|
490
|
+
Rust 在处理泛型时有一个非常强的直觉:
|
|
491
|
+
|
|
492
|
+
- 只要类型构造器确定,参数信息就应该尽可能参与统一
|
|
493
|
+
- 统一得到的结果要继续喂给后续约束求解与返回类型推导
|
|
494
|
+
|
|
495
|
+
例如在 Rust 里:
|
|
496
|
+
|
|
497
|
+
```rust
|
|
498
|
+
fn id_pair<A, B>(x: Pair<A, B>) -> Pair<A, B> { x }
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
当调用点给出 `Pair<i64, String>` 时,编译器不会因为另一侧写的是 `Pair<A, B>` 就只判断“名字一样”。它会把:
|
|
502
|
+
|
|
503
|
+
- `A = i64`
|
|
504
|
+
- `B = String`
|
|
505
|
+
|
|
506
|
+
完整带入后续链路。
|
|
507
|
+
|
|
508
|
+
Calcit 当前的问题不是“没有泛型”,而是“某些路径下统一得不彻底”。
|
|
509
|
+
|
|
510
|
+
## 优点
|
|
511
|
+
|
|
512
|
+
### 1. 行为更一致
|
|
513
|
+
|
|
514
|
+
同一个命名类型,不再因为表面写成 `Struct` 还是 `TypeRef` 就触发不同绑定规则。
|
|
515
|
+
|
|
516
|
+
### 2. `:where` 约束更可靠
|
|
517
|
+
|
|
518
|
+
很多 `:where` 检查都依赖前一步先拿到绑定结果。绑定越完整,约束检查越不容易漏报。
|
|
519
|
+
|
|
520
|
+
### 3. 返回类型特化更稳定
|
|
521
|
+
|
|
522
|
+
如果泛型返回值引用了 `A` / `B`,统一链路越完整,返回类型越不需要退回 `dynamic`。
|
|
523
|
+
|
|
524
|
+
### 4. 便于后续扩展
|
|
525
|
+
|
|
526
|
+
后面无论是继续改 `TypeRef <-> TypeRef`,还是补 enum 泛型元数据,都可以沿同一原则推进,而不是为每个组合单独发明例外。
|
|
527
|
+
|
|
528
|
+
## 缺点与风险
|
|
529
|
+
|
|
530
|
+
### 1. 会暴露更多已有问题
|
|
531
|
+
|
|
532
|
+
一旦绑定更完整,后续 `:where` 或返回类型检查就可能出现更多 warning。这不是新 bug,而是旧问题被看见了。
|
|
533
|
+
|
|
534
|
+
### 2. `TypeRef <-> TypeRef` 仍需要受控 schema lookup
|
|
535
|
+
|
|
536
|
+
enum 侧的结构升级在当前版本里已经完成,真正还更深的一步反而是 `TypeRef <-> TypeRef`:它要在不引入过度解析和循环依赖的前提下,从名字反查出声明处的泛型变量顺序。
|
|
537
|
+
|
|
538
|
+
### 3. `TypeRef <-> TypeRef` 可能引入 schema lookup 成本
|
|
539
|
+
|
|
540
|
+
如果每次都 resolve schema,会增加匹配成本,也要注意避免循环解析与缓存失效。
|
|
541
|
+
|
|
542
|
+
### 4. 兼容性上会更“严格”
|
|
543
|
+
|
|
544
|
+
过去某些路径只是宽松 `true`,不会留下更多信息。统一后可能触发更精确的 downstream 检查,用户会感觉类型系统突然变严格了。
|
|
545
|
+
|
|
546
|
+
## 建议的增量落地顺序
|
|
547
|
+
|
|
548
|
+
### 阶段 1:补齐 `Struct <-> TypeRef`
|
|
549
|
+
|
|
550
|
+
这一步已经完成。
|
|
551
|
+
|
|
552
|
+
落点:
|
|
553
|
+
|
|
554
|
+
- `matches_with_bindings`
|
|
555
|
+
- 抽出复用 helper,避免同类分支各写一遍绑定逻辑
|
|
556
|
+
|
|
557
|
+
### 阶段 2:补齐 `Enum <-> TypeRef` 的单边绑定
|
|
558
|
+
|
|
559
|
+
建议策略:
|
|
560
|
+
|
|
561
|
+
- 直接复用 `bind_declared_generics_from_applied_args`
|
|
562
|
+
- 行为与 `Struct <-> TypeRef` 保持完全对称
|
|
563
|
+
- 先只覆盖“一边 bare、一边 applied”这条路径
|
|
564
|
+
|
|
565
|
+
这一步风险低,而且能直接验证新版 enum 元数据补齐的实际收益。
|
|
566
|
+
|
|
567
|
+
### 阶段 3:补齐 `TypeRef(Applied) <-> TypeRef(Bare)`
|
|
568
|
+
|
|
569
|
+
建议策略:
|
|
570
|
+
|
|
571
|
+
- 仅在命名可 resolve 到 schema 时启用绑定
|
|
572
|
+
- 无法 resolve 时维持当前保守行为
|
|
573
|
+
|
|
574
|
+
这样不会把整个 `TypeRef` 系统一次性改成强解析。
|
|
575
|
+
|
|
576
|
+
### 阶段 4:观察 warning 面
|
|
577
|
+
|
|
578
|
+
在 bindings 更完整之后,重新评估:
|
|
579
|
+
|
|
580
|
+
- `W_GENERIC_WHERE_BOUND_MISMATCH` 是否明显增加
|
|
581
|
+
- 哪些 core schema 仍旧过宽
|
|
582
|
+
- 是否需要把某些 warning 升级为 hard error
|
|
583
|
+
|
|
584
|
+
## 最小测试建议
|
|
585
|
+
|
|
586
|
+
至少保留以下方向:
|
|
587
|
+
|
|
588
|
+
1. `Struct(Applied)` 对 `TypeRef(Bare)` 会留下 bindings
|
|
589
|
+
2. `TypeRef(Applied)` 对 `Struct(Bare)` 会留下 bindings
|
|
590
|
+
3. `TypeRef(Applied)` 对 `TypeRef(Bare)` 在可 resolve 时会留下 bindings
|
|
591
|
+
4. `Enum(Applied)` 对 `TypeRef(Bare)` 会留下 bindings
|
|
592
|
+
5. 新 bindings 会被 `:where` 检查真实消费,而不是只存在于匹配函数内部
|
|
593
|
+
|
|
594
|
+
## 开放问题
|
|
595
|
+
|
|
596
|
+
1. `TypeRef <-> TypeRef` 是否应当总是 resolve schema,还是只在单边 bare / applied 不对称时 resolve?
|
|
597
|
+
2. `TypeRef <-> TypeRef` 的 schema lookup 应该缓存到哪一层,才能避免重复解析和循环依赖?
|
|
598
|
+
3. bindings 更完整后,是否需要把某些当前依赖 `dynamic` 的 core helper 一并收紧?
|
|
599
|
+
|
|
600
|
+
## 结论
|
|
601
|
+
|
|
602
|
+
建议继续沿“Rust 风格的统一规则”推进命名类型泛型绑定,但要保持增量落地:
|
|
603
|
+
|
|
604
|
+
- 先补统一逻辑
|
|
605
|
+
- 再观察新增 warning 面
|
|
606
|
+
- 最后才决定是否提高错误等级
|
|
607
|
+
|
|
608
|
+
`Struct <-> TypeRef` 的修复已经证明这条方向风险可控;而新版 enum 元数据又把 `Enum <-> TypeRef` 从“结构前置缺失”降成了一个直接可做的小步。下一步最值得做的是:先把 enum 单边绑定补齐,验证 warning 面,再推进 `TypeRef <-> TypeRef` 的受控绑定。
|
package/{rfc → RFCs}/README.md
RENAMED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
| `04-15-type-directed-optimization-catalog.md` | Active | 基于 `&record:nth` 经验,系统梳理 Record/Tuple/Scope 等类型导向优化机会。 |
|
|
27
27
|
| `04-15-wasm-compilation-feasibility.md` | Active | WASM 编译三条路径(解释器→WASM / AOT 子集 / WASM GC)的可行性评估。 |
|
|
28
28
|
| `04-16-wasm-data-structures.md` | Active | WASM codegen 中 Tag/Record/Tuple 等数据结构的内存布局与编译策略。 |
|
|
29
|
+
| `05-31-generic-where-bounds-mfs.md` | Active | 函数 schema 泛型 `:where` 约束的最小功能规格,先作为主链路开发基线。 |
|
|
29
30
|
|
|
30
31
|
## 已执行的清理
|
|
31
32
|
|
package/build.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
use cirru_edn::{Edn, EdnRecordView, from_edn};
|
|
1
|
+
use cirru_edn::{Edn, EdnMapView, EdnRecordView, from_edn};
|
|
2
2
|
use cirru_parser::Cirru;
|
|
3
3
|
use serde::{Deserialize, Serialize};
|
|
4
4
|
use std::collections::HashMap;
|
|
@@ -102,6 +102,56 @@ fn map_key_path_segment(key: &Edn) -> String {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
fn canonical_schema_field_name(text: &str) -> Option<&'static str> {
|
|
106
|
+
match text.trim_start_matches(':') {
|
|
107
|
+
"kind" => Some("kind"),
|
|
108
|
+
"args" => Some("args"),
|
|
109
|
+
"return" => Some("return"),
|
|
110
|
+
"rest" => Some("rest"),
|
|
111
|
+
"generics" => Some("generics"),
|
|
112
|
+
"where" => Some("where"),
|
|
113
|
+
_ => None,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn canonical_schema_kind_name(text: &str) -> Option<&'static str> {
|
|
118
|
+
match text.trim_start_matches(':') {
|
|
119
|
+
"fn" => Some("fn"),
|
|
120
|
+
"macro" => Some("macro"),
|
|
121
|
+
_ => None,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fn normalize_schema_map(map: &EdnMapView) -> Edn {
|
|
126
|
+
let mut normalized = EdnMapView::default();
|
|
127
|
+
|
|
128
|
+
for (key, value) in &map.0 {
|
|
129
|
+
let normalized_key = match key {
|
|
130
|
+
Edn::Tag(tag) => Edn::tag(tag.ref_str()),
|
|
131
|
+
Edn::Str(text) => canonical_schema_field_name(text.as_ref())
|
|
132
|
+
.map(Edn::tag)
|
|
133
|
+
.unwrap_or_else(|| key.clone()),
|
|
134
|
+
Edn::Symbol(text) => canonical_schema_field_name(text.as_ref())
|
|
135
|
+
.map(Edn::tag)
|
|
136
|
+
.unwrap_or_else(|| key.clone()),
|
|
137
|
+
_ => key.clone(),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
let normalized_value = match (&normalized_key, value) {
|
|
141
|
+
(Edn::Tag(tag), Edn::Str(text)) | (Edn::Tag(tag), Edn::Symbol(text)) if tag.ref_str() == "kind" => {
|
|
142
|
+
canonical_schema_kind_name(text.as_ref())
|
|
143
|
+
.map(Edn::tag)
|
|
144
|
+
.unwrap_or_else(|| value.clone())
|
|
145
|
+
}
|
|
146
|
+
_ => value.clone(),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
normalized.insert(normalized_key, normalized_value);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Edn::Map(normalized)
|
|
153
|
+
}
|
|
154
|
+
|
|
105
155
|
/// Convert a schema Edn value (either old Quote-wrapped or new direct map) into Edn map form.
|
|
106
156
|
fn parse_schema_from_edn(value: &Edn, owner: &str) -> Result<Edn, String> {
|
|
107
157
|
// Simple scalar schemas (e.g. :dynamic, :nil) are valid as-is — skip Cirru path
|
|
@@ -110,6 +160,29 @@ fn parse_schema_from_edn(value: &Edn, owner: &str) -> Result<Edn, String> {
|
|
|
110
160
|
validate_schema_edn_no_legacy_quotes(value, owner)?;
|
|
111
161
|
return Ok(value.clone());
|
|
112
162
|
}
|
|
163
|
+
Edn::Map(map) => {
|
|
164
|
+
let normalized = normalize_schema_map(map);
|
|
165
|
+
validate_schema_edn_no_legacy_quotes(&normalized, owner)?;
|
|
166
|
+
return Ok(normalized);
|
|
167
|
+
}
|
|
168
|
+
Edn::Tuple(view)
|
|
169
|
+
if matches!(view.tag.as_ref(), Edn::Tag(tag) if matches!(tag.ref_str(), "fn" | "macro"))
|
|
170
|
+
&& matches!(view.extra.first(), Some(Edn::Map(_))) =>
|
|
171
|
+
{
|
|
172
|
+
let Some(Edn::Map(map)) = view.extra.first() else {
|
|
173
|
+
unreachable!();
|
|
174
|
+
};
|
|
175
|
+
let mut normalized = match normalize_schema_map(map) {
|
|
176
|
+
Edn::Map(map) => map,
|
|
177
|
+
_ => unreachable!(),
|
|
178
|
+
};
|
|
179
|
+
if normalized.tag_get("kind").is_none() && matches!(view.tag.as_ref(), Edn::Tag(tag) if tag.ref_str() == "macro") {
|
|
180
|
+
normalized.insert_key("kind", Edn::tag("macro"));
|
|
181
|
+
}
|
|
182
|
+
let normalized = Edn::Map(normalized);
|
|
183
|
+
validate_schema_edn_no_legacy_quotes(&normalized, owner)?;
|
|
184
|
+
return Ok(normalized);
|
|
185
|
+
}
|
|
113
186
|
_ => {}
|
|
114
187
|
}
|
|
115
188
|
// Old format: Edn::Quote wrapping Cirru — convert to direct map Edn
|