@calcit/procs 0.12.19 → 0.12.21

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 (36) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/editing-history/202507161633-wasm-data-structures.md +61 -0
  3. package/editing-history/20260416-1936-predicate-narrowing-expansion.md +31 -0
  4. package/editing-history/202604161507-wasm-data-structures-and-rfc-rename.md +27 -0
  5. package/editing-history/202604161520-wasm-bitwise-and-match.md +21 -0
  6. package/editing-history/202604161542-wasm-cross-ns-host-imports.md +38 -0
  7. package/editing-history/20260417-0026-wasm-rest-args.md +62 -0
  8. package/editing-history/202604170048-wasm-type-of.md +70 -0
  9. package/editing-history/202604170051-wasm-derived-predicates.md +34 -0
  10. package/editing-history/202604170132-monomorphize-map-filter.md +50 -0
  11. package/editing-history/202604170135-monomorphize-includes-reverse.md +31 -0
  12. package/editing-history/202604170140-fold-type-predicates.md +44 -0
  13. package/editing-history/202604170154-generic-dispatch-records-tuples.md +52 -0
  14. package/editing-history/202604170520-simplify-generic-defns-via-method-dispatch.md +49 -0
  15. package/editing-history/202604172316-wasm-do-println-fixes.md +41 -0
  16. package/editing-history/202604181208-split-emit-wasm-and-bump-version.md +15 -0
  17. package/editing-history/202604181430-wasm-list-match-and-method-dispatch.md +5 -0
  18. package/lib/calcit.procs.mjs +92 -6
  19. package/lib/package.json +2 -1
  20. package/package.json +2 -1
  21. package/rfc/02-04-runtime-traits-plan.md +613 -0
  22. package/rfc/02-14-project-modernization-roadmap.md +229 -0
  23. package/rfc/02-17-register-platform-api-rfc.md +115 -0
  24. package/rfc/02-18-language-theory-evolution-plan.md +367 -0
  25. package/rfc/02-23-optional-record-macro-plan.md +30 -0
  26. package/rfc/03-05-function-schema-dual-track-rfc.md +162 -0
  27. package/rfc/03-16-runtime-boundary-refactor-plan.md +546 -0
  28. package/rfc/03-18-query-def-tree-show-chunked-display-plan.md +301 -0
  29. package/rfc/04-13-call-arg-literal-rewrite-rfc.md +205 -0
  30. package/rfc/04-13-type-slot-mechanism-rfc.md +194 -0
  31. package/rfc/04-15-match-syntax-rfc.md +175 -0
  32. package/rfc/04-15-type-directed-optimization-catalog.md +170 -0
  33. package/rfc/04-15-wasm-compilation-feasibility.md +236 -0
  34. package/rfc/04-16-wasm-data-structures.md +192 -0
  35. package/rfc/README.md +40 -0
  36. package/ts-src/calcit.procs.mts +87 -6
Binary file
@@ -0,0 +1,61 @@
1
+ # WASM Data Structures: List, Map, Set + Optional Args + Raise
2
+
3
+ ## Summary
4
+
5
+ Comprehensive data structure support for WASM codegen: list (16 ops), map (9 ops), set (6 ops), plus `raise` and optional argument handling.
6
+
7
+ ## Changes
8
+
9
+ ### 1. Optional Args (`CalcitArgLabel::OptionalMark`)
10
+ - `compile_fn`: Skip `OptionalMark` markers — they aren't parameters, just annotations. The actual Idx labels become WASM params.
11
+ - Added `fn_arity` map alongside `fn_index` to track each function's WASM arity (counting only `Idx` labels).
12
+ - At call sites (`Calcit::Import`, `Calcit::Symbol`), pad missing optional args with `f64.const 0` (nil) to match target arity.
13
+ - **Key insight**: The Calcit preprocessor doesn't always fill nil for omitted optional args in the code tree; WASM codegen must handle the arity mismatch at the call site.
14
+
15
+ ### 2. Raise
16
+ - `CalcitProc::Raise` → evaluate and drop all args, emit `Instruction::Unreachable`.
17
+
18
+ ### 3. Memory Layouts
19
+ - **List**: `[count:f64] [elem0:f64] [elem1:f64] ...` — size `(1+count)*8`
20
+ - **Map**: `[count:f64] [key0:f64] [val0:f64] [key1:f64] [val1:f64] ...` — size `(1+count*2)*8`
21
+ - **Set**: Same as list — `[count:f64] [elem0:f64] ...`
22
+
23
+ ### 4. Helper Functions
24
+ - `emit_bump_alloc_dynamic(ctx, size_local, ptr_local)` — bump allocator with dynamic size
25
+ - `emit_ptr_to_i32(ctx, expr)` — evaluate expression to i32 pointer in a local
26
+ - `emit_load_count_i32(ctx, ptr)` — load count from first f64 slot as i32
27
+ - `emit_addr_offset(ctx, base, offset)` — compute base+offset into new local
28
+ - `emit_copy_f64_loop(ctx, dst, src, n)` — copy N f64 slots between addresses
29
+ - `emit_ds_count(ctx, args)` — shared count accessor (list/map/set)
30
+ - `emit_ds_empty(ctx, args)` — shared empty? check
31
+ - `emit_alloc_with_count(ctx, count, total_slots)` — allocate and write count header
32
+
33
+ ### 5. List Operations (16)
34
+ - Constructor: `emit_list_new` — `[] elem0 elem1 ...`
35
+ - Access: `emit_list_nth`, `emit_list_first`, `emit_list_rest`
36
+ - Mutation: `emit_list_append`, `emit_list_prepend`, `emit_list_butlast`
37
+ - Slicing: `emit_list_slice`, `emit_list_reverse`, `emit_list_concat`
38
+ - Update: `emit_list_assoc`, `emit_list_dissoc`
39
+ - Query: `emit_list_contains`, `emit_list_includes`
40
+
41
+ ### 6. Map Operations (9)
42
+ - Constructor: `emit_map_new` — `&{} key val ...`
43
+ - Access: `emit_map_get_op` — linear key scan, return value or nil
44
+ - Mutation: `emit_map_assoc` (scan for existing key, branch: update vs append), `emit_map_dissoc` (scan + copy-with-skip)
45
+ - Query: `emit_map_contains`, `emit_map_includes`
46
+ - Transform: `emit_map_to_pairs` — creates list of 2-elem lists (nested allocation in loop)
47
+
48
+ ### 7. Set Operations (6)
49
+ - Constructor: `emit_set_new` — `#{} elem ...`
50
+ - Access: `emit_set_includes` (delegates to list_includes — same layout)
51
+ - Mutation: `emit_set_include` (scan + conditional append), `emit_set_exclude` (scan + copy-with-skip)
52
+
53
+ ## WASM Patterns Used
54
+ - **Copy loop**: `block/loop/br_if/br` with i32 counter for bulk f64 element copying
55
+ - **Scan loop**: Same pattern with `f64.eq` comparison for key/value lookup; `found_idx` local with -1 sentinel
56
+ - **Conditional allocation**: `if/else` for map_assoc (new key vs update) and set_include (already present vs append)
57
+ - **Select instruction**: For boolean returns (`contains?`, `empty?`) — `select(true_val, false_val, condition)`
58
+
59
+ ## Test Results
60
+ - 90 WASM checks (55 existing + 35 new) — all pass
61
+ - 246 cargo tests — all pass
@@ -0,0 +1,31 @@
1
+ # 扩展 extract_predicate_binding 支持更多类型谓词
2
+
3
+ ## 改动概要
4
+
5
+ 将 `extract_predicate_binding` 重构为 `extract_predicate_bindings`,支持双向窄化(true/false 分支),并新增 6 个类型谓词的窄化支持。
6
+
7
+ ## 修改文件
8
+
9
+ ### src/runner/preprocess/mod.rs
10
+
11
+ - 重构 `extract_predicate_binding` → `extract_predicate_bindings`,返回 `PredicateNarrowing` 结构体(包含 `true_binding` 和 `false_binding`)
12
+ - `preprocess_if` 中 false 分支也应用窄化信息
13
+ - 新增谓词:`tag?` → `:tag`、`bool?` → `:bool`、`symbol?` → `:symbol`、`fn?` → `:fn`
14
+ - 新增 `nil?`/`some?`:当变量已知为 `Optional(T)` 时,nil? 的 false 分支窄化为 `T`,some? 的 true 分支窄化为 `T`
15
+
16
+ ### src/calcit/proc_name.rs
17
+
18
+ - `TurnTag` 的参数签名从 `some_tag("string")` 改为 `dynamic_tag()`,因为 `turn-tag` 在运行时接受 tag/symbol/string,旧签名在 `tag?` 窄化后误报
19
+
20
+ ## 知识点
21
+
22
+ 1. **双向窄化**:`if (nil? x) ... else ...`,false 分支可从 `Optional(T)` 解包为 `T`,这在链式 if 模式中可以传播类型信息
23
+ 2. **新窄化暴露的类型签名问题**:`tag?` 窄化后,`(turn-tag x)` 触发了 arg-type-mismatch 警告,因为 `turn-tag` 签名过严。类似 coercion 函数应使用 `dynamic_tag()`
24
+ 3. **核心库中 `if (tag? ...)` 出现 5 次、`if (nil? ...)` 26 次、`if (list? ...)` 41 次**——窄化覆盖面广
25
+ 4. **JS 端 tuple `.nth` 缺失**:prior session 简化了多态函数(移除 tuple? 分支),但 JS runtime 的 `invoke_method` 没有对应的 core-tuple-methods 注册,导致 JS 测试 `test_refs` 失败。这是 JS procs 层的问题,不是预处理阶段的问题
26
+
27
+ ## 测试结果
28
+
29
+ - Rust: 246 tests pass(67 cirru + 179 unit)
30
+ - Clippy: clean
31
+ - JS: 已知 pre-existing failure(tuple .nth in JS runtime)
@@ -0,0 +1,27 @@
1
+ # 202604161507 - WASM 数据结构编译 & rfc 目录重命名
2
+
3
+ ## rfc 目录整理
4
+
5
+ - `drafts/` 重命名为 `rfc/`,所有文件加上 `MM-DD-` 创建日期前缀(从 git 历史提取)
6
+ - README.md 更新为完整索引,补全之前遗漏的 5 个文件条目
7
+ - Cargo.toml 中 `drafts/` 引用更新为 `rfc/`
8
+
9
+ ## WASM 数据结构编译
10
+
11
+ ### 新增能力
12
+
13
+ - **Memory Section**: 1 页 64KB 线性内存,Global section 维护 `heap_ptr` 作为 bump allocator
14
+ - **Tag 编译**: `collect_all_tags()` 在编译期为所有 tag 字面量分配整数 ID,运行时直接用 f64 表示
15
+ - **Record 编译**: `emit_record_new()` 在堆上分配 `[struct_tag_id, field0, field1, ...]`,`emit_record_nth()` 通过偏移直接读取
16
+ - **Tuple 编译**: `emit_tuple_new()` 在堆上分配 `[tag_id, payload0, payload1, ...]`,`emit_tuple_nth()` 直接读取
17
+ - **静态结构解析**: `try_parse_defrecord_form()` 从源码 AST 静态提取 `defrecord` 的字段定义,不依赖运行时宏展开
18
+
19
+ ### 关键设计决策
20
+
21
+ - 全 f64 ABI:所有值(包括指针)用 f64 传递,需要 `i32.trunc_f64_u` / `f64.convert_i32_u` 转换
22
+ - record 字段按字母序排列(与 Calcit runtime 一致)
23
+ - bump allocator 不回收,适合短生命周期 WASM 模块
24
+
25
+ ### 测试覆盖
26
+
27
+ - 22 项 WASM 检查全部通过:17 个数值运算 + 2 个 tag 比较 + 2 个 record 求和 + 1 个 tuple 求和
@@ -0,0 +1,21 @@
1
+ # 202604161520 - WASM 位运算 & match 语法编译
2
+
3
+ ## 位运算 (bitwise operations)
4
+
5
+ - 新增 `bit-and`, `bit-or`, `bit-xor`, `bit-not`, `bit-shl`, `bit-shr` 6 个 WASM 编译支持
6
+ - 策略: f64 → i32 (trunc_f64_s) → 执行 i32 位运算 → i32 → f64 (convert_i32_s)
7
+ - 使用有符号转换 (I32TruncF64S / F64ConvertI32S) 保持负数语义
8
+
9
+ ## match 语法编译
10
+
11
+ - 实现 `match` 表达式的 WASM codegen, 支持 enum tuple 模式匹配
12
+ - 策略: 加载 tuple 的 tag_id (offset 0), 对各分支做嵌套 if/else 比较
13
+ - 支持 tag 模式 `(:variant a b)` — 从 tuple 内存按偏移读取 payload 绑定到局部变量
14
+ - 支持 wildcard `_` 作为 fallback 分支
15
+ - 注意 block_depth 正确更新以保持 recur 分支跳转的一致性
16
+
17
+ ## 测试覆盖
18
+
19
+ - 31 项 WASM 检查全部通过 (原 22 + 6 bitwise + 3 match)
20
+ - 246 cargo tests 通过 (179 + 67)
21
+ - clippy 零警告
@@ -0,0 +1,38 @@
1
+ # WASM: Cross-namespace calls & host imports (pow, sin, cos)
2
+
3
+ ## 概要
4
+
5
+ 为 WASM codegen 增加了跨命名空间函数调用和宿主函数导入两大功能。
6
+
7
+ ## 知识点
8
+
9
+ ### 跨命名空间调用
10
+
11
+ - `emit_wasm()` 从只处理 `init_ns` 改为遍历 `program_data` 全部命名空间
12
+ - `fn_defs` 从 3-tuple `(def, args, body)` 扩展为 4-tuple `(ns, def, args, body)`
13
+ - `fn_index` 同时保存 `"ns/def"` 全限定名和 `"def"` 裸名两种键
14
+ - `emit_call_expr` 中遇到 `Import` 节点时,先尝试全限定名再尝试裸名
15
+ - `collect_all_tags` 更名为 `collect_all_tags_from` 适配 4-tuple
16
+
17
+ ### 宿主函数导入 (Host Imports)
18
+
19
+ - 新增 `HostImport` 结构体和 `HOST_IMPORTS` 常量数组(pow/sin/cos)
20
+ - WASM 模块新增 `ImportSection`,从 `"math"` 模块导入函数
21
+ - **关键约束**:导入函数占据函数索引 0..N,用户函数从 N 开始
22
+ - TypeSection: 先写 host import 的类型, 再写 user function 的类型
23
+ - FunctionSection: 只写 user function(import 自动拥有类型)
24
+ - ExportSection / fn_index: 用户函数索引均需加 `num_imports` 偏移
25
+ - `emit_host_call()` 辅助函数:按名称查找 HOST_IMPORTS 并发出 Call 指令
26
+ - `CalcitProc::Sin/Cos/Pow` 从返回错误改为调用 `emit_host_call`
27
+
28
+ ### 测试
29
+
30
+ - `test-wasm.cirru` 新增 `test-wasm.helper` 命名空间(含 `add-and-double`)
31
+ - `test-wasm.mjs` 新增 `math` 导入对象 + `checkApprox()` 浮点近似断言
32
+ - 测试从 31 项增加到 39 项,全部通过
33
+
34
+ ## 注意事项
35
+
36
+ - WASM 规范要求 Import section 必须在 Function section 之前
37
+ - 添加新 host import 时需更新 HOST_IMPORTS 数组,并确认 JS 测试 runner 提供对应实现
38
+ - 后续可考虑将 host import 模块名参数化(目前硬编码 `"math"`)
@@ -0,0 +1,62 @@
1
+ # WASM: 可变参数 (rest args) 支持
2
+
3
+ ## 背景
4
+
5
+ 此前 `(defn f (a b & xs) ...)` 形式的函数在 WASM codegen 中会直接报错
6
+ "rest args not supported in WASM codegen" 而被跳过。这阻塞了 `calcit.core` 中
7
+ `<`、`-`、`/`、`dissoc`、`exclude`、`intersection`、`difference`、
8
+ `merge-non-nil`、`&str-spaced` 等大量可变参数函数的 WASM 编译。
9
+
10
+ ## 实现
11
+
12
+ 函数签名约定(匹配现有数据结构表示):
13
+
14
+ - rest 参数表现为一个 f64 参数,承载一个 list 指针。
15
+ - `(defn f (a b & xs) ...)` 的 WASM 签名是 `(a: f64, b: f64, xs: f64) -> f64`,
16
+ 其中 `xs` 是调用方构造的 list 指针。
17
+
18
+ 关键改动(全部在 `src/codegen/emit_wasm.rs`):
19
+
20
+ 1. 新增 `compute_fn_arity(args) -> (wasm_arity, Option<rest_fixed>)`:
21
+ 替代原来散落的 arity 计算,同时返回是否含 rest 以及固定参数数。
22
+ 2. 新增 `fn_has_rest: HashMap<String, u32>`,记录每个有 rest 的函数的固定
23
+ 参数数(`wasm_arity - 1`)。随 `fn_index` / `fn_arity` 一起在
24
+ `compile_fn` 之前建好。
25
+ 3. `WasmGenCtx` 新增 `fn_has_rest` 字段,与其他 lookup 表一起 clone 进上下文。
26
+ 4. `compile_fn`:
27
+ - 不再对 `CalcitArgLabel::RestMark` 报错。
28
+ - 跳过 `RestMark` 本身,但把 `&` 后的 `Idx` 仍然作为一个普通 f64 参数
29
+ 压入 `param_names`(它承载 list 指针)。
30
+ 5. `emit_call_expr`:提取新助手 `emit_call_args`,根据 callee 是否有 rest
31
+ 分两条路径:
32
+ - 无 rest:照旧逐个 `emit_expr`,缺失的 optional 参数用 `f64_const(0.0)` 填补。
33
+ - 有 rest:前 `fixed` 个参数照旧求值,多余参数调用 `emit_list_new` 打包
34
+ 成 list 指针作为最后一个参数。
35
+
36
+ ## 踩坑记录
37
+
38
+ - **`bash scripts/test-wasm.sh` 用的是 release 产物,不是 `cargo run`**。改完
39
+ Rust 代码后一定要 `cargo build --release --bin cr`,否则看到的仍是旧行为。
40
+ 这次调试时一度以为 `collect-rest` 还会被跳过,源头是 release binary 陈旧。
41
+ - `emit_list_new` 签名接受 `&[Calcit]`,直接传入 `&args_list[fixed..]`
42
+ 就够用;不需要手动写 header/slot。
43
+
44
+ ## 测试
45
+
46
+ 新增 `calcit/test-wasm.cirru` 三个用例:
47
+
48
+ - `test-rest-count`:`collect-rest 1 2 3 4` 返回的 list 长度为 3。
49
+ - `test-rest-sum`:`sum-rest 1 2 3 4 5 = 15`,验证打包 list 后由被调函数
50
+ 通过 `&list:first` / `&list:rest` / `recur` 逐项展开并求和。
51
+ - `test-rest-empty`:`sum-rest 10 20 = 30`,验证没有额外参数时 list 为空。
52
+
53
+ `scripts/test-wasm.mjs` 对应 3 条 `check(...)`。`yarn check-all` 全绿。
54
+
55
+ ## 遗留
56
+
57
+ 尚未支持的 WASM 功能(下一步继续攻克):
58
+
59
+ - `type-of`、各类 `xxx?` 谓词(需要运行时类型标记,难度较大)。
60
+ - `foldl` / `foldl-shortcut`(依赖闭包或函数指针)。
61
+ - 嵌套 `defn` / 闭包。
62
+ - 字符串字面量和字符串操作。
@@ -0,0 +1,70 @@
1
+ # WASM codegen: implement `type-of` via heap type headers
2
+
3
+ ## 背景
4
+
5
+ 在 WASM 子集里实现 `type-of` 是解锁 ~20 个核心函数(`list?`、`map?`、`set?`、
6
+ `number?` 等)的前置。所有值统一用 f64 表示,堆对象是“整数值 f64”的指针
7
+ (byte offset);没有 NaN-boxing,无法仅凭位模式区分类型。
8
+
9
+ ## 方案
10
+
11
+ 每一次堆分配前置 8 字节类型头:
12
+
13
+ ```
14
+ raw_base + 0 : i32 MAGIC (0xCA1C17A9)
15
+ raw_base + 4 : i32 tag_id (BUILTIN_TYPE_TAGS 里的下标对应的 tag_index 值)
16
+ raw_base + 8 : 原本的 payload
17
+ ```
18
+
19
+ `emit_bump_alloc` / `emit_bump_alloc_dynamic` 实际申请 `byte_size + 8` 字节,
20
+ 返回的 **logical pointer** 仍然是 `raw_base + 8`,所以所有既有 offset 算术
21
+ (count at offset 0、fields at offset 8/16/…)无需改动。
22
+
23
+ ### 11 个内建 type tag
24
+
25
+ `BUILTIN_TYPE_TAGS = ["nil","bool","number","tag","string","list","map","set",
26
+ "record","tuple","fn"]`。在 `collect_all_tags_from` 第一步 seed,确保
27
+ `:list` / `:map` 等关键字字面量和 `type-of` 返回值落到同一个 tag id。
28
+
29
+ ### `type-of` 运行时判定
30
+
31
+ ```
32
+ if v == trunc(v) // 整数
33
+ && v >= (HEAP_START + 8) // 在堆范围内
34
+ && v < current_heap_ptr {
35
+ raw_base = trunc(v) - 8
36
+ if load_i32(raw_base, 0) == MAGIC { // 魔数校验,排除纯数字误判
37
+ return f64(load_i32(raw_base, 4)) // 返回 tag id
38
+ }
39
+ }
40
+ return f64(number_tag)
41
+ ```
42
+
43
+ 之前只用“整数 + 在堆范围”做判定时,`42` 这类数字会被错误识别成指针 →
44
+ 读到垃圾 tag;加上 `MAGIC` 校验后 `test-type-of-number` 通过。
45
+
46
+ ## 踩坑
47
+
48
+ 1. **签名顺序**:`emit_bump_alloc` 的参数顺序从
49
+ `(ctx, byte_size, type_tag, ptr_local)` 调整为
50
+ `(ctx, byte_size, ptr_local, type_tag)`,以匹配 21 处调用点的脚本化改写。
51
+ 2. **Magic 必须**:只有“指针范围 + 整数值”不足以区分指针和裸数字,必须加
52
+ 唯一性较高的 magic(这里用 `0xCA1C17A9`)。
53
+ 3. **f64 ↔ i32**:`tag_id` 是小整数,用 i32 存取更便宜;新增
54
+ `mem_arg_i32` 辅助函数(align=2)。
55
+ 4. **If/Else 嵌套返回 f64**:`type-of` 用两层 `BlockType::Result(ValType::F64)`
56
+ 实现短路,避免在非指针情况下去读内存。
57
+
58
+ ## 测试
59
+
60
+ `calcit/test-wasm.cirru` + `scripts/test-wasm.mjs` 新增 5 个用例:
61
+ `test-type-of-list/map/set/number/tuple`。全部返回 1(`&= (type-of x) :list`
62
+ 之类比较)。已执行 `cargo fmt && cargo clippy --release -- -D warnings &&
63
+ cargo test --release && yarn check-all`,全部通过。
64
+
65
+ ## 后续
66
+
67
+ 解锁后可以把 `list?` / `map?` / `set?` / `number?` / `tuple?` / `record?`
68
+ 等 proc 映射成 `(&= (type-of x) :xxx)` 或直接基于 tag 比较。下一轮挑战:
69
+ 字符串支持 (24 个 skip)、nested defn/闭包 (19 个 skip)、`foldl` /
70
+ `foldl-shortcut` (13 个 skip)。
@@ -0,0 +1,34 @@
1
+ # WASM codegen: test derived type predicates on top of `type-of`
2
+
3
+ ## 背景
4
+
5
+ 刚在上一条 commit 里把 `type-of` 跑通了,语料里 `calcit.core` 的几个核心
6
+ predicates (`list?` / `map?` / `number?` / `set?` / `tuple?` / ...) 都是
7
+ `(defn pred? (x) (&= (type-of x) :tag))` 这种薄包装。既然 `type-of`、`&=`、
8
+ keyword 字面量都已经支持,这些 predicate 本来就会被 WASM 编译器吐出来,
9
+ 只是一直缺少验证。
10
+
11
+ ## 改动
12
+
13
+ `calcit/test-wasm.cirru` + `scripts/test-wasm.mjs` 增加 4 个样例:
14
+
15
+ - `test-list?-true` : `(list? ([] 1 2))` → 1
16
+ - `test-list?-false` : `(list? 42)` → 0 (验证魔数检查能挡住整数伪装)
17
+ - `test-number?-true` : `(number? 42)` → 1
18
+ - `test-map?-true` : `(map? (&{} :a 1))` → 1
19
+
20
+ `yarn check-all` 全部通过,81 个 WASM 检查全绿。
21
+
22
+ ## 当前 WASM skip 分类(跑 `cr wasm` 的 stderr)
23
+
24
+ 总计 123 条 skipping,主要集中在:
25
+
26
+ - 24 String values not yet supported
27
+ - 19 nested defn not supported
28
+ - 13 foldl / foldl-shortcut / foldr-shortcut
29
+ - 7 `&call-spread`
30
+ - 4 `Tuple operation %:: not yet supported`
31
+ - 3 `&set:destruct`
32
+ - 少量其它(`sort`, `range`, `println`, `deref`, `turn-string` 等)
33
+
34
+ 下一轮切入点优先级:字符串 → nested defn/闭包 → 高阶函数 (`call_indirect`)。
@@ -0,0 +1,50 @@
1
+ # Monomorphize `map` / `filter` at Compile Time
2
+
3
+ ## 背景
4
+
5
+ `try_specialize_polymorphic_call`(`src/runner/preprocess/mod.rs`)原本只能把
6
+ `count`、`empty?`、`first`、`rest`、`nth`、`get`、`assoc`、`contains?` 等
7
+ **built-in 多态 proc** 特化成 native proc(`NativeListCount` / `NativeMapGet`
8
+ 等),彻底避免运行时按 `type_of` 做分支。
9
+
10
+ 但是 Calcit 里最常见的集合高阶函数 `map` / `filter` / `reduce` 等都是在
11
+ `calcit-core.cirru` 里用 Calcit 本身定义的用户级 defn(例如 `&list:map`、
12
+ `&map:map`、`&set:filter`),原先不能在预处理阶段复用这套机制,运行时仍要靠
13
+ `list?`/`map?`/`set?` 三条 `cond` 分支来分发。
14
+
15
+ 本次把能力扩展到 Calcit 级别的 core def,当 receiver 的静态类型可推断时,
16
+ 直接把 `(map xs f)` 改写成 `(calcit.core/&list:map xs f)` 等形式。
17
+
18
+ ## 关键改动
19
+
20
+ - `src/runner/preprocess/mod.rs`
21
+ - `try_specialize_polymorphic_call` 新增 `file_ns: &str` 参数,用于构造
22
+ `Calcit::Import` 头的 `ImportInfo::Core { at_ns }`。
23
+ - 在已有的 proc 特化表之前新增一张“core def 特化表”:
24
+ - `("map", List(_)) -> &list:map`
25
+ - `("map", Map(_, _)) -> &map:map`
26
+ - `("filter", List(_)) -> &list:filter`
27
+ - `("filter", Map(_, _)) -> &map:filter`
28
+ - `("filter", Set(_)) -> &set:filter`
29
+ - 命中时直接构造 `Calcit::Import(CalcitImport { ns: "calcit.core", def, info:
30
+ Core { at_ns: file_ns }, def_id: Some(program::ensure_def_id(...).0) })`
31
+ 作为新的 head,保持已 preprocess 过的参数不变,避免重复预处理。
32
+ - 禁止对 `&list:map` / `&map:map` / `&set:map` / `&list:filter` /
33
+ `&map:filter` / `&set:filter` 自身再次 monomorphize,避免潜在 cycle。
34
+ - 调用点 `Ok(Calcit::from(CalcitList::from(ys)))` 前的 specialize 调用传入
35
+ `file_ns`。
36
+
37
+ ## 验证
38
+
39
+ - `cargo fmt && cargo clippy --release -- -D warnings` ✓
40
+ - `cargo test --release` ✓ 所有现有测试通过
41
+ - `yarn check-all` ✓ 全量集成测试 + WASM 测试全部 OK
42
+
43
+ ## 后续思路
44
+
45
+ - 可继续把 `reduce` / `last` / `reverse` / `concat` / `to-list` / `to-set`
46
+ 等也加入特化表;其中 `concat` / `reverse` 已有 native proc(`NativeListConcat`
47
+ / `NativeListReverse`),加到已有 proc 表即可。
48
+ - 可以考虑把谓词(`list?`、`map?`、`number?`、`string?` …)在类型已知时直接折叠成
49
+ `true` / `false`,进一步帮 WASM / JS codegen 消冗余分支。
50
+ - 未来 `&set:map` 如果单独实现,即可补齐 `("map", Set(_))` 分支。
@@ -0,0 +1,31 @@
1
+ # Monomorphize `includes?` and `reverse`
2
+
3
+ ## 背景
4
+
5
+ 继 `map` / `filter` 之后,继续扩展 `try_specialize_polymorphic_call`:
6
+
7
+ - `includes?` 原实现(calcit-core.cirru L3228)有 5 条 `if` 分支
8
+ (`nil?` / `list?` / `map?` / `set?` / `string?` fallback `.includes?`)。
9
+ - `reverse` 实际上在 runtime 就是 `&list:reverse` 的别名,但以前每次调用还是要
10
+ 经过一层 Calcit user-def 间接。
11
+
12
+ ## 改动
13
+
14
+ 在 `src/runner/preprocess/mod.rs` 的 proc 特化表中追加:
15
+
16
+ ```text
17
+ ("includes?", T::List(_)) -> NativeListIncludes
18
+ ("includes?", T::Map(_, _)) -> NativeMapIncludes
19
+ ("includes?", T::Set(_)) -> NativeSetIncludes
20
+ ("includes?", T::String) -> NativeStrIncludes
21
+ ("reverse", T::List(_)) -> NativeListReverse
22
+ ```
23
+
24
+ `concat` 因为是变长参数 defn,暂不在这里处理;后续可以在 preprocess 阶段
25
+ 把已知都是 list 的 `(concat a b)` 展开为 `(&list:concat a b)` 的折叠形式。
26
+
27
+ ## 验证
28
+
29
+ - `cargo fmt && cargo clippy --release -- -D warnings` ✓
30
+ - `cargo test --release` ✓
31
+ - `yarn check-all` ✓
@@ -0,0 +1,44 @@
1
+ # Fold type predicates when static type is known
2
+
3
+ ## 背景
4
+
5
+ `list?` / `map?` / `set?` / `string?` / `number?` / `bool?` / `tag?` / `fn?` /
6
+ `tuple?` / `record?` 这些 type predicate defn 都是 `(&= (type-of x) :xxx)`
7
+ 的形式。如果 preprocess 能拿到 `x` 的静态类型,整个调用就可以折叠成一个字面的
8
+ `true` 常量(或者保留运行时 check,但不把 `false` 折叠以保持安全)。
9
+
10
+ 在 WASM / JS codegen 场景下这尤其有收益:谓词出现在 `if` / `cond` 里,
11
+ 折叠成常量后整个分支会被后续 dead-code 消除。
12
+
13
+ ## 改动
14
+
15
+ `src/runner/preprocess/mod.rs::try_specialize_polymorphic_call` 新增一段在
16
+ proc 特化表之前的折叠逻辑:
17
+
18
+ ```rust
19
+ let predicate_true = matches!(
20
+ (fn_def, receiver_type.as_ref()),
21
+ ("list?", T::List(_))
22
+ | ("map?", T::Map(_, _))
23
+ | ("set?", T::Set(_))
24
+ | ("string?", T::String)
25
+ | ("number?", T::Number)
26
+ | ("bool?", T::Bool)
27
+ | ("tag?", T::Tag)
28
+ | ("fn?", T::Fn(_) | T::DynFn)
29
+ | ("tuple?", T::Tuple(_) | T::DynTuple)
30
+ | ("record?", T::Record(_) | T::Struct(_, _))
31
+ );
32
+ if predicate_true { return Some(Calcit::Bool(true)); }
33
+ ```
34
+
35
+ **设计选择**:只折叠“肯定为 true”的情形,不折叠“肯定为 false”的情形。
36
+ 原因:`CalcitTypeAnnotation` 枚举有 30+ variant(包括 `Optional`、`Ref`、
37
+ `TypeVar`、`TypeRef`、`Trait` 等等),要列全“除此之外的全部”枚举会很脆弱、
38
+ 以后新增 variant 容易漏。保留 runtime check 是安全的 fallback。
39
+
40
+ ## 验证
41
+
42
+ - `cargo fmt && cargo clippy --release -- -D warnings` ✓
43
+ - `cargo test --release` ✓(179 + 67 测试全通过)
44
+ - `yarn check-all` ✓
@@ -0,0 +1,52 @@
1
+ # Extend generic assoc/nth/get/count/contains?/empty?/first to records & tuples
2
+
3
+ ## 背景
4
+
5
+ Recollect 升级到 `defstruct` + `%{}` 构造记录后,在 JS 运行时调用 `assoc` /
6
+ `nth` / `get` 等会 fallback 到 `.method`,再经过 `invoke_method` 在
7
+ `structRef.impls` 中查找;但默认构造的 struct 并没有自动挂上 core 的
8
+ `:assoc` / `:nth` / `:get` impls,导致 `Error: No implementation for ... to
9
+ lookup .assoc` 等。
10
+
11
+ Rust 解释器路径能自动解析到 `&record:*` 内置 proc,所以 `cr --entry test`
12
+ 能跑过,但 JS 输出(`cr js` + Node)跑不过。
13
+
14
+ ## 改动
15
+
16
+ 在 `src/cirru/calcit-core.cirru` 的若干 generic 分发函数里显式增加
17
+ `record?` / `tuple?` 分支,直接路由到内置 proc,不再依赖 method lookup:
18
+
19
+ | 函数 | 新增分支 |
20
+ | ----------- | -------------------------------------------------------- |
21
+ | `assoc` | `record? -> &record:assoc`, `tuple? -> &tuple:assoc` |
22
+ | `nth` | `tuple? -> &tuple:nth`, `record? -> &record:nth` |
23
+ | `get` | `tuple? -> &tuple:nth`, `record? -> &record:get` |
24
+ | `count` | `tuple? -> &tuple:count`, `record? -> &record:count` |
25
+ | `contains?` | `record? -> &record:contains?` |
26
+ | `empty?` | `record? -> (&= 0 (&record:count x))`, tuple 同理 |
27
+ | `first` | `tuple? -> (&tuple:nth x 0)` |
28
+
29
+ 这几处与“运行时按 `type-of` 分支”的方向是一致的——在当前 Calcit 语义下
30
+ records / tuples 是独立 kind,本来就应该在 generic 分发里显式处理,而不是
31
+ 掉到“最后一条 method lookup” 的兜底里。
32
+
33
+ 与此前添加的 **编译期单态化**(`try_specialize_polymorphic_call`)是互补的:
34
+ 当静态类型已知时在预处理阶段直接改写成 proc 调用;静态类型未知时,运行时
35
+ 分支也能正确分发到 `&record:*` / `&tuple:*`,而不再需要结构实现端配合
36
+ impls。
37
+
38
+ ## 验证
39
+
40
+ - `cargo fmt && cargo clippy --release -- -D warnings` ✓
41
+ - `cargo test --release` ✓(179 + 67 全过)
42
+ - `yarn check-all` ✓
43
+ - recollect 全套:
44
+ - `cr --entry test` ✓
45
+ - `cr --entry test js && env=ci node test.mjs` ✓(此前 `.assoc` 报错修复)
46
+ - `cr js && yarn vite build --base=./` ✓
47
+
48
+ ## 后续
49
+
50
+ 随着 Calcit 继续演进,这些 chain-of-if 分发仍然是临时形态。更彻底的做法是
51
+ 在预处理阶段通过类型推断消除所有 generic 分发 defn,让核心库只暴露
52
+ `&list:*` / `&map:*` / `&record:*` 等单态 proc —— 见下一步语言简化计划。
@@ -0,0 +1,49 @@
1
+ # Simplify generic core defns via `.method` dispatch
2
+
3
+ ## 概要
4
+
5
+ 将 `calcit-core.cirru` 中的泛型 defn(`assoc` / `contains?` / `count` / `empty` / `empty?` / `filter` / `first` / `get` / `includes?` / `nth` / `rest` / `map`)从多分支 `if (list? x) ... if (map? x) ... if (record? x) ...` 链简化为 `list?` 快速路径 + `.method` 动态分发。
6
+
7
+ ## 动机
8
+
9
+ - 用户要求:优先使用 `.method` 这套已有概念承担多态分发,避免每新增类型(record / tuple / set 等)都要在每个 defn 中追加分支。
10
+ - 所有 built-in 类型已在 `&core-*-methods` 中注册了对应的 `.assoc` `.count` `.empty?` `.get` `.nth` 等方法条目,编译期 `try_inline_method_call` 已能在静态类型已知时把方法调用内联为直接 proc 调用,runtime 则由 `invoke_method` 处理。这使得 defn 自身不再需要枚举类型。
11
+
12
+ ## 实现
13
+
14
+ 以 `empty?` 为例:
15
+
16
+ ```cirru
17
+ defn empty? (x)
18
+ if (nil? x) true $ if (list? x) (&list:empty? x) (.empty? x)
19
+ ```
20
+
21
+ 保留 `nil?` 与 `list?` 两个前置分支,其余类型全部交给 `.empty?`。
22
+
23
+ **为什么保留 `list?` 快速路径**:
24
+ `ensure_ns_def_compiled(CORE_NS, &init-builtin-impls!)` 在预处理期会展开 `do` 等 macro,其中 `(empty? body)` 会在 impls 还未注册到 runtime 时被调用;若此时 `empty?` 体里就走 `.empty?` → `invoke_method` → `evaluate_symbol_from_program("&core-list-impls", ...)` 会命中 quick-path 失败(循环依赖),导致 panic:
25
+
26
+ ```
27
+ preprocess builtin impls: CalcitErr { kind: Var, msg: "expected symbol `&core-list-impls` from path `calcit.core`, this is a quick path, should succeed", ... }
28
+ ```
29
+
30
+ `list?` 本身只用 `(&= (type-of x) :list)` 等 proc,不依赖 impl 注册,能在 bootstrap 期安全使用,打破循环;其余类型的 methods 此时已构建完毕,可顺利走方法派发。
31
+
32
+ ## 波及范围
33
+
34
+ - `src/cirru/calcit-core.cirru`:12 个 defn 的 body 瘦身。
35
+ - 预处理期的 `try_specialize_polymorphic_call`(已有)照常将 `(assoc x k v)` 等静态可推断调用折叠为 `&list:assoc`/`&map:assoc` 等 proc。
36
+ - runtime 侧未变:仍由 `invoke_method` + `&core-*-impls` 完成最终派发。
37
+
38
+ ## 验证
39
+
40
+ - `cargo fmt`
41
+ - `cargo clippy --release -- -D warnings`
42
+ - `cargo test --release`:179 + 67 通过
43
+ - `yarn check-all`:全部 WASM/JS/解释执行测试通过
44
+ - recollect (`cr --entry test` + `cr --entry test js` + `node test.mjs`) ✓
45
+ - respo (`cr --check-only` + `cr js` + `yarn vite build`) ✓
46
+
47
+ ## 备注
48
+
49
+ - 下一步可以考虑:把那些只剩 `list?` 单分支的 defn 再收拢到一个运行时 primitive,或在 preprocess 阶段对 macro 展开期调用的 `empty?` 等函数强制内联 proc,从而彻底去掉这条 fast path。
@@ -0,0 +1,41 @@
1
+ # 202604172316 — WASM `do` body 与 println host import 修复
2
+
3
+ ## 修复内容
4
+
5
+ ### 1. `do` 在 defn body 中的 WASM 编译问题
6
+
7
+ **现象**:`defn test-println () do (println 42) 1` 编译后生成 `f64.const 0`(函数体为空)。
8
+
9
+ **根因**:Cirru 语法中 `defn f () do expr1 expr2` 解析为 `(defn f () do expr1 expr2)`,body 数组为 `[do, expr1, expr2]` 三个独立元素(不是 `[(do expr1 expr2)]`)。`do` 作为裸 Import 节点出现在 `emit_expr` 时,触发 "unsupported WASM expression" 错误,导致函数被跳过并 fallback 为 `f64.const 0`。
10
+
11
+ **修复**:在 `emit_expr` 中为 `Calcit::Import { def: "do" }` 添加特例,将其视为 no-op(emit 0.0 后由 `emit_body` 的 Drop 消耗掉)。
12
+
13
+ ```rust
14
+ // `do` as bare body expression is a no-op sequencer
15
+ Calcit::Import(import) if import.def.as_ref() == "do" => {
16
+ ctx.emit(f64_const(0.0));
17
+ }
18
+ ```
19
+
20
+ ### 2. io.log_value host import(WASM println)
21
+
22
+ - WASM codegen 对 `println`/`eprintln`/`echo` 调用 host import `io.log_value`。
23
+ - `test-wasm.mjs` 提供 `io.log_value` 实现:读取堆内存判断类型,字符串解码 UTF-8,数字直接输出。
24
+ - 添加 `test-println` 测试用例验证功能。
25
+
26
+ ### 3. TypeScript 运行时 BufList 修复
27
+
28
+ `_$n_buf_list_$o_concat` 中 `CalcitSliceList` 的 `items` 是方法而非属性,需调用 `xs.items()` 并手动迭代 Generator(TS 配置不支持 `for...of` Generator)。
29
+
30
+ ## 受影响文件
31
+
32
+ - `src/codegen/emit_wasm.rs`:`emit_expr` 新增 `do` no-op 特例;`emit_call_expr` Import 分支也保留 `do` 处理(用于 `(do ...)` 调用形式)
33
+ - `ts-src/calcit.procs.mts`:修正 `_$n_buf_list_$o_concat` 迭代逻辑
34
+ - `calcit/test-wasm.cirru`:新增 `test-println`
35
+ - `scripts/test-wasm.mjs`:新增 `io.log_value` 实现与 `test-println` 断言
36
+
37
+ ## 关键经验
38
+
39
+ - `do` 在 Calcit defn body 中是语法糖/占位符,预处理后展开为多个独立 body 表达式,不是 `(do ...)` 调用形式。WASM codegen 需将其视为 no-op。
40
+ - WASM 编译错误时 fallback 为 `f64.const 0`(静默),调试时需检查 stderr 中的 `[wasm] skipping` 日志。
41
+ - TS 的 Generator 不能直接 `for...of`,需手动调用 `.next()`。
@@ -0,0 +1,15 @@
1
+ ## Summary
2
+
3
+ - Split the WASM code generator so `src/codegen/emit_wasm.rs` keeps orchestration while runtime helpers, method dispatch, and record/tuple emitters live in dedicated submodules.
4
+ - Bumped the Calcit patch version from `0.12.20` to `0.12.21` for the next release.
5
+
6
+ ## Module Layout
7
+
8
+ - `src/codegen/emit_wasm/runtime.rs` now owns host import definitions, module assembly, and internal runtime helper builders.
9
+ - `src/codegen/emit_wasm/methods.rs` now owns dynamic method dispatch and rest-arg call argument packing.
10
+ - `src/codegen/emit_wasm/records.rs` now owns record and tuple emission helpers.
11
+
12
+ ## Notes
13
+
14
+ - The split is intended to be behavior-preserving; no WASM semantics were changed as part of this refactor.
15
+ - Version strings were updated in `Cargo.toml`, `package.json`, and `lib/package.json`, while generated JS version text will be refreshed by the validation build.
@@ -0,0 +1,5 @@
1
+ - WASM export names now stay unique by qualifying only colliding defs with `ns/def`.
2
+ - `list-match` macro now uses `&list:empty?` after the existing list guard, avoiding the unsupported generic `empty?` invoke path in WASM.
3
+ - Added minimal `Calcit::Method` handling in WASM for dynamic `.empty?`, `.count`, `.nth`, and `.get` calls.
4
+ - `&tuple:nth` in WASM now accepts dynamic indices instead of literal-only indices.
5
+ - Verified `yarn try-all` still passes in calcit after the WASM changes.