---
title: "Type Guidance"
summary: "Dynamic 审计、Option/Result 组合、嵌套数据访问和类型化 Enum 构造"
scope: "core"
kind: "guide"
category: "features"
aliases:
  - "dynamic audit"
  - "option result"
  - "typed enum"
id: core/features/type-guidance
parent: core/features
---

# Calcit 类型使用指南

Calcit 的类型规则处于表层语言与 macro 展开后的 typed core 之间，native、JS 和 WASM 复用同一份已经建立的
类型关系。完整的分层 owner、开放值与 Agent source-fix 契约见
[`09-12-layered-semantics-and-agent-fixes-rfc.md`](../RFCs/09-12-layered-semantics-and-agent-fixes-rfc.md)。

## 精简类型表达式

类型位置可以写精简类型表达式，它与 `:: 'Fn $ {} …` 形式等价。只能放类型的位置，复合类型直接裸写；
`quote $ …` 写法同样可用，`calcit edit format` 会去掉 quote：

```cirru
let
    xs $ [] 1 2 3
    pick $ fn (x) x
  assert-type xs $ List Number
  assert-type pick $ Fn T (-> T) (:generics T)
  println $ count xs
```

嵌套回调、泛型和约束按子句分行：

```cirru.no-check
assert-type table $ Map String (List app.schema/Item)
assert-type map-fn $ Fn C
  Fn T $ -> U
  -> Dynamic
  :generics C T U
  :where $ C Mappable
```

可用位置：`assert-type`、`unsafe-coerce`、`js-cast`、`parse-cirru-edn-as`、`decode-map-as` 的类型参数，`defstruct`
字段类型、`defenum` 载荷类型、`defunion` 成员、`deftrait` 方法类型，`hint-fn` 的整个签名（`hint-fn $ Fn Number (-> String) (:async)`；
只标记异步时写 `hint-fn Async`，或 `hint-fn $ Async (:features :js-ffi)`），
以及 `calcit edit schema --code 'quote $ Fn String $ -> Number'`。Snapshot 的 `:schema` 也以这种写法保存，例如
`:schema $ quote $ Fn String (-> Number)`，单个类型名保存为 `:schema $ quote Dynamic`。检查器和各后端读到的类型相同。

`Macro` 合同同样有精简写法：位置参数是必填参数，`& T` 是剩余参数，`(-> T)` 是展开结果：

```cirru.no-check
quote $ Macro (Expr Dynamic) (& SyntaxList) (-> (Expr Struct))
quote $ Macro (Expr T)
  -> $ Expr T
  :optional (Expr Dynamic)
  :capabilities :platform-read
  :generics T
```

`calcit <snapshot> edit format` 会把 `:schema`，以及上述代码位置里的 `:: 'List 'Number`、`{} (:args …) (:return …)` 等规范形式
改写为精简写法；改写前后的类型逐一比对，无法等价表达的形式（值引用如 `:: 'List Person`、只写部分字段的 hint）保持原样。

旧写法已关闭：Snapshot 加载遇到能用精简写法等价表达的 `::`/`{}` 类型或旧 tag schema
（如 `:schema :number`、`:schema $ :: 'Fn $ {} …`、`assert-type xs $ :: 'List 'Number`）时报告
`E_LEGACY_TYPE_SYNTAX`，并给出对应写法。`edit format` 本身仍能读取旧写法，旧项目或已发布的依赖先执行一次
`calcit <snapshot> edit format` 再升级。`edit schema`、`edit def` 等编辑命令仍接受旧写法的输入，但一律以精简写法写入；
`edit schema` 会给出 `W_LEGACY_TYPE_SYNTAX` 提示：

解码与转换的目标类型（`decode-map-as`、`try-decode-map-as`、`parse-cirru-edn-as`、`try-parse-cirru-edn-as`、`js-cast`
的第二个参数）里的旧写法在加载时不报错，检查时报告非阻塞的 `W_LEGACY_TYPE_SYNTAX` 提示，并给出精简写法；
`edit format` 与其他位置一样把它们改写，例如 `decode-map-as raw $ :: 'List 'Dynamic` 改为
`decode-map-as raw $ List Dynamic`，`parse-cirru-edn-as text :number` 改为 `parse-cirru-edn-as text 'Number`。

```bash
calcit app.cirru edit schema app.main/f --code "quote $ :: 'Fn $ {} (:args $ [] 'Number) (:return 'Number)"
# ⚠️  [W_LEGACY_TYPE_SYNTAX] the `::`/`{}` schema syntax `:: 'Fn …` is retired; write `quote $ Fn Number (-> Number)` instead.
```

单个类型名（内建名或 `app.main/Person` 这样的具名类型）存为 `quote Name`，type slot 写作 `*name`。

写法规则：

- 不带 `/` 的类型名和泛型变量以大写字母开头，不加 `'`；带 `/` 的限定名大小写不限；`*name` 是 type slot。
- `(Name 参数…)` 是类型应用，覆盖 `List`、`Map`、`Option`、`Result` 和带泛型的 Struct/Enum；无参数时直接写 `Name`。
- `Fn` 先写位置参数，`& T` 表示剩余参数，返回值写成恰好一个 `(-> T)` 子句；无具体返回类型时写 `(-> Dynamic)`。
- 其余子句以 tag 开头，各占一行或写在括号里：`:generics T U`、`:where (T Trait…)`、`:async`、`:features :js-ffi`。
  `:where` 只能约束本层 `:generics` 声明的变量，内层 `Fn` 不能重复声明外层的变量。

语法错误在预处理时报告 `E_TYPE_EXPR_SYNTAX`，例如：

```bash
calcit eval 'assert-type ([] 1) $ quote $ List number'
# [E_TYPE_EXPR_SYNTAX] invalid compact type expression: `number` is not a type name; …
```

### 限制

- 小写叶子、`'x`、tag 类型和裸 `->` 不属于精简写法，会报错。
- 裸写时类型表头的最后一段须以大写字母开头；小写的限定名（如 `app.lib/box`）与函数调用写法相同，仍写 `quote $ …`。
- `.`、`=`、`forall` 留给将来的类型运算符；`:generics` 中的列表项和参数化 trait 约束暂不支持。
- `assert-type` 的值引用（如 `:: 'List Person`，`Person` 是局部绑定）仍用规范形式。

## 可省略声明的闭合 helper

普通、非递归、零参数 `defn`，在函数体能够证明完整返回类型时，可以省略重复的 root `:schema`。例如下列 helper 可由正常预处理得到 `Fn () -> Number`：

```cirru
defn initial-count () $ .count $ [] 1 2 3
```

调用这个 helper 的普通方法和回调继续读取同一份已检查签名；不是从某一个调用、测试或示例猜测公开契约。编译器只在 typed core 中补充签名，不修改 Snapshot。已有明确 schema 仍然优先；显式 `Dynamic` 不会被当成缺省声明自动收紧。

当前这一步不推断有参数 helper 的公开签名，也不放宽递归、WASM import/export、FFI 元数据和异步边界。函数体存在冲突分支、未确定的类型参数或开放容器时，仍需补足契约。省略声明不是关闭类型检查，错误使用推断结果仍会失败。

Agent 可用现有查询核对证据：

```bash
calcit query type-at app.main/initial-count --path code --format edn
calcit query context app.main/initial-count --format edn
```

`type-at` 返回完整推断函数类型；`context` 保留缺省的 `:schema`，另以 `:inferred-schema` 展示编译器证据。查询不写回声明。要主动移除已确认冗余的声明，使用 `edit schema ... --clear` 后重新严格检查及运行测试；不要批量清除无法推断的边界。

## 源码函数别名复用同一调用契约

`def legacy-name module.api/new-name` 只是引用已有函数，不应绕过类型检查。
编译器沿已解析的源码引用找到真实函数，复用其参数数量、泛型关系、callback
期望类型与 optional/rest 检查；诊断保留调用方和别名表达式。别名链无需额外
wrapper，也不需要把类型放宽为 Dynamic。

这里只追踪编译后的直接引用，不执行任意 lazy 表达式来猜测函数，不把声明
的 Fn schema 当成任意实现已经满足契约的证明。循环引用不提供新证据。
这项调用检查修复不扩大 WASM 的一等函数支持范围。

## 顶层集合初始化值

顶层集合值的 schema 会逐项检查已知字面量成员。例如 `Map Tag CssValue`，其中
`CssValue` 为 String/Number union，允许混合字符串与数值；混入 Bool 会报
`E_SCHEMA_DEF_MISMATCH`。List、Set 和嵌套集合沿用同一成员关系，错误 key 也会被拒绝。
检查不执行初始化器，也不修改运行值。原有开放值策略保留，但其他开放成员不会掩盖
同一集合内的已知矛盾；接受开放证据不等于证明 Dynamic 已满足具体类型。

作为函数实参的集合字面量同样逐项检查，泛型参数按已绑定的类型代入。例如 `base` 为
`Map Tag CssValue` 时，`.merge base $ {} (:display :block) (:height true)` 的第二个
参数报 `W_METHOD_ARG_TYPE_MISMATCH`（函数写法 `merge` 报 `W_FN_ARG_TYPE_MISMATCH`），
不再因为混合推导成 `Map Tag Dynamic` 而通过（#2589）。

## 方法接收者与返回证据

类型化方法的接收者关系先由实际接收者证明，再用于检查其余参数与推导结果。
trait 方法中的隐式接收者变量与普通泛型代换复用同一关系，不会因为未写在
方法的 `:generics` 中就退成 Dynamic；已经从接收者绑定的调用方变量保持刚性，
后续参数不能把它改成另一个具体类型。未绑定的结果变量、声明为 Dynamic 的返回、
错误参数和缺失能力仍需原有证明。

外部对象方法的精确签名仍是作者声明的 JS FFI 边界，不代表编译器解析了 JavaScript
或验证了外国实现。复用已声明的宿主返回类型时，不必为修补证据丢失而添加重复 decoder；
真实开放数据和未经建模的宿主返回仍在使用处检查。

## 局部类型断言不能覆盖已知矛盾

`assert-type` 不是值转换。已有局部类型与目标类型确定不兼容时，编译器在断言处报告
`E_ASSERT_TYPE_MISMATCH`，保留实际类型、期望类型与源码位置，而不是先改写类型信息，
等后续运算或后端执行才失败。例如 String `|hello` 不能通过断言变成 Number：

```bash
calcit eval "let ((x |hello)) (assert-type x 'Number)"
```

已证明的同型断言保持合法；断言为更宽的 Dynamic 也不应丢弃已有具体类型：

```cirru
let
    x 3
  assert-type x 'Number
  assert= 4 $ + x 1
```

此处拒绝的是已有证据中的确定矛盾，包括闭合容器 payload 类型不兼容。
开放值、未知类型与需要边界检查的关系不是同一结论；这项修复不表示所有 Dynamic 断言已完成深层验证。
外部数据应在输入边界使用真实 decoder，并处理失败；不要改成 `unsafe-coerce` 绕过已知错误。
通用断言、参数和返回值证明的后续收紧由 #1538/#1542 追踪。

## 递归参数遵守函数契约

局部 `loop` 从无 payload 的 Enum 变体开始时，编译器先根据词法尾部的 `recur` 输入推导未使用的泛型槽，再选择依赖该 payload 的静态方法。下面的值会推为 `Option<Number>`，native 与生成的 JS 都返回 `|7`：

```cirru
loop
    n 0
    value $ Option :none
  if (&< n 1)
    recur 1 $ Option :some 7
    (value .unwrap) .to-string
```

正反分支顺序、局部别名、闭包捕获及 `if` / `match` 合并保留同一类型关系，不需要把方法调用换成内部 proc。宏输入与展开结果仍按当前推导类型检查；每个逻辑宏调用只求值一次。显式 `Dynamic` payload 不会因此变成 Number，矛盾或递归嵌套的泛型方程仍拒绝。此推导不改变可变 Ref 的约束，也不扩展 backend 对局部递归形状的支持。

有固定参数的递归函数应声明 `Fn` schema。`recur` 的每个实参会按所在函数的参数位置和类型检查；即使参数个数相同，只要错序造成对应位置的类型不兼容，严格检查与 `--check-only --ns` 也会报告到调用位置。相同或兼容类型的参数互换无法仅凭类型判定语义顺序，仍需测试验证。嵌套函数的 `recur` 遵守嵌套函数自己的参数契约。不要为了让错误的递归调用通过而把参数放宽为 `Dynamic`；应修正实参顺序或在确实需要开放输入的边界显式转换。

返回类型来自函数真正退出时的值。尾位置、词法函数归属、参数个数和实参类型都通过检查后，`recur` 只表示转移到下一轮，不要求它产生一个普通返回值。例如一个分支返回 `0`、另一个分支尾部 `recur` 的函数，可以由独立的数值退出证明返回 `Number`；`foldl-compare` 的 `true` / `false` 退出同理证明 `Bool`。`if`、`let`、`do` 的尾分支沿用这一推断，不需要为循环增加 `Dynamic` 或转换。

仅有 `recur`、没有独立退出证据的函数，不能用自身声明的返回类型给自己作证；嵌套函数也不能借外层函数的退出分支取得证明。开放或矛盾的退出值、未知 helper 的返回值，以及非尾部、错误参数的 `recur` 仍需修正。此推断不证明循环一定终止，也不表示所有 backend 都支持局部闭包递归；WASM 未支持的调用形状仍会明确拒绝。

普通 `defn` 在真正的尾位置直接调用自身时，编译器会在完成现有调用与返回类型检查后，复用 `recur` 的帧复用路径；例如 `match (destruct-list items)` 的 `(:some item rest-items)` 分支最后的 `(walk rest-items)` 可以遍历长列表而不增长 native 调用栈。此优化只适用于解析到当前顶层定义的直接调用，以及 `if`、`let`、`match` 的尾分支；被局部变量遮蔽的同名函数、间接调用、嵌套函数和非尾调用不改写。带可选或剩余参数等特殊参数形式的函数暂不自动改写。显式 `recur` 仍适合需要清楚表达循环意图的地方。

## 可变 Ref 的空初值

`Ref<T>` 的 payload 类型在构造时确定，之后的 `reset!` / `swap!` 只按该类型检查，不会反过来改写它。初值是无 payload 的变体（如 `Option :none`）时，需要在初始化表达式上给出 `Option<T>` 上下文：

```cirru
let
    cell $ ref $ assert-type (Option :none)
      quote $ Option Number
  reset! cell $ Option :some 7
  assert= 7 $ .unwrap-or (deref cell) 0
  reset! cell $ Option :none
  assert= 0 $ .unwrap-or (deref cell) 0
```

同一个 Ref 经局部别名写入 `Option :some |wrong` 时，同样报告 `W_RESET_ARG_TYPE_MISMATCH`。省略上下文时，`ref (Option :none)` 的 payload 槽固定为 Never，下面的写入会被拒绝，诊断会给出上面的初始化写法：

```cirru.no-check
let
    cell $ ref $ Option :none
  reset! cell $ Option :some 7
```

这样 Ref 的类型只由构造位置决定，与后续写入的出现顺序无关，也不会把尚未推导的槽当作 Dynamic。`loop` 参数可以从 `recur` 输入推导空变体的 payload，但 Ref 不按写入推导。

## JavaScript 可空值的容器合同

`JsNullish<T>` 表示 JavaScript 边界上可能缺席的值。已经证明为 `T` 的值和 `nil` 可以进入这个合同；同一关系递归适用于不可变 List、Map 与 Set，不需要逐层插入转换。例如一个保存 `Map<Tag,JsNullish<Number>>` 的 Struct 字段可以接收 `{} (:count 1)` 或 `{} (:count nil)`，具体回调同样可以存入可空回调表，其参数、返回值和 arity 仍按完整签名检查。

反方向仍需要检查：`JsNullish<T>` 不能直接作为 `T` 使用，Dynamic 元素也不能仅凭容器声明变成具体元素。可变 `Ref<T>` 保持不变性，不能把 `Ref<Number>` 扩大为 `Ref<JsNullish<Number>>` 后写入 `nil`。这项关系不把推导出的 nil 合并 `T?` 与 `JsNullish<T>` 混同，Calcit 自身的缺席值继续使用名义 `Option<T>`。

声明返回 `JsNullish<T>` 的函数按各个退出值检查。例如 `if present? value nil` 在 `value` 已证明为 `T` 时满足声明；反向分支、`match` 的已知 payload 和函数尾部 `let` 的局部别名采用同一检查。无需为了中间合并出的 nil（内部 `T?`）插入转换。初始化值按父作用域验证，新绑定和 match payload 遮蔽同名外层变量的证据。返回检查不修改局部变量的推断类型，也不把推导出的 `T?` 当成 `JsNullish<T>`。

已知字段、返回或显式 `assert-type` 合同也会逐项检查 List、Map、Set 字面量。例如 `Map<Tag,JsNullish<EventHandler>>` 可以接收 `{} (:click typed-handler) (:focus nil)`，嵌套集合采用同一规则；每个回调仍须具有兼容的完整签名。需要保存并复用混合集合时，可以在字面量处用 `assert-type` 明确检查合同。没有期望合同的异质集合仍按既有规则推导，开放的 Map 参数不会因此获得具体成员类型。检查不改变运行值，异步返回也不会自动等待集合中的 Promise。

## Dynamic 是边界，不是默认多态

普通泛型函数会按实参顺序传递已知的类型关系。例如 `filter-map-kv` 接收 `Map<String,Number>` 时，后续回调的参数可推为 `String` 和 `Number`；回调返回的 `MapEntryDecision<R,S>` 也保留泛型实参，让结果 `Map<R,S>` 继续约束后续写入。无需为了传递这些关系手工增加 `hint-fn` 或 `assert-type`。如果输入本身是显式 `Dynamic`，推断不会凭空把它收窄为某个具体类型，仍应在真实边界处 decode 或 narrow。

`Dynamic` 适合 JS FFI、框架开放数据、宏和确实无法提前知道的外部输入。普通函数不要用多个 `Dynamic` 表示“它们应该是同一个类型”：输入和返回关联时用 `:generics` 与 TypeVar；只需要能力时用 trait 与 `:where`；同质集合写出元素类型；有限异构数据定义为 Enum；可缺失值使用 `Option<T>`，带失败信息使用 `Result<T, E>`。

Dynamic 表示用户明确选择的开放 Calcit 值，不等于编译器尚未推断出的 Unknown/Unresolved，也不等于宿主拥有的
`JsObject`。开放值可以被保存、传递、放入开放容器或包装进 `Option<Dynamic>`；只有将其当成 Number、String、
具体 Struct/Enum 或某个 trait 能力使用时，才需要 decode、narrow 或显式 unsafe boundary。真正参数化且不观察
内容的泛型函数可以携带 Dynamic；声称具体内容关系但没有证据的调用继续拒绝。

`&trait-call Trait :method receiver ...` 用于消除不同 trait 的同名方法歧义，不是对 receiver 的强制转换。严格检查会拒绝无法静态识别的 trait/method、已知 Fn 方法签名的错误参数个数，以及已知内建 receiver 上确实不存在的 impl；例如 Number 不会因为显式写了 `Countable` 就获得 `.count`。局部 `impl-traits` 组合与具名泛型的附着来源目前尚不能在所有路径完整保留，缺失的静态证明不能误报成“确定没有 impl”；这类调用仍由运行时核对。普通业务优先使用带 schema 的接收者方法，让现有 trait 关系参与推断。

普通执行、编译和严格检查只报告可执行的 warning/error，不计算 Dynamic 比例。迁移存量代码时再显式查询具体位置：

```bash
calcit analyze weak-types --only coverage --summary-only
calcit analyze weak-types --only schema-dynamic,unresolved-type-slot,code-dynamic --intent unresolved --format edn
```

## 用具体类型代替裸 `Enum` / `Struct` / `Trait` / `Fn`

类型位置里的裸 `Enum`、`Struct` 接受任意 enum / struct，但丢掉了具体定义：读出的载荷和字段都是 Dynamic，
传给需要具体类型的函数时还要额外的边界证明。裸 `Trait` 接受任意 trait 定义，裸 `Fn` 丢掉参数和返回类型。
写出具体定义：

```cirru.no-check
; 处理应用自己的操作 enum，而不是任意 enum
hint-fn $ Fn app.schema/Op (-> Unit)
; 泛型容器的类型参数同样写具体类型
assert-type store $ quote $ reel.typed/State app.schema/Op app.schema/Store
; 回调写出签名
defstruct Button (:on-click $ :: Fn Dynamic (-> Unit))
```

确实接受该族任意成员的位置写 `AnyFn`、`AnyStruct`、`AnyEnum`：它们和裸 `Fn`、`Struct`、`Enum` 是同一个类型，检查、调用和运行时字段校验都一样，只是表明这里有意开放，不报 `E_BROAD_TYPE`。

```cirru.no-check
; 任意参数个数的回调，例如 memo 辅助函数
:schema $ quote $ Fn AnyFn (List Dynamic) (-> Dynamic)
; 对任意 struct 做反射
:schema $ quote $ Fn AnyStruct AnyStruct (-> Bool)
; 库回调承载应用定义的 enum；现有 `Fn Enum (-> Unit)` 的调用方不受影响
:schema $ quote $ Fn (Fn AnyEnum $ -> Unit) (-> Unit)
```

库需要把调用方的具体类型原样传回时（如 `reel.typed/State` 的操作类型参数），用 `:generics` 类型变量。类型变量不限定为 enum，可以绑定任意类型，所以它不等价于 `Enum`；只对任意 enum 做底层操作的少数位置会继续得到提示。

0.36.0 起检查时裸 `Fn` / `Struct` / `Enum` 报告 `E_BROAD_TYPE` 错误（裸 `Trait` 仍是非阻塞的 `W_BROAD_TYPE`），范围是项目自身的定义代码、examples、tests 和 `:schema`：
`assert-type`、`unsafe-coerce`、`hint-fn`、解码目标、函数返回标注、`defstruct` 字段、`defenum` 载荷和 `deftrait` 成员。

```text
[E_BROAD_TYPE] [Error] bare `Enum` accepts any enum and drops its variants, so payloads read back as Dynamic; name the concrete enum, such as `Option Number` or `app.schema/Action`. Write `AnyEnum` where any enum is intended. @app.main/on-op @3.1.1
```

函数 `:schema` 里参数位置的裸 `Enum` / `Struct`（包括回调参数的参数，如 `Fn (Fn Enum (-> Unit)) …`）会参考本次检查到达的调用点：
所有调用都传同一个具体类型时，提示末尾说明并在 hint 里给出 ``write `app.schema/Op` at arg 1 of …``；调用传了几种具体类型时列出它们，
建议改用 union 或 `:generics`；没有被检查到的调用，或有调用传入的值本身就是裸类型、`::` 匿名 enum、Dynamic 时，不给候选。
统计只覆盖从入口检查到的调用，定义 `:tests` 里的调用不算，所以改完要连同 `:tests` 一起跑。裸 `Fn` 不给候选签名；带类型参数的候选（如 `Option Number`）也不给，参数写法交给作者；剩余参数（`&`）位置不统计。

定义级 `:schema` 写的是定义值自身的种类：`defstruct` 为 `StructDef`，`defenum` 为 `EnumDef`，`deftrait` 为 `Trait`。
`deftrait` 的 `Trait` 不提示；`defstruct` / `defenum` 的 `:schema` 写成 `Enum` 或 `Struct` 时，提示改为对应的种类。

### 限制

- `E_BROAD_TYPE` 让 `--check-only`、严格检查（该定义记为 failed，依赖它的定义记为 blocked）和运行前的预检失败；裸 `Trait` 的 `W_BROAD_TYPE` 只是提示，检查仍通过。`edit format` 不改写这两种写法：具体类型只有作者知道。
- 内置的 `calcit.core`、`calcit.internal` 等命名空间里对任意 enum / struct 工作的底层 proc 保留宽泛签名，依赖模块的定义不报告。
- `AnyFn` / `AnyStruct` / `AnyEnum` 不约束到某个具体定义，读出的载荷、字段和调用结果仍是 Dynamic；能写出具体类型时仍应写具体类型。

### 开放类型的理由 `(:open …)` 与 `W_OPEN_FAMILY`

开放类型（`AnyEnum`、`AnyFn`、`AnyStruct`、`Dynamic`）只应留在确实需要的位置。确认过的签名在 `Fn` 里加一个 `:open` 选项写明理由，和 `:generics`、`:features` 同一层：

```cirru.no-check
; 对任意 enum 做反射（显示、比较、序列化）
:schema $ quote $ Fn AnyEnum (-> String) (:open :reflect)
; 接收解码前的数据
:schema $ quote $ Fn Dynamic (-> app.schema/Config) (:open :decode)
; 透传 JS 宿主值
:schema $ quote $ Fn Dynamic (-> Unit) (:features :js-ffi) (:open :host)
```

理由只有 `:reflect`、`:decode`、`:host` 三种，不改变类型：带不带 `:open` 的同一签名是同一个类型。

普通 `--check-only` 通过且计数不为零时，在最后打印一行 `Open types: [W_OPEN_FAMILY] …` 汇总（检查失败、`--all-defs` / `--keep-going` / `--incremental` 和运行前的预检不打印），统计项目自身定义 `:schema` 里没有 `:open` 理由的 `AnyEnum`、`AnyFn`、`AnyStruct` 和 `Dynamic` 个数，并指出第一个出现的定义。带 `:open` 的 `Fn` 连同它内部的类型都不计入；依赖库和内置 core 不计入。汇总不算 Notes，不进入 `--format json` 报告，也不改变检查结果。`calcit analyze weak-types` 只列出 Dynamic 的位置，`AnyEnum` / `AnyFn` / `AnyStruct` 的逐处列表暂未提供。

## 定长元组 `Tuple`

`Tuple` 描述长度固定、每个位置类型各自固定的 List，例如二维点 `Tuple Number Number`、属性对 `Tuple Tag String`。运行时它仍是普通 List，native、JS 和 WASM 的表示不变；元组只存在于类型检查中。

```cirru
let
    point $ assert-type ([] 3 4) (quote $ Tuple Number Number)
    entry $ assert-type ([] :title |Calcit) (quote $ Tuple Tag String)
  let[] (key title) entry
    assert= :title key
    assert= 9 $ &+ (nth point 0) (&str:count title)
```

- **写法**：`quote $ Tuple A B …`，至少一个位置；可以嵌套，例如 `List (Tuple Tag Number)`、`Fn (Tuple K V) (-> Bool)`。
- **字面量**：在期望类型是元组的位置（参数、返回值、带 schema 的 `def`、`assert-type`），`[]` 字面量按位置检查，长度不同或某个位置类型不符都会报错。
- **读取**：`let[] (x y) t` 让每个变量得到对应位置的类型。`nth t 0` 的下标必须是字面量，返回该位置的值本身，不包 `Option`（位置总是存在）；越界或非字面量下标报 `E_TUPLE_INDEX`。`let[]` 绑定的变量多于元组长度时报 `W_TUPLE_INDEX`。
- **与 List 的关系**：元素同类型的元组（如 `Tuple Number Number`）可以当作 `List Number` 使用，`first`、`map`、`conj` 等按 List 处理，结果是 `List T`。元素类型不同的元组不能传给任何 List，包括 `List Dynamic`；`List T` 也不能隐式当作元组，长度需要先用 `let[]` 或字面量证明。
- **能力**：`count`、`=`、`str` 对所有元组可用，同构元组还可以 `map`。`rest`、`reverse`、`sort` 这类返回同一类型的 trait 泛型操作不接受元组，因为结果的长度已经变了；需要时先把同构元组作为 `List T` 传入再调用。
- **解码**：`decode-map-as`、`try-decode-map-as`、`parse-cirru-edn-as` 等受检解码在 Native 与 JS 上可以把 `Tuple` 作为目标，也可以作为 `defunion` 的成员；WASM 后端的受检解码目前不支持 `Tuple` 目标。输入必须是长度完全一致的 list，逐位置检查类型；长度不对或某个位置不符时返回解码错误，例如 `expected tuple of 3 items, got 2`。解码结果在运行时仍是普通 list。

### 限制

- 元素类型不同的 `[]` 字面量在没有期望类型时仍推断为 `List Dynamic`，需要元组时写出 schema 或 `assert-type`。
- Map 的 key/value pair 从 0.35.0 起是 `Tuple K V`（`&map:to-list`、`filter`、`map` 回调等，#2547、#2561）。

## 解析成功不等于数据类型验证成功

String 的 `.parse-cirru-edn` 返回 `Result<Dynamic,String>`：`:ok` 只说明文本符合 Cirru EDN 语法，
不证明内部元素符合业务类型。`.parse-json` 也是开放协议边界；JSON 不是 Calcit 的默认数据格式。
查询显示 `open` 时，不应凭调用名字、声明返回类型或浅层 `assert-type` 把 payload 当成已验证的具体值。

已知目标形状时，优先在输入边界使用 `try-parse-cirru-edn-as`，由现有 decoder 真正检查每层数据。
若数据已经作为开放值保存或传递，再使用 `try-decode-map-as`；它不只接受 Map，也能验证声明的 List 等闭合形状。
两个入口都返回 `Result<T,String>`，业务必须处理失败，而不是自动插入 unwrap、默认值或 unsafe cast。

反方向用 `encode-map-as value TypeExpr`（0.35.0，#2562）：值必须已经是声明的类型，结果是 `decode-map-as` 能读回的开放形状，
满足 `decode-map-as (encode-map-as x T) T` 等于 `x`。Struct 变成 tag 为 key 的 Map，`Option :some x` 字段写 `x`，
`Option :none` 字段省略；Enum 保留名义值，只编码 payload。不再需要手写 `user-to-wire` 一类函数，详见 [Cirru EDN](./data/edn.md)。

```cirru
assert=
  Result :ok $ [] $ [] 1 2
  try-parse-cirru-edn-as "|[] ([] 1 2)" $ quote $ List (List Number)
match
  try-parse-cirru-edn-as "|[] ([] 1 |bad)" $ quote $ List (List Number)
  (:err message)
    assert= true $ message .includes? |[0][1]
  (:ok _) (raise "|无效的嵌套 Number 不应通过验证")
```

后一例语法合法，但第二层 List 的第二个元素是 String；错误保留 `[0][1]` 路径，便于 Agent 定位，
不依赖整段错误文本或给开放解析套 nominal wrapper。这些深层正反例在 native 和生成的 JS 上共享相同 Calcit 测试。
当前 WASM typed EDN parser 只支持已声明的标量及顶层同质标量集合子集；嵌套集合仍明确拒绝，
本例不表示新增 WASM 支持，也不替代 #1538 的通用断言/调用证明收紧。

## Schema 类型别名在各后端一致展开

非 Dynamic definition schema 可以通过完整的 `'namespace/definition` 名称作为类型别名使用。别名只复用底层类型关系，不创建新的运行时包装；需要名义身份时仍应使用 `defstruct` 或 `defenum`。

```bash
calcit edit def app.schema/Items --code 'quote $ def Items $ []'
calcit edit schema app.schema/Items --code "quote \$ List Number"
calcit edit def app.schema/NamedCallback --code 'quote $ defn NamedCallback (text) (count text)'
calcit edit schema app.schema/NamedCallback --code "quote \$ Fn String (-> Number)"
```

别名定义本身仍是一个普通的值，其他代码可以直接读取它，因此初始化表达式必须属于它发布的 schema：数据别名给出该类型的代表值（如上例的空 List），Fn 别名使用参数个数与 schema 一致的 `defn`，函数体按同一签名检查。`def Items &unit` 这类占位写法声明了 `List<Number>` 却求值为 Unit；只要该定义被预处理（作为值被引用，或 `--check-only --all-defs` / `--ns` 检查全部定义），就会报告 `E_SCHEMA_DEF_MISMATCH`，诊断会提示替换 `&unit` 占位。

此后 `'app.schema/Items` 可用于 Struct 字段、Enum payload、callback 或 trait 边界。静态检查、Native 构造验证和 JavaScript codegen 都按同一底层 schema 判断值；不要为 Native 单独增加 coercion。类型别名不能接收泛型实参，循环别名也不会退化成 Dynamic，而是作为无法证明的类型关系拒绝。

## 数值 refinement 只携带边界证明

需要与外部 ABI 的明确宽度对齐时，可使用 `'Int8`、`'UInt8`、`'Int16`、`'UInt16`、`'Int32`、
`'UInt32`、`'Int64`、`'UInt64`、`'Float32` 和 `'Float64`。它们不是新的运行时数值族：native 仍使用
`f64`，JavaScript 仍使用 `Number`。refinement 可以安全地作为 `'Number` 使用；普通 `'Number` 不会隐式收窄，
普通算术结果也回到 `'Number`，避免在数值变化后继续保留失效的范围证明。

从普通数值进入明确边界时，使用 `number->int8`、`number->uint8`、`number->int16`、
`number->uint16`、`number->int32`、`number->uint32`、`number->int64`、`number->uint64`、
`number->float32` 或 `number->float64`。这些函数返回 `Result<目标类型,String>`，不会截断、环绕或静默舍入：
整数目标拒绝非整数、NaN、Infinity、符号错误和越界值，`Int64`/`UInt64` 只接受 JavaScript 与 `f64`
都能连续精确表示的安全整数范围；`Float32` 只接受能原样往返的值。

```cirru
let
    port-result $ number->uint16 8080
  port-result .ok?
```

当前 source-level refinement 与受检转换在 native、JavaScript 后端一致。Component contract 与 Canonical ABI
lowering 也保留这些宽度：整数使用 `i32` / `i64`，浮点数使用 `f32` / `f64`；进入统一 `Number`
表示前后都会检查范围、整数性、符号与精确性，失败直接 trap，不会偷偷降级、截断或舍入。

兼容性的多态 collection facade 可能仍在 core schema 中保留局部 `Dynamic`，或者使用彼此独立、无法表达容器成员关系的泛型，但已知 receiver 会在预处理阶段专门化。例如 `update` 对 `List<T>` 要求 `Number` 索引和 `T -> T` updater，对 `Map<K,V>` 要求 `K` 键和 `V -> V` updater；Struct 则按静态字段类型检查。`filter`、`any?` 与 `every?` 对 List/Set 要求 `T -> Bool` predicate，`each` 则约束 callback 输入为 `T`、允许任意返回类型；`map` 对 List/Set 要求 `T -> U` mapper，并把 Set receiver lowering 到 `&set:map`。`foldl` 与 `reduce` 从初始值恢复 accumulator `U`，并要求 reducer 为 `U, T -> U`；原生 `foldl` 只有在 reducer 具有具体且兼容的 `Fn` 签名时才把初始 accumulator 类型保留为返回类型，`DynFn` 仍推断为 `Dynamic`。普通 `apply f args` 只会在 `args` 是非 Dynamic 的同质 `List<T>`、`T` 能满足 `f` 的全部 fixed/rest 输入、且展开长度能证明 callable arity 时恢复 `f` 的具体或泛型返回类型；若参数位置异构、固定参数调用的 list 长度未知、callable 未知，或存在 trait-bounded 泛型，则兼容返回仍为 `Dynamic`，应改为直接调用、先归一化参数，或在审核过的开放边界显式保留 Dynamic。双参数 `sort` 与 `&list:sort` 保留 `List<T>`，并要求 comparator 为 `T, T -> Number`；函数形式的 `&list:sort-by` 要求 selector 为 `T -> K`，同时保留 Tag 字段选择器兼容路径。List 的 `.apply` 要求函数列表中的每一项共享 `T -> U` 契约，并返回 `List<U>`；它的 direct/method 诊断会用 receiver/input 已绑定的 `T` 显示具体 callback 类型，异构输入或函数列表必须先归一化或拆成多次调用。`interleave` 同样只接受两份 `List<T>` 并返回 `List<T>`；异构数据必须先归一化，或在经过审核的开放边界显式声明 `List<Dynamic>`。单参数自然排序不受影响，Syntax collection 继续使用 phase-aware 开放契约。Map callback 接收运行时的异构 `[key value]` pair；迭代、predicate 与 fold 输入只承诺 `List<Dynamic>`，而 `map` 同时要求 callback 返回另一个 `List<Dynamic>` pair，不会把不同的 `K` / `V` 伪装成同一种成员类型。旧 `map-kv` 还允许 nil/任意 Enum 作为 drop sentinel，因此返回只能是显式兼容边界：兼容模式警告，strict 模式拒绝。typed code 应统一改用 `filter-map-kv`，以 `MapEntryDecision :keep key value` 或 `:drop` 让输出 key/value 与 callback payload 保持可证明关联。`get` 同样要求 List/String/Enum 的 `Number` 索引或 Map 的 `K` 键，`includes?` 要求 List/Set 的成员 `T`、Map 的值 `V` 或 String substring；`contains?` 要求 List/String/Enum 的 `Number` 索引、Map 的键 `K` 或 Set 的成员 `T`。`assoc` 会同时约束 List 的索引/成员、Map 的键/值、静态 Struct 字段的值类型，以及 Enum 的 `Number` payload index；Enum payload 可以异构，因此新值在没有精确 variant/slot evidence 时仍保持开放。`dissoc` 会检查全部 rest 参数：List 只能接收 `Number` 索引，Map 的每个键都必须是 `K`。用户函数 schema 的 `:rest` 会逐项检查。原生 proc 按运行时契约区分两类 typed variadic：`&map:dissoc`、`&list:concat` 与 `&merge` 会检查每一个 rest 参数，并在容器不匹配时显示完整成员类型；`[]` 与 `#{}` 的 `Variadic<T>` 只用于推断公共成员类型，异构字面量仍有意回退为 `Dynamic`。未标注的 inline callback 若能从函数体恢复返回类型，也会参与这项检查。不要把 receiver 擦除为 `Dynamic` 来绕过这些关系：在 FFI/open-data adapter 中先校验或转换，再进入集合操作。

对已知 receiver 的集合调用，成员类型会先进入 inline callback 的参数与函数体，再由同一份 contract 检查调用参数和推断返回值。`any?`、`every?` 与 `each` 因而不需要让 callback 暂存未实例化的泛型，也不需要为使用它们的宏增加例外。

## 严格检查与迁移期 quality baseline

新项目以默认严格预处理和实际目标测试作为发布门禁：

```bash
calcit calcit.cirru --check-only
calcit calcit.cirru --entry test
```

`analyze weak-types --only coverage` 与 `analyze weak-types` 只帮助定位迁移清单，不决定程序是否类型正确，也不输出 Dynamic 比例、shape/family 排名或另一套关系判断。需要清理时使用 kind、intent、definition、path 和 detail 回到源码；值能否进入 typed code 只由严格预处理诊断决定。

对于省略 schema、且编译器已经能够证明完整契约的封闭 helper，`weak-types`（含 `coverage` 视图）和 `quality` 使用同一份推断结果；增量分析也遵循这一规则。删除这类冗余标注不应增加迁移债务。分析不会把推断结果写回源码，也不会替换显式 `Dynamic`；推断失败的定义仍按原有缺失契约报告。

已弃用的 `analyze quality` 与 baseline 在下一个非 patch 版本删除，用法见 [静态分析](features/static-analysis.md)；它们不能替代严格预处理和实际目标测试。

不要为新项目生成 baseline，也不要增加 metric、intent 或统计协议。迁移不提高已有预算，并同时保证默认严格检查通过；债务清零后从项目 CI 删除 baseline 命令和文件。代码能否进入具体类型由同一套类型证明决定，不以 coverage 或 Dynamic 数量充当正确性证明，也不预告未经交付的删除版本。

baseline 是已提交的 Cirru EDN 机器生成工件。为使 GitHub 语言统计忽略其行数，同时保留文本 diff，
可在项目根目录的 `.gitattributes` 加入生成物标记：

```gitattributes
config/*-quality.cirru text linguist-generated=true
```

这不会忽略或删除 baseline；文件仍保留文本 diff，但不计入语言统计。只有外部工具明确要求 JSON
时才使用 `.json` 输出路径。更新 baseline 的 PR 仍应按 definition 审阅预算变化。

## Option / Result 组合

优先让 `Option` / `Result` 的方法表达类型流，而不是逐层 `unwrap` 或调用
`option:*` / `result:*` 的函数形式：

```cirru.no-check
user .and-then $ fn (user)
  (get user :profile) .and-then $ fn (profile) (get profile :name)

loaded .and-then $ fn (value) (validate value)
```

备用来源使用 `.or-else`。`.unwrap-or` 只用于确实需要默认值的终点，`.map` 用于同步转换，`.and-then` 用于下一个仍可能失败的操作。保留 `Option` 本身能让类型系统持续检查缺失路径；不要为了集合判断而把它解成 `nil`。

多个连续的 Option 或 Result 步骤嵌套使用接收者 `.and-then`，让错误类型的转换
保持可见：

```cirru.no-check
let
    source $ fs:path |data.cirru
    content-result source.read-text
  content-result.and-then $ fn (content)
    (parse-data content) .and-then $ fn (data)
      save-data data
```

先用 `fs:path` 把 UTF-8 String 提升为 nominal `FsPath`，再调用 `.read-text`、
`.read-dir`、`.walk-dir` 或 `.write-text!`，这些方法返回 `Result<...,String>`。
String 本身不携带文件系统语义；旧 `try-read-file` / `try-write-file` 已退役，
先通过 `fs:path` 构造路径再调用 `.read-text` / `.write-text!`。旧 `.write-text` 已在 0.29.0 退役，
调用会报告 `E_RETIRED_METHOD`；`try-read-dir` 与底层 raising procedures 暂留为兼容入口。
各入口的迁移窗口和保留原因见[升级指南](run/upgrade.md)，首选命名与失败合同见[API 角色](features/api-roles.md)。
这些文件效果支持 native 与生成的 JavaScript。WASI 0.3 Component command 目前支持基于
preopen 的 `.read-text` / `.write-text!`（UTF-8，最多 4 MiB）。写入采用 create + truncate，
失败可能留下截断或部分内容，不是原子替换；超限在打开前拒绝。`.read-dir` 和 `.walk-dir`
仍待实现。core WASM 明确拒绝宿主文件效果。

需要同步等待时使用 `wait-ms`，让缺少宿主能力或等待失败继续保留在
`Result<Unit,String>` 中。参数是明确的整数毫秒，不接受隐式舍入；异步调度仍使用
独立的 callback/task API，不把两种控制流混成一个动态接口。

Native 异步 FFI capability 也遵循相同边界原则。模块适配层用 `ffi:task`、
`ffi:response` 把不透明 AnyRef 提升为 nominal `FfiTask`、`FfiResponse`，业务层调用
`.cancel!`、`.cancel-with!`、`.resolve!`、`.reject!`。旧名暂留兼容。raw 字段保持 `Dynamic`，但
reason/payload 使用方法级泛型，因此不会为了宿主编码而抹掉调用侧类型。底层
`&ffi-task-cancel`、`&ffi-response-*` 只作为适配与兼容入口。

普通组合函数以接收者方法
作为公开形式，`option:*` / `result:*` 直接函数调用主要保留给 core lowering。

## get-in / assoc-in / update-in

`get-in` 是可能失败的开放数据访问，返回 `Result<T, PathError>`；开放输入通常为 `Result<Dynamic, PathError>`。路径缺失或遍历到 nil 返回 `PathError :missing`，中间值形状错误返回 `PathError :shape message`，不能都按旧 Option 的缺失分支处理。当接收者是完整类型的嵌套 Map、路径是非空字面量时，编译器会保留叶子类型，并把路径访问/重建展开为类型化链；调用参数仍按源码顺序各求值一次。`update-in` 的 updater 仍接收 `Option<Leaf>`，重建结果保留同质 Map 类型。空路径查询返回成功的非 nil 接收者；空路径更新仍用 Option 传递整个接收者，回调输出可以是不同类型。版本边界和错误处理见[路径查询迁移](run/upgrade.md#get-in-result)。

动态接收者、动态路径和混合容器仍是明确的兼容边界，会保留动态路径 API。新代码在数据形状已知时优先使用直接 `.get`、Option 组合和名义字段访问；只有开放数据才使用路径 API。不要用路径函数绕过 Struct 字段检查：Struct 应使用 `(:field value)` 或 `value.:field`，字段可缺失就把字段声明为 `Option<T>`。

已知 Map 前缀下面有 Dynamic 时，不必把整个写入结果也降成 Dynamic。非空字面量
路径的已知 key 与 Map key 类型匹配后，`assoc-in` / `update-in` 可以保留成功结果的
Map 前缀类型，例如 `Map<Tag, Dynamic>`；子节点仍是 Dynamic，使用前仍须检查。
这只是结果类型证明，不会展开未知遍历或证明深层字段存在。空/未知路径、错误 key
及替换已知 leaf 家族不能据此获得原 Map 类型；公开运行时检查、失败和求值顺序不变。
开放 Map 遍历仍受 WASM/WASI 的 nil 类型证据限制，不因前缀推断改善而自动可编译。

开放路径的回调输入是 `Option<Dynamic>`，输出类型与输入独立；具体使用 payload 前先 decode 或 narrow。声明 `Option<Number>` 的回调不能证明开放数据本来就是 Number，也不能仅凭返回 Number 把输入收窄。缺失仍为 `Option :none`，存在 nil 的叶子与缺失分支不混同。升级步骤见[开放路径更新合同](run/upgrade.md#0300-开放路径更新合同)。

`Map<K,V>` receiver 的 `.get`，`List<T>` receiver 的 `.get`、`.first`、`.last`，以及 String、Enum receiver 的 `.get`、`.nth`、`.first`、`.last`，会在 preprocess 阶段降低为对应类型的 `&scope:*` primitive 与显式 Option 构造，不再执行通用函数中的 receiver type predicates。对应的前缀形式保持兼容并使用同一 lowering；业务代码优先使用 receiver 形式来保留类型意图。

对 `update-in` 的缺失值给默认值或明确处理 `Option :none`，不要无条件 unwrap：

```cirru.no-check
update-in data ([] :settings :retries)
  fn (current) (current .unwrap-or 0)
```

## Enum 构造

Struct 也支持同样的类型化头部调用：

```cirru.no-check
defstruct Profile (:name String)
  :bio $ quote $ Option String

Profile :name |Ada
```

参数必须是 `:field value` 对，必填字段不能省略；末尾声明为 `Option<T>`
的字段可以省略，Calcit 会补成 `Option :none`。需要显式控制所有字段时使用
`%{} Profile ...` 并完整提供字段。`%{}?` 与底层 `&%{}?` 已退役，
包括 `--compat-types` 在内都会以 `E_PARTIAL_STRUCT_NIL_FILL` 拒绝；不要用隐式 `nil`
模拟可缺失字段。

已知 Enum 定义时使用头部调用：

```cirru.no-check
Option :some value

Result :err message
```

Calcit 会根据 Enum 定义检查 variant 和 payload，并在预处理阶段生成命名构造。`%::` 保留给显式 prototype、动态跨模块构造和兼容旧代码的边界。
