@calcit/procs 0.12.59 → 0.13.1
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/08-04-strict-cirru-edn-decoding-rfc.md +342 -0
- package/RFCs/08-05-systematic-nil-reduction-rfc.md +174 -0
- package/RFCs/README.md +3 -1
- package/editing-history/202608050112-nil-reduction-foundation.md +33 -0
- package/editing-history/202608050943-pr-review-and-test-nil-reduction.md +29 -0
- package/editing-history/202608051010-explicit-unit-without-nil.md +30 -0
- package/editing-history/202608051720-nominal-option-api-migration.md +30 -0
- package/editing-history/202608051924-expand-option-collection-apis.md +31 -0
- package/editing-history/202608052014-js-nullish-boundary.md +29 -0
- package/editing-history/202608052255-nominal-reflection-and-destruct-apis.md +29 -0
- package/editing-history/202608060104-public-nil-contract-freeze.md +36 -0
- package/editing-history/202608060215-fix-hashmap-option-docs.md +4 -0
- package/editing-history/202608060220-release-0.13.0.md +5 -0
- package/editing-history/202608061340-release-0.13.1.md +6 -0
- package/history/202608040125-js-runtime-impl-identity.md +6 -0
- package/history/202608040241-strict-cirru-edn-decoding.md +27 -0
- package/history/202608040954-strict-edn-review-followups.md +16 -0
- package/history/202608041206-harden-js-impl-brand-check.md +5 -0
- package/history/202608041223-cover-inherited-js-impl-brand.md +4 -0
- package/history/202608041911-typed-data-shape-patch.md +30 -0
- package/history/202608042252-reject-decoded-edn-collisions.md +11 -0
- package/history/202608042338-strict-edn-js-collision-parity.md +21 -0
- package/history/202608042344-isolate-strict-edn-dependency-test.md +13 -0
- package/history/202608061335-recursive-nominal-display.md +28 -0
- package/lib/calcit.procs.d.mts +35 -4
- package/lib/calcit.procs.mjs +233 -11
- package/lib/custom-formatter.mjs +28 -2
- package/lib/js-cirru.d.mts +2 -0
- package/lib/js-cirru.mjs +39 -17
- package/lib/js-enum.mjs +1 -1
- package/lib/js-impl.d.mts +1 -0
- package/lib/js-impl.mjs +28 -0
- package/lib/js-record.mjs +1 -1
- package/lib/js-struct.mjs +1 -1
- package/lib/package.json +3 -2
- package/lib/typed-edn.d.mts +4 -0
- package/lib/typed-edn.mjs +5 -0
- package/package.json +3 -2
- package/ts-src/calcit.procs.mts +241 -13
- package/ts-src/custom-formatter.mts +55 -2
- package/ts-src/js-cirru.mts +39 -19
- package/ts-src/js-enum.mts +1 -1
- package/ts-src/js-impl.mts +33 -0
- package/ts-src/js-record.mts +1 -1
- package/ts-src/js-struct.mts +1 -1
- package/ts-src/typed-edn.mts +7 -0
package/.yarn/install-state.gz
CHANGED
|
Binary file
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# RFC: Cirru EDN 严格类型化反序列化
|
|
2
|
+
|
|
3
|
+
状态:Implemented / Phase 1 + DataShape 内核(非递归类型)
|
|
4
|
+
日期:2026-08-04
|
|
5
|
+
关联:`07-31-unsafe-coerce-driven-static-type-boundary-plan.md`、`06-01-generic-binding-unification-rfc.md`、`05-31-generic-where-bounds-mfs.md`、Issue #295
|
|
6
|
+
|
|
7
|
+
## 1. 摘要
|
|
8
|
+
|
|
9
|
+
新增语言级类型边界:
|
|
10
|
+
|
|
11
|
+
```cirru
|
|
12
|
+
parse-cirru-edn-as text TypeExpr
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
它把 Cirru EDN 文本直接解码为满足 `TypeExpr` 的 Calcit 值,并保证:只要调用成功,返回值的完整递归结构、容器元素、struct 字段、enum variant/payload、泛型实参以及名义类型身份都符合目标类型。
|
|
16
|
+
|
|
17
|
+
该操作不等价于:
|
|
18
|
+
|
|
19
|
+
```cirru
|
|
20
|
+
unsafe-coerce (parse-cirru-edn text) TypeExpr
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
后者只建立静态声明,不提供运行时证明;新操作则必须完成实际解析、验证与名义值构造,建立 `validated` 类型证据。
|
|
24
|
+
|
|
25
|
+
现有 `parse-cirru-edn` 保持兼容,继续作为动态数据接口。严格 API 不允许以 `Dynamic` 作为逃生口;确实需要任意 EDN 时,应显式使用旧动态 API。
|
|
26
|
+
|
|
27
|
+
## 2. 问题
|
|
28
|
+
|
|
29
|
+
当前 `parse-cirru-edn` 的返回类型是 `Dynamic`。它的第二个 options map 只根据名称把 record/enum 重新连接到声明对象:
|
|
30
|
+
|
|
31
|
+
- 不验证 List/Map/Set 内部元素;
|
|
32
|
+
- 不验证 record 字段值的声明类型;
|
|
33
|
+
- 不验证 enum variant 和 payload 类型;
|
|
34
|
+
- 不验证泛型实参与 `:where` 约束;
|
|
35
|
+
- record 字段集合不一致时存在不可恢复的 `unreachable!` 路径;
|
|
36
|
+
- 调用方即使紧接 `assert-type`,当前浅层匹配也不能形成深度证明。
|
|
37
|
+
|
|
38
|
+
因此 options map 解决的是“名义身份恢复”,不是“类型化反序列化”。把返回值交给 `unsafe-coerce` 只会隐藏边界风险。
|
|
39
|
+
|
|
40
|
+
## 3. 设计原则
|
|
41
|
+
|
|
42
|
+
1. **动态与已验证边界分离**:动态解析和严格解码使用不同名字、不同静态语义。
|
|
43
|
+
2. **禁止隐式退化**:严格解码图中不存在 `Dynamic`、未绑定类型变量或未知自定义节点。
|
|
44
|
+
3. **直接构造目标值**:严格解码从 `cirru_edn::Edn` 直接生成目标 Calcit 值,不先生成动态值再做浅检查。
|
|
45
|
+
4. **名义身份精确**:struct/enum 使用编译器解析到的实际声明对象,不能仅凭同名结构冒充。
|
|
46
|
+
5. **Native/JS 同一规则**:两个后端使用同构的解码图和错误路径语义。
|
|
47
|
+
6. **失败可定位**:错误包含目标类型与数据路径,例如 `$.friends[2].age`。
|
|
48
|
+
7. **旧 API 兼容**:本 RFC 不改变 `parse-cirru-edn` 的现有成功结果;迁移由调用方逐处完成。
|
|
49
|
+
|
|
50
|
+
## 4. 表面语法与静态语义
|
|
51
|
+
|
|
52
|
+
### 4.1 基本形式
|
|
53
|
+
|
|
54
|
+
```cirru
|
|
55
|
+
parse-cirru-edn-as raw Person
|
|
56
|
+
|
|
57
|
+
parse-cirru-edn-as raw $ :: 'List 'Number
|
|
58
|
+
|
|
59
|
+
parse-cirru-edn-as raw $ :: Box 'String
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`TypeExpr` 使用现有类型表达式规则。命名 struct/enum 推荐直接使用声明值(如 `Person`、`Box`);内建类型与泛型应用继续使用现有 `::` 表达方式。
|
|
63
|
+
|
|
64
|
+
该形式是 syntax,而不是普通 proc,原因有三点:
|
|
65
|
+
|
|
66
|
+
- 编译器必须在运行前解析并验证 `TypeExpr`;
|
|
67
|
+
- 返回类型就是 `TypeExpr`,不能退化为 proc 的固定 `Dynamic`;
|
|
68
|
+
- JS codegen 需要把类型解析结果编译为后端无关的解码图。
|
|
69
|
+
|
|
70
|
+
### 4.2 允许的目标类型
|
|
71
|
+
|
|
72
|
+
Phase 1 支持:
|
|
73
|
+
|
|
74
|
+
- `Unit`、`Bool`、`Number`、`String`、`Symbol`、`Tag`、`Buffer`、`CirruQuote`;
|
|
75
|
+
- `Optional<T>`;
|
|
76
|
+
- `List<T>`、`Set<T>`、`Map<K,V>`、`Ref<T>`;
|
|
77
|
+
- 具有完整字段类型的 `defstruct`;
|
|
78
|
+
- 具有完整 variant/payload 类型的 `defenum`;
|
|
79
|
+
- 上述类型的有限组合;
|
|
80
|
+
- 所有泛型实参均已给出的 struct/enum 应用。
|
|
81
|
+
|
|
82
|
+
Phase 1 明确拒绝:
|
|
83
|
+
|
|
84
|
+
- `Dynamic`,以及裸 `List`/`Map`/`Set`/`Ref` 所隐含的 Dynamic 参数;
|
|
85
|
+
- 未绑定 `TypeVar`、未解析或未绑定的 `TypeSlot`;
|
|
86
|
+
- 泛型参数缺失、过多或不满足 `:where`;
|
|
87
|
+
- `Fn`、proc、trait、impl、`JsObject`、任意宿主引用;
|
|
88
|
+
- 未知 `Custom` 类型;
|
|
89
|
+
- 没有 enum 声明身份的普通 tuple;
|
|
90
|
+
- `Variadic<T>`(它是函数参数约束,不是数据类型)。
|
|
91
|
+
|
|
92
|
+
拒绝发生在预处理/编译阶段。动态输入不能迫使编译器生成“遇到不懂的节点就接受”的解码器。
|
|
93
|
+
|
|
94
|
+
### 4.3 返回类型
|
|
95
|
+
|
|
96
|
+
静态分析把整个表达式推断为已经解析、完成泛型替换后的 `TypeExpr`,其证据等级为 `validated`。Phase 1 先把类型本身接入现有推断;证据等级的结构化查询等后续静态分析基础设施具备后再暴露。
|
|
97
|
+
|
|
98
|
+
### 4.4 错误模型
|
|
99
|
+
|
|
100
|
+
为保持与 `parse-cirru-edn` 一致,Phase 1 在失败时抛出可被 `try` 捕获的错误,而不是额外引入标准库 `Result` 依赖。
|
|
101
|
+
|
|
102
|
+
错误至少包含:
|
|
103
|
+
|
|
104
|
+
- 错误类别:文本解析失败、kind 不匹配、名义名称不匹配、字段集合不匹配、variant 不存在、payload arity 不匹配、值类型不匹配;
|
|
105
|
+
- 目标类型;
|
|
106
|
+
- 结构路径;
|
|
107
|
+
- 实际 EDN kind 或名称。
|
|
108
|
+
|
|
109
|
+
示例:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
parse-cirru-edn-as failed at $.friends[2].age: expected number, got string
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 5. 严格构造规则
|
|
116
|
+
|
|
117
|
+
### 5.1 标量和容器
|
|
118
|
+
|
|
119
|
+
- 标量必须匹配对应 EDN variant,不做字符串到数字等隐式转换;
|
|
120
|
+
- `Optional<T>` 只把 EDN `nil` 解释为缺失,否则按 `T` 解码;
|
|
121
|
+
- List/Set/Map 对每个元素或键值递归解码;
|
|
122
|
+
- `Ref<T>` 只接受 EDN `atom`,并对 atom 内部值递归解码;
|
|
123
|
+
- Map key 与 value 都必须满足各自目标类型。
|
|
124
|
+
|
|
125
|
+
### 5.2 Struct
|
|
126
|
+
|
|
127
|
+
- 输入必须是 `%{}` record,不能把普通 map 自动提升为 struct;
|
|
128
|
+
- EDN record 名必须和声明名一致;
|
|
129
|
+
- 字段集合必须精确一致:未知字段和缺失字段都是错误;
|
|
130
|
+
- 不以 `nil` 代替缺失字段,不读取默认值;
|
|
131
|
+
- 字段值按泛型替换后的字段类型递归解码;
|
|
132
|
+
- 成功值的 `struct_ref` 必须是目标声明对象,保留其 trait/impl。
|
|
133
|
+
|
|
134
|
+
### 5.3 Enum
|
|
135
|
+
|
|
136
|
+
- 输入必须是 `%::` enum tuple,普通 `::` tuple 不自动升级;
|
|
137
|
+
- EDN enum 名必须和声明名一致;
|
|
138
|
+
- variant 必须存在;
|
|
139
|
+
- payload arity 必须精确一致;
|
|
140
|
+
- payload 按泛型替换后的类型逐项递归解码;
|
|
141
|
+
- 成功值的 `sum_type` 必须是目标 enum 声明对象。
|
|
142
|
+
|
|
143
|
+
### 5.4 泛型和 `:where`
|
|
144
|
+
|
|
145
|
+
对 `Box<String>`:
|
|
146
|
+
|
|
147
|
+
1. 检查 `Box` 的泛型参数数量;
|
|
148
|
+
2. 建立 `{T -> String}` 绑定;
|
|
149
|
+
3. 检查 `T` 对应的 `:where` trait 约束;
|
|
150
|
+
4. 把字段/variant 中的 `T` 替换为 `String`;
|
|
151
|
+
5. 对替换后的图递归执行同样的可解码性检查。
|
|
152
|
+
|
|
153
|
+
泛型函数内部不能依赖运行时反射出 `T`。后续公开 `EdnDecoder<T>` 后,泛型函数必须显式接收 decoder dictionary。
|
|
154
|
+
|
|
155
|
+
## 6. 共享的闭合数据形状
|
|
156
|
+
|
|
157
|
+
严格 EDN 解码已经提炼为编译器内部的 `DataShapeGraph`。EDN 执行器只负责“怎样从 EDN 读取节点”;名称解析、泛型替换、`:where` 检查和名义类型绑定都只在 shape 构建阶段执行一次。后续 JSON、FFI、持久化和 diff/patch 必须复用该 shape,不能各自实现一套不完整的类型反射。
|
|
158
|
+
|
|
159
|
+
### 6.1 当前 ABI
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
DataShapeGraph {
|
|
163
|
+
version: 1
|
|
164
|
+
root: NodeId
|
|
165
|
+
fingerprint: MD5(ABI version + complete normalized graph)
|
|
166
|
+
nodes: [DataShapeNode]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
DataShapeNode =
|
|
170
|
+
Unit | Bool | Number | String | Symbol | Tag | Buffer | CirruQuote
|
|
171
|
+
Optional(NodeId)
|
|
172
|
+
List(NodeId)
|
|
173
|
+
Set(NodeId)
|
|
174
|
+
Map { key: NodeId, value: NodeId }
|
|
175
|
+
Ref(NodeId)
|
|
176
|
+
Struct {
|
|
177
|
+
nominal_path: Namespace/Definition
|
|
178
|
+
type_args: [ClosedType]
|
|
179
|
+
fields: [(FieldTag, NodeId)]
|
|
180
|
+
}
|
|
181
|
+
Enum {
|
|
182
|
+
nominal_path: Namespace/Definition
|
|
183
|
+
type_args: [ClosedType]
|
|
184
|
+
variants: [(VariantTag, [NodeId])]
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`fingerprint` 覆盖 ABI 版本、root、所有节点、名义声明路径、完整泛型实参、字段顺序和 variant payload。它不是安全散列或输入签名,而是防止把一个类型的编译产物误用于另一个 schema 的兼容性标识。
|
|
189
|
+
|
|
190
|
+
构建阶段拒绝 Dynamic、未绑定变量、不完整泛型、失败的 where-bound、未知 custom 类型和递归 slot。每个 child node id 在 graph 完成时校验。Phase 1 暂不接受递归类型;node id 表示使未来增加 cycle-safe 占位节点时不必改写消费者模型。
|
|
191
|
+
|
|
192
|
+
关键不变量是:
|
|
193
|
+
|
|
194
|
+
> `DataShape<T>` 构建成功,表示 `T` 是一个可由编译器完整遍历并精确重建名义身份的闭合数据类型。
|
|
195
|
+
|
|
196
|
+
### 6.2 数据边界如何复用 shape
|
|
197
|
+
|
|
198
|
+
每一种边界由两部分组成:共享 shape + 格式策略。
|
|
199
|
+
|
|
200
|
+
| 能力 | 共享部分 | 格式专有部分 |
|
|
201
|
+
| --- | --- | --- |
|
|
202
|
+
| Cirru EDN decode | `DataShape<T>` | EDN kind、record/enum 编码与错误路径 |
|
|
203
|
+
| JSON decode | `DataShape<T>` | tag、map key、enum 的 JSON 表示策略 |
|
|
204
|
+
| FFI/message decode | `DataShape<T>` | 宿主值读取与资源限制 |
|
|
205
|
+
| typed patch | `DataShape<T>` | patch 操作、冲突和集合 diff 策略 |
|
|
206
|
+
|
|
207
|
+
新边界要么返回经过完整 shape 验证的 `T`,要么明确返回 Dynamic。禁止以浅层 `assert-type` 或只恢复 record 名称的方式把动态值升级成 `T`。
|
|
208
|
+
|
|
209
|
+
当前不公开可伪造的普通 record 形式 `DataShape<T>`。一等 `EdnDecoder<T>` / `DataShape<T>` 需要先有编译器派生、不可伪造的泛型 dictionary ABI;在此之前 `parse-cirru-edn-as` 继续是公开入口。
|
|
210
|
+
|
|
211
|
+
### 6.3 typed struct 更新是 patch 的前置能力
|
|
212
|
+
|
|
213
|
+
typed patch 不能建立在返回宽泛 `Record` 的 `assoc` 上。对于已知 `Struct<A...>`,核心语言必须保证:
|
|
214
|
+
|
|
215
|
+
1. 字段存在;
|
|
216
|
+
2. 写入值满足泛型替换后的字段类型;
|
|
217
|
+
3. `assoc` / `with` 返回原接收者的精确名义类型和泛型实参;
|
|
218
|
+
4. 编译器可把字段 tag 改写为位置索引,运行时仍保留 tag 作为一致性检查。
|
|
219
|
+
|
|
220
|
+
当前实现已覆盖以上四点:预处理按替换后的字段类型检查并生成 index/tag,Native indexed runtime 也会重新核对非负整数 index 与 field tag,防止旧 patch 在 schema 漂移后静默写入相邻字段。这一能力应成为所有 typed patch apply 的唯一 struct 写入路径。
|
|
221
|
+
|
|
222
|
+
## 7. `Patch<T>` 的具体协议
|
|
223
|
+
|
|
224
|
+
Recollect 当前的 `diff-twig` / `patch-twig` 继续处理开放数据树,其 Dynamic 签名是合理的兼容 API。静态业务状态另行使用不可伪造的 `Patch<T>`:
|
|
225
|
+
|
|
226
|
+
```text
|
|
227
|
+
Patch<T> {
|
|
228
|
+
shape_version: 1
|
|
229
|
+
shape_fingerprint: String
|
|
230
|
+
root: PatchNode
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
PatchNode =
|
|
234
|
+
Keep { node: NodeId }
|
|
235
|
+
Replace { node: NodeId, value: TypedValue(node) }
|
|
236
|
+
StructFields { node: NodeId, fields: [(FieldIndex, PatchNode)] }
|
|
237
|
+
EnumPayload { node: NodeId, variant: VariantIndex, payloads: [(PayloadIndex, PatchNode)] }
|
|
238
|
+
ListOps { node: NodeId, ops: [TypedListOp] }
|
|
239
|
+
MapOps { node: NodeId, ops: [TypedMapOp] }
|
|
240
|
+
SetOps { node: NodeId, ops: [TypedSetOp] }
|
|
241
|
+
RefValue { node: NodeId, patch: PatchNode }
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
具体约束:
|
|
245
|
+
|
|
246
|
+
- 每个 patch node 都声明它对应的 shape node;apply 时必须逐层匹配;
|
|
247
|
+
- `Replace` 和容器 op 的 payload 在创建或反序列化 patch 时按对应子 shape 验证,内部不得出现 Dynamic payload;
|
|
248
|
+
- struct path 使用 field index,apply 同时核对声明中的 field tag,避免 schema 漂移后误写相邻字段;
|
|
249
|
+
- enum variant 变化直接使用 `Replace`;variant 相同时才允许 payload 增量;
|
|
250
|
+
- `Ref<T>` 默认按 opaque/replace 处理,只有显式策略才递归修改可变引用;
|
|
251
|
+
- apply 首先校验 `shape_version` 和 `shape_fingerprint`,成功返回的类型始终为原来的 `T`;
|
|
252
|
+
- 网络或磁盘中的 patch 不是可信内部值,必须通过专用的 strict patch decoder 构造。
|
|
253
|
+
|
|
254
|
+
应满足:
|
|
255
|
+
|
|
256
|
+
> `apply(shape<T>, old, diff(shape<T>, old, next, strategy)) = Ok(next)`
|
|
257
|
+
>
|
|
258
|
+
> `apply(shape<T>, old, patch) = Ok(value)` 蕴含 `value: T`,且保持 struct/enum 名义身份。
|
|
259
|
+
|
|
260
|
+
### 7.1 核心与 Recollect 的职责边界
|
|
261
|
+
|
|
262
|
+
| Calcit 核心负责 | Recollect 负责 |
|
|
263
|
+
| --- | --- |
|
|
264
|
+
| `DataShape<T>` 派生、ABI 与 fingerprint | diff 启发式与遍历调度 |
|
|
265
|
+
| typed struct/enum 构造和 indexed 更新 | keyed list(如 `:id`)策略 |
|
|
266
|
+
| 不透明 `Patch<T>` 的校验和 apply 原语 | patch 合并、压缩和 memoization |
|
|
267
|
+
| Native/JS/WASM 一致的节点语义 | 开放 Dynamic tree 的兼容 API |
|
|
268
|
+
| patch strict decoder | 面向应用的 strategy 组合 API |
|
|
269
|
+
|
|
270
|
+
`DiffStrategy<T>` 绑定到 shape node 或静态路径。Recollect 的 `{:key :id}` 在派生 strategy 时应验证目标节点是 list、元素是 struct/map,且 key 字段存在并具有可比较的闭合类型;不能等运行到某条动态数据才发现策略不适用。
|
|
271
|
+
|
|
272
|
+
首个内部落地切片已实现 `Keep`、`Replace`、`StructFields` 和 `EnumPayload`:patch 创建和 apply 都核对 node id,替换值通过子 shape 验证,apply 核对 ABI version/fingerprint,并保留 base struct/enum 的名义对象。它暂时不暴露语言 API,也不接受普通 Map/tuple 伪造;下一步才是 dictionary/decoder surface 与 Recollect list/map/set strategy 的接入。
|
|
273
|
+
|
|
274
|
+
Recollect 的现有 Dynamic API 已先完成一项兼容性收紧:`change-op` 统一用名义 enum 构造,`:map-splice` 的 removed payload 修正为 `Set<Dynamic>`。这能在 operation 产生时检查 variant/arity/container kind,但不把 Dynamic payload 冒充为 `Patch<T>`。
|
|
275
|
+
|
|
276
|
+
## 8. 与旧 API 的关系
|
|
277
|
+
|
|
278
|
+
| API | 用途 | 静态结果 | 运行时保证 |
|
|
279
|
+
| --- | --- | --- | --- |
|
|
280
|
+
| `parse-cirru-edn text [options]` | 任意 EDN、旧代码兼容 | `Dynamic` | 语法有效;options 仅恢复部分身份 |
|
|
281
|
+
| `unsafe-coerce value T` | 显式信任外部保证 | `T` / trusted | 不验证、不转换 |
|
|
282
|
+
| `assert-type value T` | 现有浅层断言 | 原值 | 仅当前 matcher 覆盖范围 |
|
|
283
|
+
| `parse-cirru-edn-as text T` | 严格类型化反序列化 | `T` / validated | 深度结构与名义身份满足 `T` |
|
|
284
|
+
|
|
285
|
+
旧 API 不自动弃用,因为 REPL、配置浏览、代码数据和开放 EDN 都有合理的动态使用场景。文档应把它称为动态解析,并优先向业务状态恢复推荐严格 API。
|
|
286
|
+
|
|
287
|
+
## 9. 实施阶段
|
|
288
|
+
|
|
289
|
+
### Phase 1:严格闭环(已完成)
|
|
290
|
+
|
|
291
|
+
- 新增 `parse-cirru-edn-as` syntax 和静态返回类型;
|
|
292
|
+
- 构建无 Dynamic 节点的 `DataShapeGraph`;
|
|
293
|
+
- Native 执行器直接从 `cirru_edn::Edn` 构造类型化值;
|
|
294
|
+
- JS codegen 输出等价 graph,JS runtime 执行同样规则;
|
|
295
|
+
- 支持有限 struct/enum 组合、泛型替换与 where-bound;
|
|
296
|
+
- 增加成功、深层失败、名义名称失败、字段集合失败、variant/arity 失败和编译期拒绝用例;
|
|
297
|
+
- 文档明确旧 options map 的能力边界。
|
|
298
|
+
|
|
299
|
+
### Phase 2:共享 data shape 内核(内部实现已完成)与一等 decoder dictionary
|
|
300
|
+
|
|
301
|
+
- 已从 decoder 派生逻辑中提炼编译器内部 `DataShapeGraph`,Native/JS strict EDN 共用;
|
|
302
|
+
- 已让 typed record assoc/with 校验替换后的字段类型并保持精确接收者类型;
|
|
303
|
+
- 引入不可伪造的 `EdnDecoder<T>`;
|
|
304
|
+
- 先让命名类型解析和 schema 展开具备 cycle-safe 能力,再开放递归 struct/enum graph;
|
|
305
|
+
- `edn-decoder T` 编译期派生并 hoist graph;
|
|
306
|
+
- 泛型函数显式接收 `EdnDecoder<T>`;
|
|
307
|
+
- 提供 `decode-cirru-edn` 和 `decode-edn` 的 Result 版本;
|
|
308
|
+
- 允许显式组合 decoder,但不允许从普通 Map/Record 构造。
|
|
309
|
+
|
|
310
|
+
### Phase 3:typed diff/patch(内部最小 apply 内核已完成)
|
|
311
|
+
|
|
312
|
+
- 已实现内部 `DataPatch` 的 `Keep`、`Replace`、`StructFields`、`EnumPayload`;
|
|
313
|
+
- 已让 struct 字段和 enum payload patch 绑定到 `DataShapeGraph` node,并校验 index/tag;
|
|
314
|
+
- 已让内部 patch apply 校验 ABI/fingerprint、递归结果类型并保持名义身份;
|
|
315
|
+
- 引入一等、不透明的 `Patch<T>` 与 `DiffStrategy<T>` surface;
|
|
316
|
+
- 增加 list/map/set typed operations 和 strategy;
|
|
317
|
+
- 为 patch 的跨进程传输提供同样严格的 decoder,不暴露 Dynamic payload;
|
|
318
|
+
- 以 Recollect 的 round-trip 和嵌套 record/map fixtures 验证语义,再决定共享执行器的下沉边界。
|
|
319
|
+
|
|
320
|
+
### Phase 4:受控的格式演进
|
|
321
|
+
|
|
322
|
+
- 设计显式 custom decoder / migration API;
|
|
323
|
+
- 默认值、旧字段名、版本迁移只能通过 custom 节点引入;
|
|
324
|
+
- custom decoder 的结果仍必须进入最终目标类型的已验证构造路径。
|
|
325
|
+
|
|
326
|
+
## 10. 兼容性与风险
|
|
327
|
+
|
|
328
|
+
- 新 syntax 不改变旧代码;
|
|
329
|
+
- 严格 API 有意拒绝不完整类型,不能为了兼容改成 warning 或 Dynamic fallback;
|
|
330
|
+
- 编译期图构建可能增加时间,后续按规范化类型指纹缓存;
|
|
331
|
+
- 后续 recursive graph 必须设置构建中占位节点和执行深度/输入规模保护,避免恶意数据造成栈或资源耗尽;
|
|
332
|
+
- Native/JS 任一端暂不支持的节点应在编译期统一拒绝,不能只让某后端失败。
|
|
333
|
+
|
|
334
|
+
## 11. 验收标准
|
|
335
|
+
|
|
336
|
+
1. `parse-cirru-edn-as` 的推断结果为目标闭合类型;
|
|
337
|
+
2. `List<Map<Tag, Person>>` 等嵌套值逐层校验;
|
|
338
|
+
3. struct/enum 成功值携带精确声明身份和 impl;
|
|
339
|
+
4. Dynamic、缺泛型、未绑定变量、函数/trait/JS object 在运行前被拒绝;
|
|
340
|
+
5. Native 与 JS 对相同输入同时成功,或在同一路径以同一错误类别失败;
|
|
341
|
+
6. 旧 `parse-cirru-edn` 行为保持兼容,并移除 record 字段不匹配的 panic 路径;
|
|
342
|
+
7. 文档与集成测试覆盖动态解析、严格解码和 unsafe coercion 三种边界的区别。
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# 系统性减少 nil:类型驱动迁移 RFC
|
|
2
|
+
|
|
3
|
+
状态:Complete(下一版本发布候选;公开 nil 契约已冻结)
|
|
4
|
+
日期:2026-08-05
|
|
5
|
+
|
|
6
|
+
## 目标
|
|
7
|
+
|
|
8
|
+
在不掩盖现有运行时行为的前提下,让类型系统逐步阻止业务代码依赖 `nil`:
|
|
9
|
+
|
|
10
|
+
- 无业务返回值使用 `Unit`(运行时暂仍由 `nil` 承载);
|
|
11
|
+
- 正常的“可能缺失”最终使用名义类型 `Option<T>`;
|
|
12
|
+
- 可恢复失败使用 `Result<T, E>`;
|
|
13
|
+
- JavaScript `null`/`undefined` 只通过 `JsNullish<T>` 进入类型系统;
|
|
14
|
+
- `Optional<T>` 不再是公开 API 类型,只作为编译器识别旧 schema 与内部自举债务的兼容表示;
|
|
15
|
+
- 参数可以省略是调用约定,不再借用 `Optional<T>` 表示。
|
|
16
|
+
|
|
17
|
+
这不是立即删除运行时 `nil`。迁移顺序是先让类型契约诚实,再由诊断推动调用方显式处理,最后收紧遗留运行时行为。
|
|
18
|
+
|
|
19
|
+
## 下一版本稳定性冻结
|
|
20
|
+
|
|
21
|
+
下一版本是 nil 迁移的最终 breaking window。该版本合并、升级并发布后,以下规则进入兼容性承诺:
|
|
22
|
+
|
|
23
|
+
- 公开缺失统一返回 `Option<T>`,公开可恢复失败统一返回 `Result<T,E>`,副作用返回 `Unit`;
|
|
24
|
+
- 公开 schema 不得出现 `Optional<T>`。预处理器会发出 `W_LEGACY_OPTIONAL_SCHEMA`,该规则同样约束 `calcit.core` 的公开定义;
|
|
25
|
+
- 名称以 `&` 开头的 raw primitive 属于 semver-private 实现细节,允许在内部使用 nullable 表示,但不得直接挂入公开方法表;
|
|
26
|
+
- 已生成 JS bundle 引用的 npm runtime export 属于 codegen ABI:typed wrapper/raw proc 改名时必须保留兼容 export,并由 runtime identity test 覆盖;类型系统负责阻断重新编译的旧 FFI 源码,但不能替代旧 bundle 的装载兼容性;
|
|
27
|
+
- 新增查询 API 不得先返回 nil、再在后续版本改成 Option。类型和所有后端实现必须在首次发布时一致;
|
|
28
|
+
- `analyze weak-types --only code-nil --intent unresolved,declared-optional` 必须对 core 返回零结果;
|
|
29
|
+
- 以后若要改变这些名义返回类型,只能作为独立的非 nil 设计变更处理,不能再以“清理遗留 nil”为由制造连续 breaking change。
|
|
30
|
+
|
|
31
|
+
本次最终迁移表:
|
|
32
|
+
|
|
33
|
+
| 旧契约 | 下一版本契约 |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `first` / `last` / `nth` / `get` / `get-in` 返回值或 nil | `Option<T>` |
|
|
36
|
+
| Map/Set `.destruct` 暴露 nullable tuple | `MapDestruct<K,V>` / `SetDestruct<T>` |
|
|
37
|
+
| Record `.nth` 暴露跨后端不稳定字段顺序 | 移除公开方法;字段名 `get -> Option<T>` |
|
|
38
|
+
| `parse-float` 返回 number 或 nil | `Result<Number,String>` |
|
|
39
|
+
| `get-env` 返回 string 或 nil | `Option<String>` |
|
|
40
|
+
| `when-let` 返回 body 或 nil | `Option<R>` |
|
|
41
|
+
| `update-in` updater 接收值或 nil | updater 接收 `Option<T>` |
|
|
42
|
+
| 非穷尽 `case` / `cond` 隐式返回 nil | 明确报错;`cond` 要求最终 `true` 分支 |
|
|
43
|
+
| `dissoc-in` 空路径返回 nil | 空路径保持输入值不变 |
|
|
44
|
+
|
|
45
|
+
## 已确认的现状
|
|
46
|
+
|
|
47
|
+
当前实现已经具备迁移所需的主要构件:
|
|
48
|
+
|
|
49
|
+
- `CalcitTypeAnnotation::Optional(T)` 能表达 `T | nil`;
|
|
50
|
+
- `CalcitTypeAnnotation::JsNullish(T)` 独立表达 JavaScript 宿主空值,不与 Optional/Option 相互匹配;
|
|
51
|
+
- `nil` 推断为 `Unit`,且 `Unit` 可以匹配 `Optional<T>`;
|
|
52
|
+
- `nil?` / `some?` 已支持分支内的 Optional 类型收窄;
|
|
53
|
+
- core 已定义 `Option<T>`、`Result<T, E>` 及基本组合函数;
|
|
54
|
+
- `get` 对 List、Map 和 String 已采用可能缺失的返回推断。
|
|
55
|
+
|
|
56
|
+
主要障碍不是缺少类型,而是边界契约与历史兼容行为不一致:
|
|
57
|
+
|
|
58
|
+
1. 内建过程曾用参数类型上的 `Optional<T>` 同时表示“参数可省略”和“传入值可为 nil”;
|
|
59
|
+
2. 部分运行时会返回 nil 的过程仍声明为裸类型或 Dynamic;
|
|
60
|
+
3. `first`、`rest`、`get`、集合转换及无 else 的 `if` 等旧行为,使 nil 可以在未声明的情况下扩散;
|
|
61
|
+
4. core schema、Rust proc 签名、专用类型推断之间存在重复事实来源。
|
|
62
|
+
|
|
63
|
+
## 语义边界
|
|
64
|
+
|
|
65
|
+
### Unit
|
|
66
|
+
|
|
67
|
+
`Unit` 只用于副作用操作或明确没有有意义返回值的表达式,例如写文件、注册 watcher。运行时目前仍由 `nil` 承载 Unit,但源码不应因此显式返回 `nil` 或 `;nil`:空函数体直接声明 `Unit`,有副作用的函数让最后一个 Unit effect 自然成为返回项。只有语法必须提供占位节点时才保留 `;nil`,业务代码不得把它当缺失值容器。
|
|
68
|
+
|
|
69
|
+
### Optional<T>(遗留内部表示)
|
|
70
|
+
|
|
71
|
+
`Optional<T>` 只用于识别旧 schema 和尚未完成自举迁移的 core 内部契约。非 core 的公开函数 schema 不再允许声明 Optional;普通缺失必须使用 Option,失败使用 Result,无业务返回使用 Unit。
|
|
72
|
+
|
|
73
|
+
### JsNullish<T>
|
|
74
|
+
|
|
75
|
+
JS FFI 是无法立即消除的宿主空值边界。原生属性读取、方法调用、`aget`/`js-get` 与未声明的 `js/...` 调用返回 `JsNullish<JsObject>`:JsNullish 表达 `null`/`undefined`,不透明的 `JsObject` payload 仍要求调用方验证、转换或在可信契约处 `unsafe-coerce`。使用 `js-nullish?`/`js-present?` 收窄;旧 `nil?`/`some?` 会产生专用迁移诊断。
|
|
76
|
+
|
|
77
|
+
`JsNullish<T>` 不匹配 `Optional<T>`,因此不能传给通用 `optionally` 静默包装。只有显式 `js-nullish->option` 可以建立 `Option<T>`,且该转换不负责验证 opaque payload。
|
|
78
|
+
|
|
79
|
+
### Option<T>
|
|
80
|
+
|
|
81
|
+
新设计的查询、解析和集合查找 API 应返回 `Option<T>`。`%some` / `%none` 是普通名义值,不依赖 truthiness,也不会与 Unit 混淆。
|
|
82
|
+
|
|
83
|
+
### Result<T, E>
|
|
84
|
+
|
|
85
|
+
格式错误、IO 错误、解码失败等带原因的失败应使用 `Result<T, E>`。只有真正无错误信息价值的缺失才使用 Option。
|
|
86
|
+
|
|
87
|
+
### 可省略参数
|
|
88
|
+
|
|
89
|
+
参数可省略属于函数/过程的 arity 元数据。参数声明为 `T` 时,若调用方提供该位置,值必须匹配 `T`;只有参数值类型明确为 `Optional<T>` 时才允许显式传 nil。
|
|
90
|
+
|
|
91
|
+
## 分阶段实施
|
|
92
|
+
|
|
93
|
+
### Phase 1:契约诚实化与语义拆分
|
|
94
|
+
|
|
95
|
+
- 内建过程用独立 arity 元数据表示末尾可省略参数;
|
|
96
|
+
- proc 参数检查不再剥离 `Optional<T>`;
|
|
97
|
+
- 将底层 `&parse-float` 和 `&get-env` proc 的 nullable 返回标成 Optional,作为公开名义 API 之下的兼容边界;
|
|
98
|
+
- 将 `rest` / `butlast` 对空 List 的结果统一为同类型空 List,并同步 Native、JS、WASM;`rest` 与 `empty` 的公开契约改为 `T -> T`,因此确定存在的集合/String 不再被无条件提升为 Optional,而显式 Optional/nil 输入仍保留其可空类型;
|
|
99
|
+
- 公开 core 的 `first`、`last`、`nth`、`get`、`get-in` schema 返回名义 `Option`;内部 `&list:first`、`&list:nth`、`&map:get` 等 raw primitive 保留 nullable 表示并标记为 internal;
|
|
100
|
+
- `analyze weak-types` 将裸 `nil` 与 `;nil` 都纳入审计,仅在结构上可证明的返回位置读取函数契约,并区分 `declared-unit`、`declared-optional` 与 `unresolved`;JSON 对后两类迁移债务发出 `W_NIL_TYPE_DEBT`;
|
|
101
|
+
- `analyze.weak-types` 的机器协议升级到 schema v2,避免旧消费者在 v1 下错误接受新增的封闭 intent/diagnostic 枚举;
|
|
102
|
+
- 修正既有 `optionally` 桥接函数的契约为 `Optional<T> -> Option<T>`,为遗留 nullable 边界提供不丢失类型关系的显式出口;
|
|
103
|
+
- 为上述规则增加单元测试。
|
|
104
|
+
|
|
105
|
+
Phase 1 的类型契约修正本身不改变相关过程的运行时返回值;`rest` / `butlast` 的空集合修正与后续名义 API 切换则是单独记录的 breaking change。
|
|
106
|
+
|
|
107
|
+
低层专用推断不能通过批量 `unsafe-coerce` 清理:core 宏在检查列表形状后读取 AST,当前类型系统尚不能携带“非空列表”及 guard clause 终止证据。此类宏改用明确的 `&` raw primitive;公开包装器始终保留 Option 外层,不把内部 nullable 表示扩散成 API。
|
|
108
|
+
|
|
109
|
+
nil 审计也坚持证据边界:返回的 `do` 只有最后一项继承返回契约,返回的 `if` 只有结果分支继承契约;中间步骤、集合内容和尚未建模的控制流仍标成 `unresolved`。`declared-unit` 不计入迁移债务,`declared-optional` 则继续提示迁移到 Option/Result。
|
|
110
|
+
|
|
111
|
+
### Phase 2:建立名义安全 API
|
|
112
|
+
|
|
113
|
+
- 直接将原有 `find`、`find-index`、`index-of` 改为返回 `Option`,让旧调用在结果消费处产生明确的类型迁移提示;
|
|
114
|
+
- 将公开 `parse-float` 改为 `String -> Result<Number,String>`,`:err` 保留原始非法输入;nullable 底层过程改名为内部 `&parse-float`;
|
|
115
|
+
- 将公开 `get-env` 改为 `String -> Option<String>`,删除旧的第二个默认值参数;迁移时使用 `option:unwrap-or`,nullable 底层过程改名为内部 `&get-env`;
|
|
116
|
+
- `optionally` 仅保留为 core/internal 遗留 Optional 到 Option 的桥接,不接受 JsNullish;
|
|
117
|
+
- 反射 API `tuple-enum`、`impl-origin` 返回名义 `Option`,nullable 的 `&tuple:enum`、`&impl:origin` 只保留为内部原语;`record-struct` 则收紧为必然返回 `Struct`;
|
|
118
|
+
- `destruct-list/map/set/str` 从匿名 `:: :some/:none` tuple 升级到参数化的名义 `*Destruct` enum,让 variant 载荷参与类型检查;
|
|
119
|
+
- 泛型实参无法确定时,只把未绑定 payload 降为 Dynamic,保留 `Option<Dynamic>` / `Result<...,Dynamic>` 等外层名义类型,避免整个结果退化成 Dynamic;
|
|
120
|
+
- 对 `some?`/`nil?`、位置式 `get`/`nth` 以及底层 `&compare` 消费名义 enum 的旧 nullable 用法报告 `W_NOMINAL_ENUM_LEGACY_USE`,提示改用 Option 方法、unwrap 或 `tag-match`;
|
|
121
|
+
- core schema 成为公开函数契约,Rust proc 签名负责底层过程契约,并增加一致性审计。
|
|
122
|
+
|
|
123
|
+
core 自举宏已经迁到明确的 `&` raw primitive,公开 `first`、`last`、`nth`、`get` 不再承担 Optional 债务。JS FFI 独立为 `JsNullish<JsObject>`,不会跟随这些 API 自动变为 Option,也不能通过 `optionally` 擦除边界。
|
|
124
|
+
|
|
125
|
+
### Phase 3:typed code 严格化(已完成本 RFC 范围)
|
|
126
|
+
|
|
127
|
+
在启用类型检查的代码中逐项收紧:
|
|
128
|
+
|
|
129
|
+
- 禁止将 `Optional<T>` 直接传给要求 `T` 的参数;
|
|
130
|
+
- 非穷尽 `case` 运行时报错,`cond` 必须提供最终 `true` 分支;可能缺失的业务值显式返回 Option;
|
|
131
|
+
- 条件表达式要求 Bool,不再依赖 nil truthiness;
|
|
132
|
+
- `first`、`rest`、`get`、`map`、`filter`、`to-list`、`to-map` 等不再接受 nil 作为正常集合;
|
|
133
|
+
- 部分 Record 省略字段必须由字段类型或显式默认值许可。
|
|
134
|
+
|
|
135
|
+
每条规则先提供稳定诊断码和修复提示,再升级为错误。无类型代码继续走兼容路径,直到单独决定移除窗口。
|
|
136
|
+
|
|
137
|
+
### Phase 4:收缩运行时 nil(公开边界已完成)
|
|
138
|
+
|
|
139
|
+
- 公共安全 API 的返回值已切换为 Option/Result/Unit;
|
|
140
|
+
- 删除已无调用方的 nil-tolerant 分支;
|
|
141
|
+
- Optional 仅由迁移工具与 internal raw primitive 识别;公开 schema 由稳定诊断阻断;
|
|
142
|
+
- 审计 JS/WASM 后端,确保名义类型的表示和分支行为一致。
|
|
143
|
+
|
|
144
|
+
## 类型提示修复策略
|
|
145
|
+
|
|
146
|
+
迁移应优先给出局部、可机械执行的建议:
|
|
147
|
+
|
|
148
|
+
- `Optional<T> -> T`:提示先用 `some?`/`nil?` 收窄,或转换为 Option;
|
|
149
|
+
- `JsNullish<T> -> T`:提示使用 `js-present?`/`js-nullish?` 收窄,验证 opaque payload,并只在显式需要时调用 `js-nullish->option`;
|
|
150
|
+
- 缺少 else:提示补 else、改为 `Option<T>`,或明确声明 `Optional<T>` 兼容边界;
|
|
151
|
+
- nil 作为集合:提示在来源处处理缺失,不自动替换成空集合,因为二者业务语义不同;
|
|
152
|
+
- 可省略参数处显式传 nil:提示省略参数,或修改参数值类型为 Optional;
|
|
153
|
+
- 解析返回 Optional:提示改用后续的 Result API,以保留错误信息。
|
|
154
|
+
- 名义 Option 仍用 `some?`/`nil?` 或 tuple 位置读取:提示改用 `option:some?`、`option:none?`、`option:unwrap-or` 或 `tag-match`。
|
|
155
|
+
|
|
156
|
+
自动修复不得把 nil 无条件替换为空集合、0、空字符串或 false。
|
|
157
|
+
|
|
158
|
+
## 兼容性与验收
|
|
159
|
+
|
|
160
|
+
每个阶段必须满足:
|
|
161
|
+
|
|
162
|
+
1. proc 运行时行为与声明返回类型一致;
|
|
163
|
+
2. 参数 arity 与参数值类型分别测试;
|
|
164
|
+
3. Native、JS、WASM 的可观察结果一致;
|
|
165
|
+
4. 新诊断包含稳定代码、位置和明确修复方向;
|
|
166
|
+
5. examples、core、真实外部项目分别统计新增诊断,不能只依赖单元测试;
|
|
167
|
+
6. 本 RFC 的破坏性变更集中进入下一版本发布说明;该版本以后禁止继续追加 nil 驱动的 breaking change。
|
|
168
|
+
|
|
169
|
+
## 非目标
|
|
170
|
+
|
|
171
|
+
- 不把所有 `nil` 机械替换成 `%none`;
|
|
172
|
+
- 不在同一阶段修改全部 core 集合语义;
|
|
173
|
+
- 不用 Dynamic 掩盖无法建模的缺失;
|
|
174
|
+
- 不承诺 Option/Result 与 FFI 的零成本表示,边界转换需要单独设计和测试。
|
package/RFCs/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# RFC 整理索引
|
|
2
2
|
|
|
3
|
-
更新时间:2026-
|
|
3
|
+
更新时间:2026-08-05
|
|
4
4
|
|
|
5
5
|
## 目录原则
|
|
6
6
|
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
| `07-28-git-module-store-rfc.md` | Draft | 保持 `deps.cirru` 与 Git 模块路径,以 tag 为最佳实践并使用 pnpm 式全局目录存储;不引入 registry、lockfile、workspace 或多版本。 |
|
|
40
40
|
| `07-28-project-tooling-contract-rfc.md` | Draft | 在既有 `cr` 子命令上补强单项目工具契约,保持 EDN 树形事实来源。 |
|
|
41
41
|
| `07-28-persistent-tree-cursor-rfc.md` | Draft | `.calcit/` 本地状态、虚拟 cursor、region/marks/last-query、结构化 clipboard 与 path 迁移。 |
|
|
42
|
+
| `08-04-strict-cirru-edn-decoding-rfc.md` | Implemented | Phase 1:`parse-cirru-edn-as` 严格类型化反序列化、无 Dynamic 的 `EdnDecoderGraph`、名义身份与 Native/JS 一致性。 |
|
|
43
|
+
| `08-05-systematic-nil-reduction-rfc.md` | Partial | 类型驱动减少 nil:先拆分可省略参数与 nullable 值,再迁移至 Option/Result 并逐步收紧 typed code。 |
|
|
42
44
|
|
|
43
45
|
## 已执行的清理
|
|
44
46
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Nil reduction foundation
|
|
2
|
+
|
|
3
|
+
## 修改概要
|
|
4
|
+
|
|
5
|
+
- 将内建过程的末尾可省略参数改为独立 arity 元数据,不再把参数值类型写成 `Optional<T>`;
|
|
6
|
+
- proc 参数检查保留 `Optional<T>` 的 nullable 值语义,不再为已提供参数自动剥离 Optional;
|
|
7
|
+
- 修正 `parse-float` 与 `get-env` 的底层返回签名,使其覆盖实际 nil 返回;
|
|
8
|
+
- 增加系统性减少 nil 的分阶段 RFC,明确 Unit、Optional、Option、Result 和可省略参数的边界;
|
|
9
|
+
- 保留内部 List/Map 低层查询的 Dynamic/专用推断兼容点,避免在缺少非空集合证据时用 `unsafe-coerce` 抹掉泛型信息。
|
|
10
|
+
- 扩展 `analyze weak-types` 的 nil 审计:只在可证明的返回位置使用声明契约,区分 `declared-unit`、`declared-optional` 和 `unresolved`,并输出 `W_NIL_TYPE_DEBT`。
|
|
11
|
+
- 修正 `calcit.core/optionally` 的 schema,由错误的 `T -> Optional<T>` 改为实际语义 `Optional<T> -> Option<T>`,并补充 `%some`/`%none` 示例与 schema 回归测试。
|
|
12
|
+
|
|
13
|
+
## 知识点
|
|
14
|
+
|
|
15
|
+
- nullable value 与 omitted argument 是正交语义,不能共用 `Optional<T>`;
|
|
16
|
+
- 修正底层查询返回类型会立即暴露 core 宏对非空 List 的隐含前提;在类型系统能表达 guard clause 终止和 NonEmptyList 证据前,批量强制转换不是安全迁移;
|
|
17
|
+
- 高层 `first`、`nth`、`get` schema 已能向 typed code 暴露 Optional,低层 `&list:*` / `&map:*` 兼容点应单独审计和收缩;
|
|
18
|
+
- `parse-float` 的 core schema 已是 Optional,但 Rust proc 签名仍曾声明 Number,说明重复契约需要持续做一致性检查。
|
|
19
|
+
- nil 合理性不能仅由函数返回类型向整个函数体传播;`do` 的中间项仍可能是遗留 sentinel,只有真实返回位置可以安全继承 Unit/Optional 契约。
|
|
20
|
+
- Optional 到 Option 的桥接必须保留泛型关联;将返回值继续标成 Optional 会让 nominal Option 的分支穷尽与方法推断全部失效。
|
|
21
|
+
|
|
22
|
+
## 验证
|
|
23
|
+
|
|
24
|
+
- `cargo fmt`
|
|
25
|
+
- `cargo clippy -- -D warnings`
|
|
26
|
+
- `cargo test`
|
|
27
|
+
- `yarn compile`
|
|
28
|
+
- `yarn check-all`
|
|
29
|
+
- `yarn check-agent-interface`
|
|
30
|
+
- `cr calcit/test.cirru analyze check-examples --ns calcit.core --def optionally`
|
|
31
|
+
- `cr calcit/test.cirru eval "let ((x (optionally (parse-float |1)))) (assert-type x (:: 'Option 'Number)) , x"`
|
|
32
|
+
- `cr ~/.config/calcit/modules/respo.calcit/calcit.cirru analyze check-types --summary-only`
|
|
33
|
+
- `target/debug/cr ~/.config/calcit/modules/respo.calcit/calcit.cirru analyze weak-types --only code-nil --intent unresolved,declared-optional --deps --summary-only --format json`
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# PR review and test nil reduction
|
|
2
|
+
|
|
3
|
+
## 修改概要
|
|
4
|
+
|
|
5
|
+
- 处理 PR #297 的四条有效 review:`optionally` 使用 nominal `%some`/`%none`,schema 测试绑定同一类型变量,单表达式 `do` 识别返回位置,`analyze.weak-types` 协议升级到 v2;
|
|
6
|
+
- 为 `optionally` 增加 enum prototype 示例,避免结构相等掩盖 nominal 身份丢失;
|
|
7
|
+
- 将 `test-enum.main/unwrap-maybe` 从 `Optional<T>` 迁移为 `Option<T>`,测试同步使用 `%some`/`%none`;
|
|
8
|
+
- 将十个空 reload 和五个 Unit callback 的裸 nil 改为显式 `(;nil)`,保留专门验证 nil/Optional 兼容行为的用例;
|
|
9
|
+
- 测试代码合计减少 17 个作为默认返回或缺失 sentinel 的 nil 字面量。
|
|
10
|
+
|
|
11
|
+
## 知识点
|
|
12
|
+
|
|
13
|
+
- 普通 `:: :some` / `:: :none` 只构造结构 tuple,不会附着 `Option` enum prototype;值相等不足以验证 nominal 身份;
|
|
14
|
+
- 泛型桥接测试必须验证输入输出共享同一个 TypeVar,否则 `Optional<T> -> Option<U>` 也会误通过;
|
|
15
|
+
- `do` 的第一个 child 是操作符,单表达式返回项位于 index 1,多表达式仍只有最后一项是返回位置;
|
|
16
|
+
- 机器协议中的封闭枚举新增值属于破坏性变化,需要升级 schema version,而不是让 v1 消费者静默遇到未知值;
|
|
17
|
+
- 无业务值返回应显式走 Unit;测试缺失值应优先使用 Option,只有验证兼容边界时保留 raw nil。
|
|
18
|
+
|
|
19
|
+
## 验证
|
|
20
|
+
|
|
21
|
+
- `cargo fmt`
|
|
22
|
+
- `cargo clippy -- -D warnings`
|
|
23
|
+
- `cargo test`
|
|
24
|
+
- `yarn compile`
|
|
25
|
+
- `yarn check-all`
|
|
26
|
+
- `yarn check-agent-interface`
|
|
27
|
+
- `cr calcit/test.cirru analyze check-examples --ns calcit.core --def optionally`
|
|
28
|
+
- `cr calcit/test-enum.cirru`
|
|
29
|
+
- Respo type coverage 与 nil debt JSON 回归
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Explicit Unit without nil forms
|
|
2
|
+
|
|
3
|
+
## 修改概要
|
|
4
|
+
|
|
5
|
+
- 将十个测试 fixture 的空 `reload!` 改为空函数体,并把 schema 从 `Dynamic` 收紧为 `Fn() -> Unit`;
|
|
6
|
+
- 将五个 dummy trait callback 的 `;nil` body 改为空 body,同时把对应 trait method 从无签名 `:fn` 收紧为泛型参数到 `Unit` 返回;
|
|
7
|
+
- 删除 `&init-builtin-impls!` 已由 `Unit` schema 覆盖的显式 `;nil` else,并让 `;nil` 宏自身用空 body 自然产生 Unit;
|
|
8
|
+
- 为 `;nil` 增加一个只验证兼容边界的 Unit example;
|
|
9
|
+
- `analyze weak-types --only code-nil` 现在同时识别裸 `nil` 与 `;nil`,并提示已声明 Unit 的代码删除多余显式 nil form;
|
|
10
|
+
- 测试代码移除 15 个 `;nil`,core 普通实现再移除 2 个显式 nil form;只在专门验证 `;nil` 兼容语义的 example 中保留一次调用。
|
|
11
|
+
|
|
12
|
+
## 知识点
|
|
13
|
+
|
|
14
|
+
- Calcit 空 `defn`/`fn` body 会自然返回运行时 Unit;有显式 `Unit` schema 时,不需要用 `nil` 或 `;nil` 重复表达“无返回值”;
|
|
15
|
+
- trait method 可以用 `:: :fn`、泛型 receiver 与 `'Unit` 返回声明 no-value contract,避免 `:fn` 抹掉 callback 返回类型;
|
|
16
|
+
- `;nil` 仍可作为旧宏或必须提供 AST 占位节点的兼容工具,但不能成为绕过 nil 审计的方式;
|
|
17
|
+
- 顶层 eval 的 `(;nil)` 会形成额外一层调用并二次求值;验证宏本身应使用顶层 `;nil`,嵌套参数位置才写 `(;nil)`。
|
|
18
|
+
|
|
19
|
+
## 验证
|
|
20
|
+
|
|
21
|
+
- focused weak-types Unit/`;nil` tests
|
|
22
|
+
- modified fixture check-only/runtime tests
|
|
23
|
+
- `calcit.core/;nil` example
|
|
24
|
+
- `cargo fmt`
|
|
25
|
+
- `cargo clippy -- -D warnings`
|
|
26
|
+
- `cargo test`
|
|
27
|
+
- `yarn compile`
|
|
28
|
+
- `yarn check-all`
|
|
29
|
+
- `yarn check-agent-interface`
|
|
30
|
+
- Respo type coverage 与 nil debt 回归
|