@calcit/procs 0.13.20 → 0.13.24

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 (61) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/RFCs/08-18-calcit-typed-js-ffi-boundary-rfc.md +11 -0
  3. package/RFCs/08-19-transparent-union-types-rfc.md +522 -0
  4. package/RFCs/README.md +2 -1
  5. package/editing-history/202608181930-release-0-13-21.md +5 -0
  6. package/lib/package.json +1 -1
  7. package/package.json +1 -1
  8. /package/{history → editing-history}/202608021153-runtime-traits.md +0 -0
  9. /package/{history → editing-history}/202608021227-format-markdown-cirru.md +0 -0
  10. /package/{history → editing-history}/202608021239-review-trait-followups.md +0 -0
  11. /package/{history → editing-history}/202608021251-defimpl-symbol-traits.md +0 -0
  12. /package/{history → editing-history}/202608021255-doc-type-symbols.md +0 -0
  13. /package/{history → editing-history}/202608021322-postfix-enum-methods.md +0 -0
  14. /package/{history → editing-history}/202608021327-postfix-unwrap-examples.md +0 -0
  15. /package/{history → editing-history}/202608021507-receiver-first-static-inference.md +0 -0
  16. /package/{history → editing-history}/202608021550-review-type-correctness.md +0 -0
  17. /package/{history → editing-history}/202608021730-release-0.12.58.md +0 -0
  18. /package/{history → editing-history}/202608021922-top-level-nominal-schema.md +0 -0
  19. /package/{history → editing-history}/202608021928-release-0.12.59.md +0 -0
  20. /package/{history → editing-history}/202608040125-js-runtime-impl-identity.md +0 -0
  21. /package/{history → editing-history}/202608040241-strict-cirru-edn-decoding.md +0 -0
  22. /package/{history → editing-history}/202608040954-strict-edn-review-followups.md +0 -0
  23. /package/{history → editing-history}/202608041206-harden-js-impl-brand-check.md +0 -0
  24. /package/{history → editing-history}/202608041223-cover-inherited-js-impl-brand.md +0 -0
  25. /package/{history → editing-history}/202608041911-typed-data-shape-patch.md +0 -0
  26. /package/{history → editing-history}/202608042252-reject-decoded-edn-collisions.md +0 -0
  27. /package/{history → editing-history}/202608042338-strict-edn-js-collision-parity.md +0 -0
  28. /package/{history → editing-history}/202608042344-isolate-strict-edn-dependency-test.md +0 -0
  29. /package/{history → editing-history}/202608061335-recursive-nominal-display.md +0 -0
  30. /package/{history → editing-history}/202608061433-required-record-field-access.md +0 -0
  31. /package/{history → editing-history}/202608062004-wasm-ffi-declarations.md +0 -0
  32. /package/{history → editing-history}/202608071500-wasm-ffi-review-fixes.md +0 -0
  33. /package/{history → editing-history}/202608092318-enum-edn-option-diagnostics.md +0 -0
  34. /package/{history → editing-history}/202608100022-required-struct-field-access.md +0 -0
  35. /package/{history → editing-history}/202608100102-docs-field-access-ci.md +0 -0
  36. /package/{history → editing-history}/202608100145-cirru-edn-doc-fences.md +0 -0
  37. /package/{history → editing-history}/202608100339-struct-path-boundaries.md +0 -0
  38. /package/{history → editing-history}/202608100348-caps-branch-checkout.md +0 -0
  39. /package/{history → editing-history}/202608100349-caps-remote-branch-reset.md +0 -0
  40. /package/{history → editing-history}/202608100959-struct-path-review-followups.md +0 -0
  41. /package/{history → editing-history}/202608101042-release-0.13.8.md +0 -0
  42. /package/{history → editing-history}/202608101101-release-pr-workflow.md +0 -0
  43. /package/{history → editing-history}/202608101102-release-doc-tracked-path.md +0 -0
  44. /package/{history → editing-history}/202608110054-small-edit-advice.md +0 -0
  45. /package/{history → editing-history}/202608110125-review-optional-list-first.md +0 -0
  46. /package/{history → editing-history}/202608110133-review-core-call-sites-and-thresholds.md +0 -0
  47. /package/{history → editing-history}/202608110140-release-0.13.9.md +0 -0
  48. /package/{history → editing-history}/202608111550-trailing-option-parameters.md +0 -0
  49. /package/{history → editing-history}/202608111610-review-core-option-identity.md +0 -0
  50. /package/{history → editing-history}/202608111624-js-string-replace-termination.md +0 -0
  51. /package/{history → editing-history}/202608111650-release-0.13.11.md +0 -0
  52. /package/{202608121618-type-guidance.md → editing-history/202608121618-type-guidance.md} +0 -0
  53. /package/{202608121730-struct-option-constructor.md → editing-history/202608121730-struct-option-constructor.md} +0 -0
  54. /package/{202608121900-release-0.13.12.md → editing-history/202608121900-release-0.13.12.md} +0 -0
  55. /package/{history → editing-history}/202608131228-scope-development-dependencies.md +0 -0
  56. /package/{history → editing-history}/202608131258-audit-legacy-upgrade-playbook.md +0 -0
  57. /package/{history → editing-history}/202608131340-agent-dependency-intent-audit.md +0 -0
  58. /package/{history → editing-history}/202608131607-runtime-map-decoder.md +0 -0
  59. /package/{history → editing-history}/202608131625-core-decode-map-as-metadata.md +0 -0
  60. /package/{history → editing-history}/202608131650-release-0.13.15.md +0 -0
  61. /package/{history → editing-history}/202608171611-dependency-audit-review.md +0 -0
Binary file
@@ -119,6 +119,17 @@ read-only 与 writable。DOM、Node 和 npm API 中大量属性只读;仅检
119
119
  不得把 JavaScript object 直接伪装成 Calcit `Struct`。Struct 表示已经转换并满足 Calcit
120
120
  数据不变量的值;external trait 才表示“仍由宿主拥有,但我们信任它具有这些能力”。
121
121
 
122
+ ### 定义值不是 `Dynamic`
123
+
124
+ `defstruct`、`defenum`、`deftrait`、`defimpl` 产生的是可反射、可传递的定义值,不是
125
+ “任意未知值”。其 `CodeEntry :schema` 分别使用已有的 `StructDef`、`EnumDef`、`Trait`、
126
+ `Impl` 标记。旧快照中为兼容而遗留的根 `Dynamic` 在加载时规范化为对应标记;快照写回时
127
+ 保持该标记。
128
+
129
+ 这只描述定义值的种类,不把它们当作普通 Struct/Enum 实例,也不向 trait 匹配或泛型统一
130
+ 增加 FFI 特例。字段、variant payload 和方法签名里的 `Dynamic` 仍是实际的动态债务,继续
131
+ 参与 weak-types 与 quality gate;定义根本身不参与 Dynamic 计数。
132
+
122
133
  ### 不复制 TypeScript 类型系统
123
134
 
124
135
  本 RFC 不引入:
@@ -0,0 +1,522 @@
1
+ # RFC: `deftype` 具名透明联合类型与控制流收窄
2
+
3
+ 状态:Partial(MVP 第 1 轮已实现)
4
+ 日期:2026-08-19
5
+ 关联:`02-04-runtime-traits-plan.md`、`04-15-match-syntax-rfc.md`、`05-31-generic-where-bounds-mfs.md`、`06-01-generic-binding-unification-rfc.md`、`07-26-static-semantic-analysis-rfc.md`、`08-18-calcit-typed-js-ffi-boundary-rfc.md`
6
+
7
+ ## 1. 摘要
8
+
9
+ 本 RFC 提议新增 `deftype`,用于声明**具名、透明、无运行时包装**的联合类型,并补齐围绕联合类型的控制流收窄能力。
10
+
11
+ 首要用例是 Respo 虚拟 DOM:`Component` 与 `Element` 是两个不同的 Struct,但 `tree`、`children`、diff 与 effect 遍历都需要把它们当作同一类节点传递。当前只能把这些位置声明成 `Dynamic`,或使用 `defenum` 再包一层 constructor。前者失去字段安全,后者改变数据表示并给 DSL 增加构造、解包噪音。
12
+
13
+ 目标写法:
14
+
15
+ ```cirru.no-check
16
+ deftype RespoNode
17
+ or 'Component 'Element
18
+ ```
19
+
20
+ `Component` 和 `Element` 的值可以直接进入 `RespoNode` 位置,运行时仍保持原有 Struct 表示。代码通过 `struct-match`、`type-match?` 或带 `:narrows` 契约的 predicate 恢复具体成员类型。
21
+
22
+ ### 当前实施范围(2026-08-19)
23
+
24
+ 已落地第一轮 MVP:`deftype Name (or ...)` 是编译期透明声明,并在声明点拒绝无效成员;裸 `TypeRef` 在赋值检查和 Struct 字段运行时验证处展开为成员集合;现有 `&struct:matches?` 的 true branch 和 `struct-match` binder 会获得匹配 Struct 的具体类型。`struct-match` 对 transparent union 检查非成员/重复分支与覆盖完整性。运行时没有 union wrapper,也没有新的 JS ABI。
25
+
26
+ 尚未实现的部分保持为本 RFC 的后续阶段:`_` branch 的剩余 union、公共 `type-match?`、用户 `:narrows` guard、参数化 `deftype`,以及 data-shape/严格 EDN decode 对 union 的支持。
27
+
28
+ ## 2. 设计判断
29
+
30
+ ### 2.1 `deftype` 与 `defenum` 分工
31
+
32
+ `defenum` 继续表示需要运行时 tag、payload arity 和构造器身份的代数数据类型:
33
+
34
+ ```cirru.no-check
35
+ defenum RequestState
36
+ (:idle)
37
+ (:loading)
38
+ (:failed 'String)
39
+ ```
40
+
41
+ `deftype` 表示已有类型之间的静态集合,不产生新的值构造器:
42
+
43
+ ```cirru.no-check
44
+ deftype RespoNode
45
+ or 'Component 'Element
46
+ ```
47
+
48
+ 两者分别对应两种不同需求:
49
+
50
+ - `defenum`:值本身需要携带“属于哪个 variant”的新表示;
51
+ - `deftype`:值已经有可靠的运行时身份,只需要描述“这个位置允许哪些类型”。
52
+
53
+ ### 2.2 `deftype` 与 trait 分工
54
+
55
+ Trait 描述开放的能力集合,适合算法只依赖共同方法的场景;联合类型描述封闭的数据备选,适合分支后读取各自字段的场景。
56
+
57
+ Respo renderer 需要区分 `Component` 与 `Element`,并读取完全不同的字段,因此核心节点应使用联合类型。DOM FFI 只关心对象支持哪些字段和方法,应继续使用 `:kind :external-object` trait。
58
+
59
+ 若联合类型的所有成员都实现同一个 trait,联合值可以满足该 trait bound;这不把 union 自动转换成新的 runtime trait object。
60
+
61
+ ### 2.3 与 Rust、MoonBit 经验的关系
62
+
63
+ 本设计沿用 Rust/MoonBit 的两条经验:
64
+
65
+ 1. 封闭的数据分支应由编译器进行穷尽性与分支类型检查;
66
+ 2. 开放扩展和行为分派交给 trait/interface,不用一套机制同时承担两种职责。
67
+
68
+ Calcit 的差异是保留动态语言的数据表示:`deftype` 不要求像 Rust/MoonBit enum 一样重新包装已有值。它更接近一个具名的静态 sum,但每个成员仍使用自身的 nominal runtime identity。
69
+
70
+ ## 3. 语法
71
+
72
+ ### 3.1 基本形式
73
+
74
+ `deftype` 接收名称和一个类型表达式。MVP 只开放 `or` 类型表达式:
75
+
76
+ ```cirru.no-check
77
+ deftype RespoNode
78
+ or 'Component 'Element
79
+
80
+ deftype AttrValue
81
+ or 'String 'Number 'Bool 'EventHandler
82
+ ```
83
+
84
+ 这里采用普通前缀语法,没有引入 `A | B` 之类的中缀 token。对应 AST 形状稳定为:
85
+
86
+ ```json
87
+ ["deftype", "RespoNode", ["or", "'Component", "'Element"]]
88
+ ```
89
+
90
+ 较长的声明可以使用 Cirru 的 `,` splice 保持同一个 `or` 表达式:
91
+
92
+ ```cirru.no-check
93
+ deftype DomPropValue $ or
94
+ , 'String
95
+ , 'Number
96
+ , 'Bool
97
+ , 'EventHandler
98
+ ```
99
+
100
+ 推荐短 union 保持单行 RHS;只有成员较多时使用上面的展开写法。
101
+
102
+ ### 3.2 类型引用
103
+
104
+ 其他 schema 使用普通 nominal 引用,不展开成员:
105
+
106
+ ```cirru.no-check
107
+ defstruct Component (:name 'Tag)
108
+ :tree $ :: 'Optional 'RespoNode
109
+
110
+ defn render-node (node)
111
+ struct-match node
112
+ Component component
113
+ :tree component
114
+ Element element
115
+ :children element
116
+
117
+ :: 'Fn $ {}
118
+ :args $ [] 'RespoNode
119
+ :return 'Unit
120
+ ```
121
+
122
+ `RespoNode` 在 public schema、诊断和类型自省中保留名字;只有匹配和归一化时才读取成员集合。
123
+
124
+ ### 3.3 泛型边界
125
+
126
+ MVP 不开放参数化 `deftype`,避免同时引入 alias 参数替换、递归 kind 检查和高阶类型问题。以下能力留作独立扩展:
127
+
128
+ ```cirru.no-check
129
+ ; Future, not part of MVP.
130
+ deftype ScalarOr (T)
131
+ or 'T 'String 'Number
132
+ ```
133
+
134
+ 现有参数化 `defenum`、Struct 和容器类型不受影响。
135
+
136
+ ## 4. 静态语义
137
+
138
+ ### 4.1 名义名称,透明成员
139
+
140
+ `RespoNode` 是具名定义,工具和 schema 不应在输出中随意展开为匿名集合。但赋值关系按照成员透明计算:
141
+
142
+ - `Component` 可以赋给 `RespoNode`;
143
+ - `Element` 可以赋给 `RespoNode`;
144
+ - `RespoNode` 不能在未收窄时赋给 `Component`;
145
+ - union `A` 可以赋给 union `B`,当且仅当 `A` 的每个成员都能赋给 `B`;
146
+ - union 与成员的匹配必须保持方向性,不能因为其中一侧是宽类型而双向通过。
147
+
148
+ 这与集合包含关系一致:实际值的可能集合必须是目标类型允许集合的子集。
149
+
150
+ ### 4.2 归一化
151
+
152
+ 解析 `or` 时执行:
153
+
154
+ 1. 解析 namespace-qualified TypeRef;
155
+ 2. 展平嵌套 union;
156
+ 3. 按 nominal identity 去重;
157
+ 4. 拒绝零成员和单成员 union,单类型别名不属于 MVP;
158
+ 5. 拒绝直接或间接只由 alias 构成的循环;
159
+ 6. 允许通过 Struct/Enum 字段形成递归数据图;
160
+ 7. union 出现 `Dynamic` 时给出错误,因为 `or Dynamic T` 等价于丢失整个约束。
161
+
162
+ 例如 `Component.tree -> Optional<RespoNode>` 与 `RespoNode -> Component | Element` 是合法递归;`deftype A (or 'B)`、`deftype B (or 'A)` 不是。
163
+
164
+ ### 4.3 构造与返回
165
+
166
+ `deftype` 不生成 constructor,也不改变 `%{}`、`%::` 或字面量:
167
+
168
+ ```cirru.no-check
169
+ let
170
+ component $ %{} Component (:name :root)
171
+ element $ %{} Element (:name :div)
172
+ nodes $ [] component element
173
+ render-all nodes
174
+ ```
175
+
176
+ 当 `render-all` 的参数声明为 `List<RespoNode>` 时,list literal 与 `conj`/`append` 的泛型统一应允许成员提升到 union。
177
+
178
+ MVP 不从任意不同分支自动合成匿名 union。只有存在以下证据时才提升到具名 union:
179
+
180
+ - 函数参数或返回 schema;
181
+ - 容器的期望元素类型;
182
+ - `assert-type`;
183
+ - 已有 local 类型;
184
+ - 明确的 `deftype` 定义引用。
185
+
186
+ 这样避免一次普通 `if` 把整个程序推断成不断增长的匿名类型集合。
187
+
188
+ ### 4.4 未收窄操作
189
+
190
+ union 值未收窄前只能执行所有成员都安全支持的操作:
191
+
192
+ - 可以传给接受该 union 或更宽 union 的函数;
193
+ - 可以执行所有成员共同满足的 trait bound/method;
194
+ - 不允许直接读取某个成员独有的 Struct 字段;
195
+ - 不因为多个 Struct 恰好有同名字段就默认进行 structural field merge。
196
+
197
+ 最后一条是有意限制。共同字段合并会引入字段 variance、optional 与写操作规则,MVP 先要求显式收窄。
198
+
199
+ ## 5. 控制流收窄
200
+
201
+ 仅有 union 声明不足以替代 `Dynamic`。必须让运行时判定产生静态证据,而且证据要能通过 `if`、`cond`、`and` 和 pattern matching 传播。
202
+
203
+ ### 5.1 `struct-match`
204
+
205
+ Phase 1 直接增强现有 `struct-match`,不新增 Respo 专用 accessor:
206
+
207
+ ```cirru.no-check
208
+ defn node-name (node)
209
+ struct-match node
210
+ Component component
211
+ :name component
212
+ Element element
213
+ :name element
214
+ ```
215
+
216
+ 若 scrutinee 是 `RespoNode`:
217
+
218
+ - `Component` 分支 binder 类型为 `Component`;
219
+ - `Element` 分支 binder 类型为 `Element`;
220
+ - pattern 必须是 union 的 Struct 成员;
221
+ - 所有成员已覆盖时不要求 `_`;
222
+ - 缺少成员且没有 `_` 时给出穷尽性诊断;
223
+ - `_` binder 保留尚未覆盖成员组成的剩余 union,而不是退化成 `Dynamic`。
224
+
225
+ 这项能力应修复当前 `struct-match` runtime 能匹配、但分支 binder 仍缺少具体静态类型的问题。
226
+
227
+ ### 5.2 `type-match?`
228
+
229
+ 新增公共 predicate `type-match?`,参数顺序保持 value-first:
230
+
231
+ ```cirru.no-check
232
+ if
233
+ type-match? node Component
234
+ :tree node
235
+ nil
236
+ ```
237
+
238
+ 它同时承担运行时判定与编译器可识别的 narrowing primitive:
239
+
240
+ - true branch:`node` 收窄到 `Component`;
241
+ - false branch:从原 union 排除 `Component`;
242
+ - 第二个参数必须是静态可解析的具体类型定义;
243
+ - external-object trait 只有静态证据,没有可靠 runtime identity,不允许用于该 predicate;
244
+ - 参数化容器只检查外层 runtime kind,不声称验证内部元素类型。
245
+
246
+ 底层可复用现有 `&struct:matches?`、Enum definition identity 和 builtin kind 判定,但业务代码不应直接依赖这些 primitive 的组合。
247
+
248
+ ### 5.3 用户定义 type guard
249
+
250
+ 为了保留 `component?`、`element?` 这类领域名称,函数 schema 新增 `:narrows`:
251
+
252
+ ```cirru.no-check
253
+ defn component? (value)
254
+ type-match? value Component
255
+
256
+ :: 'Fn $ {}
257
+ :args $ [] 'RespoNode
258
+ :return 'Bool
259
+ :narrows $ {}
260
+ 0 'Component
261
+ ```
262
+
263
+ key 是从零开始的参数位置,value 是 true 分支证明的目标类型。规则如下:
264
+
265
+ - 被标记函数必须返回 `Bool`;
266
+ - 目标类型必须是对应参数声明类型的成员或子类型;
267
+ - 编译器必须验证函数体是可证明等价的 guard;MVP 不提供绕过验证的 trusted 标记;
268
+ - 普通函数不能仅靠 schema 谎称 narrowing;
269
+ - false 分支从原 union 排除目标类型。
270
+
271
+ MVP 只接受函数体直接调用 `type-match?` 的可验证 guard。组合 guard 和用户自定义验证器留到后续,避免把 `:narrows` 变成另一种 `unsafe-coerce`。
272
+
273
+ ### 5.4 逻辑表达式传播
274
+
275
+ `and` 必须按从左到右的短路语义传播 true evidence:
276
+
277
+ ```cirru.no-check
278
+ if
279
+ and
280
+ component? old-tree
281
+ component? new-tree
282
+ compare-components old-tree new-tree
283
+ nil
284
+ ```
285
+
286
+ 调用 `compare-components` 时,两个 local 都是 `Component`。`or` 的 true 分支通常只得到多个可能性的 union,false 分支则累积排除证据。
287
+
288
+ `cond` 每个后续分支继承前面 predicate 为 false 的排除结果:
289
+
290
+ ```cirru.no-check
291
+ cond
292
+ component? node
293
+ render-component node
294
+ (element? node)
295
+ render-element node
296
+ ```
297
+
298
+ 当 `node` 是 `Component | Element` 时,第二个 condition 进入前已经排除了 `Component`;`element?` 再确认具体类型。实现应保存每个 local 的 positive/negative type set,而不是只记录单个覆盖类型。
299
+
300
+ ## 6. 通用类型匹配
301
+
302
+ `struct-match` 足以覆盖 Respo 的第一阶段。为了让 union 能包含 Struct、Enum 和 builtin 类型,后续增加原生 `match-type`:
303
+
304
+ ```cirru.no-check
305
+ match-type value
306
+ Component component
307
+ :tree component
308
+ Element element
309
+ :children element
310
+ String text
311
+ count text
312
+ ```
313
+
314
+ 每个 arm 是 `Type binder body...`,符合 Cirru 现有缩进结构,不需要把 pattern 和 body 包进多层括号。
315
+
316
+ `match-type` 的职责是按具体 runtime type identity 分支;Enum 内部 variant 仍交给现有 `match`。例如先由 `match-type` 确认值属于某个 Enum definition,再在分支内用 `match` 解构 variant。
317
+
318
+ Phase 1 不要求实现 `match-type`;但 `deftype` 的 IR 与 exhaustiveness API 不应锁死为 Struct-only。
319
+
320
+ ## 7. 与 Optional、Option 和 nil 的关系
321
+
322
+ union 不隐式包含 `nil`。缺失值继续由现有类型表达:
323
+
324
+ ```cirru.no-check
325
+ defstruct Component (:name 'Tag)
326
+ :tree $ :: 'Optional 'RespoNode
327
+ ```
328
+
329
+ 迁移期可以使用 `Optional<RespoNode>` 保持现有 nil 表示。新 API 若要显式表达业务分支,仍优先使用 `Option<RespoNode>`。
330
+
331
+ 不建议声明 `RespoNode = Component | Element | Unit` 来偷渡 nullable 语义,因为这会把“没有节点”和“函数无返回值”混在一起。
332
+
333
+ ## 8. 与 trait 和 FFI 的边界
334
+
335
+ ### 8.1 普通 runtime trait
336
+
337
+ 若 union 所有成员 nominally implement `RenderNode`,则:
338
+
339
+ - `RespoNode` 可以满足 `T: RenderNode`;
340
+ - `.method` 继续根据实际 Struct/Enum 的 impl 分派;
341
+ - 任一成员缺少 impl 时,整个 union 不满足该 trait;
342
+ - 多成员同名 inherent method 不构成 trait 证据。
343
+
344
+ 这使 union 与 runtime trait 互补,而不是竞争两套多态模型。
345
+
346
+ ### 8.2 external-object trait
347
+
348
+ DOM FFI 继续直接返回小型 external trait,例如 `DomElement`、`DomInput`、`DomKeyboardEvent`。不要用 union 模拟 DOM interface inheritance,也不要把宿主对象与 Respo 内部节点放进同一个 union。
349
+
350
+ External trait 是 codegen-only 静态证据,不能参与 `type-match?` runtime narrowing。宿主 API 的字符串相关重载,例如 `keydown -> KeyboardEvent`,优先通过专用 wrapper 表达,不在 union 系统中增加 dependent typing。
351
+
352
+ ## 9. 表示与实现建议
353
+
354
+ ### 9.1 类型表示
355
+
356
+ 建议新增两层表示:
357
+
358
+ - `Calcit::TypeUnion` 或等价 definition value:保存名称、namespace、RHS 和 nominal identity;
359
+ - `CalcitTypeAnnotation::UnionRef`:保留具名引用以及按需解析后的规范化成员。
360
+
361
+ 不要在普通值上增加 `Calcit::UnionValue`。`Component` 值仍然是 `Calcit::Struct`,`Element` 值也仍然是 `Calcit::Struct`。
362
+
363
+ `type-of RespoNode` 可返回 `:type-def`,`type-of component` 仍返回 `:struct`。类型自省后续可增加 `&type:members`;MVP 只需要编译器内部 lookup。
364
+
365
+ ### 9.2 解析与生命周期
366
+
367
+ `deftype` 应与 `defstruct`、`defenum` 一样保持 top-level definition identity,并参与 snapshot/schema 解析。RHS 解析必须延迟到 namespace definitions 可见后,支持 Struct 字段与 union 之间的递归引用。
368
+
369
+ JS/native/WASM 均不需要为 union value 新增 ABI。主要后端工作是:
370
+
371
+ - 保留或擦除 type definition metadata;
372
+ - lower `type-match?` 与 `match-type`;
373
+ - 在 codegen 前完成字段访问和 method candidate 校验。
374
+
375
+ ### 9.3 类型匹配实现
376
+
377
+ 建议把 assignability 写成方向明确的关系:
378
+
379
+ ```text
380
+ is_assignable(actual, expected)
381
+ ```
382
+
383
+ 核心 union 规则:
384
+
385
+ ```text
386
+ member M -> union U iff M -> any member of U
387
+ union A -> union B iff every member of A -> B
388
+ union U -> non-union T iff every member of U -> T
389
+ ```
390
+
391
+ 最后一条通常只有 union 归一化后所有成员都可赋给同一个 trait/宽类型时成立,不能作为 downcast。
392
+
393
+ 现有 `Dynamic` 兼容规则不能用来证明 union member。若 strict schema 中 `Dynamic` 与具体成员双向匹配,联合类型仍会退化成无约束;这部分应与静态语义 RFC 一起改成方向性边界规则。
394
+
395
+ ## 10. 诊断
396
+
397
+ 建议新增稳定诊断码:
398
+
399
+ | Code | 条件 | 建议 |
400
+ | --- | --- | --- |
401
+ | `E_UNION_EMPTY` | `or` 没有成员 | 至少声明两个具体类型 |
402
+ | `E_UNION_SINGLE_MEMBER` | MVP 中只有一个成员 | 直接使用该类型 |
403
+ | `E_UNION_DYNAMIC_MEMBER` | union 包含 `Dynamic` | 移除 `Dynamic` 或保留整个位置为显式动态边界 |
404
+ | `E_UNION_ALIAS_CYCLE` | alias-only 循环 | 通过 Struct/Enum 字段建立递归 |
405
+ | `W_UNION_REQUIRES_NARROWING` | 对 union 读取成员独有字段 | 使用 `struct-match`、`type-match?` 或可信 guard |
406
+ | `W_UNION_NON_EXHAUSTIVE` | match 缺少成员 | 补齐 arm 或 `_` |
407
+ | `W_INVALID_NARROWS_CONTRACT` | `:narrows` 与函数体/参数不一致 | 改成直接 `type-match?` guard |
408
+
409
+ 诊断应显示 union 名称和剩余成员,例如:
410
+
411
+ ```text
412
+ W_UNION_REQUIRES_NARROWING: `RespoNode` may be Component | Element;
413
+ field `:tree` only exists on Component. Narrow `node` before access.
414
+ ```
415
+
416
+ ## 11. Respo 迁移目标
417
+
418
+ 第一轮迁移只改类型关系,不重写 renderer 架构:
419
+
420
+ 1. 声明 `RespoNode = Component | Element`;
421
+ 2. 把 `Component.tree`、renderer/diff/effect 参数改为 `RespoNode` 或 `Optional<RespoNode>`;
422
+ 3. 增强 `struct-match` binder narrowing;
423
+ 4. 给 `component?`、`element?` 增加可验证 `:narrows`;
424
+ 5. 删除 `as-component`、`as-element` 以及对应的 `&struct:nth` accessor;
425
+ 6. 再分别为属性值、style 值、coord key 和事件值声明小型 union;
426
+ 7. DOM host object 保持 external trait,不与 `RespoNode` union 混用。
427
+
428
+ 预期 diff 主干可以保持当前数据导向写法:
429
+
430
+ ```cirru.no-check
431
+ cond
432
+ and
433
+ component? old-tree
434
+ component? new-tree
435
+ diff-components old-tree new-tree
436
+ (and (element? old-tree) (element? new-tree))
437
+ diff-elements old-tree new-tree
438
+ ```
439
+
440
+ 这里不需要 runtime enum wrapper,也不需要把整个 diff 算法改写成 trait virtual methods。
441
+
442
+ ## 12. 实施阶段
443
+
444
+ ### Phase A:`deftype` 与 assignability
445
+
446
+ - parser/snapshot 能加载 `deftype Name (or ...)`;
447
+ - 新增具名 union annotation 与 definition lookup;
448
+ - 实现成员到 union、union 到 union 的方向性匹配;
449
+ - 支持 Fn 参数/返回、Struct 字段和容器 expected type;
450
+ - `check-types`、`analyze weak-types` 和类型打印保留 union 名称。
451
+
452
+ ### Phase B:`struct-match` 静态收窄
453
+
454
+ - branch binder 得到具体 Struct 类型;
455
+ - 对 union 做成员合法性和穷尽性检查;
456
+ - `_` 分支获得剩余 union;
457
+ - 字段读取正常 lower 到受检 `&struct:nth`。
458
+
459
+ 完成 A+B 后,Respo 已可移除大部分 `Dynamic -> Component/Element` adapter。
460
+
461
+ ### Phase C:predicate 与 flow facts
462
+
463
+ - 实现 `type-match?`;
464
+ - 实现可验证 `:narrows`;
465
+ - 在 `if`、`cond`、`and`、`or` 中传播 positive/negative member sets;
466
+ - 让多个 local 的 evidence 可以同时存在。
467
+
468
+ ### Phase D:通用匹配与共同能力
469
+
470
+ - 实现 `match-type`;
471
+ - union 对共同 trait bound 的满足检查;
472
+ - 按真实项目需要评估共同只读字段,不默认开放;
473
+ - 再评估参数化 `deftype` 与匿名 union inference。
474
+
475
+ ## 13. 验收标准
476
+
477
+ 至少覆盖以下测试:
478
+
479
+ 1. `Component`、`Element` 都能传入 `RespoNode` 参数;
480
+ 2. 其他 Struct 不能传入;
481
+ 3. `List<RespoNode>` 可构造异构列表,元素读取仍是 `RespoNode`;
482
+ 4. 未收窄访问 `:tree` 给出稳定诊断;
483
+ 5. `struct-match` 两个 branch binder 分别拥有具体 Struct 类型;
484
+ 6. 完整分支无穷尽性告警,缺少分支会告警;
485
+ 7. `component?` true/false 分支分别保留包含/排除证据;
486
+ 8. `and` 能同时收窄 old/new 两个 local;
487
+ 9. `or Dynamic Component` 被拒绝;
488
+ 10. union 值运行时表示、相等性、hash 和序列化与原成员完全一致;
489
+ 11. native 与 JS 的 `type-match?` 结果一致;
490
+ 12. external-object trait 不被误当成可运行时判定的 union member。
491
+
492
+ ## 14. 不采用的方案
493
+
494
+ ### 14.1 保持 `Dynamic`,依靠 accessor
495
+
496
+ 这会让 `as-component` 和 `&struct:nth` 只隐藏类型缺失,不提供运行时验证,也无法让容器、返回值和递归字段形成稳定关系。
497
+
498
+ ### 14.2 只使用 runtime trait object
499
+
500
+ Trait 适合共同方法,但 Respo diff 需要按具体节点种类读取不同字段,并同时比较 old/new 两个值。把全部逻辑改成 virtual methods 会引入大量 accessor 或双分派,复杂度高于封闭 union。
501
+
502
+ ### 14.3 使用 `defenum` 包装已有 Struct
503
+
504
+ 该方案类型安全,但每个节点都要额外构造和解包:
505
+
506
+ ```cirru.no-check
507
+ defenum RespoNode
508
+ (:component 'Component)
509
+ (:element 'Element)
510
+ ```
511
+
512
+ 这会改变 Respo 当前数据表示、相等性路径和 DSL 输出。`deftype` 的目标正是获得相同的静态分支能力,而不引入这层运行时包装。
513
+
514
+ ### 14.4 自动把不同分支推断为匿名 union
515
+
516
+ 全局自动合成会让 union 随控制流不断增长,并使错误信息缺少稳定领域名称。MVP 只在具名 expected type 已知时提升。
517
+
518
+ ## 15. 结论
519
+
520
+ 要在不依赖 `Dynamic` 的前提下保持 Calcit/Respo 的数据导向风格,`deftype` 是基础能力,flow narrowing 是不可分割的另一半。只实现声明而不实现 `struct-match` binder、predicate guard 和逻辑传播,最终仍会回到手写 cast/accessor。
521
+
522
+ 建议按 A+B 先打通 RespoNode,再实现 C;`match-type`、参数化 alias 和更积极的推断放到真实迁移数据证明有必要之后。
package/RFCs/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # RFC 整理索引
2
2
 
3
- 更新时间:2026-08-18
3
+ 更新时间:2026-08-19
4
4
 
5
5
  ## 目录原则
6
6
 
@@ -45,6 +45,7 @@
45
45
  | `08-05-systematic-nil-reduction-rfc.md` | Partial | 类型驱动减少 nil:先拆分可省略参数与 nullable 值,再迁移至 Option/Result 并逐步收紧 typed code。 |
46
46
  | `08-08-cross-backend-host-ffi-contracts-rfc.md` | Draft | 统一 JS/native/WASM/WASI 的逻辑 FFI 契约与诊断,ABI transport 保持 backend-specific;首个完整 shape consumer 为 JS/DOM。 |
47
47
  | `08-18-calcit-typed-js-ffi-boundary-rfc.md` | Draft | 在现有 Struct/Enum/Fn/trait 上补齐 JS capability gate 与 target validation;FFI metadata 不进入普通 trait 匹配和泛型推断。 |
48
+ | `08-19-transparent-union-types-rfc.md` | Partial | `deftype Name (or ...)` 具名透明联合类型、`struct-match`/predicate flow narrowing,以及与 runtime trait、external-object trait 的分工。 |
48
49
 
49
50
  ## 已执行的清理
50
51
 
@@ -0,0 +1,5 @@
1
+ # Release 0.13.21
2
+
3
+ - Publish the typed JavaScript FFI boundary and the related struct/enum type-resolution fixes merged from PR #367.
4
+ - Improve migration diagnostics for projects moving their version field to `deps.cirru`.
5
+ - Keep Cargo and npm package versions synchronized at `0.13.21`.
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calcit/procs",
3
- "version": "0.13.20",
3
+ "version": "0.13.24",
4
4
  "main": "./lib/calcit.procs.mjs",
5
5
  "devDependencies": {
6
6
  "@types/node": "^25.7.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calcit/procs",
3
- "version": "0.13.20",
3
+ "version": "0.13.24",
4
4
  "main": "./lib/calcit.procs.mjs",
5
5
  "devDependencies": {
6
6
  "@types/node": "^25.7.0",