@calcit/procs 0.12.48 → 0.12.50

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.
@@ -0,0 +1,24 @@
1
+ # Edit History - Type Introspection Consistency Fixes
2
+
3
+ - **Objective**: Close gaps found while researching "how to discover a type's fields/methods in Calcit" (methods via traits/impls, fields via records, Display formatting). Documented findings and a prioritized fix plan in `RFCs/07-19-type-introspection-consistency-rfc.md`, then implemented items 1-3 (item 4 deferred as optional/out of scope for this pass).
4
+
5
+ - **Fix 1 — `Enum` `Display` now includes variants** (`src/calcit.rs`):
6
+ - Previously `Calcit::Enum` printed only `(%enum :Name)`, discarding the already-tracked `variants()` (tag + payload types). Now prints `(%enum :Name (:variant1 type ...) (:variant2 ...) ...)`, matching the richness of `Struct`'s Display.
7
+ - Test: `enum_display_includes_variants` in `src/calcit.rs`'s test module.
8
+
9
+ - **Fix 2 — `&methods-of`/`&inspect-methods` accept bare `Struct`/`Enum`/`Trait` values** (not just instances):
10
+ - `src/builtins/meta.rs`: `collect_impl_records_for_value` gained `Calcit::Struct`/`Calcit::Enum` arms (read their own `impls` field directly). `iter_impls_in_precedence_order` now treats `Struct`/`Enum` the same as `Tuple`/`Record` (last-attached impl wins, i.e. reversed iteration) since `&struct:impl-traits`/`&enum:impl-traits` also `.extend()` impls onto the end.
11
+ - `Calcit::Trait` handled as a special case (new helper `trait_dot_method_names`) since traits declare methods directly, not via an `impls` list — no `impls` field exists on `CalcitTrait`.
12
+ - JS target parity: `ts-src/calcit.procs.mts`'s `lookup_impls` gained `CalcitStruct`/`CalcitEnum` branches; the `reverse`-order flag (previously only `CalcitRecord || CalcitTuple`) now also includes `CalcitStruct || CalcitEnum`; `_$n_methods_of`/`_$n_inspect_methods` special-case `CalcitTrait` the same way as the Rust side.
13
+ - Test: `calcit/test-traits.cirru`'s `test-method-introspection` — new `let` block calling `&methods-of` on `(impl-traits Person0 MyFooImpl)` (bare struct), `DemoBar` (bare enum), `MyFoo` (bare trait), asserting the expected method tags show up. Edited via `cr tree insert-after` (module entry must be the module's own file, e.g. `cr calcit/test-traits.cirru ...`, NOT the aggregated `calcit/test.cirru` — `cr tree`/`cr edit` couldn't resolve the namespace when invoked through the aggregator, while `cr query` could).
14
+
15
+ - **Fix 3 — `to-pairs`/`keys` type signature no longer false-warns on records**:
16
+ - `src/calcit/type_annotation.rs`: `matches_with_bindings`'s `(TypeRef, Record)` arm previously only matched if the `TypeRef` name equaled the record's own struct name (e.g. `"map"` vs `"Person"` → false). Added: when the `TypeRef` name is the generic `"map"` placeholder, match any record structurally (records are field-name → value, i.e. map-like), in addition to the existing exact-name match.
17
+ - Test: `generic_map_type_ref_accepts_records_structurally` in `src/calcit/type_annotation.rs`'s test module — verifies `TypeRef("map")` matches `Record(Person)` both directions, and confirms unrelated `TypeRef` names still don't match structurally (no over-broadening).
18
+ - Note: empirically, `cr --check-only` on `test-record.cirru` didn't show a warning for `keys p2` even before this fix (likely because that call site's static type inference doesn't currently narrow `p2` to `Record`), so this is a defensive/forward-looking fix for when static inference improves or explicit type annotations are used, not a fix for an observed regression today.
19
+
20
+ - **Deferred (RFC item 4, optional)**: `&struct:fields`/`&enum:variants` new introspection procs — would require registering a new `CalcitProc` end-to-end (proc_name.rs signature + string mapping, builtins.rs dispatch, records.rs/meta.rs impl, JS/IR/WASM codegen parity). Judged lower priority/cost-to-benefit than items 1-3 which fixed real capability gaps; left as a follow-up.
21
+
22
+ - **Tooling note**: `cr tree`/`cr edit` subcommands need the entry file to be the *specific module's own `.cirru` file* (e.g. `calcit/test-traits.cirru`) to resolve `<ns>/<def>` targets — passing the aggregator entry (`calcit/test.cirru`, which just lists the module in `:modules`) causes `Error: "Namespace '...' not found"` for `tree`/`edit` even though `cr query defs/def` works fine through the aggregator. `--file` snippet content must be `quote`-prefixed (e.g. `quote (let (...) ...)`) even though the repo's `Agents.md` `--stdin` removal note doesn't mention this requirement explicitly.
23
+
24
+ - **Validation**: `cargo fmt`, `cargo clippy -- -D warnings`, `cargo test` (239 lib tests incl. 2 new), `cargo run --bin cr -- calcit/test.cirru`, `yarn check-all` (compile + try-rs + try-js + try-ir + try-wasm) — all clean/passing.
@@ -0,0 +1,64 @@
1
+ # Schema kind tag round-trip regression fix (2026-07-19 19:30)
2
+
3
+ ## Symptom
4
+
5
+ Running `cr tree insert-after`/`cr edit` on [calcit/test-traits.cirru](../calcit/test-traits.cirru)
6
+ (to add new `test-method-introspection` assertions) silently rewrote the `:schema`
7
+ field of *every other, unrelated* def in the same file from its specific kind tag
8
+ (`:enum`, `:trait`, `:impl`, `:struct`) down to `:schema :dynamic`. This affected
9
+ ~15 defs (`Demo0`, `DemoBar`, `MyBar`, `MyBarImpl`, `MyFoo`, `MyFooImpl`, `Person0`,
10
+ etc.) that were never touched by the edit itself — any save of the file degraded
11
+ their schema fidelity as a side effect.
12
+
13
+ ## Root cause
14
+
15
+ `:struct`/`:enum`/`:trait`/`:impl`/`:record` shorthand schema tags load into
16
+ `CalcitTypeAnnotation::Custom(Arc<Calcit>)` (see `CalcitTypeAnnotation::from_tag_name`
17
+ in [src/calcit/type_annotation.rs](../src/calcit/type_annotation.rs)), wrapping the
18
+ kind as a bare `Calcit::Tag`. On save, `schema_annotation_to_edn` (in
19
+ [src/snapshot.rs](../src/snapshot.rs)) converted the in-memory schema back to EDN via
20
+ `CalcitTypeAnnotation::builtin_tag_name()`, which only handles primitive scalar
21
+ types (`bool`, `number`, `string`, ...) and has no arm for `Custom`, `Record`,
22
+ `Struct`, `Enum`, or `Trait` — those all fell through the `_ => None` catch-all,
23
+ so `schema_annotation_to_edn` silently defaulted to `Edn::tag("dynamic")`.
24
+
25
+ Since `code_entry_edn_pairs` recomputes every entry's schema EDN from its in-memory
26
+ `CalcitTypeAnnotation` on *every* file save (not just for touched defs), any
27
+ `cr edit`/`cr tree` write to a file containing these shorthand-tagged schemas
28
+ degrades them all to `:dynamic`, regardless of which def was actually edited.
29
+
30
+ Note `builtin_tag_name()` is also used by `to_brief_string()` and other call
31
+ sites that intentionally want more specific output (e.g. `struct Person` instead
32
+ of a bare `:struct`) for those variants — so widening `builtin_tag_name()` itself
33
+ to cover them would have regressed those call sites. The fix instead lives
34
+ directly in `schema_annotation_to_edn`, which is the one place that needs the
35
+ short kind-tag representation for the snapshot file format.
36
+
37
+ ## Fix
38
+
39
+ Added explicit match arms to `schema_annotation_to_edn` in [src/snapshot.rs](../src/snapshot.rs):
40
+ - `Custom(value)`: coerce the wrapped `Calcit::Tag` back into its `Edn::Tag` (falls
41
+ back to `:dynamic` only if the wrapped value isn't a bare tag, which shouldn't
42
+ happen via `from_tag_name`).
43
+ - `Record(_)` → `:record`, `Struct(..)` → `:struct`, `Enum(..)` → `:enum`,
44
+ `Trait(_)` → `:trait`.
45
+
46
+ This preserves the user's original expressed intent (e.g. `:schema :impl`) instead
47
+ of silently downgrading it to `:dynamic`, which would otherwise turn off
48
+ schema-based type-checking coverage for those defs.
49
+
50
+ Recovered the previously-corrupted `calcit/test-traits.cirru` by reverting to
51
+ `git checkout` and reapplying the new `test-method-introspection` assertions with
52
+ the fixed binary — confirmed via `git diff` that this time only the intended
53
+ 7 new lines changed, with zero incidental `:schema` churn.
54
+
55
+ ## Regression test
56
+
57
+ `test_custom_kind_schema_tags_round_trip_instead_of_degrading_to_dynamic` in
58
+ [src/snapshot.rs](../src/snapshot.rs) asserts `schema_annotation_to_edn(CalcitTypeAnnotation::from_tag_name(kind))`
59
+ round-trips to `Edn::tag(kind)` for `struct`/`enum`/`trait`/`impl`/`record`.
60
+
61
+ ## Validation
62
+
63
+ `cargo fmt`, `cargo clippy --all-targets -- -D warnings`, `cargo test` (all pass),
64
+ `yarn check-all` (exit 0).
@@ -0,0 +1,27 @@
1
+ # 2026-07-19 22:36 文档知识图谱与增量缓存
2
+
3
+ ## 修改概要
4
+
5
+ - 为 `cr docs` 增加文档知识图构建、检查、关系遍历、路径查询、定义反查、缺失定义和孤立节点命令。
6
+ - 使用 Markdown frontmatter 的 `id`、`parent`、`related`、`requires`、`leads_to`、`code_refs` 表达可 Git 管理的知识关系。
7
+ - 在 `~/.config/calcit/docs-cache/` 生成 JSON 文本缓存,按文件内容、解析器/schema 版本和内置 Calcit snapshot 指纹失效。
8
+ - 为文档索引 RFC、`CalcitAgent.md`、索引规范和验证案例补充使用说明。
9
+ - 为核心数据结构文档补充稳定节点 ID 与 Calcit 定义引用,验证从概念到 API 和编辑工作流的跳转。
10
+
11
+ ## 验证
12
+
13
+ - `cargo fmt --all`
14
+ - `cargo clippy --all-targets -- -D warnings`
15
+ - `cargo test -q`
16
+ - `yarn compile`
17
+ - `cr docs graph build`
18
+ - `cr docs graph check`
19
+ - `cr docs graph path core/features/list core/run/edit-tree`
20
+ - `cr docs graph explain calcit.core/nth`
21
+ - `cr docs graph missing`
22
+ - `cr docs graph orphans`
23
+
24
+ ## 后续方向
25
+
26
+ - 将 `missing` 从总量报告扩展为按公共 API、语法符号和内部定义分类。
27
+ - 接入模块或应用级 Calcit snapshot,并增加示例覆盖检查与 LLM context 聚合。
@@ -0,0 +1,17 @@
1
+ # 2026-07-19 22:45 graph missing 分批筛选
2
+
3
+ ## 修改概要
4
+
5
+ - 为 `cr docs graph missing` 增加 `--ns` namespace 前缀过滤。
6
+ - 增加 `--limit` 输出数量限制,便于按批次补充公共 API 的 `code_refs`。
7
+ - 在 `CalcitAgent.md` 和知识图谱 RFC 中补充该操作说明。
8
+ - 增加 namespace 过滤、数量限制和内部命名排除的单元测试。
9
+
10
+ ## 验证
11
+
12
+ - `cargo fmt --all`
13
+ - `cargo test -q`
14
+ - `cargo clippy --all-targets -- -D warnings`
15
+ - `yarn compile`
16
+ - `cr docs graph missing --ns calcit.core --limit 5`
17
+ - `git diff --check`
@@ -0,0 +1,16 @@
1
+ # 2026-07-19 23:10 graph explain 定义摘要
2
+
3
+ ## 修改概要
4
+
5
+ - 为 `cr docs graph explain` 增加 `--full`,输出 Calcit 定义的 doc 和 examples 可用性。
6
+ - 缓存定义记录保存 doc 文本,并将 cache schema 升级为 5,确保旧缓存自动重建。
7
+ - 更新 command echo、`CalcitAgent.md` 和 graph 验证案例。
8
+
9
+ ## 验证
10
+
11
+ - `cargo fmt --all`
12
+ - `cargo test -q`
13
+ - `cargo clippy --all-targets -- -D warnings`
14
+ - `yarn compile`
15
+ - `cr docs graph explain calcit.core/nth --full`
16
+ - `git diff --check`
@@ -210,6 +210,14 @@ export let hashFunction = (x) => {
210
210
  return h;
211
211
  }
212
212
  if (typeof x === "function") {
213
+ // method values are closures created on the fly (see invoke_method_closure);
214
+ // hash by method name so equal methods share the same hash, matching isEqual
215
+ const methodName = x.__calcitMethodName;
216
+ if (methodName != null) {
217
+ let h = mergeValueHash(defaultHash_fn, methodName);
218
+ x[calcit_dirty_hash_key] = h;
219
+ return h;
220
+ }
213
221
  fnHashCounter = fnHashCounter + 1;
214
222
  let h = mergeValueHash(defaultHash_fn, fnHashCounter);
215
223
  x[calcit_dirty_hash_key] = h;
@@ -383,6 +391,10 @@ export let toString = (x, escaped, disableJsDataWarning = false) => {
383
391
  return x.toString();
384
392
  }
385
393
  if (typeof x === "function") {
394
+ const methodName = x.__calcitMethodName;
395
+ if (methodName != null) {
396
+ return "." + methodName;
397
+ }
386
398
  return `(&fn ...)`;
387
399
  }
388
400
  if (x instanceof CalcitSymbol) {
@@ -541,7 +553,14 @@ export let _$n__$e_ = (x, y) => {
541
553
  return false;
542
554
  }
543
555
  if (tx === "function") {
544
- // comparing functions by reference
556
+ // method values are closures created on the fly (see invoke_method_closure),
557
+ // so two methods with the same name must compare equal by name, not by reference
558
+ const mx = x.__calcitMethodName;
559
+ const my = y.__calcitMethodName;
560
+ if (mx != null || my != null) {
561
+ return mx === my;
562
+ }
563
+ // comparing plain functions by reference
545
564
  return x === y;
546
565
  }
547
566
  if (x instanceof CalcitTag) {
@@ -1834,6 +1834,17 @@ function lookup_impls(obj) {
1834
1834
  tag = "&core-set-methods";
1835
1835
  impls = normalize_builtin_impls(calcit_builtin_impls.set);
1836
1836
  }
1837
+ else if (obj instanceof CalcitStruct) {
1838
+ // Bare type definitions (not yet instantiated) carry their own attached
1839
+ // impls, so introspection tools like `&methods-of` can answer "what
1840
+ // methods will instances of this type have" without a concrete instance.
1841
+ tag = obj.name.toString();
1842
+ impls = obj.impls;
1843
+ }
1844
+ else if (obj instanceof CalcitEnum) {
1845
+ tag = obj.name();
1846
+ impls = obj.impls;
1847
+ }
1837
1848
  else if (typeof obj === "number") {
1838
1849
  tag = "&core-number-methods";
1839
1850
  impls = normalize_builtin_impls(calcit_builtin_impls.number);
@@ -1862,7 +1873,7 @@ export function invoke_method(p, obj, ...args) {
1862
1873
  let tag = pair[1];
1863
1874
  // builtin impl lists are ordered by priority in calcit-core.
1864
1875
  // user-defined values use impl-traits append, so later impls override earlier ones.
1865
- let reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple;
1876
+ let reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple || obj instanceof CalcitStruct || obj instanceof CalcitEnum;
1866
1877
  let idx = reverse ? impls.length - 1 : 0;
1867
1878
  while (reverse ? idx >= 0 : idx < impls.length) {
1868
1879
  let klass = impls[idx];
@@ -1882,12 +1893,16 @@ export function invoke_method(p, obj, ...args) {
1882
1893
  export function _$n_methods_of(obj) {
1883
1894
  if (arguments.length !== 1)
1884
1895
  throw new Error("&methods-of expected 1 argument");
1896
+ // Traits declare methods directly rather than through attached impls.
1897
+ if (obj instanceof CalcitTrait) {
1898
+ return new CalcitSliceList(obj.methods.map((m) => invoke_method_closure(m.value)));
1899
+ }
1885
1900
  let pair = lookup_impls(obj);
1886
1901
  if (pair == null) {
1887
1902
  throw new Error(`&methods-of cannot resolve impls for: ${toString(obj, true)}`);
1888
1903
  }
1889
1904
  let impls = pair[0];
1890
- let reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple;
1905
+ let reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple || obj instanceof CalcitStruct || obj instanceof CalcitEnum;
1891
1906
  let seen = new Set();
1892
1907
  let ys = [];
1893
1908
  let idx = reverse ? impls.length - 1 : 0;
@@ -1895,10 +1910,11 @@ export function _$n_methods_of(obj) {
1895
1910
  let impl = impls[idx];
1896
1911
  if (impl != null) {
1897
1912
  for (let k = 0; k < impl.fields.length; k++) {
1898
- let name = "." + impl.fields[k].value;
1913
+ let rawName = impl.fields[k].value;
1914
+ let name = "." + rawName;
1899
1915
  if (!seen.has(name)) {
1900
1916
  seen.add(name);
1901
- ys.push(name);
1917
+ ys.push(invoke_method_closure(rawName));
1902
1918
  }
1903
1919
  }
1904
1920
  }
@@ -1909,12 +1925,26 @@ export function _$n_methods_of(obj) {
1909
1925
  export function _$n_inspect_methods(obj, note) {
1910
1926
  if (arguments.length !== 2)
1911
1927
  throw new Error("&inspect-methods expected 2 arguments");
1928
+ if (obj instanceof CalcitTrait) {
1929
+ console.log("\n&inspect-methods");
1930
+ console.log(`Note: ${toString(note, true)}`);
1931
+ console.log(`Value type: ${type_of(obj).toString()}`);
1932
+ console.log(`Value: ${toString(obj, true)}`);
1933
+ console.log("Method call syntax: `.method self p1 p2`");
1934
+ console.log(" - dot is part of the method name, first arg is the receiver\n");
1935
+ const names = obj.methods.map((m) => "." + m.value);
1936
+ console.log(`Trait methods declared directly (no impls): ${names.length}`);
1937
+ console.log(`\nAll methods (unique, high → low): ${names.length}`);
1938
+ console.log(" " + names.join(" "));
1939
+ console.log("\n");
1940
+ return obj;
1941
+ }
1912
1942
  let pair = lookup_impls(obj);
1913
1943
  if (pair == null) {
1914
1944
  throw new Error(`&inspect-methods cannot resolve impls for: ${toString(obj, true)}`);
1915
1945
  }
1916
1946
  let impls = pair[0];
1917
- let reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple;
1947
+ let reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple || obj instanceof CalcitStruct || obj instanceof CalcitEnum;
1918
1948
  console.log("\n&inspect-methods");
1919
1949
  console.log(`Note: ${toString(note, true)}`);
1920
1950
  console.log(`Value type: ${type_of(obj).toString()}`);
@@ -1941,7 +1971,7 @@ export function _$n_inspect_methods(obj, note) {
1941
1971
  }
1942
1972
  let ms = _$n_methods_of(obj);
1943
1973
  console.log(`\nAll methods (unique, high → low): ${ms.len()}`);
1944
- console.log(" " + Array.from(ms.items()).join(" "));
1974
+ console.log(" " + Array.from(ms.items()).map((m) => toString(m, false)).join(" "));
1945
1975
  console.log("\n");
1946
1976
  return obj;
1947
1977
  }
@@ -1963,7 +1993,7 @@ export function _$n_trait_call(traitDef, method, obj, ...args) {
1963
1993
  throw new Error(`&trait-call cannot resolve impls for: ${toString(obj, true)}`);
1964
1994
  }
1965
1995
  const impls = pair[0];
1966
- const reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple;
1996
+ const reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple || obj instanceof CalcitStruct || obj instanceof CalcitEnum;
1967
1997
  let idx = reverse ? impls.length - 1 : 0;
1968
1998
  while (reverse ? idx >= 0 : idx < impls.length) {
1969
1999
  const impl = impls[idx];
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calcit/procs",
3
- "version": "0.12.48",
3
+ "version": "0.12.50",
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.12.48",
3
+ "version": "0.12.50",
4
4
  "main": "./lib/calcit.procs.mjs",
5
5
  "devDependencies": {
6
6
  "@types/node": "^25.7.0",
@@ -238,6 +238,14 @@ export let hashFunction = (x: CalcitValue): Hash => {
238
238
  return h;
239
239
  }
240
240
  if (typeof x === "function") {
241
+ // method values are closures created on the fly (see invoke_method_closure);
242
+ // hash by method name so equal methods share the same hash, matching isEqual
243
+ const methodName = (x as { __calcitMethodName?: string }).__calcitMethodName;
244
+ if (methodName != null) {
245
+ let h = mergeValueHash(defaultHash_fn, methodName);
246
+ (x as any)[calcit_dirty_hash_key] = h;
247
+ return h;
248
+ }
241
249
  fnHashCounter = fnHashCounter + 1;
242
250
  let h = mergeValueHash(defaultHash_fn, fnHashCounter);
243
251
  (x as any)[calcit_dirty_hash_key] = h;
@@ -412,6 +420,10 @@ export let toString = (x: CalcitValue, escaped: boolean, disableJsDataWarning: b
412
420
  return x.toString();
413
421
  }
414
422
  if (typeof x === "function") {
423
+ const methodName = (x as { __calcitMethodName?: string }).__calcitMethodName;
424
+ if (methodName != null) {
425
+ return "." + methodName;
426
+ }
415
427
  return `(&fn ...)`;
416
428
  }
417
429
  if (x instanceof CalcitSymbol) {
@@ -579,7 +591,14 @@ export let _$n__$e_ = (x: CalcitValue, y: CalcitValue): boolean => {
579
591
  return false;
580
592
  }
581
593
  if (tx === "function") {
582
- // comparing functions by reference
594
+ // method values are closures created on the fly (see invoke_method_closure),
595
+ // so two methods with the same name must compare equal by name, not by reference
596
+ const mx = (x as { __calcitMethodName?: string }).__calcitMethodName;
597
+ const my = (y as { __calcitMethodName?: string }).__calcitMethodName;
598
+ if (mx != null || my != null) {
599
+ return mx === my;
600
+ }
601
+ // comparing plain functions by reference
583
602
  return x === y;
584
603
  }
585
604
  if (x instanceof CalcitTag) {
@@ -1946,6 +1946,15 @@ function lookup_impls(obj: CalcitValue): [CalcitImpl[], string] {
1946
1946
  } else if (obj instanceof CalcitSet) {
1947
1947
  tag = "&core-set-methods";
1948
1948
  impls = normalize_builtin_impls(calcit_builtin_impls.set);
1949
+ } else if (obj instanceof CalcitStruct) {
1950
+ // Bare type definitions (not yet instantiated) carry their own attached
1951
+ // impls, so introspection tools like `&methods-of` can answer "what
1952
+ // methods will instances of this type have" without a concrete instance.
1953
+ tag = obj.name.toString();
1954
+ impls = obj.impls;
1955
+ } else if (obj instanceof CalcitEnum) {
1956
+ tag = obj.name();
1957
+ impls = obj.impls;
1949
1958
  } else if (typeof obj === "number") {
1950
1959
  tag = "&core-number-methods";
1951
1960
  impls = normalize_builtin_impls(calcit_builtin_impls.number);
@@ -1971,7 +1980,7 @@ export function invoke_method(p: string, obj: CalcitValue, ...args: CalcitValue[
1971
1980
  let tag = pair[1];
1972
1981
  // builtin impl lists are ordered by priority in calcit-core.
1973
1982
  // user-defined values use impl-traits append, so later impls override earlier ones.
1974
- let reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple;
1983
+ let reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple || obj instanceof CalcitStruct || obj instanceof CalcitEnum;
1975
1984
  let idx = reverse ? impls.length - 1 : 0;
1976
1985
  while (reverse ? idx >= 0 : idx < impls.length) {
1977
1986
  let klass = impls[idx];
@@ -1991,12 +2000,16 @@ export function invoke_method(p: string, obj: CalcitValue, ...args: CalcitValue[
1991
2000
 
1992
2001
  export function _$n_methods_of(obj: CalcitValue): CalcitSliceList {
1993
2002
  if (arguments.length !== 1) throw new Error("&methods-of expected 1 argument");
2003
+ // Traits declare methods directly rather than through attached impls.
2004
+ if (obj instanceof CalcitTrait) {
2005
+ return new CalcitSliceList(obj.methods.map((m) => invoke_method_closure(m.value)));
2006
+ }
1994
2007
  let pair = lookup_impls(obj);
1995
2008
  if (pair == null) {
1996
2009
  throw new Error(`&methods-of cannot resolve impls for: ${toString(obj, true)}`);
1997
2010
  }
1998
2011
  let impls = pair[0];
1999
- let reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple;
2012
+ let reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple || obj instanceof CalcitStruct || obj instanceof CalcitEnum;
2000
2013
  let seen = new Set<string>();
2001
2014
  let ys: CalcitValue[] = [];
2002
2015
 
@@ -2005,10 +2018,11 @@ export function _$n_methods_of(obj: CalcitValue): CalcitSliceList {
2005
2018
  let impl = impls[idx];
2006
2019
  if (impl != null) {
2007
2020
  for (let k = 0; k < impl.fields.length; k++) {
2008
- let name = "." + impl.fields[k].value;
2021
+ let rawName = impl.fields[k].value;
2022
+ let name = "." + rawName;
2009
2023
  if (!seen.has(name)) {
2010
2024
  seen.add(name);
2011
- ys.push(name);
2025
+ ys.push(invoke_method_closure(rawName));
2012
2026
  }
2013
2027
  }
2014
2028
  }
@@ -2019,12 +2033,26 @@ export function _$n_methods_of(obj: CalcitValue): CalcitSliceList {
2019
2033
 
2020
2034
  export function _$n_inspect_methods(obj: CalcitValue, note: CalcitValue): CalcitValue {
2021
2035
  if (arguments.length !== 2) throw new Error("&inspect-methods expected 2 arguments");
2036
+ if (obj instanceof CalcitTrait) {
2037
+ console.log("\n&inspect-methods");
2038
+ console.log(`Note: ${toString(note, true)}`);
2039
+ console.log(`Value type: ${type_of(obj).toString()}`);
2040
+ console.log(`Value: ${toString(obj, true)}`);
2041
+ console.log("Method call syntax: `.method self p1 p2`");
2042
+ console.log(" - dot is part of the method name, first arg is the receiver\n");
2043
+ const names = obj.methods.map((m) => "." + m.value);
2044
+ console.log(`Trait methods declared directly (no impls): ${names.length}`);
2045
+ console.log(`\nAll methods (unique, high → low): ${names.length}`);
2046
+ console.log(" " + names.join(" "));
2047
+ console.log("\n");
2048
+ return obj;
2049
+ }
2022
2050
  let pair = lookup_impls(obj);
2023
2051
  if (pair == null) {
2024
2052
  throw new Error(`&inspect-methods cannot resolve impls for: ${toString(obj, true)}`);
2025
2053
  }
2026
2054
  let impls = pair[0];
2027
- let reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple;
2055
+ let reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple || obj instanceof CalcitStruct || obj instanceof CalcitEnum;
2028
2056
 
2029
2057
  console.log("\n&inspect-methods");
2030
2058
  console.log(`Note: ${toString(note, true)}`);
@@ -2054,7 +2082,7 @@ export function _$n_inspect_methods(obj: CalcitValue, note: CalcitValue): Calcit
2054
2082
 
2055
2083
  let ms = _$n_methods_of(obj);
2056
2084
  console.log(`\nAll methods (unique, high → low): ${ms.len()}`);
2057
- console.log(" " + (Array.from(ms.items()) as string[]).join(" "));
2085
+ console.log(" " + Array.from(ms.items()).map((m) => toString(m, false)).join(" "));
2058
2086
  console.log("\n");
2059
2087
 
2060
2088
  return obj;
@@ -2078,7 +2106,7 @@ export function _$n_trait_call(traitDef: CalcitValue, method: CalcitValue, obj:
2078
2106
  throw new Error(`&trait-call cannot resolve impls for: ${toString(obj, true)}`);
2079
2107
  }
2080
2108
  const impls = pair[0];
2081
- const reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple;
2109
+ const reverse = obj instanceof CalcitRecord || obj instanceof CalcitTuple || obj instanceof CalcitStruct || obj instanceof CalcitEnum;
2082
2110
  let idx = reverse ? impls.length - 1 : 0;
2083
2111
  while (reverse ? idx >= 0 : idx < impls.length) {
2084
2112
  const impl = impls[idx];