@calcit/procs 0.12.36 → 0.12.38
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/RFCs/06-01-generic-binding-unification-rfc.md +608 -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/2026-0603-1600-check-md-entry-modules.md +6 -0
- package/editing-history/2026-0603-1620-release-0.12.38.md +6 -0
- package/editing-history/202605312105-map-generic-weak-types-followup.md +10 -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/.yarn/install-state.gz
CHANGED
|
Binary file
|
|
@@ -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` 的受控绑定。
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
- Documented that `defenum` payloads can reference named structs and applied generic structs.
|
|
2
|
+
- Added runnable examples in `docs/features/enums.md` for plain struct payloads and generic struct payloads.
|
|
3
|
+
- Clarified that `%::` validates struct payload values and generic arity at runtime when constructing enum instances.
|
|
4
|
+
- Validation: `cargo run --bin cr -- demos/calcit.cirru docs check-md docs/features/enums.md`
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
- added where-bound metadata to struct and enum definitions, preserving it through parsing, hashing, compare, serialization, and enum prototype reconstruction
|
|
2
|
+
- enforced data-definition generic trait bounds at runtime for `%{}`, `%{}?`, and `%::` by inferring concrete type bindings from record fields and enum payloads
|
|
3
|
+
- kept builtin `Show` satisfaction narrow to builtin value families so function-level missing-impl warnings still fire correctly during preprocess
|
|
4
|
+
- covered the new runtime path with focused Rust tests for accepting and rejecting struct/enum payloads under generic where-bounds
|
|
5
|
+
- validated the calcit side with `cargo test where_bounds --lib`, `yarn check-all`, release `cr` build, and downstream respo compilation via `../../calcit-lang/calcit/target/release/cr js`
|
|
6
|
+
- downstream follow-up in respo needed one explicit `assert-type text :string` for the `Op::update` payload under the stricter enum payload check
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
- restored high-level `defstruct` / `defenum` where-map syntax end to end after the Rust/runtime work landed
|
|
4
|
+
- aligned JS runtime parsing in `ts-src/calcit.procs.mts` so `defstruct` / `defenum` skip optional generics and where-map entries before parsing fields or variants
|
|
5
|
+
- kept the smoke test in `calcit/test-enum.cirru` on the high-level syntax and reran broad validation
|
|
6
|
+
|
|
7
|
+
## Knowledge Points
|
|
8
|
+
|
|
9
|
+
- the JS-emitted high-level data-definition forms do not preserve where-map heads as a stable plain symbol; the first item may be a runtime function alias or even an unresolved value shape, so detecting where-map entries by exact head identity is brittle
|
|
10
|
+
- a safer JS-side strategy is structural detection: after an optional generics list, treat a list as a where-map form when its head is not a tag/symbol/string and the remaining items are all 2-item pairs
|
|
11
|
+
- for current JS codegen, generic parameter lists arrive as a simple list of `CalcitSymbol`s, so the runtime parser can ignore that leading entry without needing full type-variable reconstruction
|
|
12
|
+
|
|
13
|
+
## Validation
|
|
14
|
+
|
|
15
|
+
- `cargo fmt`
|
|
16
|
+
- `cargo test where_bounds --lib`
|
|
17
|
+
- `cargo run --bin cr -- calcit/test-enum.cirru`
|
|
18
|
+
- `yarn compile && yarn try-js`
|
|
19
|
+
- `yarn try-rs`
|
|
20
|
+
- `yarn try-ir`
|
|
21
|
+
- `yarn try-wasm`
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# 2026-06-03 check-md entry modules 与文档校验改进
|
|
2
|
+
|
|
3
|
+
- `cr docs check-md` 现在默认读取 `entry`(如 `demos/calcit.cirru`)里的 `configs.modules`,并与 CLI 的 `--dep` 合并去重,避免每次手动重复传依赖。
|
|
4
|
+
- `check-md` 的 `no-run` 模式在找不到默认入口定义时,增加了对 `app.*` 命名空间定义的回退编译检查,提升示例片段可检性。
|
|
5
|
+
- 更新 `check-md` 帮助文案,明确 `--dep` 是额外依赖,默认依赖来自 `entry configs.modules`。
|
|
6
|
+
- 新增单元测试覆盖 `entry modules + --dep` 合并去重逻辑,并通过 `docs check-md` 实测验证 `respo` 文档在不显式传 `--dep` 时可通过。
|
|
@@ -3,3 +3,13 @@
|
|
|
3
3
|
- extended `cr analyze weak-types` to classify nested schema-dynamic shapes/families, include per-definition summaries, split family/shape counts by `arg`/`return` position, and show per-definition `shape@position` contributors for the remaining hotspots
|
|
4
4
|
- confirmed the current lower-bound weak hits under the existing type system: `&record:to-map` for heterogeneous record values; `&tuple:params`, `&cirru-quote:to-list`, `parse-cirru-list`, `&format-ternary-tree`, `&list:flatten`, `format-cirru`, and `format-cirru-one-liner` for mixed list payloads that cannot be soundly collapsed to `list<'T>`
|
|
5
5
|
- verified the final baseline at schema-dynamic=290 with `cargo test --bin cr analyze_weak_types -- --nocapture`, repeated `cargo run --bin cr -- calcit/test.cirru analyze weak-types --ns calcit.core --only schema-dynamic`, and runtime suite checks via `cargo run --bin cr -- calcit/test.cirru`
|
|
6
|
+
- aligned `parse-float` runtime behavior with the existing optional-number schema across Rust and JS: invalid input now yields `nil`/`null` instead of throwing or leaking `NaN`, and added inference coverage in `calcit/test-types-inference.cirru`
|
|
7
|
+
- aligned JS record missing-field behavior with Rust/eval by making `CalcitRecord.get` return `undefined` on absent fields, which keeps top-level `get` and `get-in` optional semantics consistent across runtimes
|
|
8
|
+
- refined preprocess return inference for `calcit.core/get` and `calcit.core/get-in`, so list/string/map/record accesses produce precise `optional<...>` results for literal paths instead of collapsing to broad `dynamic` in the common safe cases
|
|
9
|
+
- tightened the generic matching path so `Struct` annotations and bare `TypeRef` names bind declared type variables consistently when only one side carries applied type arguments, reducing a gap that previously skipped bindings needed by later generic specialization and `:where` checks
|
|
10
|
+
- validated the follow-up with `cargo run --bin cr -- calcit/test-types-inference.cirru`, `cargo test binds_generic_args_from`, `cargo fmt && cargo test binds_generic_args_from`, and `yarn try-rs`
|
|
11
|
+
- added RFC `RFCs/06-01-generic-binding-unification-rfc.md` to document the next step for Rust-like generic binding unification across `Struct` / `Enum` / `TypeRef`, including concrete examples, rollout stages, and the tradeoff that enum support needs deeper metadata changes than the struct-side fix
|
|
12
|
+
- extended the enum-generic foundation end to end: runtime enum metadata now preserves declared generics, `&enum::new` accepts generic parameter lists, and applied named enum annotations like `(:: 'ResultX :number :string)` validate against payload creation instead of being rejected as "enum cannot be generic"
|
|
13
|
+
- fixed the remaining struct declaration gap in the core macros/runtime boundary: `defstruct Box ([] 'T) (:value 'T)` now lowers to a real generics list for `&struct::new`, and generic field annotations are preserved as type forms instead of being evaluated away before runtime parsing
|
|
14
|
+
- documented the current runnable generic data-structure entry points in `docs/features/records.md` and `docs/features/enums.md`, including a high-level generic `defstruct` example and the current stable generic-enum constructor form via `&enum::new`
|
|
15
|
+
- validated the new docs/examples with direct eval smoke checks for the struct and enum snippets, `cargo run --bin cr -- demos/calcit.cirru docs check-md docs/features/records.md`, `cargo run --bin cr -- demos/calcit.cirru docs check-md docs/features/enums.md`, and `cargo run --bin cr -- calcit/test-generics.cirru`
|
package/lib/calcit.procs.mjs
CHANGED
|
@@ -103,6 +103,57 @@ const list_items = (item) => {
|
|
|
103
103
|
}
|
|
104
104
|
throw new Error(`Expected list entry, got: ${item}`);
|
|
105
105
|
};
|
|
106
|
+
const isQuotedTypeVar = (item) => {
|
|
107
|
+
if (item instanceof CalcitSymbol) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (item instanceof CalcitList || item instanceof CalcitSliceList) {
|
|
111
|
+
const items = Array.from(item.items());
|
|
112
|
+
return items.length === 2 && items[0] instanceof CalcitSymbol && items[0].value === "quote" && items[1] instanceof CalcitSymbol;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
};
|
|
116
|
+
const isGenericsEntry = (entry) => {
|
|
117
|
+
if (!(entry instanceof CalcitList || entry instanceof CalcitSliceList)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const items = Array.from(entry.items());
|
|
121
|
+
return items.length > 0 && items.every(isQuotedTypeVar);
|
|
122
|
+
};
|
|
123
|
+
const isWhereMapEntry = (entry) => {
|
|
124
|
+
// where-bound can be emitted as a CalcitMap (from _$n__$M_) or as a list-of-pairs
|
|
125
|
+
if (entry instanceof CalcitMap || entry instanceof CalcitSliceMap) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (!(entry instanceof CalcitList || entry instanceof CalcitSliceList)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const items = Array.from(entry.items());
|
|
132
|
+
if (items.length < 2) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const head = items[0];
|
|
136
|
+
if (head instanceof CalcitTag || head instanceof CalcitSymbol || typeof head === "string") {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return items.slice(1).every((item) => {
|
|
140
|
+
if (!(item instanceof CalcitList || item instanceof CalcitSliceList)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const pair = Array.from(item.items());
|
|
144
|
+
return pair.length === 2;
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
const trimDataDefinitionEntries = (entries) => {
|
|
148
|
+
let start = 0;
|
|
149
|
+
if (entries[start] != null && isGenericsEntry(entries[start])) {
|
|
150
|
+
start += 1;
|
|
151
|
+
}
|
|
152
|
+
if (entries[start] != null && isWhereMapEntry(entries[start])) {
|
|
153
|
+
start += 1;
|
|
154
|
+
}
|
|
155
|
+
return entries.slice(start);
|
|
156
|
+
};
|
|
106
157
|
export let _$n_trait_$o__$o_new = function (name, methods) {
|
|
107
158
|
if (arguments.length !== 2)
|
|
108
159
|
throw new Error("&trait::new expected 2 arguments");
|
|
@@ -172,7 +223,8 @@ export let _$n_assert_traits = function (value, traitDef) {
|
|
|
172
223
|
export let defstruct = (name, ...entries) => {
|
|
173
224
|
const structName = castTag(name);
|
|
174
225
|
const fields = [];
|
|
175
|
-
|
|
226
|
+
const fieldEntries = trimDataDefinitionEntries(entries);
|
|
227
|
+
for (let entry of fieldEntries) {
|
|
176
228
|
const items = list_items(entry);
|
|
177
229
|
if (items.length !== 2) {
|
|
178
230
|
throw new Error(`defstruct expects (field type) pairs, got: ${toString(entry, true)}`);
|
|
@@ -194,7 +246,8 @@ export let defstruct = (name, ...entries) => {
|
|
|
194
246
|
export let defenum = (name, ...variants) => {
|
|
195
247
|
const enumName = castTag(name);
|
|
196
248
|
const entries = [];
|
|
197
|
-
|
|
249
|
+
const variantEntries = trimDataDefinitionEntries(variants);
|
|
250
|
+
for (let variant of variantEntries) {
|
|
198
251
|
const items = list_items(variant);
|
|
199
252
|
if (items.length === 0) {
|
|
200
253
|
throw new Error("defenum expects variant tag and payload types, got empty list");
|
|
@@ -1183,7 +1236,11 @@ export let _$n_str_$o_find_index = (x, y) => {
|
|
|
1183
1236
|
return x.indexOf(y);
|
|
1184
1237
|
};
|
|
1185
1238
|
export let parse_float = (x) => {
|
|
1186
|
-
|
|
1239
|
+
const value = parseFloat(x);
|
|
1240
|
+
if (Number.isNaN(value)) {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
return value;
|
|
1187
1244
|
};
|
|
1188
1245
|
export let trim = (x, c) => {
|
|
1189
1246
|
if (c != null) {
|
package/lib/js-record.mjs
CHANGED
package/lib/package.json
CHANGED
package/package.json
CHANGED
package/ts-src/calcit.procs.mts
CHANGED
|
@@ -126,6 +126,61 @@ const list_items = (item: CalcitValue): CalcitValue[] => {
|
|
|
126
126
|
throw new Error(`Expected list entry, got: ${item}`);
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
+
const isQuotedTypeVar = (item: CalcitValue): boolean => {
|
|
130
|
+
if (item instanceof CalcitSymbol) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (item instanceof CalcitList || item instanceof CalcitSliceList) {
|
|
134
|
+
const items = Array.from(item.items());
|
|
135
|
+
return items.length === 2 && items[0] instanceof CalcitSymbol && items[0].value === "quote" && items[1] instanceof CalcitSymbol;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const isGenericsEntry = (entry: CalcitValue): boolean => {
|
|
141
|
+
if (!(entry instanceof CalcitList || entry instanceof CalcitSliceList)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const items = Array.from(entry.items());
|
|
145
|
+
return items.length > 0 && items.every(isQuotedTypeVar);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const isWhereMapEntry = (entry: CalcitValue): boolean => {
|
|
149
|
+
// where-bound can be emitted as a CalcitMap (from _$n__$M_) or as a list-of-pairs
|
|
150
|
+
if (entry instanceof CalcitMap || entry instanceof CalcitSliceMap) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if (!(entry instanceof CalcitList || entry instanceof CalcitSliceList)) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const items = Array.from(entry.items());
|
|
157
|
+
if (items.length < 2) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const head = items[0];
|
|
161
|
+
if (head instanceof CalcitTag || head instanceof CalcitSymbol || typeof head === "string") {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return items.slice(1).every((item) => {
|
|
165
|
+
if (!(item instanceof CalcitList || item instanceof CalcitSliceList)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
const pair = Array.from(item.items());
|
|
169
|
+
return pair.length === 2;
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const trimDataDefinitionEntries = (entries: CalcitValue[]): CalcitValue[] => {
|
|
174
|
+
let start = 0;
|
|
175
|
+
if (entries[start] != null && isGenericsEntry(entries[start])) {
|
|
176
|
+
start += 1;
|
|
177
|
+
}
|
|
178
|
+
if (entries[start] != null && isWhereMapEntry(entries[start])) {
|
|
179
|
+
start += 1;
|
|
180
|
+
}
|
|
181
|
+
return entries.slice(start);
|
|
182
|
+
};
|
|
183
|
+
|
|
129
184
|
export let _$n_trait_$o__$o_new = function (name: CalcitValue, methods: CalcitValue): CalcitTrait {
|
|
130
185
|
if (arguments.length !== 2) throw new Error("&trait::new expected 2 arguments");
|
|
131
186
|
const items = list_items(methods);
|
|
@@ -195,8 +250,9 @@ export let _$n_assert_traits = function (value: CalcitValue, traitDef: CalcitVal
|
|
|
195
250
|
export let defstruct = (name: CalcitValue, ...entries: CalcitValue[]): CalcitStruct => {
|
|
196
251
|
const structName = castTag(name);
|
|
197
252
|
const fields: Array<{ tag: CalcitTag; type: CalcitValue }> = [];
|
|
253
|
+
const fieldEntries = trimDataDefinitionEntries(entries);
|
|
198
254
|
|
|
199
|
-
for (let entry of
|
|
255
|
+
for (let entry of fieldEntries) {
|
|
200
256
|
const items = list_items(entry);
|
|
201
257
|
if (items.length !== 2) {
|
|
202
258
|
throw new Error(`defstruct expects (field type) pairs, got: ${toString(entry, true)}`);
|
|
@@ -221,8 +277,9 @@ export let defstruct = (name: CalcitValue, ...entries: CalcitValue[]): CalcitStr
|
|
|
221
277
|
export let defenum = (name: CalcitValue, ...variants: CalcitValue[]): CalcitEnum => {
|
|
222
278
|
const enumName = castTag(name);
|
|
223
279
|
const entries: Array<{ tag: CalcitTag; payload: CalcitSliceList }> = [];
|
|
280
|
+
const variantEntries = trimDataDefinitionEntries(variants);
|
|
224
281
|
|
|
225
|
-
for (let variant of
|
|
282
|
+
for (let variant of variantEntries) {
|
|
226
283
|
const items = list_items(variant);
|
|
227
284
|
if (items.length === 0) {
|
|
228
285
|
throw new Error("defenum expects variant tag and payload types, got empty list");
|
|
@@ -1283,8 +1340,12 @@ export let _$n_str_$o_find_index = (x: string, y: string): number => {
|
|
|
1283
1340
|
return x.indexOf(y);
|
|
1284
1341
|
};
|
|
1285
1342
|
|
|
1286
|
-
export let parse_float = (x: string): number => {
|
|
1287
|
-
|
|
1343
|
+
export let parse_float = (x: string): number | null => {
|
|
1344
|
+
const value = parseFloat(x);
|
|
1345
|
+
if (Number.isNaN(value)) {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
return value;
|
|
1288
1349
|
};
|
|
1289
1350
|
export let trim = (x: string, c: string): string => {
|
|
1290
1351
|
if (c != null) {
|
package/ts-src/js-record.mts
CHANGED