From d3e7d457ae3052950308092ce52d7fe75053f9a6 Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 10 Mar 2026 17:55:38 +0800 Subject: [PATCH 01/57] migrate snapshot ns entries to NsEntry; tag 0.12.2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- build.rs | 37 +- calcit/add.cirru | 3 +- calcit/debug/check-args.cirru | 3 +- calcit/debug/debug-overflow.cirru | 3 +- calcit/debug/macro-ns.cirru | 6 +- calcit/debug/var-shadow.cirru | 3 +- calcit/editor/calcit.cirru | 1442 ++++------------- calcit/editor/compact.cirru | 9 +- calcit/fibo.cirru | 3 +- calcit/test-algebra.cirru | 3 +- calcit/test-cond.cirru | 3 +- calcit/test-doc-smoke.cirru | 3 +- calcit/test-edn.cirru | 3 +- calcit/test-enum.cirru | 3 +- calcit/test-fn.cirru | 3 +- calcit/test-generics.cirru | 3 +- calcit/test-gynienic.cirru | 6 +- calcit/test-hygienic.cirru | 6 +- calcit/test-invalid-tag.cirru | 3 +- calcit/test-ir-type-info.cirru | 3 +- calcit/test-js.cirru | 3 +- calcit/test-lens.cirru | 3 +- calcit/test-list.cirru | 3 +- calcit/test-macro.cirru | 3 +- calcit/test-map.cirru | 3 +- calcit/test-math.cirru | 3 +- calcit/test-method-errors.cirru | 3 +- calcit/test-method-validation.cirru | 3 +- calcit/test-nested-types.cirru | 3 +- calcit/test-nil.cirru | 3 +- calcit/test-optimize.cirru | 3 +- calcit/test-proc-type-warnings.cirru | 3 +- calcit/test-record.cirru | 3 +- calcit/test-recur-arity.cirru | 3 +- calcit/test-recursion.cirru | 3 +- calcit/test-set.cirru | 3 +- calcit/test-string.cirru | 3 +- calcit/test-sum-types.cirru | 3 +- calcit/test-tag-match-validation.cirru | 3 +- calcit/test-traits.cirru | 3 +- calcit/test-tuple.cirru | 3 +- calcit/test-types-inference.cirru | 3 +- calcit/test-types.cirru | 3 +- calcit/test.cirru | 3 +- .../schema-call-arg-type-mismatch.cirru | 21 +- calcit/type-fail/schema-kind-mismatch.cirru | 15 +- calcit/type-fail/schema-required-arity.cirru | 12 +- calcit/type-fail/schema-rest-missing.cirru | 12 +- calcit/type-fail/schema-rest-unexpected.cirru | 14 +- calcit/util.cirru | 3 +- docs/CalcitAgent.md | 1 + ...6-0310-1754-ns-entry-snapshot-migration.md | 66 + package.json | 2 +- src/bin/cli_handlers/edit.rs | 7 +- src/bin/cr_sync.rs | 70 +- src/bin/cr_tests/type_fail.rs | 2 +- src/cirru/calcit-core.cirru | 6 +- src/detailed_snapshot.rs | 60 +- src/snapshot.rs | 94 +- 61 files changed, 696 insertions(+), 1313 deletions(-) create mode 100644 editing-history/2026-0310-1754-ns-entry-snapshot-migration.md diff --git a/Cargo.lock b/Cargo.lock index 10f1c3e6..31e8c0cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.1" +version = "0.12.2" dependencies = [ "argh", "bisection_key", diff --git a/Cargo.toml b/Cargo.toml index 565f761f..0a3d557e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.1" +version = "0.12.2" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/build.rs b/build.rs index 6bf98db3..b496b85a 100644 --- a/build.rs +++ b/build.rs @@ -28,9 +28,15 @@ pub struct CodeEntry { pub schema: Option, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NsEntry { + pub doc: String, + pub code: Cirru, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct FileInSnapShot { - pub ns: CodeEntry, + pub ns: NsEntry, pub defs: HashMap, } @@ -204,6 +210,31 @@ fn parse_code_entry(edn: Edn, owner: &str) -> Result { }) } +fn parse_ns_entry(edn: Edn, owner: &str) -> Result { + let record: EdnRecordView = match edn { + Edn::Record(r) => r, + other => { + return Err(format!( + "{owner}: expected NsEntry/CodeEntry record, got {}", + format_edn_preview(&other) + )); + } + }; + let mut doc = String::new(); + let mut code: Option = None; + for (key, value) in &record.pairs { + match key.arc_str().as_ref() { + "doc" => doc = from_edn(value.clone()).map_err(|e| format!("{owner}: invalid `:doc`: {e}"))?, + "code" => code = Some(from_edn(value.clone()).map_err(|e| format!("{owner}: invalid `:code`: {e}"))?), + _ => {} + } + } + Ok(NsEntry { + doc, + code: code.ok_or_else(|| format!("{owner}: missing `:code` field in NsEntry"))?, + }) +} + fn parse_file_in_snapshot(edn: Edn, file_name: &str) -> Result { let record: EdnRecordView = match edn { Edn::Record(r) => r, @@ -214,11 +245,11 @@ fn parse_file_in_snapshot(edn: Edn, file_name: &str) -> Result = None; + let mut ns: Option = None; let mut defs: HashMap = HashMap::new(); for (key, value) in &record.pairs { match key.arc_str().as_ref() { - "ns" => ns = Some(parse_code_entry(value.clone(), &format!("{file_name}/:ns"))?), + "ns" => ns = Some(parse_ns_entry(value.clone(), &format!("{file_name}/:ns"))?), "defs" => { let map = match value { Edn::Map(m) => m, diff --git a/calcit/add.cirru b/calcit/add.cirru index 6b10cbe7..aceeb771 100644 --- a/calcit/add.cirru +++ b/calcit/add.cirru @@ -10,7 +10,6 @@ :code $ quote defn main! () $ + 1 2 :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns app.main $ :require - :examples $ [] diff --git a/calcit/debug/check-args.cirru b/calcit/debug/check-args.cirru index 824a70ed..026c0b9a 100644 --- a/calcit/debug/check-args.cirru +++ b/calcit/debug/check-args.cirru @@ -26,8 +26,7 @@ :code $ quote defn main! () (; "bad case examples for args checking") (f1 1 4) (f2 1) (f2 1 2) (f2 1 2 4) (f2) (f3 1) (f3 1 2) (f3 1 2 3) (f3) :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns check-args.main $ :require [] util.core :refer $ [] log-title inside-eval: - :examples $ [] diff --git a/calcit/debug/debug-overflow.cirru b/calcit/debug/debug-overflow.cirru index f5ee920c..dfa43486 100644 --- a/calcit/debug/debug-overflow.cirru +++ b/calcit/debug/debug-overflow.cirru @@ -37,8 +37,7 @@ , ~x0 $ &+ ~x0 rec $ ~@ xs :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns debug-overflow.main $ :require [] util.core :refer $ [] log-title inside-eval: - :examples $ [] diff --git a/calcit/debug/macro-ns.cirru b/calcit/debug/macro-ns.cirru index c6bc8afe..53893420 100644 --- a/calcit/debug/macro-ns.cirru +++ b/calcit/debug/macro-ns.cirru @@ -14,19 +14,17 @@ |v $ %{} :CodeEntry (:doc |) (:schema nil) :code $ quote (def v 100) :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns macro-ns.lib $ :require [] util.core :refer $ [] log-title inside-eval: - :examples $ [] |macro-ns.main $ %{} :FileEntry :defs $ {} |main! $ %{} :CodeEntry (:doc |) (:schema nil) :code $ quote defn main! () $ expand-1 1 :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns macro-ns.main $ :require macro-ns.lib :refer $ expand-1 - :examples $ [] diff --git a/calcit/debug/var-shadow.cirru b/calcit/debug/var-shadow.cirru index 9a3885a6..8e32f74b 100644 --- a/calcit/debug/var-shadow.cirru +++ b/calcit/debug/var-shadow.cirru @@ -12,7 +12,6 @@ f1 "|local function" println check/f1 :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns app.main $ :require ([] check-args.main :as check) - :examples $ [] diff --git a/calcit/editor/calcit.cirru b/calcit/editor/calcit.cirru index 3986ccb5..c4351898 100644 --- a/calcit/editor/calcit.cirru +++ b/calcit/editor/calcit.cirru @@ -1,5 +1,5 @@ -{} (:package |app) +{} (:entries nil) (:package |app) :configs $ {} (:compact-output? true) (:extension |.cljs) (:init-fn |app.main/main!) (:local-ui? false) (:output |src) (:port 6001) (:reload-fn |app.main/reload!) (:version |0.0.1) :modules $ [] :files $ {} @@ -16,6 +16,7 @@ :data $ {} |T $ %{} :Leaf (:at 1618661024070) (:by |u0) (:text |println) |j $ %{} :Leaf (:at 1618661026271) (:by |u0) (:text "|\"f2 in lib") + :examples $ [] |f3 $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1618661052591) (:by |u0) :data $ {} @@ -33,7 +34,8 @@ |T $ %{} :Leaf (:at 1618661071077) (:by |u0) (:text |println) |j $ %{} :Leaf (:at 1618661073107) (:by |u0) (:text "|\"v:") |r $ %{} :Leaf (:at 1618661074709) (:by |u0) (:text |x) - :ns $ %{} :CodeEntry (:doc |) + :examples $ [] + :ns $ %{} :NsEntry (:doc |) :code $ %{} :Expr (:at 1618661017191) (:by |u0) :data $ {} |T $ %{} :Leaf (:at 1618661017191) (:by |u0) (:text |ns) @@ -56,6 +58,7 @@ |T $ %{} :Leaf (:at 1618740286902) (:by |u0) (:text |&+) |j $ %{} :Leaf (:at 1618740317157) (:by |u0) (:text |~x) |r $ %{} :Leaf (:at 1618740287700) (:by |u0) (:text |1) + :examples $ [] |add-by-2 $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1618740293087) (:by |u0) :data $ {} @@ -75,34 +78,37 @@ :data $ {} |T $ %{} :Leaf (:at 1618740343769) (:by |u0) (:text |add-by-1) |j $ %{} :Leaf (:at 1618740351578) (:by |u0) (:text |~x) + :examples $ [] |add-num $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618663286974) (:by |u0) + :code $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618663289791) (:by |u0) (:text |defmacro) - |j $ %{} :Leaf (:at 1618663286974) (:by |u0) (:text |add-num) - |r $ %{} :Expr (:at 1618663286974) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |defmacro) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-num) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618663291903) (:by |u0) (:text |a) - |j $ %{} :Leaf (:at 1618663292537) (:by |u0) (:text |b) - |v $ %{} :Expr (:at 1618663324823) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |a) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |b) + |Z $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |D $ %{} :Leaf (:at 1618663328933) (:by |u0) (:text |quasiquote) - |T $ %{} :Expr (:at 1618663294505) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quasiquote) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618663307918) (:by |u0) (:text |&let) - |j $ %{} :Leaf (:at 1618663305807) (:by |u0) (:text |nil) - |r $ %{} :Expr (:at 1618663312809) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&let) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618663314951) (:by |u0) (:text |&+) - |j $ %{} :Expr (:at 1618663331895) (:by |u0) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |D $ %{} :Leaf (:at 1618663333114) (:by |u0) (:text |~) - |T $ %{} :Leaf (:at 1618663316680) (:by |u0) (:text |a) - |r $ %{} :Expr (:at 1618663335086) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |~) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |a) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |D $ %{} :Leaf (:at 1618663336609) (:by |u0) (:text |~) - |T $ %{} :Leaf (:at 1618663317648) (:by |u0) (:text |b) - :ns $ %{} :CodeEntry (:doc |) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |~) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |b) + :examples $ [] + :ns $ %{} :NsEntry (:doc |) :code $ %{} :Expr (:at 1618663277036) (:by |u0) :data $ {} |T $ %{} :Leaf (:at 1618663277036) (:by |u0) (:text |ns) @@ -151,6 +157,7 @@ |T $ %{} :Leaf (:at 1618730435581) (:by |u0) (:text |&-) |j $ %{} :Leaf (:at 1618730436881) (:by |u0) (:text |times) |r $ %{} :Leaf (:at 1618730437157) (:by |u0) (:text |1) + :examples $ [] |call-3 $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1618767957921) (:by |u0) :data $ {} @@ -176,6 +183,7 @@ |T $ %{} :Leaf (:at 1618767963282) (:by |u0) (:text |println) |j $ %{} :Leaf (:at 1618767977407) (:by |u0) (:text "|\"c is:") |r $ %{} :Leaf (:at 1618767973639) (:by |u0) (:text |c) + :examples $ [] |call-macro $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1618769676627) (:by |u0) :data $ {} @@ -205,6 +213,7 @@ :data $ {} |D $ %{} :Leaf (:at 1618769865395) (:by |u0) (:text |~@) |T $ %{} :Leaf (:at 1618769725113) (:by |u0) (:text |xs) + :examples $ [] |call-many $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1618769509051) (:by |u0) :data $ {} @@ -229,352 +238,345 @@ |T $ %{} :Leaf (:at 1618769525175) (:by |u0) (:text |println) |j $ %{} :Leaf (:at 1618769525982) (:by |u0) (:text "|\"xs") |r $ %{} :Leaf (:at 1618769526896) (:by |u0) (:text |xs) + :examples $ [] |demos $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618539520156) (:by |u0) + :code $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618539520156) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1619930563832) (:by |u0) (:text |demos) - |r $ %{} :Expr (:at 1618539520156) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |defn) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |demos) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |v $ %{} :Expr (:at 1618539523268) (:by |u0) + |Z $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618539524965) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618539525898) (:by |u0) (:text "|\"demo") - |x $ %{} :Expr (:at 1618646117925) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"demo") + |b $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618646119371) (:by |u0) (:text |println) - |j $ %{} :Expr (:at 1618646119955) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618646122999) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618658555366) (:by |u0) (:text |2) - |r $ %{} :Leaf (:at 1618646121081) (:by |u0) (:text |2) - |y $ %{} :Expr (:at 1618658517774) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618658519944) (:by |u0) (:text |println) - |L $ %{} :Leaf (:at 1618658520784) (:by |u0) (:text "|\"f1") - |T $ %{} :Expr (:at 1618658494170) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618658495406) (:by |u0) (:text |f1) - |yT $ %{} :Expr (:at 1618659585738) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633952520593) (:by |u0) (:text |print-values) - |j $ %{} :Leaf (:at 1618659590535) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618659591512) (:by |u0) (:text "|\"1") - |v $ %{} :Leaf (:at 1618659595541) (:by |u0) (:text |:a) - |x $ %{} :Expr (:at 1618659596691) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618659596880) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618659597668) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618659597892) (:by |u0) (:text |2) - |yj $ %{} :Expr (:at 1618660536373) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |d $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618660537901) (:by |u0) (:text |println) - |j $ %{} :Expr (:at 1618660538186) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"f1") + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618660568253) (:by |u0) (:text |&{}) - |j $ %{} :Leaf (:at 1618660541656) (:by |u0) (:text |:a) - |r $ %{} :Leaf (:at 1618660542971) (:by |u0) (:text |1) - |v $ %{} :Leaf (:at 1618660543782) (:by |u0) (:text |:b) - |x $ %{} :Leaf (:at 1618660544981) (:by |u0) (:text |2) - |yr $ %{} :Expr (:at 1618660963223) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618660963956) (:by |u0) (:text |println) - |j $ %{} :Expr (:at 1618660964279) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618660965160) (:by |u0) (:text |#{}) - |j $ %{} :Leaf (:at 1618660965550) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618660965773) (:by |u0) (:text |2) - |v $ %{} :Leaf (:at 1618660966299) (:by |u0) (:text |3) - |x $ %{} :Leaf (:at 1618660970012) (:by |u0) (:text ||four) - |yx $ %{} :Expr (:at 1618661082170) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661298818) (:by |u0) (:text |lib/f2) - |yy $ %{} :Expr (:at 1618661300982) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661302264) (:by |u0) (:text |f3) - |j $ %{} :Leaf (:at 1618661308107) (:by |u0) (:text "|\"arg of 3") - |yyT $ %{} :Expr (:at 1618664966181) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618664966725) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618664980683) (:by |u0) (:text "|\"quote:") - |r $ %{} :Expr (:at 1618664968766) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |f1) + |f $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618664969526) (:by |u0) (:text |quote) - |j $ %{} :Expr (:at 1618664969796) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618665001007) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618664970588) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618664970840) (:by |u0) (:text |2) - |yyb $ %{} :Expr (:at 1618665182369) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618665182898) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618665185901) (:by |u0) (:text "|\"quo:") - |r $ %{} :Leaf (:at 1618665190172) (:by |u0) (:text |'demo) - |v $ %{} :Expr (:at 1618665201691) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&{}) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |:a) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |:b) + |b $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |h $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618665202393) (:by |u0) (:text |quote) - |j $ %{} :Leaf (:at 1618665203149) (:by |u0) (:text |'demo) - |yyj $ %{} :Expr (:at 1618664972310) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |#{}) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) + |b $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text ||four) + |j $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |lib/f2) + |l $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |f3) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"arg of 3") + |n $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618664972897) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618664978986) (:by |u0) (:text "|\"eval:") - |r $ %{} :Expr (:at 1618664981960) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"quote:") + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618664982687) (:by |u0) (:text |eval) - |j $ %{} :Expr (:at 1618664983058) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quote) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618664984086) (:by |u0) (:text |quote) - |j $ %{} :Expr (:at 1618664984358) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |p $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"quo:") + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |'demo) + |Z $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quote) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |'demo) + |r $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"eval:") + |X $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |eval) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quote) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618664995431) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618664985011) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618664985257) (:by |u0) (:text |2) - |yyr $ %{} :Expr (:at 1618673510188) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618673510809) (:by |u0) (:text |if) - |j $ %{} :Leaf (:at 1618673513600) (:by |u0) (:text |true) - |r $ %{} :Expr (:at 1618673514067) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |t $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |if) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |true) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618673514609) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618673517373) (:by |u0) (:text "|\"true") - |yyv $ %{} :Expr (:at 1618673510188) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"true") + |v $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618673510809) (:by |u0) (:text |if) - |j $ %{} :Leaf (:at 1618673522034) (:by |u0) (:text |false) - |r $ %{} :Expr (:at 1618673514067) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |if) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |false) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618673514609) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618673517373) (:by |u0) (:text "|\"true") - |v $ %{} :Expr (:at 1618673524977) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"true") + |Z $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618673525729) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618673526734) (:by |u0) (:text "|\"false") - |yyx $ %{} :Expr (:at 1618673529205) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"false") + |x $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618673529821) (:by |u0) (:text |if) - |j $ %{} :Expr (:at 1618673530125) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |if) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618673534134) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618673534565) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618673534799) (:by |u0) (:text |2) - |r $ %{} :Expr (:at 1618673537272) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618673536109) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618673538376) (:by |u0) (:text "|\"3") - |v $ %{} :Expr (:at 1618673540682) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"3") + |Z $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618673541276) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618673542363) (:by |u0) (:text "|\"?") - |yyy $ %{} :Expr (:at 1618674585688) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"?") + |y $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618674587642) (:by |u0) (:text |&let) - |j $ %{} :Expr (:at 1618674588361) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&let) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618674589371) (:by |u0) (:text |a) - |j $ %{} :Leaf (:at 1618674589618) (:by |u0) (:text |1) - |r $ %{} :Expr (:at 1618674591714) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |a) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618674592232) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618674596559) (:by |u0) (:text "|\"a is:") - |r $ %{} :Leaf (:at 1618674595408) (:by |u0) (:text |a) - |yyyT $ %{} :Expr (:at 1618674585688) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618674587642) (:by |u0) (:text |&let) - |f $ %{} :Leaf (:at 1618674603307) (:by |u0) (:text |nil) - |r $ %{} :Expr (:at 1618674591714) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"a is:") + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |a) + |z $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&let) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618674592232) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618674610267) (:by |u0) (:text "|\"a is none") - |yyyj $ %{} :Expr (:at 1618674611597) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"a is none") + |zV $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618674612756) (:by |u0) (:text |&let) - |j $ %{} :Expr (:at 1618674613267) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&let) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618674613637) (:by |u0) (:text |a) - |j $ %{} :Expr (:at 1618674615215) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |a) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618674617692) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618674618272) (:by |u0) (:text |3) - |r $ %{} :Leaf (:at 1618674618576) (:by |u0) (:text |4) - |r $ %{} :Expr (:at 1618674621227) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |4) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618674621967) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618674624057) (:by |u0) (:text "|\"a is:") - |r $ %{} :Leaf (:at 1618674624971) (:by |u0) (:text |a) - |yyyr $ %{} :Expr (:at 1618681700994) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"a is:") + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |a) + |zX $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618681701504) (:by |u0) (:text |println) - |j $ %{} :Expr (:at 1618681701785) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618681702755) (:by |u0) (:text |rest) - |j $ %{} :Expr (:at 1618681703369) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |rest) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618681704264) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618681704468) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618681704653) (:by |u0) (:text |2) - |v $ %{} :Leaf (:at 1618681705572) (:by |u0) (:text |3) - |x $ %{} :Leaf (:at 1618681705808) (:by |u0) (:text |4) - |yyyv $ %{} :Expr (:at 1618682122124) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618682122607) (:by |u0) (:text |println) - |j $ %{} :Expr (:at 1618682123605) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618682124422) (:by |u0) (:text |type-of) - |j $ %{} :Expr (:at 1618682124681) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |[]) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) + |b $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |4) + |zZ $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |type-of) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618682124941) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618682127480) (:by |u0) (:text |1) - |yyyx $ %{} :Expr (:at 1618682969714) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |[]) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |zb $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |D $ %{} :Leaf (:at 1618682971333) (:by |u0) (:text |println) - |L $ %{} :Leaf (:at 1618682973563) (:by |u0) (:text "|\"result:") - |T $ %{} :Expr (:at 1618682938708) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"result:") + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618682940605) (:by |u0) (:text |foldl) - |j $ %{} :Expr (:at 1618682942439) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |foldl) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618682942650) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618682944334) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618682944566) (:by |u0) (:text |2) - |v $ %{} :Leaf (:at 1618682944835) (:by |u0) (:text |3) - |x $ %{} :Leaf (:at 1618682945203) (:by |u0) (:text |4) - |r $ %{} :Leaf (:at 1618682947341) (:by |u0) (:text |0) - |v $ %{} :Expr (:at 1618682949689) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |[]) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) + |b $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |4) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |0) + |Z $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618682953315) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618682955242) (:by |u0) (:text |f1) - |r $ %{} :Expr (:at 1618682956170) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |defn) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |f1) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618682958260) (:by |u0) (:text |acc) - |j $ %{} :Leaf (:at 1618682958862) (:by |u0) (:text |x) - |t $ %{} :Expr (:at 1618682975336) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |acc) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |x) + |Z $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618682976544) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618682979610) (:by |u0) (:text "|\"adding:") - |n $ %{} :Leaf (:at 1618683016109) (:by |u0) (:text |acc) - |r $ %{} :Leaf (:at 1618682978465) (:by |u0) (:text |x) - |v $ %{} :Expr (:at 1618682960354) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"adding:") + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |acc) + |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |x) + |b $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618682965361) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618682962994) (:by |u0) (:text |acc) - |r $ %{} :Leaf (:at 1618682964049) (:by |u0) (:text |x) - |yyyy $ %{} :Expr (:at 1618720206313) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618720206820) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618720208707) (:by |u0) (:text "|\"macro:") - |r $ %{} :Expr (:at 1618720210191) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618720209139) (:by |u0) (:text |add-num) - |j $ %{} :Leaf (:at 1618720211273) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618720211522) (:by |u0) (:text |2) - |yyyyT $ %{} :Expr (:at 1618723113290) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723114194) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618723701346) (:by |u0) (:text "|\"sum:") - |r $ %{} :Expr (:at 1618723116484) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723121717) (:by |u0) (:text |rec-sum) - |j $ %{} :Leaf (:at 1618723122699) (:by |u0) (:text |0) - |r $ %{} :Expr (:at 1618723123028) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |acc) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |x) + |zd $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"macro:") + |X $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-num) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |zf $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"sum:") + |X $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |rec-sum) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |0) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618723123387) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618723124101) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618723124374) (:by |u0) (:text |2) - |v $ %{} :Leaf (:at 1618723124700) (:by |u0) (:text |3) - |x $ %{} :Leaf (:at 1618723125706) (:by |u0) (:text |4) - |yyyyb $ %{} :Expr (:at 1618729369263) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text "|\"expand-1:") - |r $ %{} :Expr (:at 1618729369263) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text |macroexpand-1) - |j $ %{} :Expr (:at 1618729369263) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |[]) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) + |b $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |4) + |zh $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"expand-1:") + |X $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |macroexpand-1) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text |quote) - |j $ %{} :Expr (:at 1618729369263) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quote) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text |add-num) - |j $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text |2) - |yyyyj $ %{} :Expr (:at 1618728236147) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618728236844) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618728240766) (:by |u0) (:text "|\"expand:") - |r $ %{} :Expr (:at 1618728241448) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-num) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |zj $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"expand:") + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618729257611) (:by |u0) (:text |macroexpand) - |j $ %{} :Expr (:at 1618728292870) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |macroexpand) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |D $ %{} :Leaf (:at 1618728293719) (:by |u0) (:text |quote) - |T $ %{} :Expr (:at 1618728247075) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quote) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618728250500) (:by |u0) (:text |add-num) - |j $ %{} :Leaf (:at 1618728250838) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618728251146) (:by |u0) (:text |2) - |yyyyr $ %{} :Expr (:at 1618728236147) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618728236844) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618728240766) (:by |u0) (:text "|\"expand:") - |r $ %{} :Expr (:at 1618769244761) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-num) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |zl $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"expand:") + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |D $ %{} :Leaf (:at 1618769245430) (:by |u0) (:text |format-to-lisp) - |T $ %{} :Expr (:at 1618728241448) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |format-to-lisp) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618729257611) (:by |u0) (:text |macroexpand) - |j $ %{} :Expr (:at 1618728292870) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |macroexpand) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |D $ %{} :Leaf (:at 1618728293719) (:by |u0) (:text |quote) - |T $ %{} :Expr (:at 1618728247075) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quote) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618730300485) (:by |u0) (:text |add-more) - |b $ %{} :Leaf (:at 1618730406639) (:by |u0) (:text |0) - |j $ %{} :Leaf (:at 1618730347804) (:by |u0) (:text |3) - |r $ %{} :Leaf (:at 1618730348853) (:by |u0) (:text |8) - |yyyyv $ %{} :Expr (:at 1618728236147) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618728236844) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618730586955) (:by |u0) (:text "|\"expand v:") - |r $ %{} :Expr (:at 1618730585215) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730585215) (:by |u0) (:text |add-more) - |j $ %{} :Leaf (:at 1618730585215) (:by |u0) (:text |0) - |r $ %{} :Leaf (:at 1618730585215) (:by |u0) (:text |3) - |v $ %{} :Leaf (:at 1618730585215) (:by |u0) (:text |8) - |yyyyx $ %{} :Expr (:at 1618740378070) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740378663) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618740385798) (:by |u0) (:text "|\"call and call") - |r $ %{} :Expr (:at 1618740386339) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740386840) (:by |u0) (:text |add-by-2) - |j $ %{} :Leaf (:at 1618740388181) (:by |u0) (:text |10) - |yyyyy $ %{} :Expr (:at 1618770028090) (:by |u0) - :data $ {} - |5 $ %{} :Leaf (:at 1618772534094) (:by |u0) (:text |;) - |D $ %{} :Leaf (:at 1618770030105) (:by |u0) (:text |println) - |T $ %{} :Expr (:at 1618770031138) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618770034555) (:by |u0) (:text |macroexpand) - |T $ %{} :Expr (:at 1618752131764) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-more) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |0) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) + |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |8) + |zn $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"expand v:") + |X $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-more) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |0) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) + |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |8) + |zp $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"call and call") + |X $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-by-2) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |10) + |zr $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |;) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) + :data $ {} + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |macroexpand) + |V $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618752133902) (:by |u0) (:text |assert=) - |j $ %{} :Leaf (:at 1618752134923) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618752135294) (:by |u0) (:text |2) - |yyyyyT $ %{} :Expr (:at 1618767923138) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |assert=) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) + |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) + |zt $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618767932151) (:by |u0) (:text |test-args) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |test-args) + :examples $ [] |f1 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618658477086) (:by |u0) + :code $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618658477086) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618658480301) (:by |u0) (:text |f1) - |r $ %{} :Expr (:at 1618658477086) (:by |u0) + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |defn) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |f1) + |X $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |v $ %{} :Expr (:at 1618658483325) (:by |u0) + |Z $ %{} :Expr (:at 1773136412558) (:by |sync) :data $ {} - |T $ %{} :Leaf (:at 1618658484688) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618658487989) (:by |u0) (:text "|\"calling f1") + |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) + |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "||Hello with leaf!") + :examples $ [] |fib $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1619930459257) (:by |u0) :data $ {} @@ -611,6 +613,7 @@ |T $ %{} :Leaf (:at 1619930475429) (:by |u0) (:text |-) |j $ %{} :Leaf (:at 1619930476120) (:by |u0) (:text |n) |r $ %{} :Leaf (:at 1619930481371) (:by |u0) (:text |2) + :examples $ [] |main! $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1619930570377) (:by |u0) :data $ {} @@ -633,6 +636,7 @@ :data $ {} |D $ %{} :Leaf (:at 1633873455342) (:by |u0) (:text |;) |T $ %{} :Leaf (:at 1633872991931) (:by |u0) (:text |show-data) + :examples $ [] |rec-sum $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1618723127970) (:by |u0) :data $ {} @@ -666,6 +670,7 @@ :data $ {} |T $ %{} :Leaf (:at 1618723165126) (:by |u0) (:text |rest) |j $ %{} :Leaf (:at 1618723165879) (:by |u0) (:text |xs) + :examples $ [] |reload! $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1619207810174) (:by |u0) :data $ {} @@ -685,6 +690,7 @@ |y $ %{} :Expr (:at 1622292799913) (:by |u0) :data $ {} |T $ %{} :Leaf (:at 1622292800206) (:by |u0) (:text |try-method) + :examples $ [] |show-data $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1633872992647) (:by |u0) :data $ {} @@ -716,6 +722,7 @@ :data $ {} |T $ %{} :Leaf (:at 1633873012008) (:by |u0) (:text |:a) |j $ %{} :Leaf (:at 1633873013762) (:by |u0) (:text |1) + :examples $ [] |test-args $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1618767933203) (:by |u0) :data $ {} @@ -760,6 +767,7 @@ |j $ %{} :Leaf (:at 1618769762350) (:by |u0) (:text |11) |r $ %{} :Leaf (:at 1618769837129) (:by |u0) (:text |12) |v $ %{} :Leaf (:at 1618769849272) (:by |u0) (:text |13) + :examples $ [] |try-method $ %{} :CodeEntry (:doc |) :code $ %{} :Expr (:at 1622292801677) (:by |u0) :data $ {} @@ -777,7 +785,8 @@ :data $ {} |T $ %{} :Leaf (:at 1622292811398) (:by |u0) (:text |range) |j $ %{} :Leaf (:at 1622292816464) (:by |u0) (:text |11) - :ns $ %{} :CodeEntry (:doc |) + :examples $ [] + :ns $ %{} :NsEntry (:doc |) :code $ %{} :Expr (:at 1618539507433) (:by |u0) :data $ {} |T $ %{} :Leaf (:at 1618539507433) (:by |u0) (:text |ns) @@ -807,814 +816,5 @@ |T $ %{} :Leaf (:at 1618720201399) (:by |u0) (:text |[]) |j $ %{} :Leaf (:at 1618720203059) (:by |u0) (:text |add-num) |r $ %{} :Leaf (:at 1618740371002) (:by |u0) (:text |add-by-2) - :ir $ {} (:package |app) - :files $ {} - |app.lib $ {} - :configs $ {} - :defs $ {} - |f2 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618661020393) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661020393) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618661020393) (:by |u0) (:text |f2) - |r $ %{} :Expr (:at 1618661020393) (:by |u0) - :data $ {} - |v $ %{} :Expr (:at 1618661022794) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661024070) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618661026271) (:by |u0) (:text "|\"f2 in lib") - |f3 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618661052591) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661052591) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618661052591) (:by |u0) (:text |f3) - |r $ %{} :Expr (:at 1618661052591) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661067908) (:by |u0) (:text |x) - |v $ %{} :Expr (:at 1618661054823) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661055379) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618661061473) (:by |u0) (:text "|\"f3 in lib") - |x $ %{} :Expr (:at 1618661070479) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661071077) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618661073107) (:by |u0) (:text "|\"v:") - |r $ %{} :Leaf (:at 1618661074709) (:by |u0) (:text |x) - :ns $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618661017191) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661017191) (:by |u0) (:text |ns) - |j $ %{} :Leaf (:at 1618661017191) (:by |u0) (:text |app.lib) - |app.macro $ {} - :configs $ {} - :defs $ {} - |add-by-1 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618740276250) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740281235) (:by |u0) (:text |defmacro) - |j $ %{} :Leaf (:at 1618740276250) (:by |u0) (:text |add-by-1) - |r $ %{} :Expr (:at 1618740276250) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740282976) (:by |u0) (:text |x) - |v $ %{} :Expr (:at 1618740303995) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618740308945) (:by |u0) (:text |quasiquote) - |T $ %{} :Expr (:at 1618740285475) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740286902) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618740317157) (:by |u0) (:text |~x) - |r $ %{} :Leaf (:at 1618740287700) (:by |u0) (:text |1) - |add-by-2 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618740293087) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740296031) (:by |u0) (:text |defmacro) - |j $ %{} :Leaf (:at 1618740293087) (:by |u0) (:text |add-by-2) - |r $ %{} :Expr (:at 1618740293087) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740299129) (:by |u0) (:text |x) - |v $ %{} :Expr (:at 1618740300016) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740325280) (:by |u0) (:text |quasiquote) - |j $ %{} :Expr (:at 1618740327115) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740331009) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618740354540) (:by |u0) (:text |2) - |r $ %{} :Expr (:at 1618740340237) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740343769) (:by |u0) (:text |add-by-1) - |j $ %{} :Leaf (:at 1618740351578) (:by |u0) (:text |~x) - |add-num $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618663286974) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618663289791) (:by |u0) (:text |defmacro) - |j $ %{} :Leaf (:at 1618663286974) (:by |u0) (:text |add-num) - |r $ %{} :Expr (:at 1618663286974) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618663291903) (:by |u0) (:text |a) - |j $ %{} :Leaf (:at 1618663292537) (:by |u0) (:text |b) - |v $ %{} :Expr (:at 1618663324823) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618663328933) (:by |u0) (:text |quasiquote) - |T $ %{} :Expr (:at 1618663294505) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618663307918) (:by |u0) (:text |&let) - |j $ %{} :Leaf (:at 1618663305807) (:by |u0) (:text |nil) - |r $ %{} :Expr (:at 1618663312809) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618663314951) (:by |u0) (:text |&+) - |j $ %{} :Expr (:at 1618663331895) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618663333114) (:by |u0) (:text |~) - |T $ %{} :Leaf (:at 1618663316680) (:by |u0) (:text |a) - |r $ %{} :Expr (:at 1618663335086) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618663336609) (:by |u0) (:text |~) - |T $ %{} :Leaf (:at 1618663317648) (:by |u0) (:text |b) - :ns $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618663277036) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618663277036) (:by |u0) (:text |ns) - |j $ %{} :Leaf (:at 1618663277036) (:by |u0) (:text |app.macro) - |app.main $ {} - :configs $ {} - :defs $ {} - |add-more $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618730350902) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730354052) (:by |u0) (:text |defmacro) - |j $ %{} :Leaf (:at 1618730350902) (:by |u0) (:text |add-more) - |r $ %{} :Expr (:at 1618730350902) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618730403604) (:by |u0) (:text |acc) - |T $ %{} :Leaf (:at 1618730358202) (:by |u0) (:text |x) - |j $ %{} :Leaf (:at 1618730359828) (:by |u0) (:text |times) - |v $ %{} :Expr (:at 1618730361081) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730362447) (:by |u0) (:text |if) - |j $ %{} :Expr (:at 1618730365650) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730370296) (:by |u0) (:text |&<) - |b $ %{} :Leaf (:at 1618730372435) (:by |u0) (:text |times) - |j $ %{} :Leaf (:at 1618730539709) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618730533225) (:by |u0) (:text |acc) - |v $ %{} :Expr (:at 1618730378436) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730381681) (:by |u0) (:text |recur) - |j $ %{} :Expr (:at 1618730466064) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618730500531) (:by |u0) (:text |quasiquote) - |T $ %{} :Expr (:at 1618730386375) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618730388781) (:by |u0) (:text |&+) - |T $ %{} :Expr (:at 1618730485628) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618730486770) (:by |u0) (:text |~) - |T $ %{} :Leaf (:at 1618730383299) (:by |u0) (:text |x) - |j $ %{} :Expr (:at 1618730488250) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618730489428) (:by |u0) (:text |~) - |T $ %{} :Leaf (:at 1618730412605) (:by |u0) (:text |acc) - |n $ %{} :Leaf (:at 1618730516278) (:by |u0) (:text |x) - |r $ %{} :Expr (:at 1618730434451) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730435581) (:by |u0) (:text |&-) - |j $ %{} :Leaf (:at 1618730436881) (:by |u0) (:text |times) - |r $ %{} :Leaf (:at 1618730437157) (:by |u0) (:text |1) - |call-3 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618767957921) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767957921) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618767957921) (:by |u0) (:text |call-3) - |r $ %{} :Expr (:at 1618767957921) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767960551) (:by |u0) (:text |a) - |j $ %{} :Leaf (:at 1618767961787) (:by |u0) (:text |b) - |r $ %{} :Leaf (:at 1618767962162) (:by |u0) (:text |c) - |v $ %{} :Expr (:at 1618767962704) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767963282) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618767965367) (:by |u0) (:text "|\"a is:") - |r $ %{} :Leaf (:at 1618767965784) (:by |u0) (:text |a) - |x $ %{} :Expr (:at 1618767962704) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767963282) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618767969236) (:by |u0) (:text "|\"b is:") - |r $ %{} :Leaf (:at 1618767970341) (:by |u0) (:text |b) - |y $ %{} :Expr (:at 1618767962704) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767963282) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618767977407) (:by |u0) (:text "|\"c is:") - |r $ %{} :Leaf (:at 1618767973639) (:by |u0) (:text |c) - |call-macro $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618769676627) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769678801) (:by |u0) (:text |defmacro) - |j $ %{} :Leaf (:at 1618769676627) (:by |u0) (:text |call-macro) - |r $ %{} :Expr (:at 1618769676627) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769685522) (:by |u0) (:text |x0) - |j $ %{} :Leaf (:at 1618769686283) (:by |u0) (:text |&) - |r $ %{} :Leaf (:at 1618769686616) (:by |u0) (:text |xs) - |v $ %{} :Expr (:at 1618769687244) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769697898) (:by |u0) (:text |quasiquote) - |j $ %{} :Expr (:at 1618769717127) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769719548) (:by |u0) (:text |&{}) - |j $ %{} :Leaf (:at 1618769720509) (:by |u0) (:text |:a) - |n $ %{} :Expr (:at 1618769729161) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618769730971) (:by |u0) (:text |~) - |T $ %{} :Leaf (:at 1618769722734) (:by |u0) (:text |x0) - |r $ %{} :Leaf (:at 1618769723765) (:by |u0) (:text |:b) - |v $ %{} :Expr (:at 1618769809158) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618769809634) (:by |u0) (:text |[]) - |T $ %{} :Expr (:at 1618769725387) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618769865395) (:by |u0) (:text |~@) - |T $ %{} :Leaf (:at 1618769725113) (:by |u0) (:text |xs) - |call-many $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618769509051) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769509051) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618769509051) (:by |u0) (:text |call-many) - |r $ %{} :Expr (:at 1618769509051) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769511818) (:by |u0) (:text |x0) - |j $ %{} :Leaf (:at 1618769513121) (:by |u0) (:text |&) - |r $ %{} :Leaf (:at 1618769517543) (:by |u0) (:text |xs) - |t $ %{} :Expr (:at 1618769532837) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769533874) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618769535535) (:by |u0) (:text "|\"many...") - |v $ %{} :Expr (:at 1618769518829) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769519471) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618769522352) (:by |u0) (:text "|\"x0") - |r $ %{} :Leaf (:at 1618769523977) (:by |u0) (:text |x0) - |x $ %{} :Expr (:at 1618769524533) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769525175) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618769525982) (:by |u0) (:text "|\"xs") - |r $ %{} :Leaf (:at 1618769526896) (:by |u0) (:text |xs) - |demos $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618539520156) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618539520156) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1619930563832) (:by |u0) (:text |demos) - |r $ %{} :Expr (:at 1618539520156) (:by |u0) - :data $ {} - |v $ %{} :Expr (:at 1618539523268) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618539524965) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618539525898) (:by |u0) (:text "|\"demo") - |x $ %{} :Expr (:at 1618646117925) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618646119371) (:by |u0) (:text |println) - |j $ %{} :Expr (:at 1618646119955) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618646122999) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618658555366) (:by |u0) (:text |2) - |r $ %{} :Leaf (:at 1618646121081) (:by |u0) (:text |2) - |y $ %{} :Expr (:at 1618658517774) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618658519944) (:by |u0) (:text |println) - |L $ %{} :Leaf (:at 1618658520784) (:by |u0) (:text "|\"f1") - |T $ %{} :Expr (:at 1618658494170) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618658495406) (:by |u0) (:text |f1) - |yT $ %{} :Expr (:at 1618659585738) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633952520593) (:by |u0) (:text |print-values) - |j $ %{} :Leaf (:at 1618659590535) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618659591512) (:by |u0) (:text "|\"1") - |v $ %{} :Leaf (:at 1618659595541) (:by |u0) (:text |:a) - |x $ %{} :Expr (:at 1618659596691) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618659596880) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618659597668) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618659597892) (:by |u0) (:text |2) - |yj $ %{} :Expr (:at 1618660536373) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618660537901) (:by |u0) (:text |println) - |j $ %{} :Expr (:at 1618660538186) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618660568253) (:by |u0) (:text |&{}) - |j $ %{} :Leaf (:at 1618660541656) (:by |u0) (:text |:a) - |r $ %{} :Leaf (:at 1618660542971) (:by |u0) (:text |1) - |v $ %{} :Leaf (:at 1618660543782) (:by |u0) (:text |:b) - |x $ %{} :Leaf (:at 1618660544981) (:by |u0) (:text |2) - |yr $ %{} :Expr (:at 1618660963223) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618660963956) (:by |u0) (:text |println) - |j $ %{} :Expr (:at 1618660964279) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618660965160) (:by |u0) (:text |#{}) - |j $ %{} :Leaf (:at 1618660965550) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618660965773) (:by |u0) (:text |2) - |v $ %{} :Leaf (:at 1618660966299) (:by |u0) (:text |3) - |x $ %{} :Leaf (:at 1618660970012) (:by |u0) (:text ||four) - |yx $ %{} :Expr (:at 1618661082170) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661298818) (:by |u0) (:text |lib/f2) - |yy $ %{} :Expr (:at 1618661300982) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661302264) (:by |u0) (:text |f3) - |j $ %{} :Leaf (:at 1618661308107) (:by |u0) (:text "|\"arg of 3") - |yyT $ %{} :Expr (:at 1618664966181) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618664966725) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618664980683) (:by |u0) (:text "|\"quote:") - |r $ %{} :Expr (:at 1618664968766) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618664969526) (:by |u0) (:text |quote) - |j $ %{} :Expr (:at 1618664969796) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618665001007) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618664970588) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618664970840) (:by |u0) (:text |2) - |yyb $ %{} :Expr (:at 1618665182369) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618665182898) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618665185901) (:by |u0) (:text "|\"quo:") - |r $ %{} :Leaf (:at 1618665190172) (:by |u0) (:text |'demo) - |v $ %{} :Expr (:at 1618665201691) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618665202393) (:by |u0) (:text |quote) - |j $ %{} :Leaf (:at 1618665203149) (:by |u0) (:text |'demo) - |yyj $ %{} :Expr (:at 1618664972310) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618664972897) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618664978986) (:by |u0) (:text "|\"eval:") - |r $ %{} :Expr (:at 1618664981960) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618664982687) (:by |u0) (:text |eval) - |j $ %{} :Expr (:at 1618664983058) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618664984086) (:by |u0) (:text |quote) - |j $ %{} :Expr (:at 1618664984358) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618664995431) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618664985011) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618664985257) (:by |u0) (:text |2) - |yyr $ %{} :Expr (:at 1618673510188) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618673510809) (:by |u0) (:text |if) - |j $ %{} :Leaf (:at 1618673513600) (:by |u0) (:text |true) - |r $ %{} :Expr (:at 1618673514067) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618673514609) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618673517373) (:by |u0) (:text "|\"true") - |yyv $ %{} :Expr (:at 1618673510188) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618673510809) (:by |u0) (:text |if) - |j $ %{} :Leaf (:at 1618673522034) (:by |u0) (:text |false) - |r $ %{} :Expr (:at 1618673514067) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618673514609) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618673517373) (:by |u0) (:text "|\"true") - |v $ %{} :Expr (:at 1618673524977) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618673525729) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618673526734) (:by |u0) (:text "|\"false") - |yyx $ %{} :Expr (:at 1618673529205) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618673529821) (:by |u0) (:text |if) - |j $ %{} :Expr (:at 1618673530125) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618673534134) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618673534565) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618673534799) (:by |u0) (:text |2) - |r $ %{} :Expr (:at 1618673537272) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618673536109) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618673538376) (:by |u0) (:text "|\"3") - |v $ %{} :Expr (:at 1618673540682) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618673541276) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618673542363) (:by |u0) (:text "|\"?") - |yyy $ %{} :Expr (:at 1618674585688) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618674587642) (:by |u0) (:text |&let) - |j $ %{} :Expr (:at 1618674588361) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618674589371) (:by |u0) (:text |a) - |j $ %{} :Leaf (:at 1618674589618) (:by |u0) (:text |1) - |r $ %{} :Expr (:at 1618674591714) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618674592232) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618674596559) (:by |u0) (:text "|\"a is:") - |r $ %{} :Leaf (:at 1618674595408) (:by |u0) (:text |a) - |yyyT $ %{} :Expr (:at 1618674585688) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618674587642) (:by |u0) (:text |&let) - |f $ %{} :Leaf (:at 1618674603307) (:by |u0) (:text |nil) - |r $ %{} :Expr (:at 1618674591714) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618674592232) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618674610267) (:by |u0) (:text "|\"a is none") - |yyyj $ %{} :Expr (:at 1618674611597) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618674612756) (:by |u0) (:text |&let) - |j $ %{} :Expr (:at 1618674613267) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618674613637) (:by |u0) (:text |a) - |j $ %{} :Expr (:at 1618674615215) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618674617692) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618674618272) (:by |u0) (:text |3) - |r $ %{} :Leaf (:at 1618674618576) (:by |u0) (:text |4) - |r $ %{} :Expr (:at 1618674621227) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618674621967) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618674624057) (:by |u0) (:text "|\"a is:") - |r $ %{} :Leaf (:at 1618674624971) (:by |u0) (:text |a) - |yyyr $ %{} :Expr (:at 1618681700994) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618681701504) (:by |u0) (:text |println) - |j $ %{} :Expr (:at 1618681701785) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618681702755) (:by |u0) (:text |rest) - |j $ %{} :Expr (:at 1618681703369) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618681704264) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618681704468) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618681704653) (:by |u0) (:text |2) - |v $ %{} :Leaf (:at 1618681705572) (:by |u0) (:text |3) - |x $ %{} :Leaf (:at 1618681705808) (:by |u0) (:text |4) - |yyyv $ %{} :Expr (:at 1618682122124) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618682122607) (:by |u0) (:text |println) - |j $ %{} :Expr (:at 1618682123605) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618682124422) (:by |u0) (:text |type-of) - |j $ %{} :Expr (:at 1618682124681) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618682124941) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618682127480) (:by |u0) (:text |1) - |yyyx $ %{} :Expr (:at 1618682969714) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618682971333) (:by |u0) (:text |println) - |L $ %{} :Leaf (:at 1618682973563) (:by |u0) (:text "|\"result:") - |T $ %{} :Expr (:at 1618682938708) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618682940605) (:by |u0) (:text |foldl) - |j $ %{} :Expr (:at 1618682942439) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618682942650) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618682944334) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618682944566) (:by |u0) (:text |2) - |v $ %{} :Leaf (:at 1618682944835) (:by |u0) (:text |3) - |x $ %{} :Leaf (:at 1618682945203) (:by |u0) (:text |4) - |r $ %{} :Leaf (:at 1618682947341) (:by |u0) (:text |0) - |v $ %{} :Expr (:at 1618682949689) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618682953315) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618682955242) (:by |u0) (:text |f1) - |r $ %{} :Expr (:at 1618682956170) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618682958260) (:by |u0) (:text |acc) - |j $ %{} :Leaf (:at 1618682958862) (:by |u0) (:text |x) - |t $ %{} :Expr (:at 1618682975336) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618682976544) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618682979610) (:by |u0) (:text "|\"adding:") - |n $ %{} :Leaf (:at 1618683016109) (:by |u0) (:text |acc) - |r $ %{} :Leaf (:at 1618682978465) (:by |u0) (:text |x) - |v $ %{} :Expr (:at 1618682960354) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618682965361) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618682962994) (:by |u0) (:text |acc) - |r $ %{} :Leaf (:at 1618682964049) (:by |u0) (:text |x) - |yyyy $ %{} :Expr (:at 1618720206313) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618720206820) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618720208707) (:by |u0) (:text "|\"macro:") - |r $ %{} :Expr (:at 1618720210191) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618720209139) (:by |u0) (:text |add-num) - |j $ %{} :Leaf (:at 1618720211273) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618720211522) (:by |u0) (:text |2) - |yyyyT $ %{} :Expr (:at 1618723113290) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723114194) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618723701346) (:by |u0) (:text "|\"sum:") - |r $ %{} :Expr (:at 1618723116484) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723121717) (:by |u0) (:text |rec-sum) - |j $ %{} :Leaf (:at 1618723122699) (:by |u0) (:text |0) - |r $ %{} :Expr (:at 1618723123028) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723123387) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618723124101) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618723124374) (:by |u0) (:text |2) - |v $ %{} :Leaf (:at 1618723124700) (:by |u0) (:text |3) - |x $ %{} :Leaf (:at 1618723125706) (:by |u0) (:text |4) - |yyyyb $ %{} :Expr (:at 1618729369263) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text "|\"expand-1:") - |r $ %{} :Expr (:at 1618729369263) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text |macroexpand-1) - |j $ %{} :Expr (:at 1618729369263) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text |quote) - |j $ %{} :Expr (:at 1618729369263) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text |add-num) - |j $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618729369263) (:by |u0) (:text |2) - |yyyyj $ %{} :Expr (:at 1618728236147) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618728236844) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618728240766) (:by |u0) (:text "|\"expand:") - |r $ %{} :Expr (:at 1618728241448) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618729257611) (:by |u0) (:text |macroexpand) - |j $ %{} :Expr (:at 1618728292870) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618728293719) (:by |u0) (:text |quote) - |T $ %{} :Expr (:at 1618728247075) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618728250500) (:by |u0) (:text |add-num) - |j $ %{} :Leaf (:at 1618728250838) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618728251146) (:by |u0) (:text |2) - |yyyyr $ %{} :Expr (:at 1618728236147) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618728236844) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618728240766) (:by |u0) (:text "|\"expand:") - |r $ %{} :Expr (:at 1618769244761) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618769245430) (:by |u0) (:text |format-to-lisp) - |T $ %{} :Expr (:at 1618728241448) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618729257611) (:by |u0) (:text |macroexpand) - |j $ %{} :Expr (:at 1618728292870) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618728293719) (:by |u0) (:text |quote) - |T $ %{} :Expr (:at 1618728247075) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730300485) (:by |u0) (:text |add-more) - |b $ %{} :Leaf (:at 1618730406639) (:by |u0) (:text |0) - |j $ %{} :Leaf (:at 1618730347804) (:by |u0) (:text |3) - |r $ %{} :Leaf (:at 1618730348853) (:by |u0) (:text |8) - |yyyyv $ %{} :Expr (:at 1618728236147) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618728236844) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618730586955) (:by |u0) (:text "|\"expand v:") - |r $ %{} :Expr (:at 1618730585215) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730585215) (:by |u0) (:text |add-more) - |j $ %{} :Leaf (:at 1618730585215) (:by |u0) (:text |0) - |r $ %{} :Leaf (:at 1618730585215) (:by |u0) (:text |3) - |v $ %{} :Leaf (:at 1618730585215) (:by |u0) (:text |8) - |yyyyx $ %{} :Expr (:at 1618740378070) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740378663) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618740385798) (:by |u0) (:text "|\"call and call") - |r $ %{} :Expr (:at 1618740386339) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740386840) (:by |u0) (:text |add-by-2) - |j $ %{} :Leaf (:at 1618740388181) (:by |u0) (:text |10) - |yyyyy $ %{} :Expr (:at 1618770028090) (:by |u0) - :data $ {} - |5 $ %{} :Leaf (:at 1618772534094) (:by |u0) (:text |;) - |D $ %{} :Leaf (:at 1618770030105) (:by |u0) (:text |println) - |T $ %{} :Expr (:at 1618770031138) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618770034555) (:by |u0) (:text |macroexpand) - |T $ %{} :Expr (:at 1618752131764) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618752133902) (:by |u0) (:text |assert=) - |j $ %{} :Leaf (:at 1618752134923) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618752135294) (:by |u0) (:text |2) - |yyyyyT $ %{} :Expr (:at 1618767923138) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767932151) (:by |u0) (:text |test-args) - |f1 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618658477086) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618658477086) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618658480301) (:by |u0) (:text |f1) - |r $ %{} :Expr (:at 1618658477086) (:by |u0) - :data $ {} - |v $ %{} :Expr (:at 1618658483325) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618658484688) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618658487989) (:by |u0) (:text "|\"calling f1") - |fib $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1619930459257) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930459257) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1619930459257) (:by |u0) (:text |fib) - |r $ %{} :Expr (:at 1619930459257) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930460888) (:by |u0) (:text |n) - |v $ %{} :Expr (:at 1619930461450) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930461900) (:by |u0) (:text |if) - |j $ %{} :Expr (:at 1619930462153) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930465800) (:by |u0) (:text |<) - |j $ %{} :Leaf (:at 1619930466571) (:by |u0) (:text |n) - |r $ %{} :Leaf (:at 1619930467516) (:by |u0) (:text |2) - |p $ %{} :Leaf (:at 1619976301564) (:by |u0) (:text |1) - |v $ %{} :Expr (:at 1619930469154) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930469867) (:by |u0) (:text |+) - |j $ %{} :Expr (:at 1619930471373) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930473045) (:by |u0) (:text |fib) - |j $ %{} :Expr (:at 1619930473244) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930475429) (:by |u0) (:text |-) - |j $ %{} :Leaf (:at 1619930476120) (:by |u0) (:text |n) - |r $ %{} :Leaf (:at 1619930476518) (:by |u0) (:text |1) - |r $ %{} :Expr (:at 1619930471373) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930473045) (:by |u0) (:text |fib) - |j $ %{} :Expr (:at 1619930473244) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930475429) (:by |u0) (:text |-) - |j $ %{} :Leaf (:at 1619930476120) (:by |u0) (:text |n) - |r $ %{} :Leaf (:at 1619930481371) (:by |u0) (:text |2) - |main! $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1619930570377) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930570377) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1619930570377) (:by |u0) (:text |main!) - |r $ %{} :Expr (:at 1619930570377) (:by |u0) - :data $ {} - |v $ %{} :Expr (:at 1619930574797) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930577305) (:by |u0) (:text |demos) - |y $ %{} :Expr (:at 1619930582609) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1622292794753) (:by |u0) (:text |;) - |T $ %{} :Leaf (:at 1619930582609) (:by |u0) (:text |fib) - |j $ %{} :Leaf (:at 1619930582609) (:by |u0) (:text |10) - |yT $ %{} :Expr (:at 1622292783688) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1622292787836) (:by |u0) (:text |try-method) - |yj $ %{} :Expr (:at 1633872988484) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1633873455342) (:by |u0) (:text |;) - |T $ %{} :Leaf (:at 1633872991931) (:by |u0) (:text |show-data) - |rec-sum $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618723127970) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723127970) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618723127970) (:by |u0) (:text |rec-sum) - |r $ %{} :Expr (:at 1618723127970) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723129611) (:by |u0) (:text |acc) - |j $ %{} :Leaf (:at 1618723131566) (:by |u0) (:text |xs) - |v $ %{} :Expr (:at 1618723135708) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723136188) (:by |u0) (:text |if) - |j $ %{} :Expr (:at 1618723136714) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723138019) (:by |u0) (:text |empty?) - |j $ %{} :Leaf (:at 1618723146569) (:by |u0) (:text |xs) - |r $ %{} :Leaf (:at 1618723147576) (:by |u0) (:text |acc) - |v $ %{} :Expr (:at 1618723147929) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723151992) (:by |u0) (:text |recur) - |j $ %{} :Expr (:at 1618723153359) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723158533) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618723159204) (:by |u0) (:text |acc) - |r $ %{} :Expr (:at 1618723160405) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723268153) (:by |u0) (:text |nth) - |j $ %{} :Leaf (:at 1618723162178) (:by |u0) (:text |xs) - |r $ %{} :Leaf (:at 1618723268981) (:by |u0) (:text |0) - |r $ %{} :Expr (:at 1618723164698) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723165126) (:by |u0) (:text |rest) - |j $ %{} :Leaf (:at 1618723165879) (:by |u0) (:text |xs) - |reload! $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1619207810174) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619207810174) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1619207810174) (:by |u0) (:text |reload!) - |r $ %{} :Expr (:at 1619207810174) (:by |u0) - :data $ {} - |v $ %{} :Expr (:at 1619766026889) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619766027788) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1619766033570) (:by |u0) (:text "|\"reloaded 2") - |x $ %{} :Expr (:at 1619930543193) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1622292791514) (:by |u0) (:text |;) - |T $ %{} :Leaf (:at 1619930544016) (:by |u0) (:text |fib) - |j $ %{} :Leaf (:at 1619935071727) (:by |u0) (:text |40) - |y $ %{} :Expr (:at 1622292799913) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1622292800206) (:by |u0) (:text |try-method) - |show-data $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1633872992647) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633872992647) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1633872992647) (:by |u0) (:text |show-data) - |r $ %{} :Expr (:at 1633872992647) (:by |u0) - :data $ {} - |t $ %{} :Expr (:at 1633873024178) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633873031232) (:by |u0) (:text |load-console-formatter!) - |v $ %{} :Expr (:at 1633872993861) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633872996602) (:by |u0) (:text |js/console.log) - |j $ %{} :Expr (:at 1633872997079) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633873000863) (:by |u0) (:text |defrecord!) - |j $ %{} :Leaf (:at 1633873004188) (:by |u0) (:text |:Demo) - |r $ %{} :Expr (:at 1633873006952) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633873004646) (:by |u0) (:text |:a) - |j $ %{} :Leaf (:at 1633873007810) (:by |u0) (:text |1) - |v $ %{} :Expr (:at 1633873008937) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633873009838) (:by |u0) (:text |:b) - |j $ %{} :Expr (:at 1633873010851) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633873011411) (:by |u0) (:text |{}) - |j $ %{} :Expr (:at 1633873011697) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633873012008) (:by |u0) (:text |:a) - |j $ %{} :Leaf (:at 1633873013762) (:by |u0) (:text |1) - |test-args $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618767933203) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767933203) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618767933203) (:by |u0) (:text |test-args) - |r $ %{} :Expr (:at 1618767933203) (:by |u0) - :data $ {} - |v $ %{} :Expr (:at 1618767936819) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767946838) (:by |u0) (:text |call-3) - |b $ %{} :Leaf (:at 1618767951283) (:by |u0) (:text |&) - |j $ %{} :Expr (:at 1618767948145) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767948346) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618767949355) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618767949593) (:by |u0) (:text |2) - |v $ %{} :Leaf (:at 1618769480611) (:by |u0) (:text |3) - |x $ %{} :Expr (:at 1618769504303) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769507599) (:by |u0) (:text |call-many) - |j $ %{} :Leaf (:at 1618769530122) (:by |u0) (:text |1) - |y $ %{} :Expr (:at 1618769504303) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769507599) (:by |u0) (:text |call-many) - |b $ %{} :Leaf (:at 1618769543673) (:by |u0) (:text |1) - |j $ %{} :Leaf (:at 1618769540547) (:by |u0) (:text |2) - |yT $ %{} :Expr (:at 1618769504303) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769507599) (:by |u0) (:text |call-many) - |j $ %{} :Leaf (:at 1618769545875) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618769546500) (:by |u0) (:text |2) - |v $ %{} :Leaf (:at 1618769546751) (:by |u0) (:text |3) - |yj $ %{} :Expr (:at 1618769890713) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618769891472) (:by |u0) (:text |println) - |T $ %{} :Expr (:at 1618769885586) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618769888788) (:by |u0) (:text |macroexpand) - |T $ %{} :Expr (:at 1618769673535) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769675192) (:by |u0) (:text |call-macro) - |j $ %{} :Leaf (:at 1618769762350) (:by |u0) (:text |11) - |r $ %{} :Leaf (:at 1618769837129) (:by |u0) (:text |12) - |v $ %{} :Leaf (:at 1618769849272) (:by |u0) (:text |13) - |try-method $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1622292801677) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1622292802864) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1622292801677) (:by |u0) (:text |try-method) - |r $ %{} :Expr (:at 1622292801677) (:by |u0) - :data $ {} - |v $ %{} :Expr (:at 1622292803720) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1622292805545) (:by |u0) (:text |println) - |j $ %{} :Expr (:at 1622292805914) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1622292806869) (:by |u0) (:text |.count) - |j $ %{} :Expr (:at 1622292809130) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1622292811398) (:by |u0) (:text |range) - |j $ %{} :Leaf (:at 1622292816464) (:by |u0) (:text |11) - :ns $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618539507433) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618539507433) (:by |u0) (:text |ns) - |j $ %{} :Leaf (:at 1618539507433) (:by |u0) (:text |app.main) - |r $ %{} :Expr (:at 1618661030124) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661030826) (:by |u0) (:text |:require) - |j $ %{} :Expr (:at 1618661031081) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661035015) (:by |u0) (:text |app.lib) - |j $ %{} :Leaf (:at 1618661039398) (:by |u0) (:text |:as) - |r $ %{} :Leaf (:at 1618661040510) (:by |u0) (:text |lib) - |r $ %{} :Expr (:at 1618661042947) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661044709) (:by |u0) (:text |app.lib) - |j $ %{} :Leaf (:at 1618661045794) (:by |u0) (:text |:refer) - |r $ %{} :Expr (:at 1618661046024) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661046210) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618661047074) (:by |u0) (:text |f3) - |v $ %{} :Expr (:at 1618720195824) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618720199292) (:by |u0) (:text |app.macro) - |j $ %{} :Leaf (:at 1618720200969) (:by |u0) (:text |:refer) - |r $ %{} :Expr (:at 1618720201238) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618720201399) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618720203059) (:by |u0) (:text |add-num) - |r $ %{} :Leaf (:at 1618740371002) (:by |u0) (:text |add-by-2) :users $ {} |u0 $ {} (:avatar nil) (:id |u0) (:name |chen) (:nickname |chen) (:password |d41d8cd98f00b204e9800998ecf8427e) (:theme :star-trail) diff --git a/calcit/editor/compact.cirru b/calcit/editor/compact.cirru index 785f57e1..01b329b4 100644 --- a/calcit/editor/compact.cirru +++ b/calcit/editor/compact.cirru @@ -14,9 +14,8 @@ :code $ quote defn f3 (x) (println "\"f3 in lib") (println "\"v:" x) :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns app.lib) - :examples $ [] |app.macro $ %{} :FileEntry :defs $ {} |add-by-1 $ %{} :CodeEntry (:doc |) (:schema nil) @@ -35,9 +34,8 @@ quasiquote $ &let () &+ (~ a) (~ b) :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns app.macro) - :examples $ [] |app.main $ %{} :FileEntry :defs $ {} |add-more $ %{} :CodeEntry (:doc |) (:schema nil) @@ -145,9 +143,8 @@ defn try-method () $ println .count $ range 11 :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns app.main $ :require (app.lib :as lib) app.lib :refer $ [] f3 app.macro :refer $ [] add-num add-by-2 - :examples $ [] diff --git a/calcit/fibo.cirru b/calcit/fibo.cirru index bbc99581..6d13e70c 100644 --- a/calcit/fibo.cirru +++ b/calcit/fibo.cirru @@ -43,7 +43,6 @@ defn try-prime () $ println sieve-primes ([] 2 3 5 7 11 13) 17 400 :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns app.main $ :require - :examples $ [] diff --git a/calcit/test-algebra.cirru b/calcit/test-algebra.cirru index 9d356a5e..8fd58103 100644 --- a/calcit/test-algebra.cirru +++ b/calcit/test-algebra.cirru @@ -126,8 +126,7 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-algebra $ :require util.core :refer $ log-title inside-eval: - :examples $ [] diff --git a/calcit/test-cond.cirru b/calcit/test-cond.cirru index ec70522d..2f50e971 100644 --- a/calcit/test-cond.cirru +++ b/calcit/test-cond.cirru @@ -165,8 +165,7 @@ assert= 1 $ when-not false 1 assert= 1 $ when-not false 2 1 :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-cond.main $ :require [] util.core :refer $ [] inside-eval: - :examples $ [] diff --git a/calcit/test-doc-smoke.cirru b/calcit/test-doc-smoke.cirru index 1ebee708..816f8072 100644 --- a/calcit/test-doc-smoke.cirru +++ b/calcit/test-doc-smoke.cirru @@ -75,8 +75,7 @@ assert= DocTrait $ &impl:origin DotImpl assert= "|native-dot Bob" $ .label p :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-doc-smoke.main $ :require util.core :refer $ inside-eval: - :examples $ [] diff --git a/calcit/test-edn.cirru b/calcit/test-edn.cirru index 47b52562..53b48dc6 100644 --- a/calcit/test-edn.cirru +++ b/calcit/test-edn.cirru @@ -146,8 +146,7 @@ code $ quote (+ 1 2) assert= code $ eval (&data-to-code code) :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-edn.main $ :require [] util.core :refer $ [] inside-eval: - :examples $ [] diff --git a/calcit/test-enum.cirru b/calcit/test-enum.cirru index 41be535b..428d4896 100644 --- a/calcit/test-enum.cirru +++ b/calcit/test-enum.cirru @@ -77,6 +77,5 @@ :schema $ :: :fn {} (:return :unit) :args $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns test-enum.main) - :examples $ [] diff --git a/calcit/test-fn.cirru b/calcit/test-fn.cirru index 435962d1..81fb60de 100644 --- a/calcit/test-fn.cirru +++ b/calcit/test-fn.cirru @@ -23,8 +23,7 @@ assert= 3 $ f2 1 2 assert= 3 $ apply f2 ([] 1 2) :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-fn.main $ :require util.core :refer $ log-title - :examples $ [] diff --git a/calcit/test-generics.cirru b/calcit/test-generics.cirru index 23376005..75216b04 100644 --- a/calcit/test-generics.cirru +++ b/calcit/test-generics.cirru @@ -69,6 +69,5 @@ &inspect-type b &inspect-type h :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns test-generics.main) - :examples $ [] diff --git a/calcit/test-gynienic.cirru b/calcit/test-gynienic.cirru index 921c65b7..cb049967 100644 --- a/calcit/test-gynienic.cirru +++ b/calcit/test-gynienic.cirru @@ -22,9 +22,8 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] :dynamic - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (:ns test-gynienic.lib) - :examples $ [] |test-gynienic.main $ %{} :FileEntry :defs $ {} |main! $ %{} :CodeEntry (:doc |) (:schema nil) @@ -39,8 +38,7 @@ assert= (add-11 1 2) ([] 1 2 4 11 10) , true :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-gynienic.main $ :require [] test-gynienic.lib :refer $ [] add-11 - :examples $ [] diff --git a/calcit/test-hygienic.cirru b/calcit/test-hygienic.cirru index 22f734d4..97c711aa 100644 --- a/calcit/test-hygienic.cirru +++ b/calcit/test-hygienic.cirru @@ -34,9 +34,8 @@ :schema $ :: :fn {} (:return :number) :args $ [] :number - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (:ns test-hygienic.lib) - :examples $ [] |test-hygienic.main $ %{} :FileEntry :defs $ {} |main! $ %{} :CodeEntry (:doc |) @@ -58,8 +57,7 @@ :schema $ :: :fn {} (:return :bool) :args $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-hygienic.main $ :require test-hygienic.lib :refer $ add-11 add-11-safe - :examples $ [] diff --git a/calcit/test-invalid-tag.cirru b/calcit/test-invalid-tag.cirru index cdbddb76..56d38e0d 100644 --- a/calcit/test-invalid-tag.cirru +++ b/calcit/test-invalid-tag.cirru @@ -30,6 +30,5 @@ :code $ quote defn reload! () nil :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns test-invalid-tag.main) - :examples $ [] diff --git a/calcit/test-ir-type-info.cirru b/calcit/test-ir-type-info.cirru index 5cf29363..6ea4d585 100644 --- a/calcit/test-ir-type-info.cirru +++ b/calcit/test-ir-type-info.cirru @@ -44,6 +44,5 @@ println result println nested :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns app.main) - :examples $ [] diff --git a/calcit/test-js.cirru b/calcit/test-js.cirru index a7dca900..262022ee 100644 --- a/calcit/test-js.cirru +++ b/calcit/test-js.cirru @@ -258,7 +258,6 @@ assert= |a?b $ turn-string :a?b assert= |ab! $ turn-string :ab! :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-js.main $ :require (|os :as os) (|assert :as assert) - :examples $ [] diff --git a/calcit/test-lens.cirru b/calcit/test-lens.cirru index 87e8e5e5..83278001 100644 --- a/calcit/test-lens.cirru +++ b/calcit/test-lens.cirru @@ -113,7 +113,6 @@ :: :a :b $ [] 1 2 3 [] 2 2 :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-lens.main $ :require - :examples $ [] diff --git a/calcit/test-list.cirru b/calcit/test-list.cirru index 3bf18012..13e79be7 100644 --- a/calcit/test-list.cirru +++ b/calcit/test-list.cirru @@ -515,8 +515,7 @@ sort ([] 4 3 2 1) (\ &- % %2) [] 1 2 3 4 :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-list.main $ :require util.core :refer $ log-title inside-eval: - :examples $ [] diff --git a/calcit/test-macro.cirru b/calcit/test-macro.cirru index 3481e6d4..0e6f75b6 100644 --- a/calcit/test-macro.cirru +++ b/calcit/test-macro.cirru @@ -389,8 +389,7 @@ with-cpu-time $ &+ 1 2 , 3 :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-macro.main $ :require [] util.core :refer $ [] log-title inside-eval: - :examples $ [] diff --git a/calcit/test-map.cirru b/calcit/test-map.cirru index 4dd6df9e..c4118ff5 100644 --- a/calcit/test-map.cirru +++ b/calcit/test-map.cirru @@ -363,8 +363,7 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-map.main $ :require [] util.core :refer $ [] log-title inside-eval: inside-js: - :examples $ [] diff --git a/calcit/test-math.cirru b/calcit/test-math.cirru index e42c1f14..56eea93a 100644 --- a/calcit/test-math.cirru +++ b/calcit/test-math.cirru @@ -116,7 +116,6 @@ assert-detect empty? $ [] do true :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-math.main $ :require - :examples $ [] diff --git a/calcit/test-method-errors.cirru b/calcit/test-method-errors.cirru index ccfb5c88..86e56415 100644 --- a/calcit/test-method-errors.cirru +++ b/calcit/test-method-errors.cirru @@ -21,6 +21,5 @@ by-set $ .to-set (vals src) .map by-set $ fn (x) false :examples $ [] - :ns $ %{} :CodeEntry (:doc "|Namespace for standalone repro") (:schema nil) + :ns $ %{} :NsEntry (:doc "|Namespace for standalone repro") :code $ quote (ns test-method-errors.main) - :examples $ [] diff --git a/calcit/test-method-validation.cirru b/calcit/test-method-validation.cirru index 77bab1ba..d60d73a6 100644 --- a/calcit/test-method-validation.cirru +++ b/calcit/test-method-validation.cirru @@ -46,6 +46,5 @@ ; "合法的" list "方法" .first xs :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns app.main) - :examples $ [] diff --git a/calcit/test-nested-types.cirru b/calcit/test-nested-types.cirru index 8dcfd4c7..0f1a1fe1 100644 --- a/calcit/test-nested-types.cirru +++ b/calcit/test-nested-types.cirru @@ -34,6 +34,5 @@ ; "最终返回" "d,类型应该是" :number d :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns app.main) - :examples $ [] diff --git a/calcit/test-nil.cirru b/calcit/test-nil.cirru index cad2f034..df05ce13 100644 --- a/calcit/test-nil.cirru +++ b/calcit/test-nil.cirru @@ -14,8 +14,7 @@ assert= nil $ .map nil inc assert= nil $ .filter nil inc :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-nil.main $ :require util.core :refer $ log-title - :examples $ [] diff --git a/calcit/test-optimize.cirru b/calcit/test-optimize.cirru index 748faa71..e58c491f 100644 --- a/calcit/test-optimize.cirru +++ b/calcit/test-optimize.cirru @@ -57,7 +57,6 @@ assert-traits lp ShowTrait println $ .show lp :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-optimize.main $ :require - :examples $ [] diff --git a/calcit/test-proc-type-warnings.cirru b/calcit/test-proc-type-warnings.cirru index aeb15850..9d2080e4 100644 --- a/calcit/test-proc-type-warnings.cirru +++ b/calcit/test-proc-type-warnings.cirru @@ -26,6 +26,5 @@ println "|Testing type mismatch..." &+ text 10 :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns app.main) - :examples $ [] diff --git a/calcit/test-record.cirru b/calcit/test-record.cirru index f1f2ba4f..37a97420 100644 --- a/calcit/test-record.cirru +++ b/calcit/test-record.cirru @@ -254,8 +254,7 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-record.main $ :require util.core :refer $ log-title inside-js: - :examples $ [] diff --git a/calcit/test-recur-arity.cirru b/calcit/test-recur-arity.cirru index f525d246..7f0e8660 100644 --- a/calcit/test-recur-arity.cirru +++ b/calcit/test-recur-arity.cirru @@ -75,8 +75,7 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] :dynamic - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-recur-arity.main $ :require util.core :refer $ log-title - :examples $ [] diff --git a/calcit/test-recursion.cirru b/calcit/test-recursion.cirru index ce8287de..80e0843b 100644 --- a/calcit/test-recursion.cirru +++ b/calcit/test-recursion.cirru @@ -77,7 +77,6 @@ recur $ dec x assert= 6 @*count-effects :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-recursion.main $ :require - :examples $ [] diff --git a/calcit/test-set.cirru b/calcit/test-set.cirru index b73b5240..3465c925 100644 --- a/calcit/test-set.cirru +++ b/calcit/test-set.cirru @@ -124,7 +124,6 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-set.main $ :require - :examples $ [] diff --git a/calcit/test-string.cirru b/calcit/test-string.cirru index 41a039a1..ea4f3b98 100644 --- a/calcit/test-string.cirru +++ b/calcit/test-string.cirru @@ -231,8 +231,7 @@ assert-detect not $ blank? "| 1" assert-detect not $ blank? "|1 " :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-string.main $ :require [] util.core :refer $ [] inside-eval: - :examples $ [] diff --git a/calcit/test-sum-types.cirru b/calcit/test-sum-types.cirru index 9c2086e1..381e9a07 100644 --- a/calcit/test-sum-types.cirru +++ b/calcit/test-sum-types.cirru @@ -71,6 +71,5 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] :dynamic - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns test-sum-types.main) - :examples $ [] diff --git a/calcit/test-tag-match-validation.cirru b/calcit/test-tag-match-validation.cirru index c46f8c48..5a826989 100644 --- a/calcit/test-tag-match-validation.cirru +++ b/calcit/test-tag-match-validation.cirru @@ -81,6 +81,5 @@ do (println "| ✗ Unexpected error type") raise $ str "|Unexpected error:" e :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns test-tag-match-validation.main) - :examples $ [] diff --git a/calcit/test-traits.cirru b/calcit/test-traits.cirru index 228b795a..4cc75ed4 100644 --- a/calcit/test-traits.cirru +++ b/calcit/test-traits.cirru @@ -389,7 +389,6 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-traits.main $ :require - :examples $ [] diff --git a/calcit/test-tuple.cirru b/calcit/test-tuple.cirru index 67040d63..558f794d 100644 --- a/calcit/test-tuple.cirru +++ b/calcit/test-tuple.cirru @@ -89,8 +89,7 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] :dynamic - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns test-tuple.main $ :require util.core :refer $ log-title - :examples $ [] diff --git a/calcit/test-types-inference.cirru b/calcit/test-types-inference.cirru index e06d2d57..dd4ed4d0 100644 --- a/calcit/test-types-inference.cirru +++ b/calcit/test-types-inference.cirru @@ -145,6 +145,5 @@ &inspect-type wrapped &inspect-type outcome :examples $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns test-types-inference.main) - :examples $ [] diff --git a/calcit/test-types.cirru b/calcit/test-types.cirru index fd978242..5fe0659e 100644 --- a/calcit/test-types.cirru +++ b/calcit/test-types.cirru @@ -363,6 +363,5 @@ :schema $ :: :fn {} (:return :number) :args $ [] :number - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote (ns test-types.main) - :examples $ [] diff --git a/calcit/test.cirru b/calcit/test.cirru index c9b8c058..87a6292a 100644 --- a/calcit/test.cirru +++ b/calcit/test.cirru @@ -432,8 +432,7 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns app.main $ :require (test-cond.main :as test-cond) (test-hygienic.main :as test-hygienic) (test-lens.main :as test-lens) (test-list.main :as test-list) (test-macro.main :as test-macro) (test-map.main :as test-map) (test-math.main :as test-math) (test-recursion.main :as test-recursion) (test-set.main :as test-set) (test-string.main :as test-string) (test-edn.main :as test-edn) (test-js.main :as test-js) (test-record.main :as test-record) (test-nil.main :as test-nil) (test-fn.main :as test-fn) (test-tuple.main :as test-tuple) (test-algebra.main :as test-algebra) (test-types.main :as test-types) (test-types-inference.main :as test-types-inference) (test-enum.main :as test-enum) (test-generics.main :as test-generics) (test-traits.main :as test-traits) (test-doc-smoke.main :as test-doc-smoke) util.core :refer $ log-title inside-eval: inside-js: - :examples $ [] diff --git a/calcit/type-fail/schema-call-arg-type-mismatch.cirru b/calcit/type-fail/schema-call-arg-type-mismatch.cirru index 425856f2..c9491644 100644 --- a/calcit/type-fail/schema-call-arg-type-mismatch.cirru +++ b/calcit/type-fail/schema-call-arg-type-mismatch.cirru @@ -1,30 +1,30 @@ -{} (:about "|type-fail: schema-driven user function arg type checking") (:package |type-fail-schema-call-arg-type) +{} (:about "|file is generated - never edit directly; learn cr edit/tree workflows before changing") (:package |type-fail-schema-call-arg-type) :configs $ {} (:init-fn |type-fail-schema-call-arg-type.main/main!) (:reload-fn |type-fail-schema-call-arg-type.main/reload!) (:version |0.0.0) :modules $ [] :entries $ {} :files $ {} |type-fail-schema-call-arg-type.main $ %{} :FileEntry :defs $ {} - |plus1 $ %{} :CodeEntry (:doc "|Schema expects :number, call-site passes :string") - :code $ quote - defn plus1 (x) $ &+ x 1 - :examples $ [] - :schema $ :: :fn - {} (:return :number) - :args $ [] :number |main! $ %{} :CodeEntry (:doc "|Entry for type-fail schema call-site arg type mismatch") :code $ quote defn main! () $ let text |hello assert-type text :string - ; should generate warning (treated as error in --check-only) + ; should generate warning $ treated as error in --check-only plus1 text , nil :examples $ [] :schema $ :: :fn {} (:return :dynamic) :args $ [] + |plus1 $ %{} :CodeEntry (:doc "|Schema expects :number, call-site passes :string") + :code $ quote + defn plus1 (x) (&+ x 1) + :examples $ [] + :schema $ :: :fn + {} (:return :number) + :args $ [] :number |reload! $ %{} :CodeEntry (:doc "|Reload handler") :code $ quote defn reload! () nil @@ -32,6 +32,5 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] - :ns $ %{} :CodeEntry (:doc "|Namespace for schema call-site mismatch") (:schema nil) + :ns $ %{} :NsEntry (:doc "|Namespace for schema call-site mismatch") :code $ quote (ns type-fail-schema-call-arg-type.main) - :examples $ [] diff --git a/calcit/type-fail/schema-kind-mismatch.cirru b/calcit/type-fail/schema-kind-mismatch.cirru index 8776e0a8..941eb6e4 100644 --- a/calcit/type-fail/schema-kind-mismatch.cirru +++ b/calcit/type-fail/schema-kind-mismatch.cirru @@ -1,5 +1,5 @@ -{} (:about "|type-fail: schema :kind mismatch (schema says :macro, code is defn)") (:package |type-fail-schema-kind-mismatch) +{} (:about "|file is generated - never edit directly; learn cr edit/tree workflows before changing") (:package |type-fail-schema-kind-mismatch) :configs $ {} (:init-fn |type-fail-schema-kind-mismatch.main/main!) (:reload-fn |type-fail-schema-kind-mismatch.main/reload!) (:version |0.0.0) :modules $ [] :entries $ {} @@ -10,15 +10,11 @@ :code $ quote defn bad-kind () 1 :examples $ [] - :schema $ :: :fn - {} (:kind :macro) - :args $ [] + :schema $ :: :macro + {} $ :args ([]) |main! $ %{} :CodeEntry (:doc "|Entry for type-fail schema kind mismatch") :code $ quote - defn main! () $ do - ; call to force preprocessing of bad-kind - bad-kind - do true + defn main! () $ do (; call to force preprocessing of bad-kind) (bad-kind) (do true) :examples $ [] :schema $ :: :fn {} (:return :dynamic) @@ -30,6 +26,5 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] - :ns $ %{} :CodeEntry (:doc "|Namespace for schema kind mismatch") (:schema nil) + :ns $ %{} :NsEntry (:doc "|Namespace for schema kind mismatch") :code $ quote (ns type-fail-schema-kind-mismatch.main) - :examples $ [] \ No newline at end of file diff --git a/calcit/type-fail/schema-required-arity.cirru b/calcit/type-fail/schema-required-arity.cirru index 9b48b62c..40ca1a88 100644 --- a/calcit/type-fail/schema-required-arity.cirru +++ b/calcit/type-fail/schema-required-arity.cirru @@ -1,5 +1,5 @@ -{} (:about "|type-fail: schema required args arity mismatch") (:package |type-fail-schema-required-arity) +{} (:about "|file is generated - never edit directly; learn cr edit/tree workflows before changing") (:package |type-fail-schema-required-arity) :configs $ {} (:init-fn |type-fail-schema-required-arity.main/main!) (:reload-fn |type-fail-schema-required-arity.main/reload!) (:version |0.0.0) :modules $ [] :entries $ {} @@ -8,17 +8,14 @@ :defs $ {} |bad-arity $ %{} :CodeEntry (:doc "|Expect preprocess error: schema has 2 required args but code has 1") :code $ quote - defn bad-arity (x) $ do x + defn bad-arity (x) (do x) :examples $ [] :schema $ :: :fn {} (:return :number) :args $ [] :number :number |main! $ %{} :CodeEntry (:doc "|Entry for type-fail schema arity mismatch") :code $ quote - defn main! () $ do - ; calling to force preprocessing of bad-arity - bad-arity 1 - println |unreachable + defn main! () $ do (; calling to force preprocessing of bad-arity) (bad-arity 1) (println |unreachable) :examples $ [] :schema $ :: :fn {} (:return :unit) @@ -30,6 +27,5 @@ :schema $ :: :fn {} (:return :unit) :args $ [] - :ns $ %{} :CodeEntry (:doc "|Namespace for schema arity mismatch") (:schema nil) + :ns $ %{} :NsEntry (:doc "|Namespace for schema arity mismatch") :code $ quote (ns type-fail-schema-required-arity.main) - :examples $ [] diff --git a/calcit/type-fail/schema-rest-missing.cirru b/calcit/type-fail/schema-rest-missing.cirru index 19e39248..568c2459 100644 --- a/calcit/type-fail/schema-rest-missing.cirru +++ b/calcit/type-fail/schema-rest-missing.cirru @@ -1,5 +1,5 @@ -{} (:about "|type-fail: code has & rest param but schema has no :rest") (:package |type-fail-schema-rest-missing) +{} (:about "|file is generated - never edit directly; learn cr edit/tree workflows before changing") (:package |type-fail-schema-rest-missing) :configs $ {} (:init-fn |type-fail-schema-rest-missing.main/main!) (:reload-fn |type-fail-schema-rest-missing.main/reload!) (:version |0.0.0) :modules $ [] :entries $ {} @@ -8,17 +8,14 @@ :defs $ {} |bad-rest $ %{} :CodeEntry (:doc "|Expect preprocess error: code has & rest but schema is missing :rest") :code $ quote - defn bad-rest (& xs) $ do xs + defn bad-rest (& xs) (do xs) :examples $ [] :schema $ :: :fn {} (:return :list) :args $ [] |main! $ %{} :CodeEntry (:doc "|Entry for type-fail schema rest mismatch") :code $ quote - defn main! () $ do - ; calling to force preprocessing of bad-rest - bad-rest 1 2 3 - println |unreachable + defn main! () $ do (; calling to force preprocessing of bad-rest) (bad-rest 1 2 3) (println |unreachable) :examples $ [] :schema $ :: :fn {} (:return :unit) @@ -30,6 +27,5 @@ :schema $ :: :fn {} (:return :unit) :args $ [] - :ns $ %{} :CodeEntry (:doc "|Namespace for schema rest mismatch") (:schema nil) + :ns $ %{} :NsEntry (:doc "|Namespace for schema rest mismatch") :code $ quote (ns type-fail-schema-rest-missing.main) - :examples $ [] diff --git a/calcit/type-fail/schema-rest-unexpected.cirru b/calcit/type-fail/schema-rest-unexpected.cirru index a8ce2bfc..6f793ea6 100644 --- a/calcit/type-fail/schema-rest-unexpected.cirru +++ b/calcit/type-fail/schema-rest-unexpected.cirru @@ -1,5 +1,5 @@ -{} (:about "|type-fail: schema has :rest but code has no & param") (:package |type-fail-schema-rest-unexpected) +{} (:about "|file is generated - never edit directly; learn cr edit/tree workflows before changing") (:package |type-fail-schema-rest-unexpected) :configs $ {} (:init-fn |type-fail-schema-rest-unexpected.main/main!) (:reload-fn |type-fail-schema-rest-unexpected.main/reload!) (:version |0.0.0) :modules $ [] :entries $ {} @@ -8,17 +8,14 @@ :defs $ {} |bad-rest $ %{} :CodeEntry (:doc "|Expect preprocess error: schema has :rest but code has no & param") :code $ quote - defn bad-rest (x) $ do x + defn bad-rest (x) (do x) :examples $ [] :schema $ :: :fn - {} (:return :number) (:rest :number) + {} (:rest :number) (:return :number) :args $ [] :number |main! $ %{} :CodeEntry (:doc "|Entry for type-fail schema unexpected rest") :code $ quote - defn main! () $ do - ; calling to force preprocessing of bad-rest - bad-rest 1 - println |unreachable + defn main! () $ do (; calling to force preprocessing of bad-rest) (bad-rest 1) (println |unreachable) :examples $ [] :schema $ :: :fn {} (:return :unit) @@ -30,6 +27,5 @@ :schema $ :: :fn {} (:return :unit) :args $ [] - :ns $ %{} :CodeEntry (:doc "|Namespace for schema unexpected rest") (:schema nil) + :ns $ %{} :NsEntry (:doc "|Namespace for schema unexpected rest") :code $ quote (ns type-fail-schema-rest-unexpected.main) - :examples $ [] diff --git a/calcit/util.cirru b/calcit/util.cirru index 8b7e0504..0c1f6914 100644 --- a/calcit/util.cirru +++ b/calcit/util.cirru @@ -49,7 +49,6 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] - :ns $ %{} :CodeEntry (:doc |) (:schema nil) + :ns $ %{} :NsEntry (:doc |) :code $ quote ns util.core $ :require - :examples $ [] diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index dc9e053a..ee6214fa 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -498,6 +498,7 @@ cr tree replace namespace/def -p '3,2,2,5,2,4,1,2' -e 'let ((x 1)) (+ x task)' **定义操作:** - `cr edit format` - 不修改语义,按当前快照序列化逻辑重写 **snapshot 文件**(用于刷新格式) + - 也会把旧的 namespace `CodeEntry` 写法收敛成当前的 `NsEntry` 结构 - 适用:普通 `compact.cirru` / 项目 snapshot 文件 - 不适用:calcit-editor 专用的 `calcit.cirru` 结构文件 - `cr edit def ` - 添加新定义(默认若已存在会报错;加 `--overwrite` 可强制覆盖) diff --git a/editing-history/2026-0310-1754-ns-entry-snapshot-migration.md b/editing-history/2026-0310-1754-ns-entry-snapshot-migration.md new file mode 100644 index 00000000..9e67252c --- /dev/null +++ b/editing-history/2026-0310-1754-ns-entry-snapshot-migration.md @@ -0,0 +1,66 @@ +# 2026-0310-1754 — snapshot namespace entry migrate to NsEntry + +## 背景 + +snapshot 里的 `:ns` 之前复用了 `CodeEntry` 结构,导致 namespace 也带着 `:schema` 和 `:examples` 字段,语义上并不合适,而且保存时经常只是写成 `(:schema nil)`。 + +这次改动把 namespace 入口单独收敛到 `NsEntry`,同时保持读取旧 `CodeEntry` 写法的兼容性。 + +## 核心改动 + +### `src/snapshot.rs` + +- 新增 `NsEntry { doc, code }` +- `FileInSnapShot.ns` 从 `CodeEntry` 改为 `NsEntry` +- `RawFileInSnapShot.ns` 同步改为 `NsEntry` +- 新增 `TryFrom for NsEntry` 与 `From for Edn` +- 读取 snapshot 时,`ns` 只解析 `doc` 和 `code`,兼容旧 `CodeEntry` 形状并忽略 `schema/examples` +- 移除 `validate_snapshot_schemas_for_write` 对 `ns.schema` 的校验 +- `gen_meta_ns`、`create_file_from_snippet` 等内部构造统一改为写 `NsEntry` + +### `build.rs` + +- 嵌入式 core snapshot 解析结构新增 `NsEntry` +- build 阶段读取 `src/cirru/calcit-core.cirru` 时,`ns` 不再解析为 `CodeEntry` + +### `src/detailed_snapshot.rs` + +- 新增 `DetailedNsEntry { doc, code }` +- `DetailedFileInSnapshot.ns` 从 `DetailedCodeEntry` 改为 `DetailedNsEntry` +- 详细快照读取仍兼容旧 namespace record,只提取 `doc` 和 `code` + +### `src/bin/cr_sync.rs` + +- namespace change payload 从 `CodeEntry` 拆成 `SnapshotEntry::Ns(NsEntry)` +- definition change payload 保持 `SnapshotEntry::Def(CodeEntry)` +- detailed snapshot 写回时,namespace 统一序列化为 `NsEntry` + +### `src/bin/cli_handlers/edit.rs` + +- `edit add-ns` 创建新 namespace 时直接写入 `NsEntry` + +## 数据文件迁移 + +- 批量运行 `cr edit format`,将 compact snapshot 中旧的 `:ns $ %{} :CodeEntry ... (:schema nil) :examples []` 收敛为 `:NsEntry` +- 运行 `cr-sync` 重写 detailed snapshot: + - `demos/calcit.cirru` + - `calcit/editor/calcit.cirru` +- `src/cirru/calcit-core.cirru` 也已更新为 `NsEntry` + +## 兼容策略 + +1. **读取兼容**:旧 snapshot/detailed snapshot 中 `ns` 仍然可以是 `CodeEntry` record +2. **内存收敛**:加载后统一存成 `NsEntry` +3. **保存统一**:以后重新保存或 format 都写回 `NsEntry` + +## 验证 + +- `cargo fmt` ✅ +- `cargo clippy -- -D warnings` ✅ +- `cargo test` ✅ +- `yarn check-all` ✅ + +## 备注 + +- `demos/compact.tmp.cirru` 不是合法 snapshot,`cr edit format` 会报 EDN 解析错误,因此未纳入统一格式化 +- `demos/deps.cirru` 不是当前 snapshot loader 支持的结构,因此同样未走 `edit format` \ No newline at end of file diff --git a/package.json b/package.json index 2b034860..af3d4ad3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.1", + "version": "0.12.2", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", diff --git a/src/bin/cli_handlers/edit.rs b/src/bin/cli_handlers/edit.rs index 0b33d7b0..0cf3edc5 100644 --- a/src/bin/cli_handlers/edit.rs +++ b/src/bin/cli_handlers/edit.rs @@ -16,7 +16,7 @@ use calcit::cli_args::{ EditRmModuleCommand, EditRmNsCommand, EditSchemaCommand, EditSplitDefCommand, EditSubcommand, }; use calcit::snapshot::{ - self, ChangesDict, CodeEntry, FileChangeInfo, FileInSnapShot, Snapshot, save_snapshot_to_file, validate_schema_for_write, + self, ChangesDict, CodeEntry, FileChangeInfo, FileInSnapShot, NsEntry, Snapshot, save_snapshot_to_file, validate_schema_for_write, }; use cirru_parser::Cirru; use colored::Colorize; @@ -1074,7 +1074,10 @@ fn handle_add_ns(opts: &EditAddNsCommand, snapshot_file: &str) -> Result<(), Str }; let file_entry = FileInSnapShot { - ns: CodeEntry::from_code(ns_code), + ns: NsEntry { + doc: String::new(), + code: ns_code, + }, defs: HashMap::new(), }; diff --git a/src/bin/cr_sync.rs b/src/bin/cr_sync.rs index 95c3d487..b3bf3e5a 100644 --- a/src/bin/cr_sync.rs +++ b/src/bin/cr_sync.rs @@ -1,8 +1,8 @@ use argh::FromArgs; use calcit::detailed_snapshot::{ - DetailCirru, DetailedCodeEntry, DetailedFileInSnapshot, DetailedSnapshot, load_detailed_snapshot_data, + DetailCirru, DetailedCodeEntry, DetailedFileInSnapshot, DetailedNsEntry, DetailedSnapshot, load_detailed_snapshot_data, }; -use calcit::snapshot::{CodeEntry, FileInSnapShot, Snapshot, load_snapshot_data}; +use calcit::snapshot::{CodeEntry, FileInSnapShot, NsEntry, Snapshot, load_snapshot_data}; use cirru_edn::Edn; use cirru_parser::Cirru; use std::fmt::Debug; @@ -95,7 +95,9 @@ fn main() -> Result<(), Box> { println!(" {change}"); // 打印差异原因的详细信息 - if let (Some(new_entry), ChangePath::FunctionDefinition { file_name, def_name }) = (&change.new_entry, &change.path) { + if let (Some(SnapshotEntry::Def(new_entry)), ChangePath::FunctionDefinition { file_name, def_name }) = + (&change.new_entry, &change.path) + { if let Some(detailed_file) = detailed_snapshot.files.get(file_name) { if let Some(detailed_entry) = detailed_file.defs.get(def_name) { match change.change_type { @@ -117,7 +119,9 @@ fn main() -> Result<(), Box> { } } } - } else if let (Some(new_entry), ChangePath::NamespaceDefinition { file_name }) = (&change.new_entry, &change.path) { + } else if let (Some(SnapshotEntry::Ns(new_entry)), ChangePath::NamespaceDefinition { file_name }) = + (&change.new_entry, &change.path) + { if let Some(detailed_file) = detailed_snapshot.files.get(file_name) { match change.change_type { ChangeType::ModifiedCode => { @@ -186,11 +190,17 @@ impl std::fmt::Display for ChangePath { } } +#[derive(Debug, Clone)] +enum SnapshotEntry { + Ns(NsEntry), + Def(CodeEntry), +} + #[derive(Debug, Clone)] struct SnapshotChange { path: ChangePath, change_type: ChangeType, - new_entry: Option, + new_entry: Option, } #[derive(Debug, Clone, PartialEq)] @@ -262,7 +272,7 @@ fn detailed_snapshot_to_edn(snapshot: &DetailedSnapshot) -> Edn { file_record.pairs.push(("defs".into(), Edn::from(defs_map))); // Add ns field, make sure "ns" is after "defs" - file_record.pairs.push(("ns".into(), detailed_code_entry_to_edn(&v.ns))); + file_record.pairs.push(("ns".into(), detailed_ns_entry_to_edn(&v.ns))); files_map.insert(Edn::str(k.as_str()), Edn::Record(file_record)); } @@ -303,6 +313,18 @@ fn detailed_code_entry_to_edn(entry: &DetailedCodeEntry) -> Edn { Edn::Record(record) } +fn detailed_ns_entry_to_edn(entry: &DetailedNsEntry) -> Edn { + let mut record = cirru_edn::EdnRecordView { + tag: cirru_edn::EdnTag::new("NsEntry"), + pairs: Vec::new(), + }; + + record.pairs.push(("code".into(), detailed_cirru_to_edn(&entry.code))); + record.pairs.push(("doc".into(), Edn::Str(entry.doc.as_str().into()))); + + Edn::Record(record) +} + // Helper function to convert DetailCirru to Edn fn detailed_cirru_to_edn(cirru: &DetailCirru) -> Edn { match cirru { @@ -528,7 +550,7 @@ fn detect_snapshot_changes(compact: &Snapshot, detailed: &DetailedSnapshot) -> V file_name: file_name.clone(), }, change_type: ChangeType::Added, - new_entry: Some(compact_file.ns.clone()), + new_entry: Some(SnapshotEntry::Ns(compact_file.ns.clone())), }); // Then add all definitions in the new file @@ -539,7 +561,7 @@ fn detect_snapshot_changes(compact: &Snapshot, detailed: &DetailedSnapshot) -> V def_name: def_name.clone(), }, change_type: ChangeType::Added, - new_entry: Some(code_entry.clone()), + new_entry: Some(SnapshotEntry::Def(code_entry.clone())), }); } } @@ -593,7 +615,7 @@ fn compare_file_definitions( file_name: file_name.to_string(), }, change_type, - new_entry: Some(compact_file.ns.clone()), + new_entry: Some(SnapshotEntry::Ns(compact_file.ns.clone())), }); } @@ -630,7 +652,7 @@ fn compare_file_definitions( def_name: def_name.clone(), }, change_type, - new_entry: Some(compact_entry.clone()), + new_entry: Some(SnapshotEntry::Def(compact_entry.clone())), }); } } @@ -642,7 +664,7 @@ fn compare_file_definitions( def_name: def_name.clone(), }, change_type: ChangeType::Added, - new_entry: Some(compact_entry.clone()), + new_entry: Some(SnapshotEntry::Def(compact_entry.clone())), }); } } @@ -683,20 +705,18 @@ fn apply_snapshot_changes(detailed: &mut DetailedSnapshot, changes: &[SnapshotCh } } -fn apply_add_change(detailed: &mut DetailedSnapshot, path: &ChangePath, new_entry: &CodeEntry) { +fn apply_add_change(detailed: &mut DetailedSnapshot, path: &ChangePath, new_entry: &SnapshotEntry) { match path { ChangePath::FunctionDefinition { file_name, def_name } => { // Create file if it doesn't exist if !detailed.files.contains_key(file_name) { - use calcit::detailed_snapshot::{DetailedCodeEntry, DetailedFileInSnapshot}; + use calcit::detailed_snapshot::{DetailedFileInSnapshot, DetailedNsEntry}; use std::collections::HashMap; // Create empty namespace entry - let empty_ns = DetailedCodeEntry { + let empty_ns = DetailedNsEntry { doc: String::new(), - examples: vec![], code: cirru_parser::Cirru::Leaf("".into()).into(), - schema: calcit::calcit::DYNAMIC_TYPE.clone(), }; detailed.files.insert( @@ -708,7 +728,7 @@ fn apply_add_change(detailed: &mut DetailedSnapshot, path: &ChangePath, new_entr ); } - if let Some(file) = detailed.files.get_mut(file_name) { + if let (Some(file), SnapshotEntry::Def(new_entry)) = (detailed.files.get_mut(file_name), new_entry) { file.defs.insert(def_name.clone(), new_entry.clone().into()); } } @@ -721,21 +741,27 @@ fn apply_add_change(detailed: &mut DetailedSnapshot, path: &ChangePath, new_entr detailed.files.insert( file_name.clone(), DetailedFileInSnapshot { - ns: new_entry.clone().into(), + ns: match new_entry { + SnapshotEntry::Ns(entry) => entry.clone().into(), + SnapshotEntry::Def(_) => DetailedNsEntry { + doc: String::new(), + code: cirru_parser::Cirru::Leaf("".into()).into(), + }, + }, defs: HashMap::new(), }, ); - } else if let Some(file) = detailed.files.get_mut(file_name) { + } else if let (Some(file), SnapshotEntry::Ns(new_entry)) = (detailed.files.get_mut(file_name), new_entry) { file.ns = new_entry.clone().into(); } } } } -fn apply_modify_change(detailed: &mut DetailedSnapshot, path: &ChangePath, new_entry: &CodeEntry, change_type: &ChangeType) { +fn apply_modify_change(detailed: &mut DetailedSnapshot, path: &ChangePath, new_entry: &SnapshotEntry, change_type: &ChangeType) { match path { ChangePath::FunctionDefinition { file_name, def_name } => { - if let Some(file) = detailed.files.get_mut(file_name) { + if let (Some(file), SnapshotEntry::Def(new_entry)) = (detailed.files.get_mut(file_name), new_entry) { if let Some(existing_def) = file.defs.get_mut(def_name) { // Update document part existing_def.doc = new_entry.doc.clone(); @@ -751,7 +777,7 @@ fn apply_modify_change(detailed: &mut DetailedSnapshot, path: &ChangePath, new_e } } ChangePath::NamespaceDefinition { file_name } => { - if let Some(file) = detailed.files.get_mut(file_name) { + if let (Some(file), SnapshotEntry::Ns(new_entry)) = (detailed.files.get_mut(file_name), new_entry) { // Update document part file.ns.doc = new_entry.doc.clone(); diff --git a/src/bin/cr_tests/type_fail.rs b/src/bin/cr_tests/type_fail.rs index 7fbb195b..2f84e308 100644 --- a/src/bin/cr_tests/type_fail.rs +++ b/src/bin/cr_tests/type_fail.rs @@ -117,4 +117,4 @@ fn run_check_only_surfaces_schema_error_code() { err.contains("E_SCHEMA_DEF_MISMATCH"), "check-only error should contain code, got: {err}" ); -} \ No newline at end of file +} diff --git a/src/cirru/calcit-core.cirru b/src/cirru/calcit-core.cirru index 845d4519..7d6a523f 100644 --- a/src/cirru/calcit-core.cirru +++ b/src/cirru/calcit-core.cirru @@ -4857,10 +4857,9 @@ |~@ $ %{} :CodeEntry (:doc "|internal syntax for spreading interpolate value in macro\nSyntax: (~@ list-expr) inside quasiquote\nParams: list-expr (expression that evaluates to list)\nReturns: spliced list elements\nUnquotes and splices list elements inside quasiquote") (:schema nil) :code $ quote &runtime-inplementation :examples $ [] - :ns $ %{} :CodeEntry (:doc "|built-in function and macros in `calcit.core`") (:schema nil) + :ns $ %{} :NsEntry (:doc "|built-in function and macros in `calcit.core`") :code $ quote ns calcit.core $ :require (calcit.internal :as internal) - :examples $ [] |calcit.internal $ %{} :FileEntry :defs $ {} |&core-add-list-impl $ %{} :CodeEntry (:doc "|Core trait impl for Add on list") (:schema nil) @@ -5009,7 +5008,6 @@ , t0 , t0 :examples $ [] - :ns $ %{} :CodeEntry (:doc "|internal function and macros for `calcit.core`") (:schema nil) + :ns $ %{} :NsEntry (:doc "|internal function and macros for `calcit.core`") :code $ quote ns calcit.internal $ :require - :examples $ [] diff --git a/src/detailed_snapshot.rs b/src/detailed_snapshot.rs index ff338363..51025c11 100644 --- a/src/detailed_snapshot.rs +++ b/src/detailed_snapshot.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use crate::calcit::{CalcitTypeAnnotation, DYNAMIC_TYPE}; -use crate::snapshot::{CodeEntry, FileInSnapShot, gen_meta_ns}; +use crate::snapshot::{CodeEntry, FileInSnapShot, NsEntry, gen_meta_ns}; /// Detailed Cirru structure with metadata for tracking changes #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -252,10 +252,66 @@ impl TryFrom for DetailedCodeEntry { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DetailedNsEntry { + pub doc: String, + pub code: DetailCirru, +} + +impl From for DetailedNsEntry { + fn from(entry: NsEntry) -> Self { + DetailedNsEntry { + doc: entry.doc, + code: entry.code.into(), + } + } +} + +impl From for NsEntry { + fn from(detailed: DetailedNsEntry) -> Self { + NsEntry { + doc: detailed.doc, + code: detailed.code.into(), + } + } +} + +impl TryFrom for DetailedNsEntry { + type Error = String; + fn try_from(data: Edn) -> Result { + match data { + Edn::Record(record) => { + let mut doc = String::new(); + let mut code = None; + + for (key, value) in record.pairs.iter() { + match key.arc_str().as_ref() { + "doc" => { + if let Edn::Str(doc_str) = value { + doc = doc_str.to_string(); + } + } + "code" => { + code = Some(value.to_owned().try_into()?); + } + _ => {} + } + } + + Ok(DetailedNsEntry { + doc, + code: code.ok_or("Missing code field")?, + }) + } + _ => Err("Expected record for DetailedNsEntry".to_string()), + } + } +} + /// Detailed file in snapshot with metadata #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DetailedFileInSnapshot { - pub ns: DetailedCodeEntry, + pub ns: DetailedNsEntry, pub defs: HashMap, } diff --git a/src/snapshot.rs b/src/snapshot.rs index 1b3bad27..b8d9d919 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -93,9 +93,15 @@ pub struct SnapshotConfigs { pub version: String, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NsEntry { + pub doc: String, + pub code: Cirru, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct FileInSnapShot { - pub ns: CodeEntry, + pub ns: NsEntry, pub defs: HashMap, } @@ -111,7 +117,7 @@ struct RawCodeEntry { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] struct RawFileInSnapShot { - pub ns: RawCodeEntry, + pub ns: NsEntry, pub defs: HashMap, } @@ -145,8 +151,7 @@ pub fn decode_binary_snapshot(bytes: &[u8]) -> Result { let mut files: HashMap = HashMap::with_capacity(raw.files.len()); for (file_name, raw_file) in raw.files { - let ns_owner = format!("{file_name}/:ns"); - let ns = raw_file.ns.into_code_entry(&ns_owner)?; + let ns = raw_file.ns; let mut defs: HashMap = HashMap::with_capacity(raw_file.defs.len()); for (def_name, raw_entry) in raw_file.defs { @@ -219,6 +224,67 @@ impl From for Edn { } } +impl TryFrom for NsEntry { + type Error = String; + fn try_from(data: Edn) -> Result { + let mut doc = String::new(); + let mut code: Option = None; + + match data { + Edn::Record(record) => { + for (key, value) in &record.pairs { + match key.arc_str().as_ref() { + "doc" => { + doc = from_edn(value.to_owned()).map_err(|e| format!("failed to parse NsEntry.doc: {e}"))?; + } + "code" => { + code = Some(from_edn(value.to_owned()).map_err(|e| format!("failed to parse NsEntry.code: {e}"))?); + } + _ => {} + } + } + } + Edn::Map(map) => { + if let Some(value) = map.get(&Edn::Tag(EdnTag::new("doc"))) { + doc = from_edn(value.to_owned()).map_err(|e| format!("failed to parse NsEntry.doc: {e}"))?; + } + if let Some(value) = map.get(&Edn::Tag(EdnTag::new("code"))) { + code = Some(from_edn(value.to_owned()).map_err(|e| format!("failed to parse NsEntry.code: {e}"))?); + } + } + other => { + return Err(format!("failed to parse NsEntry: expected record/map, got: {other:?}")); + } + } + + Ok(NsEntry { + doc, + code: code.ok_or_else(|| "failed to parse NsEntry: missing code field".to_owned())?, + }) + } +} + +impl From for Edn { + fn from(data: NsEntry) -> Self { + Edn::record_from_pairs( + "NsEntry".into(), + &[("doc".into(), data.doc.into()), ("code".into(), data.code.into())], + ) + } +} + +impl From<&NsEntry> for Edn { + fn from(data: &NsEntry) -> Self { + Edn::record_from_pairs( + "NsEntry".into(), + &[ + ("doc".into(), data.doc.to_owned().into()), + ("code".into(), data.code.to_owned().into()), + ], + ) + } +} + /// Custom serde for `CodeEntry::schema`. /// The binary RMP format stores schemas as `Option` (compatible with `build.rs`); /// at runtime we keep a parsed `Arc` for direct use. @@ -544,8 +610,6 @@ fn validate_snapshot_schemas_for_write(snapshot: &Snapshot) -> Result<(), String continue; } - validate_schema_for_snapshot_write(&format!("{ns_name}/:ns"), &file_data.ns.schema)?; - for (def_name, code_entry) in &file_data.defs { validate_schema_for_snapshot_write(&format!("{ns_name}/{def_name}"), &code_entry.schema)?; } @@ -994,7 +1058,10 @@ fn parse_file_in_snapshot_with_context(data: Edn, file_name: &str) -> Result Result { - let mut ns = None; + let mut ns: Option = None; let mut defs = HashMap::new(); for (key, value) in record.pairs.iter() { match key.arc_str().as_ref() { "ns" => { - ns = Some(parse_code_entry_with_context(value.to_owned(), &format!("{file_name}/:ns"))?); + ns = Some(value.to_owned().try_into().map_err(|e: String| format!("{file_name}/:ns: {e}"))?); } "defs" => { let defs_map = value.view_map().map_err(|e| { @@ -1084,11 +1151,9 @@ pub fn gen_meta_ns(ns: &str, path: &str) -> FileInSnapShot { ]); FileInSnapShot { - ns: CodeEntry { + ns: NsEntry { doc: "".to_owned(), - examples: vec![], code: vec!["ns", ns].into(), - schema: DYNAMIC_TYPE.clone(), }, defs: def_dict, } @@ -1140,7 +1205,10 @@ pub fn create_file_from_snippet(raw: &str) -> Result { CodeEntry::from_code(vec![Cirru::leaf("defn"), "reload!".into(), Cirru::List(vec![])].into()), ); Ok(FileInSnapShot { - ns: CodeEntry::from_code(ns_code), + ns: NsEntry { + doc: "".to_owned(), + code: ns_code, + }, defs: def_dict, }) } From 449e779231f30c5281c1ce6fc1221d1c83090723 Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 10 Mar 2026 19:36:20 +0800 Subject: [PATCH 02/57] improve edit format status output --- src/bin/cli_handlers/edit.rs | 15 ++++++++++++--- src/snapshot.rs | 10 +++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/bin/cli_handlers/edit.rs b/src/bin/cli_handlers/edit.rs index 0cf3edc5..92819afe 100644 --- a/src/bin/cli_handlers/edit.rs +++ b/src/bin/cli_handlers/edit.rs @@ -16,7 +16,8 @@ use calcit::cli_args::{ EditRmModuleCommand, EditRmNsCommand, EditSchemaCommand, EditSplitDefCommand, EditSubcommand, }; use calcit::snapshot::{ - self, ChangesDict, CodeEntry, FileChangeInfo, FileInSnapShot, NsEntry, Snapshot, save_snapshot_to_file, validate_schema_for_write, + self, ChangesDict, CodeEntry, FileChangeInfo, FileInSnapShot, NsEntry, Snapshot, render_snapshot_content, save_snapshot_to_file, + validate_schema_for_write, }; use cirru_parser::Cirru; use colored::Colorize; @@ -85,10 +86,18 @@ pub fn handle_edit_command(cmd: &EditCommand, snapshot_file: &str) -> Result<(), } fn handle_format(_opts: &EditFormatCommand, snapshot_file: &str) -> Result<(), String> { + let original_content = fs::read_to_string(snapshot_file).map_err(|e| format!("Failed to read {snapshot_file}: {e}"))?; let snapshot = load_snapshot(snapshot_file)?; - save_snapshot(&snapshot, snapshot_file)?; + let formatted_content = render_snapshot_content(&snapshot)?; + + if formatted_content == original_content { + println!("{} No formatting changes for '{}'", "·".dimmed(), snapshot_file.dimmed()); + return Ok(()); + } + + fs::write(snapshot_file, formatted_content).map_err(|e| format!("Failed to write {snapshot_file}: {e}"))?; - println!("{} Refreshed snapshot file '{}'", "✓".green(), snapshot_file.cyan()); + println!("{} Formatted snapshot file '{}'", "✓".green(), snapshot_file.cyan()); Ok(()) } diff --git a/src/snapshot.rs b/src/snapshot.rs index b8d9d919..18919007 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -1331,7 +1331,7 @@ impl TryFrom for Edn { /// Save snapshot to compact.cirru file /// This is a shared utility function used by CLI edit commands -pub fn save_snapshot_to_file>(compact_cirru_path: P, snapshot: &Snapshot) -> Result<(), String> { +pub fn render_snapshot_content(snapshot: &Snapshot) -> Result { validate_snapshot_schemas_for_write(snapshot)?; // Build root level Edn mapping @@ -1394,6 +1394,14 @@ pub fn save_snapshot_to_file>(compact_cirru_path: P, snapshot: &S validate_serialized_snapshot_content(&content)?; + Ok(content) +} + +/// Save snapshot to compact.cirru file +/// This is a shared utility function used by CLI edit commands +pub fn save_snapshot_to_file>(compact_cirru_path: P, snapshot: &Snapshot) -> Result<(), String> { + let content = render_snapshot_content(snapshot)?; + // Write to file std::fs::write(compact_cirru_path, content).map_err(|e| format!("Failed to write compact.cirru: {e}"))?; From 8ba3533af0f528e2a86d4d004d74795abbb10ef3 Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 11 Mar 2026 01:16:39 +0800 Subject: [PATCH 03/57] upgrade parser; tag 0.12.3 --- Cargo.lock | 2 +- Cargo.toml | 2 +- package.json | 4 ++-- yarn.lock | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31e8c0cb..697d9477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.2" +version = "0.12.3" dependencies = [ "argh", "bisection_key", diff --git a/Cargo.toml b/Cargo.toml index 0a3d557e..efc4ada3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.2" +version = "0.12.3" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/package.json b/package.json index af3d4ad3..e923e472 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.2", + "version": "0.12.3", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", @@ -31,7 +31,7 @@ }, "dependencies": { "@calcit/ternary-tree": "0.0.25", - "@cirru/parser.ts": "^0.0.8", + "@cirru/parser.ts": "^0.0.9", "@cirru/writer.ts": "^0.1.7" }, "packageManager": "yarn@4.12.0" diff --git a/yarn.lock b/yarn.lock index 83db3afd..de5656bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,7 +10,7 @@ __metadata: resolution: "@calcit/procs@workspace:." dependencies: "@calcit/ternary-tree": "npm:0.0.25" - "@cirru/parser.ts": "npm:^0.0.8" + "@cirru/parser.ts": "npm:^0.0.9" "@cirru/writer.ts": "npm:^0.1.7" "@types/node": "npm:^25.0.9" typescript: "npm:^5.9.3" @@ -24,10 +24,10 @@ __metadata: languageName: node linkType: hard -"@cirru/parser.ts@npm:^0.0.8": - version: 0.0.8 - resolution: "@cirru/parser.ts@npm:0.0.8" - checksum: 10c0/a722a1ae31503cd602d4bb868f9831f7cf0a3d457cde40fc363120271ebb1320c5cabdf051393b5b10ef3e29452c6e25ab8c2b0d981605299312391f528957b6 +"@cirru/parser.ts@npm:^0.0.9": + version: 0.0.9 + resolution: "@cirru/parser.ts@npm:0.0.9" + checksum: 10c0/3b13623b8f627ac81adae0cb6a4e3664f0da09115a997ca9b78b8d5d0b0f9b9b18c1ad847e068831cb1052c7da2a00329d2af04421b1024a004eef0ddca5fd7a languageName: node linkType: hard From 98bc84c8142be617ab27e8a62394e3b9d1472742 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 12 Mar 2026 01:40:47 +0800 Subject: [PATCH 04/57] fix: vals returns :set (not :list); query def/peek/schema use tuple format; tag 0.12.4 --- Cargo.lock | 2 +- Cargo.toml | 2 +- package.json | 2 +- src/bin/cli_handlers/query.rs | 6 +++--- src/cirru/calcit-core.cirru | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 697d9477..50eb887b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.3" +version = "0.12.4" dependencies = [ "argh", "bisection_key", diff --git a/Cargo.toml b/Cargo.toml index efc4ada3..75df00e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.3" +version = "0.12.4" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/package.json b/package.json index e923e472..26f2dae2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.3", + "version": "0.12.4", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", diff --git a/src/bin/cli_handlers/query.rs b/src/bin/cli_handlers/query.rs index 6daf224a..1f69fa66 100644 --- a/src/bin/cli_handlers/query.rs +++ b/src/bin/cli_handlers/query.rs @@ -469,7 +469,7 @@ fn handle_def(input_path: &str, namespace: &str, definition: &str, show_json: bo println!("\n{}", "Schema:".bold()); if let CalcitTypeAnnotation::Fn(fn_annot) = code_entry.schema.as_ref() { - let schema_str = match snapshot::schema_edn_to_cirru(&fn_annot.to_schema_edn()) { + let schema_str = match snapshot::schema_edn_to_cirru(&fn_annot.to_wrapped_schema_edn()) { Ok(c) => cirru_parser::format(std::slice::from_ref(&c), true.into()).unwrap_or_else(|_| "(failed to format)".to_string()), Err(e) => format!("(schema error: {e})"), }; @@ -621,7 +621,7 @@ fn handle_peek(input_path: &str, namespace: &str, definition: &str) -> Result<() println!("{} {}", "Examples:".bold(), code_entry.examples.len()); if let CalcitTypeAnnotation::Fn(fn_annot) = code_entry.schema.as_ref() { - let preview = match snapshot::schema_edn_to_cirru(&fn_annot.to_schema_edn()) { + let preview = match snapshot::schema_edn_to_cirru(&fn_annot.to_wrapped_schema_edn()) { Ok(c) => c.format_one_liner()?, Err(e) => format!("(schema error: {e})"), }; @@ -673,7 +673,7 @@ fn handle_schema(input_path: &str, namespace: &str, definition: &str, json: bool println!("{} {}/{}", "Definition:".bold(), namespace.cyan(), definition.green()); if let CalcitTypeAnnotation::Fn(fn_annot) = code_entry.schema.as_ref() { - let cirru = snapshot::schema_edn_to_cirru(&fn_annot.to_schema_edn())?; + let cirru = snapshot::schema_edn_to_cirru(&fn_annot.to_wrapped_schema_edn())?; println!("{} {}", "Schema:".bold(), cirru.format_one_liner()?.dimmed()); } else { println!("{} -", "Schema:".bold()); diff --git a/src/cirru/calcit-core.cirru b/src/cirru/calcit-core.cirru index 7d6a523f..2268511f 100644 --- a/src/cirru/calcit-core.cirru +++ b/src/cirru/calcit-core.cirru @@ -4651,12 +4651,12 @@ defn vals (x) map (to-pairs x) last :examples $ [] - quote $ assert= ([] 1 2) + quote $ assert= (#{} 1 2) vals $ {} (:a 1) (:b 2) - quote $ assert= ([]) + quote $ assert= (#{}) vals $ {} :schema $ :: :fn - {} (:return :list) + {} (:return :set) :args $ [] :map |w-js-log $ %{} :CodeEntry (:doc |) :code $ quote From a106c4efd91a065048324cd20d579ba6f7753375 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 12 Mar 2026 09:55:59 +0800 Subject: [PATCH 05/57] fix: handle Calcit::Map and Calcit::Set in gen_ir dump_code (panic in ir mode with schema annotations); tag 0.12.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- package.json | 2 +- src/codegen/gen_ir.rs | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50eb887b..baa0e71d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.4" +version = "0.12.5" dependencies = [ "argh", "bisection_key", diff --git a/Cargo.toml b/Cargo.toml index 75df00e1..46f3f9c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.4" +version = "0.12.5" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/package.json b/package.json index 26f2dae2..2ac98269 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.4", + "version": "0.12.5", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", diff --git a/src/codegen/gen_ir.rs b/src/codegen/gen_ir.rs index 1cf7c0fb..0dbe1cb6 100644 --- a/src/codegen/gen_ir.rs +++ b/src/codegen/gen_ir.rs @@ -271,6 +271,21 @@ pub(crate) fn dump_code(code: &Calcit) -> Edn { } Edn::map_from_iter(entries) } + Calcit::Map(xs) => { + // Map literals can appear as hint-fn schema data injected during preprocessing. + let mut pairs = EdnListView::default(); + for (k, v) in xs.iter() { + pairs.push(Edn::from(vec![dump_code(k), dump_code(v)])); + } + Edn::map_from_iter([(Edn::tag("kind"), Edn::tag("map")), (Edn::tag("pairs"), pairs.into())]) + } + Calcit::Set(xs) => { + let mut items = EdnListView::default(); + for x in xs.iter() { + items.push(dump_code(x)); + } + Edn::map_from_iter([(Edn::tag("kind"), Edn::tag("set")), (Edn::tag("items"), items.into())]) + } Calcit::RawCode(_, code) => Edn::map_from_iter([ (Edn::tag("kind"), Edn::tag("raw-code")), (Edn::tag("code"), Edn::Str(code.to_owned())), From ca008b78d2db6b455d65a68e090c58a7f2901629 Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 13 Mar 2026 00:08:11 +0800 Subject: [PATCH 06/57] fix: sanitize Edn::AnyRef before cirru_edn::format to prevent panic in error snapshots and format-cirru-edn; tag 0.12.6 --- .github/workflows/publish.yaml | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- package.json | 2 +- src/builtins/meta.rs | 5 ++++- src/call_stack.rs | 3 ++- src/codegen/gen_ir.rs | 4 ++++ src/data/edn.rs | 25 +++++++++++++++++++++++++ 8 files changed, 39 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 99b227cd..f7f003f3 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -49,7 +49,7 @@ jobs: - run: cargo build --release - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: files: | target/release/cr diff --git a/Cargo.lock b/Cargo.lock index baa0e71d..6438fb6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.5" +version = "0.12.6" dependencies = [ "argh", "bisection_key", diff --git a/Cargo.toml b/Cargo.toml index 46f3f9c1..3640dca5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.5" +version = "0.12.6" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/package.json b/package.json index 2ac98269..c0a684bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.5", + "version": "0.12.6", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", diff --git a/src/builtins/meta.rs b/src/builtins/meta.rs index 8a8b64b1..1c7ff7f0 100644 --- a/src/builtins/meta.rs +++ b/src/builtins/meta.rs @@ -307,7 +307,10 @@ pub fn parse_cirru_edn(xs: &[Calcit]) -> Result { pub fn format_cirru_edn(xs: &[Calcit]) -> Result { match xs.first() { - Some(a) => Ok(Calcit::Str(cirru_edn::format(&edn::calcit_to_edn(a)?, true)?.into())), + Some(a) => { + let raw = edn::calcit_to_edn(a)?; + Ok(Calcit::Str(cirru_edn::format(&edn::sanitize_edn_for_format(&raw), true)?.into())) + } None => { let hint = format_proc_examples_hint(&CalcitProc::FormatCirruEdn).unwrap_or_default(); CalcitErr::err_str_with_hint( diff --git a/src/call_stack.rs b/src/call_stack.rs index aabe28a9..05ef5a06 100644 --- a/src/call_stack.rs +++ b/src/call_stack.rs @@ -168,7 +168,8 @@ pub fn display_stack_with_docs( for s in &stack.0 { let mut args = EdnListView::default(); for v in s.args.iter() { - args.push(edn::calcit_to_edn(v)?); + let edn_val = edn::calcit_to_edn(v)?; + args.push(edn::sanitize_edn_for_format(&edn_val)); } let stack_location = find_location_in_calcit(&s.code).or_else(|| s.args.iter().find_map(find_location_in_calcit)); let mut info_map = vec![ diff --git a/src/codegen/gen_ir.rs b/src/codegen/gen_ir.rs index 0dbe1cb6..94af2270 100644 --- a/src/codegen/gen_ir.rs +++ b/src/codegen/gen_ir.rs @@ -279,6 +279,10 @@ pub(crate) fn dump_code(code: &Calcit) -> Edn { } Edn::map_from_iter([(Edn::tag("kind"), Edn::tag("map")), (Edn::tag("pairs"), pairs.into())]) } + Calcit::AnyRef(_) => { + // AnyRef is an opaque runtime handle; it cannot be embedded in IR code. + Edn::map_from_iter([(Edn::tag("kind"), Edn::tag("any-ref"))]) + } Calcit::Set(xs) => { let mut items = EdnListView::default(); for x in xs.iter() { diff --git a/src/data/edn.rs b/src/data/edn.rs index 6ff209f9..483f4f9a 100644 --- a/src/data/edn.rs +++ b/src/data/edn.rs @@ -130,6 +130,31 @@ pub fn calcit_to_edn(x: &Calcit) -> Result { } } +/// Recursively replace any `Edn::AnyRef` nodes with a text placeholder so the result is safe to +/// pass to `cirru_edn::format`. Use this in display/error-snapshot paths, NOT in FFI round-trips. +pub fn sanitize_edn_for_format(e: &Edn) -> Edn { + match e { + Edn::AnyRef(_) => Edn::str("&any-ref"), + Edn::List(EdnListView(xs)) => Edn::List(EdnListView(xs.iter().map(sanitize_edn_for_format).collect())), + Edn::Set(xs) => { + let mut ys = EdnSetView::default(); + for x in xs.0.iter() { + ys.insert(sanitize_edn_for_format(x)); + } + ys.into() + } + Edn::Map(xs) => { + let mut ys = EdnMapView::default(); + for (k, v) in xs.0.iter() { + ys.insert(sanitize_edn_for_format(k), sanitize_edn_for_format(v)); + } + ys.into() + } + Edn::Atom(inner) => Edn::Atom(Box::new(sanitize_edn_for_format(inner))), + other => other.clone(), + } +} + pub fn edn_to_calcit(x: &Edn, options: &Calcit) -> Calcit { match x { Edn::Nil => Calcit::Nil, From 90b7c38b1f90be9d220b6a5efffc0d5874cdcba7 Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 13 Mar 2026 01:11:54 +0800 Subject: [PATCH 07/57] fix: sanitize nested AnyRef values before formatting EDN; tag 0.12.7 --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 4 ++-- package.json | 2 +- src/data/edn.rs | 12 ++++++++++++ 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6438fb6f..a63c30f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.6" +version = "0.12.7" dependencies = [ "argh", "bisection_key", @@ -194,7 +194,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -244,7 +244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -395,9 +395,9 @@ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libloading" -version = "0.8.9" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" dependencies = [ "cfg-if", "windows-link", @@ -621,7 +621,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -780,7 +780,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -920,7 +920,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3640dca5..01c25265 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.6" +version = "0.12.7" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" @@ -45,7 +45,7 @@ serde = { version = "1.0", features = ["derive"] } cirru_parser = "0.2.3" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -libloading = "0.8.9" +libloading = "0.9.0" ctrlc = "3.4.5" [lib] diff --git a/package.json b/package.json index c0a684bf..421e72a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.6", + "version": "0.12.7", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", diff --git a/src/data/edn.rs b/src/data/edn.rs index 483f4f9a..5026158c 100644 --- a/src/data/edn.rs +++ b/src/data/edn.rs @@ -150,6 +150,18 @@ pub fn sanitize_edn_for_format(e: &Edn) -> Edn { } ys.into() } + Edn::Tuple(EdnTupleView { tag, extra, enum_tag }) => Edn::Tuple(EdnTupleView { + tag: Arc::new(sanitize_edn_for_format(tag)), + extra: extra.iter().map(sanitize_edn_for_format).collect(), + enum_tag: enum_tag.as_ref().map(|x| Arc::new(sanitize_edn_for_format(x))), + }), + Edn::Record(EdnRecordView { tag, pairs }) => { + let mut ys = EdnRecordView::new(tag.to_owned()); + for (k, v) in pairs.iter() { + ys.insert(k.to_owned(), sanitize_edn_for_format(v)); + } + ys.into() + } Edn::Atom(inner) => Edn::Atom(Box::new(sanitize_edn_for_format(inner))), other => other.clone(), } From 2fa74b82c4f3a13eeb0722238f46da84791815e0 Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 13 Mar 2026 01:48:26 +0800 Subject: [PATCH 08/57] feat: add --trace-ffi flag and detailed native call tracing; tag 0.12.8 --- Cargo.lock | 2 +- Cargo.toml | 2 +- package.json | 2 +- src/bin/cr.rs | 2 + src/bin/injection/mod.rs | 169 +++++++++++++++++++++++++++++++++++++-- src/cli_args.rs | 3 + 6 files changed, 171 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a63c30f6..30f86241 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.7" +version = "0.12.8" dependencies = [ "argh", "bisection_key", diff --git a/Cargo.toml b/Cargo.toml index 01c25265..d2d6712c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.7" +version = "0.12.8" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/package.json b/package.json index 421e72a8..4c5eebd8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.7", + "version": "0.12.8", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", diff --git a/src/bin/cr.rs b/src/bin/cr.rs index 62c10318..c474bf3e 100644 --- a/src/bin/cr.rs +++ b/src/bin/cr.rs @@ -83,6 +83,8 @@ fn main() -> Result<(), String> { // get dirty functions injected #[cfg(not(target_arch = "wasm32"))] + injection::set_trace_ffi(cli_args.trace_ffi); + #[cfg(not(target_arch = "wasm32"))] injection::inject_platform_apis(); let core_snapshot = calcit::load_core_snapshot()?; diff --git a/src/bin/injection/mod.rs b/src/bin/injection/mod.rs index 6ff1de19..9826930f 100644 --- a/src/bin/injection/mod.rs +++ b/src/bin/injection/mod.rs @@ -2,15 +2,20 @@ use crate::runner; use cirru_edn::Edn; use colored::Colorize; use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::process; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::AtomicUsize; use std::sync::{Arc, LazyLock, Mutex}; use std::thread; +use std::time::Instant; use calcit::{ builtins, builtins::{RegisteredProcDescriptor, RegisteredProcPlatform, RegisteredProcStability}, calcit::{Calcit, CalcitErr, CalcitErrKind}, call_stack::{CallStackList, display_stack}, - data::edn::{calcit_to_edn, edn_to_calcit}, + data::edn::{calcit_to_edn, edn_to_calcit, sanitize_edn_for_format}, runner::track, }; @@ -24,15 +29,76 @@ type EdnFfiFn = fn( /// lazily cache dylibs, in case Linux drops memory of libraries static DYLIBS: LazyLock>>> = LazyLock::new(|| Mutex::new(HashMap::new())); +static TRACE_FFI: AtomicBool = AtomicBool::new(false); +static TRACE_FFI_EVENT_ID: AtomicUsize = AtomicUsize::new(1); +static TRACE_FFI_STARTED: LazyLock = LazyLock::new(Instant::now); + +pub fn set_trace_ffi(v: bool) { + TRACE_FFI.store(v, Ordering::Relaxed); + if v { + let cwd = std::env::current_dir() + .map(|p| p.display().to_string()) + .unwrap_or_else(|_| "".to_string()); + let exe = std::env::current_exe() + .map(|p| p.display().to_string()) + .unwrap_or_else(|_| "".to_string()); + trace_ffi_event( + "enable", + format!("cwd={cwd} exe={exe} abi={ABI_VERSION} host={}", std::env::consts::OS), + ); + } +} + +fn should_trace_ffi() -> bool { + TRACE_FFI.load(Ordering::Relaxed) +} + +fn format_edn_args_for_trace(args: &[Edn]) -> String { + let sanitized: Vec = args.iter().map(sanitize_edn_for_format).collect(); + match cirru_edn::format(&Edn::List(cirru_edn::EdnListView(sanitized)), true) { + Ok(s) => s.trim().to_owned(), + Err(e) => format!(""), + } +} + +fn resolve_trace_path(lib_name: &str) -> String { + let path = Path::new(lib_name); + let resolved: PathBuf = if path.is_absolute() { + path.to_path_buf() + } else { + std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")).join(path) + }; + + match resolved.canonicalize() { + Ok(p) => p.display().to_string(), + Err(_) => resolved.display().to_string(), + } +} + +fn trace_ffi_event(label: &str, message: impl AsRef) { + if should_trace_ffi() { + let event_id = TRACE_FFI_EVENT_ID.fetch_add(1, Ordering::Relaxed); + let elapsed_ms = TRACE_FFI_STARTED.elapsed().as_secs_f64() * 1000.0; + eprintln!( + "[ffi #{event_id} +{elapsed_ms:.3}ms pid={} tid={:?}] {label} {}", + process::id(), + thread::current().id(), + message.as_ref() + ); + } +} /// load dylib, cache it fn load_dylib(lib_name: &str) -> Result, CalcitErr> { + let resolved_path = resolve_trace_path(lib_name); let mut dylibs = DYLIBS .lock() .map_err(|_| CalcitErr::use_str(CalcitErrKind::Unexpected, "failed to lock dylib cache"))?; if let Some(lib) = dylibs.get(lib_name) { + trace_ffi_event("reuse-dylib", format!("lib={lib_name} resolved={resolved_path}")); return Ok(lib.to_owned()); } + trace_ffi_event("load-dylib", format!("lib={lib_name} resolved={resolved_path}")); let lib = unsafe { libloading::Library::new(lib_name) } .map_err(|e| CalcitErr::use_str(CalcitErrKind::Unexpected, format!("failed to load dylib `{lib_name}`: {e}")))?; let lib = Arc::new(lib); @@ -41,6 +107,7 @@ fn load_dylib(lib_name: &str) -> Result, CalcitErr> { } fn ensure_abi_compatible(lib: &libloading::Library, lib_name: &str) -> Result<(), CalcitErr> { + trace_ffi_event("lookup-abi", format!("lib={lib_name}")); let lookup_version: libloading::Symbol String> = unsafe { lib.get("abi_version".as_bytes()) }.map_err(|e| { CalcitErr::use_str( CalcitErrKind::Unexpected, @@ -48,6 +115,7 @@ fn ensure_abi_compatible(lib: &libloading::Library, lib_name: &str) -> Result<() ) })?; let current = lookup_version(); + trace_ffi_event("abi-version", format!("lib={lib_name} current={current} expected={ABI_VERSION}")); if current != ABI_VERSION { return CalcitErr::err_str(CalcitErrKind::Unexpected, format!("ABI versions mismatch: {current} {ABI_VERSION}")).map(|_| ()); } @@ -125,15 +193,33 @@ pub fn call_dylib_edn(xs: Vec, _call_stack: &CallStackList) -> Result = unsafe { lib.get(method.as_bytes()) }.map_err(|e| { CalcitErr::use_str( CalcitErrKind::Unexpected, format!("failed to load FFI symbol `{method}` in `{lib_name}`: {e}"), ) })?; - let ret = func(ys.to_owned())?; + let ret = func(ys.to_owned()).map_err(|e| { + trace_ffi_event("error", format!("lib={lib_name} symbol={method} {e}")); + e + })?; + trace_ffi_event( + "return", + format!( + "lib={lib_name} symbol={method} ret={}", + format_edn_args_for_trace(std::slice::from_ref(&ret)) + ), + ); Ok(edn_to_calcit(&ret, &Calcit::Nil)) } @@ -205,6 +291,14 @@ pub fn call_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Result< } track::track_task_add(); + trace_ffi_event("task-add", format!("kind=callback pending={}", track::count_pending_tasks())); + + trace_ffi_event("spawn-callback", format!( + "lib={lib_name} resolved={} symbol={method} argc={} args={}", + resolve_trace_path(&lib_name), + ys.len(), + format_edn_args_for_trace(&ys) + )); let lib = load_dylib(&lib_name)?; ensure_abi_compatible(&lib, &lib_name)?; @@ -213,10 +307,18 @@ pub fn call_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Result< let lib_name_for_thread = lib_name.clone(); let _handle = thread::spawn(move || { + trace_ffi_event( + "thread-start", + format!("lib={lib_name_for_thread} symbol={method_name} pending={}", track::count_pending_tasks()), + ); + let callback_method_name = method_name.clone(); + let callback_lib_name = lib_name_for_thread.clone(); + trace_ffi_event("lookup-symbol", format!("lib={lib_name_for_thread} symbol={method_name}")); let func: libloading::Symbol = match unsafe { lib.get(method_name.as_bytes()) } { Ok(f) => f, Err(e) => { track::track_task_release(); + trace_ffi_event("task-release", format!("kind=callback pending={}", track::count_pending_tasks())); return CalcitErr::err_str( CalcitErrKind::Unexpected, format!("failed to load FFI symbol `{method_name}` in `{lib_name_for_thread}`: {e}"), @@ -227,6 +329,14 @@ pub fn call_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Result< match func( ys.to_owned(), Arc::new(move |ps: Vec| -> Result { + trace_ffi_event( + "callback-in", + format!( + "lib={callback_lib_name} symbol={callback_method_name} argc={} args={}", + ps.len(), + format_edn_args_for_trace(&ps) + ), + ); if let Calcit::Fn { info, .. } = &callback { let mut real_args: Vec = vec![]; for p in ps { @@ -234,7 +344,14 @@ pub fn call_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Result< } let r = runner::run_fn(&real_args, info, &copied_stack); match r { - Ok(ret) => calcit_to_edn(&ret), + Ok(ret) => { + let ret_edn = calcit_to_edn(&ret)?; + trace_ffi_event("callback-out", format!( + "lib={callback_lib_name} symbol={callback_method_name} ret={}", + format_edn_args_for_trace(std::slice::from_ref(&ret_edn)) + )); + Ok(ret_edn) + } Err(e) => { display_stack(&format!("[Error] thread callback failed: {}", e.msg), &e.stack, e.location.as_ref())?; Err(format!("Error: {e}")) @@ -246,9 +363,17 @@ pub fn call_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Result< }), Arc::new(track::track_task_release), ) { - Ok(ret) => edn_to_calcit(&ret, &Calcit::Nil), + Ok(ret) => { + trace_ffi_event("return-callback", format!( + "lib={lib_name_for_thread} symbol={method_name} ret={}", + format_edn_args_for_trace(std::slice::from_ref(&ret)) + )); + edn_to_calcit(&ret, &Calcit::Nil) + } Err(e) => { track::track_task_release(); + trace_ffi_event("task-release", format!("kind=callback pending={}", track::count_pending_tasks())); + trace_ffi_event("error-callback", format!("lib={lib_name_for_thread} symbol={method_name} {e}")); // let _ = display_stack(&format!("failed to call request: {}", e), &copied_stack_1); eprintln!("failure inside ffi thread: {e}"); return CalcitErr::err_str(CalcitErrKind::Unexpected, e); @@ -304,11 +429,21 @@ pub fn blocking_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Res } track::track_task_add(); + trace_ffi_event("task-add", format!("kind=blocking pending={}", track::count_pending_tasks())); + + trace_ffi_event("blocking-call", format!( + "lib={lib_name} resolved={} symbol={method} argc={} args={}", + resolve_trace_path(&lib_name), + ys.len(), + format_edn_args_for_trace(&ys) + )); let lib = unsafe { libloading::Library::new(&lib_name) } .map_err(|e| CalcitErr::use_str(CalcitErrKind::Unexpected, format!("failed to load dylib `{lib_name}`: {e}")))?; ensure_abi_compatible(&lib, &lib_name)?; let copied_stack = Arc::new(call_stack.to_owned()); + let callback_method = method.clone(); + let callback_lib_name = lib_name.clone(); let func: libloading::Symbol = unsafe { lib.get(method.as_bytes()) }.map_err(|e| { CalcitErr::use_str( @@ -319,6 +454,14 @@ pub fn blocking_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Res match func( ys.to_owned(), Arc::new(move |ps: Vec| -> Result { + trace_ffi_event( + "blocking-callback-in", + format!( + "lib={callback_lib_name} symbol={callback_method} argc={} args={}", + ps.len(), + format_edn_args_for_trace(&ps) + ), + ); if let Calcit::Fn { info, .. } = &callback { let mut real_args: Vec = vec![]; for p in ps { @@ -326,7 +469,14 @@ pub fn blocking_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Res } let r = runner::run_fn(&real_args, info, &copied_stack); match r { - Ok(ret) => calcit_to_edn(&ret), + Ok(ret) => { + let ret_edn = calcit_to_edn(&ret)?; + trace_ffi_event("blocking-callback-out", format!( + "lib={callback_lib_name} symbol={callback_method} ret={}", + format_edn_args_for_trace(std::slice::from_ref(&ret_edn)) + )); + Ok(ret_edn) + } Err(e) => { display_stack(&format!("[Error] thread callback failed: {}", e.msg), &e.stack, e.location.as_ref())?; Err(format!("Error: {e}")) @@ -338,8 +488,15 @@ pub fn blocking_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Res }), Arc::new(track::track_task_release), ) { - Ok(ret) => edn_to_calcit(&ret, &Calcit::Nil), + Ok(ret) => { + trace_ffi_event("blocking-return", format!( + "lib={lib_name} symbol={method} ret={}", + format_edn_args_for_trace(std::slice::from_ref(&ret)) + )); + edn_to_calcit(&ret, &Calcit::Nil) + } Err(e) => { + trace_ffi_event("blocking-error", format!("lib={lib_name} symbol={method} {e}")); // TODO for more accurate tracking, need to place tracker inside foreign function // track::track_task_release(); let _ = display_stack(&format!("failed to call request: {e}"), call_stack, None); diff --git a/src/cli_args.rs b/src/cli_args.rs index 19fb4c24..429baacb 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -25,6 +25,9 @@ pub struct ToplevelCalcit { /// warn on dynamic method calls that cannot be monomorphized #[argh(switch)] pub warn_dyn_method: bool, + /// print FFI dylib calls and callbacks for debugging native crashes + #[argh(switch)] + pub trace_ffi: bool, /// entry file path, defaults to "js-out/" #[argh(option, default = "String::from(\"js-out/\")")] pub emit_path: String, From fea36dc8581d7f3afbae256f33c894571b062c6f Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 13 Mar 2026 16:13:01 +0800 Subject: [PATCH 09/57] check FFI cirru edn version; tag 0.12.9 --- Cargo.lock | 6 +- Cargo.toml | 6 +- package.json | 2 +- src/bin/injection/mod.rs | 123 +++++++++++++++++++++++++++------------ 4 files changed, 93 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30f86241..175c4c38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.8" +version = "0.12.9" dependencies = [ "argh", "bisection_key", @@ -155,9 +155,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "cirru_edn" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb334232574d3bbcef40d993d52f37b1e624bba09f750257078861f697cca567" +checksum = "f6f4f45b12dd75820a21e7209e7eae7d81f4aafa0bb0b04c547fe2609c12e0db" dependencies = [ "bincode", "cirru_parser", diff --git a/Cargo.toml b/Cargo.toml index d2d6712c..c642d4d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.8" +version = "0.12.9" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" @@ -32,14 +32,14 @@ strum = "0.25" strum_macros = "0.25" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -cirru_edn = "0.7.2" +cirru_edn = "0.7.3" cirru_parser = "0.2.3" bisection_key = "0.0.1" ureq = "3.2.0" rmp-serde = "1.3.0" [build-dependencies] -cirru_edn = "0.7.2" +cirru_edn = "0.7.3" rmp-serde = "1.3.0" serde = { version = "1.0", features = ["derive"] } cirru_parser = "0.2.3" diff --git a/package.json b/package.json index 4c5eebd8..168f1cb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.8", + "version": "0.12.9", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", diff --git a/src/bin/injection/mod.rs b/src/bin/injection/mod.rs index 9826930f..c1dd17a8 100644 --- a/src/bin/injection/mod.rs +++ b/src/bin/injection/mod.rs @@ -4,8 +4,8 @@ use colored::Colorize; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::process; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::AtomicUsize; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, LazyLock, Mutex}; use std::thread; use std::time::Instant; @@ -36,6 +36,7 @@ static TRACE_FFI_STARTED: LazyLock = LazyLock::new(Instant::now); pub fn set_trace_ffi(v: bool) { TRACE_FFI.store(v, Ordering::Relaxed); if v { + let edn_version = cirru_edn::version(); let cwd = std::env::current_dir() .map(|p| p.display().to_string()) .unwrap_or_else(|_| "".to_string()); @@ -44,7 +45,10 @@ pub fn set_trace_ffi(v: bool) { .unwrap_or_else(|_| "".to_string()); trace_ffi_event( "enable", - format!("cwd={cwd} exe={exe} abi={ABI_VERSION} host={}", std::env::consts::OS), + format!( + "cwd={cwd} exe={exe} abi={ABI_VERSION} edn={edn_version} host={}", + std::env::consts::OS + ), ); } } @@ -107,6 +111,7 @@ fn load_dylib(lib_name: &str) -> Result, CalcitErr> { } fn ensure_abi_compatible(lib: &libloading::Library, lib_name: &str) -> Result<(), CalcitErr> { + let expected_edn_version = cirru_edn::version(); trace_ffi_event("lookup-abi", format!("lib={lib_name}")); let lookup_version: libloading::Symbol String> = unsafe { lib.get("abi_version".as_bytes()) }.map_err(|e| { CalcitErr::use_str( @@ -119,6 +124,26 @@ fn ensure_abi_compatible(lib: &libloading::Library, lib_name: &str) -> Result<() if current != ABI_VERSION { return CalcitErr::err_str(CalcitErrKind::Unexpected, format!("ABI versions mismatch: {current} {ABI_VERSION}")).map(|_| ()); } + + trace_ffi_event("lookup-edn-version", format!("lib={lib_name}")); + let lookup_edn_version: libloading::Symbol String> = unsafe { lib.get("edn_version".as_bytes()) }.map_err(|e| { + CalcitErr::use_str( + CalcitErrKind::Unexpected, + format!("failed to lookup `edn_version` in `{lib_name}`: {e}"), + ) + })?; + let current_edn = lookup_edn_version(); + trace_ffi_event( + "edn-version", + format!("lib={lib_name} current={current_edn} expected={expected_edn_version}"), + ); + if current_edn != expected_edn_version { + return CalcitErr::err_str( + CalcitErrKind::Unexpected, + format!("cirru_edn versions mismatch: {current_edn} {expected_edn_version}"), + ) + .map(|_| ()); + } Ok(()) } @@ -193,12 +218,15 @@ pub fn call_dylib_edn(xs: Vec, _call_stack: &CallStackList) -> Result, call_stack: &CallStackList) -> Result< track::track_task_add(); trace_ffi_event("task-add", format!("kind=callback pending={}", track::count_pending_tasks())); - trace_ffi_event("spawn-callback", format!( - "lib={lib_name} resolved={} symbol={method} argc={} args={}", - resolve_trace_path(&lib_name), - ys.len(), - format_edn_args_for_trace(&ys) - )); + trace_ffi_event( + "spawn-callback", + format!( + "lib={lib_name} resolved={} symbol={method} argc={} args={}", + resolve_trace_path(&lib_name), + ys.len(), + format_edn_args_for_trace(&ys) + ), + ); let lib = load_dylib(&lib_name)?; ensure_abi_compatible(&lib, &lib_name)?; @@ -309,7 +340,10 @@ pub fn call_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Result< let _handle = thread::spawn(move || { trace_ffi_event( "thread-start", - format!("lib={lib_name_for_thread} symbol={method_name} pending={}", track::count_pending_tasks()), + format!( + "lib={lib_name_for_thread} symbol={method_name} pending={}", + track::count_pending_tasks() + ), ); let callback_method_name = method_name.clone(); let callback_lib_name = lib_name_for_thread.clone(); @@ -346,10 +380,13 @@ pub fn call_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Result< match r { Ok(ret) => { let ret_edn = calcit_to_edn(&ret)?; - trace_ffi_event("callback-out", format!( - "lib={callback_lib_name} symbol={callback_method_name} ret={}", - format_edn_args_for_trace(std::slice::from_ref(&ret_edn)) - )); + trace_ffi_event( + "callback-out", + format!( + "lib={callback_lib_name} symbol={callback_method_name} ret={}", + format_edn_args_for_trace(std::slice::from_ref(&ret_edn)) + ), + ); Ok(ret_edn) } Err(e) => { @@ -364,10 +401,13 @@ pub fn call_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Result< Arc::new(track::track_task_release), ) { Ok(ret) => { - trace_ffi_event("return-callback", format!( - "lib={lib_name_for_thread} symbol={method_name} ret={}", - format_edn_args_for_trace(std::slice::from_ref(&ret)) - )); + trace_ffi_event( + "return-callback", + format!( + "lib={lib_name_for_thread} symbol={method_name} ret={}", + format_edn_args_for_trace(std::slice::from_ref(&ret)) + ), + ); edn_to_calcit(&ret, &Calcit::Nil) } Err(e) => { @@ -431,12 +471,15 @@ pub fn blocking_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Res track::track_task_add(); trace_ffi_event("task-add", format!("kind=blocking pending={}", track::count_pending_tasks())); - trace_ffi_event("blocking-call", format!( - "lib={lib_name} resolved={} symbol={method} argc={} args={}", - resolve_trace_path(&lib_name), - ys.len(), - format_edn_args_for_trace(&ys) - )); + trace_ffi_event( + "blocking-call", + format!( + "lib={lib_name} resolved={} symbol={method} argc={} args={}", + resolve_trace_path(&lib_name), + ys.len(), + format_edn_args_for_trace(&ys) + ), + ); let lib = unsafe { libloading::Library::new(&lib_name) } .map_err(|e| CalcitErr::use_str(CalcitErrKind::Unexpected, format!("failed to load dylib `{lib_name}`: {e}")))?; @@ -471,10 +514,13 @@ pub fn blocking_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Res match r { Ok(ret) => { let ret_edn = calcit_to_edn(&ret)?; - trace_ffi_event("blocking-callback-out", format!( - "lib={callback_lib_name} symbol={callback_method} ret={}", - format_edn_args_for_trace(std::slice::from_ref(&ret_edn)) - )); + trace_ffi_event( + "blocking-callback-out", + format!( + "lib={callback_lib_name} symbol={callback_method} ret={}", + format_edn_args_for_trace(std::slice::from_ref(&ret_edn)) + ), + ); Ok(ret_edn) } Err(e) => { @@ -489,10 +535,13 @@ pub fn blocking_dylib_edn_fn(xs: Vec, call_stack: &CallStackList) -> Res Arc::new(track::track_task_release), ) { Ok(ret) => { - trace_ffi_event("blocking-return", format!( - "lib={lib_name} symbol={method} ret={}", - format_edn_args_for_trace(std::slice::from_ref(&ret)) - )); + trace_ffi_event( + "blocking-return", + format!( + "lib={lib_name} symbol={method} ret={}", + format_edn_args_for_trace(std::slice::from_ref(&ret)) + ), + ); edn_to_calcit(&ret, &Calcit::Nil) } Err(e) => { From eb2bac6563405efdb6d1107476433bd5d51cedc9 Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 13 Mar 2026 22:17:08 +0800 Subject: [PATCH 10/57] fix: validate applied struct type arg arity --- calcit/test-generics.cirru | 2 +- docs/CalcitAgent.md | 38 +++--- ...313-2216-validate-struct-type-arg-arity.md | 24 ++++ src/bin/cr_tests/type_fail.rs | 1 + src/builtins/records.rs | 8 ++ src/calcit/sum_type.rs | 29 ++++- src/calcit/type_annotation.rs | 121 ++++++++++++++++++ 7 files changed, 202 insertions(+), 21 deletions(-) create mode 100644 editing-history/2026-0313-2216-validate-struct-type-arg-arity.md diff --git a/calcit/test-generics.cirru b/calcit/test-generics.cirru index 75216b04..825601e8 100644 --- a/calcit/test-generics.cirru +++ b/calcit/test-generics.cirru @@ -21,7 +21,7 @@ |Wrapped $ %{} :CodeEntry (:doc |) (:schema nil) :code $ quote defenum Wrapped - :pair $ :: Pair :number :string + :pair Pair :none :examples $ [] |main! $ %{} :CodeEntry (:doc |) (:schema nil) diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index ee6214fa..99d5acd2 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -662,7 +662,7 @@ let 有两种方式标注函数返回类型: - **紧凑模式(推荐)**:紧跟在参数列表后的类型标签。 -- **正式模式**:使用 `hint-fn`(通常放在函数体开头)。 +- **正式模式**:局部 `fn` 使用 `hint-fn`(通常放在函数体开头);顶层 `defn` / `defmacro` 使用 `:schema`。 - 泛型变量:`hint-fn $ {} (:generics $ [] 'T 'S)` - 旧 clause 写法(如 `(hint-fn (return-type ...))` / `(generics ...)` / `(type-vars ...)`)已不再支持,会直接报错。 @@ -727,18 +727,18 @@ cr eval 'foldl (list 1 2 3) 0 &+' let ; 可选参数 greet $ fn (name) - assert-type name $ :: :optional :string + hint-fn $ {} (:args $ [] (:: :optional :string)) (:return :string) str "|Hello " (or name "|Guest") ; 变长参数 sum $ fn (& xs) - assert-type xs $ :: :& :number + hint-fn $ {} (:rest :number) (:return :number) reduce xs 0 &+ ; Record 约束 (使用 defstruct 定义结构体) User $ defstruct User (:name :string) get-name $ fn (u) - assert-type u User + hint-fn $ {} (:args $ [] (:: :record User)) (:return :string) get u :name println $ greet |Alice println $ sum 1 2 3 @@ -794,6 +794,8 @@ let - `:return :string` 对应整个 `join-str` 的返回值 - 内部辅助函数 `%join-str` 不是顶层定义,所以继续用 `hint-fn` +可以简单记忆为:**namespace 上的定义看 `:schema`,函数体内部的辅助函数看 `hint-fn`。** + 推荐工作流: ```bash @@ -1456,20 +1458,20 @@ cr eval 'let ((x 1)) (+ x 2)' ### 错误信息对照表 -| 错误信息 | 原因 | 解决方法 | -| ------------------------------------------------ | ------------------------------------------------- | -------------------------------------------------------------- | -| `Path index X out of bounds` | 路径索引已过期(操作后变化) | 重新运行 `cr query search` 获取最新路径 | -| `tag-match expected tuple` | 传入 vector 而非 tuple | 改用 `::` 语法,如 `:: :event-name data` | -| `unknown symbol: xxx` | 符号未定义或未 import | `cr query find xxx` 确认位置,`cr edit add-import` 引入 | -| `expects pairs in list for let` | `let` 绑定语法错误 | 改为 `let ((x val)) body`(双层括号) | -| `cannot be used as operator` | 末尾符号被当作函数调用 | 改用 `, acc` 前缀传递值,或用函数包裹 | -| `unknown data for foldl-shortcut` | 参数顺序错误(Calcit vs Clojure 差异) | Calcit 集合在第一位:`map data fn` | -| `Do not include ':require' as prefix` | `cr edit imports` 格式错误 | 去掉 `:require` 前缀,直接传 `src-ns :refer $ sym` | -| `Namespace name mismatch` | `add-ns -e` 名称不一致 | ns 表达式名称必须与位置参数完全一致 | -| 字符串被拆分成多个 token | 没有用 `\|` 或 `"` 包裹 | 使用 `\|complete string` 或 `"complete string` | -| `unexpected format` | Cirru 语法错误 | 用 `cr cirru parse ''` 验证语法 | -| `Type warning` 导致 eval 失败 | 类型不匹配(阻断执行) | 检查参数类型标注,或用 `assert-type` 确认预期类型 | -| `schema mismatch while preprocessing definition` | `:schema` 与 `defn` / `defmacro` / 参数个数不一致 | 修正 `:kind`、`:args`、`:rest`,或让代码定义与 schema 保持一致 | +| 错误信息 | 原因 | 解决方法 | +| ------------------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------ | +| `Path index X out of bounds` | 路径索引已过期(操作后变化) | 重新运行 `cr query search` 获取最新路径 | +| `tag-match expected tuple` | 传入 vector 而非 tuple | 改用 `::` 语法,如 `:: :event-name data` | +| `unknown symbol: xxx` | 符号未定义或未 import | `cr query find xxx` 确认位置,`cr edit add-import` 引入 | +| `expects pairs in list for let` | `let` 绑定语法错误 | 改为 `let ((x val)) body`(双层括号) | +| `cannot be used as operator` | 末尾符号被当作函数调用 | 改用 `, acc` 前缀传递值,或用函数包裹 | +| `unknown data for foldl-shortcut` | 参数顺序错误(Calcit vs Clojure 差异) | Calcit 集合在第一位:`map data fn` | +| `Do not include ':require' as prefix` | `cr edit imports` 格式错误 | 去掉 `:require` 前缀,直接传 `src-ns :refer $ sym` | +| `Namespace name mismatch` | `add-ns -e` 名称不一致 | ns 表达式名称必须与位置参数完全一致 | +| 字符串被拆分成多个 token | 没有用 `\|` 或 `"` 包裹 | 使用 `\|complete string` 或 `"complete string` | +| `unexpected format` | Cirru 语法错误 | 用 `cr cirru parse ''` 验证语法 | +| `Type warning` 导致 eval 失败 | 类型不匹配(阻断执行) | 优先检查 `:schema` / `hint-fn` 的参数标注;局部值再用 `assert-type` 复核 | +| `schema mismatch while preprocessing definition` | `:schema` 与 `defn` / `defmacro` / 参数个数不一致 | 修正 `:kind`、`:args`、`:rest`,或让代码定义与 schema 保持一致 | ### 调试常用命令 diff --git a/editing-history/2026-0313-2216-validate-struct-type-arg-arity.md b/editing-history/2026-0313-2216-validate-struct-type-arg-arity.md new file mode 100644 index 00000000..c9b2dbb0 --- /dev/null +++ b/editing-history/2026-0313-2216-validate-struct-type-arg-arity.md @@ -0,0 +1,24 @@ +# 2026-03-13 22:16 validate struct type arg arity + +## 背景 + +发现类型注解里可以写出 `:: Pair :number :string` 这样的形式,即使 `Pair` 是非泛型 `defstruct`。旧逻辑会把后续类型参数直接附着到 `Struct(...)` 注解上,但并不会校验 `generics` 个数,最终在值匹配时只看 struct 名字,导致漏检。 + +## 本次修改 + +- 在 `src/calcit/type_annotation.rs` 为 `CalcitTypeAnnotation` / `CalcitFnTypeAnnotation` 增加 `validate_applied_type_args()`。 +- 对 `Struct` 注解增加规则: + - 非泛型 struct 不允许携带类型参数; + - 泛型 struct 必须严格匹配声明的类型参数个数。 +- 对 `Enum` 注解增加保守规则:当前不接受额外类型参数。 +- 在 `src/builtins/records.rs` 中,`defstruct` 字段类型解析后立即做校验。 +- 在 `src/calcit/sum_type.rs` 中,`defenum` payload 类型解析后立即做校验。 +- 在 `src/calcit/sum_type.rs` 与 `src/calcit/type_annotation.rs` 补充回归测试,覆盖: + - 非泛型 struct 带类型参数时报错; + - 泛型 struct 类型参数个数不匹配时报错; + - enum payload 中引用非法 struct 类型注解时报错。 +- 同时修正 `calcit/test-generics.cirru` 中 `Wrapped` 的 payload 类型,把错误的 `:: Pair :number :string` 改为 `Pair`。 + +## 结论 + +现在像 `:: Pair :number :string` 这种写法不会再静默通过;定义期就会被校验出来,避免文档和测试继续误导后续修改。 diff --git a/src/bin/cr_tests/type_fail.rs b/src/bin/cr_tests/type_fail.rs index 2f84e308..c0179265 100644 --- a/src/bin/cr_tests/type_fail.rs +++ b/src/bin/cr_tests/type_fail.rs @@ -87,6 +87,7 @@ fn type_fail_schema_mismatch_fixtures_report_error_code() { } } + #[test] fn type_fail_call_arg_fixture_reports_warning_code() { let _guard = lock_fixture_tests(); diff --git a/src/builtins/records.rs b/src/builtins/records.rs index 2688ace7..e302a826 100644 --- a/src/builtins/records.rs +++ b/src/builtins/records.rs @@ -190,6 +190,14 @@ pub fn new_struct(xs: &[Calcit]) -> Result { } }; let field_type = CalcitTypeAnnotation::parse_type_annotation_form_with_generics(type_expr, generics.as_slice()); + if let Err(e) = field_type.validate_applied_type_args() { + let hint = format_proc_examples_hint(&CalcitProc::NativeStructNew).unwrap_or_default(); + return CalcitErr::err_str_with_hint( + CalcitErrKind::Type, + format!("&struct::new field `{field_name}` has invalid type annotation: {e}"), + hint, + ); + } fields.push((field_name, field_type)); } (Some(_), None, _) => { diff --git a/src/calcit/sum_type.rs b/src/calcit/sum_type.rs index 25058379..5af0ae69 100644 --- a/src/calcit/sum_type.rs +++ b/src/calcit/sum_type.rs @@ -137,7 +137,11 @@ impl CalcitEnum { Calcit::List(items) => { let mut payloads: Vec> = Vec::with_capacity(items.len()); for item in items.iter() { - payloads.push(CalcitTypeAnnotation::parse_type_annotation_form(item)); + let parsed = CalcitTypeAnnotation::parse_type_annotation_form(item); + parsed + .validate_applied_type_args() + .map_err(|e| format!("enum variant `{tag}` has invalid payload type annotation: {e}"))?; + payloads.push(parsed); } Ok(payloads) } @@ -152,7 +156,7 @@ impl CalcitEnum { #[cfg(test)] mod tests { use super::*; - use crate::calcit::{CalcitList, CalcitStruct, CalcitTypeAnnotation}; + use crate::calcit::{CalcitList, CalcitStruct, CalcitTuple, CalcitTypeAnnotation}; fn empty_list() -> Calcit { Calcit::List(Arc::new(CalcitList::Vector(vec![]))) @@ -186,4 +190,25 @@ mod tests { } assert_eq!(enum_proto.find_variant_by_name("ok").unwrap().arity(), 0); } + + #[test] + fn rejects_non_generic_struct_type_args_in_payloads() { + let pair = CalcitStruct::from_fields(EdnTag::new("Pair"), vec![EdnTag::new("left"), EdnTag::new("right")]); + let applied_pair = Calcit::Tuple(CalcitTuple { + tag: Arc::new(Calcit::Struct(pair)), + extra: vec![Calcit::tag("number"), Calcit::tag("string")], + sum_type: None, + }); + let record = CalcitRecord { + struct_ref: Arc::new(CalcitStruct::from_fields(EdnTag::new("Wrapped"), vec![EdnTag::new("pair")])), + values: Arc::new(vec![list_from(vec![applied_pair])]), + }; + + let err = CalcitEnum::from_record(record).expect_err("non-generic struct should reject type args in enum payloads"); + assert!( + err.contains("enum variant `pair` has invalid payload type annotation") + && err.contains("struct `Pair` is not generic but received 2 type argument(s)"), + "unexpected error: {err}" + ); + } } diff --git a/src/calcit/type_annotation.rs b/src/calcit/type_annotation.rs index eda25ce2..6f15b8ca 100644 --- a/src/calcit/type_annotation.rs +++ b/src/calcit/type_annotation.rs @@ -143,6 +143,76 @@ pub enum CalcitTypeAnnotation { } impl CalcitTypeAnnotation { + pub(crate) fn validate_applied_type_args(&self) -> Result<(), String> { + match self { + Self::List(inner) | Self::Set(inner) | Self::Ref(inner) | Self::Variadic(inner) | Self::Optional(inner) => { + inner.validate_applied_type_args() + } + Self::Map(key, value) => { + key.validate_applied_type_args()?; + value.validate_applied_type_args() + } + Self::Fn(signature) => signature.validate_applied_type_args(), + Self::Struct(base, args) => { + for arg in args.iter() { + arg.validate_applied_type_args()?; + } + + let expected = base.generics.len(); + let actual = args.len(); + if expected == 0 { + if actual > 0 { + return Err(format!( + "struct `{}` is not generic but received {actual} type argument(s)", + base.name + )); + } + } else if actual != expected { + return Err(format!( + "struct `{}` expects {expected} type argument(s), but received {actual}", + base.name + )); + } + + Ok(()) + } + Self::Enum(enum_def, args) => { + for arg in args.iter() { + arg.validate_applied_type_args()?; + } + + if !args.is_empty() { + return Err(format!( + "enum `{}` is not generic but received {} type argument(s)", + enum_def.name(), + args.len() + )); + } + + Ok(()) + } + Self::TypeRef(_, args) => { + for arg in args.iter() { + arg.validate_applied_type_args()?; + } + Ok(()) + } + Self::Record(_) | Self::Tuple(_) | Self::Trait(_) | Self::TraitSet(_) | Self::Custom(_) => Ok(()), + Self::Bool + | Self::Number + | Self::String + | Self::Symbol + | Self::Tag + | Self::DynTuple + | Self::DynFn + | Self::Buffer + | Self::CirruQuote + | Self::Dynamic + | Self::TypeVar(_) + | Self::Unit => Ok(()), + } + } + fn custom_keyword_matches(custom: &Calcit, keyword: &str) -> bool { match custom { Calcit::Tag(tag) => tag.ref_str().trim_start_matches(':') == keyword, @@ -2545,6 +2615,46 @@ mod tests { CalcitTypeAnnotation::Dynamic )); } + + #[test] + fn rejects_type_args_on_non_generic_struct_annotation() { + let pair = CalcitStruct { + name: EdnTag::new("Pair"), + fields: Arc::new(vec![]), + field_types: Arc::new(vec![]), + generics: Arc::new(vec![]), + impls: vec![], + }; + let annotation = CalcitTypeAnnotation::Struct( + Arc::new(pair), + Arc::new(vec![Arc::new(CalcitTypeAnnotation::Number), Arc::new(CalcitTypeAnnotation::String)]), + ); + + let err = annotation + .validate_applied_type_args() + .expect_err("non-generic struct should reject type args"); + assert!(err.contains("struct `Pair` is not generic"), "unexpected error: {err}"); + } + + #[test] + fn rejects_wrong_arity_on_generic_struct_annotation() { + let pair = CalcitStruct { + name: EdnTag::new("Pair"), + fields: Arc::new(vec![]), + field_types: Arc::new(vec![]), + generics: Arc::new(vec![Arc::from("A"), Arc::from("B")]), + impls: vec![], + }; + let annotation = CalcitTypeAnnotation::Struct(Arc::new(pair), Arc::new(vec![Arc::new(CalcitTypeAnnotation::Number)])); + + let err = annotation + .validate_applied_type_args() + .expect_err("generic struct should enforce arity"); + assert!( + err.contains("expects 2 type argument(s), but received 1"), + "unexpected error: {err}" + ); + } } impl fmt::Display for CalcitTypeAnnotation { @@ -2715,6 +2825,17 @@ pub struct CalcitFnTypeAnnotation { } impl CalcitFnTypeAnnotation { + pub(crate) fn validate_applied_type_args(&self) -> Result<(), String> { + for arg in &self.arg_types { + arg.validate_applied_type_args()?; + } + self.return_type.validate_applied_type_args()?; + if let Some(rest) = &self.rest_type { + rest.validate_applied_type_args()?; + } + Ok(()) + } + fn to_inline_type_schema_edn(&self) -> Edn { let args: Vec = self.arg_types.iter().map(|t| t.to_type_edn()).collect(); let mut map = EdnMapView::default(); From 0d826ea110497edba494b9667d5bcca2e2a929d9 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 15 Mar 2026 13:56:36 +0800 Subject: [PATCH 11/57] updates on feedback --- Agents.md | 6 +++- docs/CalcitAgent.md | 54 +++++++++++++++++++++++++++++++---- src/bin/cli_handlers/edit.rs | 8 ++++-- src/bin/cli_handlers/query.rs | 10 +++++++ src/bin/cli_handlers/tips.rs | 5 +++- src/bin/cr_tests/type_fail.rs | 1 - 6 files changed, 74 insertions(+), 10 deletions(-) diff --git a/Agents.md b/Agents.md index 24d1e70d..bb4ab338 100644 --- a/Agents.md +++ b/Agents.md @@ -13,12 +13,16 @@ - **一致性**:复用现有模式,保持日志和错误信息风格统一。 - **测试覆盖**:新功能必须补齐正常路径与异常分支的测试用例。 -直接使用命令修改 calcit 程序时不需要调用 cargo, 直接按照文档给出的命令行示例执行即可: +直接使用命令修改 calcit 程序时不需要调用 cargo, 直接按照文档给出的命令行示例执行即可。 + +在开始任何 `cr edit` / `cr tree` 修改前,先把下面这条命令当作**硬前置步骤**执行一遍,而不是可选建议: ```bash cr docs agents --full ``` +未先阅读最新 Agent 指南时,不要直接开始改 `compact.cirru`,避免沿用过时心智模型误判命令边界。 + ### 运行模式更新(cr / js / ir) - `cr `、`cr js`、`cr ir` 现在默认都是**单次执行**(once)。 diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index 99d5acd2..095c796d 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -4,6 +4,10 @@ ## 🚀 快速开始(新 LLM 必读) +**硬前置步骤:在执行任何 `cr edit` / `cr tree` 修改前,必须先运行一次 `cr docs agents --full`。** + +这不是建议项,而是进入实际修改前的检查项。跳过这一步,往往会直接沿用旧用法假设,尤其容易误判 `cr tree replace -p ''`、imports 输入格式和 watcher 验收边界。 + **核心原则:用命令行工具(不要直接编辑文件),用 search 定位(比逐层导航快 10 倍)** ### 标准流程 @@ -274,6 +278,7 @@ cr query modules **核心概念:** - 路径格式:逗号分隔的索引(如 `"3,2,1"`),空字符串 `""` 表示根节点 +- `-p ''` 仅表示“根节点”,**不等于推荐的整定义重写方案**;要整体替换定义时,优先使用 `cr edit def --overwrite -f ` - 每个命令都有 `--help` 查看详细参数 - 命令执行后会显示 "Next steps" 提示下一步操作 @@ -284,6 +289,7 @@ cr query modules - `-j` / `--json`:同时输出 JSON 格式(用于程序化处理) - 推荐:直接查看 Cirru 格式即可,通常不需要 JSON - `cr tree replace` - 替换节点 + - 适合局部节点替换;若目标是**整条定义**,优先改用 `cr edit def --overwrite -f `,比 `cr tree replace -p ''` 更可预期 - `cr tree replace-leaf` - 查找并替换所有匹配的 leaf 节点(无需指定路径) - `--pattern ` - 要搜索的模式(精确匹配 leaf 节点) - 使用 `-e, -f, -j` 等通用参数提供替换内容 @@ -313,6 +319,12 @@ cr query modules 简单更新尽量用结构化的 API 操作. 多行或者带特殊符号的表达式, 可以在 `.calcit-snippets/` 创建临时文件, 然后用 `cr cirru parse` 验证语法, 最后用 `-f ` 提交, 从而减少错误率. 复杂表达式建议分段, 然后搭配 `cr tree target-replace` 命令来完成多阶段提交. +**整体替换定义的经验规则:** + +- 局部节点修改:继续使用 `cr tree replace -p ''` +- 整条定义重写:优先使用 `cr edit def --overwrite -f ` +- 只有在你明确知道根节点替换后的结构,并且能立刻验证完整定义时,才考虑 `cr tree replace -p ''` + **推荐工作流(高效定位 ⭐⭐⭐):** ```bash @@ -373,6 +385,7 @@ cr tree replace namespace/def -p '3,2,2,5,2,4,1,2' -e 'let ((x 1)) (+ x task)' - **路径格式**:`"3,2,1"` 表示第3个子节点 → 第2个子节点 → 第1个子节点 - **批量修改自动提示**:搜索找到多处时,自动显示路径排序和批量替换命令 - **路径动态变化**:删除/插入后,同级后续索引会变化,按提示从后往前操作 +- **批量执行不要用 `&&` 粘成一行**:尤其当 `-e` 内容里有引号、`|` 字符串或复杂表达式时,优先逐条执行,或写入 `-f ` 避免 shell 进入未闭合引号状态 - 所有命令都会显示 Next steps 和操作提示 **结构化变更示例:** @@ -502,6 +515,8 @@ cr tree replace namespace/def -p '3,2,2,5,2,4,1,2' -e 'let ((x 1)) (+ x task)' - 适用:普通 `compact.cirru` / 项目 snapshot 文件 - 不适用:calcit-editor 专用的 `calcit.cirru` 结构文件 - `cr edit def ` - 添加新定义(默认若已存在会报错;加 `--overwrite` 可强制覆盖) + - 经验语义:**不带 `--overwrite` = create-only;带 `--overwrite` = replace existing definition** + - 若当前输出文案仍显示 `Created definition`,以你的调用方式和目标是否已存在为准理解,不要把该提示字面理解为“必然新增成功” - `cr edit rename ` - 在当前命名空间内重命名定义(不可覆盖) - `cr edit mv-def ` - 将定义移动到另一个命名空间(跨命名空间移动) - `cr edit cp --from -p [--at ]` - 在定义内复制 AST 节点到另一位置 @@ -718,7 +733,7 @@ cr eval 'foldl (list 1 2 3) 0 &+' #### 4. 复杂类型标注 - **可选类型**:`:: :optional :string` (可以是 string 或 nil) -- **变长参数**:`:: :& :number` (参数列表剩余部分均为 number) +- **变长参数**:在 Schema 中使用 `:rest :number` (参数列表剩余部分均为 number) - **结构体/枚举**:使用 `defstruct` 或 `defenum` 定义的名字 验证示例 (使用 `let` 封装多表达式以支持 `cr eval` 验证): @@ -904,6 +919,8 @@ cr edit inc \ cr query error # 命令会显示详细的错误信息或成功状态 ``` +`cr query error` 只能告诉你最近一次 Calcit 语义链路里有没有报错,例如解析、预处理、运行期异常;它**不能**证明浏览器 CSS、HTML 属性值、业务数据内容或外部系统配置是“合理的”。像 `|max(...)` 被误写成 `"|max(...)` 这类在 Cirru 层面仍合法的字符串,就可能通过 `cr query error`,但在浏览器渲染阶段失效。 + **何时使用全量操作:** ```bash @@ -937,7 +954,7 @@ cr # 或 cr js - `cr query usages ` - 查找定义的使用位置 - `cr query search [-f ]` - 搜索叶子节点 - `cr query search-expr [-f ]` - 搜索结构表达式 -- `cr query error` - 查看最近的错误堆栈 +- `cr query error` - 查看最近的错误堆栈(仅覆盖 Calcit 语义与运行链路,不覆盖 CSS/DOM/业务值合理性) --- @@ -956,8 +973,8 @@ cr edit def app.core/multiply -e 'defn multiply (x y) (* x y)' # 添加新函数(命令会提示 Next steps) cr edit def 'app.core/multiply' -e 'defn multiply (x y) (* x y)' -# 替换整个定义(-p '' 表示根路径) -cr tree replace 'app.core/multiply' -p '' -e 'defn multiply (x y z) (* x y z)' +# 替换整个定义(推荐用 overwrite,避免依赖根路径替换) +cr edit def 'app.core/multiply' --overwrite -f /tmp/multiply.cirru # 更新文档和示例 cr edit doc 'app.core/multiply' '乘法函数,返回两个数的积' @@ -981,6 +998,7 @@ cr tree replace 'ns/def' -p '' --leaf -e '' # 4. 检查结果 cr query error +# 若改动涉及 CSS / DOM / 浏览器行为,继续做实际渲染验证,不要把 query error 当最终验收 # 添加命名空间(推荐:先创建空 ns,再逐条 add-import) cr edit add-ns app.util cr edit add-import app.util -e 'calcit.core :refer $ echo' @@ -1122,10 +1140,20 @@ cr tree replace-leaf 'app.core/process' --pattern 'old-var' -e 'new-var' --leaf - **从后往前操作**(推荐):先删大索引,再删小索引 - **单次操作后重新搜索**:每次修改立即用 `cr query search` 更新路径 -- **整体重写**:用 `cr tree replace -p ''` 替换整个定义 +- **整体重写**:优先用 `cr edit def --overwrite -f `;`cr tree replace -p ''` 只保留给明确需要根节点级别改写的场景 命令会在路径错误时提示最长有效路径和可用子节点。 +### 1.5 根路径整体替换的边界 ⭐⭐⭐ + +`cr tree replace -p ''` 在语义上确实是替换根节点,但在实际操作里,它更像“根 AST 节点替换”,而不是“整条定义安全重写”。当你需要完整替换一个定义体时: + +- 更推荐 `cr edit def --overwrite -f ` +- 先在文件里组织完整定义,再一次性覆盖,验证也更直接 +- 如果你已经用 `-p ''` 替换成功,仍应立刻执行 `cr query def ` 或完整运行,确认写回后的定义结构符合预期 + +经验上,`-p ''` 更适合你已经非常确定根节点结构时的精细 AST 操作,不适合作为默认“全量改写定义”的模板。 + ### 2. 输入格式参数使用速查 ⭐⭐⭐ **参数混淆矩阵(已全面支持 `-e` 自动识别):** @@ -1315,6 +1343,19 @@ cr eval 'thread-first x (+ 1) (* 2)' # 用 thread-first 代替 -> **建议:** 命令行中优先使用英文名称(`thread-first` 而非 `->`),更清晰且无需转义。 +### 8. 多命令 `&&` 链式调用风险 ⭐⭐⭐ + +把多个 `cr tree replace`、`cr edit def -e ...` 或其他带内联代码的命令用 `&&` 串起来,在 bash/zsh 中风险很高: + +- 只要某一段 `-e` 内容里出现未正确转义的引号,shell 就会进入“继续等待补全输入”的状态,看起来像终端卡死 +- 前一条命令如果已经改写了内容,后一条命令即使没执行,你也可能以为整批操作已完成 + +更稳妥的做法: + +- 批量修改时逐条执行 +- 多行或含引号内容改用 `-f ` +- 需要批量脚本化时,放到独立 shell script,并先用最小样例验证 quoting + --- ## 🔄 完整功能开发示例 @@ -1398,6 +1439,8 @@ cr query error cr --check-only ``` +如果这次改动涉及样式、浏览器属性、字符串模板或外部接口,`cr query error` 和 `cr --check-only` 通过后,仍要继续做目标环境里的真实验收。 + ### 常见失误快速修复 ```bash @@ -1472,6 +1515,7 @@ cr eval 'let ((x 1)) (+ x 2)' | `unexpected format` | Cirru 语法错误 | 用 `cr cirru parse ''` 验证语法 | | `Type warning` 导致 eval 失败 | 类型不匹配(阻断执行) | 优先检查 `:schema` / `hint-fn` 的参数标注;局部值再用 `assert-type` 复核 | | `schema mismatch while preprocessing definition` | `:schema` 与 `defn` / `defmacro` / 参数个数不一致 | 修正 `:kind`、`:args`、`:rest`,或让代码定义与 schema 保持一致 | +| `cr query error` 无报错但页面仍异常 | 问题不在 Calcit 语义链路,而在 CSS/DOM/业务值 | 到真实运行环境核对渲染结果、属性值和外部依赖,而不是只看 `query error` | ### 调试常用命令 diff --git a/src/bin/cli_handlers/edit.rs b/src/bin/cli_handlers/edit.rs index 92819afe..c8f11b85 100644 --- a/src/bin/cli_handlers/edit.rs +++ b/src/bin/cli_handlers/edit.rs @@ -159,7 +159,7 @@ fn handle_def(opts: &EditDefCommand, snapshot_file: &str) -> Result<(), String> if exists && !opts.overwrite { return Err(format!( "Definition '{definition}' already exists in namespace '{namespace}'.\n\ - Use --overwrite to replace it, or use: cr tree replace {namespace}/{definition} -p '' -e ''" + Use --overwrite to replace it. For full-definition rewrites, prefer: cr edit def {namespace}/{definition} --overwrite -f " )); } @@ -169,15 +169,19 @@ fn handle_def(opts: &EditDefCommand, snapshot_file: &str) -> Result<(), String> save_snapshot(&snapshot, snapshot_file)?; + let action_label = if exists { "Updated" } else { "Created" }; + println!( - "{} Created definition '{}' in namespace '{}'", + "{} {} definition '{}' in namespace '{}'", "✓".green(), + action_label, definition.cyan(), namespace ); println!(); println!("{}", "Next steps:".blue().bold()); println!(" • View definition: {} '{}/{}'", "cr query def".cyan(), namespace, definition); + println!(" • Check errors: {}", "cr query error".cyan()); println!(" • Find usages: {} '{}/{}'", "cr query usages".cyan(), namespace, definition); println!( " • Add to imports: {} '{}' --refer '{}'", diff --git a/src/bin/cli_handlers/query.rs b/src/bin/cli_handlers/query.rs index 1f69fa66..8e40f117 100644 --- a/src/bin/cli_handlers/query.rs +++ b/src/bin/cli_handlers/query.rs @@ -370,6 +370,11 @@ fn handle_error() -> Result<(), String> { println!("{}", "✓ Error file is empty (no recent errors).".green()); println!(); println!("{}", "Your code compiled successfully!".dimmed()); + println!( + "{}", + "Note: this only reflects recent Calcit parsing/preprocess/runtime status; still validate browser rendering, CSS values, and external side effects separately." + .dimmed() + ); } else { println!("{}", "Last error stack trace:".bold().red()); println!("{content}"); @@ -380,6 +385,11 @@ fn handle_error() -> Result<(), String> { println!(" • Find usages: {} ''", "cr query usages".cyan()); println!(); println!("{}", "Tip: After fixing, watcher will recompile automatically (~300ms).".dimmed()); + println!( + "{}", + "Note: even when this clears, non-Calcit issues like CSS strings, DOM behavior, and external integrations can still be wrong." + .dimmed() + ); } Ok(()) diff --git a/src/bin/cli_handlers/tips.rs b/src/bin/cli_handlers/tips.rs index fcc6fbcc..52439781 100644 --- a/src/bin/cli_handlers/tips.rs +++ b/src/bin/cli_handlers/tips.rs @@ -61,7 +61,10 @@ pub fn tip_prefer_oneliner_json(show_json: bool) -> Vec { /// Tip for discouraging root-level edits when path is empty during editing pub fn tip_root_edit(path_is_empty: bool) -> Option { if path_is_empty { - Some("Editing root path; prefer local updates to avoid unintended changes".to_string()) + Some( + "Editing root path; prefer cr edit def --overwrite -f for whole-definition rewrites, and keep tree replace for intentional root-node surgery" + .to_string(), + ) } else { None } diff --git a/src/bin/cr_tests/type_fail.rs b/src/bin/cr_tests/type_fail.rs index c0179265..2f84e308 100644 --- a/src/bin/cr_tests/type_fail.rs +++ b/src/bin/cr_tests/type_fail.rs @@ -87,7 +87,6 @@ fn type_fail_schema_mismatch_fixtures_report_error_code() { } } - #[test] fn type_fail_call_arg_fixture_reports_warning_code() { let _guard = lock_fixture_tests(); From 82ed47b7ecaa16174f0ba9d805d95528e5aba734 Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 16 Mar 2026 00:32:17 +0800 Subject: [PATCH 12/57] maintain markdown docs inside repo --- docs/assets/ternary-tree-size-14.svg | 270 +++++++++++++++ docs/assets/ternary-tree-size-15.svg | 282 +++++++++++++++ docs/assets/ternary-tree-size-16.svg | 306 ++++++++++++++++ docs/assets/ternary-tree-size-17.svg | 330 ++++++++++++++++++ docs/cirru-syntax.md | 222 ++++++++++++ docs/data.md | 32 ++ docs/data/edn.md | 165 +++++++++ docs/data/persistent-data.md | 32 ++ docs/data/string.md | 9 + docs/docs | 1 + docs/ecosystem.md | 34 ++ docs/features.md | 87 +++++ docs/features/common-patterns.md | 405 ++++++++++++++++++++++ docs/features/enums.md | 179 ++++++++++ docs/features/error-handling.md | 150 ++++++++ docs/features/hashmap.md | 208 +++++++++++ docs/features/imports.md | 121 +++++++ docs/features/js-interop.md | 157 +++++++++ docs/features/list.md | 269 ++++++++++++++ docs/features/macros.md | 109 ++++++ docs/features/polymorphism.md | 124 +++++++ docs/features/records.md | 290 ++++++++++++++++ docs/features/sets.md | 151 ++++++++ docs/features/static-analysis.md | 500 +++++++++++++++++++++++++++ docs/features/traits.md | 340 ++++++++++++++++++ docs/features/tuples.md | 255 ++++++++++++++ docs/installation.md | 33 ++ docs/installation/ffi-bindings.md | 87 +++++ docs/installation/github-actions.md | 25 ++ docs/installation/modules.md | 16 + docs/intro.md | 52 +++ docs/intro/from-clojure.md | 33 ++ docs/intro/indentation-syntax.md | 63 ++++ docs/intro/overview.md | 21 ++ docs/quick-reference.md | 360 +++++++++++++++++++ docs/run.md | 67 ++++ docs/run/bundle-mode.md | 16 + docs/run/cli-options.md | 181 ++++++++++ docs/run/docs-libs.md | 107 ++++++ docs/run/edit-tree.md | 95 +++++ docs/run/entries.md | 29 ++ docs/run/eval.md | 128 +++++++ docs/run/hot-swapping.md | 56 +++ docs/run/load-deps.md | 60 ++++ docs/run/query.md | 112 ++++++ docs/structural-editor.md | 36 ++ src/bin/cli_handlers/cirru.rs | 4 +- src/bin/cli_handlers/docs.rs | 4 +- 48 files changed, 6609 insertions(+), 4 deletions(-) create mode 100644 docs/assets/ternary-tree-size-14.svg create mode 100644 docs/assets/ternary-tree-size-15.svg create mode 100644 docs/assets/ternary-tree-size-16.svg create mode 100644 docs/assets/ternary-tree-size-17.svg create mode 100644 docs/cirru-syntax.md create mode 100644 docs/data.md create mode 100644 docs/data/edn.md create mode 100644 docs/data/persistent-data.md create mode 100644 docs/data/string.md create mode 120000 docs/docs create mode 100644 docs/ecosystem.md create mode 100644 docs/features.md create mode 100644 docs/features/common-patterns.md create mode 100644 docs/features/enums.md create mode 100644 docs/features/error-handling.md create mode 100644 docs/features/hashmap.md create mode 100644 docs/features/imports.md create mode 100644 docs/features/js-interop.md create mode 100644 docs/features/list.md create mode 100644 docs/features/macros.md create mode 100644 docs/features/polymorphism.md create mode 100644 docs/features/records.md create mode 100644 docs/features/sets.md create mode 100644 docs/features/static-analysis.md create mode 100644 docs/features/traits.md create mode 100644 docs/features/tuples.md create mode 100644 docs/installation.md create mode 100644 docs/installation/ffi-bindings.md create mode 100644 docs/installation/github-actions.md create mode 100644 docs/installation/modules.md create mode 100644 docs/intro.md create mode 100644 docs/intro/from-clojure.md create mode 100644 docs/intro/indentation-syntax.md create mode 100644 docs/intro/overview.md create mode 100644 docs/quick-reference.md create mode 100644 docs/run.md create mode 100644 docs/run/bundle-mode.md create mode 100644 docs/run/cli-options.md create mode 100644 docs/run/docs-libs.md create mode 100644 docs/run/edit-tree.md create mode 100644 docs/run/entries.md create mode 100644 docs/run/eval.md create mode 100644 docs/run/hot-swapping.md create mode 100644 docs/run/load-deps.md create mode 100644 docs/run/query.md create mode 100644 docs/structural-editor.md diff --git a/docs/assets/ternary-tree-size-14.svg b/docs/assets/ternary-tree-size-14.svg new file mode 100644 index 00000000..eb34de3f --- /dev/null +++ b/docs/assets/ternary-tree-size-14.svg @@ -0,0 +1,270 @@ + + + + + + + + + +0 + +0 + + + +p3 + +p3 + + + +p3->0 + + + + + +1 + +1 + + + +p3->1 + + + + + +2 + +2 + + + +p3->2 + + + + + +p2 + +p2 + + + +p2->p3 + + + + + +p4 + +p4 + + + +p2->p4 + + + + + +p8 + +p8 + + + +p2->p8 + + + + + +3 + +3 + + + +p5 + +p5 + + + +p5->3 + + + + + +4 + +4 + + + +p5->4 + + + + + +5 + +5 + + + +p5->5 + + + + + +p4->p5 + + + + + +p6 + +p6 + + + +p4->p6 + + + + + +p7 + +p7 + + + +p4->p7 + + + + + +6 + +6 + + + +p6->6 + + + + + +7 + +7 + + + +p6->7 + + + + + +8 + +8 + + + +p6->8 + + + + + +9 + +9 + + + +p7->9 + + + + + +10 + +10 + + + +p7->10 + + + + + +11 + +11 + + + +p7->11 + + + + + +12 + +12 + + + +p8->12 + + + + + +13 + +13 + + + +p8->13 + + + + + +p1 + +p1 + + + +p1->p2 + + + + + diff --git a/docs/assets/ternary-tree-size-15.svg b/docs/assets/ternary-tree-size-15.svg new file mode 100644 index 00000000..848e1425 --- /dev/null +++ b/docs/assets/ternary-tree-size-15.svg @@ -0,0 +1,282 @@ + + + + + + + + + +0 + +0 + + + +p3 + +p3 + + + +p3->0 + + + + + +1 + +1 + + + +p3->1 + + + + + +2 + +2 + + + +p3->2 + + + + + +p2 + +p2 + + + +p2->p3 + + + + + +p4 + +p4 + + + +p2->p4 + + + + + +p8 + +p8 + + + +p2->p8 + + + + + +3 + +3 + + + +p5 + +p5 + + + +p5->3 + + + + + +4 + +4 + + + +p5->4 + + + + + +5 + +5 + + + +p5->5 + + + + + +p4->p5 + + + + + +p6 + +p6 + + + +p4->p6 + + + + + +p7 + +p7 + + + +p4->p7 + + + + + +6 + +6 + + + +p6->6 + + + + + +7 + +7 + + + +p6->7 + + + + + +8 + +8 + + + +p6->8 + + + + + +9 + +9 + + + +p7->9 + + + + + +10 + +10 + + + +p7->10 + + + + + +11 + +11 + + + +p7->11 + + + + + +12 + +12 + + + +p8->12 + + + + + +13 + +13 + + + +p8->13 + + + + + +14 + +14 + + + +p8->14 + + + + + +p1 + +p1 + + + +p1->p2 + + + + + diff --git a/docs/assets/ternary-tree-size-16.svg b/docs/assets/ternary-tree-size-16.svg new file mode 100644 index 00000000..0a68cf1c --- /dev/null +++ b/docs/assets/ternary-tree-size-16.svg @@ -0,0 +1,306 @@ + + + + + + + + + +0 + +0 + + + +p3 + +p3 + + + +p3->0 + + + + + +1 + +1 + + + +p3->1 + + + + + +2 + +2 + + + +p3->2 + + + + + +p2 + +p2 + + + +p2->p3 + + + + + +p4 + +p4 + + + +p2->p4 + + + + + +15 + +15 + + + +p2->15 + + + + + +3 + +3 + + + +p6 + +p6 + + + +p6->3 + + + + + +4 + +4 + + + +p6->4 + + + + + +5 + +5 + + + +p6->5 + + + + + +p5 + +p5 + + + +p5->p6 + + + + + +p7 + +p7 + + + +p5->p7 + + + + + +p8 + +p8 + + + +p5->p8 + + + + + +6 + +6 + + + +p7->6 + + + + + +7 + +7 + + + +p7->7 + + + + + +8 + +8 + + + +p7->8 + + + + + +9 + +9 + + + +p8->9 + + + + + +10 + +10 + + + +p8->10 + + + + + +11 + +11 + + + +p8->11 + + + + + +p4->p5 + + + + + +p9 + +p9 + + + +p4->p9 + + + + + +12 + +12 + + + +p9->12 + + + + + +13 + +13 + + + +p9->13 + + + + + +14 + +14 + + + +p9->14 + + + + + +p1 + +p1 + + + +p1->p2 + + + + + diff --git a/docs/assets/ternary-tree-size-17.svg b/docs/assets/ternary-tree-size-17.svg new file mode 100644 index 00000000..fae8a755 --- /dev/null +++ b/docs/assets/ternary-tree-size-17.svg @@ -0,0 +1,330 @@ + + + + + + + + + +0 + +0 + + + +p3 + +p3 + + + +p3->0 + + + + + +1 + +1 + + + +p3->1 + + + + + +2 + +2 + + + +p3->2 + + + + + +p2 + +p2 + + + +p2->p3 + + + + + +p4 + +p4 + + + +p2->p4 + + + + + +p10 + +p10 + + + +p2->p10 + + + + + +3 + +3 + + + +p6 + +p6 + + + +p6->3 + + + + + +4 + +4 + + + +p6->4 + + + + + +5 + +5 + + + +p6->5 + + + + + +p5 + +p5 + + + +p5->p6 + + + + + +p7 + +p7 + + + +p5->p7 + + + + + +p8 + +p8 + + + +p5->p8 + + + + + +6 + +6 + + + +p7->6 + + + + + +7 + +7 + + + +p7->7 + + + + + +8 + +8 + + + +p7->8 + + + + + +9 + +9 + + + +p8->9 + + + + + +10 + +10 + + + +p8->10 + + + + + +11 + +11 + + + +p8->11 + + + + + +p4->p5 + + + + + +p9 + +p9 + + + +p4->p9 + + + + + +12 + +12 + + + +p9->12 + + + + + +13 + +13 + + + +p9->13 + + + + + +14 + +14 + + + +p9->14 + + + + + +15 + +15 + + + +p10->15 + + + + + +16 + +16 + + + +p10->16 + + + + + +p1 + +p1 + + + +p1->p2 + + + + + diff --git a/docs/cirru-syntax.md b/docs/cirru-syntax.md new file mode 100644 index 00000000..16250d53 --- /dev/null +++ b/docs/cirru-syntax.md @@ -0,0 +1,222 @@ +## Cirru Syntax Essentials + +### 1. Indentation = Nesting + +Cirru uses **2-space indentation** to represent nested structures: + +```cirru +defn add (a b) + &+ a b +``` + +Equivalent JSON: + +```json +["defn", "add", ["a", "b"], ["&+", "a", "b"]] +``` + +### 2. The `$` Operator (Single-Child Expand) + +`$` creates a **single nested expression** on the same line: + +```cirru +do + ; Without $: explicit nesting + let + x 1 + str x + ; Multiple $ chain right-to-left + str $ &+ 1 2 + ; Equivalent to: (str (&+ 1 2)) +``` + +**Rule**: `a $ b c` → `["a", ["b", "c"]]` + +### 3. The `|` Prefix (String Literals) + +`|` marks a **string literal**: + +```cirru +println |hello +println |hello-world +println "|hello world with spaces" +``` + +- `|hello` → `"hello"` (string, not symbol) +- Without `|`: `hello` is a symbol/identifier +- For strings with spaces: `"|hello world"` + +### 4. The `,` Operator (Expression Terminator) + +`,` forces the **end of current expression**, starting a new sibling: + +```cirru +; Without comma - ambiguous +if true 1 2 + +; With comma - clear structure +if true + , 1 + , 2 +``` + +Useful in `cond`, `case`, `let` bindings: + +```cirru +let + x (- 0 3) + ; cond tests conditions in sequence, returning first matching result + cond + (&< x 0) |negative + (&= x 0) |zero + true |positive +``` + +### 5. Quasiquote, Unquote, Unquote-Splicing + +For macros: + +- `quasiquote` or backtick: template +- `~` (unquote): insert evaluated value +- `~@` (unquote-splicing): splice list contents + +```cirru.no-check +defmacro when-not (cond & body) + quasiquote $ if (not ~cond) + do ~@body +``` + +JSON equivalent: + +```json +[ + "defmacro", + "when-not", + ["cond", "&", "body"], + ["quasiquote", ["if", ["not", "~cond"], ["do", "~@body"]]] +] +``` + +## LLM Guidance & Optimization + +To ensure high-quality code generation for Calcit, follow these rules: + +### 1. Mandatory `|` Prefix for Strings + +LLMs often forget the `|` prefix. **Always** use `|` for string literals, even short ones. + +- ❌ `println "hello"` +- ✅ `println |hello` +- ✅ `println "|hello with spaces"` + +### 2. Functional `let` Binding + +`let` bindings must be a list of pairs `((name value))`. Single brackets `(name value)` are invalid. + +- ❌ `let (x 1) x` +- ✅ `let ((x 1)) x` +- ✅ **Preferred**: Use multi-line for clarity: + ```cirru.no-run + let + x 1 + y 2 + + x y + ``` + +### 3. Arity Awareness + +Calcit uses strict arity checking. Many core functions like `+`, `-`, `*`, `/` have native counterparts `&+`, `&-`, `&*`, `&/` which are binaries (2 arguments). The standard versions are often variadic macros. + +- Use `&+`, `&-`, etc. in tight loops or when 2 args are guaranteed. + +### 4. No Inline Types in Parameters + +Calcit **does not** support Clojure-style `(defn name [^Type arg] ...)`. + +- ❌ `defn add (a :number) ...` +- ✅ Use function schema for parameter types (`:schema` on top-level defs, `hint-fn` for local `fn`). +- ✅ Return types can be specified with `hint-fn` or a **trailing label** after parameters: + +```cirru +let + ; Local helper function + square $ defn square (n) + hint-fn $ {} (:args ([] :number)) (:return :number) + &* n n + ; Return type as trailing label + get-pi $ defn get-pi () :number + , 3.14159 + ; Mixed style + add $ defn add (a b) :number + hint-fn $ {} (:args ([] :number :number)) + + a b + [] (square 5) (get-pi) (add 3 4) +``` + +For namespace-level definitions, attach schema separately, for example: + +```cirru +defn square (n) + &* n n + +:: :fn $ {} (:args $ [] :number) (:return :number) +``` + +### 5. `$` and `,` Usage + +- Use `$` to avoid parentheses on the same line. +- Use `,` to separate multiline pairs in `cond` or `case` if indentation alone feels ambiguous. + +### 6. Common Patterns + +#### Function Definition + +```cirru.no-check +defn function-name (arg1 arg2) + body-expression +``` + +#### Let Binding + +```cirru +let + x 1 + y $ &+ x 2 + &* x y +``` + +#### Conditional + +```cirru.no-check +if condition + then-branch + else-branch +``` + +#### Multi-branch Cond + +```cirru.no-check +cond + (test1) result1 + (test2) result2 + true default-result +``` + +## JSON Format Rules + +When using `-j` or `--json-input`: + +1. **Everything is arrays or strings**: `["defn", "name", ["args"], ["body"]]` +2. **Numbers as strings**: `["&+", "1", "2"]` not `["&+", 1, 2]` +3. **Preserve prefixes**: `"|string"`, `"~var"`, `"~@list"` +4. **No objects**: JSON `{}` cannot be converted to Cirru + +## Common Mistakes + +| ❌ Wrong | ✅ Correct | Reason | +| ----------------------- | ------------------ | --------------------------------------------------------- | +| `println hello` | `println \|hello` | Missing `\|` for string | +| `$ a b c` at line start | `a b c` | A line is an expression, no need of `$` for extra nesting | +| `a$b` | `a $ b` | Missing space around `$` | +| `["&+", 1, 2]` | `["&+", "1", "2"]` | Numbers in syntax tree must be strings in JSON | +| Tabs for indent | 2 spaces | Cirru requires spaces | diff --git a/docs/data.md b/docs/data.md new file mode 100644 index 00000000..3489c26b --- /dev/null +++ b/docs/data.md @@ -0,0 +1,32 @@ +# Data Types + +Calcit provides several core data types, all immutable by default for functional programming: + +## Primitive Types + +- **Bool**: `true`, `false` +- **Number**: `f64` in Rust, Number in JavaScript (`1`, `3.14`, `-42`) +- **Tag**: Immutable strings starting with `:` (`:keyword`, `:demo`) - similar to Clojure keywords +- **String**: Text data with special prefix syntax (`|text`, `"|with spaces"`) + +## Collection Types + +- **Vector**: Ordered collection serving both List and Vector roles (`[] 1 2 3`) +- **HashMap**: Key-value pairs (`{} (:a 1) (:b 2)`) +- **HashSet**: Unordered unique elements (`#{} :a :b :c`) + +## Function Types + +- **Function**: User-defined functions and built-in procedures +- **Proc**: Internal procedure type for built-in functions + +## Implementation Details + +- **Rust runtime**: Uses [rpds](https://github.com/orium/rpds) for HashMap/HashSet and [ternary-tree](https://github.com/calcit-lang/ternary-tree.rs/) for vectors +- **JavaScript runtime**: Uses [ternary-tree.ts](https://github.com/calcit-lang/ternary-tree.ts) for all collections + +All data structures are persistent and immutable, following functional programming principles. For detailed information about specific types, see: + +- [String](data/string.md) - String syntax and Tags +- [Persistent Data](data/persistent-data.md) - Implementation details +- [EDN](data/edn.md) - Data notation format diff --git a/docs/data/edn.md b/docs/data/edn.md new file mode 100644 index 00000000..bfc6552f --- /dev/null +++ b/docs/data/edn.md @@ -0,0 +1,165 @@ +# Cirru Extensible Data Notation + +> Data notation based on Cirru. Learnt from [Clojure EDN](https://github.com/edn-format/edn). + +EDN data is designed to be transferred across networks are strings. 2 functions involved: + +- `parse-cirru-edn` +- `format-cirru-edn` + +although items of a HashSet nad fields of a HashMap has no guarantees, they are being formatted with an given order in order that its returns are reasonably stable. + +### Liternals + +For literals, if written in text syntax, we need to add `do` to make sure it's a line: + +```cirru +do nil +``` + +for a number: + +```cirru +do 1 +``` + +for a symbol: + +```cirru +do 's +``` + +there's also "keyword", which is called "tag" since Calcit `0.7`: + +```cirru +do :k +``` + +### String escaping + +for a string: + +```cirru +do |demo +``` + +or wrap with double quotes to support special characters like spaces: + +```cirru +do "|demo string" +``` + +or use a single double quote for mark strings: + +```cirru +do "\"demo string" +``` + +`\n` `\t` `\"` `\\` are supported. + +### Data structures: + +for a list: + +```cirru +[] 1 2 3 +``` + +or nested list inside list: + +```cirru +[] 1 2 + [] 3 4 +``` + +HashSet for unordered elements: + +```cirru +#{} :a :b :c +``` + +HashMap: + +```cirru +{} + :a 1 + :b 2 +``` + +also can be nested: + +```cirru +{} + :a 1 + :c $ {} + :d 3 +``` + +Also a record (in Calcit code, not EDN data): + +```cirru +let + A $ defstruct A (:a :dynamic) + ; Then create an instance in Calcit + %{} A + :a 1 +``` + +### Quotes + +For quoted data, there's a special semantics for representing them, since that was neccessary for `compact.cirru` usage, where code lives inside a piece of data, marked as: + +```cirru +quote $ def a 1 +``` + +at runtime, it's represented with tuples: + +```cirru +:: 'quote $ [] |def |a |1 +``` + +which means you can eval: + +```bash +$ cr eval "println $ format-cirru-edn $ :: 'quote $ [] |def |a |1" + +quote $ def a 1 + +took 0.027ms: nil +``` + +and also: + +```bash +$ cr eval 'parse-cirru-edn "|quote $ def a 1"' +took 0.011ms: (:: 'quote ([] |def |a |1)) +``` + +This is not a generic solution, but tuple is a special data structure in Calcit and can be used for marking up different types of data. + +### Buffers + +Buffers can be created using the `&buffer` function with hex values: + +```cirru +&buffer 0x03 0x55 0x77 0xff 0x00 +``` + +### Comments + +Comment expressions are started with `;`. They are evaluated into nothing, but not available anywhere, at least not available at head or inside a pair. + +Some usages: + +```cirru +[] 1 2 3 (; comment) 4 (; comment) +``` + +```cirru +{} + ; comment + :a 1 +``` + +Also notice that comments should also obey Cirru syntax. It's comments inside the syntax tree, rather than in parser. diff --git a/docs/data/persistent-data.md b/docs/data/persistent-data.md new file mode 100644 index 00000000..e1f97bae --- /dev/null +++ b/docs/data/persistent-data.md @@ -0,0 +1,32 @@ +# Persistent Data + +Calcit uses [rpds](https://github.com/orium/rpds) for HashMap and HashSet, and use [Ternary Tree](https://github.com/calcit-lang/ternary-tree.rs/) in Rust. + +For Calcit-js, it's all based on [ternary-tree.ts](https://github.com/calcit-lang/ternary-tree.ts), which is my own library. This library is quite naive and you should not count on it for good performance. + +### Optimizations for vector in Rust + +Although named "ternary tree", it's actually unbalanced 2-3 tree, with tricks learnt from [finger tree](https://en.wikipedia.org/wiki/Finger_tree) for better performance on `.push_right()` and `.pop_left()`. + +- [ternary-tree initial idea(old)](https://clojureverse.org/t/ternary-tree-structure-sharing-data-for-learning-purpose/6760) +- [Intro about optimization learnt from FingerTree(Chinese)](https://www.bilibili.com/video/BV1z44y1a7a6/) +- [internal tree layout from size 1~59](https://www.bilibili.com/video/BV1or4y1U7u2?spm_id_from=333.999.0.0) + +For example, this is the internal structure of vector `(range 14)`: + +![](https://cos-sh.tiye.me/cos-up/4a38e82ec94a39ff3fa52da11edc6d6e/ternary-tree-size-14.svg) + +when a element `14` is pushed at right, it's simply adding element at right, creating new path at a shallow branch, which means littler memory costs(compared to deeper branches): + +![](https://cos-sh.tiye.me/cos-up/23658e1d1a10bbd016a10db58d853ed8/ternary-tree-size-15.svg) + +and when another new element `15` is pushed at right, the new element is still placed at a shallow branch. Meanwhile the previous branch was pushed deeper into the middle branches of the tree: + +![](https://cos-sh.tiye.me/cos-up/4f2459576691173ac83d345e0fa87beb/ternary-tree-size-16.svg) + +so in this way, we made it cheaper in pushing new elements at right side. +These steps could be repeated agained and again, new elements are always being handled at shallow branches. + +![](https://cos-sh.tiye.me/cos-up/ad884313d4f7fa8915acf596091ee422/ternary-tree-size-17.svg) + +This was the trick learnt from finger tree. The library Calcit using is not optimal, but should be fast enough for many cases of scripting. diff --git a/docs/data/string.md b/docs/data/string.md new file mode 100644 index 00000000..7c32ea37 --- /dev/null +++ b/docs/data/string.md @@ -0,0 +1,9 @@ +# String + +The way strings are represented in Calcit is a bit unique. Strings are distinguished by a prefix. For example, `|A` represents the string `A`. If the string contains spaces, you need to enclose it in double quotes, such as `"|A B"`, where `|` is the string prefix. Due to the history of the structural editor, `"` is also a string prefix, but it is special: when used inside a string, it must be escaped as `"\"A"`. This is equivalent to `|A` and also to `"|A"`. The outermost double quotes can be omitted when there is no ambiguity. + +This somewhat unusual design exists because the structural editor naturally wraps strings in double quotes. When writing with indentation-based syntax, the outermost double quotes can be omitted for convenience. + +### Tag + +The most commonly used string type in Calcit is the Tag, which starts with a `:`, such as `:demo`. Its type is `Tag` in Rust and `string` in JavaScript. Unlike regular strings, Tags are immutable, meaning their value cannot be changed once created. This allows them to be used as keys in key-value pairs and in other scenarios where immutable values are needed. In practice, Tags are generally used to represent property keys, similar to keywords in the Clojure language. diff --git a/docs/docs b/docs/docs new file mode 120000 index 00000000..5c457d79 --- /dev/null +++ b/docs/docs @@ -0,0 +1 @@ +docs \ No newline at end of file diff --git a/docs/ecosystem.md b/docs/ecosystem.md new file mode 100644 index 00000000..987e62ae --- /dev/null +++ b/docs/ecosystem.md @@ -0,0 +1,34 @@ +# Ecosystem + +### Libraries: + +Useful libraries are maintained at . + +### Frameworks: + +- [Respo: virtual DOM library](https://github.com/Respo/respo.calcit) +- [Phlox: virtual DOM like wrapper on top of PIXI](https://github.com/Quamolit/phlox.calcit) +- [Quaterfoil: thin virtual DOM wrapper over three.js](https://github.com/Quamolit/quatrefoil.calcit) +- [Triadica: toy project rendering interactive 3D shapes with math and shader](https://github.com/Triadica/triadica-space) +- [tiny tool for drawing 3D shapes with WebGPU](https://github.com/Triadica/lagopus) +- [Cumulo: template for tiny realtime apps](https://github.com/Cumulo/cumulo-workflow.calcit) +- [Quamolit: what if we make animations in React's way?](https://github.com/Quamolit/quamolit.calcit) + +### Tools: + +- [Calcit Editor](https://github.com/calcit-lang/editor) - Structural editor for Calcit (Web-based) +- [Calcit IR viewer](https://github.com/calcit-lang/calcit-ir-viewer) - Visualize IR representation +- [Calcit Error viewer](https://github.com/calcit-lang/calcit-error-viewer) - Enhanced error display +- [Calcit Paint](https://github.com/calcit-lang/calcit-paint) - Experimental 2D shape editor +- [cr-mcp](https://github.com/calcit-lang/calcit) - MCP server for tool integration +- [calcit-bundler](https://www.npmjs.com/package/@calcit/bundler) - Bundle indentation syntax to compact format +- [caps-cli](https://www.npmjs.com/package/@calcit/caps) - Dependency management tool + +### VS Code Integration: + +- [Calcit VS Code Extension](https://marketplace.visualstudio.com/items?itemName=calcit-lang.calcit) - Syntax highlighting and snippets +- GitHub Copilot integration via cr-mcp server + +### Package Registry: + +- [libs.calcit-lang.org](https://libs.calcit-lang.org/base.cirru) - Official package index and documentation diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 00000000..32aed036 --- /dev/null +++ b/docs/features.md @@ -0,0 +1,87 @@ +# Features + +Calcit inherits most features from Clojure/ClojureScript while adding its own innovations: + +## Core Features + +- **Immutable persistent data structures** - All data is immutable by default using ternary tree implementations +- **Functional programming** - First-class functions, higher-order functions, closures +- **Lisp syntax** - Code as data, powerful macro system with hygienic macros +- **Hot code swapping** - Live code updates during development without state loss +- **JavaScript interop** - Seamless integration with JS ecosystem via ES Modules +- **Static type analysis** - Compile-time type checking and error detection + +## Unique to Calcit + +- **Indentation-based syntax** - Alternative to parentheses using `bundle_calcit`, similar to Python/Haskell +- **Structural editing** - Visual tree-based code editing with Calcit Editor (Electron app) +- **ES Modules output** - Modern JavaScript module format, tree-shakeable +- **MCP integration** - Model Context Protocol server for AI assistant tool integration +- **Ternary tree collections** - Custom persistent data structures optimized for Rust +- **Incremental compilation** - Fast hot reload with `.compact-inc.cirru` format +- **Pattern matching** - Tagged unions with compile-time validation +- **Record types** - Lightweight structs with field access validation +- **Traits & method dispatch** - Attach capability-based methods to values, with explicit disambiguation when needed + +## Language Features + +For detailed information about specific features: + +- [List](features/list.md) - Persistent vectors and list operations +- [HashMap](features/hashmap.md) - Key-value data structures and operations +- [Macros](features/macros.md) - Code generation and syntax extension +- [JavaScript Interop](features/js-interop.md) - Calling JS from Calcit and vice versa +- [Imports](features/imports.md) - Module system and dependency management +- [Polymorphism](features/polymorphism.md) - Object-oriented programming patterns +- [Traits](features/traits.md) - Capability-based method dispatch and explicit trait calls +- [Static Analysis](features/static-analysis.md) - Type checking and compile-time validation + +## Quick Find by Task + +Use this section as a keyword index for `cr docs read`: + +- **Collections**: list, map, set, tuple, record +- **Pattern Matching**: enum, tag-match, tuple-match, result +- **Types**: static-analysis, assert-type, optional, variadic +- **Methods**: trait, impl-traits, method dispatch, trait-call +- **Interop**: js interop, async, promise, js-await +- **Architecture**: imports, namespace, module, dependency + +Task-oriented jump map: + +- Data transforms → [List](features/list.md), [HashMap](features/hashmap.md), [Sets](features/sets.md) +- Domain modeling → [Records](features/records.md), [Enums](features/enums.md), [Tuples](features/tuples.md) +- Type safety → [Static Analysis](features/static-analysis.md), [Error Handling](features/error-handling.md) +- Extensibility → [Macros](features/macros.md), [Traits](features/traits.md), [Polymorphism](features/polymorphism.md) +- Runtime integration → [JavaScript Interop](features/js-interop.md), [Imports](features/imports.md) + +## Development Features + +- **Type inference** - Automatic type inference from literals and expressions +- **Compile-time checks** - Arity checking, field validation, bounds checking +- **Error handling** - Rich stack traces and error messages with source locations +- **Package management** - Git-based dependency system with `caps` CLI tool +- **Hot module replacement** - Fast iteration with live code updates +- **REPL integration** - Interactive development with `cr eval` mode +- **Bundle mode** - Single-file deployment with `cr bundle` + +## Type System + +Calcit's static analysis provides: + +- **Function arity checking** - Validates argument counts at compile time +- **Record field validation** - Checks field names exist in record types +- **Tuple bounds checking** - Validates tuple index access +- **Enum variant validation** - Ensures correct enum construction +- **Method existence checking** - Verifies methods exist for types +- **Recur arity validation** - Checks recursive calls have correct arguments +- **Return type validation** - Matches function return types with declarations + +## Performance + +- **Native execution** - Rust interpreter for fast CLI tools and scripting +- **Zero-cost abstractions** - Persistent data structures with minimal overhead +- **Lazy sequences** - Efficient processing of large datasets +- **Optimized compilation** - JavaScript output with tree-shaking support + +Calcit is designed to be familiar to Clojure developers while providing modern tooling, type safety, and excellent development experience. diff --git a/docs/features/common-patterns.md b/docs/features/common-patterns.md new file mode 100644 index 00000000..f6c2ffd2 --- /dev/null +++ b/docs/features/common-patterns.md @@ -0,0 +1,405 @@ +# Common Patterns + +This document provides practical examples and patterns for common programming tasks in Calcit. + +## Quick Recipes + +- **Filter/Map**: `-> xs (filter f) (map g)` +- **Group**: `group-by xs f` +- **Find**: `find xs f`, `index-of xs v` +- **Check All/Any**: `every? xs f`, `any? xs f` +- **State**: `defatom state 0`, `reset! state 1`, `swap! state inc` + +## Working with Collections + +### Filtering and Transforming Lists + +```cirru +; Filter even numbers and square them +-> (range 20) + filter $ fn (n) + = 0 $ &number:rem n 2 + map $ fn (n) + * n n +; => ([] 0 4 16 36 64 100 144 196 256 324) +``` + +### Grouping Data + +```cirru +let + group-by-length $ fn (words) + group-by words count + group-by-length ([] |apple |pear |banana |kiwi) +; => {} +; 4 $ [] |pear |kiwi +; 5 $ [] |apple +; 6 $ [] |banana +``` + +### Finding Elements + +```cirru +let + result1 $ find ([] 1 2 3 4 5) $ fn (x) (> x 3) + result2 $ index-of ([] :a :b :c :d) :c + result3 $ any? ([] 1 2 3) $ fn (x) (> x 2) + result4 $ every? ([] 2 4 6) $ fn (x) (= 0 $ &number:rem x 2) + println result1 + ; => 4 + println result2 + ; => 2 + println result3 + ; => true + println result4 + ; => true +``` + +## Error Handling + +### Using Result Type + +```cirru +let + MyResult $ defenum MyResult + :ok :dynamic + :err :string + safe-divide $ fn (a b) + if (= b 0) + %:: MyResult :err "|Division by zero" + %:: MyResult :ok (/ a b) + handle-result $ fn (result) + tag-match result + (:ok v) (println $ str "|Result: " v) + (:err msg) (println $ str "|Error: " msg) + handle-result $ safe-divide 10 2 + handle-result $ safe-divide 10 0 +``` + +### Using Option Type + +```cirru +let + MyOption $ defenum MyOption + :some :dynamic + :none + find-user $ fn (users id) + let + user $ find users $ fn (u) + = (get u :id) id + if (nil? user) + %:: MyOption :none + %:: MyOption :some user + println $ find-user + [] ({} (:id |001) (:name |Alice)) + , |001 +``` + +## Working with Maps + +### Nested Map Operations + +```cirru +let + data $ {} (:a $ {} (:b $ {} (:c 1))) + result1 $ get-in data $ [] :a :b :c + result2 $ assoc-in data ([] :a :b :c) 100 + result3 $ update-in data ([] :a :b :c) inc + println result1 + ; => 1 + println result2 + ; => {} (:a $ {} (:b $ {} (:c 100))) + println result3 + ; => {} (:a $ {} (:b $ {} (:c 2))) +``` + +### Merging Maps + +```cirru +let + result1 $ merge + {} (:a 1) (:b 2) + {} (:b 3) (:c 4) + {} (:d 5) + result2 $ &merge-non-nil + {} (:a 1) (:b nil) + {} (:b 2) (:c 3) + println result1 + ; => {} (:a 1) (:b 3) (:c 4) (:d 5) + println result2 + ; => {} (:a 1) (:b 2) (:c 3) +``` + +## String Manipulation + +### String Syntax + +Calcit has two ways to write strings: + +- `|text` - for strings without spaces (shorthand) +- `"|text with spaces"` - for strings with spaces (must use quotes) + +```cirru +let + s1 |HelloWorld + s2 |hello-world + s3 "|hello world" + s4 "|error in module" + println s1 + ; => |HelloWorld + println s2 + ; => |hello-world + println s3 + ; => "|hello world" + println s4 + ; => "|error in module" +``` + +### Building Strings + +```cirru +let + result1 $ str |Hello | |World + result2 $ join-str ([] :a :b :c) |, + result3 $ str-spaced :error |in :module + println result1 + ; => |HelloWorld + println result2 + ; => |a,b,c + println result3 + ; => "|error in module" +``` + +### Parsing Strings + +```cirru +let + result1 $ split |hello-world-test |-| + result2 $ split-lines |line1\nline2\nline3 + result3 $ parse-float |3.14159 + println result1 + ; => ([] |hello |world |test) + println result2 + ; => ([] |line1 |line2 |line3) + println result3 + ; => 3.14159 +``` + +### String Inspection + +```cirru +let + result1 $ starts-with? |hello-world |hello + result2 $ ends-with? |hello-world |world + result3 $ &str:find-index |hello-world |world + ; result1 => true + ; result2 => true + ; result3 => 6 (index of |world in |hello-world) + [] result1 result2 result3 +``` + +## State Management + +### Using Atoms + +```cirru +let + counter $ atom 0 + println $ deref counter + ; => 0 + reset! counter 10 + ; => 10 + swap! counter inc + ; => 11 +``` + +### Managing Collections in State + +```cirru +let + todos $ atom $ [] + add-todo! $ fn (text) + swap! todos $ fn (items) + append items $ {} (:id $ generate-id!) (:text text) (:done false) + toggle-todo! $ fn (id) + swap! todos $ fn (items) + map items $ fn (todo) + if (= (get todo :id) id) + assoc todo :done $ not (get todo :done) + , todo + add-todo! |buy-milk + add-todo! |write-docs + println $ deref todos +``` + +## Control Flow Patterns + +### Early Return Pattern + +```cirru +let + ; stub implementations for demonstration + validate-data $ fn (data) (if (= (count data) 0) nil data) + transform-data $ fn (validated) (map validated (fn (x) (* x 2))) + process-data $ defn process-data (data) + if (empty? data) + :: :err |Empty-data + let + validated $ validate-data data + if (nil? validated) + :: :err |Invalid-data + let + result $ transform-data validated + :: :ok result + process-data ([] 1 2 3) +``` + +### Pipeline Pattern + +```cirru +let + ; stub implementations for demonstration + validate-input $ fn (s) s + parse-input $ fn (s) s + transform-to-command $ fn (s) (str |cmd/ s) + process-user-input $ defn process-user-input (input) + -> input + trim + &str:slice 0 100 + validate-input + parse-input + transform-to-command + process-user-input "|hello world" +``` + +### Loop with Recur + +```cirru +; Factorial with loop/recur +defn factorial (n) + apply-args (1 n) + fn (acc n) + if (&<= n 1) acc + recur + * acc n + &- n 1 + +; Fibonacci with loop/recur +defn fibonacci (n) + apply-args (0 1 n) + fn (a b n) + if (&<= n 0) a + recur b (&+ a b) (&- n 1) +``` + +## Working with Files + +### Reading and Writing + +```cirru.no-run +let + content $ read-file |data.txt + lines $ split-lines content + println content + &doseq (line lines) + println line +``` + +## Math Operations + +### Common Calculations + +```cirru +let + round-to $ fn (n places) + let + factor $ pow 10 places + / (round $ * n factor) factor + clamp $ fn (x min-val max-val) + -> x + &max min-val + &min max-val + average $ fn (numbers) + / (apply + numbers) (count numbers) + println $ round-to 3.14159 2 + ; => 3.14 + println $ clamp 15 0 10 + ; => 10 + println $ average ([] 1 2 3 4 5) + ; => 3 +``` + +## Debugging + +### Inspecting Values + +```cirru +let + ; stub implementations for demonstration + transform-1 $ fn (x) (assoc x :step1 true) + transform-2 $ fn (x) (assoc x :step2 true) + data $ {} (:x 1) (:y 2) + result $ -> data transform-1 transform-2 + x 5 + assert |Should-be-positive $ > x 0 + assert= 4 (+ 2 2) + , result +``` + +## Performance Tips + +### Lazy Evaluation + +```cirru +let + result $ foldl-shortcut + range 1000 + , nil nil + fn (acc x) + if (> x 100) + :: true x + :: false nil + println result +``` + +### Avoiding Intermediate Collections + +```cirru +let + items $ [] ({} (:value 1)) ({} (:value 2)) ({} (:value 3)) + result1 $ reduce items 0 $ fn (acc item) + + acc (get item :value) + result2 $ apply + + map items $ fn (item) + get item :value + println result1 + ; => 6 + println result2 + ; => 6 +``` + +## Testing + +### Writing Tests + +```cirru +let + test-addition $ fn () + assert= 4 (+ 2 2) + assert= 0 (+ 0 0) + assert= -5 (+ -2 -3) + test-with-setup $ fn () + let + input $ {} (:name |test) (:value 42) + , true + test-addition +``` + +## Best Practices + +1. **Use type annotations** for function parameters and return values +2. **Prefer immutable data** - use `swap!` instead of manual mutation +3. **Use pattern matching** (`tag-match`, `record-match`) for control flow +4. **Leverage threading macros** (`->`, `->>`) for data pipelines +5. **Use enums for result types** instead of exceptions +6. **Keep functions small** and focused on a single responsibility diff --git a/docs/features/enums.md b/docs/features/enums.md new file mode 100644 index 00000000..985fab0b --- /dev/null +++ b/docs/features/enums.md @@ -0,0 +1,179 @@ +# Enums (defenum) + +Calcit enums are tagged unions — each variant has a tag (keyword) and zero or more typed payload fields. Under the hood enums are represented as tuples with a class reference. + +## Quick Recipes + +- **Define**: `defenum Shape (:circle :number) (:rect :number :number)` +- **Create**: `%:: Shape :circle 5` +- **Match**: `tag-match shape ((:circle r) ...) ((:rect w h) ...)` +- **Type Check**: `assert-type shape :enum` + +## Defining Enums + +```cirru +let + Color $ defenum Color (:red) (:green) (:blue) + c $ %:: Color :red + println c + ; => (%:: :red (:enum Color)) +``` + +Variants with payloads: + +```cirru +let + Shape $ defenum Shape (:circle :number) (:rect :number :number) + c $ %:: Shape :circle 5 + r $ %:: Shape :rect 3 4 + println c + ; => (%:: :circle 5 (:enum Shape)) + println r + ; => (%:: :rect 3 4 (:enum Shape)) +``` + +## Creating Instances + +Use `%::` with the enum definition, the variant tag, and then the payload values: + +```cirru +let + ApiResult $ defenum ApiResult (:ok :string) (:err :string) + ok $ %:: ApiResult :ok |success + err $ %:: ApiResult :err |network-error + println ok + println err +``` + +## Pattern Matching with `tag-match` + +`tag-match` branches on the variant tag and binds payload values to names: + +```cirru +let + Shape $ defenum Shape (:circle :number) (:rect :number :number) + c $ %:: Shape :circle 5 + area $ tag-match c + (:circle radius) + * radius radius 3.14159 + (:rect w h) + * w h + println area + ; => 78.53975 +``` + +Multi-line branch bodies (required when the body is more than a single call): + +```cirru +let + ApiResult $ defenum ApiResult (:ok :string) (:err :string) + ok $ %:: ApiResult :ok |success + describe $ fn (r) + tag-match r + (:ok msg) + str-spaced |OK: msg + (:err msg) + str-spaced |Error: msg + println (describe ok) + ; => OK: success +``` + +## Zero-payload Variants + +When a variant has no payload, the pattern is just the tag: + +```cirru +let + MaybeInt $ defenum MaybeInt (:some :number) (:none) + some-val $ %:: MaybeInt :some 42 + none-val $ %:: MaybeInt :none + extracted $ tag-match some-val + (:some v) + * v 2 + (:none) nil + println extracted + ; => 84 +``` + +## Checking Enum Origin + +Use `&tuple:enum` to verify a tuple belongs to a specific enum: + +```cirru +let + ApiResult $ defenum ApiResult (:ok :number) (:err :string) + x $ %:: ApiResult :ok 1 + println $ = (&tuple:enum x) ApiResult + ; => true +``` + +## Common Patterns + +### Result / Either type + +```cirru +let + AppResult $ defenum AppResult (:ok :number) (:err :string) + compute $ fn (x) + if (> x 0) + %:: AppResult :ok (* x 10) + %:: AppResult :err |negative-input + handle $ fn (r) + tag-match r + (:ok v) + str-spaced |result: v + (:err e) + str-spaced |failed: e + println $ handle (compute 5) + ; => result: 50 + println $ handle (compute -1) + ; => failed: negative-input +``` + +### Compose enums with functions + +```cirru +let + Status $ defenum Status (:pending) (:done :string) (:failed :string) + pending $ %:: Status :pending + done $ %:: Status :done |ok + is-done $ fn (s) + tag-match s + (:done _) true + (:pending) false + (:failed _) false + println (is-done pending) + ; => false + println (is-done done) + ; => true +``` + +## Type Annotations + +Field types in `defenum` declarations participate in type checking: + +```cirru.no-run +; (:ok :string) means the :ok variant has one :string payload +defenum ApiResult (:ok :string) (:err :string) + +; (:point :number :number) means :point has two :number payloads +defenum Shape (:point :number :number) (:circle :number) + +; (:none) means no payload +defenum MaybeInt (:some :number) (:none) +``` + +Runtime type validation is enforced at instance creation — passing the wrong type to `%::` will raise an error. + +## Notes + +- Enum instances are immutable tuples with a class reference. +- `tag-match` is exhaustive match; unmatched tags raise a runtime error. +- Use `&tuple:nth` to directly access payload values by index (0 = tag, 1+ = payloads). +- Enums vs plain tuples: plain `:: :tag val` tuples have no class; `%:: Enum :tag val` tuples carry their enum class for origin checking. + +## See Also + +- [Tuples](tuples.md) — raw tagged tuples without a class +- [Records](records.md) — named-field structs with `defstruct` +- [Static Analysis](static-analysis.md) — type checking for enum payloads diff --git a/docs/features/error-handling.md b/docs/features/error-handling.md new file mode 100644 index 00000000..68127786 --- /dev/null +++ b/docs/features/error-handling.md @@ -0,0 +1,150 @@ +# Error Handling + +Calcit uses `try` / `raise` for exception-based error handling. Errors are string values (or tags) propagated up the call stack. + +## Quick Recipes + +- **Catch Errors**: `try (risky-op) (fn (e) ...)` +- **Throw String**: `raise |something-went-wrong` +- **Throw Tag**: `raise :invalid-input` +- **Match Error**: `if (= e :invalid-input) ...` + +## Basic `try` / `raise` + +`try` takes an expression body and a handler function. If the body raises an error, the handler receives the error message as a string: + +```cirru +let + result $ try + raise |something-went-wrong + fn (e) + str-spaced |caught: e + println result + ; => caught: something-went-wrong +``` + +## Raising from a Function + +```cirru +let + safe-div $ fn (a b) + if (= b 0) + raise |division-by-zero + / a b + result $ try + safe-div 10 0 + fn (e) + str-spaced |error: e + println result + ; => error: division-by-zero +``` + +## Raising Tags as Error Codes + +Tags are a clean way to represent error categories: + +```cirru +let + validate-age $ fn (n) + if (< n 0) + raise :negative-age + if (> n 150) + raise :unrealistic-age + n + result $ try + validate-age -5 + fn (e) + str-spaced |validation-failed: e + println result + ; => validation-failed: :negative-age +``` + +## Silent Success vs Error Paths + +When an error is raised, execution jumps to the handler — intermediate values are not returned: + +```cirru +let + might-fail $ fn (flag) + if flag (raise |early-exit) 42 + a $ try (might-fail false) $ fn (e) -1 + b $ try (might-fail true) $ fn (e) -1 + println a + ; => 42 + println b + ; => -1 +``` + +## Nested `try` + +Inner `try` handlers can re-raise or recover selectively: + +```cirru.no-check +try + try + risky-operation + fn (e) + if (= e :recoverable) + default-value + raise e + fn (outer-e) + log-error outer-e + nil +``` + +## Using Enums for Typed Results (Preferred Pattern) + +Instead of exceptions, idiom Calcit code often uses a `Result` enum to represent success/failure without throwing: + +```cirru +let + AppResult $ defenum AppResult (:ok :number) (:err :string) + safe-compute $ fn (x) + if (> x 0) + %:: AppResult :ok (* x 10) + %:: AppResult :err |negative-input + handle $ fn (r) + tag-match r + (:ok v) + str-spaced |result: v + (:err msg) + str-spaced |failed: msg + println $ handle (safe-compute 5) + ; => result: 50 + println $ handle (safe-compute -1) + ; => failed: negative-input +``` + +This pattern avoids exceptions entirely and keeps error handling explicit in the type system. + +## Assertions + +`assert` and `assert=` raise errors during preprocessing/testing: + +```cirru.no-check +; assert a condition is true +assert (> x 0) |expected-positive + +; assert two values are equal +assert= (+ 1 2) 3 +``` + +`assert-type` checks type at preprocessing time: + +```cirru.no-check +; assert x is a number before using it +assert-type x :number +``` + +## Notes + +- `raise` accepts any value that can be converted to a string. String literals and tags work best. +- Raising maps or complex data structures may produce unexpected results — use the Result enum pattern for structured error data. +- `try` always produces a value: either the result of the body, or the result of the handler. +- `assert` / `assert=` are for development-time invariants. They generate warnings (not runtime errors) during static analysis. + +## See Also + +- [Enums](enums.md) — Result/Option patterns for typed error handling +- [Static Analysis](static-analysis.md) — `assert-type`, type hints +- [Common Patterns](common-patterns.md) — defensive programming examples diff --git a/docs/features/hashmap.md b/docs/features/hashmap.md new file mode 100644 index 00000000..357bc98a --- /dev/null +++ b/docs/features/hashmap.md @@ -0,0 +1,208 @@ +# HashMap + +Calcit HashMap is a persistent, immutable hash map. In Rust it uses [rpds::HashTrieMap](https://docs.rs/rpds/0.10.0/rpds/#hashtriemap). In JavaScript it is built on [ternary-tree](https://github.com/calcit-lang/ternary-tree.ts). + +All map operations return new maps — the original is never mutated. + +## Quick Recipes + +- **Create**: `{} (:a 1) (:b 2)` +- **Access**: `get m :a`, `contains? m :a` +- **Modify**: `assoc m :c 3`, `dissoc m :a`, `update m :a inc` +- **Transform**: `map-kv m f`, `merge m1 m2` +- **Keys/Values**: `keys m`, `vals m`, `to-pairs m` + +## Creating Maps + +`{}` is a macro that takes key-value pairs: + +```cirru +let + m $ {} + :a 1 + :b 2 + :c 3 + println m + ; => ({} (:a 1) (:b 2) (:c 3)) +``` + +Inline form: + +```cirru +let + m $ {} (:x 10) (:y 20) + println m +``` + +The low-level primitive `&{}` takes flat key-value pairs: + +```cirru.no-run +&{} :a 1 :b 2 +``` + +## Reading Values + +```cirru +let + m $ {} (:a 1) (:b 2) (:c 3) + println $ get m :a + ; => 1 + println $ get m :missing + ; => nil + println $ contains? m :b + ; => true + println $ count m + ; => 3 + println $ empty? m + ; => false +``` + +### Nested access with `get-in` + +```cirru +let + nested $ {} (:user $ {} (:name |Alice) (:age 30)) + println $ get-in nested $ [] :user :name + ; => Alice +``` + +## Modifying Maps + +All operations return a new map: + +```cirru +let + m $ {} (:a 1) (:b 2) + m2 $ assoc m :c 3 + m3 $ dissoc m2 :b + m4 $ merge m $ {} (:d 4) (:e 5) + println m2 + ; => ({} (:a 1) (:b 2) (:c 3)) + println m3 + ; => ({} (:a 1) (:c 3)) + println m4 + ; => ({} (:a 1) (:b 2) (:d 4) (:e 5)) +``` + +### Nested update with `assoc-in` + +```cirru.no-check +; update a deeply nested value +assoc-in config $ [] :server :port $ 8080 +``` + +## Iterating & Transforming + +### `map-kv` — transform entries + +Returns a new map. If the callback returns `nil`, the entry is dropped (used as filter): + +```cirru +let + m $ {} (:a 1) (:b 2) (:c 13) + doubled $ map-kv m $ fn (k v) ([] k (* v 2)) + filtered $ map-kv m $ fn (k v) + if (> v 10) nil + [] k v + println doubled + ; => ({} (:a 2) (:b 4) (:c 26)) + println filtered + ; => ({} (:a 1) (:b 2)) +``` + +### `to-pairs` — convert to set of pairs + +```cirru +let + m $ {} (:a 1) (:b 2) + println $ to-pairs m + ; => (#{} ([] :a 1) ([] :b 2)) +``` + +### `keys` and `vals` + +```cirru +let + m $ {} (:x 10) (:y 20) + println $ keys m + ; => (#{} :x :y) + println $ vals m + ; => (#{} 10 20) +``` + +### `each-kv` — side-effect iteration + +```cirru.no-check +each-kv config $ fn (k v) + println $ str k |: v +``` + +## Querying + +```cirru +let + m $ {} (:a 1) (:b 2) (:c 3) + println $ includes? m 2 + ; => true (checks values) + println $ contains? m :a + ; => true (checks keys) +``` + +## Building from Other Structures + +```cirru.no-check +; from a list of pairs +; each pair is [key value] +foldl my-pairs ({}) $ fn (acc pair) + assoc acc (nth pair 0) (nth pair 1) +``` + +Using thread macro to build up a map (inserting as first arg to each step): + +```cirru +let + base $ {} (:a 1) (:b 2) + result $ merge base $ {} (:c 3) (:d 4) + println result +``` + +## Common Patterns + +### Default value on missing key + +```cirru +let + m $ {} (:a 1) (:b 2) + val $ get m :missing + if (nil? val) :default val + ; => :default +``` + +### Counting occurrences + +```cirru +let + words $ [] :a :b :a :c :a :b + init $ {} + freq $ foldl words init $ fn (acc w) + let + cur $ get acc w + n $ if (nil? cur) 0 cur + assoc acc w (inc n) + println freq + ; ({} (:a 3) (:b 2) (:c 1)) +``` + +### Merging with override + +```cirru +let + defaults $ {} (:host |localhost) (:port 3000) (:debug false) + overrides $ {} (:port 8080) (:debug true) + merge defaults overrides + ; => ({} (:host |localhost) (:port 8080) (:debug true)) +``` + +## Implementation Notes + +HashMap key iteration order is not guaranteed. Use `to-pairs` + `sort` if you need stable order. Tags (`:kw`) are the most common key type; string keys also work but tags are faster for equality checks. diff --git a/docs/features/imports.md b/docs/features/imports.md new file mode 100644 index 00000000..2f2c7c85 --- /dev/null +++ b/docs/features/imports.md @@ -0,0 +1,121 @@ +# Imports + +Calcit loads namespaces from `compact.cirru` (the compiled representation of source files). Dependencies are tracked via `~/.config/calcit/modules/`. + +## Quick Recipes + +- **Alias**: `:require (app.lib :as lib)` +- **Refer**: `:require (app.lib :refer $ f1 f2)` +- **Core**: `calcit.core` is auto-imported +- **CLI Add**: `cr edit add-import app.main -e 'app.lib :refer $ f1'` + +## The `ns` Form + +Every source file declares its namespace at the top with `ns`: + +```cirru.no-check +ns app.demo + :require + app.lib :as lib + app.lib :refer $ f1 f2 + app.util :refer $ helper +``` + +The `:require` block accepts two kinds of rules: + +| Form | Effect | +| --------------------------- | --------------------------------------------------- | +| `mod.ns :as alias` | Imports namespace as `alias`; access via `alias/fn` | +| `mod.ns :refer $ sym1 sym2` | Imports symbols directly into scope | + +## Aliased Import + +Use `:as` to import an entire namespace under a local alias: + +```cirru.no-check +ns app.main + :require + app.model :as model + app.util :as util + +; Then use as: +; model/make-user +; util/format-date +``` + +## Direct Symbol Import + +Use `:refer` to bring specific names into the current namespace: + +```cirru.no-check +ns app.main + :require + app.math :refer $ add subtract multiply + app.string :refer $ capitalize trim-whitespace +``` + +## `calcit.core` — Auto-Imported + +All standard library functions (`map`, `filter`, `reduce`, `+`, `println`, `defn`, `let`, etc.) come from `calcit.core` and are available automatically without an explicit import. You do **not** need to require `calcit.core`. + +## JavaScript Interop Imports + +When compiling to JavaScript, Calcit generates ES module import syntax. The NS form supports additional rules for JS: + +```cirru.no-check +ns app.demo + :require + ; Regular Calcit module + app.lib :as lib + + ; NPM package with default export + |chalk :default chalk + + ; NPM package with named exports + |path :refer $ join dirname +``` + +Generated JS output: + +```js +import * as $app_DOT_lib from "./app.lib.mjs"; +import chalk from "chalk"; +import { join, dirname } from "path"; +``` + +Note the `|` prefix on npm package names — this indicates a string literal (the module specifier) vs a Calcit namespace path. + +## Avoiding Circular Imports + +Circular dependencies (A imports B, B imports A) will cause a compilation error. Structure your code with: + +- Core data types and pure functions in low-level namespaces +- Side-effectful and orchestration code at higher levels + +## Using `cr edit` for Import Management + +The `cr edit` CLI commands help manage imports safely: + +```bash +# Add a new import to a namespace +cr app.cirru edit add-import app.demo -e 'app.util :refer $ helper' + +# Override an existing import (same source namespace) +cr app.cirru edit add-import app.demo -e 'app.util :refer $ helper new-fn' -o +``` + +See `cr edit --help` for all available operations. + +## Checking Imports + +Use `cr docs search` to look up what's available in a namespace before importing: + +```bash +cr app.cirru docs search my-function +``` + +or query the examples for a specific definition: + +```bash +cr app.cirru query examples calcit.core/map +``` diff --git a/docs/features/js-interop.md b/docs/features/js-interop.md new file mode 100644 index 00000000..c72dbcfe --- /dev/null +++ b/docs/features/js-interop.md @@ -0,0 +1,157 @@ +# JavaScript Interop + +Calcit keeps JS interop syntax intentionally small. This page covers the existing core patterns: + +- global access +- property access +- method call +- array/object construction +- constructor call with `new` + +## Access global values + +Use `js/...` to read JavaScript globals and nested members: + +```cirru.no-run +do js/window.innerWidth +``` + +## Access properties + +Use `.-name` for property access: + +```cirru.no-run +let + obj $ js-object (:name |Alice) + .-name obj +``` + +This compiles to direct JS member access. For non-identifier keys, Calcit uses bracket access automatically. + +Optional access is also supported with `.?-name`, which maps to optional chaining style access. + +## Call methods + +Use `.!name` for native JS method calls (object first, then args): + +```cirru.no-run +.!setItem js/localStorage |key |value +``` + +Optional method call is supported with `.?!name`. + +> Note: `.m` and `.!m` are different. `.m` is Calcit method dispatch (traits/impls), while `.!m` is native JavaScript method invocation. + +## Construct arrays + +Use `js-array` for JavaScript arrays: + +```cirru.no-run +let + a $ js-array 1 2 + .!push a 3 4 + , a +``` + +## Construct objects + +Use `js-object` with key/value pairs: + +```cirru.no-run +js-object + :a 1 + :b 2 +``` + +`js-object` is a macro that validates input shape, so each entry must be a pair. + +Equivalent single-line form: + +```cirru.no-run +js-object (:a 1) (:b 2) +``` + +## Create instances with `new` + +Use `new` with a constructor symbol: + +```cirru.no-run +new js/Date +``` + +With arguments: + +```cirru.no-run +new js/Array 3 +``` + +## Async interop patterns + +Calcit provides async interop syntax for JS codegen. + +### Mark async functions + +Use `hint-fn $ {} (:async true)` in function body when using `js-await`: + +`js-await` should stay inside async-marked function bodies. + +```cirru.no-run +let + fetch-data $ fn () nil + fn () + hint-fn $ {} (:async true) + js-await $ fetch-data +``` + +### Await promises + +Use `js-await` for Promise-like values: + +```cirru.no-run +fn () + hint-fn $ {} (:async true) + let + p $ new js/Promise $ fn (resolve _reject) + js/setTimeout + fn () (resolve |done) + , 100 + result $ js-await p + , result +``` + +### Build Promise helpers + +A common pattern is wrapping callback APIs with `new js/Promise`: + +```cirru.no-run +defn timeout (ms) + new js/Promise $ fn (resolve _reject) + js/setTimeout resolve ms +``` + +Then consume it inside async function: + +```cirru.no-run +let + timeout $ fn (ms) $ new js/Promise $ fn (resolve _reject) + js/setTimeout resolve ms + fn () + hint-fn $ {} (:async true) + js-await $ timeout 200 +``` + +### Async iteration + +Use `js-for-await` with `js-await` for async iterables: + +```cirru.no-run +let + gen $ fn () nil + fn () + hint-fn $ {} (:async true) + js-await $ js-for-await (gen) + fn (item) + new js/Promise $ fn (resolve _reject) + js/setTimeout $ fn () + resolve item +``` diff --git a/docs/features/list.md b/docs/features/list.md new file mode 100644 index 00000000..8e3839f5 --- /dev/null +++ b/docs/features/list.md @@ -0,0 +1,269 @@ +# List + +Calcit List is a persistent, immutable vector. In Rust it uses [ternary-tree](https://github.com/calcit-lang/ternary-tree.rs) (optimized 2-3 tree with finger-tree tricks). In JavaScript it uses a similar structure with a fast-path `CalcitSliceList` for append-heavy workloads. + +All list operations return new lists — the original is never mutated. + +## Quick Recipes + +- **Create**: `[] 1 2 3` or `range 5` +- **Access**: `nth xs 0`, `first xs`, `last xs` +- **Modify**: `append xs 4`, `prepend xs 0`, `assoc xs 1 99` +- **Transform**: `map xs f`, `filter xs f`, `reduce xs 0 f` +- **Combine**: `concat xs ys`, `slice xs 1 3` + +## Creating Lists + +```cirru +let + empty-list $ [] + nums $ [] 1 2 3 4 5 + words $ [] |foo |bar |baz + println nums + ; => ([] 1 2 3 4 5) +``` + +`range` generates a sequence: + +```cirru +let + r1 $ range 5 + r2 $ range 2 7 + println r1 + ; => ([] 0 1 2 3 4) + println r2 + ; => ([] 2 3 4 5 6) +``` + +## Accessing Elements + +```cirru +let + xs $ [] 10 20 30 40 + println $ nth xs 0 + ; => 10 + println $ first xs + ; => 10 + println $ last xs + ; => 40 + println $ count xs + ; => 4 +``` + +`get` is an alias for `nth`: + +```cirru +let + xs $ [] :a :b :c + println $ get xs 1 + ; => :b +``` + +## Adding / Removing Elements + +```cirru +let + xs $ [] 1 2 3 + println $ append xs 4 + ; => ([] 1 2 3 4) + println $ prepend xs 0 + ; => ([] 0 1 2 3) + println $ conj xs 4 5 + ; => ([] 1 2 3 4 5) + println $ concat xs $ [] 4 5 + ; => ([] 1 2 3 4 5) +``` + +Update or remove by index: + +```cirru +let + xs $ [] 1 2 3 + println $ assoc xs 1 99 + ; => ([] 1 99 3) + println $ dissoc xs 1 + ; => ([] 1 3) +``` + +## Slicing & Reordering + +```cirru +let + xs $ [] 1 2 3 4 5 + println $ rest xs + ; => ([] 2 3 4 5) + println $ butlast xs + ; => ([] 1 2 3 4) + println $ slice xs 1 3 + ; => ([] 2 3) + println $ take xs 3 + ; => ([] 1 2 3) + println $ take-last xs 2 + ; => ([] 4 5) + println $ drop xs 2 + ; => ([] 3 4 5) +``` + +Sort (default ascending): + +```cirru +let + xs $ [] 3 1 4 1 5 + println $ sort xs + ; => ([] 1 1 3 4 5) +``` + +Sort by key function (method-style): + +```cirru +let + xs $ [] 1 2 3 4 5 + println $ .sort-by xs $ fn (x) (- 0 x) + ; => ([] 5 4 3 2 1) +``` + +Reverse: + +```cirru +let + xs $ [] 1 2 3 4 5 + println $ reverse xs + ; => ([] 5 4 3 2 1) +``` + +## Filtering & Finding + +```cirru +let + xs $ [] 1 2 3 4 5 + println $ filter xs $ fn (x) (> x 3) + ; => ([] 4 5) + println $ filter-not xs $ fn (x) (> x 3) + ; => ([] 1 2 3) + println $ find xs $ fn (x) (> x 3) + ; => 4 + println $ find-index xs $ fn (x) (> x 3) + ; => 3 + println $ index-of xs 3 + ; => 2 +``` + +## Transforming + +```cirru +let + xs $ [] 1 2 3 4 5 + println $ map xs $ fn (x) (* x 2) + ; => ([] 2 4 6 8 10) + println $ map-indexed xs $ fn (i x) ([] i x) + ; => ([] ([] 0 1) ([] 1 2) ([] 2 3) ([] 3 4) ([] 4 5)) +``` + +Flatten one level of nesting (method-style): + +```cirru +let + nested $ [] ([] 1 2) ([] 3 4) ([] 5) + println $ .flatten nested + ; => ([] 1 2 3 4 5) +``` + +## Aggregating + +```cirru +let + xs $ [] 1 2 3 4 5 + println $ reduce xs 0 $ fn (acc x) (+ acc x) + ; => 15 + println $ foldl xs 0 $ fn (acc x) (+ acc x) + ; => 15 + println $ any? xs $ fn (x) (> x 4) + ; => true + println $ every? xs $ fn (x) (> x 0) + ; => true +``` + +`group-by` partitions into a map keyed by the return value of the function: + +```cirru +let + xs $ [] 1 2 3 4 5 + println $ group-by xs $ fn (x) (if (> x 3) :big :small) + ; => ({} (:big ([] 4 5)) (:small ([] 1 2 3))) +``` + +## Strings from Lists + +```cirru +let + words $ [] |hello |world |foo + println $ join-str words |, + ; => hello,world,foo +``` + +## Converting + +```cirru +let + xs $ [] 1 2 2 3 3 3 + println $ .to-set xs + ; => (#{} 1 2 3) +``` + +## Thread Macro Pipelines + +The `->` thread macro is idiomatic for list transformations: + +```cirru +let + result $ -> (range 10) + filter $ fn (x) (> x 5) + map $ fn (x) (* x x) + println result + ; => ([] 36 49 64 81) +``` + +## Common Patterns + +### Building lists incrementally + +```cirru +let + source $ [] 1 2 3 4 5 + init $ [] + result $ foldl source init $ fn (acc item) + if (> item 2) + append acc (* item 10) + , acc + println result + ; => ([] 30 40 50) +``` + +### Zip two lists together + +```cirru +let + ks $ [] :a :b :c + vs $ [] 1 2 3 + zipped $ map-indexed ks $ fn (i k) ([] k (nth vs i)) + println zipped + ; => ([] ([] :a 1) ([] :b 2) ([] :c 3)) +``` + +### Deduplicate + +Convert to set (removes duplicates, loses order): + +```cirru +let + xs $ [] 1 2 2 3 3 3 + println $ .to-set xs + ; => (#{} 1 2 3) +``` + +## Implementation Notes + +- `nth` and `get` are O(log n) on the ternary tree structure. +- `append` and `prepend` are amortized O(1) in the Rust implementation. +- `concat` is O(m) where m is the size of the appended list. +- Lists are zero-indexed. diff --git a/docs/features/macros.md b/docs/features/macros.md new file mode 100644 index 00000000..130598e2 --- /dev/null +++ b/docs/features/macros.md @@ -0,0 +1,109 @@ +# Macros + +Like Clojure, Calcit uses macros to support new syntax. And macros ared evaluated during building to expand syntax tree. A `defmacro` block returns list and symbols, as well as literals: + +## Quick Recipes + +- **Define**: `defmacro my-macro (x) ...` +- **Template**: `quasiquote $ if ~x ~y ~z` +- **Splice**: `~@xs` to unpack a list into the template +- **Fresh Symbols**: `gensym |name` or `with-gensyms (a b) ...` +- **Local Bindings**: `&let (v ~item) ...` + +```cirru +defmacro noted (x0 & xs) + if (empty? xs) x0 + last xs +``` + +A normal way to use macro is to use `quasiquote` paired with `~x` and `~@xs` to insert one or a span of items. Also notice that `~x` is internally expanded to `(~ x)`, so you can also use `(~ x)` and `(~@ xs)` as well: + +```cirru +defmacro if-not (condition true-branch ? false-branch) + quasiquote $ if ~condition ~false-branch ~true-branch +``` + +To create new variables inside macro definitions, use `(gensym)` or `(gensym |name)`: + +```cirru +defmacro case (item default & patterns) + &let + v (gensym |v) + quasiquote + &let (~v ~item) + &case ~v ~default ~@patterns +``` + +For macros that need multiple fresh symbols, use `with-gensyms` from `calcit.core`: + +```cirru +defmacro swap! (a b) + with-gensyms (tmp) + quasiquote + let ((~tmp ~a)) + reset! ~a ~b + reset! ~b ~tmp +``` + +Calcit was not designed to be identical to Clojure, so there are many details here and there. + +### Macros and Static Analysis + +Macros expand before type checking, so generated code is validated: + +```cirru.no-check +defmacro assert-positive (x) + quasiquote + if (< ~x 0) + raise "|Value must be positive" + ~x + +; After expansion, type checking applies to generated code +defn process (n) + hint-fn $ {} (:args ([] :number)) + assert-positive n ; Macro expands, then type-checked +``` + +**Important**: Macro-generated functions (like loop's `f%`) are automatically excluded from certain static checks (e.g., recur arity) to avoid false positives. Functions with `%`, `$`, or `__` prefix are treated as compiler-generated. + +### Best Practices + +- **Use gensym for local variables**: Prevents name collision +- **Keep macros simple**: Complex logic belongs in functions +- **Document macro behavior**: Include usage examples +- **Test macro expansion**: Use `macroexpand-all` to verify output +- **Avoid side effects**: Macros should only transform syntax + +### Debug Macros + +Use `macroexpand-all` for debugging: + +``` +$ cr eval 'println $ format-to-cirru $ macroexpand-all $ quote $ let ((a 1) (b 2)) (+ a b)' + +&let (a 1) + &let (b 2) + + a b + +``` + +`format-to-cirru` and `format-to-lisp` are 2 custom code formatters: + +``` +$ cr eval 'println $ format-to-lisp $ macroexpand-all $ quote $ let ((a 1) (b 2)) (+ a b)' + +(&let (a 1) (&let (b 2) (+ a b))) +``` + +`macroexpand`, `macroexpand-1`, and `macroexpand-all` also print the expansion chain on stderr when nested macros are involved (for example `m1 -> m2 -> m3`). This is useful when a call site expands through helper macros before reaching final syntax. + +The syntax `macroexpand` only expand syntax tree once: + +``` +$ cr eval 'println $ format-to-cirru $ macroexpand $ quote $ let ((a 1) (b 2)) (+ a b)' + +&let (a 1) + let + b 2 + + a b +``` diff --git a/docs/features/polymorphism.md b/docs/features/polymorphism.md new file mode 100644 index 00000000..41bbaa93 --- /dev/null +++ b/docs/features/polymorphism.md @@ -0,0 +1,124 @@ +# Polymorphism + +Calcit models polymorphism with traits. Traits define method capabilities and can be attached to struct/enum definitions with `impl-traits`. + +For capability-based dispatch via struct/enum-attached impls (used by records/tuples created from them), see [Traits](traits.md). + +Historically, the idea was inspired by JavaScript, and also [borrowed from a trick of Haskell](https://www.well-typed.com/blog/2018/03/oop-in-haskell/) (simulating OOP with immutable data structures). The current model is trait-based. + +## Quick Recipes + +- **Define Trait**: `deftrait Show .show (:: :fn $ {} ...)` +- **Implement**: `defimpl ShowImpl Show .show (fn (x) ...)` +- **Attach**: `impl-traits MyStruct ShowImpl` +- **Call**: `.show instance` + +## Key terms + +- **Trait**: A named capability with method signatures (defined by `deftrait`). +- **Trait impl**: An impl record providing method implementations for a trait. +- **impl-traits**: Attaches one or more trait impl records to a struct/enum definition. +- **assert-traits**: Adds a compile-time hint and performs a runtime check that a value satisfies a trait. + +## Define a trait + +```cirru +deftrait Show + .show $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + +deftrait Eq + .eq? $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T 'T + :return :bool +``` + +Traits are values and can be referenced like normal symbols. + +## Implement a trait for a struct/enum definition + +```cirru +let + MyFoo $ deftrait MyFoo + .foo $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + MyFooImpl $ defimpl MyFooImpl MyFoo + .foo $ fn (p) (str "|foo " (:name p)) + Person0 $ defstruct Person (:name :string) + Person $ impl-traits Person0 MyFooImpl + p $ %{} Person (:name |Alice) + .foo p +``` + +`impl-traits` returns a new struct/enum definition with trait implementations attached. You can also attach multiple traits at once: + +```cirru +let + MyFoo $ deftrait MyFoo + .foo $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + ShowTrait $ deftrait ShowTrait + .show $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + EqTrait $ deftrait EqTrait + .eq $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + Person0 $ defstruct Person (:name :string) + ShowImpl $ defimpl ShowImpl ShowTrait + .show $ fn (p) (str |Person: (:name p)) + EqImpl $ defimpl EqImpl EqTrait + .eq $ fn (p) (str |eq: (:name p)) + MyFooImpl $ defimpl MyFooImpl MyFoo + .foo $ fn (p) (str |foo: (:name p)) + Person $ impl-traits Person0 ShowImpl EqImpl MyFooImpl + p $ %{} Person (:name |Alice) + [] (.show p) (.foo p) +``` + +## Trait checks and type hints + +`assert-traits` marks a local as having a trait and validates it at runtime: + +```cirru +let + MyFoo $ deftrait MyFoo + .foo $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + Person0 $ defstruct Person (:name :string) + MyFooImpl $ defimpl MyFooImpl MyFoo + .foo $ fn (p) (str-spaced |foo (:name p)) + Person $ impl-traits Person0 MyFooImpl + p $ %{} Person (:name |Alice) + assert-traits p MyFoo + .foo p +``` + +If the trait is missing or required methods are not implemented, `assert-traits` raises an error. + +## Built-in traits + +Core types provide built-in trait implementations (e.g. `Show`, `Eq`, `Compare`, `Add`, `Len`, `Mappable`). These are registered by the runtime, so values like numbers, strings, lists, maps, and records already satisfy common traits. + +## Notes + +- There is no inheritance. Behavior sharing is done via traits and `impl-traits`. +- Method calls resolve through attached trait impls first, then built-in implementations. +- Use `assert-traits` when a function relies on trait methods and you want early, clear failures. + +## Further reading + +- Dev log(中文) https://github.com/calcit-lang/calcit/discussions/44 +- Dev log in video(中文) https://www.bilibili.com/video/BV1Ky4y137cv diff --git a/docs/features/records.md b/docs/features/records.md new file mode 100644 index 00000000..e3b19f76 --- /dev/null +++ b/docs/features/records.md @@ -0,0 +1,290 @@ +# Records + +Calcit provides Records as a way to define structured data types with named fields, similar to structs in other languages. Records are defined with `defstruct` and instantiated with the `%{}` macro. + +## Quick Recipes + +- **Define**: `defstruct Point (:x :number) (:y :number)` +- **Create**: `%{} Point (:x 1) (:y 2)` +- **Access**: `get p :x` or `(:x p)` +- **Update**: `assoc p :x 10` or `update p :x inc` +- **Type Check**: `assert-type p :record` + +## Defining a Struct Type + +Use `defstruct` to declare a named type with typed fields: + +```cirru +defstruct Point (:x :number) (:y :number) +``` + +Each field is a pair of `(:field-name :type)`. Supported types include `:number`, `:string`, `:bool`, `:tag`, `:list`, `:map`, `:fn`, and `:dynamic` (untyped). + +```cirru +defstruct Person (:name :string) (:age :number) (:position :tag) +``` + +## Creating Records + +Use the `%{}` macro to instantiate a struct: + +```cirru +let + Point $ defstruct Point (:x :number) (:y :number) + p $ %{} Point (:x 1) (:y 2) + , p +``` + +Fields can also be written on separate lines: + +```cirru +let + Point $ defstruct Point (:x :number) (:y :number) + p $ %{} Point + :x 1 + :y 2 + , p +``` + +## Accessing Fields + +Use `get` (or `&record:get`) to read a field: + +```cirru +let + Point $ defstruct Point (:x :number) (:y :number) + p $ %{} Point (:x 1) (:y 2) + println $ get p :x + ; => 1 +``` + +Standard collection functions like `keys`, `count`, and `contains?` also work on records: + +```cirru +let + Point $ defstruct Point (:x :number) (:y :number) + p $ %{} Point (:x 1) (:y 2) + println $ keys p + ; => (#{} :x :y) + println $ count p + ; => 2 + println $ contains? p :x + ; => true +``` + +## Updating Fields + +Records are immutable. Use `assoc` or `record-with` to produce an updated copy: + +```cirru +let + Point $ defstruct Point (:x :number) (:y :number) + p $ %{} Point (:x 1) (:y 2) + p2 $ assoc p :x 10 + println p2 + ; => (%{} :Point (:x 10) (:y 2)) + println p + ; p is unchanged: (%{} :Point (:x 1) (:y 2)) +``` + +```cirru +let + Person $ defstruct Person (:name :string) (:age :number) (:position :tag) + p $ %{} Person (:name |Chen) (:age 20) (:position :mainland) + p2 $ record-with p (:age 21) (:position :shanghai) + println p2 + ; p2 has updated :age and :position, :name is unchanged +``` + +`&record:assoc` is the low-level variant (no type checking): + +```cirru +let + Point $ defstruct Point (:x :number) (:y :number) + p $ %{} Point (:x 1) (:y 2) + println $ &record:assoc p :x 100 +``` + +## Partial Records + +Use `%{}?` to create a record with only some fields set (others default to `nil`): + +```cirru +let + Person $ defstruct Person (:name :string) (:age :number) (:position :tag) + p1 $ %{}? Person (:name |Chen) + println $ get p1 :name + ; => |Chen + println $ get p1 :age + ; => nil +``` + +The low-level `&%{}` form accepts fields as flat keyword-value pairs (no type checking): + +```cirru +let + Person $ defstruct Person (:name :string) (:age :number) (:position :tag) + println $ &%{} Person :name |Chen :age 20 :position :mainland +``` + +## Type Checking + +```cirru +let + Point $ defstruct Point (:x :number) (:y :number) + p $ %{} Point (:x 1) (:y 2) + ; check if a value is a record (struct instance) + println $ record? p + ; => true + ; check if it matches a specific struct + println $ &record:matches? p Point + ; => true + ; get the struct definition the record was created from + println $ &record:struct p + ; compare structs directly for origin check + println $ = (&record:struct p) Point + ; => true + ; struct? checks struct definitions, not instances + println $ struct? Point + ; => true + println $ struct? p + ; => false +``` + +## Pattern Matching + +Use `record-match` to branch on record types: + +```cirru +let + Circle $ defstruct Circle (:radius :number) + Square $ defstruct Square (:side :number) + shape $ %{} Circle (:radius 5) + record-match shape + Circle c $ * 3.14 (* (get c :radius) (get c :radius)) + Square s $ * (get s :side) (get s :side) + _ _ nil +; => 78.5 +``` + +## Converting Records + +### To Map + +```cirru +let + Point $ defstruct Point (:x :number) (:y :number) + p $ %{} Point (:x 1) (:y 2) + println $ &record:to-map p + ; => {} (:x 1) (:y 2) +``` + +`merge` also works and returns a new record of the same struct: + +```cirru +let + Person $ defstruct Person (:name :string) (:age :number) (:position :tag) + p $ %{} Person (:name |Chen) (:age 20) (:position :mainland) + println $ merge p $ {} (:age 23) (:name |Ye) +``` + +## Record Name and Struct Inspection + +```cirru +let + Person $ defstruct Person (:name :string) (:age :number) (:position :tag) + p $ %{} Person (:name |Chen) (:age 20) (:position :mainland) + ; get the tag name of the record + println $ &record:get-name p + ; => :Person + ; check the struct behind a record value + println $ &record:struct p +``` + +### Struct Origin Check + +Compare struct definitions directly when you need to confirm a record's origin: + +```cirru +let + Cat $ defstruct Cat (:name :string) (:color :tag) + Dog $ defstruct Dog (:name :string) + v1 $ %{} Cat (:name |Mimi) (:color :white) + if (= (&record:struct v1) Cat) + println "|Handle Cat branch" + println "|Not a Cat" +``` + +## Polymorphism with Traits + +Define a trait with `deftrait`, implement it with `defimpl`, and attach it to a struct with `impl-traits`: + +```cirru +let + BirdTrait $ deftrait BirdTrait + .show $ :: :fn $ {} + :args $ [] 'T + :return :nil + .rename $ :: :fn $ {} + :args $ [] 'T :string + :return 'T + BirdShape $ defstruct BirdShape (:name :string) + BirdImpl $ defimpl BirdImpl BirdTrait + .show $ fn (self) + println $ get self :name + .rename $ fn (self name) + assoc self :name name + Bird $ impl-traits BirdShape BirdImpl + b $ %{} Bird (:name |Sparrow) + .show b + let + b2 $ .rename b |Eagle + .show b2 +``` + +## Common Use Cases + +### Configuration Objects + +```cirru +let + Config $ defstruct Config (:host :string) (:port :number) (:debug :bool) + config $ %{} Config (:host |localhost) (:port 3000) (:debug false) + println $ get config :port + ; => 3000 +``` + +### Domain Models + +```cirru +let + Product $ defstruct Product (:id :string) (:name :string) (:price :number) (:discount :number) + product $ %{} Product + :id |P001 + :name |Widget + :price 100 + :discount 0.9 + println $ * (get product :price) (get product :discount) + ; => 90 +``` + +## Type Annotations + +```cirru +let + User $ defstruct User (:name :string) (:age :number) (:email :string) + get-user-name $ fn (user) + hint-fn $ {} (:args ([] (:: :record User))) (:return :string) + get user :name + println $ get-user-name $ %{} User + :name |John + :age 30 + :email |john@example.com +; => John +``` + +## Performance Notes + +- Records are immutable — updates create new records +- Field access is O(1) +- Use `record-with` to update multiple fields at once and minimize intermediate allocations diff --git a/docs/features/sets.md b/docs/features/sets.md new file mode 100644 index 00000000..e68180b7 --- /dev/null +++ b/docs/features/sets.md @@ -0,0 +1,151 @@ +# Sets + +Calcit provides HashSet data structure for storing unordered unique elements. In Rust implementation, it uses `rpds::HashTrieSet`, while in JavaScript it uses a custom implementation based on ternary-tree. + +## Quick Recipes + +- **Create**: `#{:a :b :c}` +- **Add/Remove**: `include s :d`, `exclude s :a` +- **Check**: `&set:includes? s :a` +- **Operations**: `union s1 s2`, `difference s1 s2`, `intersection s1 s2` +- **Convert**: `&set:to-list s` + +## Creating Sets + +Use `#{}` to create a set: + +```cirru +#{} :a :b :c + +#{} 1 2 3 4 5 +``` + +Create an empty set: + +```cirru +#{} +``` + +## Basic Operations + +### Adding and Removing Elements + +```cirru +; Add element +include (#{} :a :b) :c +; => #{:a :b :c} + +; Remove element +exclude (#{} :a :b :c) :b +; => #{:a :c} +``` + +### Checking Membership + +```cirru +&set:includes? (#{} :a :b :c) :a +; => true + +&set:includes? (#{} :a :b :c) :x +; => false +``` + +### Set Operations + +```cirru +; Union - elements in either set +union (#{} :a :b) (#{} :b :c) +; => #{:a :b :c} + +; Difference - elements in first but not second +difference (#{} :a :b :c) (#{} :b :c :d}) +; => #{:a} + +; Intersection - elements in both sets +intersection (#{} :a :b :c) (#{} :b :c :d}) +; => #{:b :c} +``` + +## Converting Between Types + +```cirru +; Convert set to list +&set:to-list (#{} :a :b :c) +; => ([] :a :b :c) ; order may vary + +; Convert list to set +&list:to-set ([] :a :b :b :c) +; => #{:a :b :c} +``` + +## Set Properties + +```cirru +; Get element count +&set:count (#{} :a :b :c) +; => 3 + +; Check if empty +&set:empty? (#{}) +; => true +``` + +## Filtering + +```cirru +&set:filter (#{} 1 2 3 4 5) + fn (x) (> x 2) +; => #{3 4 5} +``` + +## Pattern Matching with Sets + +Use `&set:destruct` to destructure sets: + +```cirru +&set:destruct (#{} :a :b :c) +; Returns a list of elements +``` + +## Common Use Cases + +### Removing Duplicates from a List + +```cirru +-> ([] :a :b :a :c :b) + &list:to-set + &set:to-list +; => ([] :a :b :c) ; order may vary +``` + +### Checking for Unique Elements + +```cirru += (&set:count (#{} :a :b :c)) + count ([] :a :b :c) +; => true if all elements are unique +``` + +### Set Membership in Algorithms + +```cirru +let + visited $ #{} :page1 :page2 + if (&set:includes? visited :page3) + println "|Already visited" + println "|New page found" +``` + +## Type Annotations + +```cirru +defn process-tags (tags) + hint-fn $ {} (:args ([] :set)) (:return :set) + &set:filter tags $ fn (t) (not= t :draft) +``` + +## Performance Notes + +- Set operations (union, intersection, difference) are efficient due to persistent data structure sharing +- Membership tests (`&set:includes?`) are O(1) average case +- Sets are immutable - all operations return new sets diff --git a/docs/features/static-analysis.md b/docs/features/static-analysis.md new file mode 100644 index 00000000..46e66949 --- /dev/null +++ b/docs/features/static-analysis.md @@ -0,0 +1,500 @@ +# Static Type Analysis + +Calcit includes a built-in static type analysis system that performs compile-time checks to catch common errors before runtime. This system operates during the preprocessing phase and provides warnings for type mismatches and other potential issues. + +## Quick Recipes + +- **Assert Type**: `assert-type total :number` +- **Local `fn` Hint**: `hint-fn $ {} (:args ([] :number)) (:return :number)` +- **Top-level `defn` Schema**: `cr edit schema app.main/add -e ':: :fn $ {} (:args $ [] :number :number) (:return :number)'` +- **Return Type**: `hint-fn $ {} (:return :string)` +- **Compact Hint**: `defn my-fn (x) :string ...` +- **Check Traits**: `assert-traits x MyTrait` +- **Ignore Warning**: `&core:ignore-type-warning` + +## Overview + +The static analysis system provides: + +- **Type inference** - Automatically infers types from literals and expressions +- **Type annotations** - Optional type hints for function parameters and return values +- **Compile-time warnings** - Catches errors before code execution +- **Composable runtime assertions** - `assert-type` and `assert-traits` can validate values at runtime and return original values for chaining + +## Type Annotations + +### Function Parameter Types + +Function parameters should be annotated with function schema: + +- top-level `defn` / `defmacro`: prefer `:schema` +- local `fn`: use `hint-fn` with `:args` / `:rest` + +For namespace-level definitions, `:schema` is stored on the definition entry and is typically edited with `cr edit schema`, rather than written inline in the function body. + +`assert-type` is still useful, but mainly for local variables, intermediate values, and explicit checks inside the function body. + +Runnable Example: + +```cirru +let + calculate-total $ fn (items) + hint-fn $ {} (:args ([] :list)) (:return :number) + reduce items 0 + fn (acc item) + hint-fn $ {} (:args ([] :number :number)) (:return :number) + + acc item + calculate-total $ [] 1 2 3 +``` + +### Return Type Annotations + +There are two ways to specify return types: + +#### 1. Local `fn` Hint (`hint-fn`) + +Use `hint-fn` with schema map at the start of a local function body: + +Legacy clause syntax such as `(hint-fn (return-type ...))`, `(generics ...)`, and `(type-vars ...)` is no longer supported and now fails during preprocessing. + +```cirru +let + get-name $ fn (user) + hint-fn $ {} (:args ([] :dynamic)) (:return :string) + , |demo + get-name nil +``` + +#### 2. Compact Hint (Trailing Label) + +For `defn` and `fn`, you can place a type label immediately after the parameters: + +```cirru +let + add $ fn (a b) :number + + a b + add 10 20 +``` + +For namespace-level `defn` / `defmacro`, parameter and return metadata should still live in `:schema`. + +### Multiple Annotations + +```cirru +let + add $ fn (a b) :number + hint-fn $ {} (:args ([] :number :number)) + let + total $ + a b + assert-type total :number + total + add 1 2 +``` + +## Supported Types + +The following type tags are supported: + +| Tag | Calcit Type | +| ------------------- | ------------------- | +| `:nil` | Nil | +| `:bool` | Boolean | +| `:number` | Number | +| `:string` | String | +| `:symbol` | Symbol | +| `:tag` | Tag (Keyword) | +| `:list` | List | +| `:map` | Hash Map | +| `:set` | Set | +| `:tuple` | Tuple (general) | +| `:fn` | Function | +| `:ref` | Atom / Ref | +| `:any` / `:dynamic` | Any type (wildcard) | + +### Complex Types + +#### Optional Types + +Represent values that can be `nil`. Use the `:: :optional ` syntax: + +```cirru +let + greet $ fn (name) + hint-fn $ {} (:args ([] (:: :optional :string))) (:return :string) + str "|Hello " (or name "|Guest") + greet nil +``` + +#### Variadic Types + +Represent variable arguments in `&` parameters: + +```cirru +let + sum $ fn (& xs) + hint-fn $ {} (:rest :number) (:return :number) + reduce xs 0 &+ + sum 1 2 3 +``` + +#### Record and Enum Types + +Use the name defined by `defstruct` or `defenum`: + +```cirru +let + User $ defstruct User (:name :string) + get-name $ fn (u) + hint-fn $ {} (:args ([] (:: :record User))) (:return :string) + get u :name + get-name $ %{} User (:name |Alice) +``` + +## Built-in Type Checks + +### Function Arity Checking + +The system validates that function calls have the correct number of arguments: + +```cirru +defn greet (name age) + str "|Hello " name "|, you are " age + +; Error: expects 2 args but got 1 +; greet |Alice +``` + +### Record Field Access + +Validates that record fields exist: + +```cirru +defstruct User (:name :string) (:age :number) + +defn get-user-email (user) + .-email user + ; Warning: field 'email' not found in record User + ; Available fields: name, age +``` + +### Tuple Index Bounds + +Checks tuple index access at compile time: + +```cirru.no-check +let + point (%:: :Point 10 20 30) + &tuple:nth point 5 ; Warning: index 5 out of bounds, tuple has 4 elements +``` + +### Enum Variant Validation + +Validates enum construction and pattern matching: + +```cirru.no-check +defenum Result + :Ok :any + :Error :string + +; Warning: variant 'Failure' not found in enum Result +%:: Result :Failure "|something went wrong" +; Available variants: Ok, Error + +; Warning: variant 'Ok' expects 1 payload but got 2 +%:: Result :Ok 42 |extra +``` + +### Method Call Validation + +Checks that methods exist for the receiver type: + +```cirru +defn process-list (xs) + ; .unknown-method xs + println "|demo code" + ; "Warning: unknown method .unknown-method for :list" + ; Available methods: .map, .filter, .count, ... +``` + +### Recur Arity Checking + +Validates that `recur` calls have the correct number of arguments: + +```cirru +defn factorial (n acc) + if (<= n 1) acc + recur (dec n) (* n acc) + ; Warning: recur expects 2 args but got 3 + ; recur (dec n) (* n acc) 999 +``` + +**Note**: Recur arity checking automatically skips: + +- Functions with variadic parameters (`&` rest args) +- Functions with optional parameters (`?` markers) +- Macro-generated functions (e.g., from `loop` macro) +- `calcit.core` namespace functions + +## Type Inference + +The system infers types from various sources: + +### Literal Types + +```cirru +let + ; inferred as :number + x 42 + ; inferred as :string + y |hello + ; inferred as :bool + z true + ; inferred as :nil + w nil + [] x y z w +``` + +### Function Return Types + +```cirru +let + ; inferred as :list + numbers $ range 10 + ; inferred as :number + n $ &list:first numbers + [] n numbers +``` + +### Record and Struct Types + +```cirru +let + Point $ defstruct Point (:x :number) (:y :number) + p $ %{} Point (:x 10) (:y 20) + x-val (:x p) + ; x-val inferred as :number from field type + assert= x-val 10 +``` + +## Type Assertions + +Use `assert-type` to explicitly check local values during preprocessing: + +```cirru +let + transform-fn $ fn (x) (* x 2) + process-data $ fn (data) + hint-fn $ {} (:args ([] :list)) (:return :list) + let + xs data + assert-type xs :list + &list:map xs transform-fn + process-data ([] 1 2 3) +``` + +**Note**: `assert-type` is evaluated during preprocessing and removed at runtime, so there's no performance penalty. + +## Type Inspection Tool + +Use `&inspect-type` to debug type inference. Pass a symbol name and the inferred type is printed to stderr during preprocessing: + +```cirru +let + x 10 + nums $ [] 1 2 3 + assert-type nums :list + ; Prints: [&inspect-type] x => number type + &inspect-type x + ; Prints: [&inspect-type] nums => list type + &inspect-type nums + let + item $ &list:nth nums 0 + ; Prints: [&inspect-type] item => dynamic type + &inspect-type item + assert-type item :number + ; Prints: [&inspect-type] item => number type + &inspect-type item +``` + +**Note**: This is a development tool - remove it in production code. Returns `nil` at runtime. + +## Optional Types + +Calcit supports optional type annotations for nullable values: + +Definition: + +```cirru +defn find-user (id) + ; May return nil if user not found + println "|demo code" +``` + +Schema on the namespace definition: + +```cirru +:: :fn $ {} (:args $ [] :dynamic) (:return (:: :optional :record)) +``` + +## Variadic Types + +Functions with rest parameters use variadic type annotations: + +Definition: + +```cirru +defn sum (& numbers) + reduce numbers 0 + +``` + +Schema on the namespace definition: + +```cirru +:: :fn $ {} (:rest :number) (:return :number) +``` + +## Function Types + +Functions can be typed as `:fn` in schema: + +Definition: + +```cirru +defn apply-twice (f x) + f (f x) +``` + +Schema on the namespace definition: + +```cirru +:: :fn $ {} (:args $ [] :fn :number) (:return :number) +``` + +## Disabling Checks + +### Per-Function + +Skip checks for specific functions by naming them with special markers: + +- Functions with `%` in the name (macro-generated) +- Functions with `$` in the name (special markers) +- Functions starting with `__` (internal functions) + +### Per-Namespace + +Checks are automatically skipped for: + +- `calcit.core` namespace (external library) +- Functions with variadic or optional parameters (complex arity rules) + +## Best Practices + +### 1. Use Type Annotations for Public APIs + +```cirru +let + process-input $ fn (input) (assoc input :processed true) + public-api-function $ fn (input) + hint-fn $ {} (:args ([] :map)) (:return :string) + let + processed $ process-input input + assert-type processed :map + str processed + public-api-function ({} (:data |hello)) +``` + +### 2. Leverage Type Inference + +Let the system infer types from literals and function calls: + +```cirru +defn calculate-area (width height) + ; Types inferred from arithmetic operations + * width height +``` + +### 3. Add Assertions for Critical Code + +```cirru +let + dangerous-operation $ fn (data) (map data (fn (x) (* x 2))) + critical-operation $ fn (data) + hint-fn $ {} (:args ([] :list)) (:return :list) + let + checked data + assert-type checked :list + ; Ensure the local value is still what we expect before processing + dangerous-operation checked + critical-operation ([] 1 2 3) +``` + +### 4. Document Complex Types + +Definition: + +```cirru +; Function that takes a map with specific keys +defn process-user (user-map) + ; Expected keys: :name :email :age + println "|demo code" +``` + +Schema on the namespace definition: + +```cirru +:: :fn $ {} (:args $ [] :map) +``` + +## Limitations + +1. **Dynamic Code**: Type checks don't apply to dynamically generated code +2. **JavaScript Interop**: JS function calls are not type-checked +3. **Macro Expansion**: Some macros may generate code that bypasses checks +4. **Runtime Polymorphism**: Type checks are conservative with polymorphic code + +## Error Messages + +Type check warnings include: + +- **Location information**: namespace, function, and code location +- **Expected vs actual types**: clear description of the mismatch +- **Available options**: list of valid fields/methods/variants + +Example warning: + +``` +[Warn] Tuple index out of bounds: tuple has 3 element(s), but trying to access index 5, at my-app.core/process-point +``` + +## Advanced Topics + +### Custom Type Predicates + +While Calcit doesn't support custom type predicates in the static analysis system yet, you can use runtime checks: + +```cirru +defn is-positive? (n) + and (number? n) (> n 0) +``` + +### Type-Driven Development + +1. Write function signatures with type annotations +2. Let the compiler guide implementation +3. Use warnings to catch edge cases +4. Add assertions for invariants + +### Performance + +Static type analysis: + +- Runs during preprocessing phase +- Zero runtime overhead +- Only checks functions that are actually called +- Cached between hot reloads (incremental) + +## See Also + +- [Polymorphism](polymorphism.md) - Object-oriented programming patterns +- [Macros](macros.md) - Metaprogramming and code generation +- [Data](../data.md) - Data types and structures diff --git a/docs/features/traits.md b/docs/features/traits.md new file mode 100644 index 00000000..f09ed229 --- /dev/null +++ b/docs/features/traits.md @@ -0,0 +1,340 @@ +# Traits + +Calcit provides a lightweight trait system for attaching method implementations to struct/enum definitions (and using them from constructed instances and built-in types). + +It complements the “class-like” polymorphism described in [Polymorphism](polymorphism.md): + +- Struct/enum **classes** are about “this concrete type has these methods”. +- **Traits** are about “this value supports this capability (set of methods)”. + +## Quick Recipes + +- **Define Trait**: `deftrait MyTrait .method (:: :fn $ {} ...)` +- **Implement Trait**: `defimpl MyImpl MyTrait .method (fn (x) ...)` +- **Attach to Struct**: `impl-traits MyStruct MyImpl` +- **Call Method**: `.method instance` +- **Check Trait**: `assert-traits instance MyTrait` + +## Define a trait + +Use `deftrait` to define a trait and its method signatures (including type annotations). + +```cirru +deftrait MyFoo + .foo $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string +``` + +## Implement a trait + +Use `defimpl` to create an impl record for a trait. + +```cirru +let + MyFoo $ deftrait MyFoo + .foo $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + Person0 $ defstruct Person (:name :string) + MyFooImpl $ defimpl MyFooImpl MyFoo + .foo $ fn (p) + str-spaced |foo (:name p) + Person $ impl-traits Person0 MyFooImpl + p $ %{} Person (:name |Alice) + .foo p +``` + +### Impl-related syntax (cheatsheet) + +**1) `defimpl` argument order (breaking change)** + +``` +defimpl ImplName Trait ... +``` + +- First argument is the **impl record name**. +- Second argument is the **trait value** (symbol) or a **tag**. + +Examples: + +```cirru +do + ; Form 1: symbol names for impl and trait + let + PersonA0 $ defstruct PersonA (:name :string) + MyFooA $ deftrait MyFooA + .foo $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + MyFooImplA $ defimpl MyFooImplA MyFooA + .foo $ fn (p) (str-spaced |foo (:name p)) + PersonA $ impl-traits PersonA0 MyFooImplA + p $ %{} PersonA (:name |Alice) + .foo p + ; Form 2: tag keywords for impl and trait (no deftrait needed) + let + PersonB0 $ defstruct PersonB (:name :string) + MyFooImplB $ defimpl :MyFooImplB :MyFooB + .foo $ fn (p) (str-spaced |bar (:name p)) + PersonB $ impl-traits PersonB0 MyFooImplB + p $ %{} PersonB (:name |Bob) + .foo p +``` + +**2) Method pair forms** + +Prefer dot-style keys (`.foo`). Legacy tag keys (`:foo`) are still accepted for compatibility. + +```cirru +; Both forms are accepted and equivalent: +do + let + MyFoo $ deftrait MyFoo + .foo $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + Person0 $ defstruct Person (:name :string) + ; Form 1: preferred .method keys + ImplA $ defimpl ImplA MyFoo + .foo (fn (p) (str |A: (:name p))) + PersonA $ impl-traits Person0 ImplA + pa $ %{} PersonA (:name |Alice) + .foo pa + let + MyFoo $ deftrait MyFoo + .foo $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + Person0 $ defstruct Person (:name :string) + ; Form 2: legacy tag keys (compatible) + ImplB $ defimpl ImplB MyFoo + :: :foo (fn (p) (str |B: (:name p))) + PersonB $ impl-traits Person0 ImplB + pb $ %{} PersonB (:name |Bob) + .foo pb +``` + +**3) Tag-based impl (no concrete trait value)** + +If you need a pure marker and don’t want to bind to a real trait value, use tags: + +```cirru +defimpl :MyMarkerImpl :MyMarker + .dummy nil +``` + +This is also a safe replacement for the old self-referential pattern +`defimpl X X`, which can cause recursion in new builds. + +Implementation notes: + +- `defimpl` creates an “impl record” that stores the trait as its origin. +- This origin is used by `&trait-call` to match the correct implementation when method names overlap. + +## Attach impls to struct/enum definitions + +`impl-traits` attaches impl records to a **struct/enum type**. For user values, later impls override earlier impls for the same method name ("last-wins"). + +Constraints: + +- `impl-traits` only accepts **struct/enum** values. +- Record/tuple instances must be created from a struct/enum that already has impls attached (`%{}` or `%::`). + +Syntax: + +```cirru +let + MyFoo $ deftrait MyFoo + .foo $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + ImplA $ defimpl ImplA MyFoo + .foo $ fn (p) (str |A: (:name p)) + ImplB $ defimpl :ImplB :ImplB-trait + .bar $ fn (p) (str |B: (:name p)) + StructDef0 $ defstruct StructDef (:name :string) + StructDef $ impl-traits StructDef0 ImplA ImplB + x $ %{} StructDef (:name |test) + .foo x +``` + +### Public vs internal API boundary + +- Prefer public API in app/library code: `deftrait`, `defimpl`, `impl-traits`, `.method`, `&trait-call`. +- Treat internal `&...` helpers as runtime-level details; they may change more frequently and are not the stable user contract. + +```cirru +do + ; struct example + let + MyFoo $ deftrait MyFoo + .foo $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + MyFooImpl $ defimpl MyFooImpl MyFoo + .foo $ fn (p) (str-spaced |foo (:name p)) + Person0 $ defstruct Person (:name :string) + Person $ impl-traits Person0 MyFooImpl + p $ %{} Person (:name |Alice) + .foo p + ; enum example + let + ResultTrait $ deftrait ResultTrait + .describe $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + ResultImpl $ defimpl ResultImpl ResultTrait + .describe $ fn (x) + tag-match x + (:ok v) (str |ok: v) + (:err v) (str |err: v) + Result0 $ defenum Result0 (:ok :string) (:err :string) + MyResult $ impl-traits Result0 ResultImpl + r $ %:: MyResult :ok |done + .describe r +``` + +### Static analysis boundary + +For preprocess to resolve impls and inline methods, keep struct/enum definitions and `impl-traits` at **top-level `ns/def`**. If they are created inside `defn`/`defmacro` bodies, preprocess only sees dynamic values and method dispatch cannot be specialized. + +When running `warn-dyn-method`, preprocess emits extra diagnostics for: + +- `.method` call sites that have multiple trait candidates with the same method name. +- `impl-traits` used inside function/macro bodies (non-top-level attachment). + +## Docs as tests + +Key trait docs examples are mirrored by executable smoke cases in `calcit/test-doc-smoke.cirru`, including: + +- `defimpl` argument order (`ImplName` then `Trait`) +- `assert-traits` local-first requirement +- `impl-traits` only accepting struct/enum definitions + +## Method call vs explicit trait call + +Normal method invocation uses `.method` dispatch. If multiple traits provide the same method name, `.method` resolves by impl precedence. + +When you want to **disambiguate** (or bypass `.method` resolution), use `&trait-call`. + +### `&trait-call` + +Usage: `&trait-call Trait :method receiver & args` + +`&trait-call` matches by the impl record's trait origin, not just by trait name text. This avoids accidental dispatch when two different trait values share the same printed name. + +Example with two traits sharing the same method name: + +```cirru +let + MyZapA $ deftrait MyZapA + .zap $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + MyZapB $ deftrait MyZapB + .zap $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + MyZapAImpl $ defimpl MyZapAImpl MyZapA + .zap $ fn (_x) |zapA + MyZapBImpl $ defimpl MyZapBImpl MyZapB + .zap $ fn (_x) |zapB + Person0 $ defstruct Person (:name :string) + Person $ impl-traits Person0 MyZapAImpl MyZapBImpl + p $ %{} Person (:name |Alice) + ; .zap follows normal dispatch (last-wins for user impls) + .zap p + ; explicitly pick a trait’s implementation + &trait-call MyZapA :zap p + &trait-call MyZapB :zap p +``` + +## Debugging / introspection + +Two helpers are useful when debugging trait + method dispatch: + +- `&methods-of` returns a list of available method names (strings, including the leading dot). +- `&inspect-methods` prints impl records and methods to stderr, and returns the value unchanged. +- `&impl:origin` returns the trait origin stored on an impl record (or nil). + +```cirru +let + xs $ [] 1 2 + &methods-of xs + &inspect-methods xs "|list" +``` + +You can also inspect impl origins directly when validating trait dispatch: + +```cirru +let + MyFoo $ deftrait MyFoo + .foo $ :: :fn ('T) ('T) :string + Shape0 $ defenum Shape (:point :number :number) + MyFooImpl $ defimpl MyFooImpl MyFoo + .foo $ fn (t) (str |shape: (&tuple:nth t 0)) + Shape $ impl-traits Shape0 MyFooImpl + some-tuple $ %:: Shape :point 10 20 + impls $ &tuple:impls some-tuple + any? impls $ fn (impl) + = (&impl:origin impl) MyFoo +``` + +## Checking trait requirements + +`assert-traits` checks at runtime that a value implements a trait (i.e. it provides all required methods). It returns the value unchanged if the check passes. + +Notes: + +- `assert-traits` is syntax (expanded to `&assert-traits`) and its first argument must be a **local**. +- For built-in values (list/map/set/string/number/...), `assert-traits` only validates default implementations. It **does not** extend methods at runtime. +- Static analysis and runtime checks may diverge for built-ins due to limited compile-time information; this mismatch is currently allowed. + +```cirru +let + MyFoo $ deftrait MyFoo + .foo $ :: :fn $ {} + :generics $ [] 'T + :args $ [] 'T + :return :string + Person0 $ defstruct Person (:name :string) + MyFooImpl $ defimpl MyFooImpl MyFoo + .foo $ fn (p) (str-spaced |foo (:name p)) + Person $ impl-traits Person0 MyFooImpl + p $ %{} Person (:name |Alice) + assert-traits p MyFoo + .foo p +``` + +### Examples (verified with `cr eval`) + +```bash +cargo run --bin cr -- demos/compact.cirru eval 'let ((xs ([] 1 2 3))) (assert= xs (assert-traits xs calcit.core/Len)) (.len xs)' +``` + +Expected output: + +```text +3 +``` + +```bash +cargo run --bin cr -- demos/compact.cirru eval 'let ((xs ([] 1 2 3))) (assert= xs (assert-traits xs calcit.core/Mappable)) (.map xs inc)' +``` + +Expected output: + +```text +([] 2 3 4) +``` diff --git a/docs/features/tuples.md b/docs/features/tuples.md new file mode 100644 index 00000000..176d6b45 --- /dev/null +++ b/docs/features/tuples.md @@ -0,0 +1,255 @@ +# Tuples + +Tuples in Calcit are tagged unions that can hold multiple values with a tag. They are used for representing structured data and are the foundation for records and enums. + +## Quick Recipes + +- **Create**: `:: :point 10 20` +- **Create Typed**: `%:: Shape :circle 5` +- **Access**: `&tuple:nth t 1` +- **Match**: `tag-match t ((:point x y) ...)` +- **Update**: `&tuple:assoc t 1 99` + +## Creating Tuples + +### Shorthand Syntax + +Use `::` to create a tuple with a tag: + +```cirru +let + result 42 + message |error-occurred + do + :: :point 10 20 + :: :ok result + :: :err message +``` + +### With Class Syntax + +Use `%::` to create a typed tuple from an enum: + +```cirru +let + Shape $ defenum Shape (:point :number :number) (:circle :number) + %:: Shape :point 10 20 +``` + +## Tuple Structure + +A tuple consists of: + +- **Tag**: A keyword identifying the tuple type (index 0) +- **Class**: Optional class metadata (hidden) +- **Parameters**: Zero or more values (indices 1+) + +```cirru +let + t $ :: :point 10 20 + ; Index 0: :point, Index 1: 10, Index 2: 20 + [] (&tuple:nth t 0) (&tuple:nth t 1) (&tuple:nth t 2) +``` + +## Accessing Tuple Elements + +```cirru +let + t $ :: :point 10 20 + &tuple:nth t 0 + ; => :point + + &tuple:nth t 1 + ; => 10 + + &tuple:nth t 2 + ; => 20 +``` + +## Tuple Properties + +```cirru +let + t $ :: :point 10 20 + do + ; count includes the tag + &tuple:count (:: :a 1 2 3) + ; => 4 + &tuple:params t + ; => ([] 10 20) + &tuple:enum t + ; => nil (plain tuple, not from enum) +``` + +`&tuple:enum` is the source-prototype API for tuples: + +- If tuple is created from enum (`%::`), it returns that enum value. +- If tuple is created as plain tuple (`::`), it returns `nil`. + +```cirru +do + let + plain $ :: :point 10 20 + nil? $ &tuple:enum plain + ; => true + let + ApiResult $ defenum ApiResult (:ok :number) (:err :string) + ok $ %:: ApiResult :ok 1 + assert= ApiResult $ &tuple:enum ok +``` + +### Accurate Origin Check (Enum Eq) + +```cirru +let + ApiResult $ defenum ApiResult (:ok :number) (:err :string) + x $ %:: ApiResult :ok 1 + assert= (&tuple:enum x) ApiResult +``` + +### Complex Branching Example (Safe + Validation) + +```cirru +do + defenum Result + :ok :number + :err :string + let + xs $ [] + %:: Result :ok 1 + %:: Result :err |bad + :: :plain 42 + if (nil? (&tuple:enum (&list:nth xs 2))) + if (= (&tuple:enum (&list:nth xs 0)) Result) + , |result-and-plain + , |result-missing + , |unexpected +``` + +## Updating Tuples + +```cirru +; Update element at index +&tuple:assoc (:: :point 10 20) 1 100 +; => (:: :point 100 20) +``` + +## Pattern Matching with Tuples + +### tag-match + +Pattern match on enum/tuple tags: + +```cirru +let + MyResult $ defenum MyResult (:ok :number) (:err :string) + result $ %:: MyResult :ok 42 + tag-match result + (:ok v) (str |Success: v) + (:err msg) (str |Error: msg) + _ |Unknown +``` + +### list-match + +For simple list-like destructuring: + +```cirru +; list-match takes (head rest) branches — rest captures remaining elements as a list +list-match ([] :point 10 20) + () |Empty + (h tl) ([] h tl) +``` + +## Enums as Tuples + +Enums are specialized tuples with predefined variants: + +```cirru +; Define enum +defenum Option + :some :dynamic + :none + +; Create enum instances +%:: Option :some 42 +%:: Option :none + +; Check variant +&tuple:enum-has-variant? Option :some +; => true + +; Get variant arity +&tuple:enum-variant-arity Option :some +; => 1 +``` + +## Common Use Cases + +### Result Types + +```cirru +let + MyResult $ defenum MyResult (:ok :number) (:err :string) + divide $ defn divide (a b) + if (= b 0) + %:: MyResult :err |Division-by-zero + %:: MyResult :ok (/ a b) + result $ divide 10 2 + tag-match result + (:ok value) (str |ok: value) + (:err msg) (str |err: msg) +``` + +### Optional Values + +```cirru +let + MaybeInt $ defenum MaybeInt (:some :number) (:none) + find-item $ fn (items target) + reduce items (%:: MaybeInt :none) + fn (acc x) + if (= x target) (%:: MaybeInt :some x) acc + result $ find-item ([] 1 2 3) 2 + tag-match result + (:some v) v + _ |not-found +``` + +### Tagged Data + +```cirru +; Represent different message types +:: :greeting |Hello +:: :number 42 +:: :list ([] 1 2 3) +``` + +## Type Annotations + +```cirru +let + ApiResult $ defenum ApiResult (:ok :string) (:err :string) + process-result $ defn process-result (r) + hint-fn $ {} (:args ([] :dynamic)) (:return :string) + tag-match r + (:ok v) (str v) + (:err msg) msg + process-result (%:: ApiResult :ok |done) +``` + +## Tuple vs Record + +| Feature | Tuple | Record | +| --------- | ------------- | ------------------ | +| Access | By index | By field name | +| Structure | Tag + params | Named fields | +| Methods | Via class | Via traits | +| Use case | Tagged unions | Structured objects | + +## Performance Notes + +- Tuples are immutable +- Element access is O(1) +- `&tuple:assoc` creates a new tuple +- Use records for complex objects with named fields diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 00000000..c5979bfe --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,33 @@ +cargo install calcit + +# Installation + +To install Calcit, you first need to install Rust. Then, you can install Calcit using Rust's package manager: + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +After installing Rust, install Calcit with: + +```bash +cargo install calcit +``` + +Once installed, Calcit is available as a command-line tool. You can test it with: + +```bash +cr eval "echo |done" +``` + +### Binaries + +Several binaries are included: + +- `cr`: the main command-line tool for running Calcit programs +- `bundle_calcit`: bundles Calcit code into a `compact.cirru` file +- `caps`: downloads Calcit packages +- `cr-mcp`: provides a Model Context Protocol (MCP) server for Calcit compact files +- `cr-sync`: syncs changes from `compact.cirru` back to `calcit.cirru` + +Another important command is `ct`, which is the "Calcit Editor" and is available in a separate repository. diff --git a/docs/installation/ffi-bindings.md b/docs/installation/ffi-bindings.md new file mode 100644 index 00000000..46f5ea86 --- /dev/null +++ b/docs/installation/ffi-bindings.md @@ -0,0 +1,87 @@ +# Rust bindings + +> API status: unstable. + +Rust supports extending with dynamic libraries. A demo project can be found at https://github.com/calcit-lang/dylib-workflow + +Currently two APIs are supported, based on Cirru EDN data. + +First one is a synchronous [Edn](https://github.com/Cirru/cirru-edn.rs) API with type signature: + +```rust +#[no_mangle] +pub fn demo(args: Vec) -> Result { +} +``` + +The other one is an asynchorous API, it can be called multiple times, which relies on `Arc` type(not sure if we can find a better solution yet), + +```rust +#[no_mangle] +pub fn demo( + args: Vec, + handler: Arc) -> Result + Send + Sync + 'static>, + finish: Box, +) -> Result { +} +``` + +in this snippet, the function `handler` is used as the callback, which could be called multiple times. + +The function `finish` is used for indicating that the task has finished. It can be called once, or not being called. +Internally Calcit tracks with a counter to see if all asynchorous tasks are finished. +Process need to keep running when there are tasks running. + +Asynchronous tasks are based on threads, which is currently decoupled from core features of Calcit. We may need techniques like `tokio` for better performance in the future, but current solution is quite naive yet. + +Also to declare runtime compatibility, FFI dylibs need two extra functions with specific names so that Calcit could check before actually calling them: + +```rust +#[no_mangle] +pub fn abi_version() -> String { + String::from("0.0.9") +} + +#[no_mangle] +pub fn edn_version() -> String { + cirru_edn::version().to_owned() +} +``` + +`abi_version()` must match Calcit's FFI ABI version exactly. + +`edn_version()` must match the exact `cirru_edn` crate version used by the running Calcit binary. If either version differs, Calcit aborts the FFI call before invoking the target symbol. + +### Call in Calcit + +Rust code is compiled into dylibs, and then Calcit could call with: + +```cirru.no-check +&call-dylib-edn (get-dylib-path "\"/dylibs/libcalcit_std") "\"read_file" name +``` + +first argument is the file path to that dylib. And multiple arguments are supported: + +```cirru.no-check +&call-dylib-edn (get-dylib-path "\"/dylibs/libcalcit_std") "\"add_duration" (nth date 1) n k +``` + +calling a function is special, we need another function, with last argument being the callback function: + +```cirru.no-check +&call-dylib-edn-fn (get-dylib-path "\"/dylibs/libcalcit_std") "\"set_timeout" t cb +``` + +Notice that both functions call dylibs and then library instances are cached, for better consistency and performance, with some cost in memory occupation. Linux and MacOS has different strategies loading dylibs while loaded repeatedly, so Calcit just cached them and only load once. + +### Extensions + +Currently there are some early extensions: + +- [Std](https://github.com/calcit-lang/calcit.std) - some collections of util functions +- [WebSocket server binding](https://github.com/calcit-lang/calcit-wss) +- [Regex](https://github.com/calcit-lang/calcit-regex/) +- [HTTP client binding](https://github.com/calcit-lang/calcit-fetch) +- [HTTP server binding](https://github.com/calcit-lang/calcit-http) +- [Wasmtime binding](https://github.com/calcit-lang/calcit_wasmtime) +- [fswatch](https://github.com/calcit-lang/calcit-fswatch) diff --git a/docs/installation/github-actions.md b/docs/installation/github-actions.md new file mode 100644 index 00000000..aa354379 --- /dev/null +++ b/docs/installation/github-actions.md @@ -0,0 +1,25 @@ +# GitHub Actions + +To load Calcit `0.9.18` in a Ubuntu container: + +```yaml +- uses: calcit-lang/setup-cr@0.0.8 + with: + version: "0.9.18" +``` + +Latest release could be found on https://github.com/calcit-lang/setup-cr/releases/ . + +Then to load packages defined in `deps.cirru` with `caps`: + +```bash +caps --ci +``` + +The JavaScript dependency lives in `package.json`: + +```js +"@calcit/procs": "^0.9.18" +``` + +Up to date example can be found on https://github.com/calcit-lang/respo-calcit-workflow/blob/main/.github/workflows/upload.yaml#L11 . diff --git a/docs/installation/modules.md b/docs/installation/modules.md new file mode 100644 index 00000000..a51782a3 --- /dev/null +++ b/docs/installation/modules.md @@ -0,0 +1,16 @@ +# Modules directory + +Packages are managed with `caps` command, which wraps `git clone` and `git pull` to manage modules. + +Configurations inside `calcit.cirru` and `compact.cirru`: + +```cirru +:configs $ {} + :modules $ [] |memof/compact.cirru |lilac/ +``` + +Paths defined in `:modules` field are just loaded as files from `~/.config/calcit/modules/`, i.e. `~/.config/calcit/modules/memof/compact.cirru`. + +Modules that ends with `/`s are automatically suffixed compact.cirru since it's the default filename. + +To load modules in CI environments, make use of `caps --ci`. diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 00000000..fcaca847 --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,52 @@ +# Introduction + +Calcit is a scripting language that combines the power of Clojure-like functional programming with modern tooling and hot code swapping. + +> An interpreter for Calcit snapshots with hot code swapping support, built with Rust. + +Calcit is primarily inspired by ClojureScript and designed for interactive development. It can run natively via the Rust interpreter or compile to JavaScript in ES Modules syntax for web development. + +## Key Features + +- **Immutable persistent data structures** - All data is immutable by default using ternary tree implementations +- **Structural editing** - Visual tree-based code editing with Calcit Editor +- **Hot code swapping** - Live code updates during development without losing state +- **JavaScript interop** - Seamless integration with JS ecosystem and ES Modules +- **Indentation-based syntax** - Alternative to parentheses for cleaner code +- **Static type analysis** - Compile-time type checking and error detection +- **MCP (Model Context Protocol)** server - Tool integration for AI assistants +- **Fast compilation** - Rust-based interpreter with excellent performance + +## Quick Start + +You can [try Calcit WASM build online](http://repo.calcit-lang.org/calcit-wasm-play/) for simple snippets, or see the [Quick Reference](quick-reference.md) for common commands and syntax. + +Install Calcit via Cargo: + +```bash +cargo install calcit +cargo install calcit-bundler # For indentation syntax +cargo install caps-cli # For package management +``` + +## Design Philosophy + +Calcit experiments with several interesting ideas: + +- **Code as data** - Code is stored in EDN snapshot files (`.cirru`), enabling structural editing and powerful metaprogramming +- **Pattern matching** - Tagged unions and enum types with compile-time validation +- **Type inference** - Static analysis without requiring extensive type annotations +- **Incremental compilation** - Hot reload with `.compact-inc.cirru` for fast iteration +- **Ternary tree collections** - Custom persistent data structures optimized for performance +- **File-as-key/value model** - MCP server integration uses Markdown docs as knowledge base + +Most other features are inherited from ClojureScript. Calcit-js is commonly used for web development with [Respo](https://respo-mvc.org/), a virtual DOM library migrated from ClojureScript. + +## Use Cases + +- **Web development** - Compile to JS and use with Respo or other frameworks +- **Scripting** - Fast native execution for CLI tools and automation +- **Interactive development** - REPL-driven development with hot code swapping +- **Teaching** - Clean syntax and structural editor for learning functional programming + +For more details, see [Overview](intro/overview.md) and [From Clojure](intro/from-clojure.md). diff --git a/docs/intro/from-clojure.md b/docs/intro/from-clojure.md new file mode 100644 index 00000000..30030499 --- /dev/null +++ b/docs/intro/from-clojure.md @@ -0,0 +1,33 @@ +# Features from Clojure + +Calcit is mostly a ClojureScript dialect. So it should also be considered a Clojure dialect. + +There are some significant features Calcit is learning from Clojure, + +- Runtime persistent data by default, you can only simulate states with `Ref`s. +- Namespaces +- Hygienic macros(although less powerful) +- Higher order functions +- Keywords, although Calcit changed the name to "tag" since `0.7` +- Compiles to JavaScript, interops +- Hot code swapping while code modified, and trigger an `on-reload` function +- HUD for JavaScript errors + +Also there are some differences: + +| Feature | Calcit | Clojure | +| ----------------- | ------------------------------------------------ | -------------------------------------------- | +| Host Language | Rust, and use `dylib`s for extending | Java/Clojure, import Mavan packages | +| Syntax | Indentations / Syntax Tree Editor | Parentheses | +| Persistent data | unbalanced 2-3 Tree, with tricks from FingerTree | HAMT / RRB-tree | +| Package manager | `git clone` to a folder | Clojars | +| bundle js modules | ES Modules, with ESBuild/Vite | Google Closure Compiler / Webpack | +| operand order | at first | at last | +| Polymorphism | at runtime, slow `.map ([] 1 2 3) f` | at compile time, also supports multi-arities | +| REPL | only at command line: `cr eval "+ 1 2"` | a real REPL | +| `[]` syntax | `[]` is a built-in function | builtin syntax | +| `{}` syntax | `{} (:a b)` is macro, expands to `&{} :a :b` | builtin syntax | + +also Calcit is a one-person language, it has too few features compared to Clojure. + +Calcit shares many paradiams I learnt while using ClojureScript. But meanwhile it's designed to be more friendly with ES Modules ecosystem. diff --git a/docs/intro/indentation-syntax.md b/docs/intro/indentation-syntax.md new file mode 100644 index 00000000..dd874b9e --- /dev/null +++ b/docs/intro/indentation-syntax.md @@ -0,0 +1,63 @@ +## Indentation Syntax in the MCP Server + +When using the MCP (Model Context Protocol) server, each documentation or code file is exposed as a key (the filename) with its content as the value. This means you can programmatically fetch, update, or analyze any file as a single value, making it easy for tools and agents to process Calcit code and documentation. Indentation-based syntax is preserved in the file content, so structure and meaning are maintained when accessed through the MCP server. + +# Indentation-based Syntax + +Calcit was designed based on tools from [Cirru Project](http://cirru.org/), which means, it's suggested to be programming with [Calcit Editor](https://github.com/calcit-lang/editor/). It will emit a file `compact.cirru` containing data of the code. And the data is still written in [Cirru EDN](https://github.com/Cirru/cirru-edn#syntax), Clojure EDN but based on Cirru Syntax. + +For Cirru Syntax, read , and you may find a live demo at . A normal snippet looks like: this + +```cirru.no-check +defn fibo (x) + if (< x 2) 1 + + (fibo $ - x 1) (fibo $ - x 2) +``` + +But also, you can write in files and bundle `compact.cirru` with a command line `bundle_calcit`. + +To run `compact.cirru`, internally it's doing steps: + +1. parse Cirru Syntax into vectors, +2. turn Cirru vectors into Cirru EDN, which is a piece of data, +3. build program data with quoted Calcit data(very similar to EDN, but got more data types), +4. interpret program data. + +Since Cirru itself is very generic lispy syntax, it may represent various semantics, both for code and for data. + +Inside `compact.cirru`, code is like quoted data inside `(quote ...)` blocks: + +```cirru.no-run +{} (:package |app) + :configs $ {} (:init-fn |app.main/main!) (:reload-fn |app.main/reload!) + + :entries $ {} + :prime $ {} (:init-fn |app.main/try-prime) (:reload-fn |app.main/try-prime) + :modules $ [] + + :files $ {} + |app.main $ {} + :ns $ %{} :NsEntry (:doc |) + :code $ quote + ns app.main $ :require + :defs $ {} + |fibo $ %{} :CodeEntry (:doc |) + :code $ quote + defn fibo (x) + if (< x 2) (, 1) + + (fibo $ - x 1) (fibo $ - x 2) +``` + +Notice that in Cirru `|s` prepresents a string `"s"`, it's always trying to use prefixed syntax. `"\"s"` also means `|s`, and double quote marks existed for providing context of "character escaping". + +### MCP Tool + +The tool `parse_cirru_to_json` can be used to parse Cirru syntax into JSON format, which is useful for understanding how Cirru syntax is structured. + +You can generate Cirru from JSON using `format_json_to_cirru` vice versa. + +### More about Cirru + +A review of Cirru in Chinese: + + diff --git a/docs/intro/overview.md b/docs/intro/overview.md new file mode 100644 index 00000000..660f85d6 --- /dev/null +++ b/docs/intro/overview.md @@ -0,0 +1,21 @@ +# Overview + +- Immutable Data + +Values and states are represented in different data structures, which is the semantics from functional programming. Internally it's [im](https://crates.io/crates/im) in Rust and a custom [finger tree](https://github.com/calcit-lang/ternary-tree.ts) in JavaScript. + +- Lisp(Code is Data) + +Calcit-js was designed based on experiences from ClojureScript, with a bunch of builtin macros. It offers similar experiences to ClojureScript. So Calcit offers much power via macros, while keeping its core simple. + +- Indentations + +With the `cr` command, Calcit code can be written as an indentation-based language directly. So you don't have to match parentheses like in Clojure. It also means now you need to handle indentations very carefully. + +- Hot code swapping + +Calcit was built with hot swapping in mind. Combined with [calcit-editor](https://github.com/calcit-lang/editor), it watches code changes by default, and re-runs program on updates. For calcit-js, it works with Vite and Webpack to reload, learning from Elm, ClojureScript and React. + +- ES Modules Syntax + +To leverage the power of modern browsers with help of Vite, we need another ClojureScript that emits `import`/`export` for Vite. Calcit-js does this! And this page is built with Calcit-js as well, open Console to find out more. diff --git a/docs/quick-reference.md b/docs/quick-reference.md new file mode 100644 index 00000000..5d57b7dc --- /dev/null +++ b/docs/quick-reference.md @@ -0,0 +1,360 @@ +# Quick Reference + +This page provides a quick overview of key Calcit concepts and commands for rapid lookup. + +## Installation & Setup + +```bash +# Install Rust first +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + +# Install Calcit +cargo install calcit + +# Test installation +cr eval "echo |done" +``` + +## Core Commands + +- `cr` - Run Calcit program (default: `compact.cirru`) +- `cr eval "code"` - Evaluate code snippet +- `cr js` - Generate JavaScript +- `cr ir` - Generate IR representation +- `cr query ...` - Query definitions/usages/search +- `cr docs ...` - Read/search guidebook docs +- `cr libs ...` - Search/read library docs +- `cr-mcp` - Start MCP server for tool integration + +### CLI Options + +- `--watch` / `-w` - Watch files and rerun/rebuild on changes +- `--once` / `-1` - Run once (compatibility flag; default is already once) +- `--disable-stack` - Disable stack trace for errors +- `--skip-arity-check` - Skip arity check in JS codegen +- `--emit-path ` - Specify output path for JS (default: `js-out/`) +- `--init-fn ` - Specify main function +- `--reload-fn ` - Specify reload function for hot reloading +- `--entry ` - Use config entry +- `--reload-libs` - Force reload libs data during hot reload +- `--watch-dir ` - Watch assets changes + +### Markdown Checking + +- See [CLI Options](./run/cli-options.md#markdown-code-checking) for `check-md` usage and mode guidance. + +### Docs Navigation (Fast) + +- `cr docs list` - list available chapters +- `cr docs read ` - list headings in one chapter +- `cr docs read ` - fuzzy jump by heading keywords +- `cr docs read-lines -s -n ` - precise line-range reading +- `cr docs search ` - global keyword search + +## Data Types + +- **Numbers**: `1`, `3.14` +- **Strings**: `|text`, `"|with spaces"`, `"\"escaped"` +- **Tags**: `:keyword` (immutable strings, like Clojure keywords) +- **Lists**: `[] 1 2 3` +- **HashMaps**: `{} (:a 1) (:b 2)` +- **HashSets**: `#{} :a :b :c` +- **Tuples**: `:: :tag 1 2` - tagged unions with class support +- **Records**: `%{} RecordName (:key1 val1) (:key2 val2)`, similar to structs +- **Structs**: `defstruct Point (:x :number) (:y :number)` - record type definitions +- **Enums**: `defenum Result (:ok ..) (:err :string)` - sum types +- **Refs/Atoms**: `atom 0` - mutable references +- **Buffers**: `&buffer 0x01 0x02` - binary data + +## Basic Syntax + +```cirru +; Function definition +defn add (a b) + + a b +``` + +```cirru +; Conditional +let ((x 1)) + if (> x 0) |positive |negative +``` + +```cirru +; Let binding +let + a 1 + b 2 + + a b +``` + +```cirru +; Thread macro +-> (range 10) + filter $ fn (x) (> x 5) + map inc +``` + +## Type Annotations + +```cirru +let + ; Local function with type annotations + add $ fn (a b) + hint-fn $ {} (:args ([] :number :number)) (:return :number) + + a b + ; Local variadic function + sum $ fn (& xs) + hint-fn $ {} (:rest :number) (:return :number) + apply + xs + ; Struct definition + User $ defstruct User (:name :string) (:age :number) (:email :string) + x 42 + ; Type assertion (composable check, returns original value) + assert-type x :number + [] (add 3 4) (sum 1 2 3) x +``` + +Namespace-level definitions use `:schema`, for example: + +```cirru +defn add (a b) + + a b +``` + +`schema` can be attached separately: + +```cirru +:: :fn $ {} (:args $ [] :number :number) (:return :number) +``` + +### Built-in Types + +- `:number`, `:string`, `:bool`, `:nil`, `:dynamic` +- `:list`, `:map`, `:set`, `:record`, `:fn`, `:tuple` +- `:dynamic` - wildcard type (default when no annotation) +- Generic types (Cirru style): + +```cirru +let + t1 $ :: :list :number + t2 $ :: :map :string + t3 $ :: :fn $ {} + :args $ [] :number + :return :string + [] t1 t2 t3 +``` + +### Static Checks (Compile-time) + +- **Arity checking**: Function call argument count validation +- **Record field checking**: Validates field names in record access +- **Tuple index bounds**: Ensures tuple indices are valid +- **Enum tag matching**: Validates tags in `&case` and `&extract-case` +- **Method validation**: Checks method names and class types +- **Recur arity**: Validates recur argument count matches function params + +### Method & Access Syntax + +- Method call: `.map xs inc` (or shorthand `xs.map inc`) +- Tag access (map key): prefer `obj.:name` over legacy `(:name obj)` +- Trait/impl declarations prefer dot method keys like `.foo`; legacy tag keys like `:foo` remain compatible but emit a default warning in `deftrait`/`defimpl` + +## File Structure + +- `calcit.cirru` - Editor snapshot (source for structural editing) +- `compact.cirru` - Runtime format (compiled, `cr` command actually uses this) +- `deps.cirru` - Dependencies +- `.compact-inc.cirru` - Hot reload trigger, including incremental changes + +## Common Functions + +### Math + +- `+`, `-`, `*`, `/` - arithmetic (variadic) +- `&+`, `&-`, `&*`, `&/` - binary arithmetic +- `inc`, `dec` - increment/decrement +- `pow`, `sqrt`, `round`, `floor`, `ceil` +- `sin`, `cos` - trigonometric functions +- `&max`, `&min` - binary min/max +- `&number:fract` - fractional part +- `&number:rem` - remainder +- `&number:format` - format number +- `bit-shl`, `bit-shr`, `bit-and`, `bit-or`, `bit-xor`, `bit-not` + +### List Operations + +- `[]` - create list +- `append`, `prepend` - add elements +- `concat` - concatenate lists +- `nth`, `first`, `rest`, `last` - access elements +- `count`, `empty?` - list properties +- `slice` - extract sublist +- `reverse` - reverse list +- `sort`, `sort-by` - sorting +- `map`, `filter`, `reduce` - functional operations +- `foldl`, `foldl-shortcut`, `foldr-shortcut` - folding +- `range` - generate number range +- `take`, `drop` - slice operations +- `distinct` - remove duplicates +- `&list:contains?`, `&list:includes?` - membership tests + +### Map Operations + +- `{}` or `&{}` - create map +- `&map:get` - get value by key +- `&map:assoc`, `&map:dissoc` - add/remove entries +- `&map:merge` - merge maps +- `&map:contains?`, `&map:includes?` - key membership +- `keys`, `vals` - extract keys/values +- `to-pairs`, `pairs-map` - convert to/from pairs +- `&map:filter`, `&map:filter-kv` - filter entries +- `&map:common-keys`, `&map:diff-keys` - key operations + +### Set Operations + +- `#{}` - create set +- `include`, `exclude` - add/remove elements +- `union`, `difference`, `intersection` - set operations +- `&set:includes?` - membership test +- `&set:to-list` - convert to list + +### String Operations + +- `str` - concatenate to string +- `str-spaced` - join with spaces +- `&str:concat` - binary concatenation +- `trim`, `split`, `split-lines` - string manipulation +- `starts-with?`, `ends-with?` - prefix/suffix tests +- `&str:slice` - extract substring +- `&str:replace` - replace substring +- `&str:find-index` - find position +- `&str:contains?`, `&str:includes?` - substring tests +- `&str:pad-left`, `&str:pad-right` - padding +- `parse-float` - parse number from string +- `get-char-code`, `char-from-code` - character operations +- `&str:escape` - escape string + +### Tuple Operations + +- `::` - create tuple (shorthand) +- `%::` - create tuple with class +- `&tuple:nth` - access element by index +- `&tuple:assoc` - update element +- `&tuple:count` - get element count +- `&tuple:class` - get class +- `&tuple:params` - get parameters +- `&tuple:enum` - get enum tag +- `&tuple:with-class` - change class + +### Record Operations + +- `defstruct` - define a struct type with typed fields +- `%{}` - create a record instance from a struct +- `%{}?` - create a partial record (unset fields default to nil) +- `&%{}` - low-level record constructor (flat key-value pairs, no type check) +- `record-with` - update multiple fields, returns new record +- `&record:get` - get field value +- `&record:assoc` - set field value (low-level) +- `&record:struct` - get the struct definition the record was created from +- `&record:matches?` - type check +- `&record:from-map` - convert from map +- `&record:to-map` - convert to map +- `&record:get-name` - get tag name of the record's struct +- `record?`, `struct?` - predicates + +### Struct & Enum Operations + +- `defstruct` - define struct type +- `defenum` - define enum type +- `&struct::new`, `&enum::new` - create instances +- `struct?`, `enum?` - predicates +- `&tuple:enum-has-variant?` - check variant +- `&tuple:enum-variant-arity` - get variant arity +- `tag-match` - pattern matching on enums + +## Traits & Methods + +- `deftrait` - define a trait (method set + type signatures) +- `defimpl` - define an impl record for a trait: `defimpl ImplName Trait ...` +- `impl-traits` - attach impl records to a struct/enum definition (user impls: later impls override earlier ones for same method name) +- `.method` - normal method dispatch +- `&trait-call` - explicit trait method call: `&trait-call Trait :method receiver & args` +- `&methods-of` - list runtime-available methods (strings including leading dot) +- `&inspect-methods` - print impl/method resolution to stderr, returns the value unchanged +- `assert-traits` - runtime check that a value implements a trait, returns the value unchanged + +### Ref/Atom Operations + +- `atom` - create atom +- `&atom:deref` or `deref` - read value +- `reset!` - set value +- `swap!` - update with function +- `add-watch`, `remove-watch` - observe changes +- `ref?` - predicate + +### Type Predicates + +- `nil?`, `some?` - nil checks +- `number?`, `string?`, `tag?`, `symbol?` +- `list?`, `map?`, `set?`, `tuple?` +- `record?`, `struct?`, `enum?`, `ref?` +- `fn?`, `macro?` + +### Control Flow + +- `if` - conditional +- `when`, `when-not` - single-branch conditionals +- `cond` - multi-way conditional +- `case` - pattern matching on values +- `&case` - internal case macro +- `tag-match` - enum/tuple pattern matching +- `record-match` - record pattern matching +- `list-match` - list destructuring match +- `field-match` - map field matching + +### Threading Macros + +- `->` - thread first +- `->>` - thread last +- `->%` - thread with `%` placeholder +- `%<-` - reverse thread + +### Other Macros + +- `let` - local bindings +- `defn` - define function +- `defmacro` - define macro +- `fn` - anonymous function +- `quote`, `quasiquote` - code as data +- `macroexpand`, `macroexpand-all` - debug macros +- `assert`, `assert=` - assertions +- `&doseq` - side-effect iteration +- `for` - list comprehension + +### Meta Operations + +- `type-of` - get type tag +- `turn-string`, `turn-symbol`, `turn-tag` - type conversion +- `identical?` - reference equality +- `recur` - tail recursion +- `generate-id!` - unique ID generation +- `cpu-time` - timing +- `&get-os`, `&get-calcit-backend` - environment info + +### EDN/Data Operations + +- `parse-cirru-edn`, `format-cirru-edn` - EDN serialization +- `parse-cirru`, `format-cirru` - Cirru syntax +- `&data-to-code` - convert data to code +- `pr-str` - print to string + +### Effects/IO + +- `echo`, `println` - output +- `read-file`, `write-file` - file operations +- `get-env` - environment variables +- `raise` - throw error +- `quit!` - exit program + +For detailed information, see the specific documentation files in the table of contents. diff --git a/docs/run.md b/docs/run.md new file mode 100644 index 00000000..55709f95 --- /dev/null +++ b/docs/run.md @@ -0,0 +1,67 @@ +# Run Calcit + +This page is a quick navigation hub. Detailed topics are split into dedicated chapters under `run/`. + +## Quick start + +Run local project once (default behavior): + +```bash +cr +``` + +Enable watch mode explicitly: + +```bash +cr -w +``` + +Evaluate a snippet: + +```bash +cr eval 'println "|Hello world"' +``` + +Emit JavaScript / IR once: + +```bash +cr js +cr ir +``` + +## Run guide map + +- [Run in Eval mode](./run/eval.md) +- [CLI Options](./run/cli-options.md) +- [Querying definitions](./run/query.md) +- [Documentation & Libraries](./run/docs-libs.md) +- [CLI Code Editing](./run/edit-tree.md) +- [Load Deps](./run/load-deps.md) +- [Hot Swapping](./run/hot-swapping.md) +- [Bundle Mode](./run/bundle-mode.md) +- [Entries](./run/entries.md) + +## Quick find by keyword + +Use these keywords directly with `cr docs read` for faster section hits: + +- `eval`, `snippet`, `dep`, `type-check` → [Run in Eval mode](./run/eval.md) +- `watch`, `once`, `entry`, `reload-fn` → [CLI Options](./run/cli-options.md) +- `query`, `find`, `usages`, `search-expr` → [Querying definitions](./run/query.md) +- `docs`, `read-lines`, `libs`, `readme` → [Documentation & Libraries](./run/docs-libs.md) +- `edit`, `tree`, `target-replace`, `imports` → [CLI Code Editing](./run/edit-tree.md) + +Typical navigation flow: + +```bash +# 1) List headings in a chapter +cr docs read run.md + +# 2) Jump by keyword(s) +cr docs read run.md quick find + +# 3) Open the target chapter and narrow again +cr docs read query.md usages +``` + +Use this page for orientation, then jump to the specific chapter for complete examples and edge cases. diff --git a/docs/run/bundle-mode.md b/docs/run/bundle-mode.md new file mode 100644 index 00000000..885ac88b --- /dev/null +++ b/docs/run/bundle-mode.md @@ -0,0 +1,16 @@ +# Bundle Mode + +Calcit programs are primarily designed to be written using the [calcit-editor](http://github.com/calcit-lang/editor), a structural editor. + +You can also try short code snippets in eval mode: + +```bash +cr eval "+ 1 2" +# => 3 +``` + +If you prefer to write Calcit code without the calcit-editor, that's possible too. See the example in [minimal-calcit](https://github.com/calcit-lang/minimal-calcit). + +With the `bundle_calcit` command, Calcit code can be written using indentation-based syntax. This means you don't need to match parentheses as in Clojure, but you must pay close attention to indentation. + +First, bundle your files into a `compact.cirru` file. Then, use the `cr` command to run it. A `.compact-inc.cirru` file will also be generated to enable hot code swapping. Simply launch these two watchers in parallel. diff --git a/docs/run/cli-options.md b/docs/run/cli-options.md new file mode 100644 index 00000000..a8853613 --- /dev/null +++ b/docs/run/cli-options.md @@ -0,0 +1,181 @@ +# CLI Options + +```bash +Usage: cr [] [-1] [-w] [--disable-stack] [--skip-arity-check] [--warn-dyn-method] [--emit-path ] [--init-fn ] [--reload-fn ] [--entry ] [--reload-libs] [--watch-dir ] [] [] + +Top-level command. + +Positional Arguments: + input input source file, defaults to "compact.cirru" + +Options: + -1, --once run once and quit (compatibility option) + -w, --watch watch files and rerun/rebuild on changes + --disable-stack disable stack trace for errors + --skip-arity-check + skip arity check in js codegen + --warn-dyn-method + warn on dynamic method dispatch and trait-attachment diagnostics + --emit-path entry file path, defaults to "js-out/" + --init-fn specify `init_fn` which is main function + --reload-fn specify `reload_fn` which is called after hot reload + --entry specify with config entry + --reload-libs force reloading libs data during code reload + --watch-dir specify a path to watch assets changes + --help display usage information + +Commands: + js emit JavaScript rather than interpreting + ir emit Cirru EDN representation of program to program-ir.cirru + eval run program + analyze analyze code structure (call-graph, count-calls, check-examples) + query query project information (namespaces, definitions, configs) + docs documentation tools (guidebook) + cirru Cirru syntax tools (parse, format, edn) + libs fetch available Calcit libraries from registry + edit edit project code (definitions, namespaces, modules, configs) + tree fine-grained code tree operations (view and modify AST nodes) +``` + +Quick note: `cr edit format` rewrites the target snapshot using canonical serialization without changing semantics. It also normalizes legacy namespace entries that were previously serialized with `CodeEntry` into the current `NsEntry` shape. + +## Detailed Option Descriptions + +### Input File + +```bash +# Run default compact.cirru +cr + +# Run specific file +cr demos/compact.cirru +``` + +### Run Once (--once / -1) + +By default, `cr` runs once and exits. Use `--watch` (`-w`) to enable watch mode: + +```bash +cr --watch +cr -w demos/compact.cirru +``` + +`--once` is still available for compatibility: + +```bash +cr --once +cr -1 # shorthand +``` + +### Error Stack Trace (--disable-stack) + +Disables detailed stack traces in error messages, useful for cleaner output: + +```bash +cr --disable-stack +``` + +### JS Codegen Options + +**--skip-arity-check**: When generating JavaScript, skip arity checking (use cautiously): + +```bash +cr js --skip-arity-check +``` + +**--emit-path**: Specify output directory for generated JavaScript: + +```bash +cr js --emit-path dist/ +``` + +### Dynamic Method Warnings (--warn-dyn-method) + +Warn when dynamic method dispatch cannot be specialized at preprocess time, and surface related trait-attachment diagnostics: + +```bash +cr --warn-dyn-method +``` + +### Hot Reloading Configuration + +**--init-fn**: Override the main entry function: + +```bash +cr --init-fn app.main/start! +``` + +**--reload-fn**: Specify function called after code reload: + +```bash +cr --reload-fn app.main/on-reload! +``` + +**--reload-libs**: Force reload library data during hot reload (normally cached): + +```bash +cr --reload-libs +``` + +### Config Entry (--entry) + +Use specific config entry from `compact.cirru`: + +```bash +cr --entry test +cr --entry production +``` + +### Asset Watching (--watch-dir) + +Watch additional directories for changes (e.g., assets, styles): + +```bash +cr --watch-dir assets/ +cr --watch-dir styles/ --watch-dir images/ +``` + +## Common Usage Patterns + +```bash +# Development with watch mode +cr -w --reload-fn app.main/reload! + +# Production build +cr js --emit-path dist/ + +# JS watch mode +cr js -w --emit-path dist/ + +# IR watch mode +cr ir -w + +# Testing single run +cr --once --init-fn app.test/run-tests! + +# Debug mode with full stack traces +cr --reload-libs + +# CI/CD environment +cr --once --disable-stack +``` + +## Markdown code checking + +Use `docs check-md` to validate fenced code blocks in markdown files: + +```bash +cr docs check-md README.md +``` + +Load module dependencies with repeatable `--dep` options: + +```bash +cr docs check-md README.md --dep ./ --dep ~/.config/calcit/modules/memof/ +``` + +Recommended block modes: + +- `cirru`: run + preprocess + parse (preferred) +- `cirru.no-run`: preprocess + parse when runtime setup is unavailable +- `cirru.no-check`: parse only for illustrative snippets diff --git a/docs/run/docs-libs.md b/docs/run/docs-libs.md new file mode 100644 index 00000000..c511090e --- /dev/null +++ b/docs/run/docs-libs.md @@ -0,0 +1,107 @@ +# Documentation & Libraries + +Calcit includes built-in commands to navigate the language guidebook and discover community libraries. + +## Guidebook Access (`docs`) + +The `docs` subcommand allows you to read the language guidebook (like this one) without leaving the terminal. + +### Reading Chapters + +```bash +# List all chapters in the guidebook +cr docs list + +# Read a specific file (fuzzy matching supported) +cr docs read run.md + +# List headings in a file (best first step before narrowing) +cr docs read run.md + +# Jump by heading keyword(s) +cr docs read run.md quick start + +# Search for keywords across all chapters +cr docs search "polymorphism" +``` + +### Advanced Navigation (`read`) + +`cr docs read` supports fuzzy heading matching to jump straight to a section: + +```bash +# Display the "Quick start" section of run.md +cr docs read run.md "Quick start" + +# Exclude subheadings from the output +cr docs read run.md "Quick start" --no-subheadings +``` + +### Precision Reading (`read-lines`) + +Use `read-lines` for large files where you need a specific range: + +```bash +# Read 50 lines starting from line 100 of common-patterns.md +cr docs read-lines common-patterns.md --start 100 --lines 50 +``` + +## Fast Navigation Patterns + +### Pattern 1: Discover headings first, then narrow + +```bash +cr docs read query.md +cr docs read query.md usages +``` + +### Pattern 2: Search globally, then open exact chapter + +```bash +cr docs search trait +cr docs read traits.md +``` + +## Library Discovery (`libs`) + +The `libs` subcommand helps you find and understand Calcit modules. + +### Searching Registry + +```bash +# Search for libraries related to "web" +cr libs search web +``` + +### Reading Readmes + +You can read the documentation of any official library, even if not installed locally: + +```bash +# Show README of 'respo' module +cr libs readme respo + +# Read a specific markdown file inside package +cr libs readme respo -f Skills.md +``` + +### Scanning for Documentation + +```bash +# List all markdown files inside the local 'memof' module +cr libs scan-md memof +``` + +## Collaborative validation (`check-md`) + +`docs check-md` is used to verify that code blocks in your markdown documentation are correct and runnable: + +```bash +cr docs check-md README.md +``` + +It supports specific block types: + +- `cirru`: Run and validate. +- `cirru.no-run`: Validate syntax and preprocessing without running. +- `cirru.no-check`: Skip checking (illustrative). diff --git a/docs/run/edit-tree.md b/docs/run/edit-tree.md new file mode 100644 index 00000000..ef599762 --- /dev/null +++ b/docs/run/edit-tree.md @@ -0,0 +1,95 @@ +# CLI Code Editing (edit & tree) + +Calcit provides powerful CLI tools for modifying code directly without opening a text editor. These commands are optimized for both interactive use and automated scripts/agents. + +## Core Editing (cr edit) + +The `edit` command handles high-level operations on namespaces and definitions. + +```bash +# Refresh snapshot formatting without semantic changes +cr edit format +``` + +This command also rewrites older namespace records into the canonical `NsEntry` snapshot shape. + +### Managing Namespaces + +```bash +# Move or rename a definition +cr edit mv app.main/old-name app.main/new-name + +# Add a new namespace +cr edit add-ns app.util + +# Remove a namespace +cr edit rm-ns app.util +``` + +### Managing Imports + +```bash +# Add an import to a namespace +cr edit add-import app.main -e 'respo.core :refer $ deftime' + +# Bulk reset all imports for a namespace +cr edit imports app.main -f imports.cirru +``` + +## Fine-grained AST Operations (cr tree) + +The `tree` command allows precise manipulation of nodes within a definition's S-expression tree. + +### Viewing the Tree + +```bash +# View the AST of a definition with indices +cr tree view app.main/main! +``` + +### Target-based Replacement + +`target-replace` is the safest way to modify a specific node by its content: + +```bash +# Replace '1' with '10' inside the definition +cr tree target-replace app.main/main! -t 1 -e 10 +``` + +### Path-based Operations + +You can use numeric paths to locate deep nodes: + +```bash +# Replace the node at path [1 2 0] +cr tree replace app.main/main! -p 1 2 0 -e '(+ 1 2)' + +# Insert before/after a node +cr tree insert app.main/main! -p 1 0 --at before -e 'println |started' + +# Delete a node +cr tree delete app.main/main! -p 1 0 +``` + +### Copying across Definitions + +```bash +# Copy a node from one definition to another +cr tree cp app.main/target-def --from app.main/source-def -p 1 0 --at append-child +``` + +## Input Formats + +Editing commands support several ways to provide new code: + +- `-e 'code'`: Inline Cirru expression (one-liner). +- `-f file.cirru`: Multi-line code from a file (recommended for complex structures). +- `-j 'json'`: Raw JSON-serialized Cirru representation. + +> Note: For multi-line text input, prefer using `-f` with a temporary file in `.calcit-snippets/`. + +## Best Practices + +1. **Check first**: Use `cr query find` or `cr tree view` to confirm the current state. +2. **From back to front**: When performing multiple `delete` or `insert` operations at the same level, start from the highest index to avoid shifting indices. +3. **Use target-replace**: It is usually safer than path-based replacement as it validates the current content. diff --git a/docs/run/entries.md b/docs/run/entries.md new file mode 100644 index 00000000..a4007217 --- /dev/null +++ b/docs/run/entries.md @@ -0,0 +1,29 @@ +# Entries + +By default Calcit reads `:init-fn` and `:reload-fn` inside `compact.cirru` configs. You may also specify functions, + +```bash +cr compact.cirru --init-fn='app.main/main!' --reload-fn='app.main/reload!' +``` + +and even configure `:entries` in `compact.cirru`: + +```bash +cr compact.cirru --entry server +``` + +Here's an example, first lines of a `compact.cirru` file may look like: + +```cirru +{} (:package |app) + :configs $ {} (:init-fn |app.client/main!) (:reload-fn |app.client/reload!) (:version |0.0.1) + :modules $ [] |respo.calcit/ |lilac/ |recollect/ |memof/ |respo-ui.calcit/ |ws-edn.calcit/ |cumulo-util.calcit/ |respo-message.calcit/ |cumulo-reel.calcit/ + :entries $ {} + :server $ {} (:init-fn |app.server/main!) (:port 6001) (:reload-fn |app.server/reload!) (:storage-key |calcit.cirru) + :modules $ [] |lilac/ |recollect/ |memof/ |ws-edn.calcit/ |cumulo-util.calcit/ |cumulo-reel.calcit/ |calcit-wss/ |calcit.std/ + :files $ {} +``` + +There is base configs attached with `:configs`, with `:init-fn` `:reload-fn` defined, which is the inital entry of the program. + +Then there is `:entries` with `:server` entry defined, which is another entry of the program. It has its own `:init-fn` `:reload-fn` and `:modules` options. And to invoke it, you may use `--entry server` option. diff --git a/docs/run/eval.md b/docs/run/eval.md new file mode 100644 index 00000000..410e5207 --- /dev/null +++ b/docs/run/eval.md @@ -0,0 +1,128 @@ +# Run in Eval mode + +Use `eval` command to evaluate code snippets from CLI: + +```bash +$ cr eval 'echo |demo' +1 +took 0.07ms: nil +``` + +```bash +$ cr eval 'echo "|spaced string demo"' +spaced string demo +took 0.074ms: nil +``` + +## Multi-line Code + +You can run multiple expressions: + +```bash +cr eval ' +-> (range 10) + map $ fn (x) + * x x +' +# Output: calcit version: 0.5.25 +# took 0.199ms: ([] 0 1 4 9 16 25 36 49 64 81) +``` + +## Working with Context Files + +Eval can access definitions from a loaded program: + +```bash +# Load from specific file and eval with its context +cr demos/compact.cirru eval 'range 3' +# Output: ([] 0 1 2) + +# Use let bindings +cr demos/compact.cirru eval 'let ((x 1)) (+ x 2)' +# Output: 3 +``` + +You can load external modules with repeatable `--dep` options: + +```bash +cr demos/compact.cirru eval --dep ~/.config/calcit/modules/respo.calcit/ -- 'ns app.demo $ :require respo.util.detect :refer $ element?\n\nelement? nil' +``` + +If the first expression in a snippet is `ns`, its `:require` rules are merged into runtime `ns app.main`, so imported symbols can be used in the same snippet. + +## Type Checking in Eval + +Type annotations and static checks work in eval mode: + +```bash +# Type mismatch will cause error +cr demos/compact.cirru eval 'let ((x 1)) (assert-type x :string) x' +# Error: Type mismatch... + +# Correct type passes +cr demos/compact.cirru eval 'let ((x 1)) (assert-type x :number) x' +# Output: 1 +``` + +## Common Patterns + +### Quick Calculations + +```bash +cr eval '+ 1 2 3 4' +# Output: 10 + +cr eval 'apply * $ range 1 6' +# Output: 120 ; factorial of 5 +``` + +### Testing Expressions + +```bash +cr eval '&list:nth ([] :a :b :c) 1' +# Output: :b + +cr eval '&map:get ({} (:x 1) (:y 2)) :x' +# Output: 1 +``` + +### Exploring Functions + +```bash +# Check function signature +cr eval 'type-of range' +# Output: :fn + +# Test with sample data +cr eval '-> (range 5) (map inc) (filter (fn (x) (> x 2)))' +# Output: ([] 3 4 5) +``` + +## Important Notes + +### Syntax Considerations + +- **No extra brackets**: Cirru syntax doesn't need outer parentheses at top level + - ✅ `cr eval 'range 3'` + - ❌ `cr eval '(range 3)'` (adds extra nesting) + +- **Let bindings**: Use paired list format `((name value))` + - ✅ `let ((x 1)) x` + - ❌ `let (x 1) x` (triggers "expects pairs in list for let" error) + +### Error Diagnostics + +- Type warnings cause eval to fail (intentional safety feature) +- Check `.calcit-error.cirru` for complete stack traces +- Use `cr cirru parse-oneliner` to debug parse issues + +### Query Examples + +Use `cr query examples` to see usage examples: + +```bash +cr demos/compact.cirru query examples calcit.core/let +cr demos/compact.cirru query examples calcit.core/defn +``` + +For markdown snippet validation (`docs check-md`), see [CLI Options](./cli-options.md#markdown-code-checking). diff --git a/docs/run/hot-swapping.md b/docs/run/hot-swapping.md new file mode 100644 index 00000000..d4312aa7 --- /dev/null +++ b/docs/run/hot-swapping.md @@ -0,0 +1,56 @@ +# Hot Swapping + +Since there are two platforms for running Calcit, soutions for hot swapping are implemented differently. + +### Rust runtime + +Hot swapping is built inside Rust runtime. When you specity `:reload-fn` in `compact.cirru`: + +```cirru +{} + :configs $ {} + :init-fn |app.main/main! + :reload-fn |app.main/reload! +``` + +the interpreter learns that the function `reload!` is to be re-run after hot swapping. + +It relies on change event on `.compact-inc.cirru` for detecting code changes. `.compact-inc.cirru` contains informations about which namespace / which definition has changed, and interpreter will patch into internal state of the program. Program caches of current namespace will be replaced, in case that dependants also need changes. Data inside atoms are retained. Calcit encourages usages of mostly pure functions with a few atoms, programs can be safely replaced in many cases. + +But also notice that if you have effects like events listening, you have to dispose and re-attach listeners in `reload!`. + +### JavaScript runtime + +While Calcit-js is compiled to JavaScript beforing running, we need tools from JavaScript side for hot swapping, or HMR(hot module replacement). The tool I use most frequestly is [Vite](https://vitejs.dev/), with extra entry file of code: + +```js +import { main_$x_ } from "./js-out/app.main.mjs"; + +main_$x_(); + +if (import.meta.hot) { + import.meta.hot.accept("./js-out/app.main.mjs", (main) => { + main.reload_$x_(); + }); +} +``` + +There's also a `js-out/calcit.build-errors.mjs` file for hot swapping when compilation errors are detected. With this file, you can hook up you own HUD error alert with some extra code, `hud!` is the function for showing the alert: + +```cirru.no-check +ns app.main + :require + "\"./calcit.build-errors" :default build-errors + "\"bottom-tip" :default hud! + +defn reload! () $ if (nil? build-errors) + do (remove-watch *reel :changes) (clear-cache!) + add-watch *reel :changes $ fn (reel prev) (render-app!) + reset! *reel $ refresh-reel @*reel schema/store updater + hud! "\"ok~" "\"Ok" + hud! "\"error" build-errors +``` + +One tricky thing to hot swap is macros. But you don't need to worry about that in newer versions. + +Vite is for browsers. When you want to HMR in Node.js , Webpack provides some mechanism for that, you can refer to the [boilerplate](https://github.com/minimal-xyz/minimal-webpack-esm-hmr). However I'm not using this since Calcit-js switched to `.mjs` files. Node.js can run `.mjs` files without a bundler, it's huge gain in debugging. Plus I want to try more in Calcit-rs when possible since packages from Rust also got good qualitiy, and it's better to have hot swapping in Calcit Rust runtime. diff --git a/docs/run/load-deps.md b/docs/run/load-deps.md new file mode 100644 index 00000000..5327cc2c --- /dev/null +++ b/docs/run/load-deps.md @@ -0,0 +1,60 @@ +# Load Dependencies + +`caps` command is used for downloading dependencies declared in `deps.cirru`. The name "caps" stands for "Calcit Dependencies". + +`deps.cirru` declares dependencies, which correspond to repositories on GitHub. Specify a branch or a tag: + +```cirru +{} + :calcit-version |0.9.18 + :dependencies $ {} + |calcit-lang/memof |0.0.11 + |calcit-lang/lilac |main +``` + +Run `caps` to download. Sources are downloaded into `~/.config/calcit/modules/`. If a module contains `build.sh`, it will be executed mostly for compiling Rust dylibs. + +To load modules, use `:modules` configuration in `calcit.cirru` and `compact.cirru`: + +```cirru +:configs $ {} + :modules $ [] |memof/compact.cirru |lilac/ +``` + +Paths defined in `:modules` field are just loaded as files from `~/.config/calcit/modules/`, i.e. `~/.config/calcit/modules/memof/compact.cirru`. + +Modules that ends with `/`s are automatically suffixed `compact.cirru` since it's the default filename. + +### Outdated + +To check outdated modules, run: + +```bash +caps outdated +``` + +### CLI Options + +``` +caps --help +Usage: caps [] [-v] [--pull-branch] [--ci] [--local-debug] [] [] + +Top-level command. + +Positional Arguments: + input input file + +Options: + -v, --verbose verbose mode + --pull-branch pull branch in the repo + --ci CI mode loads shallow repo via HTTPS + --local-debug debug mode, clone to test-modules/ + --help, help display usage information + +Commands: + outdated show outdated versions + download download named packages with org/repo@branch +``` + +- "pull branch" to fetch update if only branch name is specified like `main`. +- "ci" does not support `git@` protocol, only `https://` protocol. diff --git a/docs/run/query.md b/docs/run/query.md new file mode 100644 index 00000000..f6496f37 --- /dev/null +++ b/docs/run/query.md @@ -0,0 +1,112 @@ +# Querying Definitions + +Calcit provides a powerful `query` subcommand to inspect code, find definitions, and analyze usages directly from the command line. + +## Core Query Commands + +### List Namespaces (`ns`) + +```bash +# List all loaded namespaces +cr query ns + +# Show definitions in a specific namespace +cr query ns calcit.core +``` + +### Read Code (`def`) + +```bash +# Show full source code of a definition +cr query def calcit.core/assoc +``` + +### Peek Signature (`peek`) + +```bash +# Show documentation and examples without the full body +cr query peek calcit.core/map +``` + +### Check Examples (`examples`) + +```bash +# Extract only the examples section +cr query examples calcit.core/let +``` + +### Find Symbol (`find`) + +```bash +# Search for a symbol across ALL loaded namespaces +cr query find assoc +``` + +### Analyze Usages (`usages`) + +```bash +# Find where a specific definition is used +cr query usages app.main/main! +``` + +### Search Text (`search`) + +```bash +# Search for raw text (leaf values) across project +cr query search hello + +# Limit to one definition +cr query search hello -f app.main/main! +``` + +### Search Expressions (`search-expr`) + +```bash +# Search structural expressions (Cirru pattern) +cr query search-expr "fn (x)" + +# Limit to one definition +cr query search-expr "fn (x)" -f app.main/main! +``` + +## Quick Recipes (for fast locating) + +### Locate a symbol and jump to definition + +```bash +cr query find assoc +cr query def calcit.core/assoc +``` + +### Locate all call sites before refactor + +```bash +cr query usages app.main/main! +``` + +### Locate by text when you only remember a fragment + +```bash +cr query search "reload" +``` + +## Runtime Code Inspection + +You can also use built-in functions to inspect live data and definitions: + +```cirru +let + Point $ defstruct Point (:x :number) (:y :number) + p (%{} Point (:x 1) (:y 2)) + do + ; Get all methods/traits implemented by a value + println $ &methods-of p + ; Get tag name of a record or enum + println $ &record:get-name p + ; Describe any value's internal type + println $ &inspect-type p +``` + +### Getting Help + +Use `cr query --help` for the full list of available query subcommands. diff --git a/docs/structural-editor.md b/docs/structural-editor.md new file mode 100644 index 00000000..79d10cf4 --- /dev/null +++ b/docs/structural-editor.md @@ -0,0 +1,36 @@ +# Structural Editor + +> **Deprecated:** As Calcit shifts toward LLM-generated code workflows, command-line operations and type annotations have become more important. The structural editor approach is no longer recommended. Agent interfaces are preferred over direct user interaction. + +As demonstrated in [Cirru Project](http://cirru.org/), it's for higher goals of auto-layout code editor. [Calcit Editor](https://github.com/calcit-lang/editor) was incubated in Cirru. + +Structural editing makes Calcit a lot different from existing languages, even unique among Lisps. + +Calcit Editor uses a `calcit.cirru` as snapshot file, which contains much informations. And it is compiled into `compact.cirru` for evaluating. +Example of a `compact.cirru` file is more readable: + +```cirru.no-run +{} (:package |app) + :configs $ {} (:init-fn |app.main/main!) (:reload-fn |app.main/reload!) + :modules $ [] + :files $ {} + |app.main $ %{} :FileEntry + :defs $ {} + |main! $ %{} :CodeEntry (:doc |) + :code $ quote + defn main! () (+ 1 2) + :examples $ [] + |reload! $ %{} :CodeEntry (:doc |) + :code $ quote + defn reload! () + :examples $ [] + :ns $ %{} :NsEntry (:doc |) + :code $ quote + ns app.main $ :require +``` + +![Calcit Editor](https://cos-sh.tiye.me/cos-up/07eb872cbbe9826474bb1343d8757c39/image.png) + +Also [Hovenia Editor](https://github.com/Cirru/hovenia-editor) is another experiment rendering S-Expressions into Canvas. + +![Hovernia Editor](https://cos-sh.tiye.me/cos-up/bb9cf76c519fa03d52cb64856761afc6/image.png) diff --git a/src/bin/cli_handlers/cirru.rs b/src/bin/cli_handlers/cirru.rs index 388ce66b..5b891760 100644 --- a/src/bin/cli_handlers/cirru.rs +++ b/src/bin/cli_handlers/cirru.rs @@ -129,7 +129,7 @@ fn handle_parse_edn(edn_str: &str) -> Result<(), String> { fn handle_show_guide() -> Result<(), String> { let home_dir = std::env::var("HOME").map_err(|_| "Failed to get HOME directory".to_string())?; - let guide_path = format!("{home_dir}/.config/calcit/guidebook-repo/docs/cirru-syntax.md"); + let guide_path = format!("{home_dir}/.config/calcit/docs/cirru-syntax.md"); match std::fs::read_to_string(&guide_path) { Ok(content) => { @@ -138,7 +138,7 @@ fn handle_show_guide() -> Result<(), String> { } Err(_) => Err(format!( "Cirru syntax guide not found at: {guide_path}\n\ - Please ensure the guidebook repository is cloned to ~/.config/calcit/guidebook-repo/" + Please ensure the docs folder is links to ~/.config/calcit/docs" )), } } diff --git a/src/bin/cli_handlers/docs.rs b/src/bin/cli_handlers/docs.rs index e1da6c8b..7351c905 100644 --- a/src/bin/cli_handlers/docs.rs +++ b/src/bin/cli_handlers/docs.rs @@ -138,13 +138,13 @@ fn find_doc_by_filename<'a>(guide_docs: &'a HashMap, filename: fn get_guidebook_dir() -> Result { let home_dir = std::env::var("HOME").map_err(|_| "Unable to get HOME environment variable")?; - let docs_dir = Path::new(&home_dir).join(".config/calcit/guidebook-repo/docs"); + let docs_dir = Path::new(&home_dir).join(".config/calcit/docs"); if !docs_dir.exists() { return Err(format!( "Guidebook documentation directory not found: {docs_dir:?}\n\n\ To set up guidebook documentation, please run:\n\ - git clone https://github.com/calcit-lang/guidebook-repo.git ~/.config/calcit/guidebook-repo" + git clone https://github.com/calcit-lang/calcit.git && ln -s ~/.config/calcit/calcit/docs ~/.config/calcit/docs" )); } From 52e7cd6f354226ba414ca870c594d37766567d83 Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 16 Mar 2026 14:50:17 +0800 Subject: [PATCH 13/57] chore: reorganize profiling tools and docs --- .../2026-0316-1449-profiling-tools-reorg.md | 13 + profiling/README.md | 84 ++++++ profiling/profile-once.sh | 44 +++ profiling/profile-summary.py | 197 ++++++++++++++ profiling/samply-once.sh | 65 +++++ profiling/samply-summary.py | 255 ++++++++++++++++++ scripts/profiling.sh | 7 - 7 files changed, 658 insertions(+), 7 deletions(-) create mode 100644 editing-history/2026-0316-1449-profiling-tools-reorg.md create mode 100644 profiling/README.md create mode 100755 profiling/profile-once.sh create mode 100644 profiling/profile-summary.py create mode 100755 profiling/samply-once.sh create mode 100755 profiling/samply-summary.py delete mode 100755 scripts/profiling.sh diff --git a/editing-history/2026-0316-1449-profiling-tools-reorg.md b/editing-history/2026-0316-1449-profiling-tools-reorg.md new file mode 100644 index 00000000..7c83dd31 --- /dev/null +++ b/editing-history/2026-0316-1449-profiling-tools-reorg.md @@ -0,0 +1,13 @@ +# Profiling tools reorganization + +## Summary + +- Moved profiling scripts from `scripts/` into dedicated `profiling/` directory. +- Added `profiling/README.md` with end-to-end usage for xctrace and samply workflows. +- Removed obsolete `scripts/profiling.sh` flamegraph helper script. + +## Knowledge notes + +- Keep profiling tooling isolated under `profiling/` to reduce script namespace clutter. +- `samply` profiling should run against built `target/*/cr` binaries to avoid sampling `rustc` compile threads. +- Preserve `.tmp-profiles/` as transient output directory for trace artifacts. diff --git a/profiling/README.md b/profiling/README.md new file mode 100644 index 00000000..92f82f2a --- /dev/null +++ b/profiling/README.md @@ -0,0 +1,84 @@ +# Profiling 工具说明 + +本目录集中放置 calcit 的 profiling 脚本: + +- `profile-once.sh`:基于 `xctrace Time Profiler` 一键录制并汇总。 +- `profile-summary.py`:解析 `xctrace` 导出的 XML 热点。 +- `samply-once.sh`:基于 `samply` 一键录制并汇总(函数级 self-time)。 +- `samply-summary.py`:解析 `.samply` 并输出热点函数占比。 + +## 1) xctrace 路线 + +### 一键执行 + +```bash +profiling/profile-once.sh calcit/fibo.cirru --top 20 --include 'calcit::' +``` + +输出: + +- `.tmp-profiles/-.trace` +- 终端中的热点函数统计、前缀聚合和优化提示 + +### 仅汇总已有 trace/xml + +```bash +python3 profiling/profile-summary.py --trace .tmp-profiles/fibo-xxxx.trace --top 30 +python3 profiling/profile-summary.py --xml /path/to/time-profile.xml --top 30 +``` + +常用参数: + +- `--top N`:显示前 N 个热点 +- `--include REGEX`:仅保留匹配符号(可重复) +- `--exclude REGEX`:排除匹配符号(可重复) +- `--keep-xml`:保留由 trace 导出的临时 XML + +## 2) samply 路线(更精准函数级) + +### 一键执行(debug) + +```bash +profiling/samply-once.sh calcit/fibo.cirru --top 20 --collapse-hash +``` + +### 一键执行(release) + +```bash +profiling/samply-once.sh calcit/fibo.cirru --release --top 20 --include 'calcit::|im_ternary_tree|alloc::' +``` + +输出: + +- `.tmp-profiles/--.samply` +- 终端中的函数级 self-time 热点(含占比) + +### 仅汇总已有 `.samply` + +```bash +python3 profiling/samply-summary.py --input .tmp-profiles/fibo-debug-xxxx.samply --binary target/debug/cr --top 30 +``` + +release 对应: + +```bash +python3 profiling/samply-summary.py --input .tmp-profiles/fibo-release-xxxx.samply --binary target/release/cr --top 30 +``` + +常用参数: + +- `--thread INDEX`:指定线程,默认自动选择样本最多线程 +- `--collapse-hash`:折叠 Rust 哈希后缀,便于分组 +- `--image-base 0x100000000`:`atos` 符号化基址(默认值) +- `--include / --exclude`:正则过滤 + +## 依赖 + +- xctrace 路线:`xctrace`, `cargo`, `python3` +- samply 路线:`samply`, `cargo`, `python3` +- 可选(samply 地址回填):`atos`(macOS 自带) + +## 说明 + +- `samply-once.sh` 会先 `cargo build`,再直接对 `target/debug/cr` 或 `target/release/cr` 录制,避免把 `rustc` 编译过程混入热点。 +- 临时产物位于 `.tmp-profiles/`,可按需清理。 diff --git a/profiling/profile-once.sh b/profiling/profile-once.sh new file mode 100755 index 00000000..6da6e4a0 --- /dev/null +++ b/profiling/profile-once.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: profiling/profile-once.sh [profile-summary args...]" >&2 + echo "Example: profiling/profile-once.sh calcit/fibo.cirru --top 20 --include 'calcit::'" >&2 + exit 1 +fi + +entry="$1" +shift + +if [[ ! -f "$entry" ]]; then + echo "Entry file not found: $entry" >&2 + exit 2 +fi + +for cmd in xctrace cargo python3; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Missing required command: $cmd" >&2 + exit 2 + fi +done + +mkdir -p .tmp-profiles + +entry_name="$(basename "$entry" .cirru)" +stamp="$(date +%Y%m%d-%H%M%S)" +trace_path=".tmp-profiles/${entry_name}-${stamp}.trace" + +echo "Recording trace to: $trace_path" +xctrace record \ + --template "Time Profiler" \ + --output "$trace_path" \ + --launch -- \ + cargo run --release --bin cr -- "$entry" + +echo +echo "Summarizing hotspots..." +python3 profiling/profile-summary.py --trace "$trace_path" "$@" + +echo +echo "Done. Trace bundle: $trace_path" \ No newline at end of file diff --git a/profiling/profile-summary.py b/profiling/profile-summary.py new file mode 100644 index 00000000..215f3c78 --- /dev/null +++ b/profiling/profile-summary.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 + +import argparse +import os +import re +import subprocess +import sys +import tempfile +import xml.etree.ElementTree as ET +from collections import Counter +from pathlib import Path + + +def export_time_profile_xml(trace_path: Path, xml_path: Path) -> None: + cmd = [ + "xctrace", + "export", + "--input", + str(trace_path), + "--xpath", + '/trace-toc/run[@number="1"]/data/table[@schema="time-profile"]', + "--output", + str(xml_path), + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + stderr = result.stderr.strip() or "(no stderr)" + raise RuntimeError(f"xctrace export failed: {stderr}") + + +def parse_frame_names(xml_path: Path) -> Counter[str]: + counts: Counter[str] = Counter() + for _, elem in ET.iterparse(xml_path, events=("end",)): + if elem.tag == "frame": + name = elem.attrib.get("name") + if name: + counts[name] += 1 + elem.clear() + return counts + + +def compile_patterns(patterns: list[str]) -> list[re.Pattern[str]]: + compiled = [] + for pattern in patterns: + compiled.append(re.compile(pattern)) + return compiled + + +def filter_counts( + counts: Counter[str], + include_patterns: list[re.Pattern[str]], + exclude_patterns: list[re.Pattern[str]], +) -> Counter[str]: + if not include_patterns and not exclude_patterns: + return counts + + filtered: Counter[str] = Counter() + for name, count in counts.items(): + if include_patterns and not any(regex.search(name) for regex in include_patterns): + continue + if exclude_patterns and any(regex.search(name) for regex in exclude_patterns): + continue + filtered[name] = count + return filtered + + +def summarize_by_prefix(counts: Counter[str]) -> list[tuple[str, int]]: + group = Counter() + for name, count in counts.items(): + parts = name.split("::") + if len(parts) >= 2: + key = "::".join(parts[:2]) + else: + key = parts[0] + group[key] += count + return group.most_common(10) + + +def derive_hints(counts: Counter[str]) -> list[str]: + joined = "\n".join(counts.keys()) + hints: list[str] = [] + if re.search(r"calcit::runner::(call_expr|evaluate_expr|run_fn_owned)", joined): + hints.append("解释器调度路径是主要热点,可优先检查 `call_expr`/`evaluate_expr` 的分支和数据转换。") + if re.search(r"CalcitProc::get_type_signature|check_proc_arity|type_annotation", joined): + hints.append("运行时类型签名/arity 检查占比不低,可考虑缓存签名结果或减少重复检查路径。") + if re.search(r"im_ternary_tree::.*to_vec|CalcitList::to_vec|drop_left", joined): + hints.append("持久结构与 Vec 转换频繁,建议减少 `to_vec` 往返或批量化访问。") + if re.search(r"alloc::|RawVec|drop::|triomphe::|rpds::", joined): + hints.append("分配与释放开销明显,建议优先减少短生命周期对象和临时容器创建。") + return hints + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Summarize xctrace Time Profiler output into LLM-friendly hotspot text." + ) + parser.add_argument("--trace", type=Path, help="Path to .trace bundle generated by xctrace") + parser.add_argument("--xml", type=Path, help="Path to exported time-profile XML") + parser.add_argument("--top", type=int, default=30, help="Number of top hotspot functions") + parser.add_argument( + "--include", + action="append", + default=[], + help="Regex include filter for symbol names (repeatable)", + ) + parser.add_argument( + "--exclude", + action="append", + default=[], + help="Regex exclude filter for symbol names (repeatable)", + ) + parser.add_argument( + "--keep-xml", + action="store_true", + help="Keep temporary XML when using --trace", + ) + args = parser.parse_args() + if not args.trace and not args.xml: + parser.error("One of --trace or --xml is required") + if args.trace and args.xml: + parser.error("Use only one of --trace or --xml") + if args.top <= 0: + parser.error("--top must be > 0") + return args + + +def main() -> int: + args = parse_args() + + xml_path: Path + temp_dir = None + if args.trace: + if not args.trace.exists(): + print(f"Trace file not found: {args.trace}", file=sys.stderr) + return 2 + temp_dir = tempfile.TemporaryDirectory(prefix="xctrace-export-") + xml_path = Path(temp_dir.name) / "time-profile.xml" + try: + export_time_profile_xml(args.trace, xml_path) + except RuntimeError as error: + print(str(error), file=sys.stderr) + return 3 + if args.keep_xml: + kept = args.trace.parent / f"{args.trace.name}.time-profile.xml" + kept.write_bytes(xml_path.read_bytes()) + print(f"Saved exported XML to: {kept}") + else: + xml_path = args.xml + if not xml_path.exists(): + print(f"XML file not found: {xml_path}", file=sys.stderr) + return 2 + + counts = parse_frame_names(xml_path) + include_patterns = compile_patterns(args.include) + exclude_patterns = compile_patterns(args.exclude) + filtered = filter_counts(counts, include_patterns, exclude_patterns) + + total_frames = sum(counts.values()) + filtered_frames = sum(filtered.values()) + print(f"Source XML: {xml_path}") + print(f"Total named frames: {total_frames}") + print(f"Frames after filter: {filtered_frames}") + print() + + print(f"Top {args.top} Hotspots") + print("-" * 72) + if not filtered: + print("(no matched symbols)") + else: + for name, count in filtered.most_common(args.top): + ratio = (count / filtered_frames * 100.0) if filtered_frames else 0.0 + print(f"{count:6d} {ratio:6.2f}% {name}") + + print() + print("Top Prefix Groups") + print("-" * 72) + for group, count in summarize_by_prefix(filtered): + ratio = (count / filtered_frames * 100.0) if filtered_frames else 0.0 + print(f"{count:6d} {ratio:6.2f}% {group}") + + print() + print("Optimization Hints") + print("-" * 72) + hints = derive_hints(filtered) + if not hints: + print("- 无明显通用模式,请结合 Top Hotspots 逐个函数分析。") + else: + for hint in hints: + print(f"- {hint}") + + if temp_dir is not None and not args.keep_xml: + temp_dir.cleanup() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/profiling/samply-once.sh b/profiling/samply-once.sh new file mode 100755 index 00000000..7b9ab818 --- /dev/null +++ b/profiling/samply-once.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: profiling/samply-once.sh [--release] [samply-summary args...]" >&2 + echo "Example: profiling/samply-once.sh calcit/fibo.cirru --top 20 --include 'calcit::'" >&2 + echo "Example: profiling/samply-once.sh calcit/fibo.cirru --release --collapse-hash" >&2 + exit 1 +fi + +entry="$1" +shift + +if [[ ! -f "$entry" ]]; then + echo "Entry file not found: $entry" >&2 + exit 2 +fi + +mode="debug" +if [[ "${1:-}" == "--release" ]]; then + mode="release" + shift +fi + +for cmd in samply cargo python3; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Missing required command: $cmd" >&2 + exit 2 + fi +done + +mkdir -p .tmp-profiles + +entry_name="$(basename "$entry" .cirru)" +stamp="$(date +%Y%m%d-%H%M%S)" +samply_path=".tmp-profiles/${entry_name}-${mode}-${stamp}.samply" + +if [[ "$mode" == "release" ]]; then + build_args=(build --release --bin cr) + binary="target/release/cr" +else + build_args=(build --bin cr) + binary="target/debug/cr" +fi + +echo "Building binary: $binary" +cargo "${build_args[@]}" + +if [[ ! -f "$binary" ]]; then + echo "Expected binary not found after build: $binary" >&2 + exit 3 +fi + +echo "Recording samply profile to: $samply_path" +samply record --save-only -o "$samply_path" -- "$binary" "$entry" + +binary_arg=(--binary "$binary") + +echo +echo "Summarizing hotspots..." +python3 profiling/samply-summary.py --input "$samply_path" "${binary_arg[@]}" "$@" + +echo +echo "Done. Samply file: $samply_path" \ No newline at end of file diff --git a/profiling/samply-summary.py b/profiling/samply-summary.py new file mode 100755 index 00000000..51200e55 --- /dev/null +++ b/profiling/samply-summary.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 + +import argparse +import json +import re +import subprocess +import sys +from collections import Counter +from pathlib import Path + + +ADDRESS_RE = re.compile(r"^0x[0-9a-fA-F]+$") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Summarize samply JSON into function-level self-time hotspots." + ) + parser.add_argument("--input", type=Path, required=True, help="Path to .samply JSON") + parser.add_argument("--top", type=int, default=30, help="Number of top functions") + parser.add_argument("--thread", type=int, help="Thread index to analyze (default: auto)") + parser.add_argument( + "--include", + action="append", + default=[], + help="Regex include filter for symbol names (repeatable)", + ) + parser.add_argument( + "--exclude", + action="append", + default=[], + help="Regex exclude filter for symbol names (repeatable)", + ) + parser.add_argument( + "--binary", + type=Path, + help="Mach-O binary for atos symbolization fallback (e.g. target/debug/cr)", + ) + parser.add_argument( + "--image-base", + type=lambda raw: int(raw, 0), + default=0x100000000, + help="Image base added to frame address before atos (default: 0x100000000)", + ) + parser.add_argument( + "--collapse-hash", + action="store_true", + help="Collapse Rust symbol hash suffix (::h...) for easier grouping", + ) + args = parser.parse_args() + if args.top <= 0: + parser.error("--top must be > 0") + return args + + +def compile_patterns(patterns: list[str]) -> list[re.Pattern[str]]: + return [re.compile(pattern) for pattern in patterns] + + +def choose_thread(data: dict, explicit: int | None) -> tuple[int, dict]: + threads = data.get("threads") + if not isinstance(threads, list) or not threads: + raise ValueError("No threads found in .samply file") + + if explicit is not None: + if explicit < 0 or explicit >= len(threads): + raise ValueError(f"--thread out of range: {explicit} (total {len(threads)})") + return explicit, threads[explicit] + + best_idx = 0 + best_count = -1 + for index, thread in enumerate(threads): + samples = thread.get("samples", {}) + stacks = samples.get("stack", []) + count = sum(1 for entry in stacks if entry is not None) + if count > best_count: + best_count = count + best_idx = index + return best_idx, threads[best_idx] + + +def lookup_frame_symbol(thread: dict, frame_index: int) -> tuple[str, int | None]: + frame_table = thread.get("frameTable", {}) + func_table = thread.get("funcTable", {}) + native_symbols = thread.get("nativeSymbols", {}) + strings = thread.get("stringArray", []) + + func_index = frame_table.get("func", [None])[frame_index] + if func_index is not None: + name_index = func_table.get("name", [None])[func_index] + if name_index is not None: + symbol = strings[name_index] + if ADDRESS_RE.match(symbol): + return symbol, int(symbol, 16) + return symbol, None + + native_index = frame_table.get("nativeSymbol", [None])[frame_index] + if native_index is not None: + name_index = native_symbols.get("name", [None])[native_index] + if name_index is not None: + symbol = strings[name_index] + if ADDRESS_RE.match(symbol): + return symbol, int(symbol, 16) + return symbol, None + + address = frame_table.get("address", [None])[frame_index] + if address is not None: + return f"0x{address:x}", address + + return "", None + + +def atos_symbolize(binary: Path, image_base: int, addresses: list[int]) -> dict[int, str]: + if not addresses: + return {} + + absolute_addresses = [hex(image_base + address) for address in addresses] + cmd = ["atos", "-o", str(binary), *absolute_addresses] + result = subprocess.run(cmd, capture_output=True, text=True) + + mapping: dict[int, str] = {} + lines = result.stdout.splitlines() + for address, line in zip(addresses, lines): + symbol = line.strip() + if symbol and not ADDRESS_RE.match(symbol): + mapping[address] = symbol + return mapping + + +def normalize_symbol(raw: str, collapse_hash: bool) -> str: + text = raw.strip() + if collapse_hash: + text = re.sub(r"::h[0-9a-f]{16,}$", "", text) + return text + + +def summarize( + thread: dict, + include_patterns: list[re.Pattern[str]], + exclude_patterns: list[re.Pattern[str]], + collapse_hash: bool, + atos_map: dict[int, str], +) -> tuple[Counter[str], float]: + stack_table = thread.get("stackTable", {}) + samples = thread.get("samples", {}) + + sample_stacks = samples.get("stack", []) + weights = samples.get("weight") + if not isinstance(weights, list) or len(weights) != len(sample_stacks): + weights = [1.0] * len(sample_stacks) + + counts: Counter[str] = Counter() + total_weight = 0.0 + for index, stack_index in enumerate(sample_stacks): + if stack_index is None: + continue + frame_index = stack_table.get("frame", [None])[stack_index] + if frame_index is None: + continue + symbol, address = lookup_frame_symbol(thread, frame_index) + if address is not None and address in atos_map: + symbol = atos_map[address] + symbol = normalize_symbol(symbol, collapse_hash) + + if include_patterns and not any(regex.search(symbol) for regex in include_patterns): + continue + if exclude_patterns and any(regex.search(symbol) for regex in exclude_patterns): + continue + + weight = float(weights[index]) + counts[symbol] += weight + total_weight += weight + + return counts, total_weight + + +def collect_unresolved_addresses(thread: dict, sample_limit: int) -> list[int]: + stack_table = thread.get("stackTable", {}) + sample_stacks = thread.get("samples", {}).get("stack", []) + addresses: Counter[int] = Counter() + + for stack_index in sample_stacks: + if stack_index is None: + continue + frame_index = stack_table.get("frame", [None])[stack_index] + if frame_index is None: + continue + symbol, address = lookup_frame_symbol(thread, frame_index) + if address is None: + continue + if ADDRESS_RE.match(symbol): + addresses[address] += 1 + + return [address for address, _ in addresses.most_common(sample_limit)] + + +def main() -> int: + args = parse_args() + if not args.input.exists(): + print(f"Input file not found: {args.input}", file=sys.stderr) + return 2 + + try: + data = json.loads(args.input.read_text()) + except json.JSONDecodeError as error: + print(f"Invalid JSON in {args.input}: {error}", file=sys.stderr) + return 2 + + try: + thread_index, thread = choose_thread(data, args.thread) + except ValueError as error: + print(str(error), file=sys.stderr) + return 2 + + include_patterns = compile_patterns(args.include) + exclude_patterns = compile_patterns(args.exclude) + + atos_map: dict[int, str] = {} + if args.binary is not None: + unresolved = collect_unresolved_addresses(thread, sample_limit=max(200, args.top * 10)) + if unresolved: + atos_map = atos_symbolize(args.binary, args.image_base, unresolved) + + counts, total_weight = summarize( + thread, + include_patterns, + exclude_patterns, + args.collapse_hash, + atos_map, + ) + + print(f"Input .samply: {args.input}") + print(f"Thread index: {thread_index}") + print(f"Thread name: {thread.get('name', '')}") + print(f"Samples after filter (weight): {total_weight:.2f}") + if args.binary is not None: + print(f"Atos binary: {args.binary}") + print(f"Atos symbolized addresses: {len(atos_map)}") + print() + + print(f"Top {args.top} Self-Time Hotspots") + print("-" * 72) + if not counts: + print("(no matched symbols)") + return 0 + + for symbol, weight in counts.most_common(args.top): + ratio = (weight / total_weight * 100.0) if total_weight else 0.0 + print(f"{weight:10.2f} {ratio:6.2f}% {symbol}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/scripts/profiling.sh b/scripts/profiling.sh deleted file mode 100755 index 33ce5136..00000000 --- a/scripts/profiling.sh +++ /dev/null @@ -1,7 +0,0 @@ - -cargo build --release -sudo flamegraph -o target/my_flamegraph.svg target/release/calcit -echo `pwd`/`ls target/my_flamegraph.svg` | pbcopy - -echo -echo "Copiled svg path." \ No newline at end of file From 7f446e185985a4c48d05a9331182be26549cca12 Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 16 Mar 2026 23:54:11 +0800 Subject: [PATCH 14/57] refactor: clean runtime boundary compatibility --- drafts/runtime-boundary-refactor-plan.md | 438 +++++++++ ...2026-0316-2353-runtime-boundary-cleanup.md | 14 + src/bin/cr.rs | 8 +- src/bin/cr_tests/type_fail.rs | 2 +- src/calcit.rs | 3 +- src/calcit/symbol.rs | 2 +- src/calcit/thunk.rs | 31 +- src/calcit/type_annotation.rs | 16 +- src/call_tree.rs | 4 - src/codegen/emit_js.rs | 70 +- src/codegen/emit_js/deps.rs | 33 +- src/codegen/gen_ir.rs | 10 +- src/lib.rs | 2 +- src/program.rs | 831 ++++++++++++++++-- src/runner.rs | 101 ++- src/runner/preprocess.rs | 266 +++--- 16 files changed, 1547 insertions(+), 284 deletions(-) create mode 100644 drafts/runtime-boundary-refactor-plan.md create mode 100644 editing-history/2026-0316-2353-runtime-boundary-cleanup.md diff --git a/drafts/runtime-boundary-refactor-plan.md b/drafts/runtime-boundary-refactor-plan.md new file mode 100644 index 00000000..25c431b2 --- /dev/null +++ b/drafts/runtime-boundary-refactor-plan.md @@ -0,0 +1,438 @@ +# Runtime Boundary Refactor Plan + +> 目标:在**保留 watch 模式热更新**与**保留 JS codegen 依赖原始代码结构**两项能力的前提下,重构 runtime / preprocess / codegen 边界,降低热路径上的 lookup、clone、thunk 污染与全局状态耦合。 + +## 为什么现在做 + +当前实现把 3 类本应分开的信息混在了一起: + +- 源代码与预处理后的代码表示; +- 运行时求值缓存; +- 跨 reload 保留的状态。 + +这直接导致几个后果: + +- `preprocess` 会为初始化写入 `Nil` 占位,再递归求值/包 thunk,再写回全局状态; +- runtime 为了 JS codegen 保留 `Calcit::Thunk(Code)`,code 与 value 共存于一套值表示中; +- hot reload 过去只能依赖旧的 evaled store 做粗粒度清理,难以做精确失效; +- lookup 热点落在全局字符串查表与容器扫描上,而不是落在真正的业务执行上。 + +这些问题并不是 watch 模式或 JS codegen 本身要求的,而是**当前把两者都编码进 runtime 值模型**造成的。 + +## 不变约束 + +本重构默认以下语义保持不变: + +1. watch 模式下,`namespace/def` 对应的状态在定义未发生实质性变化时可保留。 +2. JS codegen 必须能获得足够稳定的代码表示,不能依赖“值已经在 Rust runtime 里算完”。 +3. thunk / lazy top-level def 的用户语义保持一致。 +4. 现有宏、preprocess、trait/method 校验规则在外部行为上不主动改变。 +5. 调试能力允许暂时退化,后续由专门工具补足。 + +## 核心判断 + +要保留这两项能力,同时让边界变清晰,必须接受一件事: + +**compile-time representation 和 runtime representation 不能再共用同一个 `Calcit` 值对象。** + +也就是说: + +- codegen 看的是编译产物; +- runtime 看的是求值状态; +- 持久状态看的是稳定 identity 绑定的 state slot; +- 三者不再复用一份“既像代码又像值”的对象。 + +这不是语言语义变化,而是实现层面的去同构。 + +## 当前进度 + +截至 2026-03-16,已经完成的不是“纯设计”,而是一部分边界已经落地: + +- `DefId` 已经引入,并建立了 `ns/def -> DefId` 稳定索引; +- `CompiledDef` 已经存在,且开始承载 `preprocessed_code`、`codegen_form`、`deps`、`type_summary` 等编译期信息; +- JS / IR codegen 已经切到读取 compiled layer,而不是直接读取 evaled program; +- runtime 已经有并行的 `DefId -> RuntimeCell` 稠密表,作为旧 `EntryBook` 之外的新快路径; +- `CalcitImport` 已开始携带稳定 `def_id` 缓存,runtime lookup 已优先消费它; +- import/runtime lookup 兼容路径里的旧 `coord -> EntryBook` 残余已经清掉,`CalcitImport.coord` 与相关 runner 参数已删除; +- `RuntimeCell` 的最小状态机已经落下,包含 `Cold | Resolving | Ready | Errored`,且 preprocess 的循环保护已从“先写 `Nil`”切到显式 `Resolving`; +- `yarn check-all` 已经作为当前重构的主验证门槛,并且需要先于 `cargo test` 跑通。 + +同时也要明确,当前还没有真正完成的部分是: + +- 旧的 `PROGRAM_EVALED_DATA_STATE` 与 `lookup_evaled_def*` 兼容路径已经删除,`preprocess` 成功路径也不再直接写 runtime cache;compiled fallback 现在只做“读 compiled value”,不再把 compiled payload 回填成 `RuntimeCell::Ready`; +- 全局 `CompiledDef` 已不再携带 `runtime_value` 这类 runtime payload 字段;普通 preprocess 输出已经完全不构造 runtime payload,普通 compiled `Fn/Macro/Proc/Syntax/LazyValue` 都改为需要时从 `preprocessed_code` 临时 materialize;当前剩余耦合主要集中在 compiled/runtime 仍共享同一套 `Calcit` 值表示,以及 codegen snapshot 在 source-backed defs 缺 compiled metadata 时仍可能退回到 runtime-derived snapshot fallback entry; +- `Calcit::Thunk` 仍存在于公开值模型中,但 runtime 主路径已经不再依赖它作为缓存载体:lazy def 的待求值占位优先放进 `RuntimeCell::Lazy`,`Ready(Thunk)` 已被禁止写入 runtime store,preprocess 与普通 lookup 也不再把 runtime lazy cell 重新包装成公共 thunk;当前剩余问题主要转向 snapshot fallback 与少量兼容语义分支; +- watch 模式已经开始利用 compiled deps 做 def 级 invalidation;当前 CLI incremental reload 不再默认按 package 整片清空,而是从 changed defs / ns 头部变更出发做依赖闭包清理。剩余缺口主要是 state slot 尚未引入,以及 watch/reload 回归覆盖还不够强。 + +这意味着当前状态更准确的表述是: + +**Phase 1 和 Phase 2 已经实装,Phase 3A/3B 已经稳定运行,Phase 3D 的主路径删除也已完成;剩余重点转向 3C 以及 preprocess/runtime 的最终拆边。** + +## 目标边界模型 + +建议把当前系统拆成 4 层。 + +### 1. Source Layer + +输入资产: + +- snapshot / changeset +- 原始 Cirru +- 文档、schema、examples + +特点: + +- 只负责装载、diff、持久化 +- 不承担 runtime lookup +- 不承担 codegen 的最终消费格式 + +### 2. Compiled Layer + +新增核心对象:`CompiledDef` + +建议字段: + +- `def_id`: 稳定定义 ID +- `version_id`: 本次编译版本号 +- `kind`: `Fn | Macro | LazyValue | ConstValue | SyntaxProxy | NativeProxy` +- `source_meta`: `ns/def/doc/schema/examples` +- `preprocessed_code`: 预处理后的 lowered form +- `codegen_form`: 供 JS/IR 输出的稳定表示 +- `deps`: 定义级依赖列表 +- `type_summary`: 参数/返回值/trait 约束摘要 +- `state_policy`: `Stateless | PreserveAcrossReload | ResetOnChange` + +职责: + +- 承接 preprocess 的主要产出 +- 为 codegen 提供输入 +- 为 runtime 提供可执行 payload + +关键变化: + +- `emit_js` 不再读取 evaled program,而是直接消费 `CompiledDef.codegen_form` +- thunk 不再承担“保留 code for codegen”的职责 + +### 3. Runtime Layer + +新增核心对象:`RuntimeCell` + +建议状态机: + +- `Cold` +- `Resolving` +- `Ready(Calcit)` +- `Errored(CalcitErr)` + +可选扩展: + +- `ReadyLazy(Calcit)` 用于保留部分 lazy 语义 + +职责: + +- 仅管理定义的求值状态 +- 只缓存运行时值,不保存 codegen 所需代码 +- 通过 `DefId` 直接索引,而不是热路径上反复字符串查找 + +关键变化: + +- 旧的 `PROGRAM_EVALED_DATA_STATE` 已删除,runtime store 现在实际就是按 `DefId` 索引的 runtime cell table +- `evaluate_symbol_from_program` 首先解析到 `DefId`,之后只在 runtime table 中操作 +- `Calcit::Thunk` 可逐步降级为 runtime 内部机制,而非通用值构造器 + +### 4. Persistent State Layer + +新增核心对象:`StateSlot` + +建议形式: + +- `StateSlotId` +- `owner_def_id` +- `generation` +- `payload` + +职责: + +- 保存需要跨 reload 保留的状态 +- 与 runtime 值缓存分离 +- 在 hot reload 时按稳定 identity 决定复用或重置 + +关键变化: + +- “保留状态”不再等于“保留整个 def 的 evaled value” +- `Ref` / atom / runtime resource 迁移到显式 state slot 体系 + +## 两项关键能力如何保留 + +### A. 保留 watch 模式 + +现状问题: + +- reload 主要通过局部删除 evaled defs 实现; +- 定义与状态耦合,导致失效粒度粗; +- 同一个 def 的代码变更与状态保留难以分开判断。 + +重构后: + +1. source diff 生成 changed defs 集合。 +2. 仅重编译受影响的 `CompiledDef`。 +3. 根据依赖图传播 invalidation 到 `RuntimeCell`。 +4. `PersistentStateLayer` 按 `state_policy + identity` 判断是否复用。 +5. `reload-fn` 基于新的 compiled graph 与 preserved state 执行。 + +语义收益: + +- 代码热更新与状态保留不再互相污染; +- 可以做更精确的 def 级失效,而不是 package 级清理; +- watch 路径的运行时开销更可控。 + +### B. 保留 JS codegen + +现状问题: + +- `emit_js` 直接从 evaled program 读取定义; +- value defs 必须保留成 thunk 才能拿到 code; +- runtime 值表示被 codegen 需求反向塑形。 + +重构后: + +- codegen 仅读 `CompiledDef.codegen_form` +- runtime 是否已经求值,与 codegen 无关 +- lazy value 是否在 Rust runtime 里缓存,也与 JS 输出无关 + +语义收益: + +- JS codegen 与 runtime 生命周期解耦; +- thunk 从“跨层共享对象”退化为“编译/运行时的内部策略”; +- 后续 IR/JS backend 可单独优化。 + +## 需要引入的稳定 identity + +这是整个重构的关键。 + +建议新增: + +- `NsId` +- `DefId` +- `CompiledVersionId` +- `StateSlotId` + +规则: + +- `DefId` 对应语义上的“这个定义”,跨编译版本保持稳定; +- `CompiledVersionId` 对应某次重编译产物; +- `StateSlotId` 对应可保留状态的拥有者; +- `coord`/字符串查找只能作为 debug 与兼容路径,不再作为主索引。 + +## 对 thunk 的重定义 + +当前问题不在于 thunk 本身,而在于 thunk 暴露成了 runtime 公共值模型的一部分。 + +建议分两步处理: + +### 阶段一:保留外部语义,收缩内部职责 + +- 用户仍可观察到 lazy top-level def 行为 +- 但 codegen 不再读取 `Calcit::Thunk` +- `Calcit::Thunk` 只用于 runtime 求值过程 + +### 阶段二:把 thunk 从通用值中移出 + +- 仅在 `RuntimeCell` 内部表达 lazy state +- runtime 对外暴露的仍是正常 `Calcit` +- codegen / preprocess 不再依赖 thunk 值分支 + +这一步会显著降低 evaluator、preprocess、codegen 三方的共享复杂度。 + +## 对 preprocess 的新边界定义 + +建议明确: + +- preprocess 的职责是把 source form 变成 `CompiledDef.preprocessed_code` +- preprocess 可以读取 `CompiledDef` 依赖与类型摘要 +- preprocess 不直接写 runtime value table +- preprocess 不再通过写 `Nil` 到 evaled defs 来打断循环 + +循环依赖处理改为: + +- 编译层使用 `Compiling / Compiled / Failed` 状态; +- runtime 层使用 `Resolving / Ready / Errored` 状态; +- 两套状态机分别处理,不再混用。 + +这里需要特别澄清一件事: + +当前 `preprocess` 已经开始产出 compiled metadata,成功路径也已经不再直接写 runtime cache;runtime 也不再在 `Cold` 状态下把 compiled payload 回填成 `RuntimeCell::Ready`。对于 lazy def,当前只会先把 compiled metadata 重新播种成 `RuntimeCell::Lazy`,而不是直接伪装成 ready runtime value。这说明边界已经继续前进一步,但还没有完全完成去同构。最终目标依然是: + +- `preprocess` 负责产出 `CompiledDef`; +- runtime 在真正需要值时,才从 compiled payload 驱动求值; +- 循环检测不再依赖“先写一个 `Nil` 到 runtime 再回来填值”。 + +## 数据结构建议 + +本次重构不是简单把 `EntryBook` 换成 `HashMap`,但主路径的数据结构必须同步升级。 + +建议: + +- `DefId -> RuntimeCell` 使用稠密表或 `Vec` +- `ns/def -> DefId` 使用只读索引表(初始化后稳定) +- source/meta 数据仍可保留 `HashMap` +- state slot table 使用稠密索引或 slab 风格容器 + +原则: + +- 字符串查找只发生在装载/编译边界; +- steady-state runtime 尽量只做整数索引; +- 热路径避免 `Arc` 比较和容器扫描。 + +## 迁移阶段 + +### Phase 0: 约束冻结 + +- 明确哪些行为必须保持 +- 给 watch / reload / js codegen 补回归测试 +- 记录当前热点和基线 +- 验证顺序固定为:`cargo fmt && yarn check-all && cargo test -q` + +### Phase 1: 引入 `DefId`,不改外部行为 + +- 建立 `ns/def -> DefId` 索引 +- lookup 仍兼容旧路径 +- 为后续 runtime/compiled 分层做准备 + +当前状态:已完成。 + +### Phase 2: 引入 `CompiledDef` + +- 把 preprocess 产物显式化 +- `emit_js` 切到 `CompiledDef.codegen_form` +- 仍保留旧 runtime 值缓存 + +当前状态:主体已完成,但 codegen snapshot 仍允许从 runtime ready 值做 fallback 填补空洞。 + +### Phase 3: 引入 `RuntimeCell` + +- 将 `PROGRAM_EVALED_DATA_STATE` 重写为 runtime cell table +- thunk 改为 runtime 内部状态机的一部分 +- `evaluate_symbol_from_program` 改为 `DefId` 驱动 + +这里不应该一次性硬切,建议拆成 4 个小阶段: + +#### Phase 3A: 并行 runtime 索引 + +- 建立 `DefId -> runtime slot` 稠密表; +- 先让 runtime slot 成为主快路径;当时 `write_evaled_def` / reload / changeset 仍与旧表并行同步; +- `evaluate_symbol_from_program` 优先走 `DefId`,旧路径保留 fallback。 + +当前状态:已完成。 + +#### Phase 3B: 显式 RuntimeCell 状态机 + +- 把 `Option` 提升为 `RuntimeCell`; +- 最少先实现 `Cold | Resolving | Ready | Errored`; +- 把“循环中先写 `Nil`”替换成显式 `Resolving`。 + +当前状态:已完成,且正常 runtime 路径已经不再依赖兼容 lookup。`preprocess` 已不再把 `Resolving` 暂时转成 `Calcit::Nil`,而是把“确保已 preprocess”与“读取可用值”分开;并且普通 preprocess 输出已经完全不再构造 runtime payload。compiled fallback 也已经不再把 cold def 写回 `RuntimeCell::Ready`,而只是临时读取 compiled value。全局 `CompiledDef` 已不再保存 `runtime_value`,普通 compiled `Fn/Macro/Proc/Syntax/LazyValue` 改为按需从 `preprocessed_code` materialize。codegen snapshot 现在会优先把 source-backed 缺口补成真正的 compiled def,只有补不上时才退回 runtime-derived snapshot fallback。剩余串线主要是 compiled/runtime 仍共用 `Calcit` 这套值表示,以及 snapshot fallback 仍作为最后兜底存在。 + +#### Phase 3C: thunk 职责内收 + +- `Calcit::Thunk` 继续保留外部语义; +- 但 runtime 内部缓存与 lazy 状态优先放进 `RuntimeCell`; +- 减少 thunk 对全局写回和 code 表示的承担。 + +当前状态:主路径已基本收尾。thunk 仍是公开 `Calcit` 值模型的一部分,但 runtime store 已不再接受 `Ready(Thunk)` 这类形态;lazy def 的未求值占位优先放进 `RuntimeCell::Lazy`,raw fallback 若得到 `Calcit::Thunk(Code)` 也会立刻规范化回 lazy cell。`eval_symbol_from_program` 也不再把 lazy thunk 返回给调用方,preprocess 查值路径同样不再把 runtime lazy cell 重新包装成公共 thunk。当前剩余工作主要不再是 thunk 主路径,而是继续压缩 snapshot fallback compiled entry 的存在范围,清理少量旧命名/提示语残留,并补齐更直接的 watch/reload 回归测试。 + +#### Phase 3D: 删除旧 EntryBook 热路径依赖 + +- `evaluate_symbol_from_program` 不再依赖 `coord -> EntryBook` 作为主快路径; +- `PROGRAM_EVALED_DATA_STATE` 从主 runtime store 降级为兼容层或被删除; +- watch/reload 改为直接操作 runtime cell table。 + +当前状态:主体已完成。`coord -> EntryBook` 主快路径已经删除,`PROGRAM_EVALED_DATA_STATE` 也已删除;watch/reload 也已经开始直接基于 `DefId` 与 compiled deps 做依赖闭包失效。剩余问题不再是“还停留在 package 级清理”,而是要把剩余边界情况和回归测试补齐,并继续把状态保留语义从值缓存里彻底拆出去。 + +### Phase 4: 引入 `PersistentStateLayer` + +- `Ref` 与其他跨 reload 状态转入 state slot +- 定义级值缓存与状态对象彻底脱钩 + +这一步的前提不是“有了 DefId 就可以做”,而是: + +- runtime cell 的 identity 已经稳定; +- reload invalidation 规则已经以 `DefId` 为主; +- 不再把“值缓存是否保留”误当成“状态是否保留”。 + +### Phase 5: 删除旧耦合路径 + +- 删除 codegen 对 evaled program 的依赖 +- 删除 preprocess 对 runtime 写入 `Nil` 的循环保护策略 +- 删除或收缩 `Calcit::Thunk` 的公共职责 + +进入 Phase 5 的标志应该非常明确,而不是“感觉差不多了”: + +- `yarn check-all` 与 `cargo test` 在没有旧 evaled fallback 的情况下仍然稳定通过; +- JS/IR codegen 不再从 runtime 值层偷拿任何结构信息; +- runtime cycle detection 已完全基于 `RuntimeCell` 状态机; +- watch reload 可以基于 compiled deps + stable identity 做解释得通的失效。 + +## 接下来应该怎么做 + +如果目标是继续往前推进,同时不让风险失控,下一步不应该直接去碰 state slot,也不需要再把重点放回 Phase 3B;更合理的是继续收尾 Phase 3C,并收缩 snapshot/codegen-only fallback。 + +具体就是: + +1. 继续收缩 runtime-derived snapshot fallback entry 的存在范围,优先区分哪些定义只是 runtime-only 注入,哪些本应来自 source/compiled 数据;source-backed defs 则优先补成真正的 compiled def。 +2. 给新的 compiled-deps reload invalidation 补更直接的 watch/reload 回归测试,并继续收缩仍需兜底的边界情况。 +3. 仅在确有必要时,再继续清理少数仍保留公开 thunk 语义的兼容分支;不要再把重点放回 runtime 主路径。 +4. 每一步都以 `cargo fmt && yarn check-all && cargo test -q` 为门槛,而不是只跑 Rust 单测。 + +换句话说,下一步的目标不是“再引入一个新层”,也不是回头重复 3B,而是: + +**把已经接进去的 runtime state machine 和 snapshot/codegen 边界,从“只剩最后几座桥”继续推进到真正职责清晰、桥接范围可解释。** + +## 预期收益 + +### 性能 + +- steady-state runtime 从字符串 lookup 转向整数索引 +- preprocess 不再反复通过 runtime state 做协调 +- codegen 不再污染 runtime 值模型 + +### 架构 + +- compile / runtime / state 三层边界清晰 +- hot reload 失效规则可解释、可测试 +- thunk 语义从“跨层共享”变成“局部实现细节” + +### 可维护性 + +- 性能问题更容易定位到某一层 +- 调试工具可针对不同层独立建设 +- 以后若加 IR backend,不需要继续借 runtime 值做桥接 + +## 主要风险 + +1. 旧代码默认假设“所有定义最终都能在 `Calcit` 值层找到表示”,重构后会打破这个心智模型。 +2. reload 语义如果没有清晰 identity 规则,容易出现状态误保留或误清理。 +3. 宏与 preprocess 若隐式依赖 runtime 当前行为,需要在阶段 2 前先全面梳理。 +4. 调试输出会暂时退化,因为 call stack / source mapping 需要重新挂接到 `CompiledDef`。 + +## 建议的第一步实现 + +如果只做一个高 ROI 起步动作,建议先做: + +**先引入 `DefId + CompiledDef`,并把 JS codegen 从 evaled program 上拆下来。** + +理由: + +- 这是最容易与现有 runtime 并存的一步; +- 能立刻切断“为了 JS 保留 thunk”这一层耦合; +- 一旦 codegen 不再依赖 runtime value,后续 runtime/state 分层会简单很多。 + +## 最终判断 + +在保留 watch 模式与 JS codegen 的前提下,Calcit 仍然可以做激进重构,而且值得做。 + +真正需要放弃的不是功能,而是这件事: + +**“同一个 runtime 值对象同时承载源码、预处理结果、惰性求值状态、热更新身份与 codegen 输入。”** + +只要不再坚持这件事,边界就能重新变清晰。 diff --git a/editing-history/2026-0316-2353-runtime-boundary-cleanup.md b/editing-history/2026-0316-2353-runtime-boundary-cleanup.md new file mode 100644 index 00000000..6bee9e1e --- /dev/null +++ b/editing-history/2026-0316-2353-runtime-boundary-cleanup.md @@ -0,0 +1,14 @@ +# Runtime Boundary Cleanup + +## Summary + +- removed the dead `CalcitImport.coord` compatibility path and the related runner parameter flow +- renamed remaining `evaled`-era runtime lookup terminology to `runtime-ready` / `runtime cache` +- synced the runtime boundary refactor draft with the current migration state and recent cleanup progress + +## Knowledge + +- `CalcitImport.def_id` is now the stable import-side runtime lookup anchor; the old `coord -> EntryBook` path no longer needs to be preserved in import metadata +- type-annotation lookup registration should describe runtime-ready value lookup, not the removed evaled-store model +- incremental reload messaging should talk about clearing runtime caches rather than evaled states to match the current architecture +- this migration tail is now mostly about narrowing fallback bridges and improving reload coverage, not about preserving old runtime naming or lookup compatibility \ No newline at end of file diff --git a/src/bin/cr.rs b/src/bin/cr.rs index c474bf3e..1e305f0f 100644 --- a/src/bin/cr.rs +++ b/src/bin/cr.rs @@ -345,7 +345,7 @@ fn recall_program(content: &str, entries: &ProgramEntries, settings: &ToplevelCa // Steps: // 1. load changes file, and patch to program_code - // 2. clears evaled states, gensym counter + // 2. clears runtime caches, gensym counter // 3. rerun program, and catch error let data = cirru_edn::parse(content).map_err(|e| { @@ -394,10 +394,10 @@ fn recall_program(content: &str, entries: &ProgramEntries, settings: &ToplevelCa program::apply_code_changes(&changes)?; println!("{} Changes applied to program", "✓".green()); - // clear data in evaled states - program::clear_all_program_evaled_defs(entries.init_ns.to_owned(), entries.reload_ns.to_owned(), settings.reload_libs)?; + // clear invalidated runtime cache entries + program::clear_runtime_caches_for_changes(&changes, settings.reload_libs)?; builtins::meta::force_reset_gensym_index()?; - println!("cleared evaled states and reset gensym index."); + println!("cleared runtime caches and reset gensym index."); // Create a minimal snapshot for documentation lookup during incremental updates // In practice, this could be enhanced to maintain documentation state diff --git a/src/bin/cr_tests/type_fail.rs b/src/bin/cr_tests/type_fail.rs index 2f84e308..098f7357 100644 --- a/src/bin/cr_tests/type_fail.rs +++ b/src/bin/cr_tests/type_fail.rs @@ -27,7 +27,7 @@ fn load_fixture_entries(path: &str) -> ProgramEntries { let (init_ns, init_def) = util::string::extract_ns_def(&config_init).expect("extract init ns/def"); let (reload_ns, reload_def) = util::string::extract_ns_def(&config_reload).expect("extract reload ns/def"); - program::clear_all_program_evaled_defs(init_ns.clone().into(), reload_ns.clone().into(), true).expect("clear evaled defs"); + program::clear_runtime_caches_for_reload(init_ns.clone().into(), reload_ns.clone().into(), true).expect("clear runtime caches"); let warmup_warnings: RefCell> = RefCell::new(vec![]); runner::preprocess::preprocess_ns_def( diff --git a/src/calcit.rs b/src/calcit.rs index 8d51f0c5..196843e2 100644 --- a/src/calcit.rs +++ b/src/calcit.rs @@ -163,7 +163,6 @@ impl fmt::Display for Calcit { } // TODO, escaping choices Thunk(thunk) => match thunk { CalcitThunk::Code { code, .. } => f.write_str(&format!("(&thunk _ {code})")), - CalcitThunk::Evaled { code, value } => f.write_str(&format!("(&thunk {value} {code})")), }, CirruQuote(code) => f.write_str(&format!("(&cirru-quote {code})")), Ref(name, _locked_pair) => f.write_str(&format!("(&ref {name} ...)")), @@ -1389,7 +1388,7 @@ mod tests { at_def: Arc::from("demo"), at_ns: Arc::from("app.main"), }), - coord: None, + def_id: None, }); assert_ne!(symbol, local); diff --git a/src/calcit/symbol.rs b/src/calcit/symbol.rs index 4bd5659c..cf2b00df 100644 --- a/src/calcit/symbol.rs +++ b/src/calcit/symbol.rs @@ -86,7 +86,7 @@ pub struct CalcitImport { /// from npm package, use `default` and asterisk in js pub def: Arc, pub info: Arc, - pub coord: Option<(u16, u16)>, + pub def_id: Option, } /// compare at namespace level, ignore at_def diff --git a/src/calcit/thunk.rs b/src/calcit/thunk.rs index cf52b943..c8121ccd 100644 --- a/src/calcit/thunk.rs +++ b/src/calcit/thunk.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::{Calcit, CalcitErr, call_stack::CallStackList, program, runner::evaluate_expr}; +use crate::{Calcit, CalcitErr, CalcitErrKind, call_stack::CallStackList, program, runner::evaluate_expr}; use super::CalcitScope; @@ -14,34 +14,37 @@ pub struct CalcitThunkInfo { #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum CalcitThunk { Code { code: Arc, info: Arc }, - Evaled { code: Arc, value: Arc }, } impl CalcitThunk { pub fn get_code(&self) -> &Calcit { match self { Self::Code { code, .. } => code, - Self::Evaled { code, .. } => code, } } /// evaluate the thunk, and write back to program state pub fn evaluated(&self, scope: &CalcitScope, call_stack: &CallStackList) -> Result { match self { - Self::Evaled { value, .. } => Ok((**value).to_owned()), Self::Code { code, info } => { + program::mark_runtime_def_resolving(&info.ns, &info.def); + // println!("from thunk: {}", sym); - let evaled_v = evaluate_expr(code, scope, &info.ns, call_stack)?; + let runtime_value = match evaluate_expr(code, scope, &info.ns, call_stack) { + Ok(value) => value, + Err(e) => { + program::mark_runtime_def_errored(&info.ns, &info.def, Arc::from(e.to_string())); + return Err(e); + } + }; + // and write back to program state to fix duplicated evalution - program::write_evaled_def( - &info.ns, - &info.def, - Calcit::Thunk(Self::Evaled { - code: code.to_owned(), - value: Arc::new(evaled_v.to_owned()), - }), - )?; - Ok(evaled_v) + if let Err(e) = program::write_runtime_ready(&info.ns, &info.def, runtime_value.to_owned()) { + program::mark_runtime_def_errored(&info.ns, &info.def, Arc::from(e.as_str())); + return Err(CalcitErr::use_msg_stack(CalcitErrKind::Unexpected, e, call_stack)); + } + + Ok(runtime_value) } } } diff --git a/src/calcit/type_annotation.rs b/src/calcit/type_annotation.rs index 6f15b8ca..bf83a834 100644 --- a/src/calcit/type_annotation.rs +++ b/src/calcit/type_annotation.rs @@ -22,7 +22,7 @@ use std::sync::{LazyLock, OnceLock}; // to avoid a circular dependency: type_annotation → program → snapshot → calcit. // --------------------------------------------------------------------------- type LookupFn = fn(&str, &str) -> Option; -static LOOKUP_EVALED_DEF: OnceLock = OnceLock::new(); +static LOOKUP_RUNTIME_READY_DEF: OnceLock = OnceLock::new(); static LOOKUP_DEF_CODE: OnceLock = OnceLock::new(); thread_local! { static TYPE_ANNOTATION_WARNING_CONTEXT: RefCell>> = const { RefCell::new(vec![]) }; @@ -31,8 +31,8 @@ thread_local! { /// Register program-level lookup functions. Must be called once at startup /// (e.g. from `program::extract_program_data`) before any type-annotation /// resolution that needs import-chain traversal. -pub fn register_program_lookups(evaled_lookup: LookupFn, code_lookup: LookupFn) { - let _ = LOOKUP_EVALED_DEF.set(evaled_lookup); +pub fn register_program_lookups(runtime_ready_lookup: LookupFn, code_lookup: LookupFn) { + let _ = LOOKUP_RUNTIME_READY_DEF.set(runtime_ready_lookup); let _ = LOOKUP_DEF_CODE.set(code_lookup); } @@ -70,8 +70,8 @@ fn emit_legacy_fn_type_syntax_warning(schema_hint: &str, form: &Calcit) { } } -fn lookup_evaled_def(ns: &str, def: &str) -> Option { - LOOKUP_EVALED_DEF.get().and_then(|f| f(ns, def)) +fn lookup_runtime_ready_registered(ns: &str, def: &str) -> Option { + LOOKUP_RUNTIME_READY_DEF.get().and_then(|f| f(ns, def)) } fn lookup_def_code_registered(ns: &str, def: &str) -> Option { @@ -1647,7 +1647,7 @@ impl CalcitTypeAnnotation { return None; } - let resolved = lookup_evaled_def(import.ns.as_ref(), import.def.as_ref()) + let resolved = lookup_runtime_ready_registered(import.ns.as_ref(), import.def.as_ref()) .or_else(|| lookup_def_code_registered(import.ns.as_ref(), import.def.as_ref())) .map(|value| CalcitTypeAnnotation::from_calcit(&value)); @@ -2199,7 +2199,7 @@ fn resolve_calcit_value(form: &Calcit) -> Option { return None; } - let resolved = lookup_evaled_def(import.ns.as_ref(), import.def.as_ref()) + let resolved = lookup_runtime_ready_registered(import.ns.as_ref(), import.def.as_ref()) .map(|value| resolve_type_def_from_code(&value).unwrap_or(value)) .or_else(|| { lookup_def_code_registered(import.ns.as_ref(), import.def.as_ref()) @@ -2215,7 +2215,7 @@ fn resolve_calcit_value(form: &Calcit) -> Option { resolved } - Calcit::Symbol { sym, info, .. } => lookup_evaled_def(info.at_ns.as_ref(), sym) + Calcit::Symbol { sym, info, .. } => lookup_runtime_ready_registered(info.at_ns.as_ref(), sym) .map(|value| resolve_type_def_from_code(&value).unwrap_or(value)) .or_else(|| { lookup_def_code_registered(info.at_ns.as_ref(), sym).map(|value| resolve_type_def_from_code(&value).unwrap_or(value)) diff --git a/src/call_tree.rs b/src/call_tree.rs index e5485c89..f259f935 100644 --- a/src/call_tree.rs +++ b/src/call_tree.rs @@ -367,10 +367,6 @@ impl CallTreeAnalyzer { crate::calcit::CalcitThunk::Code { code, .. } => { Self::extract_calls_recursive(code, current_ns, calls); } - crate::calcit::CalcitThunk::Evaled { code, value } => { - Self::extract_calls_recursive(code, current_ns, calls); - Self::extract_calls_recursive(value, current_ns, calls); - } }, Calcit::Tuple(tuple) => { for item in &tuple.extra { diff --git a/src/codegen/emit_js.rs b/src/codegen/emit_js.rs index 90a2c4db..83a2653c 100644 --- a/src/codegen/emit_js.rs +++ b/src/codegen/emit_js.rs @@ -30,7 +30,7 @@ use crate::codegen::skip_arity_check; use crate::program; use crate::util::string::{has_ns_part, matches_js_var, wrap_js_str}; use args::{gen_args_code, gen_call_args_with_temps}; -use deps::{contains_symbol, sort_by_deps}; +use deps::{contains_symbol, sort_compiled_defs_by_deps}; use helpers::{cirru_to_js, is_js_unavailable_procs, write_file_if_changed}; use paths::{to_js_import_name, to_mjs_filename}; use runtime::{get_proc_prefix, is_cirru_string}; @@ -1212,6 +1212,20 @@ fn hinted_async(xs: &CalcitList) -> bool { xs.iter().skip(1).any(schema_marks_async) } +fn extract_preprocessed_fn_parts(code: &Calcit) -> Result<(CalcitFnArgs, Vec), String> { + let Calcit::List(items) = code else { + return Err(format!("expected preprocessed defn list, got: {code}")); + }; + + match (items.first(), items.get(1), items.get(2)) { + (Some(Calcit::Syntax(CalcitSyntax::Defn, _)), Some(Calcit::Symbol { .. }), Some(Calcit::List(args))) => { + let raw_args = get_raw_args_fn(args)?; + Ok((raw_args, items.drop_left().drop_left().drop_left().to_vec())) + } + _ => Err(format!("expected preprocessed defn form, got: {code}")), + } +} + pub fn emit_js(entry_ns: &str, emit_path: &str) -> Result<(), String> { let code_emit_path = Path::new(emit_path); if !code_emit_path.exists() { @@ -1220,7 +1234,7 @@ pub fn emit_js(entry_ns: &str, emit_path: &str) -> Result<(), String> { let mut unchanged_ns: HashSet> = HashSet::new(); - let program = program::clone_evaled_program(); + let program = program::clone_compiled_program_snapshot()?; for (ns, file) in program.iter() { // println!("\nstart handling: {}\n", ns); // side-effects, reset tracking state @@ -1259,7 +1273,7 @@ pub fn emit_js(entry_ns: &str, emit_path: &str) -> Result<(), String> { let mut direct_code = String::from(""); // dirty code to run directly let mut tags_code = String::new(); - let mut import_code = if &*ns == "calcit.core" { + let mut import_code = if &**ns == "calcit.core" { snippets::tmpl_import_procs(wrap_js_str("@calcit/procs")) } else { format!("\nimport * as $clt from {core_lib};") @@ -1272,11 +1286,11 @@ pub fn emit_js(entry_ns: &str, emit_path: &str) -> Result<(), String> { def_names.insert(def.to_owned()); } - let deps_in_order = sort_by_deps(&file.to_hashmap()); + let deps_in_order = sort_compiled_defs_by_deps(file); // println!("deps order: {:?}", deps_in_order); for def in deps_in_order { - if &*ns == calcit::CORE_NS { + if &**ns == calcit::CORE_NS { // some defs from core can be replaced by calcit.procs if is_js_unavailable_procs(&def) { continue; @@ -1287,58 +1301,54 @@ pub fn emit_js(entry_ns: &str, emit_path: &str) -> Result<(), String> { } } - let f = file.lookup(&def).unwrap().0.to_owned(); + let compiled_def = file.get(&def).expect("compiled def for codegen"); - match &f { + match &compiled_def.kind { // probably not work here - Calcit::Proc(..) => { + program::CompiledDefKind::Proc => { writeln!(defs_code, "\nvar {} = $procs.{};", escape_var(&def), escape_var(&def)).expect("write"); } - Calcit::Fn { info, .. } => { - gen_stack::push_call_stack(&info.def_ns, &info.name, StackKind::Codegen, f.to_owned(), &[]); + program::CompiledDefKind::Fn => { + let (raw_args, raw_body) = extract_preprocessed_fn_parts(&compiled_def.preprocessed_code)?; + gen_stack::push_call_stack(&ns, &def, StackKind::Codegen, compiled_def.preprocessed_code.to_owned(), &[]); let passed_defs = PassedDefs { ns: &ns, local_defs: &def_names, file_imports: &file_imports, }; - defs_code.push_str(&gen_js_func( - &def, - &info.args, - &info.body, - &passed_defs, - true, - &collected_tags, - &ns, - )?); + defs_code.push_str(&gen_js_func(&def, &raw_args, &raw_body, &passed_defs, true, &collected_tags, &ns)?); gen_stack::pop_call_stack(); } - Calcit::Thunk(thunk) => { + program::CompiledDefKind::LazyValue => { // TODO need topological sorting for accuracy // values are called directly, put them after fns - gen_stack::push_call_stack(&ns, &def, StackKind::Codegen, thunk.get_code().to_owned(), &[]); + gen_stack::push_call_stack(&ns, &def, StackKind::Codegen, compiled_def.codegen_form.to_owned(), &[]); writeln!( vals_code, "\nexport var {} = {};", escape_var(&def), - to_js_code(thunk.get_code(), &ns, &def_names, &file_imports, &collected_tags, None)? + to_js_code(&compiled_def.codegen_form, &ns, &def_names, &file_imports, &collected_tags, None)? ) .expect("write"); gen_stack::pop_call_stack() } // macro are not traced in codegen since already expanded - Calcit::Macro { .. } => {} - Calcit::Syntax(_, _) => { + program::CompiledDefKind::Macro => {} + program::CompiledDefKind::Syntax => { // should he handled inside compiler } - Calcit::Bool(_) | Calcit::Number(_) => { - eprintln!("[Warn] expected thunk, got macro. skipped `{ns}/{def} {f}`") + program::CompiledDefKind::Value if matches!(&compiled_def.codegen_form, Calcit::Bool(_) | Calcit::Number(_)) => { + eprintln!( + "[Warn] expected thunk, got macro. skipped `{ns}/{def} {}`", + compiled_def.codegen_form + ) } - _ => { - eprintln!("[Warn] expected thunk for js, skipped `{ns}/{def} {f}`") + program::CompiledDefKind::Value => { + eprintln!("[Warn] expected thunk for js, skipped `{ns}/{def} {}`", compiled_def.codegen_form) } } } - if &*ns == calcit::CORE_NS { + if &**ns == calcit::CORE_NS { // add at end of file to register builtin classes direct_code.push_str(&snippets::tmpl_classes_registering()) } @@ -1387,7 +1397,7 @@ pub fn emit_js(entry_ns: &str, emit_path: &str) -> Result<(), String> { } } - let tag_prefix = if &*ns == "calcit.core" { "" } else { "$clt." }; + let tag_prefix = if &**ns == "calcit.core" { "" } else { "$clt." }; let mut tag_arr = String::from("["); let mut ordered_tags: Vec = vec![]; for k in collected_tags.borrow().iter() { diff --git a/src/codegen/emit_js/deps.rs b/src/codegen/emit_js/deps.rs index ee5703c4..5ff80d01 100644 --- a/src/codegen/emit_js/deps.rs +++ b/src/codegen/emit_js/deps.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use crate::calcit::{Calcit, CalcitImport, CalcitLocal}; +use crate::program::{CompiledFileData, DefId}; pub(super) fn contains_symbol(xs: &Calcit, symbol: &str) -> bool { match xs { @@ -15,6 +16,7 @@ pub(super) fn contains_symbol(xs: &Calcit, symbol: &str) -> bool { } } +#[cfg(test)] pub(super) fn sort_by_deps(deps: &HashMap, Calcit>) -> Vec> { let mut deps_graph: HashMap, HashSet>> = HashMap::new(); let mut def_names: Vec> = Vec::with_capacity(deps.len()); @@ -33,12 +35,41 @@ pub(super) fn sort_by_deps(deps: &HashMap, Calcit>) -> Vec> { deps_graph.insert(name.to_owned(), deps_info); } + sort_names_by_graph(def_names, &deps_graph) +} + +pub(super) fn sort_compiled_defs_by_deps(file: &CompiledFileData) -> Vec> { + let mut deps_graph: HashMap, HashSet>> = HashMap::new(); + let mut def_names: Vec> = Vec::with_capacity(file.defs.len()); + let mut local_defs: HashMap> = HashMap::with_capacity(file.defs.len()); + + for (name, compiled) in &file.defs { + def_names.push(name.to_owned()); + local_defs.insert(compiled.def_id, name.to_owned()); + } + + for (name, compiled) in &file.defs { + let mut deps_info: HashSet> = HashSet::new(); + for dep_id in &compiled.deps { + if let Some(dep_name) = local_defs.get(dep_id) + && dep_name != name + { + deps_info.insert(dep_name.to_owned()); + } + } + deps_graph.insert(name.to_owned(), deps_info); + } + + sort_names_by_graph(def_names, &deps_graph) +} + +fn sort_names_by_graph(mut def_names: Vec>, deps_graph: &HashMap, HashSet>>) -> Vec> { def_names.sort(); let mut result: Vec> = Vec::with_capacity(def_names.len()); 'outer: for name in def_names { for (idx, existing) in result.iter().enumerate() { - if depends_on(existing, &name, &deps_graph, 3) { + if depends_on(existing, &name, deps_graph, 3) { result.insert(idx, name.to_owned()); continue 'outer; } diff --git a/src/codegen/gen_ir.rs b/src/codegen/gen_ir.rs index 94af2270..f7ff72bc 100644 --- a/src/codegen/gen_ir.rs +++ b/src/codegen/gen_ir.rs @@ -35,7 +35,7 @@ fn extract_import_type_info(ns: &str, def: &str) -> Edn { return Edn::Nil; } - let result = match program::lookup_evaled_def(ns, def) { + let result = match program::lookup_runtime_ready(ns, def) { Some(value) => { let annotation = CalcitTypeAnnotation::from_calcit(&value); match annotation { @@ -92,18 +92,18 @@ impl From for Edn { } pub fn emit_ir(init_fn: &str, reload_fn: &str, emit_path: &str) -> Result<(), String> { - let program_data = program::clone_evaled_program(); + let program_data = program::clone_compiled_program_snapshot()?; let mut files: HashMap, IrDataFile> = HashMap::new(); for (ns, file_info) in program_data.iter() { let mut defs: HashMap, Edn> = HashMap::new(); - for (def, code) in file_info.iter() { - defs.insert(def, dump_code(code)); + for (def, compiled) in &file_info.defs { + defs.insert(def.to_owned(), dump_code(&compiled.codegen_form)); } let file = IrDataFile { defs }; - files.insert(ns, file); + files.insert(Arc::clone(ns), file); } let data = IrData { diff --git a/src/lib.rs b/src/lib.rs index 67e262b6..af040959 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,7 +78,7 @@ pub fn run_program_with_docs(init_ns: Arc, init_def: Arc, params: &[Ca hint: None, }); } - match program::lookup_evaled_def(&init_ns, &init_def) { + match program::lookup_runtime_ready(&init_ns, &init_def).or_else(|| program::lookup_compiled_runtime_value(&init_ns, &init_def)) { None => CalcitErr::err_str(CalcitErrKind::Var, format!("entry not initialized: {init_ns}/{init_def}")), Some(entry) => match entry { Calcit::Fn { info, .. } => { diff --git a/src/program.rs b/src/program.rs index 05e61ba4..8fc14065 100644 --- a/src/program.rs +++ b/src/program.rs @@ -1,21 +1,114 @@ mod entry_book; -use std::collections::HashMap; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet, VecDeque}; use std::sync::Arc; use std::sync::LazyLock; use std::sync::RwLock; use cirru_parser::Cirru; -use crate::calcit::{self, Calcit, CalcitTypeAnnotation, DYNAMIC_TYPE}; +use crate::calcit::{self, Calcit, CalcitScope, CalcitThunk, CalcitThunkInfo, CalcitTypeAnnotation, DYNAMIC_TYPE}; +use crate::call_stack::CallStackList; use crate::data::cirru::code_to_calcit; +use crate::runner; use crate::snapshot; use crate::snapshot::Snapshot; use crate::util::string::extract_pkg_from_ns; pub use entry_book::EntryBook; -pub type ProgramEvaledData = EntryBook>; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RuntimeCell { + Cold, + Resolving, + Lazy { code: Arc, info: Arc }, + Ready(Calcit), + Errored(Arc), +} + +pub type ProgramRuntimeData = Vec; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct DefId(pub u32); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CompiledDefKind { + Proc, + Fn, + Macro, + Syntax, + LazyValue, + Value, +} + +impl CompiledDefKind { + fn from_runtime_value(value: &Calcit) -> Self { + match value { + Calcit::Proc(..) => Self::Proc, + Calcit::Fn { .. } => Self::Fn, + Calcit::Macro { .. } => Self::Macro, + Calcit::Syntax(..) => Self::Syntax, + Calcit::Thunk(..) => Self::LazyValue, + _ => Self::Value, + } + } + + fn from_preprocessed_code(code: &Calcit) -> Self { + match code { + Calcit::Proc(..) => Self::Proc, + Calcit::Syntax(..) => Self::Syntax, + Calcit::List(xs) => match xs.first() { + Some(Calcit::Syntax(calcit::CalcitSyntax::Defn, _)) => Self::Fn, + Some(Calcit::Symbol { sym, .. }) if sym.as_ref() == "defn" => Self::Fn, + Some(Calcit::Syntax(calcit::CalcitSyntax::Defmacro, _)) => Self::Macro, + Some(Calcit::Symbol { sym, .. }) if sym.as_ref() == "defmacro" => Self::Macro, + _ => Self::LazyValue, + }, + _ => Self::LazyValue, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompiledDef { + pub def_id: DefId, + pub version_id: u32, + pub kind: CompiledDefKind, + pub preprocessed_code: Calcit, + pub codegen_form: Calcit, + pub deps: Vec, + pub type_summary: Option>, + pub source_code: Option, + pub schema: Arc, + pub doc: Arc, + pub examples: Vec, +} + +pub type ProgramCompiledData = HashMap, CompiledFileData>; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompiledFileData { + pub defs: HashMap, CompiledDef>, +} + +impl CompiledFileData { + pub fn keys(&self) -> impl Iterator> { + self.defs.keys() + } + + pub fn get(&self, def: &str) -> Option<&CompiledDef> { + self.defs.get(def) + } +} + +pub type CompiledProgram = HashMap, CompiledFileData>; + +#[derive(Debug, Default)] +struct ProgramDefIdIndex { + next_id: u32, + by_ns: HashMap, HashMap, DefId>>, +} /// definition entry with code and documentation #[derive(Debug, Clone, PartialEq, Eq)] @@ -48,10 +141,420 @@ pub enum ImportRule { pub type ProgramCodeData = HashMap, ProgramFileData>; -/// data of program running -static PROGRAM_EVALED_DATA_STATE: LazyLock> = LazyLock::new(|| RwLock::new(EntryBook::default())); +/// runtime values keyed by stable DefId, used by normal runtime lookup paths +static PROGRAM_RUNTIME_DATA_STATE: LazyLock> = LazyLock::new(|| RwLock::new(vec![])); +/// preprocessed / compiled definitions for codegen and future runtime boundary split +static PROGRAM_COMPILED_DATA_STATE: LazyLock> = LazyLock::new(|| RwLock::new(HashMap::new())); /// raw code information before program running pub static PROGRAM_CODE_DATA: LazyLock> = LazyLock::new(|| RwLock::new(HashMap::new())); +static PROGRAM_DEF_ID_INDEX: LazyLock> = LazyLock::new(|| RwLock::new(ProgramDefIdIndex::default())); + +fn ensure_runtime_capacity(runtime: &mut ProgramRuntimeData, def_id: DefId) { + let idx = def_id.0 as usize; + if runtime.len() <= idx { + runtime.resize(idx + 1, RuntimeCell::Cold); + } +} + +fn register_program_def_id(ns: &str, def: &str) -> DefId { + let mut index = PROGRAM_DEF_ID_INDEX.write().expect("write program def id index"); + if let Some(def_id) = index.by_ns.get(ns).and_then(|defs| defs.get(def)) { + *def_id + } else { + let def_id = DefId(index.next_id); + index.next_id += 1; + index.by_ns.entry(Arc::from(ns)).or_default().insert(Arc::from(def), def_id); + def_id + } +} + +pub fn ensure_def_id(ns: &str, def: &str) -> DefId { + register_program_def_id(ns, def) +} + +pub fn lookup_def_id(ns: &str, def: &str) -> Option { + let index = PROGRAM_DEF_ID_INDEX.read().expect("read program def id index"); + index.by_ns.get(ns).and_then(|defs| defs.get(def)).copied() +} + +fn collect_ns_def_ids(ns: &str) -> Vec { + let index = PROGRAM_DEF_ID_INDEX.read().expect("read program def id index"); + index.by_ns.get(ns).map(|defs| defs.values().copied().collect()).unwrap_or_default() +} + +fn write_runtime_cell(def_id: DefId, cell: RuntimeCell) { + let mut runtime = PROGRAM_RUNTIME_DATA_STATE.write().expect("write runtime data"); + ensure_runtime_capacity(&mut runtime, def_id); + runtime[def_id.0 as usize] = cell; +} + +fn write_runtime_value(def_id: DefId, value: Calcit) { + write_runtime_cell(def_id, RuntimeCell::Ready(value)); +} + +fn write_runtime_lazy(def_id: DefId, code: Arc, info: Arc) { + write_runtime_cell(def_id, RuntimeCell::Lazy { code, info }); +} + +pub fn write_runtime_lazy_value(ns: &str, def: &str, code: Arc, info: Arc) { + let def_id = ensure_def_id(ns, def); + write_runtime_lazy(def_id, code, info); +} + +fn clear_runtime_value(def_id: DefId) { + let mut runtime = PROGRAM_RUNTIME_DATA_STATE.write().expect("write runtime data"); + if let Some(slot) = runtime.get_mut(def_id.0 as usize) { + *slot = RuntimeCell::Cold; + } +} + +pub fn mark_runtime_def_cold(ns: &str, def: &str) { + let def_id = ensure_def_id(ns, def); + clear_runtime_value(def_id); +} + +pub fn refresh_runtime_cell_from_preprocessed(ns: &str, def: &str, preprocessed_code: &Calcit) { + let def_id = ensure_def_id(ns, def); + match CompiledDefKind::from_preprocessed_code(preprocessed_code) { + CompiledDefKind::LazyValue => write_runtime_lazy( + def_id, + Arc::new(preprocessed_code.to_owned()), + Arc::new(CalcitThunkInfo { + ns: Arc::from(ns), + def: Arc::from(def), + }), + ), + _ => clear_runtime_value(def_id), + } +} + +pub fn seed_runtime_lazy_from_compiled(ns: &str, def: &str) -> bool { + let Some(compiled) = lookup_compiled_def(ns, def) else { + return false; + }; + + if compiled.kind != CompiledDefKind::LazyValue { + return false; + } + + match lookup_runtime_cell_by_id(compiled.def_id) { + Some(RuntimeCell::Lazy { .. } | RuntimeCell::Ready(_) | RuntimeCell::Resolving | RuntimeCell::Errored(_)) => false, + Some(RuntimeCell::Cold) | None => { + write_runtime_lazy( + compiled.def_id, + Arc::new(compiled.preprocessed_code), + Arc::new(CalcitThunkInfo { + ns: Arc::from(ns), + def: Arc::from(def), + }), + ); + true + } + } +} + +fn clear_runtime_ns(ns: &str) { + for def_id in collect_ns_def_ids(ns) { + clear_runtime_value(def_id); + } +} + +pub fn lookup_runtime_cell_by_id(def_id: DefId) -> Option { + let runtime = PROGRAM_RUNTIME_DATA_STATE.read().expect("read runtime data"); + runtime.get(def_id.0 as usize).cloned() +} + +pub fn lookup_runtime_cell(ns: &str, def: &str) -> Option { + lookup_def_id(ns, def).and_then(lookup_runtime_cell_by_id) +} + +pub fn lookup_runtime_ready_by_id(def_id: DefId) -> Option { + match lookup_runtime_cell_by_id(def_id)? { + RuntimeCell::Ready(value) => Some(value), + _ => None, + } +} + +pub fn lookup_runtime_ready(ns: &str, def: &str) -> Option { + lookup_def_id(ns, def).and_then(lookup_runtime_ready_by_id) +} + +pub fn mark_runtime_def_resolving(ns: &str, def: &str) { + let def_id = ensure_def_id(ns, def); + write_runtime_cell(def_id, RuntimeCell::Resolving); +} + +pub fn mark_runtime_def_errored(ns: &str, def: &str, message: Arc) { + let def_id = ensure_def_id(ns, def); + write_runtime_cell(def_id, RuntimeCell::Errored(message)); +} + +fn collect_compiled_dep_keys(code: &Calcit, deps: &mut Vec<(Arc, Arc)>) { + match code { + Calcit::Import(import) => deps.push((import.ns.to_owned(), import.def.to_owned())), + Calcit::Thunk(thunk) => collect_compiled_dep_keys(thunk.get_code(), deps), + Calcit::Fn { info, .. } => { + for item in info.body.iter() { + collect_compiled_dep_keys(item, deps); + } + } + Calcit::List(xs) => { + for item in xs.iter() { + collect_compiled_dep_keys(item, deps); + } + } + _ => {} + } +} + +pub fn collect_compiled_deps(code: &Calcit) -> Vec { + let mut keys: Vec<(Arc, Arc)> = vec![]; + collect_compiled_dep_keys(code, &mut keys); + + let mut deps: Vec = vec![]; + for (ns, def) in keys { + if let Some(def_id) = lookup_def_id(&ns, &def) + && !deps.contains(&def_id) + { + deps.push(def_id); + } + } + deps.sort(); + deps +} + +fn build_compiled_def( + ns: &str, + def: &str, + version_id: u32, + preprocessed_code: Calcit, + codegen_form: Calcit, + deps: Vec, + type_summary: Option>, + source_code: Option, + schema: Arc, + doc: Arc, + examples: Vec, +) -> CompiledDef { + let kind = CompiledDefKind::from_preprocessed_code(&preprocessed_code); + + CompiledDef { + def_id: ensure_def_id(ns, def), + version_id, + kind, + preprocessed_code, + codegen_form, + deps, + type_summary, + source_code, + schema, + doc, + examples, + } +} + +fn build_snapshot_fallback_compiled_def( + ns: &str, + def: &str, + runtime_value: Calcit, + source_entry: Option<&ProgramDefEntry>, +) -> CompiledDef { + let codegen_form = match &runtime_value { + Calcit::Thunk(thunk) => thunk.get_code().to_owned(), + _ => runtime_value.to_owned(), + }; + let kind = CompiledDefKind::from_runtime_value(&runtime_value); + let deps = collect_compiled_deps(&codegen_form); + + CompiledDef { + def_id: ensure_def_id(ns, def), + version_id: 0, + kind, + preprocessed_code: codegen_form.to_owned(), + codegen_form, + deps, + type_summary: source_entry + .and_then(|entry| calcit::CalcitTypeAnnotation::summarize_code(&entry.code)) + .map(Arc::from), + source_code: source_entry.map(|entry| entry.code.to_owned()), + schema: source_entry + .map(|entry| entry.schema.clone()) + .unwrap_or_else(|| DYNAMIC_TYPE.clone()), + doc: source_entry.map(|entry| entry.doc.clone()).unwrap_or_else(|| Arc::from("")), + examples: source_entry.map(|entry| entry.examples.clone()).unwrap_or_default(), + } +} + +fn ensure_source_backed_compiled_def_for_snapshot(ns: &str, def: &str) -> Option { + if let Some(compiled) = lookup_compiled_def(ns, def) { + return Some(compiled); + } + + if !has_def_code(ns, def) { + return None; + } + + let warnings: RefCell> = RefCell::new(vec![]); + if runner::preprocess::preprocess_ns_def(ns, def, &warnings, &CallStackList::default()).is_err() { + return None; + } + + let warnings = warnings.borrow(); + if !warnings.is_empty() { + return None; + } + + lookup_compiled_def(ns, def) +} + +pub fn write_compiled_def(ns: &str, def: &str, compiled: CompiledDef) { + let mut program = PROGRAM_COMPILED_DATA_STATE.write().expect("write compiled program data"); + let file = program + .entry(Arc::from(ns)) + .or_insert_with(|| CompiledFileData { defs: HashMap::new() }); + file.defs.insert(Arc::from(def), compiled); +} + +pub fn store_compiled_output( + ns: &str, + def: &str, + version_id: u32, + preprocessed_code: Calcit, + codegen_form: Calcit, + deps: Vec, + type_summary: Option>, + source_code: Option, + schema: Arc, + doc: Arc, + examples: Vec, +) { + let compiled = build_compiled_def( + ns, + def, + version_id, + preprocessed_code, + codegen_form, + deps, + type_summary, + source_code, + schema, + doc, + examples, + ); + write_compiled_def(ns, def, compiled); +} + +pub fn lookup_compiled_def(ns: &str, def: &str) -> Option { + let program = PROGRAM_COMPILED_DATA_STATE.read().expect("read compiled program data"); + let file = program.get(ns)?; + file.defs.get(def).cloned() +} + +pub fn lookup_compiled_runtime_value(ns: &str, def: &str) -> Option { + let compiled = lookup_compiled_def(ns, def)?; + + match compiled.kind { + CompiledDefKind::Fn | CompiledDefKind::Macro | CompiledDefKind::Proc | CompiledDefKind::Syntax => { + runner::evaluate_expr(&compiled.preprocessed_code, &CalcitScope::default(), ns, &CallStackList::default()).ok() + } + CompiledDefKind::LazyValue => None, + _ => None, + } +} + +fn remove_compiled_def(ns: &str, def: &str) { + let mut program = PROGRAM_COMPILED_DATA_STATE.write().expect("write compiled program data"); + if let Some(file) = program.get_mut(ns) { + file.defs.remove(def); + if file.defs.is_empty() { + program.remove(ns); + } + } +} + +fn remove_compiled_ns(ns: &str) { + let mut program = PROGRAM_COMPILED_DATA_STATE.write().expect("write compiled program data"); + program.remove(ns); +} + +fn collect_transitive_dependent_def_ids(compiled: &ProgramCompiledData, seed_ids: &HashSet) -> HashSet { + if seed_ids.is_empty() { + return HashSet::new(); + } + + let mut reverse_deps: HashMap> = HashMap::new(); + for file in compiled.values() { + for compiled_def in file.defs.values() { + for dep in &compiled_def.deps { + reverse_deps.entry(*dep).or_default().push(compiled_def.def_id); + } + } + } + + let mut affected = seed_ids.clone(); + let mut pending: VecDeque = seed_ids.iter().copied().collect(); + + while let Some(def_id) = pending.pop_front() { + if let Some(dependents) = reverse_deps.get(&def_id) { + for dependent_id in dependents { + if affected.insert(*dependent_id) { + pending.push_back(*dependent_id); + } + } + } + } + + affected +} + +fn collect_changed_seed_def_ids(changes: &snapshot::ChangesDict, index: &ProgramDefIdIndex) -> HashSet { + let mut seeds: HashSet = HashSet::new(); + + for ns in &changes.removed { + if let Some(defs) = index.by_ns.get(ns) { + seeds.extend(defs.values().copied()); + } + } + + for (ns, info) in &changes.changed { + if info.ns.is_some() + && let Some(defs) = index.by_ns.get(ns) + { + seeds.extend(defs.values().copied()); + } + + for def in info.removed_defs.iter() { + if let Some(def_id) = index.by_ns.get(ns).and_then(|defs| defs.get(def.as_str())) { + seeds.insert(*def_id); + } + } + + for def in info.changed_defs.keys() { + if let Some(def_id) = index.by_ns.get(ns).and_then(|defs| defs.get(def.as_str())) { + seeds.insert(*def_id); + } + } + } + + seeds +} + +fn collect_reload_affected_def_ids( + changes: &snapshot::ChangesDict, + compiled: &ProgramCompiledData, + index: &ProgramDefIdIndex, +) -> HashSet { + let seed_ids = collect_changed_seed_def_ids(changes, index); + collect_transitive_dependent_def_ids(compiled, &seed_ids) +} + +fn register_program_def_ids(program_data: &ProgramCodeData) { + for (ns, file) in program_data { + for def in file.defs.keys() { + let _ = register_program_def_id(ns, def); + } + } +} fn extract_import_rule(nodes: &Cirru) -> Result, String> { match nodes { @@ -155,7 +658,7 @@ fn extract_file_data(file: &snapshot::FileInSnapShot, ns: Arc) -> Result Result { // Register the program lookup functions in type_annotation so it can resolve // imported type definitions without a circular module dependency. - calcit::register_program_lookups(lookup_evaled_def, lookup_def_code); + calcit::register_program_lookups(lookup_runtime_ready, lookup_def_code); let mut xs: ProgramCodeData = HashMap::with_capacity(s.files.len()); @@ -164,6 +667,8 @@ pub fn extract_program_data(s: &Snapshot) -> Result { xs.insert(ns.to_owned().into(), file_info); } + register_program_def_ids(&xs); + Ok(xs) } @@ -238,15 +743,6 @@ pub fn lookup_ns_target_in_import(ns: &str, alias: &str) -> Option> { } } -/// try if we can load coord from evaled data -pub fn tip_coord(ns: &str, def: &str) -> Option<(u16, u16)> { - let program = { PROGRAM_EVALED_DATA_STATE.read().expect("read program code") }; - match program.lookup(ns) { - Some(file) => file.0.lookup(def).map(|(_, v)| (file.1, v)), - None => None, - } -} - // imported via :default pub fn lookup_default_target_in_import(at_ns: &str, alias: &str) -> Option> { let program = { PROGRAM_CODE_DATA.read().expect("read program code") }; @@ -259,46 +755,54 @@ pub fn lookup_default_target_in_import(at_ns: &str, alias: &str) -> Option Option { - let s2 = PROGRAM_EVALED_DATA_STATE.read().expect("read program data"); - s2.lookup(ns)?.0.lookup(def).map(|p| p.0).cloned() -} +// Dirty mutating global states +pub fn write_runtime_ready(ns: &str, def: &str, value: Calcit) -> Result<(), String> { + let def_id = ensure_def_id(ns, def); -pub fn load_by_index(ns_idx: u16, ns: &str, def_idx: u16, def: &str) -> Option { - let s2 = PROGRAM_EVALED_DATA_STATE.read().expect("read program data"); - let (file, ns_cache) = s2.load(ns_idx).ok()?; - if ns == ns_cache { - let (value, def_cache) = file.load(def_idx).ok()?; - if def == def_cache { Some(value.to_owned()) } else { None } - } else { - None + match value { + Calcit::Thunk(CalcitThunk::Code { code, info }) => write_runtime_lazy(def_id, code, info), + other => write_runtime_value(def_id, other), } + + Ok(()) } -// Dirty mutating global states -pub fn write_evaled_def(ns: &str, def: &str, value: Calcit) -> Result<(), String> { - // println!("writing {} {}", ns, def); - let mut program = PROGRAM_EVALED_DATA_STATE.write().expect("read program data"); +pub fn clone_compiled_program_snapshot() -> Result { + let mut compiled: CompiledProgram = PROGRAM_COMPILED_DATA_STATE.read().expect("read compiled program data").to_owned(); + let program_code = PROGRAM_CODE_DATA.read().expect("read program code").to_owned(); + let program_def_ids = PROGRAM_DEF_ID_INDEX.read().expect("read program def id index").by_ns.clone(); + let runtime = PROGRAM_RUNTIME_DATA_STATE.read().expect("read runtime data").to_owned(); - match program.lookup_mut(ns) { - Some(file_entry) => { - file_entry.0.insert(Arc::from(def), value); - } - None => { - let mut file_entry = EntryBook::default(); - file_entry.insert(Arc::from(def), value); - program.insert(Arc::from(ns), file_entry); + for (ns, defs) in &program_def_ids { + let source_file = program_code.get(ns.as_ref()); + let compiled_file = compiled + .entry(ns.to_owned()) + .or_insert_with(|| CompiledFileData { defs: HashMap::new() }); + + for (def, def_id) in defs { + if compiled_file.defs.contains_key(def.as_ref()) { + continue; + } + let source_entry = source_file.and_then(|data| data.defs.get(def.as_ref())); + + if source_entry.is_some() + && let Some(compiled_def) = ensure_source_backed_compiled_def_for_snapshot(ns, def) + { + compiled_file.defs.insert(def.to_owned(), compiled_def); + continue; + } + + let Some(RuntimeCell::Ready(runtime_value)) = runtime.get(def_id.0 as usize).cloned() else { + continue; + }; + compiled_file.defs.insert( + def.to_owned(), + build_snapshot_fallback_compiled_def(ns, def, runtime_value, source_entry), + ); } } - Ok(()) -} - -// take a snapshot for codegen -pub fn clone_evaled_program() -> ProgramEvaledData { - let program = &PROGRAM_EVALED_DATA_STATE.read().expect("read program data"); - (**program).to_owned() + Ok(compiled) } pub fn apply_code_changes(changes: &snapshot::ChangesDict) -> Result<(), String> { @@ -306,10 +810,17 @@ pub fn apply_code_changes(changes: &snapshot::ChangesDict) -> Result<(), String> let coord0 = vec![]; for (ns, file) in &changes.added { - program_code.insert(ns.to_owned(), extract_file_data(file, ns.to_owned())?); + let file_info = extract_file_data(file, ns.to_owned())?; + for def in file_info.defs.keys() { + let _ = register_program_def_id(ns, def); + } + program_code.insert(ns.to_owned(), file_info); + remove_compiled_ns(ns); } for ns in &changes.removed { + clear_runtime_ns(ns); program_code.remove(ns); + remove_compiled_ns(ns); } for (ns, info) in &changes.changed { // println!("handling ns: {:?} {}", ns, program_code.contains_key(ns)); @@ -318,6 +829,9 @@ pub fn apply_code_changes(changes: &snapshot::ChangesDict) -> Result<(), String> file.import_map = extract_import_map(v, ns)?; } for (def, code) in &info.added_defs { + let _ = register_program_def_id(ns, def); + clear_runtime_value(ensure_def_id(ns, def)); + remove_compiled_def(ns, def); let calcit_code = code_to_calcit(code, ns, def, coord0.to_owned())?; let entry = ProgramDefEntry { code: calcit_code, @@ -328,9 +842,16 @@ pub fn apply_code_changes(changes: &snapshot::ChangesDict) -> Result<(), String> file.defs.insert(def.to_owned().into(), entry); } for def in &info.removed_defs { + if let Some(def_id) = lookup_def_id(ns, def) { + clear_runtime_value(def_id); + } file.defs.remove(def.as_str()); + remove_compiled_def(ns, def); } for (def, code) in &info.changed_defs { + let def_id = register_program_def_id(ns, def); + clear_runtime_value(def_id); + remove_compiled_def(ns, def); let calcit_code = code_to_calcit(code, ns, def, coord0.to_owned())?; let (schema, doc, examples) = match file.defs.get(def.as_str()) { Some(existing) => (existing.schema.clone(), existing.doc.clone(), existing.examples.clone()), @@ -351,27 +872,225 @@ pub fn apply_code_changes(changes: &snapshot::ChangesDict) -> Result<(), String> Ok(()) } -/// clear evaled data after reloading -pub fn clear_all_program_evaled_defs(init_ns: Arc, reload_ns: Arc, reload_libs: bool) -> Result<(), String> { - let mut program = PROGRAM_EVALED_DATA_STATE.write().expect("open program data"); +/// clear runtime and compiled caches after reloading +pub fn clear_runtime_caches_for_reload(init_ns: Arc, reload_ns: Arc, reload_libs: bool) -> Result<(), String> { + let mut runtime = PROGRAM_RUNTIME_DATA_STATE.write().expect("open runtime data"); + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("open compiled program data"); if reload_libs { - (*program).clear(); + runtime.clear(); + compiled.clear(); } else { // reduce changes of libs. could be dirty in some cases let init_pkg = extract_pkg_from_ns(init_ns.to_owned()).ok_or_else(|| format!("failed to extract pkg from: {init_ns}"))?; let reload_pkg = extract_pkg_from_ns(reload_ns.to_owned()).ok_or_else(|| format!("failed to extract pkg from: {reload_ns}"))?; + let ns_keys: Vec> = { + let index = PROGRAM_DEF_ID_INDEX.read().expect("read program def id index"); + index.by_ns.keys().cloned().collect() + }; + let mut to_remove: Vec> = vec![]; - let xs = program.keys(); - for k in xs { - if k == &init_pkg || k == &reload_pkg || k.starts_with(&format!("{init_pkg}.")) || k.starts_with(&format!("{reload_pkg}.")) { + for k in ns_keys { + if k == init_pkg || k == reload_pkg || k.starts_with(&format!("{init_pkg}.")) || k.starts_with(&format!("{reload_pkg}.")) { to_remove.push(k.to_owned()); } else { continue; } } for k in to_remove { - (*program).remove(&k); + for def_id in collect_ns_def_ids(&k) { + if let Some(slot) = runtime.get_mut(def_id.0 as usize) { + *slot = RuntimeCell::Cold; + } + } + compiled.remove(&k); + } + } + Ok(()) +} + +pub fn clear_runtime_caches_for_changes(changes: &snapshot::ChangesDict, reload_libs: bool) -> Result<(), String> { + if reload_libs { + let mut runtime = PROGRAM_RUNTIME_DATA_STATE.write().expect("open runtime data"); + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("open compiled program data"); + runtime.clear(); + compiled.clear(); + return Ok(()); + } + + let affected_def_ids = { + let compiled = PROGRAM_COMPILED_DATA_STATE.read().expect("read compiled program data"); + let index = PROGRAM_DEF_ID_INDEX.read().expect("read program def id index"); + collect_reload_affected_def_ids(changes, &compiled, &index) + }; + + if affected_def_ids.is_empty() { + return Ok(()); + } + + let mut runtime = PROGRAM_RUNTIME_DATA_STATE.write().expect("open runtime data"); + for def_id in &affected_def_ids { + if let Some(slot) = runtime.get_mut(def_id.0 as usize) { + *slot = RuntimeCell::Cold; } } + + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("open compiled program data"); + for file in compiled.values_mut() { + file.defs.retain(|_, compiled_def| !affected_def_ids.contains(&compiled_def.def_id)); + } + compiled.retain(|_, file| !file.defs.is_empty()); + Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::calcit::{CalcitImport, ImportInfo}; + + fn compiled_def_for_test(def_id: DefId, deps: Vec) -> CompiledDef { + CompiledDef { + def_id, + version_id: 0, + kind: CompiledDefKind::Value, + preprocessed_code: Calcit::Nil, + codegen_form: Calcit::Nil, + deps, + type_summary: None, + source_code: None, + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + } + } + + #[test] + fn reload_invalidation_collects_transitive_dependents() { + let mut compiled: ProgramCompiledData = HashMap::new(); + compiled.insert( + Arc::from("app.main"), + CompiledFileData { + defs: HashMap::from([ + (Arc::from("a"), compiled_def_for_test(DefId(1), vec![])), + (Arc::from("b"), compiled_def_for_test(DefId(2), vec![DefId(1)])), + (Arc::from("c"), compiled_def_for_test(DefId(3), vec![DefId(2)])), + (Arc::from("d"), compiled_def_for_test(DefId(4), vec![])), + ]), + }, + ); + + let mut index = ProgramDefIdIndex::default(); + index.by_ns.insert( + Arc::from("app.main"), + HashMap::from([ + (Arc::from("a"), DefId(1)), + (Arc::from("b"), DefId(2)), + (Arc::from("c"), DefId(3)), + (Arc::from("d"), DefId(4)), + ]), + ); + + let mut changes = snapshot::ChangesDict::default(); + changes.changed.insert( + Arc::from("app.main"), + snapshot::FileChangeInfo { + ns: None, + added_defs: HashMap::new(), + removed_defs: HashSet::new(), + changed_defs: HashMap::from([(String::from("a"), Cirru::Leaf(Arc::from("1")))]), + }, + ); + + let affected = collect_reload_affected_def_ids(&changes, &compiled, &index); + assert_eq!(affected, HashSet::from([DefId(1), DefId(2), DefId(3)])); + } + + #[test] + fn reload_invalidation_expands_namespace_header_changes() { + let compiled: ProgramCompiledData = HashMap::from([ + ( + Arc::from("app.main"), + CompiledFileData { + defs: HashMap::from([ + (Arc::from("a"), compiled_def_for_test(DefId(1), vec![])), + (Arc::from("b"), compiled_def_for_test(DefId(2), vec![])), + ]), + }, + ), + ( + Arc::from("app.consumer"), + CompiledFileData { + defs: HashMap::from([(Arc::from("use-main"), compiled_def_for_test(DefId(3), vec![DefId(2)]))]), + }, + ), + ]); + + let mut index = ProgramDefIdIndex::default(); + index.by_ns.insert( + Arc::from("app.main"), + HashMap::from([(Arc::from("a"), DefId(1)), (Arc::from("b"), DefId(2))]), + ); + index + .by_ns + .insert(Arc::from("app.consumer"), HashMap::from([(Arc::from("use-main"), DefId(3))])); + + let mut changes = snapshot::ChangesDict::default(); + changes.changed.insert( + Arc::from("app.main"), + snapshot::FileChangeInfo { + ns: Some(Cirru::Leaf(Arc::from("ns"))), + added_defs: HashMap::new(), + removed_defs: HashSet::new(), + changed_defs: HashMap::new(), + }, + ); + + let affected = collect_reload_affected_def_ids(&changes, &compiled, &index); + assert_eq!(affected, HashSet::from([DefId(1), DefId(2), DefId(3)])); + } + + #[test] + fn snapshot_fallback_preserves_dependency_metadata() { + let dep_id = register_program_def_id("dep.ns", "value"); + let _ = register_program_def_id("app.main", "dep"); + + let runtime_value = Calcit::from(vec![Calcit::Import(CalcitImport { + ns: Arc::from("dep.ns"), + def: Arc::from("value"), + info: Arc::new(ImportInfo::SameFile { at_def: Arc::from("dep") }), + def_id: Some(dep_id.0), + })]); + + let fallback = build_snapshot_fallback_compiled_def("app.main", "dep", runtime_value, None); + assert_eq!(fallback.deps, vec![dep_id]); + } + + #[test] + fn write_runtime_ready_normalizes_thunk_into_lazy_cell() { + let thunk_ns = "tests.runtime"; + let thunk_def = "lazy-demo"; + let thunk_code = Arc::new(Calcit::Nil); + let thunk_info = Arc::new(CalcitThunkInfo { + ns: Arc::from(thunk_ns), + def: Arc::from(thunk_def), + }); + + write_runtime_ready( + thunk_ns, + thunk_def, + Calcit::Thunk(CalcitThunk::Code { + code: thunk_code.clone(), + info: thunk_info.clone(), + }), + ) + .expect("write thunk into runtime"); + + match lookup_runtime_cell(thunk_ns, thunk_def) { + Some(RuntimeCell::Lazy { code, info }) => { + assert_eq!(code, thunk_code); + assert_eq!(info, thunk_info); + } + other => panic!("expected lazy runtime cell, got {other:?}"), + } + } +} diff --git a/src/runner.rs b/src/runner.rs index b5253cbe..a91dc05b 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -7,13 +7,33 @@ use std::vec; use crate::builtins; use crate::calcit::{ CORE_NS, Calcit, CalcitArgLabel, CalcitErr, CalcitErrKind, CalcitFn, CalcitFnArgs, CalcitImport, CalcitList, CalcitLocal, CalcitProc, - CalcitScope, CalcitSyntax, MethodKind, NodeLocation, + CalcitScope, CalcitSyntax, CalcitThunk, MethodKind, NodeLocation, }; use crate::call_stack::{CallStackList, StackKind, using_stack}; use crate::data::cirru; use crate::program; use crate::util::string::has_ns_part; +fn build_runtime_cell_error(ns: &str, def: &str, call_stack: &CallStackList, cell: program::RuntimeCell) -> CalcitErr { + match cell { + program::RuntimeCell::Resolving => CalcitErr::use_msg_stack( + CalcitErrKind::Unexpected, + format!("definition is still resolving: {ns}/{def}"), + call_stack, + ), + program::RuntimeCell::Errored(message) => CalcitErr::use_msg_stack( + CalcitErrKind::Unexpected, + format!("definition is in errored state: {ns}/{def}\n{message}"), + call_stack, + ), + program::RuntimeCell::Cold | program::RuntimeCell::Lazy { .. } | program::RuntimeCell::Ready(_) => CalcitErr::use_msg_stack( + CalcitErrKind::Unexpected, + format!("unexpected runtime state for {ns}/{def}"), + call_stack, + ), + } +} + fn format_fn_arg_labels(args: &CalcitFnArgs) -> String { match args { CalcitFnArgs::Args(xs) => xs @@ -85,7 +105,7 @@ pub fn evaluate_expr(expr: &Calcit, scope: &CalcitScope, file_ns: &str, call_sta evaluate_symbol(sym, scope, &info.at_ns, &info.at_def, location, call_stack) } Local(CalcitLocal { idx, .. }) => evaluate_symbol_from_scope(*idx, scope), - Import(CalcitImport { ns, def, coord, .. }) => evaluate_symbol_from_program(def, ns, *coord, call_stack), + Import(CalcitImport { ns, def, def_id, .. }) => evaluate_symbol_from_program(def, ns, *def_id, call_stack), List(xs) => match xs.first() { None => Err(CalcitErr::use_msg_stack_location( CalcitErrKind::Arity, @@ -423,13 +443,39 @@ pub fn evaluate_symbol_from_scope(idx: u16, scope: &CalcitScope) -> Result, + def_id: Option, call_stack: &CallStackList, ) -> Result { - let v0 = match coord { - Some((ns_idx, def_idx)) => program::load_by_index(ns_idx, file_ns, def_idx, sym), - None => None, - }; + let runtime_def_id = def_id.map(program::DefId).or_else(|| program::lookup_def_id(file_ns, sym)); + + if let Some(def_id) = runtime_def_id { + if let Some(program::RuntimeCell::Cold) = program::lookup_runtime_cell_by_id(def_id) { + let _ = program::seed_runtime_lazy_from_compiled(file_ns, sym); + } + + if let Some(cell) = program::lookup_runtime_cell_by_id(def_id) { + match cell { + program::RuntimeCell::Lazy { code, info } => { + return CalcitThunk::Code { code, info }.evaluated(&CalcitScope::default(), call_stack); + } + program::RuntimeCell::Ready(value) => { + return match value { + Calcit::Thunk(thunk) => thunk.evaluated(&CalcitScope::default(), call_stack), + _ => Ok(value), + }; + } + program::RuntimeCell::Resolving | program::RuntimeCell::Errored(_) => { + return Err(build_runtime_cell_error(file_ns, sym, call_stack, cell)); + } + program::RuntimeCell::Cold => {} + } + } + } + + let v0 = runtime_def_id + .and_then(program::lookup_runtime_ready_by_id) + .or_else(|| program::lookup_compiled_runtime_value(file_ns, sym)) + .or_else(|| program::lookup_def_id(file_ns, sym).and_then(|def_id| program::lookup_runtime_ready_by_id(def_id))); // if v0.is_none() { // println!("slow path reading symbol: {}/{}", file_ns, sym) // } @@ -470,15 +516,46 @@ pub fn parse_ns_def(s: &str) -> Option<(Arc, Arc)> { } } -/// without unfolding thunks +/// resolve a program symbol to an available value for namespace lookup paths pub fn eval_symbol_from_program(sym: &str, ns: &str, call_stack: &CallStackList) -> Result, CalcitErr> { - if let Some(v) = program::lookup_evaled_def(ns, sym) { + if let Some(def_id) = program::lookup_def_id(ns, sym) { + if let Some(program::RuntimeCell::Cold) = program::lookup_runtime_cell_by_id(def_id) { + let _ = program::seed_runtime_lazy_from_compiled(ns, sym); + } + + if let Some(cell) = program::lookup_runtime_cell_by_id(def_id) { + match cell { + program::RuntimeCell::Lazy { code, info } => { + return CalcitThunk::Code { code, info } + .evaluated(&CalcitScope::default(), call_stack) + .map(Some); + } + program::RuntimeCell::Ready(v) => return Ok(Some(v)), + program::RuntimeCell::Resolving | program::RuntimeCell::Errored(_) => { + return Err(build_runtime_cell_error(ns, sym, call_stack, cell)); + } + program::RuntimeCell::Cold => {} + } + } + } + if let Some(v) = program::lookup_compiled_runtime_value(ns, sym) { return Ok(Some(v)); } if let Some(code) = program::lookup_def_code(ns, sym) { - let v = evaluate_expr(&code, &CalcitScope::default(), ns, call_stack)?; - program::write_evaled_def(ns, sym, v.to_owned()).map_err(|e| CalcitErr::use_msg_stack(CalcitErrKind::Unexpected, e, call_stack))?; - return Ok(Some(v)); + match evaluate_expr(&code, &CalcitScope::default(), ns, call_stack) { + Ok(v) => { + program::write_runtime_ready(ns, sym, v.to_owned()) + .map_err(|e| CalcitErr::use_msg_stack(CalcitErrKind::Unexpected, e, call_stack))?; + return match v { + Calcit::Thunk(thunk) => thunk.evaluated(&CalcitScope::default(), call_stack).map(Some), + _ => Ok(Some(v)), + }; + } + Err(e) => { + program::mark_runtime_def_errored(ns, sym, Arc::from(e.to_string())); + return Err(e); + } + } } Ok(None) } diff --git a/src/runner/preprocess.rs b/src/runner/preprocess.rs index 06cba03d..ec6579d2 100644 --- a/src/runner/preprocess.rs +++ b/src/runner/preprocess.rs @@ -2,8 +2,8 @@ use crate::{ builtins::{is_js_syntax_procs, is_proc_name, is_registered_proc}, calcit::{ self, Calcit, CalcitArgLabel, CalcitEnum, CalcitErr, CalcitErrKind, CalcitFn, CalcitFnArgs, CalcitImpl, CalcitImport, CalcitList, - CalcitLocal, CalcitProc, CalcitRecord, CalcitScope, CalcitStruct, CalcitSymbolInfo, CalcitSyntax, CalcitThunk, CalcitThunkInfo, - CalcitTrait, CalcitTypeAnnotation, GENERATED_DEF, ImportInfo, LocatedWarning, NodeLocation, RawCodeType, SchemaKind, + CalcitLocal, CalcitProc, CalcitRecord, CalcitScope, CalcitStruct, CalcitSymbolInfo, CalcitSyntax, CalcitTrait, + CalcitTypeAnnotation, GENERATED_DEF, ImportInfo, LocatedWarning, NodeLocation, RawCodeType, SchemaKind, }, call_stack::{CallStackList, StackKind}, codegen, program, runner, @@ -66,86 +66,106 @@ impl<'a> PreprocessContext<'a> { } } -/// returns the resolved symbol(only functions and macros are used), -/// if code related is not preprocessed, do it internally. -pub fn preprocess_ns_def( +fn lookup_preprocessed_ns_def_value(ns: &str, def: &str) -> Option { + if let Some(cell) = program::lookup_runtime_cell(ns, def) { + match cell { + program::RuntimeCell::Ready(v) => return Some(v), + program::RuntimeCell::Lazy { .. } + | program::RuntimeCell::Resolving + | program::RuntimeCell::Errored(_) + | program::RuntimeCell::Cold => {} + } + } + + let _ = program::seed_runtime_lazy_from_compiled(ns, def); + + program::lookup_compiled_runtime_value(ns, def) +} + +fn ensure_ns_def_preprocessed( raw_ns: &str, raw_def: &str, check_warnings: &RefCell>, call_stack: &CallStackList, -) -> Result, CalcitErr> { +) -> Result<(), CalcitErr> { let ns = raw_ns; let def = raw_def; // println!("preprocessing def: {}/{}", ns, def); - match program::lookup_evaled_def(ns, def) { - Some(v) => { - // println!("{}/{} has inited", ns, def); - Ok(Some(v)) + if let Some(cell) = program::lookup_runtime_cell(ns, def) { + match cell { + program::RuntimeCell::Ready(_) | program::RuntimeCell::Lazy { .. } | program::RuntimeCell::Resolving => return Ok(()), + program::RuntimeCell::Errored(_) | program::RuntimeCell::Cold => {} } - None => { - // println!("init for... {}/{}", ns, def); - match program::lookup_def_code(ns, def) { - Some(code) => { - // write a nil value first to prevent dead loop - let loc = NodeLocation::new(Arc::from(ns), Arc::from(def), Arc::from(vec![])); - program::write_evaled_def(ns, def, Calcit::Nil) - .map_err(|e| CalcitErr::use_msg_stack_location(CalcitErrKind::Unexpected, e, call_stack, Some(loc)))?; - - let next_stack = call_stack.extend(ns, def, StackKind::Fn, &code, &[]); - - let mut scope_types = ScopeTypes::new(); - let context_label = format!("{ns}/{def}"); - let resolved_code = calcit::with_type_annotation_warning_context(context_label, || { - preprocess_expr(&code, &HashSet::new(), &mut scope_types, ns, check_warnings, &next_stack) - })?; - // println!("\n resolve code to run: {:?}", resolved_code); - let v = if is_fn_or_macro(&resolved_code) { - runner::evaluate_expr(&resolved_code, &CalcitScope::default(), ns, &next_stack)? - } else { - Calcit::Thunk(CalcitThunk::Code { - code: Arc::new(resolved_code), - info: Arc::new(CalcitThunkInfo { - ns: ns.into(), - def: def.into(), - }), - }) - }; - // println!("\nwriting value to: {}/{} {:?}", ns, def, v); - program::write_evaled_def(ns, def, v.to_owned()).map_err(|e| { - CalcitErr::use_msg_stack_location( - CalcitErrKind::Unexpected, - e, - call_stack, - Some(NodeLocation::new(Arc::from(ns), Arc::from(def), Arc::from(vec![]))), - ) - })?; + } - Ok(Some(v)) - } - None if ns.starts_with('|') || ns.starts_with('"') => Ok(None), - None => { - let loc = NodeLocation::new(Arc::from(ns), Arc::from(def), Arc::from(vec![])); - Err(CalcitErr::use_msg_stack_location( - CalcitErrKind::Var, - format!("unknown ns/def in program: {ns}/{def}"), - call_stack, - Some(loc), - )) + if lookup_preprocessed_ns_def_value(ns, def).is_some() { + return Ok(()); + } + + // println!("init for... {}/{}", ns, def); + match program::lookup_def_code(ns, def) { + Some(code) => { + // mark the def as resolving first to prevent dead loop during recursive preprocess. + program::mark_runtime_def_resolving(ns, def); + + let next_stack = call_stack.extend(ns, def, StackKind::Fn, &code, &[]); + + let mut scope_types = ScopeTypes::new(); + let context_label = format!("{ns}/{def}"); + let resolved_code = match calcit::with_type_annotation_warning_context(context_label, || { + preprocess_expr(&code, &HashSet::new(), &mut scope_types, ns, check_warnings, &next_stack) + }) { + Ok(resolved) => resolved, + Err(e) => { + program::mark_runtime_def_errored(ns, def, Arc::from(e.to_string())); + return Err(e); } - } + }; + // println!("\n resolve code to run: {:?}", resolved_code); + let preprocessed_code = resolved_code.to_owned(); + let codegen_form = resolved_code.to_owned(); + let deps = program::collect_compiled_deps(&codegen_form); + let type_summary = calcit::CalcitTypeAnnotation::summarize_code(&code).map(Arc::from); + program::store_compiled_output( + ns, + def, + 0, + preprocessed_code, + codegen_form, + deps, + type_summary, + Some(code.to_owned()), + program::lookup_def_schema(ns, def), + program::lookup_def_doc(ns, def).map(Arc::from).unwrap_or_else(|| Arc::from("")), + program::lookup_def_examples(ns, def).unwrap_or_default(), + ); + program::refresh_runtime_cell_from_preprocessed(ns, def, &resolved_code); + + Ok(()) + } + None if ns.starts_with('|') || ns.starts_with('"') => Ok(()), + None => { + let loc = NodeLocation::new(Arc::from(ns), Arc::from(def), Arc::from(vec![])); + Err(CalcitErr::use_msg_stack_location( + CalcitErrKind::Var, + format!("unknown ns/def in program: {ns}/{def}"), + call_stack, + Some(loc), + )) } } } -fn is_fn_or_macro(code: &Calcit) -> bool { - match code { - Calcit::List(xs) => match xs.first() { - Some(Calcit::Symbol { sym, .. }) => &**sym == "defn" || &**sym == "defmacro", - Some(Calcit::Syntax(s, ..)) => s == &CalcitSyntax::Defn || s == &CalcitSyntax::Defmacro, - _ => false, - }, - _ => false, - } +/// returns the resolved symbol(only functions and macros are used), +/// if code related is not preprocessed, do it internally. +pub fn preprocess_ns_def( + raw_ns: &str, + raw_def: &str, + check_warnings: &RefCell>, + call_stack: &CallStackList, +) -> Result, CalcitErr> { + ensure_ns_def_preprocessed(raw_ns, raw_def, check_warnings, call_stack)?; + Ok(lookup_preprocessed_ns_def_value(raw_ns, raw_def)) } pub fn preprocess_expr( @@ -178,7 +198,7 @@ pub fn preprocess_expr( at_def: info.at_def.to_owned(), at_ns: ns_alias, }), - coord: program::tip_coord(&target_ns, &def_part), + def_id: Some(program::ensure_def_id(&target_ns, &def_part).0), }); Ok(form) } else if program::has_def_code(&ns_alias, &def_part) { @@ -194,7 +214,7 @@ pub fn preprocess_expr( at_ns: info.at_ns.to_owned(), at_def: info.at_def.to_owned(), }), - coord: program::tip_coord(&ns_alias, &def_part), + def_id: Some(program::ensure_def_id(&ns_alias, &def_part).0), }); Ok(form) @@ -248,7 +268,7 @@ pub fn preprocess_expr( info: Arc::new(ImportInfo::SameFile { at_def: info.at_def.to_owned(), }), - coord: program::tip_coord(def_ns, def), + def_id: Some(program::ensure_def_id(def_ns, def).0), }); Ok(form) } else if let Ok(p) = def.parse::() { @@ -263,7 +283,7 @@ pub fn preprocess_expr( ns: calcit::CORE_NS.into(), def: def.to_owned(), info: Arc::new(ImportInfo::Core { at_ns: file_ns.into() }), - coord: program::tip_coord(calcit::CORE_NS, def), + def_id: Some(program::ensure_def_id(calcit::CORE_NS, def).0), }); Ok(form) } else if program::has_def_code(def_ns, def) { @@ -286,7 +306,7 @@ pub fn preprocess_expr( at_def: at_def.to_owned(), } }), - coord: program::tip_coord(def_ns, def), + def_id: Some(program::ensure_def_id(def_ns, def).0), }); Ok(form) } else if is_registered_proc(def) { @@ -308,7 +328,7 @@ pub fn preprocess_expr( at_ns: def_ns.to_owned(), at_def: at_def.to_owned(), }), - coord: program::tip_coord(&target_ns, def), + def_id: Some(program::ensure_def_id(&target_ns, def).0), }); Ok(form) } @@ -324,7 +344,7 @@ pub fn preprocess_expr( at_ns: file_ns.into(), at_def: at_def.to_owned(), }), - coord: None, + def_id: None, })) } else { let mut names: Vec> = Vec::with_capacity(scope_defs.len()); @@ -394,8 +414,8 @@ fn preprocess_list_call( // "handling list call: {} {:?}, {}", // primes::CrListWrap(xs.to_owned()), // head_form, - // if head_evaled.is_some() { - // head_evaled.to_owned().expect("debug") + // if head_runtime_value.is_some() { + // head_runtime_value.to_owned().expect("debug") // } else { // Calcit::Nil // } @@ -481,7 +501,7 @@ fn preprocess_list_call( ns: calcit::CORE_NS.into(), def: "get".into(), info: Arc::new(ImportInfo::Core { at_ns: Arc::from(file_ns) }), - coord: program::tip_coord(calcit::CORE_NS, "get"), + def_id: Some(program::ensure_def_id(calcit::CORE_NS, "get").0), }); let code = Calcit::from(CalcitList::from(&[get_method, args[0].to_owned(), head.to_owned()])); @@ -2429,9 +2449,9 @@ fn infer_type_from_expr(expr: &Calcit, scope_types: &ScopeTypes) -> Option { // Try to resolve generic return type using call arguments @@ -2670,39 +2690,25 @@ fn extract_field_name(field_arg: &Calcit) -> Option<&str> { } } +fn resolve_program_value_for_preprocess(ns: &str, def: &str, def_id: Option) -> Option { + let call_stack = CallStackList::default(); + runner::evaluate_symbol_from_program(def, ns, def_id, &call_stack).ok() +} + fn resolve_enum_value(target: &Calcit, scope_types: &ScopeTypes) -> Option { match target { Calcit::Enum(enum_def) => Some(enum_def.to_owned()), Calcit::Record(record) => CalcitEnum::from_record(record.to_owned()).ok(), - Calcit::Symbol { sym, info, .. } => match program::lookup_evaled_def(&info.at_ns, sym) { + Calcit::Symbol { sym, info, .. } => match resolve_program_value_for_preprocess(&info.at_ns, sym, None) { + Some(Calcit::Enum(enum_def)) => Some(enum_def), + Some(Calcit::Record(record)) => CalcitEnum::from_record(record).ok(), + _ => None, + }, + Calcit::Import(CalcitImport { ns, def, def_id, .. }) => match resolve_program_value_for_preprocess(ns, def, *def_id) { Some(Calcit::Enum(enum_def)) => Some(enum_def), Some(Calcit::Record(record)) => CalcitEnum::from_record(record).ok(), - Some(Calcit::Thunk(thunk)) => { - let call_stack = CallStackList::default(); - match thunk.evaluated(&CalcitScope::default(), &call_stack) { - Ok(Calcit::Enum(enum_def)) => Some(enum_def), - Ok(Calcit::Record(record)) => CalcitEnum::from_record(record).ok(), - _ => None, - } - } _ => None, }, - Calcit::Import(CalcitImport { ns, def, .. }) => { - match program::lookup_evaled_def(ns, def) { - Some(Calcit::Enum(enum_def)) => Some(enum_def), - Some(Calcit::Record(record)) => CalcitEnum::from_record(record).ok(), - // Handle Thunk case: force evaluation to get the enum value - Some(Calcit::Thunk(thunk)) => { - let call_stack = CallStackList::default(); - match thunk.evaluated(&CalcitScope::default(), &call_stack) { - Ok(Calcit::Enum(enum_def)) => Some(enum_def), - Ok(Calcit::Record(record)) => CalcitEnum::from_record(record).ok(), - _ => None, - } - } - _ => None, - } - } _ => resolve_type_value(target, scope_types) .and_then(|t| t.as_struct().cloned()) .and_then(|struct_def| { @@ -2727,7 +2733,7 @@ fn resolve_record_value(target: &Calcit, scope_types: &ScopeTypes) -> Option match program::lookup_evaled_def(&info.at_ns, sym) { + Calcit::Symbol { sym, info, .. } => match resolve_program_value_for_preprocess(&info.at_ns, sym, None) { Some(Calcit::Record(record)) => Some(record), Some(Calcit::Enum(enum_def)) => Some(enum_def.to_record_prototype()), Some(Calcit::Struct(struct_def)) => { @@ -2737,26 +2743,11 @@ fn resolve_record_value(target: &Calcit, scope_types: &ScopeTypes) -> Option { - let call_stack = CallStackList::default(); - match thunk.evaluated(&CalcitScope::default(), &call_stack) { - Ok(Calcit::Record(record)) => Some(record), - Ok(Calcit::Enum(enum_def)) => Some(enum_def.to_record_prototype()), - Ok(Calcit::Struct(struct_def)) => { - let values = vec![Calcit::Nil; struct_def.fields.len()]; - Some(CalcitRecord { - struct_ref: Arc::new(struct_def.to_owned()), - values: Arc::new(values), - }) - } - _ => None, - } - } _ => None, }, - Calcit::Import(CalcitImport { ns, def, .. }) => { - let evaled = program::lookup_evaled_def(ns, def); - match evaled { + Calcit::Import(CalcitImport { ns, def, def_id, .. }) => { + let runtime_value = resolve_program_value_for_preprocess(ns, def, *def_id); + match runtime_value { Some(Calcit::Record(record)) => Some(record), Some(Calcit::Enum(enum_def)) => Some(enum_def.to_record_prototype()), Some(Calcit::Struct(struct_def)) => { @@ -2766,21 +2757,6 @@ fn resolve_record_value(target: &Calcit, scope_types: &ScopeTypes) -> Option { - let call_stack = CallStackList::default(); - match thunk.evaluated(&CalcitScope::default(), &call_stack) { - Ok(Calcit::Record(record)) => Some(record), - Ok(Calcit::Enum(enum_def)) => Some(enum_def.to_record_prototype()), - Ok(Calcit::Struct(struct_def)) => { - let values = vec![Calcit::Nil; struct_def.fields.len()]; - Some(CalcitRecord { - struct_ref: Arc::new(struct_def.to_owned()), - values: Arc::new(values), - }) - } - _ => None, - } - } _ => None, } } @@ -2801,7 +2777,7 @@ fn collect_impl_records_from_value(value: &Calcit, call_stack: &CallStackList) - let resolve_impl = |value: &Calcit| -> Option { match value { Calcit::Impl(imp) => Some(imp.to_owned()), - Calcit::Import(import) => match runner::evaluate_symbol_from_program(&import.def, &import.ns, None, call_stack) { + Calcit::Import(import) => match runner::evaluate_symbol_from_program(&import.def, &import.ns, import.def_id, call_stack) { Ok(Calcit::Impl(imp)) => Some(imp), _ => None, }, @@ -2854,7 +2830,7 @@ fn get_impl_records_from_type(type_value: &CalcitTypeAnnotation, call_stack: &Ca if let CalcitTypeAnnotation::Custom(value) = type_value { match value.as_ref() { Calcit::Import(import) => { - return match runner::evaluate_symbol_from_program(&import.def, &import.ns, None, call_stack) { + return match runner::evaluate_symbol_from_program(&import.def, &import.ns, import.def_id, call_stack) { Ok(value) => collect_impl_records_from_value(&value, call_stack), Err(_) => None, }; @@ -4087,7 +4063,7 @@ mod tests { ns: Arc::from("tests.method.ns"), def: Arc::from("greet"), info: Arc::new(ImportInfo::SameFile { at_def: Arc::from("demo") }), - coord: None, + def_id: None, }); let method_impl = CalcitImpl { @@ -4168,13 +4144,13 @@ mod tests { let record_ns = "tests.method.impls"; let record_def = "&test-greeter-impls"; - program::write_evaled_def(record_ns, record_def, Calcit::Record(impl_record)).expect("register record impls"); + program::write_runtime_ready(record_ns, record_def, Calcit::Record(impl_record)).expect("register record impls"); let record_import = Calcit::Import(CalcitImport { ns: Arc::from(record_ns), def: Arc::from(record_def), info: Arc::new(ImportInfo::SameFile { at_def: Arc::from("demo") }), - coord: None, + def_id: None, }); scope_types.insert(Arc::from("user"), CalcitTypeAnnotation::parse_type_annotation_form(&record_import)); From b963ef552df925dd1758a025883ef55b2051015a Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 17 Mar 2026 01:53:32 +0800 Subject: [PATCH 15/57] Refine runtime boundary lookup paths --- .gitignore | 2 + drafts/runtime-boundary-refactor-plan.md | 30 +- ...17-0150-runtime-boundary-lookup-cleanup.md | 18 + src/bin/cr.rs | 2 +- src/calcit/fns.rs | 2 +- src/calcit/proc_name.rs | 21 +- src/calcit/thunk.rs | 4 + src/calcit/type_annotation.rs | 63 ++- src/call_stack.rs | 16 + src/codegen/emit_js.rs | 83 ++- src/codegen/gen_ir.rs | 13 +- src/lib.rs | 8 +- src/program.rs | 507 +++++++++--------- src/program/tests.rs | 433 +++++++++++++++ src/runner.rs | 290 +++++----- src/runner/preprocess.rs | 95 ++-- 16 files changed, 1084 insertions(+), 503 deletions(-) create mode 100644 editing-history/2026-0317-0150-runtime-boundary-lookup-cleanup.md create mode 100644 src/program/tests.rs diff --git a/.gitignore b/.gitignore index c27c707e..24d71199 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ demos/ .yarn .calcit-snippets/ + +.tmp-profiles/ \ No newline at end of file diff --git a/drafts/runtime-boundary-refactor-plan.md b/drafts/runtime-boundary-refactor-plan.md index 25c431b2..19b4cd67 100644 --- a/drafts/runtime-boundary-refactor-plan.md +++ b/drafts/runtime-boundary-refactor-plan.md @@ -46,7 +46,7 @@ ## 当前进度 -截至 2026-03-16,已经完成的不是“纯设计”,而是一部分边界已经落地: +截至 2026-03-17,已经完成的不是“纯设计”,而是一部分边界已经落地: - `DefId` 已经引入,并建立了 `ns/def -> DefId` 稳定索引; - `CompiledDef` 已经存在,且开始承载 `preprocessed_code`、`codegen_form`、`deps`、`type_summary` 等编译期信息; @@ -55,12 +55,32 @@ - `CalcitImport` 已开始携带稳定 `def_id` 缓存,runtime lookup 已优先消费它; - import/runtime lookup 兼容路径里的旧 `coord -> EntryBook` 残余已经清掉,`CalcitImport.coord` 与相关 runner 参数已删除; - `RuntimeCell` 的最小状态机已经落下,包含 `Cold | Resolving | Ready | Errored`,且 preprocess 的循环保护已从“先写 `Nil`”切到显式 `Resolving`; -- `yarn check-all` 已经作为当前重构的主验证门槛,并且需要先于 `cargo test` 跑通。 +- JS codegen 现在会显式跳过 core 中仅由 runtime 提供的 placeholder 定义,以及 `syntax`/`proc` 这类本就不应按普通顶层值发射的定义;这也移除了 `calcit.core.mjs` 中形如 `eval = &runtime-inplementation` 的伪导出; +- `clone_compiled_program_snapshot()` 已开始按“仅收集缺口定义”的两阶段方式补齐 snapshot,而不是先整表 clone source/index/runtime 全局状态后再筛选; +- runtime-derived snapshot fallback 现在进一步收窄为“仅对 runtime-only defs 生效”;只要 source-backed def 仍存在,就不会再因为旧 runtime 值而静默补出 snapshot entry; +- runtime-only snapshot fallback 已不再携带 source/schema/doc/examples 这类 source 元数据,snapshot 填充任务本身也不再为 fallback 路径 clone 整个 `ProgramDefEntry`; +- `seed_runtime_lazy_from_compiled()`、`lookup_compiled_runtime_value()`、`lookup_codegen_type_hint()` 已开始按需读取 compiled 字段,而不是在热路径上先 clone 整份 `CompiledDef`; +- `runner`/`lib`/`preprocess` 主调用方已经迁到“先取 compiled executable payload,再按需求值”的边界;旧的 `lookup_compiled_runtime_value()` 兼容包装已删除。 +- IR/codegen type-hint 查询已不再通过执行 compiled payload 来补信息;metadata 查询现在只依赖 compiled/source schema 与现成 runtime 值。 +- runtime symbol lookup 已不再假设 compiled 执行会回填 runtime cache;执行后的二次 runtime reread 兼容分支已移除。 +- `preprocess` 读取已编译定义时,lazy def 现在优先经由 `RuntimeCell::Lazy` 求值,不再绕过 runtime 状态机直接执行 compiled payload。 +- `run_program_with_docs` 已直接复用 `preprocess_ns_def()` 返回的入口值,不再在入口初始化后额外单独走一次 compiled 执行桥接。 +- `runner` 内部两处“runtime cell -> compiled executable fallback”逻辑已合并到统一 helper,减少了边界复制和不一致分支。 +- `preprocess` 的宽松读取路径也已并到同一组 helper,不再单独复制一份 `RuntimeCell::Lazy`/compiled fallback 分支。 +- 默认 scope 下的 thunk 求值入口也已收敛到 `CalcitThunk::evaluated_default()`,减少 runtime/lazy 入口上的样板逻辑。 +- runtime cell 与 compiled executable fallback 的统一入口现已下沉到 `program` 层;`runner` 只保留 runtime state 到用户态错误的映射。 +- `evaluate_compiled_def()` 现已退回 `program` 内部私有 helper;compiled payload 执行不再作为跨模块公共入口暴露。 +- `preprocess` 的 lenient lookup 也已直接回到 `program` 层,不再经由 `runner` 做一次中转;compiled executable code lookup 同时收紧为 `program` 内部 helper。 +- compiled output 写入接口已改为 payload struct 传参,`program` 内部构造链不再依赖 11 参数长调用;现有 clippy `too_many_arguments` 噪音已被顺手收掉。 +- `resolve_runtime_or_compiled_def()` 现在只负责调度;runtime cell 求值与 compiled payload 执行已拆成独立私有 helper,内部边界更接近最终形态。 +- runtime-only 路径中的 `seed + lookup cell + resolve cell` 已继续收敛成单独 helper,`resolve_runtime_or_compiled_def()` 现在更明确地只做 `runtime or compiled` 两段调度。 +- compiled execution 热路径已改为直接借用 compiled payload;执行时不再额外 clone 一份 `preprocessed_code`,只有测试/显式查询 executable code 时才保留复制语义。 +- `yarn check-all` 已经作为当前重构的主验证门槛,并且当前门槛重新保持可通过。 同时也要明确,当前还没有真正完成的部分是: - 旧的 `PROGRAM_EVALED_DATA_STATE` 与 `lookup_evaled_def*` 兼容路径已经删除,`preprocess` 成功路径也不再直接写 runtime cache;compiled fallback 现在只做“读 compiled value”,不再把 compiled payload 回填成 `RuntimeCell::Ready`; -- 全局 `CompiledDef` 已不再携带 `runtime_value` 这类 runtime payload 字段;普通 preprocess 输出已经完全不构造 runtime payload,普通 compiled `Fn/Macro/Proc/Syntax/LazyValue` 都改为需要时从 `preprocessed_code` 临时 materialize;当前剩余耦合主要集中在 compiled/runtime 仍共享同一套 `Calcit` 值表示,以及 codegen snapshot 在 source-backed defs 缺 compiled metadata 时仍可能退回到 runtime-derived snapshot fallback entry; +- 全局 `CompiledDef` 已不再携带 `runtime_value` 这类 runtime payload 字段;普通 preprocess 输出已经完全不构造 runtime payload,普通 compiled `Fn/Macro/Proc/Syntax/LazyValue` 都改为需要时从 `preprocessed_code` 临时 materialize;当前剩余耦合主要集中在 runtime 执行路径仍会把 compiled executable payload materialize 成公共 `Calcit` 值,而 metadata / codegen 查询已基本不再走这条路;snapshot fallback 已不再对 source-backed defs 静默兜底。 - `Calcit::Thunk` 仍存在于公开值模型中,但 runtime 主路径已经不再依赖它作为缓存载体:lazy def 的待求值占位优先放进 `RuntimeCell::Lazy`,`Ready(Thunk)` 已被禁止写入 runtime store,preprocess 与普通 lookup 也不再把 runtime lazy cell 重新包装成公共 thunk;当前剩余问题主要转向 snapshot fallback 与少量兼容语义分支; - watch 模式已经开始利用 compiled deps 做 def 级 invalidation;当前 CLI incremental reload 不再默认按 package 整片清空,而是从 changed defs / ns 头部变更出发做依赖闭包清理。剩余缺口主要是 state slot 尚未引入,以及 watch/reload 回归覆盖还不够强。 @@ -339,7 +359,7 @@ - 但 runtime 内部缓存与 lazy 状态优先放进 `RuntimeCell`; - 减少 thunk 对全局写回和 code 表示的承担。 -当前状态:主路径已基本收尾。thunk 仍是公开 `Calcit` 值模型的一部分,但 runtime store 已不再接受 `Ready(Thunk)` 这类形态;lazy def 的未求值占位优先放进 `RuntimeCell::Lazy`,raw fallback 若得到 `Calcit::Thunk(Code)` 也会立刻规范化回 lazy cell。`eval_symbol_from_program` 也不再把 lazy thunk 返回给调用方,preprocess 查值路径同样不再把 runtime lazy cell 重新包装成公共 thunk。当前剩余工作主要不再是 thunk 主路径,而是继续压缩 snapshot fallback compiled entry 的存在范围,清理少量旧命名/提示语残留,并补齐更直接的 watch/reload 回归测试。 +当前状态:主路径已基本收尾。thunk 仍是公开 `Calcit` 值模型的一部分,但 runtime store 已不再接受 `Ready(Thunk)` 这类形态;lazy def 的未求值占位优先放进 `RuntimeCell::Lazy`,raw fallback 若得到 `Calcit::Thunk(Code)` 也会立刻规范化回 lazy cell。`eval_symbol_from_program` 也不再把 lazy thunk 返回给调用方,preprocess 查值路径同样不再把 runtime lazy cell 重新包装成公共 thunk。JS codegen 还额外收掉了一层旧桥接:core 中由 runtime 提供的 placeholder 定义、以及 syntax/proc 名称,不再伪装成普通 JS 顶层导出。当前剩余工作主要不再是 thunk 主路径,而是继续压缩 snapshot fallback compiled entry 的存在范围,清理少量旧命名/提示语残留,并补齐更直接的 watch/reload 回归测试。 #### Phase 3D: 删除旧 EntryBook 热路径依赖 @@ -379,7 +399,7 @@ 具体就是: -1. 继续收缩 runtime-derived snapshot fallback entry 的存在范围,优先区分哪些定义只是 runtime-only 注入,哪些本应来自 source/compiled 数据;source-backed defs 则优先补成真正的 compiled def。 +1. 继续收缩 runtime-derived snapshot fallback entry 的存在范围,优先区分哪些定义只是 runtime-only 注入,哪些本应来自 source/compiled 数据;source-backed defs 则优先补成真正的 compiled def,并继续减少 remaining lookup 热路径上的 compiled clone / runtime-derived materialize。 2. 给新的 compiled-deps reload invalidation 补更直接的 watch/reload 回归测试,并继续收缩仍需兜底的边界情况。 3. 仅在确有必要时,再继续清理少数仍保留公开 thunk 语义的兼容分支;不要再把重点放回 runtime 主路径。 4. 每一步都以 `cargo fmt && yarn check-all && cargo test -q` 为门槛,而不是只跑 Rust 单测。 diff --git a/editing-history/2026-0317-0150-runtime-boundary-lookup-cleanup.md b/editing-history/2026-0317-0150-runtime-boundary-lookup-cleanup.md new file mode 100644 index 00000000..88126488 --- /dev/null +++ b/editing-history/2026-0317-0150-runtime-boundary-lookup-cleanup.md @@ -0,0 +1,18 @@ +# Runtime boundary lookup cleanup + +## Summary + +- Consolidated runtime-vs-compiled lookup helpers inside `program` and removed extra `runner` bridging paths. +- Tightened snapshot fallback and codegen metadata lookup so source-backed defs no longer silently rely on runtime-derived compiled entries. +- Skipped runtime-only placeholder defs in JS/IR codegen and added program-level regression tests for reload invalidation and snapshot behavior. + +## Knowledge points + +- Runtime execution helpers should live in `program` so `runner` only maps runtime state to evaluation flow and user-facing errors. +- Metadata queries such as codegen type hints should prefer compiled/source schema and only fall back to ready runtime values, never by executing compiled payloads. +- Reload invalidation needs direct transitive-dependency tests plus namespace-header coverage; otherwise runtime cache cleanup regresses quietly. + +## Validation + +- `cargo fmt` +- release fibo profiling during optimization review \ No newline at end of file diff --git a/src/bin/cr.rs b/src/bin/cr.rs index 1e305f0f..ec6c01e1 100644 --- a/src/bin/cr.rs +++ b/src/bin/cr.rs @@ -1254,7 +1254,7 @@ fn analyze_code_entry(ns: &str, def_name: &str, entry: &snapshot::CodeEntry) -> if ns == calcit::calcit::CORE_NS { if let Ok(proc) = (*def_name).parse::() { if let Some(sig) = proc.get_type_signature() { - return analyze_builtin_proc(def_name, &sig); + return analyze_builtin_proc(def_name, sig); } } // Then check if this is a builtin syntax diff --git a/src/calcit/fns.rs b/src/calcit/fns.rs index a072bd0f..570325c0 100644 --- a/src/calcit/fns.rs +++ b/src/calcit/fns.rs @@ -187,7 +187,7 @@ impl CalcitScope { /// mutable insertiong of variable pub fn insert_mut(&mut self, key: u16, value: Calcit) { - self.0 = self.0.push(ScopePair { key, value }) + self.0 = self.0.push_right(ScopePair { key, value }) } pub fn get_names(&self) -> String { diff --git a/src/calcit/proc_name.rs b/src/calcit/proc_name.rs index b9693574..4073cecc 100644 --- a/src/calcit/proc_name.rs +++ b/src/calcit/proc_name.rs @@ -1,7 +1,11 @@ -use strum_macros::{AsRefStr, EnumString}; +use std::collections::HashMap; +use std::sync::{Arc, LazyLock}; + +use strum::IntoEnumIterator; +use strum_macros::{AsRefStr, EnumIter, EnumString}; /// represent builtin functions for performance reasons. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, EnumString, strum_macros::Display, AsRefStr)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, EnumString, EnumIter, strum_macros::Display, AsRefStr)] pub enum CalcitProc { // meta #[strum(serialize = "type-of")] @@ -390,7 +394,6 @@ pub enum CalcitProc { } use crate::CalcitTypeAnnotation; -use std::sync::Arc; /// Type signature for a Proc (builtin function) #[derive(Debug, Clone)] @@ -482,7 +485,11 @@ impl CalcitProc { /// Get the type signature for this proc if available /// Returns None for procs without type annotations - pub fn get_type_signature(&self) -> Option { + pub fn get_type_signature(&self) -> Option<&'static ProcTypeSignature> { + PROC_TYPE_SIGNATURES.get(self) + } + + fn build_type_signature(&self) -> Option { use CalcitProc::*; match self { @@ -1133,3 +1140,9 @@ impl CalcitProc { self.get_type_signature().is_some() } } + +static PROC_TYPE_SIGNATURES: LazyLock> = LazyLock::new(|| { + CalcitProc::iter() + .filter_map(|proc| proc.build_type_signature().map(|signature| (proc, signature))) + .collect() +}); diff --git a/src/calcit/thunk.rs b/src/calcit/thunk.rs index c8121ccd..185d5b2b 100644 --- a/src/calcit/thunk.rs +++ b/src/calcit/thunk.rs @@ -23,6 +23,10 @@ impl CalcitThunk { } } + pub fn evaluated_default(&self, call_stack: &CallStackList) -> Result { + self.evaluated(&CalcitScope::default(), call_stack) + } + /// evaluate the thunk, and write back to program state pub fn evaluated(&self, scope: &CalcitScope, call_stack: &CallStackList) -> Result { match self { diff --git a/src/calcit/type_annotation.rs b/src/calcit/type_annotation.rs index bf83a834..167a2725 100644 --- a/src/calcit/type_annotation.rs +++ b/src/calcit/type_annotation.rs @@ -335,23 +335,38 @@ impl CalcitTypeAnnotation { } /// If `form` is a `hint-fn` expression, return its argument items (everything after the head). - fn get_hint_fn_items(form: &Calcit) -> Option { + fn get_hint_fn_items(form: &Calcit) -> Option<&CalcitList> { let Calcit::List(list) = form else { return None }; if !Self::is_hint_fn_form(list) { return None; } - list.skip(1).ok() + Some(list) } - fn is_schema_key(form: &Calcit, name: &str) -> bool { + fn schema_key_name(form: &Calcit) -> Option<&str> { match form { - Calcit::Tag(tag) => tag.ref_str().trim_start_matches(':') == name, + Calcit::Tag(tag) => { + let raw = tag.ref_str(); + Some(raw.strip_prefix(':').unwrap_or(raw)) + } Calcit::Symbol { sym, .. } => { let raw = sym.as_ref(); - raw == name || raw.trim_start_matches(':') == name + Some(raw.strip_prefix(':').unwrap_or(raw)) } - Calcit::Str(text) => text.as_ref() == name, - _ => false, + Calcit::Str(text) => Some(text.as_ref()), + _ => None, + } + } + + fn schema_key_matches_any(form: &Calcit, keys: &[&str]) -> bool { + let Some(key) = Self::schema_key_name(form) else { + return false; + }; + match keys { + [first] => key == *first, + [first, second] => key == *first || key == *second, + [first, second, third] => key == *first || key == *second || key == *third, + _ => keys.contains(&key), } } @@ -368,7 +383,7 @@ impl CalcitTypeAnnotation { match form { Calcit::Map(xs) => { for (key, value) in xs { - if keys.iter().any(|name| Self::is_schema_key(key, name)) { + if Self::schema_key_matches_any(key, keys) { return Some(value); } } @@ -392,7 +407,7 @@ impl CalcitTypeAnnotation { let Some(value) = pair.get(1) else { continue; }; - if keys.iter().any(|name| Self::is_schema_key(key, name)) { + if Self::schema_key_matches_any(key, keys) { return Some(value); } } @@ -402,10 +417,28 @@ impl CalcitTypeAnnotation { } } + fn schema_has_any_field(form: &Calcit, keys: &[&str]) -> bool { + match form { + Calcit::Map(xs) => xs.iter().any(|(key, _)| Self::schema_key_matches_any(key, keys)), + Calcit::List(xs) => { + if !matches!(xs.first(), Some(head) if Self::is_schema_map_literal_head(head)) { + return false; + } + xs.iter().skip(1).any(|entry| { + let Calcit::List(pair) = entry else { + return false; + }; + pair.first().is_some_and(|key| Self::schema_key_matches_any(key, keys)) + }) + } + _ => false, + } + } + pub fn extract_return_type_from_hint_form(form: &Calcit) -> Option> { let generics = Self::extract_generics_from_hint_form(form).unwrap_or_default(); let items = Self::get_hint_fn_items(form)?; - for item in items.iter() { + for item in items.iter().skip(1) { if let Some(type_expr) = Self::extract_schema_value(item, &["return"]) { return Some(CalcitTypeAnnotation::parse_type_annotation_form_with_generics( type_expr, @@ -418,7 +451,7 @@ impl CalcitTypeAnnotation { pub fn extract_generics_from_hint_form(form: &Calcit) -> Option>> { let items = Self::get_hint_fn_items(form)?; - for item in items.iter() { + for item in items.iter().skip(1) { if let Some(value) = Self::extract_schema_value(item, &["generics"]) { if let Some(vars) = Self::parse_generics_list(value) { return Some(vars); @@ -526,9 +559,7 @@ impl CalcitTypeAnnotation { generics: &[Arc], strict_named_refs: bool, ) -> Option> { - let has_schema_fields = ["args", "return", "generics", "rest", "kind"] - .iter() - .any(|key| Self::extract_schema_value(form, &[*key]).is_some()); + let has_schema_fields = Self::schema_has_any_field(form, &["args", "return", "generics", "rest", "kind"]); if !has_schema_fields { return Self::infer_malformed_fn_schema(form, generics, strict_named_refs); } @@ -567,7 +598,7 @@ impl CalcitTypeAnnotation { pub fn extract_arg_types_from_hint_form(form: &Calcit, params: &[Arc]) -> Option>> { let generics = Self::extract_generics_from_hint_form(form).unwrap_or_default(); let items = Self::get_hint_fn_items(form)?; - for item in items.iter() { + for item in items.iter().skip(1) { if let Some(args_form) = Self::extract_schema_value(item, &["args"]) { let types = Self::parse_schema_args_types(args_form, params.len(), generics.as_slice()); return Some(types); @@ -1590,7 +1621,7 @@ impl CalcitTypeAnnotation { Calcit::Import(import) => Self::from_import(import).unwrap_or(Self::Dynamic), Calcit::Proc(proc) => { if let Some(signature) = proc.get_type_signature() { - Self::from_function_parts(signature.arg_types, signature.return_type) + Self::from_function_parts(signature.arg_types.clone(), signature.return_type.clone()) } else { Self::Dynamic } diff --git a/src/call_stack.rs b/src/call_stack.rs index 05ef5a06..97249f8d 100644 --- a/src/call_stack.rs +++ b/src/call_stack.rs @@ -93,6 +93,22 @@ impl CallStackList { self.to_owned() } } + + /// create new entry when code and args are already owned, avoiding an extra clone of args + pub fn extend_owned(&self, ns: &str, def: &str, kind: StackKind, code: Calcit, args: Vec) -> CallStackList { + let b = TRACK_STACK.load(std::sync::atomic::Ordering::Relaxed); + if b { + self.push_left(CalcitStack { + ns: Arc::from(ns), + def: Arc::from(def), + code, + args, + kind, + }) + } else { + self.to_owned() + } + } } // show simplified version of stack diff --git a/src/codegen/emit_js.rs b/src/codegen/emit_js.rs index 83a2653c..159f5447 100644 --- a/src/codegen/emit_js.rs +++ b/src/codegen/emit_js.rs @@ -89,6 +89,31 @@ fn is_preferred_js_proc(name: &str) -> bool { ) } +fn is_quote_head(value: &Calcit) -> bool { + matches!(value, Calcit::Syntax(CalcitSyntax::Quote, _)) + || matches!(value, Calcit::Symbol { sym, .. } if sym.as_ref() == "quote") + || matches!(value, Calcit::Import(CalcitImport { ns, def, .. }) if &**ns == calcit::CORE_NS && &**def == "quote") +} + +fn is_runtime_placeholder_form(value: &Calcit) -> bool { + matches!(value, Calcit::Symbol { sym, .. } if sym.as_ref() == "&runtime-inplementation") +} + +fn is_runtime_placeholder_quote(value: &Calcit) -> bool { + let Calcit::List(items) = value else { + return false; + }; + items.len() == 2 && items.first().is_some_and(is_quote_head) && items.get(1).is_some_and(is_runtime_placeholder_form) +} + +fn should_skip_core_def_codegen(def: &str, compiled_def: &program::CompiledDef) -> bool { + if CalcitSyntax::is_valid(def) || is_proc_name(def) || is_js_syntax_procs(def) { + return true; + } + + compiled_def.source_code.as_ref().is_some_and(is_runtime_placeholder_quote) +} + fn quote_to_js(xs: &Calcit, var_prefix: &str, tags: &RefCell>) -> Result { match xs { Calcit::Symbol { sym, .. } => Ok(format!("new {var_prefix}CalcitSymbol({})", escape_cirru_str(sym))), @@ -1251,7 +1276,7 @@ pub fn emit_js(entry_ns: &str, emit_path: &str) -> Result<(), String> { let app_pkg_name = entry_ns.split('.').collect::>()[0]; let pkg_name = ns.split('.').collect::>()[0]; // TODO simpler if app_pkg_name != pkg_name { - match internal_states::lookup_prev_ns_cache(&ns) { + match internal_states::lookup_prev_ns_cache(ns) { Some(v) if v == defs_in_current => { // same as last time, skip continue; @@ -1261,7 +1286,7 @@ pub fn emit_js(entry_ns: &str, emit_path: &str) -> Result<(), String> { } } // remember defs of each ns for comparing - internal_states::write_as_ns_cache(&ns, defs_in_current); + internal_states::write_as_ns_cache(ns, defs_in_current); // reset index each file reset_js_gensym_index(); @@ -1290,7 +1315,12 @@ pub fn emit_js(entry_ns: &str, emit_path: &str) -> Result<(), String> { // println!("deps order: {:?}", deps_in_order); for def in deps_in_order { + let compiled_def = file.get(&def).expect("compiled def for codegen"); + if &**ns == calcit::CORE_NS { + if should_skip_core_def_codegen(&def, compiled_def) { + continue; + } // some defs from core can be replaced by calcit.procs if is_js_unavailable_procs(&def) { continue; @@ -1301,8 +1331,6 @@ pub fn emit_js(entry_ns: &str, emit_path: &str) -> Result<(), String> { } } - let compiled_def = file.get(&def).expect("compiled def for codegen"); - match &compiled_def.kind { // probably not work here program::CompiledDefKind::Proc => { @@ -1310,24 +1338,24 @@ pub fn emit_js(entry_ns: &str, emit_path: &str) -> Result<(), String> { } program::CompiledDefKind::Fn => { let (raw_args, raw_body) = extract_preprocessed_fn_parts(&compiled_def.preprocessed_code)?; - gen_stack::push_call_stack(&ns, &def, StackKind::Codegen, compiled_def.preprocessed_code.to_owned(), &[]); + gen_stack::push_call_stack(ns, &def, StackKind::Codegen, compiled_def.preprocessed_code.to_owned(), &[]); let passed_defs = PassedDefs { - ns: &ns, + ns, local_defs: &def_names, file_imports: &file_imports, }; - defs_code.push_str(&gen_js_func(&def, &raw_args, &raw_body, &passed_defs, true, &collected_tags, &ns)?); + defs_code.push_str(&gen_js_func(&def, &raw_args, &raw_body, &passed_defs, true, &collected_tags, ns)?); gen_stack::pop_call_stack(); } program::CompiledDefKind::LazyValue => { // TODO need topological sorting for accuracy // values are called directly, put them after fns - gen_stack::push_call_stack(&ns, &def, StackKind::Codegen, compiled_def.codegen_form.to_owned(), &[]); + gen_stack::push_call_stack(ns, &def, StackKind::Codegen, compiled_def.codegen_form.to_owned(), &[]); writeln!( vals_code, "\nexport var {} = {};", escape_var(&def), - to_js_code(&compiled_def.codegen_form, &ns, &def_names, &file_imports, &collected_tags, None)? + to_js_code(&compiled_def.codegen_form, ns, &def_names, &file_imports, &collected_tags, None)? ) .expect("write"); gen_stack::pop_call_stack() @@ -1413,7 +1441,7 @@ pub fn emit_js(entry_ns: &str, emit_path: &str) -> Result<(), String> { tag_arr.push(']'); tags_code.push_str(&snippets::tmpl_tags_init(&tag_arr, tag_prefix)); - let js_file_path = code_emit_path.join(to_mjs_filename(&ns)); + let js_file_path = code_emit_path.join(to_mjs_filename(ns)); let wrote_new = write_file_if_changed( &js_file_path, &format!("{import_code}{tags_code}\n{defs_code}\n\n{vals_code}\n{direct_code}"), @@ -1439,6 +1467,29 @@ mod tests { use super::*; use crate::calcit::CalcitSymbolInfo; + fn runtime_placeholder_quote() -> Calcit { + Calcit::List(Arc::new(CalcitList::from(&[ + Calcit::Syntax(CalcitSyntax::Quote, Arc::from(calcit::CORE_NS)), + symbol("&runtime-inplementation"), + ]))) + } + + fn compiled_def_for_codegen_test(kind: program::CompiledDefKind, source_code: Option) -> program::CompiledDef { + program::CompiledDef { + def_id: program::DefId(0), + version_id: 0, + kind, + preprocessed_code: Calcit::Nil, + codegen_form: Calcit::Nil, + deps: vec![], + type_summary: None, + source_code, + schema: calcit::DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + } + } + fn symbol(name: &str) -> Calcit { Calcit::Symbol { sym: Arc::from(name), @@ -1475,4 +1526,16 @@ mod tests { let hint = CalcitList::from(&[Calcit::Syntax(CalcitSyntax::HintFn, Arc::from("tests")), schema]); assert!(!hinted_async(&hint)); } + + #[test] + fn core_codegen_skips_runtime_placeholder_defs() { + let compiled = compiled_def_for_codegen_test(program::CompiledDefKind::LazyValue, Some(runtime_placeholder_quote())); + assert!(should_skip_core_def_codegen("range", &compiled)); + } + + #[test] + fn core_codegen_skips_syntax_names_even_without_runtime_placeholder_source() { + let compiled = compiled_def_for_codegen_test(program::CompiledDefKind::LazyValue, None); + assert!(should_skip_core_def_codegen("eval", &compiled)); + } } diff --git a/src/codegen/gen_ir.rs b/src/codegen/gen_ir.rs index f7ff72bc..3834ae4d 100644 --- a/src/codegen/gen_ir.rs +++ b/src/codegen/gen_ir.rs @@ -35,16 +35,9 @@ fn extract_import_type_info(ns: &str, def: &str) -> Edn { return Edn::Nil; } - let result = match program::lookup_runtime_ready(ns, def) { - Some(value) => { - let annotation = CalcitTypeAnnotation::from_calcit(&value); - match annotation { - CalcitTypeAnnotation::Dynamic => Edn::Nil, - _ => dump_type_annotation(&annotation), - } - } - None => Edn::Nil, - }; + let result = program::lookup_codegen_type_hint(ns, def) + .map(|annotation| dump_type_annotation(annotation.as_ref())) + .unwrap_or(Edn::Nil); if pushed { TYPE_INFO_STACK.with(|stack| { diff --git a/src/lib.rs b/src/lib.rs index af040959..974af857 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,15 +56,15 @@ pub fn run_program_with_docs(init_ns: Arc, init_def: Arc, params: &[Ca let check_warnings = RefCell::new(LocatedWarning::default_list()); // preprocess to init - match runner::preprocess::preprocess_ns_def(&init_ns, &init_def, &check_warnings, &CallStackList::default()) { - Ok(_) => (), + let init_entry = match runner::preprocess::preprocess_ns_def(&init_ns, &init_def, &check_warnings, &CallStackList::default()) { + Ok(entry) => entry, Err(failure) => { eprintln!("\nfailed preprocessing, {failure}"); let headline = failure.headline(); call_stack::display_stack_with_docs(&headline, &failure.stack, failure.location.as_ref(), failure.hint.as_deref())?; return CalcitErr::err_str(failure.kind, headline); } - } + }; let warnings = check_warnings.borrow(); if !warnings.is_empty() { @@ -78,7 +78,7 @@ pub fn run_program_with_docs(init_ns: Arc, init_def: Arc, params: &[Ca hint: None, }); } - match program::lookup_runtime_ready(&init_ns, &init_def).or_else(|| program::lookup_compiled_runtime_value(&init_ns, &init_def)) { + match init_entry.or_else(|| program::lookup_runtime_ready(&init_ns, &init_def)) { None => CalcitErr::err_str(CalcitErrKind::Var, format!("entry not initialized: {init_ns}/{init_def}")), Some(entry) => match entry { Calcit::Fn { info, .. } => { diff --git a/src/program.rs b/src/program.rs index 8fc14065..db5c92bf 100644 --- a/src/program.rs +++ b/src/program.rs @@ -1,5 +1,8 @@ mod entry_book; +#[cfg(test)] +mod tests; + use std::cell::RefCell; use std::collections::{HashMap, HashSet, VecDeque}; use std::sync::Arc; @@ -8,7 +11,7 @@ use std::sync::RwLock; use cirru_parser::Cirru; -use crate::calcit::{self, Calcit, CalcitScope, CalcitThunk, CalcitThunkInfo, CalcitTypeAnnotation, DYNAMIC_TYPE}; +use crate::calcit::{self, Calcit, CalcitErr, CalcitScope, CalcitThunk, CalcitThunkInfo, CalcitTypeAnnotation, DYNAMIC_TYPE}; use crate::call_stack::CallStackList; use crate::data::cirru::code_to_calcit; use crate::runner; @@ -27,6 +30,18 @@ pub enum RuntimeCell { Errored(Arc), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RuntimeResolveMode { + Strict, + Lenient, +} + +#[derive(Debug, Clone)] +pub enum RuntimeResolveError { + RuntimeCell(RuntimeCell), + Eval(CalcitErr), +} + pub type ProgramRuntimeData = Vec; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -85,6 +100,18 @@ pub struct CompiledDef { pub examples: Vec, } +pub struct CompiledDefPayload { + pub version_id: u32, + pub preprocessed_code: Calcit, + pub codegen_form: Calcit, + pub deps: Vec, + pub type_summary: Option>, + pub source_code: Option, + pub schema: Arc, + pub doc: Arc, + pub examples: Vec, +} + pub type ProgramCompiledData = HashMap, CompiledFileData>; #[derive(Debug, Clone, PartialEq, Eq)] @@ -229,20 +256,23 @@ pub fn refresh_runtime_cell_from_preprocessed(ns: &str, def: &str, preprocessed_ } pub fn seed_runtime_lazy_from_compiled(ns: &str, def: &str) -> bool { - let Some(compiled) = lookup_compiled_def(ns, def) else { + let Some((def_id, preprocessed_code)) = with_compiled_def(ns, def, |compiled| { + if compiled.kind == CompiledDefKind::LazyValue { + Some((compiled.def_id, compiled.preprocessed_code.clone())) + } else { + None + } + }) + .flatten() else { return false; }; - if compiled.kind != CompiledDefKind::LazyValue { - return false; - } - - match lookup_runtime_cell_by_id(compiled.def_id) { + match lookup_runtime_cell_by_id(def_id) { Some(RuntimeCell::Lazy { .. } | RuntimeCell::Ready(_) | RuntimeCell::Resolving | RuntimeCell::Errored(_)) => false, Some(RuntimeCell::Cold) | None => { write_runtime_lazy( - compiled.def_id, - Arc::new(compiled.preprocessed_code), + def_id, + Arc::new(preprocessed_code), Arc::new(CalcitThunkInfo { ns: Arc::from(ns), def: Arc::from(def), @@ -323,42 +353,25 @@ pub fn collect_compiled_deps(code: &Calcit) -> Vec { deps } -fn build_compiled_def( - ns: &str, - def: &str, - version_id: u32, - preprocessed_code: Calcit, - codegen_form: Calcit, - deps: Vec, - type_summary: Option>, - source_code: Option, - schema: Arc, - doc: Arc, - examples: Vec, -) -> CompiledDef { - let kind = CompiledDefKind::from_preprocessed_code(&preprocessed_code); +fn build_compiled_def(ns: &str, def: &str, payload: CompiledDefPayload) -> CompiledDef { + let kind = CompiledDefKind::from_preprocessed_code(&payload.preprocessed_code); CompiledDef { def_id: ensure_def_id(ns, def), - version_id, + version_id: payload.version_id, kind, - preprocessed_code, - codegen_form, - deps, - type_summary, - source_code, - schema, - doc, - examples, + preprocessed_code: payload.preprocessed_code, + codegen_form: payload.codegen_form, + deps: payload.deps, + type_summary: payload.type_summary, + source_code: payload.source_code, + schema: payload.schema, + doc: payload.doc, + examples: payload.examples, } } -fn build_snapshot_fallback_compiled_def( - ns: &str, - def: &str, - runtime_value: Calcit, - source_entry: Option<&ProgramDefEntry>, -) -> CompiledDef { +fn build_runtime_only_snapshot_fallback_compiled_def(ns: &str, def: &str, runtime_value: Calcit) -> CompiledDef { let codegen_form = match &runtime_value { Calcit::Thunk(thunk) => thunk.get_code().to_owned(), _ => runtime_value.to_owned(), @@ -373,15 +386,11 @@ fn build_snapshot_fallback_compiled_def( preprocessed_code: codegen_form.to_owned(), codegen_form, deps, - type_summary: source_entry - .and_then(|entry| calcit::CalcitTypeAnnotation::summarize_code(&entry.code)) - .map(Arc::from), - source_code: source_entry.map(|entry| entry.code.to_owned()), - schema: source_entry - .map(|entry| entry.schema.clone()) - .unwrap_or_else(|| DYNAMIC_TYPE.clone()), - doc: source_entry.map(|entry| entry.doc.clone()).unwrap_or_else(|| Arc::from("")), - examples: source_entry.map(|entry| entry.examples.clone()).unwrap_or_default(), + type_summary: None, + source_code: None, + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], } } @@ -395,12 +404,7 @@ fn ensure_source_backed_compiled_def_for_snapshot(ns: &str, def: &str) -> Option } let warnings: RefCell> = RefCell::new(vec![]); - if runner::preprocess::preprocess_ns_def(ns, def, &warnings, &CallStackList::default()).is_err() { - return None; - } - - let warnings = warnings.borrow(); - if !warnings.is_empty() { + if runner::preprocess::compile_source_def_for_snapshot(ns, def, &warnings, &CallStackList::default()).is_err() { return None; } @@ -415,32 +419,8 @@ pub fn write_compiled_def(ns: &str, def: &str, compiled: CompiledDef) { file.defs.insert(Arc::from(def), compiled); } -pub fn store_compiled_output( - ns: &str, - def: &str, - version_id: u32, - preprocessed_code: Calcit, - codegen_form: Calcit, - deps: Vec, - type_summary: Option>, - source_code: Option, - schema: Arc, - doc: Arc, - examples: Vec, -) { - let compiled = build_compiled_def( - ns, - def, - version_id, - preprocessed_code, - codegen_form, - deps, - type_summary, - source_code, - schema, - doc, - examples, - ); +pub fn store_compiled_output(ns: &str, def: &str, payload: CompiledDefPayload) { + let compiled = build_compiled_def(ns, def, payload); write_compiled_def(ns, def, compiled); } @@ -450,16 +430,136 @@ pub fn lookup_compiled_def(ns: &str, def: &str) -> Option { file.defs.get(def).cloned() } -pub fn lookup_compiled_runtime_value(ns: &str, def: &str) -> Option { - let compiled = lookup_compiled_def(ns, def)?; +fn with_compiled_def(ns: &str, def: &str, f: impl FnOnce(&CompiledDef) -> T) -> Option { + let program = PROGRAM_COMPILED_DATA_STATE.read().expect("read compiled program data"); + let file = program.get(ns)?; + let compiled = file.defs.get(def)?; + Some(f(compiled)) +} + +fn is_compiled_executable_kind(kind: &CompiledDefKind) -> bool { + matches!( + kind, + CompiledDefKind::Fn | CompiledDefKind::Macro | CompiledDefKind::Proc | CompiledDefKind::Syntax + ) +} - match compiled.kind { - CompiledDefKind::Fn | CompiledDefKind::Macro | CompiledDefKind::Proc | CompiledDefKind::Syntax => { - runner::evaluate_expr(&compiled.preprocessed_code, &CalcitScope::default(), ns, &CallStackList::default()).ok() +fn with_compiled_executable_payload(ns: &str, def: &str, f: impl FnOnce(&Calcit) -> T) -> Option { + with_compiled_def(ns, def, |compiled| { + if is_compiled_executable_kind(&compiled.kind) { + Some(f(&compiled.preprocessed_code)) + } else { + None } - CompiledDefKind::LazyValue => None, - _ => None, + }) + .flatten() +} + +#[cfg(test)] +fn lookup_compiled_executable_code(ns: &str, def: &str) -> Option { + with_compiled_executable_payload(ns, def, Calcit::to_owned) +} + +fn execute_compiled_executable_payload(ns: &str, def: &str) -> Option { + with_compiled_executable_payload(ns, def, |code| { + runner::evaluate_expr(code, &CalcitScope::default(), ns, &CallStackList::default()).ok() + }) + .flatten() +} + +fn resolve_runtime_ready_value(value: Calcit, call_stack: &CallStackList) -> Result { + match value { + Calcit::Thunk(thunk) => thunk.evaluated_default(call_stack).map_err(RuntimeResolveError::Eval), + _ => Ok(value), + } +} + +fn resolve_runtime_cell_value( + cell: RuntimeCell, + mode: RuntimeResolveMode, + call_stack: &CallStackList, +) -> Result, RuntimeResolveError> { + match cell { + RuntimeCell::Lazy { code, info } => CalcitThunk::Code { code, info } + .evaluated_default(call_stack) + .map(Some) + .map_err(RuntimeResolveError::Eval), + RuntimeCell::Ready(value) => resolve_runtime_ready_value(value, call_stack).map(Some), + RuntimeCell::Resolving | RuntimeCell::Errored(_) => { + if matches!(mode, RuntimeResolveMode::Strict) { + Err(RuntimeResolveError::RuntimeCell(cell)) + } else { + Ok(None) + } + } + RuntimeCell::Cold => Ok(None), + } +} + +fn resolve_runtime_def_value( + ns: &str, + def: &str, + def_id: Option, + mode: RuntimeResolveMode, + call_stack: &CallStackList, +) -> Result, RuntimeResolveError> { + let runtime_def_id = def_id.or_else(|| lookup_def_id(ns, def)); + + if let Some(def_id) = runtime_def_id { + if let Some(RuntimeCell::Cold) = lookup_runtime_cell_by_id(def_id) { + let _ = seed_runtime_lazy_from_compiled(ns, def); + } + + if let Some(cell) = lookup_runtime_cell_by_id(def_id) { + return resolve_runtime_cell_value(cell, mode, call_stack); + } + } + + Ok(None) +} + +pub fn resolve_runtime_or_compiled_def( + ns: &str, + def: &str, + def_id: Option, + mode: RuntimeResolveMode, + call_stack: &CallStackList, +) -> Result, RuntimeResolveError> { + if let Some(value) = resolve_runtime_def_value(ns, def, def_id, mode, call_stack)? { + return Ok(Some(value)); + } + + Ok(execute_compiled_executable_payload(ns, def)) +} + +pub fn lookup_runtime_or_compiled_def_lenient(ns: &str, def: &str) -> Option { + resolve_runtime_or_compiled_def(ns, def, None, RuntimeResolveMode::Lenient, &CallStackList::default()) + .ok() + .flatten() +} + +fn annotation_from_value(value: &Calcit) -> Option> { + let annotation = Arc::new(calcit::CalcitTypeAnnotation::from_calcit(value)); + if matches!(annotation.as_ref(), CalcitTypeAnnotation::Dynamic) { + None + } else { + Some(annotation) + } +} + +pub fn lookup_codegen_type_hint(ns: &str, def: &str) -> Option> { + if let Some(schema) = with_compiled_def(ns, def, |compiled| compiled.schema.clone()) + && !matches!(schema.as_ref(), CalcitTypeAnnotation::Dynamic) + { + return Some(schema); + } + + let source_schema = lookup_def_schema(ns, def); + if !matches!(source_schema.as_ref(), CalcitTypeAnnotation::Dynamic) { + return Some(source_schema); } + + lookup_runtime_ready(ns, def).as_ref().and_then(annotation_from_value) } fn remove_compiled_def(ns: &str, def: &str) { @@ -767,38 +867,77 @@ pub fn write_runtime_ready(ns: &str, def: &str, value: Calcit) -> Result<(), Str Ok(()) } -pub fn clone_compiled_program_snapshot() -> Result { - let mut compiled: CompiledProgram = PROGRAM_COMPILED_DATA_STATE.read().expect("read compiled program data").to_owned(); - let program_code = PROGRAM_CODE_DATA.read().expect("read program code").to_owned(); - let program_def_ids = PROGRAM_DEF_ID_INDEX.read().expect("read program def id index").by_ns.clone(); - let runtime = PROGRAM_RUNTIME_DATA_STATE.read().expect("read runtime data").to_owned(); +#[derive(Debug, Clone)] +struct SnapshotFillTask { + ns: Arc, + def: Arc, + source_backed: bool, + runtime_value: Option, +} + +fn collect_snapshot_fill_tasks(compiled: &CompiledProgram) -> Vec { + let program_code = PROGRAM_CODE_DATA.read().expect("read program code"); + let program_def_ids = PROGRAM_DEF_ID_INDEX.read().expect("read program def id index"); + let runtime = PROGRAM_RUNTIME_DATA_STATE.read().expect("read runtime data"); + + let mut tasks = vec![]; - for (ns, defs) in &program_def_ids { + for (ns, defs) in &program_def_ids.by_ns { + let existing_defs = compiled.get(ns).map(|file| &file.defs); let source_file = program_code.get(ns.as_ref()); - let compiled_file = compiled - .entry(ns.to_owned()) - .or_insert_with(|| CompiledFileData { defs: HashMap::new() }); for (def, def_id) in defs { - if compiled_file.defs.contains_key(def.as_ref()) { + if existing_defs.is_some_and(|defs| defs.contains_key(def.as_ref())) { continue; } - let source_entry = source_file.and_then(|data| data.defs.get(def.as_ref())); - if source_entry.is_some() - && let Some(compiled_def) = ensure_source_backed_compiled_def_for_snapshot(ns, def) - { - compiled_file.defs.insert(def.to_owned(), compiled_def); + let source_backed = source_file.is_some_and(|data| data.defs.contains_key(def.as_ref())); + let runtime_value = runtime.get(def_id.0 as usize).and_then(|cell| match cell { + RuntimeCell::Ready(value) => Some(value.to_owned()), + _ => None, + }); + + if !source_backed && runtime_value.is_none() { continue; } - let Some(RuntimeCell::Ready(runtime_value)) = runtime.get(def_id.0 as usize).cloned() else { - continue; - }; - compiled_file.defs.insert( - def.to_owned(), - build_snapshot_fallback_compiled_def(ns, def, runtime_value, source_entry), - ); + tasks.push(SnapshotFillTask { + ns: ns.to_owned(), + def: def.to_owned(), + source_backed, + runtime_value, + }); + } + } + + tasks +} + +fn should_use_runtime_snapshot_fallback(task: &SnapshotFillTask) -> bool { + !task.source_backed && task.runtime_value.is_some() +} + +pub fn clone_compiled_program_snapshot() -> Result { + let mut compiled: CompiledProgram = PROGRAM_COMPILED_DATA_STATE.read().expect("read compiled program data").to_owned(); + let tasks = collect_snapshot_fill_tasks(&compiled); + + for task in tasks { + let compiled_file = compiled + .entry(task.ns.clone()) + .or_insert_with(|| CompiledFileData { defs: HashMap::new() }); + + if task.source_backed + && let Some(compiled_def) = ensure_source_backed_compiled_def_for_snapshot(task.ns.as_ref(), task.def.as_ref()) + { + compiled_file.defs.insert(task.def.clone(), compiled_def); + continue; + } + + if should_use_runtime_snapshot_fallback(&task) + && let Some(runtime_value) = task.runtime_value + { + let fallback = build_runtime_only_snapshot_fallback_compiled_def(task.ns.as_ref(), task.def.as_ref(), runtime_value); + compiled_file.defs.insert(task.def, fallback); } } @@ -942,155 +1081,3 @@ pub fn clear_runtime_caches_for_changes(changes: &snapshot::ChangesDict, reload_ Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::calcit::{CalcitImport, ImportInfo}; - - fn compiled_def_for_test(def_id: DefId, deps: Vec) -> CompiledDef { - CompiledDef { - def_id, - version_id: 0, - kind: CompiledDefKind::Value, - preprocessed_code: Calcit::Nil, - codegen_form: Calcit::Nil, - deps, - type_summary: None, - source_code: None, - schema: DYNAMIC_TYPE.clone(), - doc: Arc::from(""), - examples: vec![], - } - } - - #[test] - fn reload_invalidation_collects_transitive_dependents() { - let mut compiled: ProgramCompiledData = HashMap::new(); - compiled.insert( - Arc::from("app.main"), - CompiledFileData { - defs: HashMap::from([ - (Arc::from("a"), compiled_def_for_test(DefId(1), vec![])), - (Arc::from("b"), compiled_def_for_test(DefId(2), vec![DefId(1)])), - (Arc::from("c"), compiled_def_for_test(DefId(3), vec![DefId(2)])), - (Arc::from("d"), compiled_def_for_test(DefId(4), vec![])), - ]), - }, - ); - - let mut index = ProgramDefIdIndex::default(); - index.by_ns.insert( - Arc::from("app.main"), - HashMap::from([ - (Arc::from("a"), DefId(1)), - (Arc::from("b"), DefId(2)), - (Arc::from("c"), DefId(3)), - (Arc::from("d"), DefId(4)), - ]), - ); - - let mut changes = snapshot::ChangesDict::default(); - changes.changed.insert( - Arc::from("app.main"), - snapshot::FileChangeInfo { - ns: None, - added_defs: HashMap::new(), - removed_defs: HashSet::new(), - changed_defs: HashMap::from([(String::from("a"), Cirru::Leaf(Arc::from("1")))]), - }, - ); - - let affected = collect_reload_affected_def_ids(&changes, &compiled, &index); - assert_eq!(affected, HashSet::from([DefId(1), DefId(2), DefId(3)])); - } - - #[test] - fn reload_invalidation_expands_namespace_header_changes() { - let compiled: ProgramCompiledData = HashMap::from([ - ( - Arc::from("app.main"), - CompiledFileData { - defs: HashMap::from([ - (Arc::from("a"), compiled_def_for_test(DefId(1), vec![])), - (Arc::from("b"), compiled_def_for_test(DefId(2), vec![])), - ]), - }, - ), - ( - Arc::from("app.consumer"), - CompiledFileData { - defs: HashMap::from([(Arc::from("use-main"), compiled_def_for_test(DefId(3), vec![DefId(2)]))]), - }, - ), - ]); - - let mut index = ProgramDefIdIndex::default(); - index.by_ns.insert( - Arc::from("app.main"), - HashMap::from([(Arc::from("a"), DefId(1)), (Arc::from("b"), DefId(2))]), - ); - index - .by_ns - .insert(Arc::from("app.consumer"), HashMap::from([(Arc::from("use-main"), DefId(3))])); - - let mut changes = snapshot::ChangesDict::default(); - changes.changed.insert( - Arc::from("app.main"), - snapshot::FileChangeInfo { - ns: Some(Cirru::Leaf(Arc::from("ns"))), - added_defs: HashMap::new(), - removed_defs: HashSet::new(), - changed_defs: HashMap::new(), - }, - ); - - let affected = collect_reload_affected_def_ids(&changes, &compiled, &index); - assert_eq!(affected, HashSet::from([DefId(1), DefId(2), DefId(3)])); - } - - #[test] - fn snapshot_fallback_preserves_dependency_metadata() { - let dep_id = register_program_def_id("dep.ns", "value"); - let _ = register_program_def_id("app.main", "dep"); - - let runtime_value = Calcit::from(vec![Calcit::Import(CalcitImport { - ns: Arc::from("dep.ns"), - def: Arc::from("value"), - info: Arc::new(ImportInfo::SameFile { at_def: Arc::from("dep") }), - def_id: Some(dep_id.0), - })]); - - let fallback = build_snapshot_fallback_compiled_def("app.main", "dep", runtime_value, None); - assert_eq!(fallback.deps, vec![dep_id]); - } - - #[test] - fn write_runtime_ready_normalizes_thunk_into_lazy_cell() { - let thunk_ns = "tests.runtime"; - let thunk_def = "lazy-demo"; - let thunk_code = Arc::new(Calcit::Nil); - let thunk_info = Arc::new(CalcitThunkInfo { - ns: Arc::from(thunk_ns), - def: Arc::from(thunk_def), - }); - - write_runtime_ready( - thunk_ns, - thunk_def, - Calcit::Thunk(CalcitThunk::Code { - code: thunk_code.clone(), - info: thunk_info.clone(), - }), - ) - .expect("write thunk into runtime"); - - match lookup_runtime_cell(thunk_ns, thunk_def) { - Some(RuntimeCell::Lazy { code, info }) => { - assert_eq!(code, thunk_code); - assert_eq!(info, thunk_info); - } - other => panic!("expected lazy runtime cell, got {other:?}"), - } - } -} diff --git a/src/program/tests.rs b/src/program/tests.rs new file mode 100644 index 00000000..a1813b37 --- /dev/null +++ b/src/program/tests.rs @@ -0,0 +1,433 @@ +use super::*; +use crate::calcit::{CalcitImport, ImportInfo}; +use crate::data::cirru::code_to_calcit; +use std::sync::{LazyLock, Mutex}; + +static PROGRAM_TEST_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); + +fn lock_program_test_state() -> std::sync::MutexGuard<'static, ()> { + PROGRAM_TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner()) +} + +fn reset_program_test_state() { + PROGRAM_RUNTIME_DATA_STATE.write().expect("reset runtime data").clear(); + PROGRAM_COMPILED_DATA_STATE.write().expect("reset compiled data").clear(); + PROGRAM_CODE_DATA.write().expect("reset program code").clear(); + *PROGRAM_DEF_ID_INDEX.write().expect("reset def id index") = ProgramDefIdIndex::default(); +} + +fn compiled_def_for_test(def_id: DefId, deps: Vec) -> CompiledDef { + CompiledDef { + def_id, + version_id: 0, + kind: CompiledDefKind::Value, + preprocessed_code: Calcit::Nil, + codegen_form: Calcit::Nil, + deps, + type_summary: None, + source_code: None, + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + } +} + +#[test] +fn reload_invalidation_collects_transitive_dependents() { + let mut compiled: ProgramCompiledData = HashMap::new(); + compiled.insert( + Arc::from("app.main"), + CompiledFileData { + defs: HashMap::from([ + (Arc::from("a"), compiled_def_for_test(DefId(1), vec![])), + (Arc::from("b"), compiled_def_for_test(DefId(2), vec![DefId(1)])), + (Arc::from("c"), compiled_def_for_test(DefId(3), vec![DefId(2)])), + (Arc::from("d"), compiled_def_for_test(DefId(4), vec![])), + ]), + }, + ); + + let mut index = ProgramDefIdIndex::default(); + index.by_ns.insert( + Arc::from("app.main"), + HashMap::from([ + (Arc::from("a"), DefId(1)), + (Arc::from("b"), DefId(2)), + (Arc::from("c"), DefId(3)), + (Arc::from("d"), DefId(4)), + ]), + ); + + let mut changes = snapshot::ChangesDict::default(); + changes.changed.insert( + Arc::from("app.main"), + snapshot::FileChangeInfo { + ns: None, + added_defs: HashMap::new(), + removed_defs: HashSet::new(), + changed_defs: HashMap::from([(String::from("a"), Cirru::Leaf(Arc::from("1")))]), + }, + ); + + let affected = collect_reload_affected_def_ids(&changes, &compiled, &index); + assert_eq!(affected, HashSet::from([DefId(1), DefId(2), DefId(3)])); +} + +#[test] +fn reload_invalidation_expands_namespace_header_changes() { + let compiled: ProgramCompiledData = HashMap::from([ + ( + Arc::from("app.main"), + CompiledFileData { + defs: HashMap::from([ + (Arc::from("a"), compiled_def_for_test(DefId(1), vec![])), + (Arc::from("b"), compiled_def_for_test(DefId(2), vec![])), + ]), + }, + ), + ( + Arc::from("app.consumer"), + CompiledFileData { + defs: HashMap::from([(Arc::from("use-main"), compiled_def_for_test(DefId(3), vec![DefId(2)]))]), + }, + ), + ]); + + let mut index = ProgramDefIdIndex::default(); + index.by_ns.insert( + Arc::from("app.main"), + HashMap::from([(Arc::from("a"), DefId(1)), (Arc::from("b"), DefId(2))]), + ); + index + .by_ns + .insert(Arc::from("app.consumer"), HashMap::from([(Arc::from("use-main"), DefId(3))])); + + let mut changes = snapshot::ChangesDict::default(); + changes.changed.insert( + Arc::from("app.main"), + snapshot::FileChangeInfo { + ns: Some(Cirru::Leaf(Arc::from("ns"))), + added_defs: HashMap::new(), + removed_defs: HashSet::new(), + changed_defs: HashMap::new(), + }, + ); + + let affected = collect_reload_affected_def_ids(&changes, &compiled, &index); + assert_eq!(affected, HashSet::from([DefId(1), DefId(2), DefId(3)])); +} + +#[test] +fn snapshot_fallback_preserves_dependency_metadata() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let dep_id = register_program_def_id("dep.ns", "value"); + let _ = register_program_def_id("app.main", "dep"); + + let runtime_value = Calcit::from(vec![Calcit::Import(CalcitImport { + ns: Arc::from("dep.ns"), + def: Arc::from("value"), + info: Arc::new(ImportInfo::SameFile { at_def: Arc::from("dep") }), + def_id: Some(dep_id.0), + })]); + + let fallback = build_runtime_only_snapshot_fallback_compiled_def("app.main", "dep", runtime_value); + assert_eq!(fallback.deps, vec![dep_id]); +} + +#[test] +fn write_runtime_ready_normalizes_thunk_into_lazy_cell() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let thunk_ns = "tests.runtime"; + let thunk_def = "lazy-demo"; + let thunk_code = Arc::new(Calcit::Nil); + let thunk_info = Arc::new(CalcitThunkInfo { + ns: Arc::from(thunk_ns), + def: Arc::from(thunk_def), + }); + + write_runtime_ready( + thunk_ns, + thunk_def, + Calcit::Thunk(CalcitThunk::Code { + code: thunk_code.clone(), + info: thunk_info.clone(), + }), + ) + .expect("write thunk into runtime"); + + match lookup_runtime_cell(thunk_ns, thunk_def) { + Some(RuntimeCell::Lazy { code, info }) => { + assert_eq!(code, thunk_code); + assert_eq!(info, thunk_info); + } + other => panic!("expected lazy runtime cell, got {other:?}"), + } +} + +#[test] +fn clear_runtime_caches_for_changes_clears_transitive_dependents() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let def_a = ensure_def_id("app.main", "a"); + let def_b = ensure_def_id("app.main", "b"); + let def_c = ensure_def_id("app.main", "c"); + let def_d = ensure_def_id("app.main", "d"); + + { + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("seed compiled data"); + compiled.insert( + Arc::from("app.main"), + CompiledFileData { + defs: HashMap::from([ + (Arc::from("a"), compiled_def_for_test(def_a, vec![])), + (Arc::from("b"), compiled_def_for_test(def_b, vec![def_a])), + (Arc::from("c"), compiled_def_for_test(def_c, vec![def_b])), + (Arc::from("d"), compiled_def_for_test(def_d, vec![])), + ]), + }, + ); + } + + write_runtime_ready("app.main", "a", Calcit::Number(1.0)).expect("seed runtime a"); + write_runtime_ready("app.main", "b", Calcit::Number(2.0)).expect("seed runtime b"); + write_runtime_ready("app.main", "c", Calcit::Number(3.0)).expect("seed runtime c"); + write_runtime_ready("app.main", "d", Calcit::Number(4.0)).expect("seed runtime d"); + + let mut changes = snapshot::ChangesDict::default(); + changes.changed.insert( + Arc::from("app.main"), + snapshot::FileChangeInfo { + ns: None, + added_defs: HashMap::new(), + removed_defs: HashSet::new(), + changed_defs: HashMap::from([(String::from("a"), Cirru::Leaf(Arc::from("1")))]), + }, + ); + + clear_runtime_caches_for_changes(&changes, false).expect("clear runtime caches for changes"); + + assert_eq!(lookup_runtime_cell("app.main", "a"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_runtime_cell("app.main", "b"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_runtime_cell("app.main", "c"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_runtime_ready("app.main", "d"), Some(Calcit::Number(4.0))); + + let compiled = PROGRAM_COMPILED_DATA_STATE.read().expect("read compiled data"); + let compiled_file = compiled.get("app.main").expect("compiled file should remain for unaffected defs"); + assert!(!compiled_file.defs.contains_key("a")); + assert!(!compiled_file.defs.contains_key("b")); + assert!(!compiled_file.defs.contains_key("c")); + assert!(compiled_file.defs.contains_key("d")); +} + +#[test] +fn clear_runtime_caches_for_changes_expands_namespace_header_invalidation() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let main_a = ensure_def_id("app.main", "a"); + let main_b = ensure_def_id("app.main", "b"); + let consumer_use = ensure_def_id("app.consumer", "use-main"); + let helper_keep = ensure_def_id("app.helper", "keep"); + + { + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("seed compiled data"); + compiled.insert( + Arc::from("app.main"), + CompiledFileData { + defs: HashMap::from([ + (Arc::from("a"), compiled_def_for_test(main_a, vec![])), + (Arc::from("b"), compiled_def_for_test(main_b, vec![])), + ]), + }, + ); + compiled.insert( + Arc::from("app.consumer"), + CompiledFileData { + defs: HashMap::from([(Arc::from("use-main"), compiled_def_for_test(consumer_use, vec![main_b]))]), + }, + ); + compiled.insert( + Arc::from("app.helper"), + CompiledFileData { + defs: HashMap::from([(Arc::from("keep"), compiled_def_for_test(helper_keep, vec![]))]), + }, + ); + } + + write_runtime_ready("app.main", "a", Calcit::Number(1.0)).expect("seed runtime main/a"); + write_runtime_ready("app.main", "b", Calcit::Number(2.0)).expect("seed runtime main/b"); + write_runtime_ready("app.consumer", "use-main", Calcit::Number(3.0)).expect("seed runtime consumer/use-main"); + write_runtime_ready("app.helper", "keep", Calcit::Number(9.0)).expect("seed runtime helper/keep"); + + let mut changes = snapshot::ChangesDict::default(); + changes.changed.insert( + Arc::from("app.main"), + snapshot::FileChangeInfo { + ns: Some(Cirru::Leaf(Arc::from("ns"))), + added_defs: HashMap::new(), + removed_defs: HashSet::new(), + changed_defs: HashMap::new(), + }, + ); + + clear_runtime_caches_for_changes(&changes, false).expect("clear runtime caches for namespace header change"); + + assert_eq!(lookup_runtime_cell("app.main", "a"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_runtime_cell("app.main", "b"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_runtime_cell("app.consumer", "use-main"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_runtime_ready("app.helper", "keep"), Some(Calcit::Number(9.0))); + + let compiled = PROGRAM_COMPILED_DATA_STATE.read().expect("read compiled data"); + assert!(!compiled.get("app.main").is_some_and(|file| file.defs.contains_key("a"))); + assert!(!compiled.get("app.main").is_some_and(|file| file.defs.contains_key("b"))); + assert!(!compiled.get("app.consumer").is_some_and(|file| file.defs.contains_key("use-main"))); + assert!(compiled.get("app.helper").is_some_and(|file| file.defs.contains_key("keep"))); +} + +#[test] +fn snapshot_prefers_source_backed_compiled_def_even_with_warnings() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let warn_code = + code_to_calcit(&Cirru::Leaf(Arc::from("missing-symbol")), "app.warn", "warny", vec![]).expect("build source-backed code"); + + PROGRAM_CODE_DATA.write().expect("seed program code").insert( + Arc::from("app.warn"), + ProgramFileData { + import_map: HashMap::new(), + defs: HashMap::from([( + Arc::from("warny"), + ProgramDefEntry { + code: warn_code.clone(), + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + )]), + }, + ); + let _ = ensure_def_id("app.warn", "warny"); + + write_runtime_ready("app.warn", "warny", Calcit::Number(42.0)).expect("seed runtime fallback value"); + + let snapshot = clone_compiled_program_snapshot().expect("clone compiled snapshot"); + let compiled = snapshot + .get("app.warn") + .and_then(|file| file.defs.get("warny")) + .expect("snapshot should include source-backed compiled def"); + + assert_eq!(compiled.kind, CompiledDefKind::LazyValue); + assert_eq!(compiled.codegen_form, warn_code); + assert_eq!(compiled.source_code, Some(compiled.codegen_form.clone())); +} + +#[test] +fn runtime_snapshot_fallback_only_allows_runtime_only_defs() { + let runtime_only = SnapshotFillTask { + ns: Arc::from("app.runtime"), + def: Arc::from("demo"), + source_backed: false, + runtime_value: Some(Calcit::Number(42.0)), + }; + assert!(should_use_runtime_snapshot_fallback(&runtime_only)); + + let source_backed = SnapshotFillTask { + ns: Arc::from("app.source"), + def: Arc::from("demo"), + source_backed: true, + runtime_value: Some(Calcit::Number(42.0)), + }; + assert!(!should_use_runtime_snapshot_fallback(&source_backed)); +} + +#[test] +fn lookup_codegen_type_hint_prefers_compiled_schema_over_runtime_value() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let schema = Arc::new(CalcitTypeAnnotation::String); + store_compiled_output( + "app.codegen", + "typed", + CompiledDefPayload { + version_id: 0, + preprocessed_code: Calcit::Nil, + codegen_form: Calcit::Nil, + deps: vec![], + type_summary: None, + source_code: None, + schema: schema.clone(), + doc: Arc::from(""), + examples: vec![], + }, + ); + write_runtime_ready("app.codegen", "typed", Calcit::Number(42.0)).expect("seed runtime value"); + + let hint = lookup_codegen_type_hint("app.codegen", "typed").expect("lookup codegen type hint"); + assert!(matches!(hint.as_ref(), CalcitTypeAnnotation::String)); +} + +#[test] +fn lookup_codegen_type_hint_falls_back_to_runtime_value() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let _ = ensure_def_id("app.codegen", "runtime-only"); + write_runtime_ready("app.codegen", "runtime-only", Calcit::Number(42.0)).expect("seed runtime value"); + + let hint = lookup_codegen_type_hint("app.codegen", "runtime-only").expect("lookup runtime fallback type hint"); + assert!(matches!(hint.as_ref(), CalcitTypeAnnotation::Number)); +} + +#[test] +fn compiled_executable_code_only_exposes_executable_kinds() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + store_compiled_output( + "app.compiled", + "callable", + CompiledDefPayload { + version_id: 0, + preprocessed_code: Calcit::Number(1.0), + codegen_form: Calcit::Nil, + deps: vec![], + type_summary: None, + source_code: None, + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + ); + + { + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("adjust compiled kind"); + compiled + .get_mut("app.compiled") + .and_then(|file| file.defs.get_mut("callable")) + .expect("compiled callable") + .kind = CompiledDefKind::Fn; + } + + assert_eq!( + lookup_compiled_executable_code("app.compiled", "callable"), + Some(Calcit::Number(1.0)) + ); + + { + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("adjust compiled kind"); + compiled + .get_mut("app.compiled") + .and_then(|file| file.defs.get_mut("callable")) + .expect("compiled callable") + .kind = CompiledDefKind::LazyValue; + } + + assert_eq!(lookup_compiled_executable_code("app.compiled", "callable"), None); +} diff --git a/src/runner.rs b/src/runner.rs index a91dc05b..3fb2027d 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,13 +1,14 @@ pub mod preprocess; pub mod track; +use std::cell::RefCell; use std::sync::Arc; use std::vec; use crate::builtins; use crate::calcit::{ CORE_NS, Calcit, CalcitArgLabel, CalcitErr, CalcitErrKind, CalcitFn, CalcitFnArgs, CalcitImport, CalcitList, CalcitLocal, CalcitProc, - CalcitScope, CalcitSyntax, CalcitThunk, MethodKind, NodeLocation, + CalcitScope, CalcitSyntax, MethodKind, NodeLocation, }; use crate::call_stack::{CallStackList, StackKind, using_stack}; use crate::data::cirru; @@ -34,6 +35,18 @@ fn build_runtime_cell_error(ns: &str, def: &str, call_stack: &CallStackList, cel } } +fn resolve_runtime_or_compiled_def( + ns: &str, + def: &str, + def_id: Option, + call_stack: &CallStackList, +) -> Result, CalcitErr> { + program::resolve_runtime_or_compiled_def(ns, def, def_id, program::RuntimeResolveMode::Strict, call_stack).map_err(|err| match err { + program::RuntimeResolveError::RuntimeCell(cell) => build_runtime_cell_error(ns, def, call_stack, cell), + program::RuntimeResolveError::Eval(failure) => failure, + }) +} + fn format_fn_arg_labels(args: &CalcitFnArgs) -> String { match args { CalcitFnArgs::Args(xs) => xs @@ -157,19 +170,19 @@ pub fn call_expr( spreading: bool, ) -> Result { // println!("calling expr: {}", xs); - let rest_nodes = xs.drop_left(); match v { Calcit::Proc(p) => { let values = if spreading { - evaluate_spreaded_args(rest_nodes, scope, file_ns, call_stack)? + evaluate_spreaded_args_from(xs, 1, scope, file_ns, call_stack)? } else { - evaluate_args(rest_nodes, scope, file_ns, call_stack)? + evaluate_args_from(xs, 1, scope, file_ns, call_stack)? }; builtins::handle_proc(*p, &values, call_stack) } Calcit::Syntax(s, def_ns) => { + let rest_nodes = xs.skip(1).expect("expected syntax rest nodes"); if using_stack() { - let next_stack = call_stack.extend(def_ns, s.as_ref(), StackKind::Syntax, &Calcit::from(xs), &rest_nodes.to_vec()); + let next_stack = call_stack.extend_owned(def_ns, s.as_ref(), StackKind::Syntax, Calcit::from(xs), rest_nodes.to_vec()); builtins::handle_syntax(s, &rest_nodes, scope, file_ns, &next_stack).map_err(|e| { if e.stack.is_empty() { let mut e2 = e; @@ -186,9 +199,9 @@ pub fn call_expr( Calcit::Method(name, kind) => { if matches!(kind, MethodKind::Invoke(_)) { let values = if spreading { - evaluate_spreaded_args(rest_nodes, scope, file_ns, call_stack)? + evaluate_spreaded_args_from(xs, 1, scope, file_ns, call_stack)? } else { - evaluate_args(rest_nodes, scope, file_ns, call_stack)? + evaluate_args_from(xs, 1, scope, file_ns, call_stack)? }; if using_stack() { let next_stack = call_stack.extend(file_ns, name, StackKind::Method, &Calcit::Nil, &values); @@ -197,8 +210,8 @@ pub fn call_expr( builtins::meta::invoke_method(name, &values, call_stack) } } else if matches!(kind, MethodKind::TagAccess) { - if rest_nodes.len() == 1 { - let obj = evaluate_expr(&rest_nodes[0], scope, file_ns, call_stack)?; + if xs.len() == 2 { + let obj = evaluate_expr(&xs[1], scope, file_ns, call_stack)?; let tag = evaluate_expr(&Calcit::tag(name), scope, file_ns, call_stack)?; if let Calcit::Map(m) = obj { match m.get(&tag) { @@ -243,9 +256,9 @@ pub fn call_expr( } Calcit::Fn { info, .. } => { let values = if spreading { - evaluate_spreaded_args(rest_nodes, scope, file_ns, call_stack)? + evaluate_spreaded_args_from(xs, 1, scope, file_ns, call_stack)? } else { - evaluate_args(rest_nodes, scope, file_ns, call_stack)? + evaluate_args_from(xs, 1, scope, file_ns, call_stack)? }; if using_stack() { let next_stack = call_stack.extend(&info.def_ns, &info.name, StackKind::Fn, &Calcit::from(xs), &values); @@ -260,14 +273,15 @@ pub fn call_expr( &Calcit::from(xs.to_owned()).lisp_str() ); + let mut current_values: Vec = xs.iter().skip(1).cloned().collect(); + let next_stack = if using_stack() { - call_stack.extend(&info.def_ns, &info.name, StackKind::Macro, &Calcit::from(xs), &rest_nodes.to_vec()) + call_stack.extend_owned(&info.def_ns, &info.name, StackKind::Macro, Calcit::from(xs), current_values.clone()) } else { call_stack.to_owned() }; // TODO moving to preprocess - let mut current_values: Vec = rest_nodes.to_vec(); // println!("eval macro: {} {}", x, expr.lisp_str())); // println!("macro... {} {}", x, CrListWrap(current_values.to_owned())); @@ -276,7 +290,7 @@ pub fn call_expr( Ok(loop { // need to handle recursion bind_marked_args(&mut body_scope, &info.args, ¤t_values, call_stack)?; - let code = evaluate_lines(&info.body.to_vec(), &body_scope, &info.def_ns, &next_stack)?; + let code = evaluate_lines(info.body.as_ref().as_slice(), &body_scope, &info.def_ns, &next_stack)?; match code { Calcit::Recur(ys) => { current_values = ys; @@ -289,8 +303,8 @@ pub fn call_expr( }) } Calcit::Tag(k) => { - if rest_nodes.len() == 1 { - let v = evaluate_expr(&rest_nodes[0], scope, file_ns, call_stack)?; + if xs.len() == 2 { + let v = evaluate_expr(&xs[1], scope, file_ns, call_stack)?; if let Calcit::Map(m) = v { match m.get(&Calcit::Tag(k.to_owned())) { @@ -308,7 +322,7 @@ pub fn call_expr( } else { Err(CalcitErr::use_msg_stack_location( CalcitErrKind::Arity, - format!("tag only takes 1 argument, got: {rest_nodes}"), + format!("tag only takes 1 argument, got: {}", xs.len().saturating_sub(1)), call_stack, xs.first().and_then(|node| node.get_location()), )) @@ -316,9 +330,9 @@ pub fn call_expr( } Calcit::Registered(alias) => { let values = if spreading { - evaluate_spreaded_args(rest_nodes, scope, file_ns, call_stack)? + evaluate_spreaded_args_from(xs, 1, scope, file_ns, call_stack)? } else { - evaluate_args(rest_nodes, scope, file_ns, call_stack)? + evaluate_args_from(xs, 1, scope, file_ns, call_stack)? }; builtins::call_registered_proc(alias, values, call_stack).map_err(|e| { if e.kind == CalcitErrKind::Var { @@ -446,36 +460,7 @@ pub fn evaluate_symbol_from_program( def_id: Option, call_stack: &CallStackList, ) -> Result { - let runtime_def_id = def_id.map(program::DefId).or_else(|| program::lookup_def_id(file_ns, sym)); - - if let Some(def_id) = runtime_def_id { - if let Some(program::RuntimeCell::Cold) = program::lookup_runtime_cell_by_id(def_id) { - let _ = program::seed_runtime_lazy_from_compiled(file_ns, sym); - } - - if let Some(cell) = program::lookup_runtime_cell_by_id(def_id) { - match cell { - program::RuntimeCell::Lazy { code, info } => { - return CalcitThunk::Code { code, info }.evaluated(&CalcitScope::default(), call_stack); - } - program::RuntimeCell::Ready(value) => { - return match value { - Calcit::Thunk(thunk) => thunk.evaluated(&CalcitScope::default(), call_stack), - _ => Ok(value), - }; - } - program::RuntimeCell::Resolving | program::RuntimeCell::Errored(_) => { - return Err(build_runtime_cell_error(file_ns, sym, call_stack, cell)); - } - program::RuntimeCell::Cold => {} - } - } - } - - let v0 = runtime_def_id - .and_then(program::lookup_runtime_ready_by_id) - .or_else(|| program::lookup_compiled_runtime_value(file_ns, sym)) - .or_else(|| program::lookup_def_id(file_ns, sym).and_then(|def_id| program::lookup_runtime_ready_by_id(def_id))); + let v0 = resolve_runtime_or_compiled_def(file_ns, sym, def_id.map(program::DefId), call_stack)?; // if v0.is_none() { // println!("slow path reading symbol: {}/{}", file_ns, sym) // } @@ -494,10 +479,7 @@ pub fn evaluate_symbol_from_program( } else { unreachable!("expected symbol from path, this is a quick path, should succeed") }; - match v { - Calcit::Thunk(thunk) => thunk.evaluated(&CalcitScope::default(), call_stack), - _ => Ok(v), - } + Ok(v) } pub fn parse_ns_def(s: &str) -> Option<(Arc, Arc)> { @@ -518,44 +500,12 @@ pub fn parse_ns_def(s: &str) -> Option<(Arc, Arc)> { /// resolve a program symbol to an available value for namespace lookup paths pub fn eval_symbol_from_program(sym: &str, ns: &str, call_stack: &CallStackList) -> Result, CalcitErr> { - if let Some(def_id) = program::lookup_def_id(ns, sym) { - if let Some(program::RuntimeCell::Cold) = program::lookup_runtime_cell_by_id(def_id) { - let _ = program::seed_runtime_lazy_from_compiled(ns, sym); - } - - if let Some(cell) = program::lookup_runtime_cell_by_id(def_id) { - match cell { - program::RuntimeCell::Lazy { code, info } => { - return CalcitThunk::Code { code, info } - .evaluated(&CalcitScope::default(), call_stack) - .map(Some); - } - program::RuntimeCell::Ready(v) => return Ok(Some(v)), - program::RuntimeCell::Resolving | program::RuntimeCell::Errored(_) => { - return Err(build_runtime_cell_error(ns, sym, call_stack, cell)); - } - program::RuntimeCell::Cold => {} - } - } - } - if let Some(v) = program::lookup_compiled_runtime_value(ns, sym) { + if let Some(v) = resolve_runtime_or_compiled_def(ns, sym, None, call_stack)? { return Ok(Some(v)); } - if let Some(code) = program::lookup_def_code(ns, sym) { - match evaluate_expr(&code, &CalcitScope::default(), ns, call_stack) { - Ok(v) => { - program::write_runtime_ready(ns, sym, v.to_owned()) - .map_err(|e| CalcitErr::use_msg_stack(CalcitErrKind::Unexpected, e, call_stack))?; - return match v { - Calcit::Thunk(thunk) => thunk.evaluated(&CalcitScope::default(), call_stack).map(Some), - _ => Ok(Some(v)), - }; - } - Err(e) => { - program::mark_runtime_def_errored(ns, sym, Arc::from(e.to_string())); - return Err(e); - } - } + if program::has_def_code(ns, sym) { + let warnings: RefCell> = RefCell::new(vec![]); + return preprocess::preprocess_ns_def(ns, sym, &warnings, call_stack); } Ok(None) } @@ -567,14 +517,14 @@ pub fn run_fn(values: &[Calcit], info: &CalcitFn, call_stack: &CallStackList) -> if args.len() != values.len() { return Err(build_fn_arity_mismatch_error(info, values, call_stack, "call")); } - for (idx, v) in args.iter().enumerate() { - body_scope.insert_mut(*v, values[idx].to_owned()); + for (&arg, value) in args.iter().zip(values) { + body_scope.insert_mut(arg, value.to_owned()); } } CalcitFnArgs::MarkedArgs(args) => bind_marked_args(&mut body_scope, args, values, call_stack)?, } - let v = evaluate_lines(&info.body.to_vec(), &body_scope, &info.def_ns, call_stack)?; + let v = evaluate_lines(info.body.as_slice(), &body_scope, &info.def_ns, call_stack)?; if let Calcit::Recur(xs) = v { let mut current_values = xs.to_vec(); @@ -584,13 +534,13 @@ pub fn run_fn(values: &[Calcit], info: &CalcitFn, call_stack: &CallStackList) -> if args.len() != current_values.len() { return Err(build_fn_arity_mismatch_error(info, ¤t_values, call_stack, "recur")); } - for (idx, v) in args.iter().enumerate() { - body_scope.insert_mut(*v, current_values[idx].to_owned()); + for (&arg, value) in args.iter().zip(¤t_values) { + body_scope.insert_mut(arg, value.to_owned()); } } CalcitFnArgs::MarkedArgs(args) => bind_marked_args(&mut body_scope, args, ¤t_values, call_stack)?, } - let v = evaluate_lines(&info.body.to_vec(), &body_scope, &info.def_ns, call_stack)?; + let v = evaluate_lines(info.body.as_slice(), &body_scope, &info.def_ns, call_stack)?; match v { Calcit::Recur(xs) => current_values = xs.to_vec(), result => return Ok(result), @@ -608,8 +558,8 @@ pub fn run_fn_owned(values: Vec, info: &CalcitFn, call_stack: &CallStack if args.len() != values.len() { return Err(build_fn_arity_mismatch_error(info, &values, call_stack, "call")); } - for (idx, v) in values.into_iter().enumerate() { - body_scope.insert_mut(args[idx], v); + for (&arg, value) in args.iter().zip(values) { + body_scope.insert_mut(arg, value); } } CalcitFnArgs::MarkedArgs(args) => bind_marked_args(&mut body_scope, args, &values, call_stack)?, @@ -625,8 +575,8 @@ pub fn run_fn_owned(values: Vec, info: &CalcitFn, call_stack: &CallStack if args.len() != current_values.len() { return Err(build_fn_arity_mismatch_error(info, ¤t_values, call_stack, "recur")); } - for (idx, v) in current_values.into_iter().enumerate() { - body_scope.insert_mut(args[idx], v); + for (&arg, value) in args.iter().zip(current_values) { + body_scope.insert_mut(arg, value); } } CalcitFnArgs::MarkedArgs(args) => bind_marked_args(&mut body_scope, args, ¤t_values, call_stack)?, @@ -674,10 +624,8 @@ pub fn bind_marked_args( if spreading { match arg { CalcitArgLabel::Idx(idx) => { - let mut chunk: Vec = vec![]; - while let Some(v) = values.get(pop_values_idx.get_and_inc()) { - chunk.push(v.to_owned()); - } + let chunk = values[pop_values_idx.0..].to_vec(); + pop_values_idx.0 = values.len(); scope.insert_mut(*idx, Calcit::from(CalcitList::Vector(chunk))); if pop_args_idx.0 < args.len() { return Err(CalcitErr::use_msg_stack( @@ -769,19 +717,32 @@ pub fn evaluate_args( file_ns: &str, call_stack: &CallStackList, ) -> Result, CalcitErr> { - let mut ret: Vec = Vec::with_capacity(items.len()); - for item in &items { - // if let Calcit::Syntax(CalcitSyntax::ArgSpread, _) = item { - // unreachable!("unexpected spread in args: {items}, should be handled before calling this") - // } + evaluate_args_from(&items, 0, scope, file_ns, call_stack) +} + +pub fn evaluate_args_from( + items: &CalcitList, + start: usize, + scope: &CalcitScope, + file_ns: &str, + call_stack: &CallStackList, +) -> Result, CalcitErr> { + let mut ret: Vec = Vec::with_capacity(items.len().saturating_sub(start)); + let mut idx = 0; + items.traverse_result::(&mut |item| { + if idx < start { + idx += 1; + return Ok(()); + } + idx += 1; if item.is_expr_evaluated() { ret.push(item.to_owned()); } else { - let v = evaluate_expr(item, scope, file_ns, call_stack)?; - ret.push(v); + ret.push(evaluate_expr(item, scope, file_ns, call_stack)?); } - } + Ok(()) + })?; // println!("Evaluated args: {}", ret); Ok(ret) } @@ -794,61 +755,80 @@ pub fn evaluate_spreaded_args( file_ns: &str, call_stack: &CallStackList, ) -> Result, CalcitErr> { - let mut ret: Vec = Vec::with_capacity(items.len()); + evaluate_spreaded_args_from(&items, 0, scope, file_ns, call_stack) +} + +pub fn evaluate_spreaded_args_from( + items: &CalcitList, + start: usize, + scope: &CalcitScope, + file_ns: &str, + call_stack: &CallStackList, +) -> Result, CalcitErr> { + let mut ret: Vec = Vec::with_capacity(items.len().saturating_sub(start)); let mut spreading = false; - items.traverse_result(&mut |item| match item { - Calcit::Syntax(CalcitSyntax::ArgSpread, _) => { - spreading = true; - Ok(()) + let mut idx = 0; + items.traverse_result::(&mut |item| { + if idx < start { + idx += 1; + return Ok(()); } - _ => { - if item.is_expr_evaluated() { - if spreading { - match item { - Calcit::List(xs) => { - xs.traverse(&mut |x| { - ret.push(x.to_owned()); - }); - spreading = false; - Ok(()) + idx += 1; + + match item { + Calcit::Syntax(CalcitSyntax::ArgSpread, _) => { + spreading = true; + } + _ => { + if item.is_expr_evaluated() { + if spreading { + match item { + Calcit::List(xs) => { + xs.traverse(&mut |x| { + ret.push(x.to_owned()); + }); + spreading = false; + } + a => { + return Err(CalcitErr::use_msg_stack_location( + CalcitErrKind::Arity, + format!("expected list for spreading, got: {a}"), + call_stack, + a.get_location(), + )); + } } - a => Err(CalcitErr::use_msg_stack_location( - CalcitErrKind::Arity, - format!("expected list for spreading, got: {a}"), - call_stack, - a.get_location(), - )), + } else { + ret.push(item.to_owned()); } } else { - ret.push(item.to_owned()); - Ok(()) - } - } else { - let v = evaluate_expr(item, scope, file_ns, call_stack)?; - - if spreading { - match v { - Calcit::List(xs) => { - xs.traverse(&mut |x| { - ret.push(x.to_owned()); - }); - spreading = false; - Ok(()) + let v = evaluate_expr(item, scope, file_ns, call_stack)?; + + if spreading { + match v { + Calcit::List(xs) => { + xs.traverse(&mut |x| { + ret.push(x.to_owned()); + }); + spreading = false; + } + a => { + return Err(CalcitErr::use_msg_stack_location( + CalcitErrKind::Arity, + format!("expected list for spreading, got: {a}"), + call_stack, + a.get_location(), + )); + } } - a => Err(CalcitErr::use_msg_stack_location( - CalcitErrKind::Arity, - format!("expected list for spreading, got: {a}"), - call_stack, - a.get_location(), - )), + } else { + ret.push(v); } - } else { - ret.push(v); - Ok(()) } } } + Ok(()) })?; // println!("Evaluated args: {}", ret); Ok(ret) diff --git a/src/runner/preprocess.rs b/src/runner/preprocess.rs index ec6579d2..cc953a52 100644 --- a/src/runner/preprocess.rs +++ b/src/runner/preprocess.rs @@ -67,19 +67,29 @@ impl<'a> PreprocessContext<'a> { } fn lookup_preprocessed_ns_def_value(ns: &str, def: &str) -> Option { - if let Some(cell) = program::lookup_runtime_cell(ns, def) { - match cell { - program::RuntimeCell::Ready(v) => return Some(v), - program::RuntimeCell::Lazy { .. } - | program::RuntimeCell::Resolving - | program::RuntimeCell::Errored(_) - | program::RuntimeCell::Cold => {} - } - } - - let _ = program::seed_runtime_lazy_from_compiled(ns, def); + program::lookup_runtime_or_compiled_def_lenient(ns, def) +} - program::lookup_compiled_runtime_value(ns, def) +fn store_preprocessed_compiled_output(ns: &str, def: &str, source_code: &Calcit, resolved_code: &Calcit) { + let preprocessed_code = resolved_code.to_owned(); + let codegen_form = resolved_code.to_owned(); + let deps = program::collect_compiled_deps(&codegen_form); + let type_summary = calcit::CalcitTypeAnnotation::summarize_code(source_code).map(Arc::from); + program::store_compiled_output( + ns, + def, + program::CompiledDefPayload { + version_id: 0, + preprocessed_code, + codegen_form, + deps, + type_summary, + source_code: Some(source_code.to_owned()), + schema: program::lookup_def_schema(ns, def), + doc: program::lookup_def_doc(ns, def).map(Arc::from).unwrap_or_else(|| Arc::from("")), + examples: program::lookup_def_examples(ns, def).unwrap_or_default(), + }, + ); } fn ensure_ns_def_preprocessed( @@ -122,23 +132,7 @@ fn ensure_ns_def_preprocessed( } }; // println!("\n resolve code to run: {:?}", resolved_code); - let preprocessed_code = resolved_code.to_owned(); - let codegen_form = resolved_code.to_owned(); - let deps = program::collect_compiled_deps(&codegen_form); - let type_summary = calcit::CalcitTypeAnnotation::summarize_code(&code).map(Arc::from); - program::store_compiled_output( - ns, - def, - 0, - preprocessed_code, - codegen_form, - deps, - type_summary, - Some(code.to_owned()), - program::lookup_def_schema(ns, def), - program::lookup_def_doc(ns, def).map(Arc::from).unwrap_or_else(|| Arc::from("")), - program::lookup_def_examples(ns, def).unwrap_or_default(), - ); + store_preprocessed_compiled_output(ns, def, &code, &resolved_code); program::refresh_runtime_cell_from_preprocessed(ns, def, &resolved_code); Ok(()) @@ -168,6 +162,37 @@ pub fn preprocess_ns_def( Ok(lookup_preprocessed_ns_def_value(raw_ns, raw_def)) } +pub fn compile_source_def_for_snapshot( + ns: &str, + def: &str, + check_warnings: &RefCell>, + call_stack: &CallStackList, +) -> Result<(), CalcitErr> { + if program::lookup_compiled_def(ns, def).is_some() { + return Ok(()); + } + + let Some(code) = program::lookup_def_code(ns, def) else { + let loc = NodeLocation::new(Arc::from(ns), Arc::from(def), Arc::from(vec![])); + return Err(CalcitErr::use_msg_stack_location( + CalcitErrKind::Var, + format!("unknown ns/def in program: {ns}/{def}"), + call_stack, + Some(loc), + )); + }; + + let mut scope_types = ScopeTypes::new(); + let context_label = format!("{ns}/{def}"); + let resolved_code = calcit::with_type_annotation_warning_context(context_label, || { + preprocess_expr(&code, &HashSet::new(), &mut scope_types, ns, check_warnings, call_stack) + })?; + + store_preprocessed_compiled_output(ns, def, &code, &resolved_code); + + Ok(()) +} + pub fn preprocess_expr( expr: &Calcit, scope_defs: &HashSet>, @@ -438,7 +463,7 @@ fn preprocess_list_call( // println!("macro... {} {}", x, CrListWrap(current_values.to_owned())); let code = Calcit::List(Arc::new(xs.to_owned())); - let next_stack = call_stack.extend(&info.def_ns, &info.name, StackKind::Macro, &code, &args.to_vec()); + let next_stack = call_stack.extend_owned(&info.def_ns, &info.name, StackKind::Macro, code, args.to_vec()); let mut body_scope = CalcitScope::default(); @@ -2253,8 +2278,8 @@ fn infer_type_from_expr(expr: &Calcit, scope_types: &ScopeTypes) -> Option Option Date: Tue, 17 Mar 2026 10:02:36 +0800 Subject: [PATCH 16/57] Tighten runtime boundary closure work --- drafts/runtime-boundary-refactor-plan.md | 79 ++++-- ...17-0150-runtime-boundary-lookup-cleanup.md | 12 +- src/program/tests.rs | 250 ++++++++++++++++++ src/runner.rs | 38 ++- src/runner/preprocess.rs | 67 ++--- 5 files changed, 362 insertions(+), 84 deletions(-) diff --git a/drafts/runtime-boundary-refactor-plan.md b/drafts/runtime-boundary-refactor-plan.md index 19b4cd67..c8bee1f6 100644 --- a/drafts/runtime-boundary-refactor-plan.md +++ b/drafts/runtime-boundary-refactor-plan.md @@ -1,6 +1,6 @@ # Runtime Boundary Refactor Plan -> 目标:在**保留 watch 模式热更新**与**保留 JS codegen 依赖原始代码结构**两项能力的前提下,重构 runtime / preprocess / codegen 边界,降低热路径上的 lookup、clone、thunk 污染与全局状态耦合。 +> 调整后的目标排序:先把 **compiled/runtime 边界站稳并让结构清晰**,其次争取 **热路径性能收益**,再次才考虑 **减少实体与语义收口**。watch 热更新与 JS codegen 继续保留,但不再作为继续扩张新层的理由。 ## 为什么现在做 @@ -44,6 +44,17 @@ 这不是语言语义变化,而是实现层面的去同构。 +## 调整后的判断 + +这份方案到现在仍然有意义,但它的意义已经变化: + +- 这不再是一份适合继续扩张的“四层重构蓝图”; +- 更准确的定位,是一份 **compiled/runtime 拆边收官计划**; +- 继续推进的目标,是把已经落地的边界收紧、补齐回归测试、删除迁移期桥接; +- 暂时不继续推进的目标,是引入更多长期实体来追求理论完备。 + +换句话说,当前阶段要继续的是“收尾”,不是“扩编”。 + ## 当前进度 截至 2026-03-17,已经完成的不是“纯设计”,而是一部分边界已经落地: @@ -90,7 +101,7 @@ ## 目标边界模型 -建议把当前系统拆成 4 层。 +建议保留这套分层模型作为**分析框架**,而不是要求实现层面继续一比一落四层实体。 ### 1. Source Layer @@ -182,6 +193,8 @@ - “保留状态”不再等于“保留整个 def 的 evaled value” - `Ref` / atom / runtime resource 迁移到显式 state slot 体系 +当前判断:这一层保留为后续方向,但**不作为当前阶段的执行目标**。只有在 watch/reload 语义已经被现有 compiled/runtime 边界稳定支撑、且确实遇到状态保留表达不足时,才值得继续引入。 + ## 两项关键能力如何保留 ### A. 保留 watch 模式 @@ -304,6 +317,30 @@ - steady-state runtime 尽量只做整数索引; - 热路径避免 `Arc` 比较和容器扫描。 +## 当前执行面 + +不再把后续工作定义成“继续推进到完整四层”,而是改成下面三个收官面: + +### A. 站稳 compiled/runtime 边界 + +- 继续删除仍停留在迁移期的桥接 helper 与兜底分支; +- 保证 metadata/codegen 查询不再借 runtime 执行补信息; +- 保证 runtime lookup 不再假设 compiled 执行会隐式回填缓存; +- 把 `program` 维持为边界聚合点,避免 `runner`/`preprocess` 再各自复制一份 fallback 逻辑。 + +### B. 补齐 watch/reload 回归测试 + +- 直接覆盖 changed def、namespace header 变更、依赖闭包失效; +- 验证 source-backed def 不会被 runtime-derived snapshot 静默复活; +- 验证 lazy/runtime-only def 的 fallback 仍符合当前保留语义; +- 把 `cargo fmt && yarn check-all && cargo test -q` 作为固定门槛。 + +### C. 做减法而不是加层 + +- 优先合并过渡期命名、兼容包装、重复 helper; +- 暂不引入 `PersistentStateLayer` / `StateSlotId` 这类新实体; +- 只有在现有模型无法表达真实需求时,才新增一层概念。 + ## 迁移阶段 ### Phase 0: 约束冻结 @@ -380,6 +417,8 @@ - reload invalidation 规则已经以 `DefId` 为主; - 不再把“值缓存是否保留”误当成“状态是否保留”。 +当前判断:**暂停**。这一步不是当前瓶颈,也不符合“先让结构更清晰、再减少实体”的目标排序。除非后续出现无法用现有 compiled/runtime 边界解释的 reload state 问题,否则不进入实现。 + ### Phase 5: 删除旧耦合路径 - 删除 codegen 对 evaled program 的依赖 @@ -393,20 +432,20 @@ - runtime cycle detection 已完全基于 `RuntimeCell` 状态机; - watch reload 可以基于 compiled deps + stable identity 做解释得通的失效。 -## 接下来应该怎么做 +当前判断:保留为**收尾检查表**,不再视为一个需要继续扩展设计面的阶段。 -如果目标是继续往前推进,同时不让风险失控,下一步不应该直接去碰 state slot,也不需要再把重点放回 Phase 3B;更合理的是继续收尾 Phase 3C,并收缩 snapshot/codegen-only fallback。 +## 接下来应该怎么做 -具体就是: +下一步不再是“补完大设计”,而是按下面顺序收官: -1. 继续收缩 runtime-derived snapshot fallback entry 的存在范围,优先区分哪些定义只是 runtime-only 注入,哪些本应来自 source/compiled 数据;source-backed defs 则优先补成真正的 compiled def,并继续减少 remaining lookup 热路径上的 compiled clone / runtime-derived materialize。 -2. 给新的 compiled-deps reload invalidation 补更直接的 watch/reload 回归测试,并继续收缩仍需兜底的边界情况。 -3. 仅在确有必要时,再继续清理少数仍保留公开 thunk 语义的兼容分支;不要再把重点放回 runtime 主路径。 -4. 每一步都以 `cargo fmt && yarn check-all && cargo test -q` 为门槛,而不是只跑 Rust 单测。 +1. 继续收缩 runtime-derived snapshot fallback,只保留真正 runtime-only defs 需要的兜底;source-backed defs 一律优先走 compiled/source 数据。 +2. 补齐 watch/reload 回归测试,重点覆盖 changed defs、ns header、依赖闭包 invalidation,以及 snapshot fallback 不误补 source-backed defs。 +3. 清理还停留在迁移期的 helper、命名和双份 lookup 分支,让 `program` 成为唯一边界聚合点。 +4. 在上述三步稳定之后,再重新评估是否还需要继续压缩公开 thunk 语义,或是否真的存在引入 state slot 的必要。 -换句话说,下一步的目标不是“再引入一个新层”,也不是回头重复 3B,而是: +换句话说,下一步的目标是: -**把已经接进去的 runtime state machine 和 snapshot/codegen 边界,从“只剩最后几座桥”继续推进到真正职责清晰、桥接范围可解释。** +**把已经接进去的 runtime state machine 和 snapshot/codegen 边界,推进到职责清晰、测试充足、且不再继续引入新实体。** ## 预期收益 @@ -435,24 +474,24 @@ 3. 宏与 preprocess 若隐式依赖 runtime 当前行为,需要在阶段 2 前先全面梳理。 4. 调试输出会暂时退化,因为 call stack / source mapping 需要重新挂接到 `CompiledDef`。 -## 建议的第一步实现 +## 当前建议的第一步 -如果只做一个高 ROI 起步动作,建议先做: +如果只做一个高 ROI 的下一步动作,建议先做: -**先引入 `DefId + CompiledDef`,并把 JS codegen 从 evaled program 上拆下来。** +**先补强 watch/reload 回归测试,并用这些测试倒逼继续收缩 snapshot/runtime fallback。** 理由: -- 这是最容易与现有 runtime 并存的一步; -- 能立刻切断“为了 JS 保留 thunk”这一层耦合; -- 一旦 codegen 不再依赖 runtime value,后续 runtime/state 分层会简单很多。 +- 这是当前最能验证边界是否真的站稳的手段; +- 这会直接暴露哪些 fallback 仍然只是迁移期补丁; +- 这比继续设计新层更符合“结构清晰优先”的目标。 ## 最终判断 -在保留 watch 模式与 JS codegen 的前提下,Calcit 仍然可以做激进重构,而且值得做。 +在保留 watch 模式与 JS codegen 的前提下,这条线仍值得继续,但应该以“收官和减法”为主,而不是继续做激进扩层。 -真正需要放弃的不是功能,而是这件事: +真正需要继续放弃的不是功能,而是这件事: **“同一个 runtime 值对象同时承载源码、预处理结果、惰性求值状态、热更新身份与 codegen 输入。”** -只要不再坚持这件事,边界就能重新变清晰。 +只要不再坚持这件事,并且不再为它继续引入额外层级,边界就能重新变清晰,而且实现会更可控。 diff --git a/editing-history/2026-0317-0150-runtime-boundary-lookup-cleanup.md b/editing-history/2026-0317-0150-runtime-boundary-lookup-cleanup.md index 88126488..c9f818df 100644 --- a/editing-history/2026-0317-0150-runtime-boundary-lookup-cleanup.md +++ b/editing-history/2026-0317-0150-runtime-boundary-lookup-cleanup.md @@ -5,14 +5,24 @@ - Consolidated runtime-vs-compiled lookup helpers inside `program` and removed extra `runner` bridging paths. - Tightened snapshot fallback and codegen metadata lookup so source-backed defs no longer silently rely on runtime-derived compiled entries. - Skipped runtime-only placeholder defs in JS/IR codegen and added program-level regression tests for reload invalidation and snapshot behavior. +- Reframed the runtime-boundary draft into a closure plan focused on stabilizing compiled/runtime boundaries, adding watch-reload regression coverage, and avoiding new architectural layers. +- Simplified `runner` and `preprocess` lookup flow by removing thin wrappers, deduplicating repeated symbol fallback order, and collapsing silent program-value reads onto shared helpers. +- Added regression coverage for reload package clearing, source-backed snapshot rebuild after changes, compiled fallback cache behavior, and strict-vs-lenient runtime resolution semantics. ## Knowledge points - Runtime execution helpers should live in `program` so `runner` only maps runtime state to evaluation flow and user-facing errors. - Metadata queries such as codegen type hints should prefer compiled/source schema and only fall back to ready runtime values, never by executing compiled payloads. - Reload invalidation needs direct transitive-dependency tests plus namespace-header coverage; otherwise runtime cache cleanup regresses quietly. +- If compiled execution is used as a fallback read path, tests must assert it does not implicitly backfill runtime cells; otherwise compiled/runtime boundaries drift back together. +- Keep cleanup work biased toward removing duplicate lookup order and unused parameters before introducing any new abstraction; the stable payoff is clearer boundaries with lower entity count. +- `runner`-side error mapping for strict runtime resolution is still a meaningful boundary, but repeated namespace lookup order and required-value program reads can be shared safely. ## Validation - `cargo fmt` -- release fibo profiling during optimization review \ No newline at end of file +- release fibo profiling during optimization review +- `cargo test clear_runtime_caches_for_reload -- --nocapture` +- `cargo test snapshot_rebuilds_changed_source_backed_def_after_reload_changes -- --nocapture` +- `cargo test program::tests -- --nocapture` +- `cargo test -q` diff --git a/src/program/tests.rs b/src/program/tests.rs index a1813b37..0c753c3f 100644 --- a/src/program/tests.rs +++ b/src/program/tests.rs @@ -289,6 +289,171 @@ fn clear_runtime_caches_for_changes_expands_namespace_header_invalidation() { assert!(compiled.get("app.helper").is_some_and(|file| file.defs.contains_key("keep"))); } +#[test] +fn clear_runtime_caches_for_reload_clears_selected_packages_only() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let app_main = ensure_def_id("app.main", "entry"); + let app_extra = ensure_def_id("app.extra", "helper"); + let demo_reload = ensure_def_id("demo.feature", "reload"); + let util_keep = ensure_def_id("util.keep", "value"); + + { + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("seed compiled data"); + compiled.insert( + Arc::from("app.main"), + CompiledFileData { + defs: HashMap::from([(Arc::from("entry"), compiled_def_for_test(app_main, vec![]))]), + }, + ); + compiled.insert( + Arc::from("app.extra"), + CompiledFileData { + defs: HashMap::from([(Arc::from("helper"), compiled_def_for_test(app_extra, vec![]))]), + }, + ); + compiled.insert( + Arc::from("demo.feature"), + CompiledFileData { + defs: HashMap::from([(Arc::from("reload"), compiled_def_for_test(demo_reload, vec![]))]), + }, + ); + compiled.insert( + Arc::from("util.keep"), + CompiledFileData { + defs: HashMap::from([(Arc::from("value"), compiled_def_for_test(util_keep, vec![]))]), + }, + ); + } + + write_runtime_ready("app.main", "entry", Calcit::Number(1.0)).expect("seed runtime app.main/entry"); + write_runtime_ready("app.extra", "helper", Calcit::Number(2.0)).expect("seed runtime app.extra/helper"); + write_runtime_ready("demo.feature", "reload", Calcit::Number(3.0)).expect("seed runtime demo.feature/reload"); + write_runtime_ready("util.keep", "value", Calcit::Number(9.0)).expect("seed runtime util.keep/value"); + + clear_runtime_caches_for_reload(Arc::from("app.main"), Arc::from("demo.feature"), false) + .expect("clear runtime caches for reload packages"); + + assert_eq!(lookup_runtime_cell("app.main", "entry"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_runtime_cell("app.extra", "helper"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_runtime_cell("demo.feature", "reload"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_runtime_ready("util.keep", "value"), Some(Calcit::Number(9.0))); + + let compiled = PROGRAM_COMPILED_DATA_STATE.read().expect("read compiled data"); + assert!(!compiled.contains_key("app.main")); + assert!(!compiled.contains_key("app.extra")); + assert!(!compiled.contains_key("demo.feature")); + assert!(compiled.get("util.keep").is_some_and(|file| file.defs.contains_key("value"))); +} + +#[test] +fn clear_runtime_caches_for_reload_with_reload_libs_clears_all_namespaces() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let app_main = ensure_def_id("app.main", "entry"); + let util_keep = ensure_def_id("util.keep", "value"); + + { + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("seed compiled data"); + compiled.insert( + Arc::from("app.main"), + CompiledFileData { + defs: HashMap::from([(Arc::from("entry"), compiled_def_for_test(app_main, vec![]))]), + }, + ); + compiled.insert( + Arc::from("util.keep"), + CompiledFileData { + defs: HashMap::from([(Arc::from("value"), compiled_def_for_test(util_keep, vec![]))]), + }, + ); + } + + write_runtime_ready("app.main", "entry", Calcit::Number(1.0)).expect("seed runtime app.main/entry"); + write_runtime_ready("util.keep", "value", Calcit::Number(9.0)).expect("seed runtime util.keep/value"); + + clear_runtime_caches_for_reload(Arc::from("app.main"), Arc::from("demo.feature"), true) + .expect("clear all runtime caches for reload libs"); + + assert_eq!(lookup_runtime_cell("app.main", "entry"), None); + assert_eq!(lookup_runtime_cell("util.keep", "value"), None); + + let compiled = PROGRAM_COMPILED_DATA_STATE.read().expect("read compiled data"); + assert!(compiled.is_empty()); +} + +#[test] +fn snapshot_rebuilds_changed_source_backed_def_after_reload_changes() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let old_code = code_to_calcit(&Cirru::Leaf(Arc::from("1")), "app.reload", "demo", vec![]).expect("build initial source-backed code"); + + PROGRAM_CODE_DATA.write().expect("seed program code").insert( + Arc::from("app.reload"), + ProgramFileData { + import_map: HashMap::new(), + defs: HashMap::from([( + Arc::from("demo"), + ProgramDefEntry { + code: old_code.clone(), + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + )]), + }, + ); + + let _ = ensure_def_id("app.reload", "demo"); + + store_compiled_output( + "app.reload", + "demo", + CompiledDefPayload { + version_id: 0, + preprocessed_code: Calcit::Number(1.0), + codegen_form: Calcit::Number(1.0), + deps: vec![], + type_summary: None, + source_code: Some(old_code), + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + ); + write_runtime_ready("app.reload", "demo", Calcit::Number(1.0)).expect("seed stale runtime value"); + + let mut changes = snapshot::ChangesDict::default(); + changes.changed.insert( + Arc::from("app.reload"), + snapshot::FileChangeInfo { + ns: None, + added_defs: HashMap::new(), + removed_defs: HashSet::new(), + changed_defs: HashMap::from([(String::from("demo"), Cirru::Leaf(Arc::from("2")))]), + }, + ); + + apply_code_changes(&changes).expect("apply source changes"); + clear_runtime_caches_for_changes(&changes, false).expect("clear runtime caches for source change"); + + assert_eq!(lookup_runtime_cell("app.reload", "demo"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_compiled_def("app.reload", "demo"), None); + + let snapshot = clone_compiled_program_snapshot().expect("clone compiled snapshot after reload changes"); + let rebuilt = snapshot + .get("app.reload") + .and_then(|file| file.defs.get("demo")) + .expect("snapshot should rebuild changed source-backed def"); + + assert_eq!(rebuilt.codegen_form, Calcit::Number(2.0)); + assert_eq!(rebuilt.preprocessed_code, Calcit::Number(2.0)); + assert_eq!(rebuilt.source_code, Some(Calcit::Number(2.0))); +} + #[test] fn snapshot_prefers_source_backed_compiled_def_even_with_warnings() { let _guard = lock_program_test_state(); @@ -385,6 +550,91 @@ fn lookup_codegen_type_hint_falls_back_to_runtime_value() { assert!(matches!(hint.as_ref(), CalcitTypeAnnotation::Number)); } +#[test] +fn lenient_compiled_fallback_does_not_backfill_runtime_cache() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let _ = ensure_def_id("app.compiled", "callable"); + write_runtime_ready("app.compiled", "callable", Calcit::Number(0.0)).expect("seed runtime slot"); + mark_runtime_def_cold("app.compiled", "callable"); + + store_compiled_output( + "app.compiled", + "callable", + CompiledDefPayload { + version_id: 0, + preprocessed_code: Calcit::Number(7.0), + codegen_form: Calcit::Nil, + deps: vec![], + type_summary: None, + source_code: None, + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + ); + + { + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("adjust compiled kind"); + compiled + .get_mut("app.compiled") + .and_then(|file| file.defs.get_mut("callable")) + .expect("compiled callable") + .kind = CompiledDefKind::Fn; + } + + let value = lookup_runtime_or_compiled_def_lenient("app.compiled", "callable"); + assert_eq!(value, Some(Calcit::Number(7.0))); + assert_eq!(lookup_runtime_cell("app.compiled", "callable"), Some(RuntimeCell::Cold)); +} + +#[test] +fn runtime_resolve_mode_handles_resolving_cell_differently() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + mark_runtime_def_resolving("app.runtime", "pending"); + + let strict = resolve_runtime_or_compiled_def( + "app.runtime", + "pending", + None, + RuntimeResolveMode::Strict, + &CallStackList::default(), + ); + assert!(matches!(strict, Err(RuntimeResolveError::RuntimeCell(RuntimeCell::Resolving)))); + + let lenient = resolve_runtime_or_compiled_def( + "app.runtime", + "pending", + None, + RuntimeResolveMode::Lenient, + &CallStackList::default(), + ); + assert_eq!(lenient.expect("lenient resolving lookup"), None); +} + +#[test] +fn runtime_resolve_mode_handles_errored_cell_differently() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + mark_runtime_def_errored("app.runtime", "broken", Arc::from("boom")); + + let strict = resolve_runtime_or_compiled_def("app.runtime", "broken", None, RuntimeResolveMode::Strict, &CallStackList::default()); + assert!(matches!(strict, Err(RuntimeResolveError::RuntimeCell(RuntimeCell::Errored(message))) if message.as_ref() == "boom")); + + let lenient = resolve_runtime_or_compiled_def( + "app.runtime", + "broken", + None, + RuntimeResolveMode::Lenient, + &CallStackList::default(), + ); + assert_eq!(lenient.expect("lenient errored lookup"), None); +} + #[test] fn compiled_executable_code_only_exposes_executable_kinds() { let _guard = lock_program_test_state(); diff --git a/src/runner.rs b/src/runner.rs index 3fb2027d..40db6763 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -35,6 +35,20 @@ fn build_runtime_cell_error(ns: &str, def: &str, call_stack: &CallStackList, cel } } +fn require_symbol_from_program(sym: &str, ns: &str, call_stack: &CallStackList) -> Result { + eval_symbol_from_program(sym, ns, call_stack).map(|value| value.expect("value")) +} + +fn lookup_symbol_in_program_namespaces(sym: &str, file_ns: &str, call_stack: &CallStackList) -> Result, CalcitErr> { + if let Some(value) = eval_symbol_from_program(sym, CORE_NS, call_stack)? { + Ok(Some(value)) + } else if let Some(value) = eval_symbol_from_program(sym, file_ns, call_stack)? { + Ok(Some(value)) + } else { + Ok(None) + } +} + fn resolve_runtime_or_compiled_def( ns: &str, def: &str, @@ -391,10 +405,7 @@ pub fn evaluate_symbol( ) -> Result { let v = match parse_ns_def(sym) { Some((ns_part, def_part)) => match program::lookup_ns_target_in_import(file_ns, &ns_part) { - Some(target_ns) => match eval_symbol_from_program(&def_part, &target_ns, call_stack) { - Ok(v) => Ok(v.expect("value")), - Err(e) => Err(e), - }, + Some(target_ns) => require_symbol_from_program(&def_part, &target_ns, call_stack), None => Err(CalcitErr::use_msg_stack_location( CalcitErrKind::Var, format!("unknown ns target: {ns_part}/{def_part}"), @@ -416,12 +427,10 @@ pub fn evaluate_symbol( Ok(Calcit::Proc(p)) } else if builtins::is_registered_proc(sym) { Ok(Calcit::Registered(sym.into())) - } else if let Some(v) = eval_symbol_from_program(sym, CORE_NS, call_stack)? { - Ok(v) - } else if let Some(v) = eval_symbol_from_program(sym, file_ns, call_stack)? { + } else if let Some(v) = lookup_symbol_in_program_namespaces(sym, file_ns, call_stack)? { Ok(v) } else if let Some(target_ns) = program::lookup_def_target_in_import(file_ns, sym) { - eval_symbol_from_program(sym, &target_ns, call_stack).map(|v| v.expect("value")) + require_symbol_from_program(sym, &target_ns, call_stack) } else { let vars = scope.get_names(); Err(CalcitErr::use_msg_stack_location( @@ -461,20 +470,9 @@ pub fn evaluate_symbol_from_program( call_stack: &CallStackList, ) -> Result { let v0 = resolve_runtime_or_compiled_def(file_ns, sym, def_id.map(program::DefId), call_stack)?; - // if v0.is_none() { - // println!("slow path reading symbol: {}/{}", file_ns, sym) - // } let v = if let Some(v) = v0 { v - } else if let Some(v) = eval_symbol_from_program(sym, CORE_NS, call_stack)? { - v - } else if file_ns == CORE_NS { - if let Some(v) = eval_symbol_from_program(sym, CORE_NS, call_stack)? { - v - } else { - unreachable!("expected symbol from path, this is a quick path, should succeed") - } - } else if let Some(v) = eval_symbol_from_program(sym, file_ns, call_stack)? { + } else if let Some(v) = lookup_symbol_in_program_namespaces(sym, file_ns, call_stack)? { v } else { unreachable!("expected symbol from path, this is a quick path, should succeed") diff --git a/src/runner/preprocess.rs b/src/runner/preprocess.rs index cc953a52..17b149cf 100644 --- a/src/runner/preprocess.rs +++ b/src/runner/preprocess.rs @@ -66,10 +66,6 @@ impl<'a> PreprocessContext<'a> { } } -fn lookup_preprocessed_ns_def_value(ns: &str, def: &str) -> Option { - program::lookup_runtime_or_compiled_def_lenient(ns, def) -} - fn store_preprocessed_compiled_output(ns: &str, def: &str, source_code: &Calcit, resolved_code: &Calcit) { let preprocessed_code = resolved_code.to_owned(); let codegen_form = resolved_code.to_owned(); @@ -108,7 +104,7 @@ fn ensure_ns_def_preprocessed( } } - if lookup_preprocessed_ns_def_value(ns, def).is_some() { + if program::lookup_runtime_or_compiled_def_lenient(ns, def).is_some() { return Ok(()); } @@ -159,7 +155,7 @@ pub fn preprocess_ns_def( call_stack: &CallStackList, ) -> Result, CalcitErr> { ensure_ns_def_preprocessed(raw_ns, raw_def, check_warnings, call_stack)?; - Ok(lookup_preprocessed_ns_def_value(raw_ns, raw_def)) + Ok(program::lookup_runtime_or_compiled_def_lenient(raw_ns, raw_def)) } pub fn compile_source_def_for_snapshot( @@ -659,15 +655,7 @@ fn preprocess_list_call( if let Some(call_head) = ys.first() { warn_on_dynamic_trait_call(call_head, &processed_args, scope_types, file_ns, def_name.as_ref(), check_warnings); - warn_on_method_name_conflict( - call_head, - &processed_args, - scope_types, - file_ns, - def_name.as_ref(), - check_warnings, - call_stack, - ); + warn_on_method_name_conflict(call_head, &processed_args, scope_types, file_ns, def_name.as_ref(), check_warnings); } // Check Proc argument types if available @@ -728,7 +716,7 @@ fn preprocess_list_call( if !has_spread { if let Some(call_head) = ys.first() { - if let Some(optimized_call) = try_inline_method_call(call_head, &processed_args, scope_types, call_stack, file_ns) { + if let Some(optimized_call) = try_inline_method_call(call_head, &processed_args, scope_types, file_ns) { return Ok(optimized_call); } } @@ -1596,7 +1584,7 @@ fn check_record_method_args( } // Get impl records for the type - let Some(impl_records) = get_impl_records_from_type(&type_value, &CallStackList::default()) else { + let Some(impl_records) = get_impl_records_from_type(&type_value) else { return; // No impl record, skip check }; @@ -1817,7 +1805,6 @@ fn warn_on_method_name_conflict( file_ns: &str, def_name: &str, check_warnings: &RefCell>, - call_stack: &CallStackList, ) { if file_ns == calcit::CORE_NS { return; @@ -1839,7 +1826,7 @@ fn warn_on_method_name_conflict( return; }; - let Some(impl_records) = get_impl_records_from_type(type_value.as_ref(), call_stack) else { + let Some(impl_records) = get_impl_records_from_type(type_value.as_ref()) else { return; }; @@ -1902,13 +1889,7 @@ fn warn_on_method_name_conflict( } } -fn try_inline_method_call( - head: &Calcit, - args: &CalcitList, - scope_types: &ScopeTypes, - call_stack: &CallStackList, - file_ns: &str, -) -> Option { +fn try_inline_method_call(head: &Calcit, args: &CalcitList, scope_types: &ScopeTypes, file_ns: &str) -> Option { match head { Calcit::Method(method_name, calcit::MethodKind::Invoke(type_value)) => { let mut resolved_type = type_value.clone(); @@ -1925,7 +1906,7 @@ fn try_inline_method_call( return None; } let type_ref = resolved_type.as_ref(); - let impl_records = get_impl_records_from_type(type_ref, call_stack)?; + let impl_records = get_impl_records_from_type(type_ref)?; let (_impl_index, _impl_record, method_entry) = find_method_entry_with_impl(type_ref, &impl_records, method_name.as_ref())?; if let Some(callable_head) = pick_callable_from_method_entry(method_entry, file_ns) { @@ -2027,7 +2008,7 @@ fn validate_method_call( } // Get impl records for the type - let Some(impl_records) = get_impl_records_from_type(&type_value, call_stack) else { + let Some(impl_records) = get_impl_records_from_type(&type_value) else { return Ok(()); // No impl record, skip validation }; @@ -2794,16 +2775,16 @@ fn resolve_record_value(target: &Calcit, scope_types: &ScopeTypes) -> Option Option>> { +fn collect_impl_records_from_value(value: &Calcit) -> Option>> { let resolve_impl = |value: &Calcit| -> Option { match value { Calcit::Impl(imp) => Some(imp.to_owned()), - Calcit::Import(import) => match runner::evaluate_symbol_from_program(&import.def, &import.ns, import.def_id, call_stack) { - Ok(Calcit::Impl(imp)) => Some(imp), + Calcit::Import(import) => match resolve_program_value_for_preprocess(&import.ns, &import.def, import.def_id) { + Some(Calcit::Impl(imp)) => Some(imp), _ => None, }, - Calcit::Symbol { sym, info, .. } => match runner::evaluate_symbol_from_program(sym, &info.at_ns, None, call_stack) { - Ok(Calcit::Impl(imp)) => Some(imp), + Calcit::Symbol { sym, info, .. } => match resolve_program_value_for_preprocess(&info.at_ns, sym, None) { + Some(Calcit::Impl(imp)) => Some(imp), _ => None, }, _ => None, @@ -2824,7 +2805,7 @@ fn collect_impl_records_from_value(value: &Calcit, call_stack: &CallStackList) - } } -fn get_impl_records_from_type(type_value: &CalcitTypeAnnotation, call_stack: &CallStackList) -> Option>> { +fn get_impl_records_from_type(type_value: &CalcitTypeAnnotation) -> Option>> { if let Some(struct_def) = type_value.as_struct() { return Some(struct_def.impls.to_owned()); } @@ -2842,18 +2823,18 @@ fn get_impl_records_from_type(type_value: &CalcitTypeAnnotation, call_stack: &Ca } if let Some(class_symbol) = core_impl_list_symbol_from_type_annotation(type_value) { - return match runner::evaluate_symbol_from_program(class_symbol, calcit::CORE_NS, None, call_stack) { - Ok(value) => collect_impl_records_from_value(&value, call_stack), - Err(_) => None, + return match resolve_program_value_for_preprocess(calcit::CORE_NS, class_symbol, None) { + Some(value) => collect_impl_records_from_value(&value), + None => None, }; } if let CalcitTypeAnnotation::Custom(value) = type_value { match value.as_ref() { Calcit::Import(import) => { - return match runner::evaluate_symbol_from_program(&import.def, &import.ns, import.def_id, call_stack) { - Ok(value) => collect_impl_records_from_value(&value, call_stack), - Err(_) => None, + return match resolve_program_value_for_preprocess(&import.ns, &import.def, import.def_id) { + Some(value) => collect_impl_records_from_value(&value), + None => None, }; } Calcit::Symbol { sym, info, .. } => { @@ -2861,9 +2842,9 @@ fn get_impl_records_from_type(type_value: &CalcitTypeAnnotation, call_stack: &Ca Some((ns_part, def_part)) => (ns_part, def_part), None => (info.at_ns.to_owned(), sym.to_owned()), }; - return match runner::evaluate_symbol_from_program(&target_def, &target_ns, None, call_stack) { - Ok(value) => collect_impl_records_from_value(&value, call_stack), - Err(_) => None, + return match resolve_program_value_for_preprocess(&target_ns, &target_def, None) { + Some(value) => collect_impl_records_from_value(&value), + None => None, }; } _ => {} From 68e0bb98fdd95a49743236d5c352bf27f349e1e6 Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 17 Mar 2026 20:28:08 +0800 Subject: [PATCH 17/57] refactor: streamline runtime boundary and type hot paths --- .gitignore | 4 +- drafts/runtime-boundary-refactor-plan.md | 81 ++- ... 2026-0309-1753-schema-type-fail-tests.md} | 0 ...=> 2026-0309-1819-add-test-fail-script.md} | 0 ...> 2026-0309-1944-split-type-fail-tests.md} | 0 ...n-runtime-boundary-optimization-summary.md | 73 ++ src/bin/cli_handlers/docs.rs | 6 +- src/bin/cr.rs | 12 +- src/bin/cr_tests/type_fail.rs | 4 +- src/calcit/type_annotation.rs | 86 ++- src/lib.rs | 15 +- src/program.rs | 186 ++--- src/program/tests.rs | 491 +++++++++++--- src/runner.rs | 3 +- src/runner/preprocess.rs | 634 ++++++++++++------ 15 files changed, 1147 insertions(+), 448 deletions(-) rename editing-history/{202603091753-schema-type-fail-tests.md => 2026-0309-1753-schema-type-fail-tests.md} (100%) rename editing-history/{202603091819-add-test-fail-script.md => 2026-0309-1819-add-test-fail-script.md} (100%) rename editing-history/{202603091944-split-type-fail-tests.md => 2026-0309-1944-split-type-fail-tests.md} (100%) create mode 100644 editing-history/2026-0317-2026-afternoon-runtime-boundary-optimization-summary.md diff --git a/.gitignore b/.gitignore index 24d71199..9a022ebc 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ demos/ .calcit-snippets/ -.tmp-profiles/ \ No newline at end of file +.tmp-profiles/ + +profiling/*.gz \ No newline at end of file diff --git a/drafts/runtime-boundary-refactor-plan.md b/drafts/runtime-boundary-refactor-plan.md index c8bee1f6..7eef4608 100644 --- a/drafts/runtime-boundary-refactor-plan.md +++ b/drafts/runtime-boundary-refactor-plan.md @@ -55,6 +55,38 @@ 换句话说,当前阶段要继续的是“收尾”,不是“扩编”。 +## 当前唯一目标 + +当前不再追求把文档里的四层模型一比一做完,真正执行时只守下面 4 条边界: + +1. `preprocess == compile`:只负责确保 `CompiledDef` 存在,不再承担“顺手返回一个可运行值”的通用职责。 +2. `run == runtime`:只负责从 `RuntimeCell`/compiled executable payload 取运行值,不再替 metadata/codegen 补结构。 +3. `reload == invalidation`:只负责 source change apply 与 `DefId` 依赖闭包失效,不再维护额外 package 级兼容语义。 +4. `codegen == compiled snapshot`:只读取 compiled snapshot;任何 runtime fallback 都视为过渡期兼容,而不是长期设计。 + +这 4 条如果站稳,后续性能优化才会变得局部且可解释: + +- preprocess 的性能只看编译与依赖收集; +- run 的性能只看 `DefId` 索引、runtime lock 和求值; +- reload 的性能只看变更种子与反向依赖图; +- codegen 的性能只看 compiled snapshot 构造与 emitter。 + +## 当前不做 + +为了保证进度,这一阶段明确不做: + +- 不引入新的长期层级实体来“补完”理论模型; +- 不把 `PersistentStateLayer` / `StateSlotId` 推进成当前实现目标; +- 不再扩张新的 compiled/runtime 双向桥接 helper; +- 不为了照顾少量旧路径,继续保留“preprocess 顺手执行、runtime 顺手提供 codegen 结构”这种混合语义。 + +## 当前收官清单 + +- `[已基本完成]` 把 `preprocess` 公开入口收敛为 `ensure_ns_def_compiled()`,并把只需要 ensure 的调用点迁走。 +- `[进行中]` 继续缩小 runtime-derived snapshot fallback,直到它只剩明确、可解释的过渡语义,或者可以被完全删除。 +- `[待继续]` 补齐 watch/reload 回归测试,特别是 changed defs、ns header、removed defs、依赖闭包 invalidation。 +- `[待继续]` 清掉迁移期命名和重复 lookup helper,让 `program` 成为唯一边界聚合点。 + ## 当前进度 截至 2026-03-17,已经完成的不是“纯设计”,而是一部分边界已经落地: @@ -68,37 +100,53 @@ - `RuntimeCell` 的最小状态机已经落下,包含 `Cold | Resolving | Ready | Errored`,且 preprocess 的循环保护已从“先写 `Nil`”切到显式 `Resolving`; - JS codegen 现在会显式跳过 core 中仅由 runtime 提供的 placeholder 定义,以及 `syntax`/`proc` 这类本就不应按普通顶层值发射的定义;这也移除了 `calcit.core.mjs` 中形如 `eval = &runtime-inplementation` 的伪导出; - `clone_compiled_program_snapshot()` 已开始按“仅收集缺口定义”的两阶段方式补齐 snapshot,而不是先整表 clone source/index/runtime 全局状态后再筛选; -- runtime-derived snapshot fallback 现在进一步收窄为“仅对 runtime-only defs 生效”;只要 source-backed def 仍存在,就不会再因为旧 runtime 值而静默补出 snapshot entry; +- runtime-derived snapshot fallback 现在进一步收窄为“仅对被现有 compiled graph 实际引用到的 runtime-only defs 生效”;只要 source-backed def 仍存在,或 runtime-only def 只是未被引用的残留 runtime 值,就不会再因为旧 runtime 值而静默补出 snapshot entry; +- runtime-only snapshot fallback 也不再原样嵌入任意 `RuntimeCell::Ready` 值;当前会先把 runtime 值转成可快照的 Calcit 数据/代码形式,像 `ref` / `buffer` / `any-ref` / 运行时函数句柄这类本就不稳定的 runtime-only 值将直接被跳过; - runtime-only snapshot fallback 已不再携带 source/schema/doc/examples 这类 source 元数据,snapshot 填充任务本身也不再为 fallback 路径 clone 整个 `ProgramDefEntry`; +- snapshot 补缺现在也只会在真正拿到 compiled def / runtime-only fallback 时才创建 namespace entry;source-backed rebuild 失败不再留下空壳 compiled file; - `seed_runtime_lazy_from_compiled()`、`lookup_compiled_runtime_value()`、`lookup_codegen_type_hint()` 已开始按需读取 compiled 字段,而不是在热路径上先 clone 整份 `CompiledDef`; - `runner`/`lib`/`preprocess` 主调用方已经迁到“先取 compiled executable payload,再按需求值”的边界;旧的 `lookup_compiled_runtime_value()` 兼容包装已删除。 - IR/codegen type-hint 查询已不再通过执行 compiled payload 来补信息;metadata 查询现在只依赖 compiled/source schema 与现成 runtime 值。 - runtime symbol lookup 已不再假设 compiled 执行会回填 runtime cache;执行后的二次 runtime reread 兼容分支已移除。 - `preprocess` 读取已编译定义时,lazy def 现在优先经由 `RuntimeCell::Lazy` 求值,不再绕过 runtime 状态机直接执行 compiled payload。 -- `run_program_with_docs` 已直接复用 `preprocess_ns_def()` 返回的入口值,不再在入口初始化后额外单独走一次 compiled 执行桥接。 +- `run_program_with_docs` 已切到 `ensure_ns_def_compiled() + evaluate_symbol_from_program()`,不再依赖 `preprocess` 返回入口值。 - `runner` 内部两处“runtime cell -> compiled executable fallback”逻辑已合并到统一 helper,减少了边界复制和不一致分支。 - `preprocess` 的宽松读取路径也已并到同一组 helper,不再单独复制一份 `RuntimeCell::Lazy`/compiled fallback 分支。 - 默认 scope 下的 thunk 求值入口也已收敛到 `CalcitThunk::evaluated_default()`,减少 runtime/lazy 入口上的样板逻辑。 - runtime cell 与 compiled executable fallback 的统一入口现已下沉到 `program` 层;`runner` 只保留 runtime state 到用户态错误的映射。 - `evaluate_compiled_def()` 现已退回 `program` 内部私有 helper;compiled payload 执行不再作为跨模块公共入口暴露。 -- `preprocess` 的 lenient lookup 也已直接回到 `program` 层,不再经由 `runner` 做一次中转;compiled executable code lookup 同时收紧为 `program` 内部 helper。 +- `preprocess` 已不再依赖 lenient runtime/compiled probe 作为前置判断;预处理阶段现在只检查 runtime cell / compiled metadata,读取已编译定义值则走专用 helper,不再复用通用 lookup 包装。 - compiled output 写入接口已改为 payload struct 传参,`program` 内部构造链不再依赖 11 参数长调用;现有 clippy `too_many_arguments` 噪音已被顺手收掉。 - `resolve_runtime_or_compiled_def()` 现在只负责调度;runtime cell 求值与 compiled payload 执行已拆成独立私有 helper,内部边界更接近最终形态。 - runtime-only 路径中的 `seed + lookup cell + resolve cell` 已继续收敛成单独 helper,`resolve_runtime_or_compiled_def()` 现在更明确地只做 `runtime or compiled` 两段调度。 - compiled execution 热路径已改为直接借用 compiled payload;执行时不再额外 clone 一份 `preprocessed_code`,只有测试/显式查询 executable code 时才保留复制语义。 +- 仅剩测试使用的 `lookup_runtime_or_compiled_def_lenient()` 兼容包装也已删除;lenient 语义现在直接通过 `resolve_runtime_or_compiled_def(..., RuntimeResolveMode::Lenient, ...)` 覆盖。 +- `clear_runtime_caches_for_reload()` 的兼容入口已改为先生成 package 范围的伪 `ns` 变更,再统一复用 `clear_runtime_caches_for_changes()` 的依赖闭包 invalidation,而不再维护独立的 package 扫描清理逻辑。 - `yarn check-all` 已经作为当前重构的主验证门槛,并且当前门槛重新保持可通过。 +- 当前阶段的固定门槛 `cargo fmt && cargo test -q && yarn check-all` 已重新可通过。 同时也要明确,当前还没有真正完成的部分是: - 旧的 `PROGRAM_EVALED_DATA_STATE` 与 `lookup_evaled_def*` 兼容路径已经删除,`preprocess` 成功路径也不再直接写 runtime cache;compiled fallback 现在只做“读 compiled value”,不再把 compiled payload 回填成 `RuntimeCell::Ready`; -- 全局 `CompiledDef` 已不再携带 `runtime_value` 这类 runtime payload 字段;普通 preprocess 输出已经完全不构造 runtime payload,普通 compiled `Fn/Macro/Proc/Syntax/LazyValue` 都改为需要时从 `preprocessed_code` 临时 materialize;当前剩余耦合主要集中在 runtime 执行路径仍会把 compiled executable payload materialize 成公共 `Calcit` 值,而 metadata / codegen 查询已基本不再走这条路;snapshot fallback 已不再对 source-backed defs 静默兜底。 +- 全局 `CompiledDef` 已不再携带 `runtime_value` 这类 runtime payload 字段;普通 preprocess 输出已经完全不构造 runtime payload,普通 compiled `Fn/Macro/Proc/Syntax/LazyValue` 都改为需要时从 `preprocessed_code` 临时 materialize;当前剩余耦合主要集中在 runtime 执行路径仍会把 compiled executable payload materialize 成公共 `Calcit` 值,而 metadata / codegen 查询已基本不再走这条路;snapshot fallback 已不再对 source-backed defs 静默兜底,也不再为失败的 source-backed rebuild 保留空壳 namespace,未被 compiled graph 引用的 runtime-only 残留值不会再进入 snapshot,而且即便被引用,像 `ref` / `any-ref` 这类不可稳定序列化的 runtime-only 值也会被跳过。 - `Calcit::Thunk` 仍存在于公开值模型中,但 runtime 主路径已经不再依赖它作为缓存载体:lazy def 的待求值占位优先放进 `RuntimeCell::Lazy`,`Ready(Thunk)` 已被禁止写入 runtime store,preprocess 与普通 lookup 也不再把 runtime lazy cell 重新包装成公共 thunk;当前剩余问题主要转向 snapshot fallback 与少量兼容语义分支; -- watch 模式已经开始利用 compiled deps 做 def 级 invalidation;当前 CLI incremental reload 不再默认按 package 整片清空,而是从 changed defs / ns 头部变更出发做依赖闭包清理。剩余缺口主要是 state slot 尚未引入,以及 watch/reload 回归覆盖还不够强。 +- watch 模式已经开始利用 compiled deps 做 def 级 invalidation;CLI incremental reload 主路径与 reload 兼容入口现在都已统一复用 changes-based 依赖闭包清理,而不再维护一套独立 package 清理逻辑。剩余缺口主要是 state slot 尚未引入,以及 watch/reload 回归覆盖还可以继续加密。 这意味着当前状态更准确的表述是: **Phase 1 和 Phase 2 已经实装,Phase 3A/3B 已经稳定运行,Phase 3D 的主路径删除也已完成;剩余重点转向 3C 以及 preprocess/runtime 的最终拆边。** +## 基于最近 samply 的现状补充(2026-03-17) + +已使用 `profiling/samply-once.sh` 对 `calcit/test.cirru` 与 `calcit/fibo.cirru` 进行 release 采样,当前热点特征: + +- `program::materialize_compiled_executable_payload` 仍在热路径出现,说明 runtime 对 compiled payload 的过渡 materialize 仍有收缩空间; +- `CalcitTypeAnnotation::extract_schema_value/schema_key_matches_any` 频繁命中,说明 hint/schema 解析与匹配仍是可优化热点; +- `CallStackList::extend_owned` 在 fibo 采样中有可见占比,说明运行态栈构造仍可能偏积极; +- 采样中可见 `serde_json` 热点,主要来自 profiling/输出链路,不应直接等同于 steady-state runtime 核心开销。 + +结论:边界拆分方向正确,但“runtime materialize 收缩 + type annotation 热点收敛”已经进入可以直接动手优化的阶段。 + ## 目标边界模型 建议保留这套分层模型作为**分析框架**,而不是要求实现层面继续一比一落四层实体。 @@ -294,7 +342,7 @@ 这里需要特别澄清一件事: -当前 `preprocess` 已经开始产出 compiled metadata,成功路径也已经不再直接写 runtime cache;runtime 也不再在 `Cold` 状态下把 compiled payload 回填成 `RuntimeCell::Ready`。对于 lazy def,当前只会先把 compiled metadata 重新播种成 `RuntimeCell::Lazy`,而不是直接伪装成 ready runtime value。这说明边界已经继续前进一步,但还没有完全完成去同构。最终目标依然是: +当前 `preprocess` 已经开始产出 compiled metadata,成功路径也已经不再直接写 runtime cache;预处理前置检查也不再通过 lenient runtime/compiled probe 侧向触发执行路径。runtime 也不再在 `Cold` 状态下把 compiled payload 回填成 `RuntimeCell::Ready`。对于 lazy def,当前只会先把 compiled metadata 重新播种成 `RuntimeCell::Lazy`,而不是直接伪装成 ready runtime value。这说明边界已经继续前进一步,但还没有完全完成去同构。最终目标依然是: - `preprocess` 负责产出 `CompiledDef`; - runtime 在真正需要值时,才从 compiled payload 驱动求值; @@ -396,7 +444,7 @@ - 但 runtime 内部缓存与 lazy 状态优先放进 `RuntimeCell`; - 减少 thunk 对全局写回和 code 表示的承担。 -当前状态:主路径已基本收尾。thunk 仍是公开 `Calcit` 值模型的一部分,但 runtime store 已不再接受 `Ready(Thunk)` 这类形态;lazy def 的未求值占位优先放进 `RuntimeCell::Lazy`,raw fallback 若得到 `Calcit::Thunk(Code)` 也会立刻规范化回 lazy cell。`eval_symbol_from_program` 也不再把 lazy thunk 返回给调用方,preprocess 查值路径同样不再把 runtime lazy cell 重新包装成公共 thunk。JS codegen 还额外收掉了一层旧桥接:core 中由 runtime 提供的 placeholder 定义、以及 syntax/proc 名称,不再伪装成普通 JS 顶层导出。当前剩余工作主要不再是 thunk 主路径,而是继续压缩 snapshot fallback compiled entry 的存在范围,清理少量旧命名/提示语残留,并补齐更直接的 watch/reload 回归测试。 +当前状态:主路径已基本收尾。thunk 仍是公开 `Calcit` 值模型的一部分,但 runtime store 已不再接受 `Ready(Thunk)` 这类形态;lazy def 的未求值占位优先放进 `RuntimeCell::Lazy`,raw fallback 若得到 `Calcit::Thunk(Code)` 也会立刻规范化回 lazy cell。`eval_symbol_from_program` 也不再把 lazy thunk 返回给调用方,preprocess 查值路径同样不再把 runtime lazy cell 重新包装成公共 thunk,且预处理前置检查已不再通过 lenient lookup 间接复用执行路径。JS codegen 还额外收掉了一层旧桥接:core 中由 runtime 提供的 placeholder 定义、以及 syntax/proc 名称,不再伪装成普通 JS 顶层导出。当前剩余工作主要不再是 thunk 主路径,而是继续压缩 snapshot fallback compiled entry 的存在范围,并视需要继续补 watch/reload 回归测试。 #### Phase 3D: 删除旧 EntryBook 热路径依赖 @@ -404,7 +452,7 @@ - `PROGRAM_EVALED_DATA_STATE` 从主 runtime store 降级为兼容层或被删除; - watch/reload 改为直接操作 runtime cell table。 -当前状态:主体已完成。`coord -> EntryBook` 主快路径已经删除,`PROGRAM_EVALED_DATA_STATE` 也已删除;watch/reload 也已经开始直接基于 `DefId` 与 compiled deps 做依赖闭包失效。剩余问题不再是“还停留在 package 级清理”,而是要把剩余边界情况和回归测试补齐,并继续把状态保留语义从值缓存里彻底拆出去。 +当前状态:主体已完成。`coord -> EntryBook` 主快路径已经删除,`PROGRAM_EVALED_DATA_STATE` 也已删除;watch/reload 主路径与兼容 reload 入口也都已经统一基于 `DefId` 与 compiled deps 做依赖闭包失效。剩余问题不再是“还停留在 package 级清理”,而是要把剩余边界情况和回归测试补齐,并继续把状态保留语义从值缓存里彻底拆出去。 ### Phase 4: 引入 `PersistentStateLayer` @@ -438,10 +486,11 @@ 下一步不再是“补完大设计”,而是按下面顺序收官: -1. 继续收缩 runtime-derived snapshot fallback,只保留真正 runtime-only defs 需要的兜底;source-backed defs 一律优先走 compiled/source 数据。 -2. 补齐 watch/reload 回归测试,重点覆盖 changed defs、ns header、依赖闭包 invalidation,以及 snapshot fallback 不误补 source-backed defs。 -3. 清理还停留在迁移期的 helper、命名和双份 lookup 分支,让 `program` 成为唯一边界聚合点。 -4. 在上述三步稳定之后,再重新评估是否还需要继续压缩公开 thunk 语义,或是否真的存在引入 state slot 的必要。 +1. 先做热点导向减法:继续收缩 `materialize_compiled_executable_payload` 的热路径触发频率,减少 runtime 侧重复 materialize。 +2. 再收敛类型系统热路径:优先减少 `hint/schema` 解析与匹配的重复工作(缓存、去重分支、减少中间分配)。 +3. 继续收缩 runtime-derived snapshot fallback,只保留真正 runtime-only defs 需要的兜底;source-backed defs 一律优先走 compiled/source 数据。 +4. 补齐 watch/reload 回归测试,重点覆盖 changed defs、ns header、removed defs、依赖闭包 invalidation,以及 snapshot fallback 不误补 source-backed defs。 +5. 清理还停留在迁移期的 helper、命名和双份 lookup 分支,让 `program` 成为唯一边界聚合点。 换句话说,下一步的目标是: @@ -478,13 +527,13 @@ 如果只做一个高 ROI 的下一步动作,建议先做: -**先补强 watch/reload 回归测试,并用这些测试倒逼继续收缩 snapshot/runtime fallback。** +**先基于 samply 热点收缩 runtime materialize 触发频率,并配套保留 `yarn check-all` + `cargo test` 的语义门槛。** 理由: -- 这是当前最能验证边界是否真的站稳的手段; -- 这会直接暴露哪些 fallback 仍然只是迁移期补丁; -- 这比继续设计新层更符合“结构清晰优先”的目标。 +- 这是当前最直接、可量化的性能收益来源; +- 这与“run == runtime”边界固化目标一致,不会引入新实体; +- 在现有测试门槛下可快速验证语义不回退。 ## 最终判断 diff --git a/editing-history/202603091753-schema-type-fail-tests.md b/editing-history/2026-0309-1753-schema-type-fail-tests.md similarity index 100% rename from editing-history/202603091753-schema-type-fail-tests.md rename to editing-history/2026-0309-1753-schema-type-fail-tests.md diff --git a/editing-history/202603091819-add-test-fail-script.md b/editing-history/2026-0309-1819-add-test-fail-script.md similarity index 100% rename from editing-history/202603091819-add-test-fail-script.md rename to editing-history/2026-0309-1819-add-test-fail-script.md diff --git a/editing-history/202603091944-split-type-fail-tests.md b/editing-history/2026-0309-1944-split-type-fail-tests.md similarity index 100% rename from editing-history/202603091944-split-type-fail-tests.md rename to editing-history/2026-0309-1944-split-type-fail-tests.md diff --git a/editing-history/2026-0317-2026-afternoon-runtime-boundary-optimization-summary.md b/editing-history/2026-0317-2026-afternoon-runtime-boundary-optimization-summary.md new file mode 100644 index 00000000..01677da8 --- /dev/null +++ b/editing-history/2026-0317-2026-afternoon-runtime-boundary-optimization-summary.md @@ -0,0 +1,73 @@ +# 2026-03-17 下午至晚间改动总结(1739-2023) + +## 总览 + +- 时间段:`2026-0317-1739` 至 `2026-0317-2023` +- 主要改动:测试减法、program/preprocess 架构减法、类型注解热路径优化、samply 验证、计划文档同步 + +## 关键变更(按时间顺序) + +### 1) 测试减法(1739 / 1745) + +- 清理 `src/program/tests.rs` 中 helper/重复语义测试: + - `runtime_snapshot_fallback_only_allows_runtime_only_defs` + - `preprocess_ns_def_accepts_compiled_only_value_without_source_lookup` +- 保留行为级回归用例,继续覆盖 snapshot/runtime-only 与 compiled-only 消费边界。 + +### 2) preprocess 推断逻辑去重(1751) + +- 文件:`src/runner/preprocess.rs` +- 抽取 `infer_return_type_from_compiled_callable(...)`,统一 `Import`/`Symbol` 分支的 compiled callable 返回类型推断。 +- 保留 `Symbol` 分支对源码 tag 的回退解析行为。 + +### 3) program snapshot helper 内联(1753) + +- 文件:`src/program.rs` +- 删除并内联单次用途 helper: + - `collect_referenced_compiled_def_ids(...)` + - `should_use_runtime_snapshot_fallback(...)` +- 逻辑并入 `collect_snapshot_fill_tasks(...)` 与 `build_snapshot_fill_compiled_def(...)`。 + +### 4) 文档刷新 + 首轮热路径减法(1812) + +- 文件:`drafts/runtime-boundary-refactor-plan.md` + - 修正过时描述:`run_program_with_docs` 现状与 preprocess 返回值关系。 + - 将阶段状态与 `samply` 观察、下一步优先级对齐。 +- 文件:`src/runner/preprocess.rs` + - 去除 `drop_left` 中间列表分配。 + - `resolve_generic_return_type` 改为接收迭代器,调用处直接 `iter().skip(1)`。 + +### 5) materialize executable fast path(2012) + +- 文件:`src/program.rs` +- `materialize_compiled_executable_payload(...)`: + - `Proc | Syntax` 直接返回 `preprocessed_code`。 + - `Fn | Macro` 继续 `evaluate_expr` materialize。 + - `LazyValue | Value` 继续保持不可执行语义。 +- 删除无调用 helper:`with_compiled_executable_payload(...)`。 + +### 6) type-annotation 单次扫描收敛(2023) + +- 文件:`src/calcit/type_annotation.rs` +- 在 `parse_fn_annotation_from_schema_form` 中引入 `collect_fn_schema_fields`,由多次 key 扫描改为一次遍历收集。 +- 删除无用 helper:`schema_has_any_field`。 + +## 验证汇总 + +- 多轮定向测试均通过: + - `cargo test -q program::tests` + - `cargo test -q runner::preprocess::tests` + - `cargo test -q calcit::type_annotation::tests` +- 全量 Rust 测试持续通过:`cargo test -q`(会话末保持全绿) +- 语义门禁持续通过:`yarn check-all` + +## profiling 结论汇总 + +- 使用既有流程:`profiling/samply-once.sh` + `profiling/samply-summary.py` +- 在 materialize 目标链路过滤中,样本权重由 14 降至 7(单轮观测,方向符合预期)。 +- `type_annotation::*` 热点仍可见,后续应继续压缩 schema key 匹配/提取路径分配与分支成本。 + +## 本轮经验 + +- 以“删 helper/删重复分支/删中间分配”为主线做减法,优先保证行为级测试覆盖。 +- 每次改动后固定执行“定向测试 → 全量 Rust → `yarn check-all`”可有效阻断语义回退。 \ No newline at end of file diff --git a/src/bin/cli_handlers/docs.rs b/src/bin/cli_handlers/docs.rs index 7351c905..2dd926b5 100644 --- a/src/bin/cli_handlers/docs.rs +++ b/src/bin/cli_handlers/docs.rs @@ -578,7 +578,7 @@ fn prepare_program_for_snippet(shared_files: &HashMap> = &RefCell::new(vec![]); - runner::preprocess::preprocess_ns_def( + runner::preprocess::ensure_ns_def_compiled( calcit::calcit::CORE_NS, calcit::calcit::BUILTIN_IMPLS_ENTRY, check_warnings, @@ -592,10 +592,10 @@ fn prepare_program_for_snippet(shared_files: &HashMap Result<(), String> { let check_warnings: &RefCell> = &RefCell::new(vec![]); - runner::preprocess::preprocess_ns_def(&entries.init_ns, &entries.init_def, check_warnings, &CallStackList::default()) + runner::preprocess::ensure_ns_def_compiled(&entries.init_ns, &entries.init_def, check_warnings, &CallStackList::default()) .map_err(|failure| failure.msg)?; - runner::preprocess::preprocess_ns_def(&entries.reload_ns, &entries.reload_def, check_warnings, &CallStackList::default()) + runner::preprocess::ensure_ns_def_compiled(&entries.reload_ns, &entries.reload_def, check_warnings, &CallStackList::default()) .map_err(|failure| failure.msg)?; let warnings = check_warnings.borrow(); diff --git a/src/bin/cr.rs b/src/bin/cr.rs index ec6c01e1..dd8b10e6 100644 --- a/src/bin/cr.rs +++ b/src/bin/cr.rs @@ -197,7 +197,7 @@ fn main() -> Result<(), String> { runner::preprocess::set_warn_dyn_method(cli_args.warn_dyn_method); // make sure builtin classes are touched - runner::preprocess::preprocess_ns_def( + runner::preprocess::ensure_ns_def_compiled( calcit::calcit::CORE_NS, calcit::calcit::BUILTIN_IMPLS_ENTRY, check_warnings, @@ -415,7 +415,7 @@ fn recall_program(content: &str, entries: &ProgramEntries, settings: &ToplevelCa // when there's services, make sure their code get preprocessed too let check_warnings: &RefCell> = &RefCell::new(vec![]); if let Err(e) = - runner::preprocess::preprocess_ns_def(&entries.init_ns, &entries.init_def, check_warnings, &CallStackList::default()) + runner::preprocess::ensure_ns_def_compiled(&entries.init_ns, &entries.init_def, check_warnings, &CallStackList::default()) { return Err(e.to_string()); } @@ -450,7 +450,7 @@ fn run_check_only(entries: &ProgramEntries) -> Result<(), String> { eprintln!("{}", "Check-only mode: validating code...".dimmed()); // preprocess init_fn - match runner::preprocess::preprocess_ns_def(&entries.init_ns, &entries.init_def, check_warnings, &CallStackList::default()) { + match runner::preprocess::ensure_ns_def_compiled(&entries.init_ns, &entries.init_def, check_warnings, &CallStackList::default()) { Ok(_) => { println!(" {} {}", "✓".green(), format!("{} preprocessed", entries.init_fn).dimmed()); } @@ -463,7 +463,7 @@ fn run_check_only(entries: &ProgramEntries) -> Result<(), String> { } // preprocess reload_fn - match runner::preprocess::preprocess_ns_def(&entries.reload_ns, &entries.reload_def, check_warnings, &CallStackList::default()) { + match runner::preprocess::ensure_ns_def_compiled(&entries.reload_ns, &entries.reload_def, check_warnings, &CallStackList::default()) { Ok(_) => { println!(" {} {}", "✓".green(), format!("{} preprocessed", entries.reload_fn).dimmed()); } @@ -514,7 +514,7 @@ fn run_codegen(entries: &ProgramEntries, emit_path: &str, ir_mode: bool) -> Resu gen_stack::clear_stack(); // preprocess to init - match runner::preprocess::preprocess_ns_def(&entries.init_ns, &entries.init_def, check_warnings, &CallStackList::default()) { + match runner::preprocess::ensure_ns_def_compiled(&entries.init_ns, &entries.init_def, check_warnings, &CallStackList::default()) { Ok(_) => (), Err(failure) => { eprintln!("\nfailed preprocessing, {failure}"); @@ -530,7 +530,7 @@ fn run_codegen(entries: &ProgramEntries, emit_path: &str, ir_mode: bool) -> Resu } // preprocess to reload - match runner::preprocess::preprocess_ns_def(&entries.reload_ns, &entries.reload_def, check_warnings, &CallStackList::default()) { + match runner::preprocess::ensure_ns_def_compiled(&entries.reload_ns, &entries.reload_def, check_warnings, &CallStackList::default()) { Ok(_) => (), Err(failure) => { eprintln!("\nfailed preprocessing, {failure}"); diff --git a/src/bin/cr_tests/type_fail.rs b/src/bin/cr_tests/type_fail.rs index 098f7357..3109383e 100644 --- a/src/bin/cr_tests/type_fail.rs +++ b/src/bin/cr_tests/type_fail.rs @@ -30,7 +30,7 @@ fn load_fixture_entries(path: &str) -> ProgramEntries { program::clear_runtime_caches_for_reload(init_ns.clone().into(), reload_ns.clone().into(), true).expect("clear runtime caches"); let warmup_warnings: RefCell> = RefCell::new(vec![]); - runner::preprocess::preprocess_ns_def( + runner::preprocess::ensure_ns_def_compiled( calcit::calcit::CORE_NS, calcit::calcit::BUILTIN_IMPLS_ENTRY, &warmup_warnings, @@ -93,7 +93,7 @@ fn type_fail_call_arg_fixture_reports_warning_code() { let entries = load_fixture_entries("calcit/type-fail/schema-call-arg-type-mismatch.cirru"); let warnings: RefCell> = RefCell::new(vec![]); - runner::preprocess::preprocess_ns_def(&entries.init_ns, &entries.init_def, &warnings, &CallStackList::default()) + runner::preprocess::ensure_ns_def_compiled(&entries.init_ns, &entries.init_def, &warnings, &CallStackList::default()) .expect("call-arg fixture should preprocess with warnings, not hard errors"); let warnings = warnings.borrow(); diff --git a/src/calcit/type_annotation.rs b/src/calcit/type_annotation.rs index 167a2725..142dc1ed 100644 --- a/src/calcit/type_annotation.rs +++ b/src/calcit/type_annotation.rs @@ -417,22 +417,80 @@ impl CalcitTypeAnnotation { } } - fn schema_has_any_field(form: &Calcit, keys: &[&str]) -> bool { + fn collect_fn_schema_fields<'a>(form: &'a Calcit) -> (bool, Option<&'a Calcit>, Option<&'a Calcit>, Option<&'a Calcit>, Option<&'a Calcit>, Option<&'a Calcit>) { + let mut has_any = false; + let mut generics = None; + let mut args = None; + let mut returns = None; + let mut rest = None; + let mut kind = None; + + let mut visit_pair = |key: &'a Calcit, value: &'a Calcit| { + let Some(key_name) = Self::schema_key_name(key) else { + return; + }; + match key_name { + "generics" => { + has_any = true; + if generics.is_none() { + generics = Some(value); + } + } + "args" => { + has_any = true; + if args.is_none() { + args = Some(value); + } + } + "return" => { + has_any = true; + if returns.is_none() { + returns = Some(value); + } + } + "rest" => { + has_any = true; + if rest.is_none() { + rest = Some(value); + } + } + "kind" => { + has_any = true; + if kind.is_none() { + kind = Some(value); + } + } + _ => {} + } + }; + match form { - Calcit::Map(xs) => xs.iter().any(|(key, _)| Self::schema_key_matches_any(key, keys)), + Calcit::Map(xs) => { + for (key, value) in xs { + visit_pair(key, value); + } + } Calcit::List(xs) => { if !matches!(xs.first(), Some(head) if Self::is_schema_map_literal_head(head)) { - return false; + return (false, None, None, None, None, None); } - xs.iter().skip(1).any(|entry| { + for entry in xs.iter().skip(1) { let Calcit::List(pair) = entry else { - return false; + continue; }; - pair.first().is_some_and(|key| Self::schema_key_matches_any(key, keys)) - }) + let Some(key) = pair.get(0) else { + continue; + }; + let Some(value) = pair.get(1) else { + continue; + }; + visit_pair(key, value); + } } - _ => false, + _ => return (false, None, None, None, None, None), } + + (has_any, generics, args, returns, rest, kind) } pub fn extract_return_type_from_hint_form(form: &Calcit) -> Option> { @@ -559,24 +617,24 @@ impl CalcitTypeAnnotation { generics: &[Arc], strict_named_refs: bool, ) -> Option> { - let has_schema_fields = Self::schema_has_any_field(form, &["args", "return", "generics", "rest", "kind"]); + let (has_schema_fields, local_generics_form, args_form, return_form, rest_form, kind_form) = Self::collect_fn_schema_fields(form); if !has_schema_fields { return Self::infer_malformed_fn_schema(form, generics, strict_named_refs); } - let local_generics = Self::extract_schema_value(form, &["generics"]) + let local_generics = local_generics_form .and_then(Self::parse_generics_list) .unwrap_or_default(); let scope = Self::extend_generics_scope(generics, local_generics.as_slice()); - let arg_types = Self::extract_schema_value(form, &["args"]) + let arg_types = args_form .map(|args_form| Self::parse_schema_args_list(args_form, scope.as_slice(), strict_named_refs)) .unwrap_or_default(); - let return_type = Self::extract_schema_value(form, &["return"]) + let return_type = return_form .map(|item| Self::parse_type_annotation_form_inner(item, scope.as_slice(), strict_named_refs)) .unwrap_or_else(|| Arc::new(Self::Dynamic)); - let rest_type = Self::extract_schema_value(form, &["rest"]) + let rest_type = rest_form .map(|item| Self::parse_type_annotation_form_inner(item, scope.as_slice(), strict_named_refs)); - let fn_kind = match Self::extract_schema_value(form, &["kind"]) { + let fn_kind = match kind_form { Some(Calcit::Tag(tag)) if tag.ref_str() == "macro" => SchemaKind::Macro, Some(Calcit::Symbol { sym, .. }) if matches!(sym.as_ref(), ":macro" | "macro") => SchemaKind::Macro, _ => SchemaKind::Fn, diff --git a/src/lib.rs b/src/lib.rs index 974af857..51b42a0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,9 +55,8 @@ pub fn run_program(init_ns: Arc, init_def: Arc, params: &[Calcit]) -> pub fn run_program_with_docs(init_ns: Arc, init_def: Arc, params: &[Calcit]) -> Result { let check_warnings = RefCell::new(LocatedWarning::default_list()); - // preprocess to init - let init_entry = match runner::preprocess::preprocess_ns_def(&init_ns, &init_def, &check_warnings, &CallStackList::default()) { - Ok(entry) => entry, + match runner::preprocess::ensure_ns_def_compiled(&init_ns, &init_def, &check_warnings, &CallStackList::default()) { + Ok(()) => {} Err(failure) => { eprintln!("\nfailed preprocessing, {failure}"); let headline = failure.headline(); @@ -78,9 +77,9 @@ pub fn run_program_with_docs(init_ns: Arc, init_def: Arc, params: &[Ca hint: None, }); } - match init_entry.or_else(|| program::lookup_runtime_ready(&init_ns, &init_def)) { - None => CalcitErr::err_str(CalcitErrKind::Var, format!("entry not initialized: {init_ns}/{init_def}")), - Some(entry) => match entry { + + match runner::evaluate_symbol_from_program(&init_def, &init_ns, None, &CallStackList::default()) { + Ok(entry) => match entry { Calcit::Fn { info, .. } => { let result = runner::run_fn(params, &info, &CallStackList::default()); match result { @@ -93,6 +92,10 @@ pub fn run_program_with_docs(init_ns: Arc, init_def: Arc, params: &[Ca } _ => CalcitErr::err_str(CalcitErrKind::Type, format!("expected function entry, got: {entry}")), }, + Err(failure) => { + call_stack::display_stack_with_docs(&failure.msg, &failure.stack, failure.location.as_ref(), failure.hint.as_deref())?; + Err(failure) + } } } diff --git a/src/program.rs b/src/program.rs index db5c92bf..8cfdb1d6 100644 --- a/src/program.rs +++ b/src/program.rs @@ -13,7 +13,7 @@ use cirru_parser::Cirru; use crate::calcit::{self, Calcit, CalcitErr, CalcitScope, CalcitThunk, CalcitThunkInfo, CalcitTypeAnnotation, DYNAMIC_TYPE}; use crate::call_stack::CallStackList; -use crate::data::cirru::code_to_calcit; +use crate::data::{cirru::code_to_calcit, data_to_calcit}; use crate::runner; use crate::snapshot; use crate::snapshot::Snapshot; @@ -240,21 +240,6 @@ pub fn mark_runtime_def_cold(ns: &str, def: &str) { clear_runtime_value(def_id); } -pub fn refresh_runtime_cell_from_preprocessed(ns: &str, def: &str, preprocessed_code: &Calcit) { - let def_id = ensure_def_id(ns, def); - match CompiledDefKind::from_preprocessed_code(preprocessed_code) { - CompiledDefKind::LazyValue => write_runtime_lazy( - def_id, - Arc::new(preprocessed_code.to_owned()), - Arc::new(CalcitThunkInfo { - ns: Arc::from(ns), - def: Arc::from(def), - }), - ), - _ => clear_runtime_value(def_id), - } -} - pub fn seed_runtime_lazy_from_compiled(ns: &str, def: &str) -> bool { let Some((def_id, preprocessed_code)) = with_compiled_def(ns, def, |compiled| { if compiled.kind == CompiledDefKind::LazyValue { @@ -371,15 +356,12 @@ fn build_compiled_def(ns: &str, def: &str, payload: CompiledDefPayload) -> Compi } } -fn build_runtime_only_snapshot_fallback_compiled_def(ns: &str, def: &str, runtime_value: Calcit) -> CompiledDef { - let codegen_form = match &runtime_value { - Calcit::Thunk(thunk) => thunk.get_code().to_owned(), - _ => runtime_value.to_owned(), - }; +fn build_runtime_only_snapshot_fallback_compiled_def(ns: &str, def: &str, runtime_value: Calcit) -> Option { + let codegen_form = data_to_calcit(&runtime_value, ns, def).ok()?; let kind = CompiledDefKind::from_runtime_value(&runtime_value); let deps = collect_compiled_deps(&codegen_form); - CompiledDef { + Some(CompiledDef { def_id: ensure_def_id(ns, def), version_id: 0, kind, @@ -391,7 +373,7 @@ fn build_runtime_only_snapshot_fallback_compiled_def(ns: &str, def: &str, runtim schema: DYNAMIC_TYPE.clone(), doc: Arc::from(""), examples: vec![], - } + }) } fn ensure_source_backed_compiled_def_for_snapshot(ns: &str, def: &str) -> Option { @@ -444,10 +426,11 @@ fn is_compiled_executable_kind(kind: &CompiledDefKind) -> bool { ) } -fn with_compiled_executable_payload(ns: &str, def: &str, f: impl FnOnce(&Calcit) -> T) -> Option { +#[cfg(test)] +fn lookup_compiled_executable_code(ns: &str, def: &str) -> Option { with_compiled_def(ns, def, |compiled| { if is_compiled_executable_kind(&compiled.kind) { - Some(f(&compiled.preprocessed_code)) + Some(compiled.preprocessed_code.to_owned()) } else { None } @@ -455,16 +438,37 @@ fn with_compiled_executable_payload(ns: &str, def: &str, f: impl FnOnce(&Calc .flatten() } -#[cfg(test)] -fn lookup_compiled_executable_code(ns: &str, def: &str) -> Option { - with_compiled_executable_payload(ns, def, Calcit::to_owned) -} +fn materialize_compiled_executable_payload( + ns: &str, + def: &str, + call_stack: &CallStackList, +) -> Result, RuntimeResolveError> { + let Some(result) = with_compiled_def(ns, def, |compiled| { + if !is_compiled_executable_kind(&compiled.kind) { + return None; + } -fn execute_compiled_executable_payload(ns: &str, def: &str) -> Option { - with_compiled_executable_payload(ns, def, |code| { - runner::evaluate_expr(code, &CalcitScope::default(), ns, &CallStackList::default()).ok() + match compiled.kind { + CompiledDefKind::Proc | CompiledDefKind::Syntax => Some(Ok(compiled.preprocessed_code.to_owned())), + CompiledDefKind::Fn | CompiledDefKind::Macro => { + Some(runner::evaluate_expr(&compiled.preprocessed_code, &CalcitScope::default(), ns, call_stack)) + } + CompiledDefKind::LazyValue | CompiledDefKind::Value => None, + } }) - .flatten() + .flatten() else { + return Ok(None); + }; + + result.map(Some).map_err(RuntimeResolveError::Eval) +} + +pub fn resolve_compiled_executable_def( + ns: &str, + def: &str, + call_stack: &CallStackList, +) -> Result, RuntimeResolveError> { + materialize_compiled_executable_payload(ns, def, call_stack) } fn resolve_runtime_ready_value(value: Calcit, call_stack: &CallStackList) -> Result { @@ -506,8 +510,11 @@ fn resolve_runtime_def_value( let runtime_def_id = def_id.or_else(|| lookup_def_id(ns, def)); if let Some(def_id) = runtime_def_id { - if let Some(RuntimeCell::Cold) = lookup_runtime_cell_by_id(def_id) { - let _ = seed_runtime_lazy_from_compiled(ns, def); + match lookup_runtime_cell_by_id(def_id) { + Some(RuntimeCell::Cold) | None => { + let _ = seed_runtime_lazy_from_compiled(ns, def); + } + Some(RuntimeCell::Lazy { .. } | RuntimeCell::Ready(_) | RuntimeCell::Resolving | RuntimeCell::Errored(_)) => {} } if let Some(cell) = lookup_runtime_cell_by_id(def_id) { @@ -529,13 +536,7 @@ pub fn resolve_runtime_or_compiled_def( return Ok(Some(value)); } - Ok(execute_compiled_executable_payload(ns, def)) -} - -pub fn lookup_runtime_or_compiled_def_lenient(ns: &str, def: &str) -> Option { - resolve_runtime_or_compiled_def(ns, def, None, RuntimeResolveMode::Lenient, &CallStackList::default()) - .ok() - .flatten() + materialize_compiled_executable_payload(ns, def, call_stack) } fn annotation_from_value(value: &Calcit) -> Option> { @@ -872,6 +873,7 @@ struct SnapshotFillTask { ns: Arc, def: Arc, source_backed: bool, + referenced_by_compiled: bool, runtime_value: Option, } @@ -879,6 +881,12 @@ fn collect_snapshot_fill_tasks(compiled: &CompiledProgram) -> Vec = HashSet::new(); + for file in compiled.values() { + for compiled_def in file.defs.values() { + referenced_def_ids.extend(compiled_def.deps.iter().copied()); + } + } let mut tasks = vec![]; @@ -892,12 +900,13 @@ fn collect_snapshot_fill_tasks(compiled: &CompiledProgram) -> Vec Some(value.to_owned()), _ => None, }); - if !source_backed && runtime_value.is_none() { + if !source_backed && (!referenced_by_compiled || runtime_value.is_none()) { continue; } @@ -905,6 +914,7 @@ fn collect_snapshot_fill_tasks(compiled: &CompiledProgram) -> Vec Vec bool { - !task.source_backed && task.runtime_value.is_some() +fn build_snapshot_fill_compiled_def(task: SnapshotFillTask) -> Option { + if task.source_backed { + return ensure_source_backed_compiled_def_for_snapshot(task.ns.as_ref(), task.def.as_ref()); + } + + if !task.source_backed + && task.referenced_by_compiled + && task.runtime_value.is_some() + && let Some(runtime_value) = task.runtime_value + { + return build_runtime_only_snapshot_fallback_compiled_def(task.ns.as_ref(), task.def.as_ref(), runtime_value); + } + + None } pub fn clone_compiled_program_snapshot() -> Result { @@ -922,22 +944,11 @@ pub fn clone_compiled_program_snapshot() -> Result { let tasks = collect_snapshot_fill_tasks(&compiled); for task in tasks { - let compiled_file = compiled - .entry(task.ns.clone()) - .or_insert_with(|| CompiledFileData { defs: HashMap::new() }); - - if task.source_backed - && let Some(compiled_def) = ensure_source_backed_compiled_def_for_snapshot(task.ns.as_ref(), task.def.as_ref()) - { - compiled_file.defs.insert(task.def.clone(), compiled_def); - continue; - } - - if should_use_runtime_snapshot_fallback(&task) - && let Some(runtime_value) = task.runtime_value - { - let fallback = build_runtime_only_snapshot_fallback_compiled_def(task.ns.as_ref(), task.def.as_ref(), runtime_value); - compiled_file.defs.insert(task.def, fallback); + let ns = task.ns.clone(); + let def = task.def.clone(); + if let Some(compiled_def) = build_snapshot_fill_compiled_def(task) { + let compiled_file = compiled.entry(ns).or_insert_with(|| CompiledFileData { defs: HashMap::new() }); + compiled_file.defs.insert(def, compiled_def); } } @@ -1013,38 +1024,37 @@ pub fn apply_code_changes(changes: &snapshot::ChangesDict) -> Result<(), String> /// clear runtime and compiled caches after reloading pub fn clear_runtime_caches_for_reload(init_ns: Arc, reload_ns: Arc, reload_libs: bool) -> Result<(), String> { - let mut runtime = PROGRAM_RUNTIME_DATA_STATE.write().expect("open runtime data"); - let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("open compiled program data"); if reload_libs { + let mut runtime = PROGRAM_RUNTIME_DATA_STATE.write().expect("open runtime data"); + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("open compiled program data"); runtime.clear(); compiled.clear(); - } else { - // reduce changes of libs. could be dirty in some cases - let init_pkg = extract_pkg_from_ns(init_ns.to_owned()).ok_or_else(|| format!("failed to extract pkg from: {init_ns}"))?; - let reload_pkg = extract_pkg_from_ns(reload_ns.to_owned()).ok_or_else(|| format!("failed to extract pkg from: {reload_ns}"))?; - let ns_keys: Vec> = { - let index = PROGRAM_DEF_ID_INDEX.read().expect("read program def id index"); - index.by_ns.keys().cloned().collect() - }; - - let mut to_remove: Vec> = vec![]; - for k in ns_keys { - if k == init_pkg || k == reload_pkg || k.starts_with(&format!("{init_pkg}.")) || k.starts_with(&format!("{reload_pkg}.")) { - to_remove.push(k.to_owned()); - } else { - continue; - } - } - for k in to_remove { - for def_id in collect_ns_def_ids(&k) { - if let Some(slot) = runtime.get_mut(def_id.0 as usize) { - *slot = RuntimeCell::Cold; - } - } - compiled.remove(&k); + return Ok(()); + } + + let init_pkg = extract_pkg_from_ns(init_ns.to_owned()).ok_or_else(|| format!("failed to extract pkg from: {init_ns}"))?; + let reload_pkg = extract_pkg_from_ns(reload_ns.to_owned()).ok_or_else(|| format!("failed to extract pkg from: {reload_ns}"))?; + let ns_keys: Vec> = { + let index = PROGRAM_DEF_ID_INDEX.read().expect("read program def id index"); + index.by_ns.keys().cloned().collect() + }; + + let mut changes = snapshot::ChangesDict::default(); + for ns in ns_keys { + if ns == init_pkg || ns == reload_pkg || ns.starts_with(&format!("{init_pkg}.")) || ns.starts_with(&format!("{reload_pkg}.")) { + changes.changed.insert( + ns, + snapshot::FileChangeInfo { + ns: Some(Cirru::Leaf(Arc::from("ns"))), + added_defs: HashMap::new(), + removed_defs: HashSet::new(), + changed_defs: HashMap::new(), + }, + ); } } - Ok(()) + + clear_runtime_caches_for_changes(&changes, false) } pub fn clear_runtime_caches_for_changes(changes: &snapshot::ChangesDict, reload_libs: bool) -> Result<(), String> { diff --git a/src/program/tests.rs b/src/program/tests.rs index 0c753c3f..c94ebd24 100644 --- a/src/program/tests.rs +++ b/src/program/tests.rs @@ -1,6 +1,8 @@ use super::*; use crate::calcit::{CalcitImport, ImportInfo}; +use crate::call_stack::CallStackList; use crate::data::cirru::code_to_calcit; +use crate::run_program_with_docs; use std::sync::{LazyLock, Mutex}; static PROGRAM_TEST_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); @@ -32,91 +34,6 @@ fn compiled_def_for_test(def_id: DefId, deps: Vec) -> CompiledDef { } } -#[test] -fn reload_invalidation_collects_transitive_dependents() { - let mut compiled: ProgramCompiledData = HashMap::new(); - compiled.insert( - Arc::from("app.main"), - CompiledFileData { - defs: HashMap::from([ - (Arc::from("a"), compiled_def_for_test(DefId(1), vec![])), - (Arc::from("b"), compiled_def_for_test(DefId(2), vec![DefId(1)])), - (Arc::from("c"), compiled_def_for_test(DefId(3), vec![DefId(2)])), - (Arc::from("d"), compiled_def_for_test(DefId(4), vec![])), - ]), - }, - ); - - let mut index = ProgramDefIdIndex::default(); - index.by_ns.insert( - Arc::from("app.main"), - HashMap::from([ - (Arc::from("a"), DefId(1)), - (Arc::from("b"), DefId(2)), - (Arc::from("c"), DefId(3)), - (Arc::from("d"), DefId(4)), - ]), - ); - - let mut changes = snapshot::ChangesDict::default(); - changes.changed.insert( - Arc::from("app.main"), - snapshot::FileChangeInfo { - ns: None, - added_defs: HashMap::new(), - removed_defs: HashSet::new(), - changed_defs: HashMap::from([(String::from("a"), Cirru::Leaf(Arc::from("1")))]), - }, - ); - - let affected = collect_reload_affected_def_ids(&changes, &compiled, &index); - assert_eq!(affected, HashSet::from([DefId(1), DefId(2), DefId(3)])); -} - -#[test] -fn reload_invalidation_expands_namespace_header_changes() { - let compiled: ProgramCompiledData = HashMap::from([ - ( - Arc::from("app.main"), - CompiledFileData { - defs: HashMap::from([ - (Arc::from("a"), compiled_def_for_test(DefId(1), vec![])), - (Arc::from("b"), compiled_def_for_test(DefId(2), vec![])), - ]), - }, - ), - ( - Arc::from("app.consumer"), - CompiledFileData { - defs: HashMap::from([(Arc::from("use-main"), compiled_def_for_test(DefId(3), vec![DefId(2)]))]), - }, - ), - ]); - - let mut index = ProgramDefIdIndex::default(); - index.by_ns.insert( - Arc::from("app.main"), - HashMap::from([(Arc::from("a"), DefId(1)), (Arc::from("b"), DefId(2))]), - ); - index - .by_ns - .insert(Arc::from("app.consumer"), HashMap::from([(Arc::from("use-main"), DefId(3))])); - - let mut changes = snapshot::ChangesDict::default(); - changes.changed.insert( - Arc::from("app.main"), - snapshot::FileChangeInfo { - ns: Some(Cirru::Leaf(Arc::from("ns"))), - added_defs: HashMap::new(), - removed_defs: HashSet::new(), - changed_defs: HashMap::new(), - }, - ); - - let affected = collect_reload_affected_def_ids(&changes, &compiled, &index); - assert_eq!(affected, HashSet::from([DefId(1), DefId(2), DefId(3)])); -} - #[test] fn snapshot_fallback_preserves_dependency_metadata() { let _guard = lock_program_test_state(); @@ -132,7 +49,8 @@ fn snapshot_fallback_preserves_dependency_metadata() { def_id: Some(dep_id.0), })]); - let fallback = build_runtime_only_snapshot_fallback_compiled_def("app.main", "dep", runtime_value); + let fallback = build_runtime_only_snapshot_fallback_compiled_def("app.main", "dep", runtime_value) + .expect("runtime-only fallback should serialize import-based value"); assert_eq!(fallback.deps, vec![dep_id]); } @@ -290,13 +208,14 @@ fn clear_runtime_caches_for_changes_expands_namespace_header_invalidation() { } #[test] -fn clear_runtime_caches_for_reload_clears_selected_packages_only() { +fn clear_runtime_caches_for_reload_clears_selected_packages_and_dependents() { let _guard = lock_program_test_state(); reset_program_test_state(); let app_main = ensure_def_id("app.main", "entry"); let app_extra = ensure_def_id("app.extra", "helper"); let demo_reload = ensure_def_id("demo.feature", "reload"); + let util_consumer = ensure_def_id("util.consumer", "use-app"); let util_keep = ensure_def_id("util.keep", "value"); { @@ -319,6 +238,12 @@ fn clear_runtime_caches_for_reload_clears_selected_packages_only() { defs: HashMap::from([(Arc::from("reload"), compiled_def_for_test(demo_reload, vec![]))]), }, ); + compiled.insert( + Arc::from("util.consumer"), + CompiledFileData { + defs: HashMap::from([(Arc::from("use-app"), compiled_def_for_test(util_consumer, vec![app_main]))]), + }, + ); compiled.insert( Arc::from("util.keep"), CompiledFileData { @@ -330,6 +255,7 @@ fn clear_runtime_caches_for_reload_clears_selected_packages_only() { write_runtime_ready("app.main", "entry", Calcit::Number(1.0)).expect("seed runtime app.main/entry"); write_runtime_ready("app.extra", "helper", Calcit::Number(2.0)).expect("seed runtime app.extra/helper"); write_runtime_ready("demo.feature", "reload", Calcit::Number(3.0)).expect("seed runtime demo.feature/reload"); + write_runtime_ready("util.consumer", "use-app", Calcit::Number(4.0)).expect("seed runtime util.consumer/use-app"); write_runtime_ready("util.keep", "value", Calcit::Number(9.0)).expect("seed runtime util.keep/value"); clear_runtime_caches_for_reload(Arc::from("app.main"), Arc::from("demo.feature"), false) @@ -338,12 +264,14 @@ fn clear_runtime_caches_for_reload_clears_selected_packages_only() { assert_eq!(lookup_runtime_cell("app.main", "entry"), Some(RuntimeCell::Cold)); assert_eq!(lookup_runtime_cell("app.extra", "helper"), Some(RuntimeCell::Cold)); assert_eq!(lookup_runtime_cell("demo.feature", "reload"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_runtime_cell("util.consumer", "use-app"), Some(RuntimeCell::Cold)); assert_eq!(lookup_runtime_ready("util.keep", "value"), Some(Calcit::Number(9.0))); let compiled = PROGRAM_COMPILED_DATA_STATE.read().expect("read compiled data"); assert!(!compiled.contains_key("app.main")); assert!(!compiled.contains_key("app.extra")); assert!(!compiled.contains_key("demo.feature")); + assert!(!compiled.get("util.consumer").is_some_and(|file| file.defs.contains_key("use-app"))); assert!(compiled.get("util.keep").is_some_and(|file| file.defs.contains_key("value"))); } @@ -454,6 +382,134 @@ fn snapshot_rebuilds_changed_source_backed_def_after_reload_changes() { assert_eq!(rebuilt.source_code, Some(Calcit::Number(2.0))); } +#[test] +fn removed_source_def_changes_still_invalidate_transitive_dependents() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let shared_code = + code_to_calcit(&Cirru::Leaf(Arc::from("1")), "app.main", "shared", vec![]).expect("build shared source-backed code"); + let consumer_code = + code_to_calcit(&Cirru::Leaf(Arc::from("2")), "app.consumer", "use-shared", vec![]).expect("build consumer source-backed code"); + let helper_code = + code_to_calcit(&Cirru::Leaf(Arc::from("3")), "app.helper", "keep", vec![]).expect("build helper source-backed code"); + + PROGRAM_CODE_DATA.write().expect("seed program code").extend(HashMap::from([ + ( + Arc::from("app.main"), + ProgramFileData { + import_map: HashMap::new(), + defs: HashMap::from([( + Arc::from("shared"), + ProgramDefEntry { + code: shared_code, + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + )]), + }, + ), + ( + Arc::from("app.consumer"), + ProgramFileData { + import_map: HashMap::new(), + defs: HashMap::from([( + Arc::from("use-shared"), + ProgramDefEntry { + code: consumer_code, + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + )]), + }, + ), + ( + Arc::from("app.helper"), + ProgramFileData { + import_map: HashMap::new(), + defs: HashMap::from([( + Arc::from("keep"), + ProgramDefEntry { + code: helper_code, + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + )]), + }, + ), + ])); + + let shared_def = ensure_def_id("app.main", "shared"); + let consumer_def = ensure_def_id("app.consumer", "use-shared"); + let helper_def = ensure_def_id("app.helper", "keep"); + + { + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("seed compiled data"); + compiled.extend(HashMap::from([ + ( + Arc::from("app.main"), + CompiledFileData { + defs: HashMap::from([(Arc::from("shared"), compiled_def_for_test(shared_def, vec![]))]), + }, + ), + ( + Arc::from("app.consumer"), + CompiledFileData { + defs: HashMap::from([(Arc::from("use-shared"), compiled_def_for_test(consumer_def, vec![shared_def]))]), + }, + ), + ( + Arc::from("app.helper"), + CompiledFileData { + defs: HashMap::from([(Arc::from("keep"), compiled_def_for_test(helper_def, vec![]))]), + }, + ), + ])); + } + + write_runtime_ready("app.main", "shared", Calcit::Number(1.0)).expect("seed runtime shared"); + write_runtime_ready("app.consumer", "use-shared", Calcit::Number(2.0)).expect("seed runtime use-shared"); + write_runtime_ready("app.helper", "keep", Calcit::Number(3.0)).expect("seed runtime keep"); + + let mut changes = snapshot::ChangesDict::default(); + changes.changed.insert( + Arc::from("app.main"), + snapshot::FileChangeInfo { + ns: None, + added_defs: HashMap::new(), + removed_defs: HashSet::from([String::from("shared")]), + changed_defs: HashMap::new(), + }, + ); + + apply_code_changes(&changes).expect("apply removed source changes"); + clear_runtime_caches_for_changes(&changes, false).expect("clear runtime caches for removed source change"); + + assert!( + PROGRAM_CODE_DATA + .read() + .expect("read program code") + .get("app.main") + .is_some_and(|file| !file.defs.contains_key("shared")) + ); + + assert_eq!(lookup_runtime_cell("app.main", "shared"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_runtime_cell("app.consumer", "use-shared"), Some(RuntimeCell::Cold)); + assert_eq!(lookup_runtime_ready("app.helper", "keep"), Some(Calcit::Number(3.0))); + + let compiled = PROGRAM_COMPILED_DATA_STATE.read().expect("read compiled data"); + assert!(!compiled.get("app.main").is_some_and(|file| file.defs.contains_key("shared"))); + assert!( + !compiled + .get("app.consumer") + .is_some_and(|file| file.defs.contains_key("use-shared")) + ); + assert!(compiled.get("app.helper").is_some_and(|file| file.defs.contains_key("keep"))); +} + #[test] fn snapshot_prefers_source_backed_compiled_def_even_with_warnings() { let _guard = lock_program_test_state(); @@ -493,22 +549,115 @@ fn snapshot_prefers_source_backed_compiled_def_even_with_warnings() { } #[test] -fn runtime_snapshot_fallback_only_allows_runtime_only_defs() { - let runtime_only = SnapshotFillTask { - ns: Arc::from("app.runtime"), - def: Arc::from("demo"), - source_backed: false, - runtime_value: Some(Calcit::Number(42.0)), - }; - assert!(should_use_runtime_snapshot_fallback(&runtime_only)); - - let source_backed = SnapshotFillTask { - ns: Arc::from("app.source"), - def: Arc::from("demo"), - source_backed: true, - runtime_value: Some(Calcit::Number(42.0)), - }; - assert!(!should_use_runtime_snapshot_fallback(&source_backed)); +fn snapshot_skips_empty_namespace_when_source_backed_rebuild_fails() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let failing_code = code_to_calcit( + &Cirru::List(vec![ + Cirru::leaf("if"), + Cirru::leaf("true"), + Cirru::leaf("1"), + Cirru::leaf("2"), + Cirru::leaf("3"), + ]), + "app.fail", + "broken", + vec![], + ) + .expect("build failing source-backed code"); + + PROGRAM_CODE_DATA.write().expect("seed program code").insert( + Arc::from("app.fail"), + ProgramFileData { + import_map: HashMap::new(), + defs: HashMap::from([( + Arc::from("broken"), + ProgramDefEntry { + code: failing_code, + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + )]), + }, + ); + let _ = ensure_def_id("app.fail", "broken"); + + let snapshot = clone_compiled_program_snapshot().expect("clone compiled snapshot"); + + assert!(!snapshot.contains_key("app.fail")); +} + +#[test] +fn snapshot_skips_unreferenced_runtime_only_defs() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let _ = ensure_def_id("app.runtime", "unused"); + write_runtime_ready("app.runtime", "unused", Calcit::Number(42.0)).expect("seed unreferenced runtime-only value"); + + let snapshot = clone_compiled_program_snapshot().expect("clone compiled snapshot"); + assert!(!snapshot.contains_key("app.runtime")); +} + +#[test] +fn snapshot_keeps_referenced_runtime_only_defs() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let runtime_def = ensure_def_id("app.runtime", "shared"); + let consumer_def = ensure_def_id("app.consumer", "use-shared"); + + { + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("seed compiled data"); + compiled.insert( + Arc::from("app.consumer"), + CompiledFileData { + defs: HashMap::from([(Arc::from("use-shared"), compiled_def_for_test(consumer_def, vec![runtime_def]))]), + }, + ); + } + + write_runtime_ready("app.runtime", "shared", Calcit::Number(42.0)).expect("seed referenced runtime-only value"); + + let snapshot = clone_compiled_program_snapshot().expect("clone compiled snapshot"); + let compiled = snapshot + .get("app.runtime") + .and_then(|file| file.defs.get("shared")) + .expect("referenced runtime-only def should be preserved in snapshot"); + + assert_eq!(compiled.codegen_form, Calcit::Number(42.0)); + assert_eq!(compiled.source_code, None); +} + +#[test] +fn snapshot_skips_unserializable_referenced_runtime_only_defs() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let runtime_def = ensure_def_id("app.runtime", "shared-atom"); + let consumer_def = ensure_def_id("app.consumer", "use-shared-atom"); + + { + let mut compiled = PROGRAM_COMPILED_DATA_STATE.write().expect("seed compiled data"); + compiled.insert( + Arc::from("app.consumer"), + CompiledFileData { + defs: HashMap::from([(Arc::from("use-shared-atom"), compiled_def_for_test(consumer_def, vec![runtime_def]))]), + }, + ); + } + + write_runtime_ready( + "app.runtime", + "shared-atom", + crate::builtins::quick_build_atom(Calcit::Number(42.0)), + ) + .expect("seed referenced runtime-only atom"); + + let snapshot = clone_compiled_program_snapshot().expect("clone compiled snapshot"); + assert!(!snapshot.contains_key("app.runtime")); } #[test] @@ -584,11 +733,145 @@ fn lenient_compiled_fallback_does_not_backfill_runtime_cache() { .kind = CompiledDefKind::Fn; } - let value = lookup_runtime_or_compiled_def_lenient("app.compiled", "callable"); + let value = resolve_runtime_or_compiled_def( + "app.compiled", + "callable", + None, + RuntimeResolveMode::Lenient, + &CallStackList::default(), + ) + .expect("lenient compiled fallback should succeed"); assert_eq!(value, Some(Calcit::Number(7.0))); assert_eq!(lookup_runtime_cell("app.compiled", "callable"), Some(RuntimeCell::Cold)); } +#[test] +fn preprocess_ns_def_materializes_compiled_function_without_backfilling_runtime() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let fn_code = code_to_calcit( + &Cirru::List(vec![ + Cirru::leaf("defn"), + Cirru::leaf("callable"), + Cirru::List(vec![Cirru::leaf("x")]), + Cirru::leaf("x"), + ]), + "app.preprocess", + "callable", + vec![], + ) + .expect("parse fn payload"); + + let def_id = ensure_def_id("app.preprocess", "callable"); + store_compiled_output( + "app.preprocess", + "callable", + CompiledDefPayload { + version_id: 0, + preprocessed_code: fn_code, + codegen_form: Calcit::Nil, + deps: vec![], + type_summary: None, + source_code: None, + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + ); + write_runtime_ready("app.preprocess", "callable", Calcit::Number(0.0)).expect("seed runtime slot"); + mark_runtime_def_cold("app.preprocess", "callable"); + + let warnings = RefCell::new(vec![]); + crate::runner::preprocess::ensure_ns_def_compiled("app.preprocess", "callable", &warnings, &CallStackList::default()) + .expect("compiled function should materialize for preprocess"); + let value = resolve_compiled_executable_def( + "app.preprocess", + "callable", + &CallStackList::default(), + ) + .expect("lookup compiled function after ensure"); + + assert!(matches!(value, Some(Calcit::Fn { .. }))); + assert_eq!(lookup_runtime_cell_by_id(def_id), Some(RuntimeCell::Cold)); +} + +#[test] +fn lazy_runtime_resolution_seeds_from_compiled_when_runtime_slot_is_missing() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let def_id = ensure_def_id("app.preprocess", "lazy-value"); + store_compiled_output( + "app.preprocess", + "lazy-value", + CompiledDefPayload { + version_id: 0, + preprocessed_code: Calcit::Number(7.0), + codegen_form: Calcit::Number(7.0), + deps: vec![], + type_summary: None, + source_code: None, + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + ); + + let value = resolve_runtime_or_compiled_def( + "app.preprocess", + "lazy-value", + None, + RuntimeResolveMode::Lenient, + &CallStackList::default(), + ) + .expect("resolve compiled lazy value"); + + assert_eq!(value, Some(Calcit::Number(7.0))); + assert_eq!(lookup_runtime_cell_by_id(def_id), Some(RuntimeCell::Ready(Calcit::Number(7.0)))); +} + +#[test] +fn run_program_compiles_then_executes_without_runtime_backfill() { + let _guard = lock_program_test_state(); + reset_program_test_state(); + + let fn_code = code_to_calcit( + &Cirru::List(vec![ + Cirru::leaf("defn"), + Cirru::leaf("main"), + Cirru::List(vec![]), + Cirru::leaf("7"), + ]), + "app.main", + "main", + vec![], + ) + .expect("parse main fn"); + + PROGRAM_CODE_DATA.write().expect("seed program code").insert( + Arc::from("app.main"), + ProgramFileData { + import_map: HashMap::new(), + defs: HashMap::from([( + Arc::from("main"), + ProgramDefEntry { + code: fn_code, + schema: DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + )]), + }, + ); + + let result = run_program_with_docs(Arc::from("app.main"), Arc::from("main"), &[]).expect("run compiled main"); + + assert_eq!(result, Calcit::Number(7.0)); + assert!(lookup_compiled_def("app.main", "main").is_some()); + assert_eq!(lookup_runtime_cell("app.main", "main"), None); +} + #[test] fn runtime_resolve_mode_handles_resolving_cell_differently() { let _guard = lock_program_test_state(); diff --git a/src/runner.rs b/src/runner.rs index 40db6763..bfed5d3d 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -503,7 +503,8 @@ pub fn eval_symbol_from_program(sym: &str, ns: &str, call_stack: &CallStackList) } if program::has_def_code(ns, sym) { let warnings: RefCell> = RefCell::new(vec![]); - return preprocess::preprocess_ns_def(ns, sym, &warnings, call_stack); + preprocess::ensure_ns_def_compiled(ns, sym, &warnings, call_stack)?; + return resolve_runtime_or_compiled_def(ns, sym, None, call_stack); } Ok(None) } diff --git a/src/runner/preprocess.rs b/src/runner/preprocess.rs index 17b149cf..4bd25d1e 100644 --- a/src/runner/preprocess.rs +++ b/src/runner/preprocess.rs @@ -16,11 +16,30 @@ use std::{cell::RefCell, vec}; use im_ternary_tree::TernaryTreeList; use strum::ParseError; +use cirru_edn::EdnTag; type ScopeTypes = HashMap, Arc>; static WARN_DYN_METHOD: AtomicBool = AtomicBool::new(false); +thread_local! { + static PREPROCESS_COMPILE_GUARD: RefCell, Arc)>> = RefCell::new(HashSet::new()); +} + +fn with_preprocess_compile_guard(ns: &str, def: &str, f: impl FnOnce() -> Result) -> Result, CalcitErr> { + let key = (Arc::from(ns), Arc::from(def)); + let inserted = PREPROCESS_COMPILE_GUARD.with(|guard| guard.borrow_mut().insert(key.clone())); + if !inserted { + return Ok(None); + } + + let result = f(); + PREPROCESS_COMPILE_GUARD.with(|guard| { + guard.borrow_mut().remove(&key); + }); + result.map(Some) +} + pub fn set_warn_dyn_method(enabled: bool) { WARN_DYN_METHOD.store(enabled, Ordering::SeqCst); } @@ -96,66 +115,170 @@ fn ensure_ns_def_preprocessed( ) -> Result<(), CalcitErr> { let ns = raw_ns; let def = raw_def; - // println!("preprocessing def: {}/{}", ns, def); - if let Some(cell) = program::lookup_runtime_cell(ns, def) { - match cell { - program::RuntimeCell::Ready(_) | program::RuntimeCell::Lazy { .. } | program::RuntimeCell::Resolving => return Ok(()), - program::RuntimeCell::Errored(_) | program::RuntimeCell::Cold => {} - } - } - if program::lookup_runtime_or_compiled_def_lenient(ns, def).is_some() { + if program::lookup_compiled_def(ns, def).is_some() { return Ok(()); } - // println!("init for... {}/{}", ns, def); - match program::lookup_def_code(ns, def) { - Some(code) => { - // mark the def as resolving first to prevent dead loop during recursive preprocess. - program::mark_runtime_def_resolving(ns, def); + let Some(()) = with_preprocess_compile_guard(ns, def, || { + match program::lookup_def_code(ns, def) { + Some(code) => { + let next_stack = call_stack.extend(ns, def, StackKind::Fn, &code, &[]); - let next_stack = call_stack.extend(ns, def, StackKind::Fn, &code, &[]); + let mut scope_types = ScopeTypes::new(); + let context_label = format!("{ns}/{def}"); + let resolved_code = calcit::with_type_annotation_warning_context(context_label, || { + preprocess_expr(&code, &HashSet::new(), &mut scope_types, ns, check_warnings, &next_stack) + })?; + store_preprocessed_compiled_output(ns, def, &code, &resolved_code); - let mut scope_types = ScopeTypes::new(); - let context_label = format!("{ns}/{def}"); - let resolved_code = match calcit::with_type_annotation_warning_context(context_label, || { - preprocess_expr(&code, &HashSet::new(), &mut scope_types, ns, check_warnings, &next_stack) - }) { - Ok(resolved) => resolved, - Err(e) => { - program::mark_runtime_def_errored(ns, def, Arc::from(e.to_string())); - return Err(e); - } - }; - // println!("\n resolve code to run: {:?}", resolved_code); - store_preprocessed_compiled_output(ns, def, &code, &resolved_code); - program::refresh_runtime_cell_from_preprocessed(ns, def, &resolved_code); + Ok(()) + } + None if ns.starts_with('|') || ns.starts_with('"') => Ok(()), + None => { + let loc = NodeLocation::new(Arc::from(ns), Arc::from(def), Arc::from(vec![])); + Err(CalcitErr::use_msg_stack_location( + CalcitErrKind::Var, + format!("unknown ns/def in program: {ns}/{def}"), + call_stack, + Some(loc), + )) + } + } + })? else { + return Ok(()); + }; - Ok(()) + Ok(()) +} + +pub fn ensure_ns_def_compiled( + raw_ns: &str, + raw_def: &str, + check_warnings: &RefCell>, + call_stack: &CallStackList, +) -> Result<(), CalcitErr> { + ensure_ns_def_preprocessed(raw_ns, raw_def, check_warnings, call_stack) +} + +fn lookup_callable_ns_def_for_preprocess( + raw_ns: &str, + raw_def: &str, + check_warnings: &RefCell>, + call_stack: &CallStackList, +) -> Result, CalcitErr> { + ensure_ns_def_compiled(raw_ns, raw_def, check_warnings, call_stack)?; + Ok(match program::resolve_compiled_executable_def(raw_ns, raw_def, call_stack).ok().flatten() { + value @ Some(Calcit::Macro { .. } | Calcit::Fn { .. }) => value, + _ => None, + }) +} + +fn resolve_trait_def_from_source_code(code: &Calcit) -> Option { + if let Calcit::Thunk(thunk) = code { + return resolve_trait_def_from_source_code(thunk.get_code()); + } + + let Calcit::List(items) = code else { + return None; + }; + + if let Some(head) = items.first() { + if matches!(head, Calcit::Syntax(CalcitSyntax::Quote, _)) + || matches!(head, Calcit::Symbol { sym, .. } if sym.as_ref() == "quote") + || matches!(head, Calcit::Import(CalcitImport { ns, def, .. }) if &**ns == calcit::CORE_NS && &**def == "quote") + { + if let Some(inner) = items.get(1) { + return resolve_trait_def_from_source_code(inner); + } } - None if ns.starts_with('|') || ns.starts_with('"') => Ok(()), - None => { - let loc = NodeLocation::new(Arc::from(ns), Arc::from(def), Arc::from(vec![])); - Err(CalcitErr::use_msg_stack_location( - CalcitErrKind::Var, - format!("unknown ns/def in program: {ns}/{def}"), - call_stack, - Some(loc), - )) + } + + let head = items.first()?; + if matches!(head, Calcit::Proc(CalcitProc::NativeTraitNew)) + || matches!(head, Calcit::Symbol { sym, .. } if sym.as_ref() == "&trait::new") + || matches!(head, Calcit::Import(CalcitImport { ns, def, .. }) if &**ns == calcit::CORE_NS && &**def == "&trait::new") + { + return parse_trait_new_source(items.as_ref()); + } + if matches!(head, Calcit::Symbol { sym, .. } if sym.as_ref() == "deftrait") + || matches!(head, Calcit::Import(CalcitImport { ns, def, .. }) if &**ns == calcit::CORE_NS && &**def == "deftrait") + { + return parse_deftrait_source(items.as_ref()); + } + + None +} + +fn parse_trait_name_from_source(form: &Calcit) -> Option { + match form { + Calcit::Symbol { sym, .. } | Calcit::Str(sym) => Some(EdnTag::from(sym.as_ref())), + Calcit::Tag(tag) => Some(tag.to_owned()), + _ => None, + } +} + +fn parse_trait_method_name_from_source(form: &Calcit) -> Option { + match form { + Calcit::Method(name, _) | Calcit::Symbol { sym: name, .. } | Calcit::Str(name) => Some(EdnTag::from(name.as_ref())), + Calcit::Tag(tag) => Some(tag.to_owned()), + _ => None, + } +} + +fn parse_trait_method_specs_from_source<'a>(items: impl Iterator) -> Option<(Vec, Vec>)> { + let mut methods: Vec = vec![]; + let mut method_types: Vec> = vec![]; + + for item in items { + let Calcit::List(entry) = item else { + return None; + }; + if entry.len() != 2 { + return None; } + + let method_name = parse_trait_method_name_from_source(entry.first()?)?; + let type_form = entry.get(1)?; + let method_type = calcit::with_type_annotation_warning_context(format!("trait:{}", method_name.ref_str()), || { + CalcitTypeAnnotation::parse_type_annotation_form(type_form) + }); + methods.push(method_name); + method_types.push(method_type); } + + Some((methods, method_types)) +} + +fn parse_trait_new_source(items: &CalcitList) -> Option { + let name = parse_trait_name_from_source(items.get(1)?)?; + let method_specs = match items.get(2)? { + Calcit::List(list) => list, + _ => return None, + }; + let (methods, method_types) = parse_trait_method_specs_from_source(method_specs.iter())?; + Some(CalcitTrait::new(name, methods, method_types)) } -/// returns the resolved symbol(only functions and macros are used), -/// if code related is not preprocessed, do it internally. -pub fn preprocess_ns_def( +fn parse_deftrait_source(items: &CalcitList) -> Option { + let name = parse_trait_name_from_source(items.get(1)?)?; + let (methods, method_types) = parse_trait_method_specs_from_source(items.iter().skip(2))?; + Some(CalcitTrait::new(name, methods, method_types)) +} + +fn lookup_trait_ns_def_for_preprocess( raw_ns: &str, raw_def: &str, check_warnings: &RefCell>, call_stack: &CallStackList, -) -> Result, CalcitErr> { - ensure_ns_def_preprocessed(raw_ns, raw_def, check_warnings, call_stack)?; - Ok(program::lookup_runtime_or_compiled_def_lenient(raw_ns, raw_def)) +) -> Result>, CalcitErr> { + ensure_ns_def_compiled(raw_ns, raw_def, check_warnings, call_stack)?; + Ok( + program::lookup_compiled_def(raw_ns, raw_def) + .and_then(|compiled| compiled.source_code) + .and_then(|code| resolve_trait_def_from_source_code(&code)) + .map(Arc::new), + ) } pub fn compile_source_def_for_snapshot( @@ -209,7 +332,7 @@ pub fn preprocess_expr( // TODO js syntax to handle in future } else if let Some(target_ns) = program::lookup_ns_target_in_import(&info.at_ns, &ns_alias) { // make sure the target is preprocessed - let _macro_fn = preprocess_ns_def(&target_ns, &def_part, check_warnings, call_stack)?; + ensure_ns_def_compiled(&target_ns, &def_part, check_warnings, call_stack)?; let form = Calcit::Import(CalcitImport { ns: target_ns.to_owned(), @@ -226,7 +349,7 @@ pub fn preprocess_expr( // refer to namespace/def directly for some usages // make sure the target is preprocessed - let _macro_fn = preprocess_ns_def(&ns_alias, &def_part, check_warnings, call_stack)?; + ensure_ns_def_compiled(&ns_alias, &def_part, check_warnings, call_stack)?; let form = Calcit::Import(CalcitImport { ns: ns_alias.to_owned(), @@ -281,7 +404,7 @@ pub fn preprocess_expr( // println!("same file: {}/{} at {}/{}", def_ns, def, file_ns, at_def); // make sure the target is preprocessed - let _macro_fn = preprocess_ns_def(def_ns, def, check_warnings, call_stack)?; + ensure_ns_def_compiled(def_ns, def, check_warnings, call_stack)?; let form = Calcit::Import(CalcitImport { ns: def_ns.to_owned(), @@ -298,7 +421,7 @@ pub fn preprocess_expr( // println!("find in core def: {}", def); // make sure the target is preprocessed - let _macro_fn = preprocess_ns_def(calcit::CORE_NS, def, check_warnings, call_stack)?; + ensure_ns_def_compiled(calcit::CORE_NS, def, check_warnings, call_stack)?; let form = Calcit::Import(CalcitImport { ns: calcit::CORE_NS.into(), @@ -312,7 +435,7 @@ pub fn preprocess_expr( // println!("again same file: {}/{} at {}/{}", def_ns, def, file_ns, at_def); // make sure the target is preprocessed - let _macro_fn = preprocess_ns_def(def_ns, def, check_warnings, call_stack)?; + ensure_ns_def_compiled(def_ns, def, check_warnings, call_stack)?; let form = Calcit::Import(CalcitImport { ns: def_ns.to_owned(), @@ -340,7 +463,7 @@ pub fn preprocess_expr( // TODO js syntax to handle in future // make sure the target is preprocessed - let _macro_fn = preprocess_ns_def(&target_ns, def, check_warnings, call_stack)?; + ensure_ns_def_compiled(&target_ns, def, check_warnings, call_stack)?; let form = Calcit::Import(CalcitImport { ns: target_ns.to_owned(), @@ -427,7 +550,7 @@ fn preprocess_list_call( let def_name = grab_def_name(head); let head_value = match &head_form { - Calcit::Import(CalcitImport { ns, def, .. }) => preprocess_ns_def(ns, def, check_warnings, call_stack)?, + Calcit::Import(CalcitImport { ns, def, .. }) => lookup_callable_ns_def_for_preprocess(ns, def, check_warnings, call_stack)?, _ => None, }; @@ -2183,9 +2306,9 @@ fn infer_if_return_type(xs: &CalcitList, scope_types: &ScopeTypes) -> Option( fn_info: &crate::calcit::CalcitFn, - call_args: &CalcitList, + call_args: impl Iterator, scope_types: &ScopeTypes, ) -> Option> { // Only attempt resolution when there are generics and the return type contains TypeVars @@ -2196,7 +2319,7 @@ fn resolve_generic_return_type( let mut bindings: HashMap, Arc> = HashMap::new(); // Match each actual argument against the declared argument type to build bindings - for (arg, expected_type) in call_args.iter().zip(fn_info.arg_types.iter()) { + for (arg, expected_type) in call_args.zip(fn_info.arg_types.iter()) { if matches!(**expected_type, CalcitTypeAnnotation::Dynamic) { continue; } @@ -2215,6 +2338,28 @@ fn resolve_generic_return_type( if resolved.contains_type_var() { None } else { Some(resolved) } } +fn infer_return_type_from_compiled_callable( + ns: &str, + def: &str, + call_expr: &CalcitList, + scope_types: &ScopeTypes, +) -> Option> { + let compiled_value = program::resolve_compiled_executable_def(ns, def, &CallStackList::default()) + .ok() + .flatten()?; + + match compiled_value { + Calcit::Fn { info, .. } => { + if let Some(resolved) = resolve_generic_return_type(&info, call_expr.iter().skip(1), scope_types) { + return Some(resolved); + } + Some(info.return_type.clone()) + } + Calcit::Proc(proc) => proc.get_type_signature().map(|type_sig| type_sig.return_type.clone()), + _ => None, + } +} + /// Infer type from an expression (for &let bindings) /// Supports: /// - Literals (number, string, bool, nil) @@ -2451,74 +2596,25 @@ fn infer_type_from_expr(expr: &Calcit, scope_types: &ScopeTypes) -> Option { - // Try to resolve generic return type using call arguments - if info.return_type.contains_type_var() { - let call_args = xs.drop_left(); - if let Some(resolved) = resolve_generic_return_type(&info, &call_args, scope_types) { - return Some(resolved); - } - } - return Some(info.return_type.clone()); - } - // For builtin procs, get type signature - Calcit::Proc(proc) => { - if let Some(type_sig) = proc.get_type_signature() { - return Some(type_sig.return_type.clone()); - } - } - _ => {} - } - } - - // Fallback: check code definition (for not-yet-evaluated definitions) - if let Some(code) = program::lookup_def_code(ns, def) { - // Code is the AST, might be a defn with return type annotation - // Format: (defn name (args) :return-type body) or (defn name (args) body) - if let Calcit::List(ref xs) = code { - // Check if it's a defn: first element should be Symbol "defn" - if let Some(Calcit::Symbol { sym, .. }) = xs.first() { - if sym.as_ref() == "defn" { - // Defn format: (defn name (args) [:return-type] body...) - // Return type is the 3rd element (index 3) if it's a tag - if let Some(ret_type) = xs.get(3) { - if matches!(ret_type, Calcit::Tag(_)) { - return Some(CalcitTypeAnnotation::parse_type_annotation_form(ret_type)); - } - } - } - } - } - // For compiled functions in code, get return_type from info - if let Calcit::Fn { info, .. } = code { - if info.return_type.contains_type_var() { - let call_args = xs.drop_left(); - if let Some(resolved) = resolve_generic_return_type(&info, &call_args, scope_types) { - return Some(resolved); - } - } - return Some(info.return_type.clone()); - } - } - None + infer_return_type_from_compiled_callable(ns, def, xs, scope_types) } // Symbol: might be a function reference before preprocessing // Try to resolve it and get the return type Calcit::Symbol { sym, info, .. } => { - // Try to lookup in program - if let Some(Calcit::Fn { info: fn_info, .. }) = program::lookup_def_code(&info.at_ns, sym) { - if fn_info.return_type.contains_type_var() { - let call_args = xs.drop_left(); - if let Some(resolved) = resolve_generic_return_type(&fn_info, &call_args, scope_types) { - return Some(resolved); - } + if let Some(inferred) = infer_return_type_from_compiled_callable(&info.at_ns, sym, xs, scope_types) { + return Some(inferred); + } + + if let Some(code) = program::lookup_def_code(&info.at_ns, sym) { + if let Calcit::List(xs) = code + && let Some(Calcit::Symbol { sym, .. }) = xs.first() + && sym.as_ref() == "defn" + && let Some(ret_type) = xs.get(3) + && matches!(ret_type, Calcit::Tag(_)) + { + return Some(CalcitTypeAnnotation::parse_type_annotation_form(ret_type)); } - return Some(fn_info.return_type.clone()); } None } @@ -2526,8 +2622,7 @@ fn infer_type_from_expr(expr: &Calcit, scope_types: &ScopeTypes) -> Option { if info.return_type.contains_type_var() { - let call_args = xs.drop_left(); - if let Some(resolved) = resolve_generic_return_type(info, &call_args, scope_types) { + if let Some(resolved) = resolve_generic_return_type(info, xs.iter().skip(1), scope_types) { return Some(resolved); } } @@ -3534,21 +3629,21 @@ pub fn preprocess_assert_traits( let resolved = match trait_form { Calcit::Symbol { sym, info, .. } => match runner::parse_ns_def(sym) { - Some((ns_part, def_part)) => preprocess_ns_def(&ns_part, &def_part, ctx.check_warnings, ctx.call_stack) + Some((ns_part, def_part)) => lookup_trait_ns_def_for_preprocess(&ns_part, &def_part, ctx.check_warnings, ctx.call_stack) .ok() .flatten(), - None => preprocess_ns_def(&info.at_ns, sym, ctx.check_warnings, ctx.call_stack) + None => lookup_trait_ns_def_for_preprocess(&info.at_ns, sym, ctx.check_warnings, ctx.call_stack) .ok() .flatten(), }, - Calcit::Import(import) => preprocess_ns_def(&import.ns, &import.def, ctx.check_warnings, ctx.call_stack) + Calcit::Import(import) => lookup_trait_ns_def_for_preprocess(&import.ns, &import.def, ctx.check_warnings, ctx.call_stack) .ok() .flatten(), _ => None, }; - if let Some(Calcit::Trait(trait_def)) = resolved { - trait_defs.push(Arc::new(trait_def)); + if let Some(trait_def) = resolved { + trait_defs.push(trait_def); } else if fallback_entry.is_none() { fallback_entry = Some(Arc::new(CalcitTypeAnnotation::Custom(Arc::new((*trait_form).to_owned())))); } @@ -3717,9 +3812,16 @@ fn validate_def_schema_during_preprocess( #[cfg(test)] mod tests { use super::*; - use crate::calcit::{CalcitFn, CalcitFnArgs, CalcitMacro, CalcitRecord, CalcitScope, CalcitStruct}; + use crate::calcit::{CalcitFn, CalcitFnArgs, CalcitFnUsageMeta, CalcitImport, CalcitMacro, CalcitRecord, CalcitScope, CalcitStruct, ImportInfo}; use crate::data::cirru::code_to_calcit; use cirru_parser::Cirru; + use std::sync::{LazyLock, Mutex}; + + static PREPROCESS_TEST_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); + + fn lock_preprocess_test_state() -> std::sync::MutexGuard<'static, ()> { + PREPROCESS_TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner()) + } struct WarnDynMethodGuard { prev: bool, @@ -3842,7 +3944,8 @@ mod tests { ]); let code = code_to_calcit(&expr, "tests.optional", "demo", vec![]).expect("parse cirru"); - let scope_defs: HashSet> = HashSet::new(); + let mut scope_defs: HashSet> = HashSet::new(); + scope_defs.insert(Arc::from("x")); let mut scope_types: ScopeTypes = ScopeTypes::new(); let warnings = RefCell::new(vec![]); let stack = CallStackList::default(); @@ -3963,6 +4066,205 @@ mod tests { ); } + #[test] + fn lookup_trait_for_preprocess_reads_source_backed_trait_without_runtime_value() { + let _guard = lock_preprocess_test_state(); + + let trait_code = code_to_calcit( + &Cirru::List(vec![ + Cirru::leaf("deftrait"), + Cirru::leaf("MySourceTrait"), + Cirru::List(vec![Cirru::leaf(".show"), Cirru::leaf(":fn")]), + ]), + "tests.source-trait", + "MySourceTrait", + vec![], + ) + .expect("parse trait def"); + + let mut program_code = program::PROGRAM_CODE_DATA.write().expect("open program code"); + program_code.insert( + Arc::from("tests.source-trait"), + program::ProgramFileData { + import_map: HashMap::new(), + defs: HashMap::from([( + Arc::from("MySourceTrait"), + program::ProgramDefEntry { + code: trait_code, + schema: calcit::DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + )]), + }, + ); + drop(program_code); + + let warnings = RefCell::new(vec![]); + let stack = CallStackList::default(); + + let trait_def = lookup_trait_ns_def_for_preprocess("tests.source-trait", "MySourceTrait", &warnings, &stack) + .expect("lookup trait") + .expect("trait should resolve from source-backed compiled data"); + + assert_eq!(trait_def.name.ref_str(), "MySourceTrait"); + assert!(trait_def.has_method("show")); + } + + #[test] + fn infers_imported_generic_return_type_from_compiled_function_without_runtime_ready() { + let _guard = lock_preprocess_test_state(); + + let ns = "tests.generic-infer"; + let def = "identity"; + let def_id = program::lookup_def_id(ns, def).unwrap_or_else(|| { + program::mark_runtime_def_cold(ns, def); + program::lookup_def_id(ns, def).expect("register def id") + }); + + let generic_name: Arc = Arc::from("T"); + program::write_compiled_def( + ns, + def, + program::CompiledDef { + def_id, + version_id: 0, + kind: program::CompiledDefKind::Fn, + preprocessed_code: Calcit::Fn { + id: Arc::from("tests.generic-infer/identity"), + info: Arc::new(CalcitFn { + name: Arc::from(def), + def_ns: Arc::from(ns), + def_ref: None, + usage: CalcitFnUsageMeta::default(), + scope: Arc::new(CalcitScope::default()), + args: Arc::new(CalcitFnArgs::Args(vec![CalcitLocal::track_sym(&Arc::from("x"))])), + body: vec![], + generics: Arc::new(vec![generic_name.clone()]), + return_type: Arc::new(CalcitTypeAnnotation::TypeVar(generic_name.clone())), + arg_types: vec![Arc::new(CalcitTypeAnnotation::TypeVar(generic_name))], + }), + }, + codegen_form: Calcit::Nil, + deps: vec![], + type_summary: None, + source_code: None, + schema: calcit::DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + ); + + let call = Calcit::List(Arc::new(CalcitList::from(&[ + Calcit::Import(CalcitImport { + ns: Arc::from(ns), + def: Arc::from(def), + info: Arc::new(ImportInfo::NsReferDef { + at_ns: Arc::from("tests.caller"), + at_def: Arc::from("demo"), + }), + def_id: Some(def_id.0), + }), + Calcit::Number(1.0), + ][..]))); + + let inferred = infer_type_from_expr(&call, &ScopeTypes::new()).expect("infer import call type"); + assert!(matches!(inferred.as_ref(), CalcitTypeAnnotation::Number)); + + let call = Calcit::List(Arc::new(CalcitList::from(&[ + Calcit::Symbol { + sym: Arc::from(def), + info: Arc::new(CalcitSymbolInfo { + at_ns: Arc::from(ns), + at_def: Arc::from("demo"), + }), + location: None, + }, + Calcit::Number(1.0), + ][..]))); + + let inferred = infer_type_from_expr(&call, &ScopeTypes::new()).expect("infer symbol call type"); + assert!(matches!(inferred.as_ref(), CalcitTypeAnnotation::Number)); + } + + #[test] + fn ensure_ns_def_compiled_refreshes_source_backed_output_even_when_runtime_is_ready() { + let _guard = lock_preprocess_test_state(); + + let ns = "tests.runtime-shortcut"; + let def = "value"; + let source_code = Calcit::Number(1.0); + + let mut program_code = program::PROGRAM_CODE_DATA.write().expect("open program code"); + program_code.insert( + Arc::from(ns), + program::ProgramFileData { + import_map: HashMap::new(), + defs: HashMap::from([( + Arc::from(def), + program::ProgramDefEntry { + code: source_code, + schema: calcit::DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + )]), + }, + ); + drop(program_code); + + let warnings = RefCell::new(vec![]); + let stack = CallStackList::default(); + program::write_runtime_ready(ns, def, Calcit::Number(99.0)).expect("seed stale runtime value"); + + ensure_ns_def_compiled(ns, def, &warnings, &stack).expect("compile source-backed def with ready runtime cell"); + let compiled = program::lookup_compiled_def(ns, def).expect("compiled output should exist"); + assert_eq!(compiled.preprocessed_code, Calcit::Number(1.0)); + } + + #[test] + fn ensure_ns_def_compiled_handles_recursive_source_with_compile_guard() { + let _guard = lock_preprocess_test_state(); + + let ns = "tests.recursive-compile"; + let def = "loop"; + let recursive_code = code_to_calcit( + &Cirru::List(vec![ + Cirru::leaf("defn"), + Cirru::leaf(def), + Cirru::List(vec![]), + Cirru::List(vec![Cirru::leaf(def)]), + ]), + ns, + def, + vec![], + ) + .expect("parse recursive fn"); + + let mut program_code = program::PROGRAM_CODE_DATA.write().expect("open program code"); + program_code.insert( + Arc::from(ns), + program::ProgramFileData { + import_map: HashMap::new(), + defs: HashMap::from([( + Arc::from(def), + program::ProgramDefEntry { + code: recursive_code, + schema: calcit::DYNAMIC_TYPE.clone(), + doc: Arc::from(""), + examples: vec![], + }, + )]), + }, + ); + drop(program_code); + + let warnings = RefCell::new(vec![]); + let stack = CallStackList::default(); + ensure_ns_def_compiled(ns, def, &warnings, &stack).expect("compile recursive source def"); + assert!(program::lookup_compiled_def(ns, def).is_some(), "recursive source def should compile once"); + } + #[test] fn validates_record_field_access() { use cirru_edn::EdnTag; @@ -4108,88 +4410,6 @@ mod tests { assert_eq!(nodes.len(), 2, "call should keep receiver argument"); } - #[test] - #[ignore] // TODO: This test was failing before our changes - needs investigation - fn rewrites_method_call_with_fn_entry_via_record_get() { - use cirru_edn::EdnTag; - - let expr = Cirru::List(vec![Cirru::leaf(".greet"), Cirru::leaf("user")]); - let code = code_to_calcit(&expr, "tests.method", "demo", vec![]).expect("parse cirru"); - - let mut scope_defs: HashSet> = HashSet::new(); - scope_defs.insert(Arc::from("user")); - let mut scope_types: ScopeTypes = ScopeTypes::new(); - - let fn_args = CalcitFnArgs::Args(vec![]); - let arg_types = fn_args.empty_arg_types(); - let fn_info = Arc::new(CalcitFn { - name: Arc::from("greet"), - def_ns: Arc::from("tests.method.ns"), - def_ref: None, - usage: crate::calcit::CalcitFnUsageMeta::default(), - scope: Arc::new(CalcitScope::default()), - args: Arc::new(fn_args), - body: vec![], - generics: Arc::new(vec![]), - return_type: crate::calcit::DYNAMIC_TYPE.clone(), - arg_types, - }); - let method_fn = Calcit::Fn { - id: Arc::from("tests.method.ns/greet"), - info: fn_info, - }; - - let impl_record = CalcitRecord { - struct_ref: Arc::new(CalcitStruct::from_fields(EdnTag::from("Greeter"), vec![EdnTag::from("greet")])), - values: Arc::new(vec![method_fn.clone()]), - }; - - let record_ns = "tests.method.impls"; - let record_def = "&test-greeter-impls"; - program::write_runtime_ready(record_ns, record_def, Calcit::Record(impl_record)).expect("register record impls"); - - let record_import = Calcit::Import(CalcitImport { - ns: Arc::from(record_ns), - def: Arc::from(record_def), - info: Arc::new(ImportInfo::SameFile { at_def: Arc::from("demo") }), - def_id: None, - }); - scope_types.insert(Arc::from("user"), CalcitTypeAnnotation::parse_type_annotation_form(&record_import)); - - let warnings = RefCell::new(vec![]); - let stack = CallStackList::default(); - - let resolved = - preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.method", &warnings, &stack).expect("preprocess method call"); - - let nodes = match resolved { - Calcit::List(xs) => xs.to_vec(), - other => panic!("expected list form, got {other}"), - }; - assert_eq!(nodes.len(), 2, "call should include head and receiver arg"); - - let head_nodes = match nodes.first() { - Some(Calcit::List(xs)) => xs.to_vec(), - other => panic!("expected fallback head to be a list, got {other:?}"), - }; - assert_eq!(head_nodes.len(), 3, "record-get form should include proc, record ref, and tag"); - assert!( - matches!(head_nodes.first(), Some(Calcit::Proc(CalcitProc::NativeRecordGet))), - "head should call &record:get" - ); - match head_nodes.get(1) { - Some(Calcit::Import(import)) => { - assert_eq!(&*import.ns, record_ns, "record reference should target injected namespace"); - assert_eq!(&*import.def, record_def, "record reference should target injected definition"); - } - other => panic!("expected record reference import, got {other:?}"), - } - match head_nodes.get(2) { - Some(Calcit::Tag(tag)) => assert_eq!(tag, &EdnTag::from("greet")), - other => panic!("expected method tag, got {other:?}"), - }; - } - #[test] fn validates_method_field_access() { use cirru_edn::EdnTag; From 9750393d13a619c5565f5db02fb9f4059b7b8fae Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 17 Mar 2026 20:34:09 +0800 Subject: [PATCH 18/57] refactor: reduce schema-key hot path branches --- ...n-runtime-boundary-optimization-summary.md | 22 +++- src/calcit/type_annotation.rs | 46 ++++--- src/program.rs | 15 ++- src/runner/preprocess.rs | 118 ++++++++++-------- 4 files changed, 112 insertions(+), 89 deletions(-) diff --git a/editing-history/2026-0317-2026-afternoon-runtime-boundary-optimization-summary.md b/editing-history/2026-0317-2026-afternoon-runtime-boundary-optimization-summary.md index 01677da8..562f1a04 100644 --- a/editing-history/2026-0317-2026-afternoon-runtime-boundary-optimization-summary.md +++ b/editing-history/2026-0317-2026-afternoon-runtime-boundary-optimization-summary.md @@ -1,8 +1,8 @@ -# 2026-03-17 下午至晚间改动总结(1739-2023) +# 2026-03-17 下午至晚间改动总结(1739-2031) ## 总览 -- 时间段:`2026-0317-1739` 至 `2026-0317-2023` +- 时间段:`2026-0317-1739` 至 `2026-0317-2031` - 主要改动:测试减法、program/preprocess 架构减法、类型注解热路径优化、samply 验证、计划文档同步 ## 关键变更(按时间顺序) @@ -52,6 +52,20 @@ - 在 `parse_fn_annotation_from_schema_form` 中引入 `collect_fn_schema_fields`,由多次 key 扫描改为一次遍历收集。 - 删除无用 helper:`schema_has_any_field`。 +### 7) schema key 热路径进一步减法(2031) + +- 文件:`src/calcit/type_annotation.rs` +- 新增单 key 快路径: + - `schema_key_matches(...)` + - `extract_schema_value_single(...)` +- 将常见单 key 查询点改为快路径: + - `extract_return_type_from_hint_form` + - `extract_generics_from_hint_form` + - `extract_arg_types_from_hint_form` +- 清理过渡遗留 helper: + - `schema_key_matches_any(...)` + - `extract_schema_value(...)` + ## 验证汇总 - 多轮定向测试均通过: @@ -65,9 +79,9 @@ - 使用既有流程:`profiling/samply-once.sh` + `profiling/samply-summary.py` - 在 materialize 目标链路过滤中,样本权重由 14 降至 7(单轮观测,方向符合预期)。 -- `type_annotation::*` 热点仍可见,后续应继续压缩 schema key 匹配/提取路径分配与分支成本。 +- 在 schema-key 相关过滤中,基线 `fibo-release-iter5-20260317.samply` 为 21,本轮新采样 `fibo-release-20260317-203129.samply` 为 5,方向上显著下降。 ## 本轮经验 - 以“删 helper/删重复分支/删中间分配”为主线做减法,优先保证行为级测试覆盖。 -- 每次改动后固定执行“定向测试 → 全量 Rust → `yarn check-all`”可有效阻断语义回退。 \ No newline at end of file +- 每次改动后固定执行“定向测试 → 全量 Rust → `yarn check-all`”可有效阻断语义回退。 diff --git a/src/calcit/type_annotation.rs b/src/calcit/type_annotation.rs index 142dc1ed..a373e82e 100644 --- a/src/calcit/type_annotation.rs +++ b/src/calcit/type_annotation.rs @@ -358,16 +358,8 @@ impl CalcitTypeAnnotation { } } - fn schema_key_matches_any(form: &Calcit, keys: &[&str]) -> bool { - let Some(key) = Self::schema_key_name(form) else { - return false; - }; - match keys { - [first] => key == *first, - [first, second] => key == *first || key == *second, - [first, second, third] => key == *first || key == *second || key == *third, - _ => keys.contains(&key), - } + fn schema_key_matches(form: &Calcit, key: &str) -> bool { + matches!(Self::schema_key_name(form), Some(name) if name == key) } fn is_schema_map_literal_head(form: &Calcit) -> bool { @@ -379,11 +371,11 @@ impl CalcitTypeAnnotation { } } - fn extract_schema_value<'a>(form: &'a Calcit, keys: &[&str]) -> Option<&'a Calcit> { + fn extract_schema_value_single<'a>(form: &'a Calcit, key: &str) -> Option<&'a Calcit> { match form { Calcit::Map(xs) => { - for (key, value) in xs { - if Self::schema_key_matches_any(key, keys) { + for (entry_key, value) in xs { + if Self::schema_key_matches(entry_key, key) { return Some(value); } } @@ -401,13 +393,13 @@ impl CalcitTypeAnnotation { if pair.len() < 2 { continue; } - let Some(key) = pair.get(0) else { + let Some(entry_key) = pair.get(0) else { continue; }; let Some(value) = pair.get(1) else { continue; }; - if Self::schema_key_matches_any(key, keys) { + if Self::schema_key_matches(entry_key, key) { return Some(value); } } @@ -417,7 +409,16 @@ impl CalcitTypeAnnotation { } } - fn collect_fn_schema_fields<'a>(form: &'a Calcit) -> (bool, Option<&'a Calcit>, Option<&'a Calcit>, Option<&'a Calcit>, Option<&'a Calcit>, Option<&'a Calcit>) { + fn collect_fn_schema_fields<'a>( + form: &'a Calcit, + ) -> ( + bool, + Option<&'a Calcit>, + Option<&'a Calcit>, + Option<&'a Calcit>, + Option<&'a Calcit>, + Option<&'a Calcit>, + ) { let mut has_any = false; let mut generics = None; let mut args = None; @@ -497,7 +498,7 @@ impl CalcitTypeAnnotation { let generics = Self::extract_generics_from_hint_form(form).unwrap_or_default(); let items = Self::get_hint_fn_items(form)?; for item in items.iter().skip(1) { - if let Some(type_expr) = Self::extract_schema_value(item, &["return"]) { + if let Some(type_expr) = Self::extract_schema_value_single(item, "return") { return Some(CalcitTypeAnnotation::parse_type_annotation_form_with_generics( type_expr, generics.as_slice(), @@ -510,7 +511,7 @@ impl CalcitTypeAnnotation { pub fn extract_generics_from_hint_form(form: &Calcit) -> Option>> { let items = Self::get_hint_fn_items(form)?; for item in items.iter().skip(1) { - if let Some(value) = Self::extract_schema_value(item, &["generics"]) { + if let Some(value) = Self::extract_schema_value_single(item, "generics") { if let Some(vars) = Self::parse_generics_list(value) { return Some(vars); } @@ -622,9 +623,7 @@ impl CalcitTypeAnnotation { return Self::infer_malformed_fn_schema(form, generics, strict_named_refs); } - let local_generics = local_generics_form - .and_then(Self::parse_generics_list) - .unwrap_or_default(); + let local_generics = local_generics_form.and_then(Self::parse_generics_list).unwrap_or_default(); let scope = Self::extend_generics_scope(generics, local_generics.as_slice()); let arg_types = args_form .map(|args_form| Self::parse_schema_args_list(args_form, scope.as_slice(), strict_named_refs)) @@ -632,8 +631,7 @@ impl CalcitTypeAnnotation { let return_type = return_form .map(|item| Self::parse_type_annotation_form_inner(item, scope.as_slice(), strict_named_refs)) .unwrap_or_else(|| Arc::new(Self::Dynamic)); - let rest_type = rest_form - .map(|item| Self::parse_type_annotation_form_inner(item, scope.as_slice(), strict_named_refs)); + let rest_type = rest_form.map(|item| Self::parse_type_annotation_form_inner(item, scope.as_slice(), strict_named_refs)); let fn_kind = match kind_form { Some(Calcit::Tag(tag)) if tag.ref_str() == "macro" => SchemaKind::Macro, Some(Calcit::Symbol { sym, .. }) if matches!(sym.as_ref(), ":macro" | "macro") => SchemaKind::Macro, @@ -657,7 +655,7 @@ impl CalcitTypeAnnotation { let generics = Self::extract_generics_from_hint_form(form).unwrap_or_default(); let items = Self::get_hint_fn_items(form)?; for item in items.iter().skip(1) { - if let Some(args_form) = Self::extract_schema_value(item, &["args"]) { + if let Some(args_form) = Self::extract_schema_value_single(item, "args") { let types = Self::parse_schema_args_types(args_form, params.len(), generics.as_slice()); return Some(types); } diff --git a/src/program.rs b/src/program.rs index 8cfdb1d6..06ec2b01 100644 --- a/src/program.rs +++ b/src/program.rs @@ -450,9 +450,12 @@ fn materialize_compiled_executable_payload( match compiled.kind { CompiledDefKind::Proc | CompiledDefKind::Syntax => Some(Ok(compiled.preprocessed_code.to_owned())), - CompiledDefKind::Fn | CompiledDefKind::Macro => { - Some(runner::evaluate_expr(&compiled.preprocessed_code, &CalcitScope::default(), ns, call_stack)) - } + CompiledDefKind::Fn | CompiledDefKind::Macro => Some(runner::evaluate_expr( + &compiled.preprocessed_code, + &CalcitScope::default(), + ns, + call_stack, + )), CompiledDefKind::LazyValue | CompiledDefKind::Value => None, } }) @@ -463,11 +466,7 @@ fn materialize_compiled_executable_payload( result.map(Some).map_err(RuntimeResolveError::Eval) } -pub fn resolve_compiled_executable_def( - ns: &str, - def: &str, - call_stack: &CallStackList, -) -> Result, RuntimeResolveError> { +pub fn resolve_compiled_executable_def(ns: &str, def: &str, call_stack: &CallStackList) -> Result, RuntimeResolveError> { materialize_compiled_executable_payload(ns, def, call_stack) } diff --git a/src/runner/preprocess.rs b/src/runner/preprocess.rs index 4bd25d1e..708e51fc 100644 --- a/src/runner/preprocess.rs +++ b/src/runner/preprocess.rs @@ -14,9 +14,9 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::{cell::RefCell, vec}; +use cirru_edn::EdnTag; use im_ternary_tree::TernaryTreeList; use strum::ParseError; -use cirru_edn::EdnTag; type ScopeTypes = HashMap, Arc>; @@ -120,32 +120,31 @@ fn ensure_ns_def_preprocessed( return Ok(()); } - let Some(()) = with_preprocess_compile_guard(ns, def, || { - match program::lookup_def_code(ns, def) { - Some(code) => { - let next_stack = call_stack.extend(ns, def, StackKind::Fn, &code, &[]); + let Some(()) = with_preprocess_compile_guard(ns, def, || match program::lookup_def_code(ns, def) { + Some(code) => { + let next_stack = call_stack.extend(ns, def, StackKind::Fn, &code, &[]); - let mut scope_types = ScopeTypes::new(); - let context_label = format!("{ns}/{def}"); - let resolved_code = calcit::with_type_annotation_warning_context(context_label, || { - preprocess_expr(&code, &HashSet::new(), &mut scope_types, ns, check_warnings, &next_stack) - })?; - store_preprocessed_compiled_output(ns, def, &code, &resolved_code); + let mut scope_types = ScopeTypes::new(); + let context_label = format!("{ns}/{def}"); + let resolved_code = calcit::with_type_annotation_warning_context(context_label, || { + preprocess_expr(&code, &HashSet::new(), &mut scope_types, ns, check_warnings, &next_stack) + })?; + store_preprocessed_compiled_output(ns, def, &code, &resolved_code); - Ok(()) - } - None if ns.starts_with('|') || ns.starts_with('"') => Ok(()), - None => { - let loc = NodeLocation::new(Arc::from(ns), Arc::from(def), Arc::from(vec![])); - Err(CalcitErr::use_msg_stack_location( - CalcitErrKind::Var, - format!("unknown ns/def in program: {ns}/{def}"), - call_stack, - Some(loc), - )) - } + Ok(()) + } + None if ns.starts_with('|') || ns.starts_with('"') => Ok(()), + None => { + let loc = NodeLocation::new(Arc::from(ns), Arc::from(def), Arc::from(vec![])); + Err(CalcitErr::use_msg_stack_location( + CalcitErrKind::Var, + format!("unknown ns/def in program: {ns}/{def}"), + call_stack, + Some(loc), + )) } - })? else { + })? + else { return Ok(()); }; @@ -168,10 +167,12 @@ fn lookup_callable_ns_def_for_preprocess( call_stack: &CallStackList, ) -> Result, CalcitErr> { ensure_ns_def_compiled(raw_ns, raw_def, check_warnings, call_stack)?; - Ok(match program::resolve_compiled_executable_def(raw_ns, raw_def, call_stack).ok().flatten() { - value @ Some(Calcit::Macro { .. } | Calcit::Fn { .. }) => value, - _ => None, - }) + Ok( + match program::resolve_compiled_executable_def(raw_ns, raw_def, call_stack).ok().flatten() { + value @ Some(Calcit::Macro { .. } | Calcit::Fn { .. }) => value, + _ => None, + }, + ) } fn resolve_trait_def_from_source_code(code: &Calcit) -> Option { @@ -226,7 +227,9 @@ fn parse_trait_method_name_from_source(form: &Calcit) -> Option { } } -fn parse_trait_method_specs_from_source<'a>(items: impl Iterator) -> Option<(Vec, Vec>)> { +fn parse_trait_method_specs_from_source<'a>( + items: impl Iterator, +) -> Option<(Vec, Vec>)> { let mut methods: Vec = vec![]; let mut method_types: Vec> = vec![]; @@ -3812,7 +3815,9 @@ fn validate_def_schema_during_preprocess( #[cfg(test)] mod tests { use super::*; - use crate::calcit::{CalcitFn, CalcitFnArgs, CalcitFnUsageMeta, CalcitImport, CalcitMacro, CalcitRecord, CalcitScope, CalcitStruct, ImportInfo}; + use crate::calcit::{ + CalcitFn, CalcitFnArgs, CalcitFnUsageMeta, CalcitImport, CalcitMacro, CalcitRecord, CalcitScope, CalcitStruct, ImportInfo, + }; use crate::data::cirru::code_to_calcit; use cirru_parser::Cirru; use std::sync::{LazyLock, Mutex}; @@ -4155,33 +4160,37 @@ mod tests { }, ); - let call = Calcit::List(Arc::new(CalcitList::from(&[ - Calcit::Import(CalcitImport { - ns: Arc::from(ns), - def: Arc::from(def), - info: Arc::new(ImportInfo::NsReferDef { - at_ns: Arc::from("tests.caller"), - at_def: Arc::from("demo"), + let call = Calcit::List(Arc::new(CalcitList::from( + &[ + Calcit::Import(CalcitImport { + ns: Arc::from(ns), + def: Arc::from(def), + info: Arc::new(ImportInfo::NsReferDef { + at_ns: Arc::from("tests.caller"), + at_def: Arc::from("demo"), + }), + def_id: Some(def_id.0), }), - def_id: Some(def_id.0), - }), - Calcit::Number(1.0), - ][..]))); + Calcit::Number(1.0), + ][..], + ))); let inferred = infer_type_from_expr(&call, &ScopeTypes::new()).expect("infer import call type"); assert!(matches!(inferred.as_ref(), CalcitTypeAnnotation::Number)); - let call = Calcit::List(Arc::new(CalcitList::from(&[ - Calcit::Symbol { - sym: Arc::from(def), - info: Arc::new(CalcitSymbolInfo { - at_ns: Arc::from(ns), - at_def: Arc::from("demo"), - }), - location: None, - }, - Calcit::Number(1.0), - ][..]))); + let call = Calcit::List(Arc::new(CalcitList::from( + &[ + Calcit::Symbol { + sym: Arc::from(def), + info: Arc::new(CalcitSymbolInfo { + at_ns: Arc::from(ns), + at_def: Arc::from("demo"), + }), + location: None, + }, + Calcit::Number(1.0), + ][..], + ))); let inferred = infer_type_from_expr(&call, &ScopeTypes::new()).expect("infer symbol call type"); assert!(matches!(inferred.as_ref(), CalcitTypeAnnotation::Number)); @@ -4262,7 +4271,10 @@ mod tests { let warnings = RefCell::new(vec![]); let stack = CallStackList::default(); ensure_ns_def_compiled(ns, def, &warnings, &stack).expect("compile recursive source def"); - assert!(program::lookup_compiled_def(ns, def).is_some(), "recursive source def should compile once"); + assert!( + program::lookup_compiled_def(ns, def).is_some(), + "recursive source def should compile once" + ); } #[test] From 557e8b126e790b7d4ba226468c3909567cf706b1 Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 18 Mar 2026 02:10:46 +0800 Subject: [PATCH 19/57] Add chunked display for query and tree show --- docs/CalcitAgent.md | 66 ++- ...uery-def-tree-show-chunked-display-plan.md | 301 ++++++++++ src/bin/cli_handlers/chunk_display.rs | 525 ++++++++++++++++++ src/bin/cli_handlers/common.rs | 75 ++- src/bin/cli_handlers/mod.rs | 1 + src/bin/cli_handlers/query.rs | 75 ++- src/bin/cli_handlers/tree.rs | 98 ++-- src/calcit/type_annotation.rs | 82 +-- src/cli_args.rs | 26 +- src/program/tests.rs | 8 +- 10 files changed, 1119 insertions(+), 138 deletions(-) create mode 100644 drafts/query-def-tree-show-chunked-display-plan.md create mode 100644 src/bin/cli_handlers/chunk_display.rs diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index 095c796d..e7939462 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -14,9 +14,9 @@ ```bash # 搜索 → 修改 → 验证 -cr query search 'symbol' -f 'ns/def' # 1. 定位(输出:[3,2,1] in ...) -cr tree replace 'ns/def' -p '3,2,1' --leaf -e 'new' # 2. 修改 -cr tree show 'ns/def' -p '3,2,1' # 3. 验证(可选) +cr query search 'symbol' -f 'ns/def' # 1. 定位(输出:[3.2.1] in ...) +cr tree replace 'ns/def' -p '3.2.1' --leaf -e 'new' # 2. 修改 +cr tree show 'ns/def' -p '3.2.1' # 3. 验证(可选) ``` ### 三种搜索方式 @@ -160,7 +160,7 @@ Calcit 程序使用 `cr` 命令: - `-f ` - 过滤到特定命名空间或定义(可缩小范围提升速度) - `-l / --loose`:宽松匹配,包含模式 - `-d `:限制搜索深度 - - `-p `:从指定路径开始搜索(如 `"3,2,1"`) + - `-p `:从指定路径开始搜索(如 `"3.2.1"`,也兼容 `"3,2,1"`) - 返回:完整路径 + 父级上下文,多个匹配时自动显示批量替换命令 - 示例: - `cr query search 'println' -f app.main/main!` - 精确搜索(过滤到某定义) @@ -180,7 +180,7 @@ Calcit 程序使用 `cr` 命令: - `cr query search-expr 'dispatch! (:: :states)' -l` - 匹配 `dispatch! (:: :states data)` 类型的表达式 - `cr query search-expr 'memof1-call-by' -l` - 查找记忆化调用 -**搜索结果格式:** `[索引1,索引2,...] in 父级上下文`,可配合 `cr tree show -p ''` 查看节点。**修改代码时优先用 search 命令,比逐层导航快 10 倍。** +**搜索结果格式:** `[索引1.索引2...] in 父级上下文`,可配合 `cr tree show -p ''` 查看节点。逗号路径仍兼容,但文档与输出优先使用点号。**修改代码时优先用 search 命令,比逐层导航快 10 倍。** ### LLM 辅助:动态方法提示 @@ -277,7 +277,7 @@ cr query modules **核心概念:** -- 路径格式:逗号分隔的索引(如 `"3,2,1"`),空字符串 `""` 表示根节点 +- 路径格式:优先使用点号分隔的索引(如 `"3.2.1"`),逗号写法 `"3,2,1"` 仍兼容;空字符串 `""` 表示根节点 - `-p ''` 仅表示“根节点”,**不等于推荐的整定义重写方案**;要整体替换定义时,优先使用 `cr edit def --overwrite -f ` - 每个命令都有 `--help` 查看详细参数 - 命令执行后会显示 "Next steps" 提示下一步操作 @@ -332,13 +332,13 @@ cr query modules # 1. 快速定位目标节点(一步到位) cr query search 'target-symbol' -f namespace/def -# 输出:[3,2,5,1] in (fn (x) target-symbol ...) +# 输出:[3.2.5.1] in (fn (x) target-symbol ...) # 2. 直接修改(路径已知) -cr tree replace namespace/def -p '3,2,5,1' --leaf -e 'new-symbol' +cr tree replace namespace/def -p '3.2.5.1' --leaf -e 'new-symbol' # 3. 验证结果(可选) -cr tree show namespace/def -p '3,2,5,1' +cr tree show namespace/def -p '3.2.5.1' # ===== 方案 B:批量重命名(多处修改) ===== @@ -346,11 +346,11 @@ cr tree show namespace/def -p '3,2,5,1' # 1. 搜索所有匹配位置 cr query search 'old-name' -f namespace/def # 自动显示:4 处匹配,已按路径从大到小排序 -# [3,2,5,8] [3,2,5,2] [3,1,0] [2,1] +# [3.2.5.8] [3.2.5.2] [3.1.0] [2.1] # 2. 按提示从后往前修改(避免路径变化) -cr tree replace namespace/def -p '3,2,5,8' --leaf -e 'new-name' -cr tree replace namespace/def -p '3,2,5,2' --leaf -e 'new-name' +cr tree replace namespace/def -p '3.2.5.8' --leaf -e 'new-name' +cr tree replace namespace/def -p '3.2.5.2' --leaf -e 'new-name' # ... 继续按序修改 # 或:一次性替换所有匹配项 @@ -370,19 +370,19 @@ cr tree target-replace namespace/def --pattern 'old-symbol' -e 'new-symbol' --le # 1. 搜索包含特定模式的表达式 cr query search-expr "fn (task)" -f namespace/def -l -# 输出:[3,2,2,5,2,4,1] in (map $ fn (task) ...) +# 输出:[3.2.2.5.2.4.1] in (map $ fn (task) ...) # 2. 查看完整结构(可选) -cr tree show namespace/def -p '3,2,2,5,2,4,1' +cr tree show namespace/def -p '3.2.2.5.2.4.1' # 3. 修改整个表达式或子节点 -cr tree replace namespace/def -p '3,2,2,5,2,4,1,2' -e 'let ((x 1)) (+ x task)' +cr tree replace namespace/def -p '3.2.2.5.2.4.1.2' -e 'let ((x 1)) (+ x task)' ``` **关键技巧:** - **优先使用 `search` 系列命令**:比逐层导航快 10+ 倍,一步直达目标 -- **路径格式**:`"3,2,1"` 表示第3个子节点 → 第2个子节点 → 第1个子节点 +- **路径格式**:`"3.2.1"` 表示第3个子节点 → 第2个子节点 → 第1个子节点;`"3,2,1"` 仍兼容 - **批量修改自动提示**:搜索找到多处时,自动显示路径排序和批量替换命令 - **路径动态变化**:删除/插入后,同级后续索引会变化,按提示从后往前操作 - **批量执行不要用 `&&` 粘成一行**:尤其当 `-e` 内容里有引号、`|` 字符串或复杂表达式时,优先逐条执行,或写入 `-f ` 避免 shell 进入未闭合引号状态 @@ -447,26 +447,32 @@ cr tree replace namespace/def -p '3,2,2,5,2,4,1,2' -e 'let ((x 1)) (+ x task)' 当需要构造非常复杂的嵌套结构(例如递归循环、多级 `let` 或 `if`)时,直接通过 `-e` 传入单行 Cirru 代码容易遇到 shell 转义、括号对齐或长度限制等问题。推荐使用**分段占位组装**策略: +简单提示: + +- 占位符统一使用 `{{NAME}}` 风格,例如 `{{BODY}}`、`{{TRUE_BRANCH}}`; +- 大表达式可以先用 `cr query def ` 看整体分片,再用 `cr tree show -p ''` 深入某个片段; +- 真正填充时,优先用 `cr tree target-replace` 找占位符,不唯一时再退回路径替换。 + 1. **确立骨架**:先替换目标节点为一个带有占位符的简单 JSON 结构。 ```bash - cr tree replace ns/def -p '4,0' -j '["let", [["x", "1"]], "BODY"]' + cr tree replace ns/def -p '4.0' -j '["let", [["x", "1"]], "{{BODY}}"]' ``` 2. **定位占位符**:使用 `tree show` 确认占位符的具体路径。 ```bash - cr tree show ns/def -p '4,0' - # 输出显示 "BODY" 在索引 2,即路径 [4,0,2] + cr tree show ns/def -p '4.0' + # 输出显示 "{{BODY}}" 在索引 2,即路径 [4.0.2] ``` 3. **填充内容**:针对占位符路径进行下一层的精细替换。 - ```bash - cr tree replace ns/def -p '4,0,2' -j '["if", ["=", "x", "1"], "TRUE_BRANCH", "FALSE_BRANCH"]' - ``` + ```bash + cr tree replace ns/def -p '4.0.2' -j '["if", ["=", "x", "1"], "{{TRUE_BRANCH}}", "{{FALSE_BRANCH}}"]' + ``` -4. **递归迭代**:重复上述步骤直到所有占位符(`TRUE_BRANCH`, `FALSE_BRANCH` 等)都被替换为最终逻辑。 +4. **递归迭代**:重复上述步骤直到所有占位符(如 `{{TRUE_BRANCH}}`、`{{FALSE_BRANCH}}`)都被替换为最终逻辑。 **优势:** @@ -1035,10 +1041,10 @@ cr edit config init-fn app.main/main! ```bash # 1. 搜索并定位目标子表达式 cr query search-expr 'complex-call arg1' -f 'app.core/process-data' -l -# 输出示例:[3,2,1] in (let ((x ...)) ...) +# 输出示例:[3.2.1] in (let ((x ...)) ...) # 2. 提取为新定义(原位置自动替换为新名字 extracted-calc) -cr edit split-def 'app.core/process-data' -p '3,2,1' -n extracted-calc +cr edit split-def 'app.core/process-data' -p '3.2.1' -n extracted-calc # 3. 查看结果 cr query def 'app.core/extracted-calc' # 新定义 @@ -1234,7 +1240,9 @@ send-to-component! $ :: :clipboard/read text - **JSON 格式 (`-j / --json`, `-J`, `-e`)**: 字数上限 **2000**。 **大资源处理建议:** -如果需要修改复杂的长函数,不要尝试一次性替换整个定义。应先构建主体结构,使用占位符(如 `?PLACEHOLDER_FEATURE`, 注意避免重复),然后通过 `cr tree target-replace` 进行精准的分段替换. +如果需要修改复杂的长函数,不要尝试一次性替换整个定义。应先构建主体结构,使用占位符,统一写成 `{{PLACEHOLDER_FEATURE}}` 这种花括号形式,并注意避免重复,然后通过 `cr tree target-replace` 或按路径的 `cr tree replace` 做精准的分段替换。 + +补充提示:现在 `cr query def` 和 `cr tree show` 遇到大表达式时会自动输出分片结果。若你采用多阶段创建,建议从第一步就使用 `{{NAME}}` 风格占位符,这样后续在分片视图中更容易识别骨架、复制坐标并继续填充内容。 ### 5. 命名空间操作陷阱 ⭐⭐⭐ @@ -1419,10 +1427,10 @@ cr edit add-import 'app.core' -e 'app.util :refer $ calculate-discount' # 在函数体中使用新定义(先定位插入位置) cr query search 'total-price' -f 'app.core/checkout' -# 输出:[3,2,1] in (let ((total-price ...)) ...) +# 输出:[3.2.1] in (let ((total-price ...)) ...) # 修改调用 -cr tree replace 'app.core/checkout' -p '3,2,1' -e 'calculate-discount total-price 0.1' +cr tree replace 'app.core/checkout' -p '3.2.1' -e 'calculate-discount total-price 0.1' ``` ### 步骤 5:触发热更新并验证 @@ -1452,7 +1460,7 @@ cr edit rename 'app.util/calculte-discount' 'calculate-discount' # 函数参数顺序传错 → 定位并修改调用 cr query search 'calculate-discount' -f 'app.core/checkout' -cr tree replace 'app.core/checkout' -p '3,2,1' --leaf -e 'calculate-discount' +cr tree replace 'app.core/checkout' -p '3.2.1' --leaf -e 'calculate-discount' ``` --- diff --git a/drafts/query-def-tree-show-chunked-display-plan.md b/drafts/query-def-tree-show-chunked-display-plan.md new file mode 100644 index 00000000..c96c42ef --- /dev/null +++ b/drafts/query-def-tree-show-chunked-display-plan.md @@ -0,0 +1,301 @@ +# `cr query def` / `cr tree show` 分片展示方案 + +## 目标 + +当定义或子树比较小时,继续保持当前直接输出的体验。 + +当表达式足够大时,切换到“分片展示”模式,满足下面几点: + +- 保留一个可快速阅读的根视图; +- 用占位符替换体积过大的子树; +- 按坐标顺序输出被拆出的片段; +- 允许通过参数调整每个片段期望容纳的节点数; +- 输出与文档优先使用点号路径,同时兼容逗号路径输入。 + +## 选定案例 + +本次以 `find-children-diffs` 作为主案例。 + +原因: + +- 它已经足够大,能明显暴露当前整块输出的可读性问题; +- 它内部天然存在 `cond`、嵌套 `let`、局部分支组等适合作为切分边界的结构; +- 它贴近真实工作流,既适合人阅读,也适合作为 LLM 分治理解的目标。 + +## 对比结论 + +### 基线:直接整块输出 + +当前的 `cr query def` 会一次性打印完整定义。 + +当前的 `cr tree show` 会打印目标子树,然后列出直接子节点路径。 + +这个方案在小表达式上没有问题,但面对大表达式时会出现这些缺点: + +- 主流程和局部细节混在一起,不容易先抓到整体结构; +- 重复性的局部绑定会占掉大量屏幕; +- 用户必须反复在 `search`、`show`、原始代码块之间切换; +- LLM 容易被附近细节吸住,反而丢掉更高层的分支关系。 + +### 1 倍预算 + +较早实验里,`find-children-diffs` 的目标片段大小大约是 `24` 个节点。 + +这一版会被拆成 `14` 个片段,很多片段落在 `19-39` 节点之间。结构虽然有效,但整体仍然偏碎,不利于连续阅读。 + +### 2 倍预算 + +把目标片段大小提高到大约 `48` 个节点后,同一个案例会变成 `9` 个片段: + +- `75, 57, 55, 53, 53, 42, 41, 39, 35` + +这一版更适合作为默认方案,原因是: + +- 顶层控制流可以一眼看清; +- 局部初始化逻辑更容易整体保留; +- 片段数足够少,可以顺序浏览; +- 切分边界看起来更接近语义单元,而不是机械按大小切开。 + +## 表达式拆分示例 + +下面用一个简化的表达式说明“整块输出”和“分片输出”的区别。 + +原始表达式: + +```cirru +let + a $ compute-a x + b $ compute-b x + cond + ready? a b + handle-ready a b + fallback? + let + details $ build-details a b + view $ render-view details + emit! view + track! details + true + let + err $ build-error a b + warn err + recover err +``` + +分片展示后,根片段会更像这样: + +```cirru +ROOT at root +let + a $ compute-a x + b $ compute-b x + cond + ready? a b + handle-ready a b + fallback? + {{let_emit_track_01}}@2.2 + true + {{let_warn_recover_02}}@2.3 +``` + +再继续列出拆出的片段: + +```cirru +{{let_emit_track_01}} at 2.2 +let + details $ build-details a b + view $ render-view details + emit! view + track! details + +{{let_warn_recover_02}} at 2.3 +let + err $ build-error a b + warn err + recover err +``` + +这样做的好处是: + +- 根片段只保留主流程; +- 细节块被移动到后面,阅读顺序更清晰; +- 每个占位符都带精确坐标,仍然能回到原树上继续定位或编辑。 + +## 决策 + +默认采用“递归语义切分”作为大表达式的展示策略。 + +更具体地说: + +- 以现有递归语义启发式为基础; +- 只有当表达式超过某个可配置阈值时才启用分片; +- 默认预算采用比早期实验更宽松的配置; +- 保留 `--raw` 作为脚本和精确检查时的逃生口。 + +## 预期交互 + +### `cr query def` + +对于较小定义: + +- 保持现在的直接输出。 + +对于较大定义: + +- 元信息照常输出; +- 用 `Chunked Cirru:` 替代一整块超长 Cirru; +- 先打印根片段,再按坐标顺序打印拆出的片段; +- 每个片段展示:坐标、节点数、最大深度、Cirru 内容。 + +### `cr tree show` + +对于较小子树: + +- 保持现在的输出形状。 + +对于较大子树: + +- 先展示节点类型和子树统计; +- 再展示分片后的根预览; +- 后续继续列出拆出的片段; +- 子节点导航提示仍然基于原始树路径; +- 用户可以直接根据片段坐标继续向下钻取。 + +## CLI 参数设计 + +这些参数应同时提供给 `cr query def` 和 `cr tree show`: + +- `--chunk-target-nodes `:每个片段理想节点数; +- `--chunk-max-nodes `:停止继续细分前允许的软上限; +- `--chunk-trigger-nodes `:总节点数低于该值时不启用分片; +- `--raw`:强制回退到旧的整块输出。 + +当前建议默认值: + +- `chunk-target-nodes = 56` +- `chunk-max-nodes = 68` +- `chunk-trigger-nodes = 88` + +原因: + +- `target` 需要足够大,避免过度切碎; +- `max` 略高于 `target`,避免为了几颗节点继续无意义细分; +- `trigger` 需要过滤掉本来还能整体阅读的中等表达式。 + +## 路径方案 + +### 输出格式 + +所有新的展示输出统一优先使用点号路径,例如: + +- `3.2.4.1.1` + +这样和前面的分片研究保持一致,也更方便人和 LLM 直接复制。 + +### 输入兼容 + +路径输入保持两种写法都支持: + +- 逗号:`3,2,4,1,1` +- 点号:`3.2.4.1.1` + +规则: + +- 空字符串仍表示根节点; +- 同一路径里混用点号和逗号时直接报错,避免静默歧义; +- 文档只主推点号写法,逗号作为兼容行为说明一次即可。 + +## 内部实现计划 + +### 1. 提取可复用的分片模块 + +把展示侧的切分逻辑从实验脚本迁移到 Rust,形成独立的 CLI 辅助模块。 + +职责包括: + +- 计算节点统计信息; +- 收集候选子树; +- 基于递归语义策略选择切点; +- 生成占位符; +- 输出有序分解结果; +- 统一格式化路径。 + +### 2. 第一阶段只做展示层改造 + +第一阶段不修改 snapshot 存储、不修改 AST 语义、不修改编辑行为。 + +仅让 `query def` 与 `tree show` 使用新的分片渲染。 + +### 3. 集中处理路径兼容 + +扩展公共路径解析逻辑,让各 CLI handler 都能接受点号路径,而不是每个命令各自手写兼容。 + +同时增加统一的路径格式化函数,避免到处手写 `join(",")` 或 `join(".")`。 + +### 4. 更新命令处理逻辑 + +`handle_def`: + +- 先计算定义总节点数; +- 根据阈值决定直接输出还是分片输出; +- 保持 `--json` 行为不变。 + +`handle_show`: + +- 先计算目标子树节点数; +- 根据阈值决定直接输出还是分片输出; +- 保持子节点导航提示和替换提示。 + +### 5. 文档迁移 + +把文档示例统一调整为优先使用点号路径,例如: + +```bash +cr tree show ns/def -p '3.2.1' +``` + +逗号兼容只在合适位置说明一次,不重复铺满全文。 + +## 风险与应对 + +### 风险:展示过于“魔法化” + +应对: + +- 保留 `--raw`; +- 明确打印分片统计信息; +- 每个片段都保留精确坐标。 + +### 风险:点号路径与命名空间里的点冲突 + +应对: + +- 点号/逗号解析只作用于路径参数; +- 不把这套解析逻辑用于 `ns/def` 目标字符串。 + +### 风险:分片展示影响编辑工作流 + +应对: + +- 分片只影响展示,不影响数据结构; +- 真正的编辑命令仍然对原始树坐标生效。 + +## 推进顺序 + +1. 先写这份方案文档。 +2. 在 Rust 中加入可复用的分片与路径辅助逻辑。 +3. 把分片渲染接入 `cr query def`。 +4. 把分片渲染接入 `cr tree show`。 +5. 加入点号路径兼容。 +6. 更新文档,主推点号路径。 +7. 用 `find-children-diffs` 做真实案例验证,并微调默认值。 + +## 验收标准 + +- `cr query def` 在大定义上默认输出分片结果; +- `cr tree show` 在大子树上默认输出分片结果; +- 两个命令都支持 `--chunk-target-nodes`; +- 两个命令都接受点号路径; +- 逗号路径保持兼容; +- 文档示例优先使用点号路径; +- `--raw` 可以恢复旧的整块输出行为。 diff --git a/src/bin/cli_handlers/chunk_display.rs b/src/bin/cli_handlers/chunk_display.rs new file mode 100644 index 00000000..c16e7ca9 --- /dev/null +++ b/src/bin/cli_handlers/chunk_display.rs @@ -0,0 +1,525 @@ +use std::collections::HashSet; +use std::sync::Arc; + +use cirru_parser::Cirru; + +use super::common::format_path; + +const CORE_BOUNDARY_FORMS: &[&str] = &[ + "let", + "&let", + "cond", + "if", + "when", + "case-default", + "tag-match", + "list-match", + "fn", + "loop", + "do", + "->", +]; + +const CORE_FORM_ALIASES: &[(&str, &str)] = &[ + ("defn", "defn"), + ("defmacro", "macro"), + ("fn", "fn"), + ("let", "let"), + ("&let", "let"), + ("cond", "cond"), + ("if", "if"), + ("when", "when"), + ("case-default", "case"), + ("tag-match", "match"), + ("list-match", "list"), + ("loop", "loop"), + ("do", "do"), + ("->", "flow"), +]; + +#[derive(Clone, Debug)] +pub struct ChunkDisplayOptions { + pub trigger_nodes: usize, + pub target_nodes: usize, + pub max_nodes: usize, + pub max_branches: usize, +} + +impl Default for ChunkDisplayOptions { + fn default() -> Self { + Self { + trigger_nodes: 88, + target_nodes: 56, + max_nodes: 68, + max_branches: 64, + } + } +} + +#[derive(Clone, Debug)] +pub struct ChunkedDisplay { + pub total: NodeStats, + pub fragments: Vec, +} + +#[derive(Clone, Debug)] +pub struct RenderedFragment { + pub id: String, + pub coord: String, + pub nodes: usize, + pub depth: usize, + pub cirru: String, +} + +#[derive(Clone, Debug, Default)] +pub struct NodeStats { + pub nodes: usize, + pub branches: usize, + pub leaves: usize, + pub max_depth: usize, +} + +#[derive(Clone, Debug)] +struct Candidate { + path: Vec, + nodes: usize, + weighted_score: f64, +} + +#[derive(Clone, Debug)] +struct Profile { + max_fanout: usize, + max_branch_children: usize, +} + +#[derive(Clone, Debug)] +struct PendingFragment { + id: String, + path: Vec, + tree: Cirru, +} + +pub fn maybe_chunk_node(node: &Cirru, options: &ChunkDisplayOptions) -> Result, String> { + let total = collect_stats(node, 0); + if total.nodes < options.trigger_nodes { + return Ok(None); + } + + let fragments = recursive_partition(node, options)?; + if fragments.len() <= 1 { + return Ok(None); + } + + Ok(Some(ChunkedDisplay { + total, + fragments: build_ordered_decomposition(&fragments)?, + })) +} + +fn recursive_partition(root: &Cirru, options: &ChunkDisplayOptions) -> Result, String> { + let mut used_names = HashSet::new(); + let mut placeholder_index = 1; + let mut pending = vec![PendingFragment { + id: "ROOT".to_string(), + path: vec![], + tree: root.clone(), + }]; + let mut finished: Vec = vec![]; + + while !pending.is_empty() { + pending.sort_by(|left, right| collect_stats(&right.tree, 0).nodes.cmp(&collect_stats(&left.tree, 0).nodes)); + let mut current = pending.remove(0); + let current_stats = collect_stats(¤t.tree, 0); + + if current_stats.nodes <= options.max_nodes { + finished.push(current); + continue; + } + + let Some(cut) = pick_best_semantic_cut(¤t.tree, options) else { + finished.push(current); + continue; + }; + + let subtree = get_node(¤t.tree, &cut.path) + .cloned() + .ok_or_else(|| format!("Failed to get subtree at {}", format_path(&cut.path)))?; + let global_path = extend_path(¤t.path, &cut.path); + let placeholder = make_placeholder_name(&subtree, placeholder_index, &mut used_names); + placeholder_index += 1; + + let placeholder_leaf = Cirru::Leaf(Arc::from(format!("{placeholder}@{}", format_path(&global_path)))); + current.tree = replace_subtree(¤t.tree, &cut.path, &placeholder_leaf)?; + + pending.push(current); + pending.push(PendingFragment { + id: placeholder, + path: global_path, + tree: subtree, + }); + + if placeholder_index > options.max_branches { + break; + } + } + + finished.extend(pending); + Ok(finished) +} + +fn build_ordered_decomposition(fragments: &[PendingFragment]) -> Result, String> { + let mut root_fragment: Option = None; + let mut non_root: Vec = vec![]; + + for entry in fragments { + let stats = collect_stats(&entry.tree, 0); + let rendered = RenderedFragment { + id: entry.id.clone(), + coord: if entry.path.is_empty() { + "root".to_string() + } else { + format_path(&entry.path) + }, + nodes: stats.nodes, + depth: stats.max_depth, + cirru: format_cirru_fragment(&entry.tree)?, + }; + + if entry.path.is_empty() { + root_fragment = Some(rendered); + } else { + non_root.push(rendered); + } + } + + non_root.sort_by(|left, right| compare_coords(&left.coord, &right.coord)); + + let mut result = vec![root_fragment.ok_or_else(|| "Missing ROOT fragment".to_string())?]; + result.extend(non_root); + Ok(result) +} + +fn pick_best_semantic_cut(root: &Cirru, options: &ChunkDisplayOptions) -> Option { + let min_nodes = usize::max(6, (options.target_nodes as f64 * 0.65).floor() as usize); + let profile = build_profile(root); + let candidates = collect_candidates(root, min_nodes, 1, &profile, options.target_nodes); + let semantic_ceiling = options.max_nodes + 14; + let bounded: Vec = candidates.iter().filter(|item| item.nodes <= semantic_ceiling).cloned().collect(); + let pool = if bounded.is_empty() { candidates } else { bounded }; + + pool.into_iter().max_by(|left, right| { + left + .weighted_score + .partial_cmp(&right.weighted_score) + .unwrap_or(std::cmp::Ordering::Equal) + .then(left.nodes.cmp(&right.nodes)) + }) +} + +fn collect_candidates(root: &Cirru, min_nodes: usize, min_depth: usize, profile: &Profile, target_nodes: usize) -> Vec { + let mut output = vec![]; + collect_candidates_inner(root, &mut output, vec![], 0, None, min_nodes, min_depth, profile, target_nodes); + output +} + +#[allow(clippy::too_many_arguments)] +fn collect_candidates_inner( + node: &Cirru, + output: &mut Vec, + path: Vec, + depth: usize, + parent_nodes: Option, + min_nodes: usize, + min_depth: usize, + profile: &Profile, + target_nodes: usize, +) { + let Cirru::List(items) = node else { + return; + }; + + let stats = collect_stats(node, depth); + let branch_children: Vec<&Cirru> = items.iter().filter(|child| matches!(child, Cirru::List(_))).collect(); + let child_sizes: Vec = branch_children.iter().map(|child| collect_stats(child, 0).nodes).collect(); + let fanout = items.len(); + let branch_children_len = branch_children.len(); + let parent_share = parent_nodes.map(|size| stats.nodes as f64 / size as f64).unwrap_or(1.0); + let head_symbol = get_head_symbol(node); + + if !path.is_empty() && depth >= min_depth && stats.nodes >= min_nodes { + let size_fit = clamp01(1.0 - (stats.nodes as f64 - target_nodes as f64).abs() / target_nodes as f64); + let share_fit = clamp01(1.0 - (parent_share - 0.3).abs() / 0.22); + let fanout_score = clamp01(fanout as f64 / usize::max(2, profile.max_fanout) as f64); + let branchiness_score = clamp01(branch_children_len as f64 / usize::max(1, profile.max_branch_children) as f64); + let syntax_boundary_score = if head_symbol.as_deref().is_some_and(is_boundary_form) { + 1.0 + } else { + clamp01(0.3 + 0.4 * fanout_score + 0.3 * branchiness_score) + }; + let density_score = stats.branches as f64 / usize::max(1, stats.nodes) as f64; + let semantic_score = 0.28 * share_fit + + 0.22 * compute_entropy(&child_sizes) + + 0.2 * branchiness_score + + 0.18 * syntax_boundary_score + + 0.12 * density_score; + let weighted_score = 0.38 * size_fit + 0.62 * semantic_score; + + output.push(Candidate { + path: path.clone(), + nodes: stats.nodes, + weighted_score, + }); + } + + for (index, child) in items.iter().enumerate() { + let mut next_path = path.clone(); + next_path.push(index); + collect_candidates_inner( + child, + output, + next_path, + depth + 1, + Some(stats.nodes), + min_nodes, + min_depth, + profile, + target_nodes, + ); + } +} + +fn build_profile(root: &Cirru) -> Profile { + fn walk(node: &Cirru, max_fanout: &mut usize, max_branch_children: &mut usize) { + if let Cirru::List(items) = node { + let fanout = items.len(); + let branch_children = items.iter().filter(|child| matches!(child, Cirru::List(_))).count(); + *max_fanout = (*max_fanout).max(fanout); + *max_branch_children = (*max_branch_children).max(branch_children); + for child in items { + walk(child, max_fanout, max_branch_children); + } + } + } + + let mut max_fanout = 1; + let mut max_branch_children = 1; + walk(root, &mut max_fanout, &mut max_branch_children); + Profile { + max_fanout, + max_branch_children, + } +} + +fn collect_stats(node: &Cirru, depth: usize) -> NodeStats { + match node { + Cirru::Leaf(_) => NodeStats { + nodes: 1, + branches: 0, + leaves: 1, + max_depth: depth, + }, + Cirru::List(items) => { + let mut stats = NodeStats { + nodes: 1, + branches: 1, + leaves: 0, + max_depth: depth, + }; + for child in items { + let child_stats = collect_stats(child, depth + 1); + stats.nodes += child_stats.nodes; + stats.branches += child_stats.branches; + stats.leaves += child_stats.leaves; + stats.max_depth = stats.max_depth.max(child_stats.max_depth); + } + stats + } + } +} + +fn get_head_symbol(node: &Cirru) -> Option { + match node { + Cirru::List(items) => match items.first() { + Some(Cirru::Leaf(s)) => Some(s.to_string()), + _ => None, + }, + Cirru::Leaf(_) => None, + } +} + +fn get_node<'a>(node: &'a Cirru, path: &[usize]) -> Option<&'a Cirru> { + let mut current = node; + for index in path { + let Cirru::List(items) = current else { + return None; + }; + current = items.get(*index)?; + } + Some(current) +} + +fn replace_subtree(node: &Cirru, target_path: &[usize], replacement: &Cirru) -> Result { + fn inner(node: &Cirru, target_path: &[usize], replacement: &Cirru, path: &mut Vec) -> Result { + if path.as_slice() == target_path { + return Ok(replacement.clone()); + } + + match node { + Cirru::Leaf(_) => Ok(node.clone()), + Cirru::List(items) => { + let mut output = Vec::with_capacity(items.len()); + for (index, child) in items.iter().enumerate() { + path.push(index); + output.push(inner(child, target_path, replacement, path)?); + path.pop(); + } + Ok(Cirru::List(output)) + } + } + } + + inner(node, target_path, replacement, &mut vec![]) +} + +fn extend_path(left: &[usize], right: &[usize]) -> Vec { + let mut output = left.to_vec(); + output.extend_from_slice(right); + output +} + +fn compute_entropy(values: &[usize]) -> f64 { + let total: usize = values.iter().sum(); + if total == 0 || values.len() <= 1 { + return 0.0; + } + + let mut entropy = 0.0; + for value in values { + if *value == 0 { + continue; + } + let probability = *value as f64 / total as f64; + entropy -= probability * probability.log2(); + } + entropy / (values.len() as f64).log2() +} + +fn clamp01(value: f64) -> f64 { + value.clamp(0.0, 1.0) +} + +fn is_boundary_form(symbol: &str) -> bool { + CORE_BOUNDARY_FORMS.contains(&symbol) +} + +fn alias_token(token: &str) -> &str { + CORE_FORM_ALIASES + .iter() + .find_map(|(from, to)| (*from == token).then_some(*to)) + .unwrap_or(token) +} + +fn sanitize_token(token: &str) -> Option { + let alias = alias_token(token); + let cleaned = alias + .trim_start_matches(|c| [':', '\'', '"', '~', '&', '|'].contains(&c)) + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect::() + .trim_matches('_') + .to_string(); + if cleaned.is_empty() { + None + } else { + Some(cleaned.to_lowercase().chars().take(12).collect()) + } +} + +fn collect_name_tokens(node: &Cirru, limit: usize, depth_limit: usize, depth: usize, output: &mut Vec) { + if output.len() >= limit { + return; + } + match node { + Cirru::Leaf(s) => { + if let Some(token) = sanitize_token(s) { + if !output.contains(&token) { + output.push(token); + } + } + } + Cirru::List(items) => { + if depth >= depth_limit { + return; + } + for child in items { + collect_name_tokens(child, limit, depth_limit, depth + 1, output); + if output.len() >= limit { + break; + } + } + } + } +} + +fn make_placeholder_name(node: &Cirru, index: usize, used_names: &mut HashSet) -> String { + let mut tokens = vec![]; + collect_name_tokens(node, 2, 2, 0, &mut tokens); + let base = if tokens.is_empty() { + "branch".to_string() + } else { + tokens.join("_") + }; + let mut suffix = index; + let mut candidate = format!("{base}_{suffix:02}"); + while used_names.contains(&candidate) { + suffix += 1; + candidate = format!("{base}_{suffix:02}"); + } + used_names.insert(candidate.clone()); + format!("{{{{{candidate}}}}}") +} + +fn compare_coords(left: &str, right: &str) -> std::cmp::Ordering { + let left_parts: Vec = if left == "root" { + vec![] + } else { + left.split('.').filter_map(|item| item.parse::().ok()).collect() + }; + let right_parts: Vec = if right == "root" { + vec![] + } else { + right.split('.').filter_map(|item| item.parse::().ok()).collect() + }; + + for index in 0..left_parts.len().max(right_parts.len()) { + let left_value = left_parts.get(index).copied().unwrap_or(usize::MIN); + let right_value = right_parts.get(index).copied().unwrap_or(usize::MIN); + match left_value.cmp(&right_value) { + std::cmp::Ordering::Equal => continue, + other => return other, + } + } + std::cmp::Ordering::Equal +} + +fn format_cirru_fragment(node: &Cirru) -> Result { + cirru_parser::format(std::slice::from_ref(node), cirru_parser::CirruWriterOptions { use_inline: false }) + .map(|text| text.trim().to_string()) + .map_err(|e| format!("Failed to format chunked Cirru: {e}")) +} + +#[cfg(test)] +mod tests { + use super::{ChunkDisplayOptions, maybe_chunk_node}; + + fn parse_first(text: &str) -> cirru_parser::Cirru { + cirru_parser::parse(text).unwrap().into_iter().next().unwrap() + } + + #[test] + fn does_not_chunk_small_nodes() { + let node = parse_first("defn add (a b) &+ a b"); + let options = ChunkDisplayOptions::default(); + assert!(maybe_chunk_node(&node, &options).unwrap().is_none()); + } +} diff --git a/src/bin/cli_handlers/common.rs b/src/bin/cli_handlers/common.rs index dfac5994..a7c1bf90 100644 --- a/src/bin/cli_handlers/common.rs +++ b/src/bin/cli_handlers/common.rs @@ -48,27 +48,45 @@ pub fn cirru_to_json(node: &Cirru) -> String { serde_json::to_string_pretty(&cirru_to_json_value(node)).unwrap_or_else(|_| "[]".to_string()) } -/// Parse path string like "2,1,0" to Vec +pub fn format_path_with_separator(path: &[usize], separator: &str) -> String { + path.iter().map(|i| i.to_string()).collect::>().join(separator) +} + +pub fn format_path(path: &[usize]) -> String { + format_path_with_separator(path, ".") +} + +pub fn format_path_bracketed(path: &[usize]) -> String { + if path.is_empty() { + "root".to_string() + } else { + format!("[{}]", format_path(path)) + } +} + +/// Parse path string like "2,1,0" or "2.1.0" to Vec pub fn parse_path(path_str: &str) -> Result, String> { if path_str.is_empty() { return Ok(vec![]); } + let has_comma = path_str.contains(','); + let has_dot = path_str.contains('.'); + + if has_comma && has_dot { + return Err(format!( + "Invalid path '{path_str}': mixed separators are not allowed. Use either comma-separated or dot-separated coordinates." + )); + } + + let separator = if has_dot { '.' } else { ',' }; + path_str - .split(',') + .split(separator) .map(|s| s.trim().parse::().map_err(|e| format!("Invalid path index '{s}': {e}"))) .collect() } -/// Validate input source conflicts -pub fn validate_input_sources(sources: &[bool]) -> Result<(), String> { - let count = sources.iter().filter(|&&x| x).count(); - if count > 1 { - return Err(ERR_MULTIPLE_INPUT_SOURCES.to_string()); - } - Ok(()) -} - /// Validate input flag conflicts pub fn validate_input_flags(leaf_input: bool, json_input: bool) -> Result<(), String> { if leaf_input && json_input { @@ -77,6 +95,14 @@ pub fn validate_input_flags(leaf_input: bool, json_input: bool) -> Result<(), St Ok(()) } +pub fn validate_input_sources(sources: &[bool]) -> Result<(), String> { + if sources.iter().filter(|&&enabled| enabled).count() > 1 { + Err(ERR_MULTIPLE_INPUT_SOURCES.to_string()) + } else { + Ok(()) + } +} + /// Read code input from file, inline code, or json option. /// Exactly one input source should be used. pub fn read_code_input(file: &Option, code: &Option, json: &Option) -> Result, String> { @@ -300,3 +326,30 @@ pub fn parse_input_to_cirru( } } } + +#[cfg(test)] +mod tests { + use super::{format_path, format_path_bracketed, format_path_with_separator, parse_path}; + + #[test] + fn parses_comma_separated_paths() { + assert_eq!(parse_path("3,2,1").unwrap(), vec![3, 2, 1]); + } + + #[test] + fn parses_dot_separated_paths() { + assert_eq!(parse_path("3.2.1").unwrap(), vec![3, 2, 1]); + } + + #[test] + fn rejects_mixed_separators() { + assert!(parse_path("3,2.1").is_err()); + } + + #[test] + fn formats_paths_with_dot_by_default() { + assert_eq!(format_path(&[3, 2, 1]), "3.2.1"); + assert_eq!(format_path_bracketed(&[3, 2, 1]), "[3.2.1]"); + assert_eq!(format_path_with_separator(&[3, 2, 1], ","), "3,2,1"); + } +} diff --git a/src/bin/cli_handlers/mod.rs b/src/bin/cli_handlers/mod.rs index da90986e..94cc1e62 100644 --- a/src/bin/cli_handlers/mod.rs +++ b/src/bin/cli_handlers/mod.rs @@ -2,6 +2,7 @@ //! //! These handlers implement: query, docs, cirru, libs, edit, tree subcommands +mod chunk_display; mod cirru; mod cirru_validator; mod common; diff --git a/src/bin/cli_handlers/query.rs b/src/bin/cli_handlers/query.rs index 8e40f117..11fdc37f 100644 --- a/src/bin/cli_handlers/query.rs +++ b/src/bin/cli_handlers/query.rs @@ -2,9 +2,11 @@ //! //! Handles: cr query ns, defs, def, at, peek, examples, find, usages, pkg, config, error, modules +use super::chunk_display::{ChunkDisplayOptions, ChunkedDisplay, maybe_chunk_node}; +use super::common::{format_path_bracketed, parse_path}; use super::tips::{Tips, tip_prefer_oneliner_json, tip_query_defs_list, tip_query_ns_list}; use calcit::CalcitTypeAnnotation; -use calcit::cli_args::{QueryCommand, QuerySubcommand}; +use calcit::cli_args::{QueryCommand, QueryDefCommand, QuerySubcommand}; use calcit::load_core_snapshot; use calcit::snapshot; use calcit::util::string::strip_shebang; @@ -40,7 +42,7 @@ pub fn handle_query_command(cmd: &QueryCommand, input_path: &str) -> Result<(), QuerySubcommand::Modules(_) => handle_modules(input_path), QuerySubcommand::Def(opts) => { let (ns, def) = parse_target(&opts.target)?; - handle_def(input_path, ns, def, opts.json) + handle_def(input_path, ns, def, opts) } QuerySubcommand::Peek(opts) => { let (ns, def) = parse_target(&opts.target)?; @@ -444,7 +446,33 @@ fn handle_modules(input_path: &str) -> Result<(), String> { Ok(()) } -fn handle_def(input_path: &str, namespace: &str, definition: &str, show_json: bool) -> Result<(), String> { +fn render_chunked_display(display: &ChunkedDisplay) { + println!("{}", "Chunked Cirru:".bold()); + println!( + "{}", + format!( + "nodes: {}, branches: {}, leaves: {}, max depth: {}, fragments: {}", + display.total.nodes, + display.total.branches, + display.total.leaves, + display.total.max_depth, + display.fragments.len() + ) + .dimmed() + ); + println!(); + + for fragment in &display.fragments { + println!("{} {}", fragment.id.cyan().bold(), format!("at {}", fragment.coord).dimmed()); + println!("{}", format!("nodes: {}, max depth: {}", fragment.nodes, fragment.depth).dimmed()); + for line in fragment.cirru.lines() { + println!(" {line}"); + } + println!(); + } +} + +fn handle_def(input_path: &str, namespace: &str, definition: &str, opts: &QueryDefCommand) -> Result<(), String> { let snapshot = load_snapshot(input_path)?; let file_data = snapshot @@ -473,10 +501,6 @@ fn handle_def(input_path: &str, namespace: &str, definition: &str, show_json: bo println!("\n{} {}", "Examples:".bold(), code_entry.examples.len()); } - println!("\n{}", "Cirru:".bold()); - let cirru_str = cirru_parser::format(&[code_entry.code.clone()], true.into()).unwrap_or_else(|_| "(failed to format)".to_string()); - println!("{cirru_str}"); - println!("\n{}", "Schema:".bold()); if let CalcitTypeAnnotation::Fn(fn_annot) = code_entry.schema.as_ref() { let schema_str = match snapshot::schema_edn_to_cirru(&fn_annot.to_wrapped_schema_edn()) { @@ -488,7 +512,29 @@ fn handle_def(input_path: &str, namespace: &str, definition: &str, show_json: bo println!("{}", "(none)".dimmed()); } - if show_json { + if !opts.raw { + let chunk_options = ChunkDisplayOptions { + trigger_nodes: opts.chunk_trigger_nodes, + target_nodes: opts.chunk_target_nodes, + max_nodes: opts.chunk_max_nodes, + max_branches: 64, + }; + if let Some(display) = maybe_chunk_node(&code_entry.code, &chunk_options)? { + println!(); + render_chunked_display(&display); + } else { + println!("\n{}", "Cirru:".bold()); + let cirru_str = + cirru_parser::format(&[code_entry.code.clone()], true.into()).unwrap_or_else(|_| "(failed to format)".to_string()); + println!("{cirru_str}"); + } + } else { + println!("\n{}", "Cirru:".bold()); + let cirru_str = cirru_parser::format(&[code_entry.code.clone()], true.into()).unwrap_or_else(|_| "(failed to format)".to_string()); + println!("{cirru_str}"); + } + + if opts.json { println!("\n{}", "JSON:".bold()); let json = code_entry_to_json(code_entry); println!("{}", serde_json::to_string(&json).unwrap()); @@ -499,12 +545,12 @@ fn handle_def(input_path: &str, namespace: &str, definition: &str, show_json: bo "Try `cr query search -f '{namespace}/{definition}'` to find coordinates of a leaf node" )); tips.add(format!( - "Use `cr tree show {namespace}/{definition} -p '0'` to explore tree for editing" + "Use `cr tree show {namespace}/{definition} -p '0'` or `-p '0.1'` to explore tree for editing" )); if !code_entry.examples.is_empty() { tips.add(format!("Use `cr query examples {namespace}/{definition}` to view examples")); } - tips.append(tip_prefer_oneliner_json(show_json)); + tips.append(tip_prefer_oneliner_json(opts.json)); tips.print(); Ok(()) @@ -1123,8 +1169,7 @@ fn handle_search_leaf( if path_str.is_empty() { Some(vec![]) } else { - let path: Result, _> = path_str.split(',').map(|s| s.trim().parse::()).collect(); - Some(path.map_err(|e| format!("Invalid start path '{path_str}': {e}"))?) + Some(parse_path(path_str).map_err(|e| format!("Invalid start path '{path_str}': {e}"))?) } } else { None @@ -1147,11 +1192,7 @@ fn handle_search_leaf( } if let Some(ref path) = parsed_start_path { - let path_display = if path.is_empty() { - "root".to_string() - } else { - format!("[{}]", path.iter().map(|i| i.to_string()).collect::>().join(",")) - }; + let path_display = format_path_bracketed(path); println!(" {} {}", "Start path:".dimmed(), path_display.cyan()); } println!(); diff --git a/src/bin/cli_handlers/tree.rs b/src/bin/cli_handlers/tree.rs index d3bf5773..389c0a4e 100644 --- a/src/bin/cli_handlers/tree.rs +++ b/src/bin/cli_handlers/tree.rs @@ -1,7 +1,10 @@ use cirru_parser::Cirru; use colored::Colorize; -use super::common::{ERR_CODE_INPUT_REQUIRED, cirru_to_json, parse_input_to_cirru, parse_path, read_code_input}; +use super::chunk_display::{ChunkDisplayOptions, ChunkedDisplay, maybe_chunk_node}; +use super::common::{ + ERR_CODE_INPUT_REQUIRED, cirru_to_json, format_path, format_path_bracketed, parse_input_to_cirru, parse_path, read_code_input, +}; use super::tips::{Tips, tip_prefer_oneliner_json, tip_root_edit}; use crate::cli_args::{ TreeAppendChildCommand, TreeCommand, TreeDeleteCommand, TreeInsertAfterCommand, TreeInsertBeforeCommand, TreeInsertChildCommand, @@ -151,7 +154,7 @@ fn show_diff_preview(old_node: &Cirru, new_node: &Cirru, operation: &str, path: "\n{}: {} at path [{}]\n", "Preview".blue().bold(), operation, - path.iter().map(|i| i.to_string()).collect::>().join(",") + format_path(path) )); output.push('\n'); @@ -169,6 +172,32 @@ fn show_diff_preview(old_node: &Cirru, new_node: &Cirru, operation: &str, path: output } +fn render_chunked_display(display: &ChunkedDisplay) { + println!("{}", "Chunked preview".green().bold()); + println!( + "{}", + format!( + "nodes: {}, branches: {}, leaves: {}, max depth: {}, fragments: {}", + display.total.nodes, + display.total.branches, + display.total.leaves, + display.total.max_depth, + display.fragments.len() + ) + .dimmed() + ); + println!(); + + for fragment in &display.fragments { + println!("{} {}", fragment.id.cyan().bold(), format!("at {}", fragment.coord).dimmed()); + println!("{}", format!("nodes: {}, max depth: {}", fragment.nodes, fragment.depth).dimmed()); + for line in fragment.cirru.lines() { + println!(" {line}"); + } + println!(); + } +} + // ============================================================================ // Command handlers // ============================================================================ @@ -219,11 +248,7 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> let valid_node = navigate_to_path(&code_entry.code, valid_path).unwrap(); // Format the valid path display - let valid_path_display = if valid_path.is_empty() { - "root".to_string() - } else { - format!("[{}]", valid_path.iter().map(|i| i.to_string()).collect::>().join(",")) - }; + let valid_path_display = format_path_bracketed(valid_path); // Get preview of the valid node let node_preview = match &valid_node { @@ -254,12 +279,7 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> eprintln!( "{} View it with: {}", "→".cyan(), - format!( - "cr tree show {} -p '{}'", - opts.target, - valid_path.iter().map(|i| i.to_string()).collect::>().join(",") - ) - .cyan() + format!("cr tree show {} -p '{}'", opts.target, format_path(valid_path)).cyan() ); } Cirru::List(items) => { @@ -272,12 +292,7 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> eprintln!( "{} View it with: {}", "→".cyan(), - format!( - "cr tree show {} -p '{}'", - opts.target, - valid_path.iter().map(|i| i.to_string()).collect::>().join(",") - ) - .cyan() + format!("cr tree show {} -p '{}'", opts.target, format_path(valid_path)).cyan() ); // Show first few children as hints @@ -289,7 +304,7 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> let child_path = if valid_path.is_empty() { i.to_string() } else { - format!("{},{}", valid_path.iter().map(|i| i.to_string()).collect::>().join(","), i) + format!("{}.{}", format_path(valid_path), i) }; eprintln!(" [{}] {} {} -p '{}'", i, child_preview.yellow(), "->".dimmed(), child_path); } @@ -308,7 +323,7 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> let path_display = if path.is_empty() { "(root)".to_string() } else { - path.iter().map(|i| i.to_string()).collect::>().join(",") + format_path(&path) }; println!( "{}: {} path: [{}]", @@ -320,16 +335,28 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> let node_type = match &node { Cirru::Leaf(_) => "leaf", Cirru::List(items) => { + let chunk_options = ChunkDisplayOptions { + trigger_nodes: opts.chunk_trigger_nodes, + target_nodes: opts.chunk_target_nodes, + max_nodes: opts.chunk_max_nodes, + max_branches: 64, + }; + let chunked_display = if opts.raw { None } else { maybe_chunk_node(&node, &chunk_options)? }; + println!("{}: {} ({} items)", "Type".green().bold(), "list".yellow(), items.len()); println!(); - println!("{}:", "Cirru preview".green().bold()); - println!(" "); - let cirru_str = cirru_parser::format(std::slice::from_ref(&node), cirru_parser::CirruWriterOptions { use_inline: true }) - .map_err(|e| format!("Failed to format Cirru: {e}"))?; - for line in cirru_str.lines() { - println!(" {line}"); + if let Some(display) = chunked_display { + render_chunked_display(&display); + } else { + println!("{}:", "Cirru preview".green().bold()); + println!(" "); + let cirru_str = cirru_parser::format(std::slice::from_ref(&node), cirru_parser::CirruWriterOptions { use_inline: true }) + .map_err(|e| format!("Failed to format Cirru: {e}"))?; + for line in cirru_str.lines() { + println!(" {line}"); + } + println!(); } - println!(); if !items.is_empty() { println!("{}:", "Children".green().bold()); @@ -338,7 +365,7 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> let child_path = if opts.path.is_empty() { i.to_string() } else { - format!("{},{}", opts.path, i) + format!("{}.{}", format_path(&path), i) }; println!(" [{}] {} {} -p '{}'", i, type_str.yellow(), "->".dimmed(), child_path); } @@ -359,10 +386,15 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> " • Replace: {} {} -p '{}' {}", "cr tree replace".cyan(), opts.target, - opts.path, + format_path(&path), "-e 'cirru one-liner'".dimmed() ); - println!(" • Delete: {} {} -p '{}'", "cr tree delete".cyan(), opts.target, opts.path); + println!( + " • Delete: {} {} -p '{}'", + "cr tree delete".cyan(), + opts.target, + format_path(&path) + ); println!(); let mut tips = Tips::new(); tips.append(tip_prefer_oneliner_json(show_json)); @@ -382,12 +414,12 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> " • Replace: {} {} -p '{}' --leaf -e ''", "cr tree replace".cyan(), opts.target, - opts.path + format_path(&path) ); if !path.is_empty() { // Show parent path for context let parent_path = &path[..path.len() - 1]; - let parent_path_str = parent_path.iter().map(|i| i.to_string()).collect::>().join(","); + let parent_path_str = format_path(parent_path); println!( " • View parent: {} {} -p '{}'", "cr tree show".cyan(), diff --git a/src/calcit/type_annotation.rs b/src/calcit/type_annotation.rs index a373e82e..5af98b62 100644 --- a/src/calcit/type_annotation.rs +++ b/src/calcit/type_annotation.rs @@ -86,6 +86,16 @@ pub static DYNAMIC_TYPE: LazyLock> = LazyLock::new(|| pub(crate) type TypeBindings = HashMap, Arc>; +#[derive(Default)] +struct FnSchemaFields<'a> { + has_any: bool, + generics: Option<&'a Calcit>, + args: Option<&'a Calcit>, + returns: Option<&'a Calcit>, + rest: Option<&'a Calcit>, + kind: Option<&'a Calcit>, +} + /// Unified representation of type annotations propagated through preprocessing #[derive(Debug, Clone, PartialEq, Eq)] pub enum CalcitTypeAnnotation { @@ -409,22 +419,8 @@ impl CalcitTypeAnnotation { } } - fn collect_fn_schema_fields<'a>( - form: &'a Calcit, - ) -> ( - bool, - Option<&'a Calcit>, - Option<&'a Calcit>, - Option<&'a Calcit>, - Option<&'a Calcit>, - Option<&'a Calcit>, - ) { - let mut has_any = false; - let mut generics = None; - let mut args = None; - let mut returns = None; - let mut rest = None; - let mut kind = None; + fn collect_fn_schema_fields<'a>(form: &'a Calcit) -> FnSchemaFields<'a> { + let mut fields = FnSchemaFields::default(); let mut visit_pair = |key: &'a Calcit, value: &'a Calcit| { let Some(key_name) = Self::schema_key_name(key) else { @@ -432,33 +428,33 @@ impl CalcitTypeAnnotation { }; match key_name { "generics" => { - has_any = true; - if generics.is_none() { - generics = Some(value); + fields.has_any = true; + if fields.generics.is_none() { + fields.generics = Some(value); } } "args" => { - has_any = true; - if args.is_none() { - args = Some(value); + fields.has_any = true; + if fields.args.is_none() { + fields.args = Some(value); } } "return" => { - has_any = true; - if returns.is_none() { - returns = Some(value); + fields.has_any = true; + if fields.returns.is_none() { + fields.returns = Some(value); } } "rest" => { - has_any = true; - if rest.is_none() { - rest = Some(value); + fields.has_any = true; + if fields.rest.is_none() { + fields.rest = Some(value); } } "kind" => { - has_any = true; - if kind.is_none() { - kind = Some(value); + fields.has_any = true; + if fields.kind.is_none() { + fields.kind = Some(value); } } _ => {} @@ -473,7 +469,7 @@ impl CalcitTypeAnnotation { } Calcit::List(xs) => { if !matches!(xs.first(), Some(head) if Self::is_schema_map_literal_head(head)) { - return (false, None, None, None, None, None); + return FnSchemaFields::default(); } for entry in xs.iter().skip(1) { let Calcit::List(pair) = entry else { @@ -488,10 +484,10 @@ impl CalcitTypeAnnotation { visit_pair(key, value); } } - _ => return (false, None, None, None, None, None), + _ => return FnSchemaFields::default(), } - (has_any, generics, args, returns, rest, kind) + fields } pub fn extract_return_type_from_hint_form(form: &Calcit) -> Option> { @@ -618,21 +614,25 @@ impl CalcitTypeAnnotation { generics: &[Arc], strict_named_refs: bool, ) -> Option> { - let (has_schema_fields, local_generics_form, args_form, return_form, rest_form, kind_form) = Self::collect_fn_schema_fields(form); - if !has_schema_fields { + let fields = Self::collect_fn_schema_fields(form); + if !fields.has_any { return Self::infer_malformed_fn_schema(form, generics, strict_named_refs); } - let local_generics = local_generics_form.and_then(Self::parse_generics_list).unwrap_or_default(); + let local_generics = fields.generics.and_then(Self::parse_generics_list).unwrap_or_default(); let scope = Self::extend_generics_scope(generics, local_generics.as_slice()); - let arg_types = args_form + let arg_types = fields + .args .map(|args_form| Self::parse_schema_args_list(args_form, scope.as_slice(), strict_named_refs)) .unwrap_or_default(); - let return_type = return_form + let return_type = fields + .returns .map(|item| Self::parse_type_annotation_form_inner(item, scope.as_slice(), strict_named_refs)) .unwrap_or_else(|| Arc::new(Self::Dynamic)); - let rest_type = rest_form.map(|item| Self::parse_type_annotation_form_inner(item, scope.as_slice(), strict_named_refs)); - let fn_kind = match kind_form { + let rest_type = fields + .rest + .map(|item| Self::parse_type_annotation_form_inner(item, scope.as_slice(), strict_named_refs)); + let fn_kind = match fields.kind { Some(Calcit::Tag(tag)) if tag.ref_str() == "macro" => SchemaKind::Macro, Some(Calcit::Symbol { sym, .. }) if matches!(sym.as_ref(), ":macro" | "macro") => SchemaKind::Macro, _ => SchemaKind::Fn, diff --git a/src/cli_args.rs b/src/cli_args.rs index 429baacb..8c546de0 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -331,6 +331,18 @@ pub struct QueryDefCommand { /// also output JSON format for programmatic consumption #[argh(switch, short = 'j')] pub json: bool, + /// preferred nodes per display fragment when large expressions are chunked + #[argh(option, default = "56")] + pub chunk_target_nodes: usize, + /// stop recursive chunk splitting once fragments fall below this node count + #[argh(option, default = "68")] + pub chunk_max_nodes: usize, + /// only enable chunked display when total expression nodes reach this threshold + #[argh(option, default = "88")] + pub chunk_trigger_nodes: usize, + /// force raw full-definition display without chunking + #[argh(switch)] + pub raw: bool, } #[derive(FromArgs, PartialEq, Debug, Clone)] @@ -1108,7 +1120,7 @@ pub struct TreeShowCommand { /// target in format "namespace/definition" #[argh(positional)] pub target: String, - /// path to the node (comma-separated indices, e.g. "2,1,0") + /// path to the node (dot-separated preferred, comma-separated also accepted; e.g. "2.1.0") #[argh(option, short = 'p')] pub path: String, /// max depth for result preview (0 = unlimited, default 2) @@ -1117,6 +1129,18 @@ pub struct TreeShowCommand { /// also output JSON format for programmatic consumption #[argh(switch, short = 'j')] pub json: bool, + /// preferred nodes per display fragment when large expressions are chunked + #[argh(option, default = "56")] + pub chunk_target_nodes: usize, + /// stop recursive chunk splitting once fragments fall below this node count + #[argh(option, default = "68")] + pub chunk_max_nodes: usize, + /// only enable chunked display when total expression nodes reach this threshold + #[argh(option, default = "88")] + pub chunk_trigger_nodes: usize, + /// force raw subtree display without chunking + #[argh(switch)] + pub raw: bool, } /// copy node from one path to another within a definition diff --git a/src/program/tests.rs b/src/program/tests.rs index c94ebd24..78b557db 100644 --- a/src/program/tests.rs +++ b/src/program/tests.rs @@ -785,12 +785,8 @@ fn preprocess_ns_def_materializes_compiled_function_without_backfilling_runtime( let warnings = RefCell::new(vec![]); crate::runner::preprocess::ensure_ns_def_compiled("app.preprocess", "callable", &warnings, &CallStackList::default()) .expect("compiled function should materialize for preprocess"); - let value = resolve_compiled_executable_def( - "app.preprocess", - "callable", - &CallStackList::default(), - ) - .expect("lookup compiled function after ensure"); + let value = resolve_compiled_executable_def("app.preprocess", "callable", &CallStackList::default()) + .expect("lookup compiled function after ensure"); assert!(matches!(value, Some(Calcit::Fn { .. }))); assert_eq!(lookup_runtime_cell_by_id(def_id), Some(RuntimeCell::Cold)); From 70dad3c0695d0b24471115965bb258f987810ff0 Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 18 Mar 2026 14:29:02 +0800 Subject: [PATCH 20/57] docs: close agent workflow feedback gaps --- docs/CalcitAgent.md | 1599 ++++-------------------------------- docs/run/agent-advanced.md | 1581 +++++++++++++++++++++++++++++++++++ 2 files changed, 1758 insertions(+), 1422 deletions(-) create mode 100644 docs/run/agent-advanced.md diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index e7939462..f229450a 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -1,1550 +1,305 @@ -# Calcit 编程 Agent 指南 +# Calcit Agent 快速实践(局部查看与编辑优先) -本文档为 AI Agent 提供 Calcit 项目的操作指南。 +本文档面向 Agent/LLM 的高频工作流,目标是**更快定位、最小改动、低噪音验证**。 -## 🚀 快速开始(新 LLM 必读) +## Cirru 语法速览(先看这个) -**硬前置步骤:在执行任何 `cr edit` / `cr tree` 修改前,必须先运行一次 `cr docs agents --full`。** +结构化编辑依赖“树 + 路径”。先能读懂 Cirru,才能稳定算出路径坐标。 -这不是建议项,而是进入实际修改前的检查项。跳过这一步,往往会直接沿用旧用法假设,尤其容易误判 `cr tree replace -p ''`、imports 输入格式和 watcher 验收边界。 +- Cirru 是缩进风格的 S-expression,缩进层级就是树层级。 +- 行内空格分隔节点;嵌套表达式是子节点。 +- 常见字面量: + - `|text` 或 `"|text"`:字符串, 两者等价, 区别是后者能处理好空格. 其中 `"|t"` 在老代码也会用 "\"t" 写. + - `:tag`:tag + - `[]` / `{}`:集合构造 +- 你在 `cr query search` 里看到的 `[5.5.1.3]`,本质是“第 5 个子节点的第 5 个子节点的第 1 个子节点的第 3 个子节点”。 -**核心原则:用命令行工具(不要直接编辑文件),用 search 定位(比逐层导航快 10 倍)** +### 坐标如何从代码中读出来 -### 标准流程 - -```bash -# 搜索 → 修改 → 验证 -cr query search 'symbol' -f 'ns/def' # 1. 定位(输出:[3.2.1] in ...) -cr tree replace 'ns/def' -p '3.2.1' --leaf -e 'new' # 2. 修改 -cr tree show 'ns/def' -p '3.2.1' # 3. 验证(可选) -``` - -### 三种搜索方式 - -```bash -cr query search 'target' -f 'ns/def' # 搜索符号/字符串 -cr query search-expr 'fn (x)' -f 'ns/def' -l # 搜索代码结构 -cr tree replace-leaf 'ns/def' --pattern 'old' -e 'new' --leaf # 批量替换叶子节点 -``` - -### 效率对比 - -| 操作 | 传统方法 | search 方法 | 效率 | -| ---------- | ----------------------- | ------------------- | -------- | -| 定位符号 | 逐层 `tree show` 10+ 步 | `query search` 1 步 | **10倍** | -| 查找表达式 | 手动遍历代码 | `search-expr` 1 步 | **10倍** | -| 批量重命名 | 手动找每处 | 自动列出所有位置 | **5倍** | - ---- - -## ⚠️ 重要警告:禁止直接修改的文件 - -以下文件**严格禁止使用文本替换或直接编辑**: - -- **`compact.cirru`** - 这是 Calcit 程序的紧凑快照格式,必须使用 `cr edit` 相关命令进行修改 - -这两个文件的格式对空格和结构极其敏感,直接文本修改会破坏文件结构。请使用下面文档中的 CLI 命令进行代码查询和修改。 - -## Calcit 与 Cirru 的关系 - -- **Calcit** 是编程语言本身(一门类似 Clojure 的函数式编程语言) -- **Cirru** 是语法格式(缩进风格的 S-expression,类似去掉括号改用缩进的 Lisp) -- **关系**:Calcit 代码使用 Cirru 语法书写和存储 - -**具体体现:** - -- `compact.cirru` 使用 Cirru 语法存储, 尽量用 `cr edit` 和 `cr tree` 命令修改 -- `cr cirru` 工具用于 Cirru 语法与 JSON 的转换(帮助理解和生成代码) -- Cirru 语法特点: - - 用缩进代替括号(类似 Python/YAML) - - 字符串用前缀 `|` 或 `"` 标记(如 `|hello` 表示字符串 "hello") - - 单行用空格分隔元素(如 `defn add (a b) (+ a b)`) - -**类比理解:** - -- Python 语言 ← 使用 → Python 语法 -- Calcit 语言 ← 使用 → Cirru 语法 - -生成 Calcit 代码前,建议先运行 `cr cirru show-guide` 了解 Cirru 语法规则。 - ---- - -## Calcit CLI 命令 - -Calcit 程序使用 `cr` 命令: - -### 主要运行命令 - -- `cr` 或 `cr compact.cirru` - 代码解释执行,默认读取 config 执行 init-fn 定义的入口(默认单次执行后退出) -- `cr -w` 或 `cr --watch` - 解释执行监听模式(显式启用监听) -- `cr compact.cirru js` - 编译生成 JavaScript 代码(默认单次编译) -- `cr compact.cirru js -w` / `cr compact.cirru js --watch` - JS 监听编译模式 -- `cr compact.cirru ir` - 生成 program-ir.cirru(默认单次生成) -- `cr compact.cirru ir -w` / `cr compact.cirru ir --watch` - IR 监听生成模式 -- `cr -1 ` - 执行一次然后退出(兼容参数,当前默认行为已是 once) -- `cr --check-only` - 仅检查代码正确性,不执行程序 - - 对 init_fn 和 reload_fn 进行预处理验证 - - 输出:预处理进度、warnings、检查耗时 - - 用于 CI/CD 或快速验证代码修改 -- `cr js -1` - 检查代码正确性,生成 JavaScript(兼容参数,默认已是单次) -- `cr js --check-only` - 检查代码正确性,不生成 JavaScript -- `cr --no-tips ...` - 隐藏所有编辑/查询命令输出的 "Tips:" 提示行(适合脚本/Agent 使用) - - 示例:`cr --no-tips demos/compact.cirru query def calcit.core/foldl` -- `cr eval '' [--dep ...]` - 执行一段 Calcit 代码片段,用于快速验证写法 - - **不需要**项目 `compact.cirru`:core 内置函数(`range`、`+`、`map` 等)直接可用 - - 项目自定义函数不可直接 eval(代码未加载),需用 `--dep` 加载外部模块 - - `--dep` 参数可以加载 `~/.config/calcit/modules/` 中的模块(直接使用模块名),可多次使用 - - 示例:`cr eval 'range 5'`、`cr eval 'echo 1' --dep calcit.std` - -### 查询子命令 (`cr query`) - -这些命令用于查询项目信息: - -**项目全局分析:** - -- `cr analyze call-graph` - 分析从入口点开始的调用图结构 -- `cr analyze count-calls` - 统计每个定义的调用次数 - - _使用示例:_ - - ```bash - # 分析整个项目的调用图 - cr analyze call-graph - # 统计调用次数 - cr analyze count-calls - ``` - -**基础查询:** - -- `cr query ns [--deps]` - 列出项目中所有命名空间(--deps 包含依赖) -- `cr query ns ` - 读取命名空间详情(imports, 定义预览) -- `cr query defs ` - 列出命名空间中的定义 -- `cr query pkg` - 获取项目包名 -- `cr query config` - 读取项目配置(init_fn, reload_fn, version) -- `cr query error` - 读取 .calcit-error.cirru 错误堆栈文件 -- `cr query modules` - 列出项目依赖的模块(来自 compact.cirru 配置) - -**渐进式代码探索(Progressive Disclosure):** - -- `cr query peek ` - 查看定义签名(参数、文档、表达式数量),不返回完整实现体 - - 输出:Doc、Form 类型、参数列表、Body 表达式数量、首个表达式预览、Examples 数量 - - 用于快速了解函数接口,减少 token 消耗 -- `cr query def [-j]` - 读取定义的完整 Cirru 代码 - - 默认输出:Doc、Examples 数量、Cirru 格式代码 - - `-j` / `--json`:同时输出 JSON 格式(用于程序化处理) - - 推荐:LLM 直接读取 Cirru 格式即可,通常不需要 JSON -- `cr query schema [-j] [--no-tips]` - 读取定义当前的 schema - - 默认输出:Definition 标识 + schema 的 Cirru one-liner 预览 - - `-j` / `--json`:输出 schema 对应的 Cirru EDN 结构;无 schema 时输出 `nil` - - 适合在修改前确认 `:args` / `:return` / `:rest` 当前值 -- `cr query examples ` - 读取定义的示例代码 - - 输出:每个 example 的 Cirru 格式和 JSON 格式 - -**符号搜索与引用分析:** - -- `cr query find [--deps] [-f] [-n ]` - 跨命名空间搜索符号 - - 默认精确匹配:返回定义位置 + 所有引用位置(带上下文预览) - - `-f` / `--fuzzy`:模糊搜索,匹配 "namespace/definition" 格式的路径 - - `-n `:限制模糊搜索结果数量(默认 20) - - `--deps`:扩展到 `calcit.*` 内置核心命名空间(默认已包含项目 modules 依赖) -- `cr query usages [--deps]` - 查找定义的所有使用位置 - - 返回:引用该定义的所有位置(带上下文预览) - - 用于理解代码影响范围,重构前的影响分析 - - `--deps`:同上,扩展到 `calcit.*` 核心命名空间 - -**代码模式搜索(快速定位 ⭐⭐⭐):** - -- `cr query search [-f ] [-l]` - 搜索叶子节点(符号/字符串),比逐层导航快 10 倍 - - **搜索范围**:默认包含项目代码、全部 modules 依赖和 calcit.core 内置函数(无需 `--deps` 标志) - - `--entry `:额外加载 `entries..modules` 里的依赖(用于 entry 级依赖场景) - - `-f ` - 过滤到特定命名空间或定义(可缩小范围提升速度) - - `-l / --loose`:宽松匹配,包含模式 - - `-d `:限制搜索深度 - - `-p `:从指定路径开始搜索(如 `"3.2.1"`,也兼容 `"3,2,1"`) - - 返回:完整路径 + 父级上下文,多个匹配时自动显示批量替换命令 - - 示例: - - `cr query search 'println' -f app.main/main!` - 精确搜索(过滤到某定义) - - `cr query search 'comp-' -f app.ui/layout -l` - 模糊搜索(所有 comp- 开头) - - `cr query search 'task-id'` - 全项目搜索(含 modules) - -**高级结构搜索(搜索代码结构 ⭐⭐⭐):** - -- `cr query search-expr [-f ] [-l] [-j]` - 搜索结构表达式(List) - - **搜索范围**:同 `search`,默认包含全部依赖和 calcit.core - - `--entry `:同上,额外加载指定 entry 的 modules - - `-l / --loose`:宽松匹配,从头部开始的前缀匹配(嵌套表达式也支持前缀) - - `-j / --json`:将模式解析为 JSON 数组 - - 示例: - - `cr query search-expr 'fn (x)' -f app.main/process -l` - 查找函数定义 - - `cr query search-expr '>> state task-id' -l` - 查找状态访问(匹配 `>> state task-id ...` 或 `>> state`) - - `cr query search-expr 'dispatch! (:: :states)' -l` - 匹配 `dispatch! (:: :states data)` 类型的表达式 - - `cr query search-expr 'memof1-call-by' -l` - 查找记忆化调用 - -**搜索结果格式:** `[索引1.索引2...] in 父级上下文`,可配合 `cr tree show -p ''` 查看节点。逗号路径仍兼容,但文档与输出优先使用点号。**修改代码时优先用 search 命令,比逐层导航快 10 倍。** - -### LLM 辅助:动态方法提示 - -在运行时调试 trait 分派时,可使用以下内置函数(低频场景,需运行期有值后调用): - -- `&methods-of value` — 列出某值的可用方法名(返回字符串列表 `[] |.foo |.bar ...`) -- `&inspect-methods value` — 打印方法与 impl 来源(调试 trait override 链,可临时插入 pipeline) -- `&impl:origin impl` — 读取 impl record 的 trait 来源 -- `&trait-call Trait :method receiver & args` — 显式消歧:只调用属于指定 trait 的方法实现 - -> 📖 深入了解 trait 实现机制:`cr docs read traits.md` 或 `cr docs search 'trait-call'` - -### 文档子命令 (`cr docs`) - -查询 Calcit 语言文档(guidebook): - -- `cr docs search [-c ] [-f ]` - 按关键词搜索文档内容 - - `-c ` - 显示匹配行的上下文行数(默认 5) - - `-f ` - 按文件名过滤搜索结果 - - 输出:匹配行及其上下文,带行号和高亮 - - 示例:`cr docs search 'macro' -c 10` 或 `cr docs search 'defn' -f macros.md` - -- `cr docs read [ ...]` - 按 Markdown 标题阅读文档 - - 不传标题时:列出文档内所有标题 - - 传入一个或多个标题关键词时:按标题做模糊匹配并输出对应章节内容 - - 示例:`cr docs read macros.md` 或 `cr docs read run.md eval options` - -- `cr docs read-lines [-s ] [-n ]` - 按行读取文档(兼容旧行为) - - `-s ` - 起始行号(默认 0) - - `-n ` - 读取行数(默认 80) - - 输出:文档内容、当前范围、是否有更多内容 - - 示例:`cr docs read-lines intro.md -s 20 -n 30` - -- `cr docs list` - 列出所有可用文档 -- `cr docs agents [ ...] [--full]` - 读取 Agent 指南(即本文档,优先本地缓存,按天自动刷新) - - 不传标题时列出所有标题;传关键词时按标题模糊匹配输出对应章节 - -### Cirru 语法工具 (`cr cirru`) - -用于 Cirru 语法和 JSON 之间的转换: - -- `cr cirru parse ''` - 解析 Cirru 代码为 JSON -- `cr cirru format ''` - 格式化 JSON 为 Cirru 代码 -- `cr cirru parse-edn ''` - 解析 Cirru EDN 为 JSON -- `cr cirru show-guide` - 显示 Cirru 语法指南(帮助 LLM 生成正确的 Cirru 代码) - -**⚠️ 重要:生成 Cirru 代码前请先阅读语法指南** - -运行 `cr cirru show-guide` 获取完整的 Cirru 语法说明,包括: - -- `$` 操作符(单节点展开) -- `|` 前缀(字符串字面量), 这个是 Cirru 特殊的地方, 而不是直接用引号包裹 -- `,` 操作符(注释标记) -- `~` 和 `~@`(宏展开) -- 常见错误和避免方法 - -### 库管理 (`cr libs`) - -查询和了解 Calcit 官方库: - -- `cr libs` - 列出所有官方库 -- `cr libs search ` - 按关键词搜索库(搜索名称、描述、分类) -- `cr libs readme [-f ]` - 查看库的文档 - - 优先从本地 `~/.config/calcit/modules/` 读取 - - 本地不存在时从 GitHub 仓库获取 - - `-f` 参数可指定其他文档文件(如 `-f Skills.md`) - - 默认读取 `README.md` -- `cr libs scan-md ` - 扫描本地模块目录下的所有 `.md` 文件 - - 递归扫描子目录 - - 显示相对路径列表 -- `caps` - 安装/更新依赖(默认读取 `deps.cirru`,也可传自定义文件路径) - - 独立工具(非 `cr` 子命令) - - `caps`:按 `deps.cirru` 当前依赖执行更新 - - `caps add /`:添加依赖并执行默认更新流程 - - `caps remove /`:移除依赖并执行默认更新流程 - - `caps add/remove` 同时支持完整 GitHub 地址(如 `https://github.com/calcit-lang/memof`) - - `caps add -r `:写入指定分支/版本(默认 `main`) - -**查看已安装模块:** - -```bash -# 列出 ~/.config/calcit/modules/ 下所有已安装的模块 -ls ~/.config/calcit/modules/ - -# 查看当前项目配置的模块依赖 -cr query modules -``` - -### 精细代码树操作 (`cr tree`) - -⚠️ **关键警告:路径索引动态变化** - -删除或插入节点后,同级后续节点的索引会自动改变。**必须从后往前操作**或**每次修改后重新搜索路径**。 - -**核心概念:** - -- 路径格式:优先使用点号分隔的索引(如 `"3.2.1"`),逗号写法 `"3,2,1"` 仍兼容;空字符串 `""` 表示根节点 -- `-p ''` 仅表示“根节点”,**不等于推荐的整定义重写方案**;要整体替换定义时,优先使用 `cr edit def --overwrite -f ` -- 每个命令都有 `--help` 查看详细参数 -- 命令执行后会显示 "Next steps" 提示下一步操作 - -**主要操作:** - -- `cr tree show -p '' [-j]` - 查看节点 - - 默认输出:节点类型、Cirru 预览、子节点索引列表、操作提示 - - `-j` / `--json`:同时输出 JSON 格式(用于程序化处理) - - 推荐:直接查看 Cirru 格式即可,通常不需要 JSON -- `cr tree replace` - 替换节点 - - 适合局部节点替换;若目标是**整条定义**,优先改用 `cr edit def --overwrite -f `,比 `cr tree replace -p ''` 更可预期 -- `cr tree replace-leaf` - 查找并替换所有匹配的 leaf 节点(无需指定路径) - - `--pattern ` - 要搜索的模式(精确匹配 leaf 节点) - - 使用 `-e, -f, -j` 等通用参数提供替换内容 - - 自动遍历整个定义,一次性替换所有匹配项 - - 示例:`cr tree replace-leaf 'ns/def' --pattern 'old-name' -e 'new-name' --leaf` -- `cr tree target-replace` - 基于内容的唯一替换(无需指定路径,更安全 ⭐⭐⭐) - - `--pattern ` - 要搜索的模式(精确匹配 leaf 节点) - - 使用 `-e, -f, -j` 等通用参数提供替换内容 - - 逻辑:自动查找叶子节点,若唯一则替换;若不唯一则报错并列出所有位置及修改命令建议。 -- `cr tree delete -p ''` - 删除指定路径节点(⚠️ 后续同级索引自动减小) - - 示例:`cr tree delete app.core/fn -p '3,2'` -- `cr tree insert-before -p ''` / `cr tree insert-after` - 在路径节点的前/后插入兄弟节点 - - 示例:`cr tree insert-before app.core/fn -p '3,2' -e 'new-expr'` -- `cr tree insert-child -p ''` / `cr tree append-child` - 在某节点内部最前/最后插入子节点 - - 示例:`cr tree append-child app.core/fn -p '3' --leaf -e 'new-arg'` -- `cr tree swap-next -p ''` / `cr tree swap-prev` - 将节点与其下一个/上一个兄弟节点交换位置 -- `cr tree rewrite` - 用引用原节点的新结构替换节点(`--with` 必须;需引用子节点时使用) -- `cr tree wrap` - 快捷包裹节点:将 `self` 替换为原节点(`rewrite --with self=.` 的语法糖) -- `cr tree unwrap` - 将节点的所有子节点展开拼接到父节点中(拆包),原节点消失 -- `cr tree raise` - 用指定子节点替换其父节点(Paredit raise-sexp) - -**输入方式(通用):** - -- `-e ''` - 内联代码(自动识别 Cirru/JSON) -- `--leaf` - 强制作为 leaf 节点(符号或字符串) -- `-j ''` / `-f ` - -简单更新尽量用结构化的 API 操作. 多行或者带特殊符号的表达式, 可以在 `.calcit-snippets/` 创建临时文件, 然后用 `cr cirru parse` 验证语法, 最后用 `-f ` 提交, 从而减少错误率. 复杂表达式建议分段, 然后搭配 `cr tree target-replace` 命令来完成多阶段提交. - -**整体替换定义的经验规则:** - -- 局部节点修改:继续使用 `cr tree replace -p ''` -- 整条定义重写:优先使用 `cr edit def --overwrite -f ` -- 只有在你明确知道根节点替换后的结构,并且能立刻验证完整定义时,才考虑 `cr tree replace -p ''` - -**推荐工作流(高效定位 ⭐⭐⭐):** - -```bash -# ===== 方案 A:单点修改(精确定位) ===== - -# 1. 快速定位目标节点(一步到位) -cr query search 'target-symbol' -f namespace/def -# 输出:[3.2.5.1] in (fn (x) target-symbol ...) - -# 2. 直接修改(路径已知) -cr tree replace namespace/def -p '3.2.5.1' --leaf -e 'new-symbol' - -# 3. 验证结果(可选) -cr tree show namespace/def -p '3.2.5.1' - - -# ===== 方案 B:批量重命名(多处修改) ===== - -# 1. 搜索所有匹配位置 -cr query search 'old-name' -f namespace/def -# 自动显示:4 处匹配,已按路径从大到小排序 -# [3.2.5.8] [3.2.5.2] [3.1.0] [2.1] - -# 2. 按提示从后往前修改(避免路径变化) -cr tree replace namespace/def -p '3.2.5.8' --leaf -e 'new-name' -cr tree replace namespace/def -p '3.2.5.2' --leaf -e 'new-name' -# ... 继续按序修改 - -# 或:一次性替换所有匹配项 -cr tree replace-leaf namespace/def --pattern 'old-name' -e 'new-name' --leaf - - -# ===== 方案 C:基于内容的半自动替换(最推荐 ⭐⭐⭐) ===== - -# 1. 尝试基于叶子节点内容直接替换 -cr tree target-replace namespace/def --pattern 'old-symbol' -e 'new-symbol' --leaf - -# 2. 如果存在多个匹配,命令会报错并给出详细指引(包含具体路径的 replace 命令建议) -# 如果确定要全部替换,可改用 tree replace-leaf - - -# ===== 方案 D:结构搜索(查找表达式) ===== - -# 1. 搜索包含特定模式的表达式 -cr query search-expr "fn (task)" -f namespace/def -l -# 输出:[3.2.2.5.2.4.1] in (map $ fn (task) ...) - -# 2. 查看完整结构(可选) -cr tree show namespace/def -p '3.2.2.5.2.4.1' - -# 3. 修改整个表达式或子节点 -cr tree replace namespace/def -p '3.2.2.5.2.4.1.2' -e 'let ((x 1)) (+ x task)' -``` - -**关键技巧:** - -- **优先使用 `search` 系列命令**:比逐层导航快 10+ 倍,一步直达目标 -- **路径格式**:`"3.2.1"` 表示第3个子节点 → 第2个子节点 → 第1个子节点;`"3,2,1"` 仍兼容 -- **批量修改自动提示**:搜索找到多处时,自动显示路径排序和批量替换命令 -- **路径动态变化**:删除/插入后,同级后续索引会变化,按提示从后往前操作 -- **批量执行不要用 `&&` 粘成一行**:尤其当 `-e` 内容里有引号、`|` 字符串或复杂表达式时,优先逐条执行,或写入 `-f ` 避免 shell 进入未闭合引号状态 -- 所有命令都会显示 Next steps 和操作提示 - -**结构化变更示例:** - -`cr tree rewrite` 用于在替换时引用原节点及其子节点,必须传至少一个 `--with name=path`(不需要引用时直接用 `replace`)。`--with` 格式:`name=path`,`.` 表示原节点本身,数字表示子节点索引。 - -- **包裹节点**(推荐用 `wrap`,`self` 作为占位符): - - ```bash - # 将路径 "3,2" 的节点包裹在 println 中(self = 原节点) - cr tree wrap ns/def -p '3,2' -e 'println self' - - # 等价的完整写法(需要引用子节点时才用 rewrite) - cr tree rewrite ns/def -p '3,2' -e 'println self' -w 'self=.' - ``` - -- **引用原节点局部**(`rhs=2` 引用子节点索引 2): - - 假设原节点是 `+ 1 2`(路径 `3,1`),子节点索引 2 是 `2` - - 将其重构为 `* rhs 10`: - - ```bash - cr tree rewrite ns/def -p '3,1' -e '* rhs 10' -w 'rhs=2' - ``` - -- **多处重用原节点**: - - ```bash - # 将节点变为 `+ x x`(self 引用原节点本身) - cr tree rewrite ns/def -p '2' -e '+ self self' -w 'self=.' - ``` - -- **拆包节点**(`unwrap`)——将节点的所有子节点展开拼接到父节点中,原节点消失: - - ```bash - # 将路径 "3,2" 的节点拆包,所有子节点直接插入到原位置 - cr tree unwrap ns/def -p '3,2' - ``` - - 详细参数和示例使用 `cr tree --help` 查看。 - -- **提升子节点替换父节点**(`raise`)——用某子节点整体替换掉其父节点(Paredit `raise-sexp`): - - ```bash - # 路径 "3,2" 的节点整体替换掉其父节点 "3" - # 使用场景:去掉 let 外层只保留返回值,或去掉 if 只保留 then/else 分支 - cr tree raise ns/def -p '3,2' - ``` - -- **提取子表达式为新定义**(`split-def`)——将某路径的子表达式提取为同 ns 的新定义,原位替换为新名字: - - ```bash - # 将路径 "3,2" 的子表达式提取为新定义 compute-helper(同 namespace) - cr edit split-def app.util/process -p '3,2' -n compute-helper - ``` - - 详细参数使用 `cr edit split-def --help` 查看。 - -### 复杂表达式分段组装策略 (Incremental Assembly) ⭐⭐⭐ - -当需要构造非常复杂的嵌套结构(例如递归循环、多级 `let` 或 `if`)时,直接通过 `-e` 传入单行 Cirru 代码容易遇到 shell 转义、括号对齐或长度限制等问题。推荐使用**分段占位组装**策略: - -简单提示: - -- 占位符统一使用 `{{NAME}}` 风格,例如 `{{BODY}}`、`{{TRUE_BRANCH}}`; -- 大表达式可以先用 `cr query def ` 看整体分片,再用 `cr tree show -p ''` 深入某个片段; -- 真正填充时,优先用 `cr tree target-replace` 找占位符,不唯一时再退回路径替换。 - -1. **确立骨架**:先替换目标节点为一个带有占位符的简单 JSON 结构。 - - ```bash - cr tree replace ns/def -p '4.0' -j '["let", [["x", "1"]], "{{BODY}}"]' - ``` - -2. **定位占位符**:使用 `tree show` 确认占位符的具体路径。 - - ```bash - cr tree show ns/def -p '4.0' - # 输出显示 "{{BODY}}" 在索引 2,即路径 [4.0.2] - ``` - -3. **填充内容**:针对占位符路径进行下一层的精细替换。 - - ```bash - cr tree replace ns/def -p '4.0.2' -j '["if", ["=", "x", "1"], "{{TRUE_BRANCH}}", "{{FALSE_BRANCH}}"]' - ``` - -4. **递归迭代**:重复上述步骤直到所有占位符(如 `{{TRUE_BRANCH}}`、`{{FALSE_BRANCH}}`)都被替换为最终逻辑。 - -**优势:** - -- **精确性**:使用 JSON 格式 (`-j`) 可以完全避免 Cirru 缩进或括号解析的歧义。 -- **低风险**:每次只修改一小部分,出错时容易通过 `tree show` 快速定位。 -- **绕过限制**:解决某些终端对超长命令行参数的限制。 - -### 代码编辑 (`cr edit`) - -直接编辑 compact.cirru 项目代码,支持两种输入方式: - -- `--file ` 或 `-f ` - 从文件读取(默认 Cirru 格式,使用 `-J` 指定 JSON) -- `--json ` 或 `-j ` - 内联 JSON 字符串 - -额外支持“内联代码”参数: - -- `--code ` 或 `-e `:直接在命令行里传入一段代码。 - - 默认按 **Cirru 单行表达式(one-liner)** 解析。 - - 如果输入“看起来像 JSON”(例如 `-e '"abc"'`,或 `-e '["a"]'` 这类 `[...]` 且包含 `"`),则会按 JSON 解析。 - - ⚠️ 当输入看起来像 JSON 但 JSON 不合法时,会直接报错(不会回退当成 Cirru one-liner)。 - -对 `--file` 输入,还支持以下“格式开关”(与 `-J/--json-input` 类似): - -- `--leaf`:把输入当成 **leaf 节点**,直接使用 Cirru 符号或 `|text` 字符串,无需 JSON 引号。 - - 传入符号:`-e 'my-symbol'` - - 传入字符串:加 Cirru 字符串前缀 `|` 或 `"`,例如 `-e '|my string'` 或 `-e '"my string'` - -⚠️ 注意:这些开关彼此互斥(一次只用一个)。 - -**推荐简化规则(命令行更好写):** - -- **JSON(单行)**:优先用 `-j ''` 或 `-e ''`(不需要 `-J`)。 -- **Cirru 单行表达式**:用 `-e ''`(`-e` 默认按 one-liner 解析)。 -- **Cirru 多行缩进**:用 `-f file.cirru`。 -- `-J/--json-input` 主要用于 **file** 读入 JSON(如 `-f code.json -J`)。 - -补充:`-e/--code` 只有在 `[...]` 内部包含 `"` 时才会自动按 JSON 解析(例如 `-e '["a"]'`)。 -像 `-e '[]'` / `-e '[ ]'` 会默认按 Cirru one-liner 处理;如果你需要“空 JSON 数组”,用显式 JSON:`-j '[]'`。 - -如果你想在命令行里明确“这段就是 JSON”,请用 `-j ''`(`-J` 是给 file 用的)。 - -**定义操作:** - -- `cr edit format` - 不修改语义,按当前快照序列化逻辑重写 **snapshot 文件**(用于刷新格式) - - 也会把旧的 namespace `CodeEntry` 写法收敛成当前的 `NsEntry` 结构 - - 适用:普通 `compact.cirru` / 项目 snapshot 文件 - - 不适用:calcit-editor 专用的 `calcit.cirru` 结构文件 -- `cr edit def ` - 添加新定义(默认若已存在会报错;加 `--overwrite` 可强制覆盖) - - 经验语义:**不带 `--overwrite` = create-only;带 `--overwrite` = replace existing definition** - - 若当前输出文案仍显示 `Created definition`,以你的调用方式和目标是否已存在为准理解,不要把该提示字面理解为“必然新增成功” -- `cr edit rename ` - 在当前命名空间内重命名定义(不可覆盖) -- `cr edit mv-def ` - 将定义移动到另一个命名空间(跨命名空间移动) -- `cr edit cp --from -p [--at ]` - 在定义内复制 AST 节点到另一位置 -- `cr edit mv --from -p [--at ]` - 在定义内移动 AST 节点(复制后删除原位置;自动防止移入自身子树) -- `cr edit split-def -p -n ` - 将定义内某路径的子表达式提取为同命名空间内的新定义,原位置替换为新定义名称(新名称不可与已有定义重名) -- `cr edit rm-def ` - 删除定义 -- `cr edit doc ''` - 更新定义的文档 -- `cr edit schema ` - 更新定义 schema(写入前会校验 schema 结构) - - 常用输入:`-e ':: :fn $ {} (:args $ [] :number :number) (:return :number)'` - - 也支持 `-f ` / `-j ''` / `-J` / `--leaf` - - `--clear`:清空 schema,恢复为 `nil` - - 写入后会保存为直接 map 形式;后续运行与 preprocess 会用它做 `defn` / `defmacro` 一致性校验 -- `cr edit examples ` - 设置定义的示例代码(批量替换) -- `cr edit add-example ` - 添加单个示例 -- `cr edit rm-example ` - 删除指定索引的示例(0-based) - -**命名空间操作:** - -> ⚠️ **关键:各命令的 `-e` 期望格式不同,不可混用,详见下方「命名空间操作陷阱」** - -- `cr edit add-ns ` - 添加命名空间 - - 无 `-e`:创建空 ns(推荐;再用 `add-import` 逐条添加) - - `-e 'ns my.ns $ :require ...'`:需传完整 `ns` 表达式,名称必须与位置参数一致 -- `cr edit rm-ns ` - 删除命名空间 -- `cr edit imports ` - 更新导入规则(**全量替换**所有 import) - - `-e 'source-ns :refer $ sym1 sym2'`:单条规则(**不含** `:require` 前缀) - - `-f rules.cirru`:多条规则文件,每行一条(推荐多条场景) - - `-j '[["src-ns",":refer",["sym"]],...]'`:JSON 数组格式,每元素为一条规则 -- `cr edit add-import ` - 添加单个 import 规则(**不替换**已有规则) - - `-e 'source-ns :refer $ sym1 sym2'`:单条规则 - - `-o` / `--overwrite`:覆盖已存在的同名源 ns 规则 -- `cr edit rm-import ` - 移除指定来源的 import 规则 -- `cr edit ns-doc ''` - 更新命名空间文档 - -**模块和配置:** - -- `cr edit add-module ` - 添加模块依赖 -- `cr edit rm-module ` - 删除模块依赖 -- `cr edit config ` - 设置配置(key: init-fn, reload-fn, version) - -**增量变更导出:** - -- `cr edit inc` - 记录增量代码变更并导出到 `.compact-inc.cirru`,触发 watcher 热更新 - - `--added ` - 标记新增的定义 - - `--changed ` - 标记修改的定义 - - `--removed ` - 标记删除的定义 - - TIP: 使用 `cr edit mv` 移动定义后,需手动执行 `cr edit inc --removed --added ` 以更新 watcher。 - - `--added-ns ` - 标记新增的命名空间 - - `--removed-ns ` - 标记删除的命名空间 - - `--ns-updated ` - 标记命名空间导入变更 - - 配合 watcher 使用实现热更新(详见"开发调试"章节) - -使用 `--help` 参数了解详细的输入方式和参数选项。 - ---- - -## Calcit 语言基础 - -### Cirru 语法核心概念 - -**与其他 Lisp 的区别:** - -- **缩进语法**:用缩进代替括号(类似 Python/YAML),单行用空格分隔 -- **字符串前缀**:`|hello` 或 `"hello"` 表示字符串,`|` 前缀更简洁 -- **无方括号花括号**:只用圆括号概念(体现在 JSON 转换中),Cirru 文本层面无括号 - -**常见混淆点:** - -❌ **错误理解:** Calcit 字符串是 `"x"` → JSON 是 `"\"x\""` -✅ **正确理解:** Cirru `|x` → JSON `"x"`,Cirru `"x"` → JSON `"x"` - -**字符串 vs 符号的关键区分:** - -- `|Add` 或 `"Add` → **字符串**(用于显示文本、属性值等, 前缀形式区分字面量类型) -- `Add` → **符号/变量名**(Calcit 会在作用域中查找) -- 常见错误:受其他语言习惯影响,忘记加 `|` 前缀导致 `unknown symbol` 错误 - -**CLI 使用提示:** - -- 替换包含空格的字符串:`--leaf -e '|text with spaces'` 或 `-j '"text"'` -- 避免解析为列表:字符串字面量必须用 `--leaf` 或 `-j` 明确标记 - -**示例对照:** - -| Cirru 代码 | JSON 等价 | JavaScript 等价 | -| ---------------- | -------------------------------- | ------------------------ | -| `\|hello` | `"hello"` | `"hello"` | -| `"world"` | `"world"` | `"world"` | -| `\|a b c` | `"a b c"` | `"a b c"` | -| `fn (x) (+ x 1)` | `["fn", ["x"], ["+", "x", "1"]]` | `fn(x) { return x + 1 }` | - -### 数据结构:Tuple vs Vector - -Calcit 特有的两种序列类型: - -**Tuple (`::`)** - 不可变、用于模式匹配 - -```cirru -; 创建 tuple -:: :event/type data - -; 模式匹配 -tag-match event - (:event/click data) (handle-click data) - (:event/input text) (handle-input text) -``` - -**Vector (`[]`)** - 可变、用于列表操作 - -```cirru -; 创建 vector -[] item1 item2 item3 - -; DOM 列表 -div {} $ [] - button {} |Click - span {} |Text -``` - -**常见错误:** +示例表达式(简化): ```cirru -; ❌ 错误:用 vector 传事件 -send-event! $ [] :clipboard/read text -; 报错:tag-match expected tuple - -; ✅ 正确:用 tuple -send-event! $ :: :clipboard/read text +defn demo (state) + let + result $ collect! state + println result ``` -### 类型标注与检查 - -Calcit 提供了静态类型分析系统,可以在预处理阶段发现潜在的类型错误。 +- `query def` 先看全貌,不改。 +- `query search collect! -f app.main/demo` 拿到路径(假设返回 `[3.1.2]`)。 +- `tree show app.main/demo -p '3.1.2'` 验证该坐标确实是目标子树。 +- 再做 replace/rewrite,避免“猜路径”。 -#### 1. 顶层定义优先使用 `:schema`,局部函数继续使用 `hint-fn` +### `$` 与 `,` 对坐标的影响(结合 Cirru 教程) -现在更推荐这样分工: +这两个符号都很常见,但它们对“树形坐标”的影响方式不同。 -- 顶层 `defn` / `defmacro` 的参数、返回值、泛型信息,优先写到 `:schema` -- 局部 `fn` / 内部辅助函数,继续用 `hint-fn` -- `assert-type` 仍然可用,但更适合做函数体内的额外约束或中间值检查,而不是顶层定义的主标注方式 +#### `$`:常常会改变树深度(更容易引起路径变化) -验证示例: +`$` 用于把右侧表达式折叠成一个子结构,通常会让目标节点进入更深一层。 ```cirru -let - sum-items $ fn (items) - foldl items 0 $ fn (acc item) - hint-fn $ {} - :args $ [] :number :number - :return :number - &+ acc item - sum-items ([] 1 2 3) -``` - -#### 2. 返回类型标注 - -有两种方式标注函数返回类型: - -- **紧凑模式(推荐)**:紧跟在参数列表后的类型标签。 -- **正式模式**:局部 `fn` 使用 `hint-fn`(通常放在函数体开头);顶层 `defn` / `defmacro` 使用 `:schema`。 - - 泛型变量:`hint-fn $ {} (:generics $ [] 'T 'S)` - - 旧 clause 写法(如 `(hint-fn (return-type ...))` / `(generics ...)` / `(type-vars ...)`)已不再支持,会直接报错。 - -验证示例: - -```cirru -let - ; 紧凑模式 - add $ fn (a b) :number - &+ a b - ; 正式模式 - get-name $ fn (user) - hint-fn $ {} (:args $ [] :dynamic) (:return :string) - |demo - ; 泛型声明示例 - id $ fn (x) - hint-fn $ {} (:generics $ [] 'T) (:args $ [] 'T) (:return 'T) - x - add 1 2 -``` - -#### 3. 支持的类型标签 - -| 标签 | 说明 | -| ---------- | ----------------- | -| `:number` | 数字 | -| `:string` | 字符串 | -| `:bool` | 布尔值 | -| `:symbol` | 符号 | -| `:tag` | 标签 (Keyword) | -| `:list` | 列表 | -| `:map` | 哈希映射 | -| `:set` | 集合 | -| `:tuple` | Tuple | -| `:fn` | 函数 | -| `:dynamic` | 任意类型 (通配符) | - -> 约定:动态类型标注统一使用 `:dynamic`,不再使用 `:any` 或 `nil` 作为 dynamic 的显式写法。 - -**高阶函数(HOF)回调类型检查:** - -内置 HOF(`foldl`、`sort`、`filter`、`find`、`find-index`、`filter-not`、`mapcat`、`group-by` 等)的回调参数已强制要求 `:fn` 类型。传入非函数值(如数字、字符串)时会在预处理阶段触发类型警告: - -```bash -# ❌ 错误:第三个参数应为函数,传了数字 -cr eval 'foldl (list 1 2 3) 0 42' -# Type warning: expects :fn but got :number +; "写法 A" +result $ collect! state -# ✅ 正确 -cr eval 'foldl (list 1 2 3) 0 &+' +; "等价写法 B" +result (collect! state) ``` -#### 4. 复杂类型标注 +- 当你把一段调用改成/改掉 `$` 形式时,命中节点的路径经常会变深或变浅。 +- 经验:改 `$` 之后,不复用旧路径,重新 `query search` 一次。 -- **可选类型**:`:: :optional :string` (可以是 string 或 nil) -- **变长参数**:在 Schema 中使用 `:rest :number` (参数列表剩余部分均为 number) -- **结构体/枚举**:使用 `defstruct` 或 `defenum` 定义的名字 +#### `,`:在“重起一行”场景里用于保持目标节点形态(有助于坐标稳定) -验证示例 (使用 `let` 封装多表达式以支持 `cr eval` 验证): +`,` 常用于告诉解析器“这里是值节点,不是再发起一次调用”。 ```cirru -let - ; 可选参数 - greet $ fn (name) - hint-fn $ {} (:args $ [] (:: :optional :string)) (:return :string) - str "|Hello " (or name "|Guest") - - ; 变长参数 - sum $ fn (& xs) - hint-fn $ {} (:rest :number) (:return :number) - reduce xs 0 &+ - - ; Record 约束 (使用 defstruct 定义结构体) - User $ defstruct User (:name :string) - get-name $ fn (u) - hint-fn $ {} (:args $ [] (:: :record User)) (:return :string) - get u :name - println $ greet |Alice - println $ sum 1 2 3 - println $ get-name (%{} User (:name |Bob)) -``` - -**验证类型:** 运行或者编译时会先完成校验. - -#### 5. Schema 与 `defn` / `defmacro` 一致性检查 - -如果定义带有 `:schema`,现在不仅 `cr analyze check-types` 会检查,普通运行路径也会在 **preprocess 阶段** 直接校验: - -- `:: :fn` 必须对应 `defn` -- `:: :macro` 必须对应 `defmacro` -- `:args` 的必选参数个数必须和实际参数列表一致 -- `:rest` 必须和代码里的 `&` rest 参数一致 - -这意味着下面几类命令都会在启动时直接失败,而不是等到 `analyze` 才发现: - -- `cr ` -- `cr --check-only ` -- `cr js` -- `yarn try-rs` - -复杂但正确的示例(顶层用 `:schema`,局部函数用 `hint-fn`): - -```cirru -|join-str $ %{} :CodeEntry (:doc |) - :code $ quote - defn join-str (xs0 sep) - apply-args (| xs0 true) - defn %join-str (acc xs beginning?) - hint-fn $ {} - :args $ [] :string :list :bool - :return :string - list-match xs - () acc - (x0 xss) - recur - &str:concat - if beginning? acc $ &str:concat acc sep - , x0 - , xss false - :examples $ [] - :schema $ :: :fn $ {} (:return :string) - :args $ [] :list :string -``` +; "写法 A" +a (b c) d -这个例子里,schema 与代码是完全对齐的: - -- `:: :fn` 对应 `defn` -- `:args` 里 2 个必选参数,对应 `(xs0 sep)` -- `:return :string` 对应整个 `join-str` 的返回值 -- 内部辅助函数 `%join-str` 不是顶层定义,所以继续用 `hint-fn` - -可以简单记忆为:**namespace 上的定义看 `:schema`,函数体内部的辅助函数看 `hint-fn`。** - -推荐工作流: - -```bash -# 先查看 calcit.core 里真实存在的 schema -cr query schema calcit.core/join-str - -# 再仿照它给自己的定义写 schema -cr edit schema app.main/my-fn -e ':: :fn $ {} (:args $ [] :list :string) (:return :string)' - -# 最后验证 -cr --check-only -cr analyze check-types +; "等价写法 B" +a + b c + , d ``` -实务上,`analyze check-types` 更适合做全量巡检;普通运行路径现在会做 fail-fast 阻断。 - -### 其他易错点 - -比较容易犯的错误: +- 在这组例子中,目标值 `d` 都是 `a` 的同级参数,通常可以视为同一坐标层级(只是写法不同)。 +- 如果把 `, d` 误写成单独一行 `d`,它可能被解析成“调用形态”,节点类型会变化,后续路径与搜索命中也可能随之变化。 +- 所以:`,` 本身通常不引入额外层级;它更多是在“换行写法”下保持你想要的 AST 形态。 -- Calcit 中字符串通过前缀区分,`|` 和 `"` 开头表示字符串。`|x` 对应 JavaScript 字符串 `"x"`。产生 JSON 时注意不要重复包裹引号。 -- Calcit 采用 Cirru 缩进语法,可以理解成去掉跨行括号改用缩进的 Lisp 变种。用 `cr cirru parse` 和 `cr cirru format` 互相转化试验。 -- Calcit 跟 Clojure 在语义上比较像,但语法层面只用圆括号,不用方括号花括号。 - ---- +#### 实操规则(最稳) -## 开发调试 +凡是改到 `$` 或 `,`(尤其是从单行改成多行)时: -简单脚本可直接使用 `cr ` 执行(默认单次)。编译 JavaScript 用 `cr js` 执行一次编译。 -若需要监听模式,显式添加 `-w` / `--watch`(如 `cr -w `、`cr js -w`)。 +1. 先 `tree show` 看当前子树。 +2. 修改后立刻 `query search -f ` 重拿路径。 +3. 再继续下一步结构化编辑(`replace/wrap/rewrite`)。 -Calcit snapshot 文件中 config 有 `init-fn` 和 `reload-fn` 配置: +## 0) 硬前置步骤 -- 初次启动调用 `init-fn` -- 每次修改代码后调用 `reload-fn` - -**典型开发流程:** - -```bash -# 1. 启动监听模式(用户自行使用) -cr -w # 解释执行监听模式 -cr js -w # JS 编译监听模式 -cr ir -w # IR 生成监听模式 - -# 2. 修改代码后触发增量更新(详见"增量触发更新"章节) -cr edit inc --changed ns/def - -# 3. 一次性执行/编译(用于简单脚本) -cr # 执行一次 -cr js # 编译一次 -cr ir # 生成一次 IR -``` - -### 增量触发更新(推荐)⭐⭐⭐ - -当使用监听模式(`cr -w` / `cr js -w` / `cr ir -w`)开发时,推荐使用 `cr edit inc` 命令触发增量更新,而非全量重新编译/执行: - -**工作流程:** +在任何 `cr edit` / `cr tree` 修改前,先执行一次: ```bash -# 【终端 1】启动 watcher(监听模式) -cr -w # 或 cr js -w / cr ir -w - -# 【终端 2】修改代码后触发增量更新 -# 修改定义 -cr edit def app.core/my-fn -e 'defn my-fn (x) (+ x 1)' - -# 触发增量更新 -cr edit inc --changed app.core/my-fn - -# 等待 ~300ms 后查看编译结果 -cr query error +cr docs agents --full ``` -**增量更新命令参数:** - -```bash -# 新增定义 -cr edit inc --added namespace/definition - -# 修改定义 -cr edit inc --changed namespace/definition - -# 删除定义 -cr edit inc --removed namespace/definition - -# 新增命名空间 -cr edit inc --added-ns namespace - -# 删除命名空间 -cr edit inc --removed-ns namespace - -# 更新命名空间导入 -cr edit inc --ns-updated namespace - -# 组合使用(批量更新) -cr edit inc \ - --changed app.core/add \ - --changed app.core/multiply \ - --removed app.core/old-fn -``` - -**查看编译结果:** - -```bash -cr query error # 命令会显示详细的错误信息或成功状态 -``` - -`cr query error` 只能告诉你最近一次 Calcit 语义链路里有没有报错,例如解析、预处理、运行期异常;它**不能**证明浏览器 CSS、HTML 属性值、业务数据内容或外部系统配置是“合理的”。像 `|max(...)` 被误写成 `"|max(...)` 这类在 Cirru 层面仍合法的字符串,就可能通过 `cr query error`,但在浏览器渲染阶段失效。 - -**何时使用全量操作:** - -```bash -# 极少数情况:增量更新不符合预期时 -cr -1 js # 重新编译 JavaScript -cr -1 # 重新执行程序 - -# 或重启监听模式(Ctrl+C 停止后重启) -cr # 或 cr js -``` - -**增量更新优势:** 快速反馈、精确控制变更范围、watcher 保持运行状态 +这一步不是建议项,用于避免沿用旧命令心智模型。 --- -## 文档支持 - -遇到疑问时使用: - -- `cr docs search ` - 搜索 Calcit 教程内容 -- `cr docs agents [ ...] [--full]` - 读取 Agent 指南(优先本地缓存,按天自动刷新) -- `cr docs read [ ...]` - 按标题查看章节(不传标题时列标题) -- `cr docs read --full` - 直接读取整份文档内容 -- `cr docs read-lines -s -n ` - 按行读取文档 -- `cr docs list` - 查看所有可用文档 -- `cr query ns ` - 查看命名空间说明和函数文档 -- `cr query peek ` - 快速查看定义签名 -- `cr query def ` - 读取完整语法树 -- `cr query examples ` - 查看示例代码 -- `cr query find ` - 跨命名空间搜索符号 -- `cr query usages ` - 查找定义的使用位置 -- `cr query search [-f ]` - 搜索叶子节点 -- `cr query search-expr [-f ]` - 搜索结构表达式 -- `cr query error` - 查看最近的错误堆栈(仅覆盖 Calcit 语义与运行链路,不覆盖 CSS/DOM/业务值合理性) - ---- - -## 代码修改示例 - -### 添加新函数 - -```bash -# Cirru one liner -cr edit def app.core/multiply -e 'defn multiply (x y) (* x y)' -``` - -### 基本操作 - -```bash -# 添加新函数(命令会提示 Next steps) -cr edit def 'app.core/multiply' -e 'defn multiply (x y) (* x y)' - -# 替换整个定义(推荐用 overwrite,避免依赖根路径替换) -cr edit def 'app.core/multiply' --overwrite -f /tmp/multiply.cirru - -# 更新文档和示例 -cr edit doc 'app.core/multiply' '乘法函数,返回两个数的积' -cr edit add-example 'app.core/multiply' -e 'multiply 5 6' - -# 移动或重构定义 -cr edit mv 'app.core/multiply' 'app.util/multiply-numbers' -``` - -### 修改定义工作流(命令会显示子节点索引和 Next steps) +## 1) 默认约定(基于反馈) -```bash -# 1. 搜索定位 -cr query search '' -f 'ns/def' -l - -# 2. 查看节点(输出会显示索引和操作提示) -cr tree show 'ns/def' -p '' - -# 3. 执行替换(会显示 diff 和验证命令) -cr tree replace 'ns/def' -p '' --leaf -e '' - -# 4. 检查结果 -cr query error -# 若改动涉及 CSS / DOM / 浏览器行为,继续做实际渲染验证,不要把 query error 当最终验收 -# 添加命名空间(推荐:先创建空 ns,再逐条 add-import) -cr edit add-ns app.util -cr edit add-import app.util -e 'calcit.core :refer $ echo' - -# 添加导入规则(单条) -cr edit add-import app.main -e 'app.util :refer $ helper' -# 覆盖已有同名 import -cr edit add-import app.main -e 'app.util :refer $ helper util-fn' -o - -# 移除导入规则 -cr edit rm-import app.main app.util +- 默认优先 **Cirru 输出**,避免默认 JSON 带来的 token 膨胀。 +- 大定义默认先 `query peek`,确认签名与规模后再 `query def`,避免首次信息过载。 +- 路径统一使用点号:`'5.5.1.3'`。 +- 大函数先“看结构再下刀”:先 `query def`,再 `query search` 拿路径,再 `tree show -p` 聚焦子树。 +- 搜索命中很多时,修改遵循: + - 从大索引往前改,或 + - 每次修改后重新 `query search` 避免路径漂移。 +- Tips 需要但应可控: + - 默认最多一条(快速扫读) + - 支持“全部/静默”模式切换(建议使用 `--tips-level` 统一控制) -# 全量替换 imports(单条用 -e,多条用 -f 文件或 -j JSON) -cr edit imports app.main -e 'app.util :refer $ helper' # 单条 -cr edit imports app.main -f my-imports.cirru # 多条(每行一条规则) -cr edit imports app.main -j '[["app.lib",":as","lib"],["app.util",":refer",["helper"]]]' # JSON - -# 更新项目配置 -cr edit config init-fn app.main/main! -``` - ---- +> 说明:当前 CLI 已支持 `--no-tips`。`--tips-level` 作为统一分级开关建议保留在后续实现中。 --- -## 🔧 实战重构场景 - -以下是开发中最常见的局部修复和重构操作,帮助 Agent 快速找到对应命令。 - -### 提取子表达式为新定义(`edit split-def`) - -**场景:** 函数体内某个嵌套子表达式太复杂,想拆成独立的命名定义。 - -```bash -# 1. 搜索并定位目标子表达式 -cr query search-expr 'complex-call arg1' -f 'app.core/process-data' -l -# 输出示例:[3.2.1] in (let ((x ...)) ...) - -# 2. 提取为新定义(原位置自动替换为新名字 extracted-calc) -cr edit split-def 'app.core/process-data' -p '3.2.1' -n extracted-calc - -# 3. 查看结果 -cr query def 'app.core/extracted-calc' # 新定义 -cr query def 'app.core/process-data' # 原定义(原位已变成 extracted-calc) - -# 4. 如需给新定义加函数签名(用 tree replace 重构根节点) -cr tree replace 'app.core/extracted-calc' -p '' -e 'defn extracted-calc (x) body-expr' -``` - -**注意:**`split-def` 仅创建新定义并替换引用,不会自动在其他 ns 添加 import。对外暴露时记得 `cr edit add-import`。 - -### 重命名定义(`edit rename`) - -**场景:** 定义名字需要在同一命名空间内改名。 - -```bash -# 1. 确认有哪些地方引用到 -cr query usages 'app.core/old-name' - -# 2. 重命名(不允许覆盖已有定义) -cr edit rename 'app.core/old-name' 'new-name' - -# 3. 批量更新所有引用(search 会自动提示批量命令) -cr query search 'old-name' # 找到所有引用位置 -cr tree replace-leaf 'app.core/caller-fn' --pattern 'old-name' -e 'new-name' --leaf -``` - -### 迁移定义到另一命名空间(`edit mv-def`) - -**场景:** 某函数放错了命名空间,需要迁移。 - -```bash -# 移动定义 -cr edit mv-def 'app.core/helper-fn' 'app.util/helper-fn' - -# 在使用方添加 import -cr edit add-import 'app.main' -e 'app.util :refer $ helper-fn' - -# 通知 watcher(热更新场景) -cr edit inc --removed 'app.core/helper-fn' --added 'app.util/helper-fn' -``` - -### 在定义内移动 / 复制 AST 节点(`edit mv` / `edit cp`) - -**场景:** 函数体内某个子表达式需要移到另一位置,或复制用于多处。 - -```bash -# 定位节点 -cr query search-expr 'process item' -f 'app.core/main-fn' -l -# 输出:[3,1,2] - -# 移动(原位置消失) -cr edit mv 'app.core/main-fn' --from '3,1,2' -p '3,2' --at before - -# 复制(原位置保留,新位置多一份) -cr edit cp 'app.core/main-fn' --from '3,1,2' -p '3,2' --at after -``` - -### 包裹 / 拆包 / 提升节点(`tree wrap` / `tree unwrap` / `tree raise`) - -**场景:** 临时包裹一层 `println` 调试、反向拆掉包装层、或用子节点替换掉父节点。 - -```bash -# 包裹(wrap):将节点包进新表达式,self = 原节点 -cr tree wrap 'app.core/main-fn' -p '3,2' -e 'println self' - -# 包裹成 let 绑定(self = 原表达式) -cr tree wrap 'app.core/main-fn' -p '3,2' -e 'let ((result self)) result' - -# 拆包(unwrap):删除该节点,所有子节点展开到原位置 -cr tree unwrap 'app.core/main-fn' -p '3,2' - -# 提升(raise):用该子节点整体替换其父节点 -# 场景:去掉 if 只保留 then 分支,或去掉 let 只保留最终返回值 -cr tree raise 'app.core/main-fn' -p '3,2,1' -``` +## 2) 5 步最小模板(看大表达式并可编辑) -### 批量重命名局部变量(`tree replace-leaf` / `tree target-replace`) +1. 定位目标定义:`cr query defs ` +2. 先轻看再全看:`cr query peek `,必要时再 `cr query def ` +3. 搜关键词拿路径:`cr query search -f ` +4. 聚焦子树确认上下文:`cr tree show -p ''`(复杂时加 `-j`) +5. 修改并验证:`cr tree replace ...` 或 `cr edit inc --changed `,然后 `cr js` -**场景:** 某函数内某个局部变量名需要统一改掉。 +### 示例(大函数) ```bash -# 若只有一处:内容定位直接替换(最安全 ⭐) -cr tree target-replace 'app.core/process' --pattern 'old-var' -e 'new-var' --leaf - -# 若多处:一次性全部替换 -cr tree replace-leaf 'app.core/process' --pattern 'old-var' -e 'new-var' --leaf +cr query peek respo.render.diff/find-element-diffs +cr query def respo.render.diff/find-element-diffs +cr query search collect! -f respo.render.diff/find-element-diffs +cr tree show respo.render.diff/find-element-diffs -p '5.5.1.3' -j +cr edit inc --changed respo.render.diff/find-element-diffs +cr js ``` --- -## ⚠️ 常见陷阱和最佳实践 - -### 1. 路径索引动态变化问题 ⭐⭐⭐ - -**核心原则:** 删除/插入会改变同级后续节点索引。 - -**批量修改策略:** - -- **从后往前操作**(推荐):先删大索引,再删小索引 -- **单次操作后重新搜索**:每次修改立即用 `cr query search` 更新路径 -- **整体重写**:优先用 `cr edit def --overwrite -f `;`cr tree replace -p ''` 只保留给明确需要根节点级别改写的场景 - -命令会在路径错误时提示最长有效路径和可用子节点。 - -### 1.5 根路径整体替换的边界 ⭐⭐⭐ - -`cr tree replace -p ''` 在语义上确实是替换根节点,但在实际操作里,它更像“根 AST 节点替换”,而不是“整条定义安全重写”。当你需要完整替换一个定义体时: +## 3) 高频命令(只保留最常用) -- 更推荐 `cr edit def --overwrite -f ` -- 先在文件里组织完整定义,再一次性覆盖,验证也更直接 -- 如果你已经用 `-p ''` 替换成功,仍应立刻执行 `cr query def ` 或完整运行,确认写回后的定义结构符合预期 +### 查询 -经验上,`-p ''` 更适合你已经非常确定根节点结构时的精细 AST 操作,不适合作为默认“全量改写定义”的模板。 +- `cr query defs `:列出命名空间定义。 +- `cr query def `:查看定义(默认 Cirru)。 +- `cr query search -f `:按关键词拿路径。 +- `cr tree show -p ''`:查看局部子树。 -### 2. 输入格式参数使用速查 ⭐⭐⭐ +### 编辑 -**参数混淆矩阵(已全面支持 `-e` 自动识别):** +- `cr tree replace -p '' -e ''`:替换指定节点。 +- `cr tree target-replace --pattern '' -e '' --leaf`:按内容唯一定位替换(优先)。 +- `cr edit inc --changed `:增量编译当前修改定义。 -| 场景 | 示例用法 | 解析结果 | 说明 | -| ------------------- | -------------------------------------- | ----------------------------- | --------------------------------- | -| **表达式 (Cirru)** | `-e 'defn add (a b) (+ a b)'` | `["defn", "add", ...]` (List) | 默认按 Cirru one-liner 解析 | -| **原子符号 (Leaf)** | `--leaf -e 'my-symbol'` | `"my-symbol"` (Leaf) | **推荐**,避免被包装成 list | -| **字符串 (Leaf)** | `--leaf -e '\|hello world'` | `"hello world"` (Leaf) | 符号前缀 `\|` 表示字符串 | -| **JSON 数组** | `-e '["+", "x", "1"]'` | `["+", "x", "1"]` (List) | **自动识别** (含 `[` 且有 `"`) | -| **JSON 字符串** | `-e '"my leaf"'` | `"my leaf"` (Leaf) | **自动识别** (含引用的字符串) | -| **内联 JSON** | `-j '["defn", ...]'` | `["defn", ...]` (List) | 显式按 JSON 解析,忽略 Cirru 规则 | -| **外部文件** | `-f code.cirru` (或 `-f code.json -J`) | 根据文件内容解析 | `-J` 用于标记文件内是 JSON | +### 结构化策略(常用 5 招) -**核心规则:** +下面是“尽量不手写大段代码”的编辑策略,按风险从低到高使用。 -1. **智能识别模式**:`-e / --code` 现在会自动识别 JSON。如果你传入 `["a"]` 或 `"a"`,它会直接按 JSON 处理,无需再额外加 `-J` 或 `-j`。 -2. **强制 Leaf 模式**:如果你需要确保输入是一个叶子节点(符号或字符串),请在任何地方使用 `--leaf` 开关。它会将原始输入直接作为内容,不经过任何解析。 -3. **显式 JSON 模式**:如果你想明确告诉工具“这段就是 JSON”,优先用 `-j ''`。 -4. **统一性**:`cr tree` 和 `cr edit` 的所有子命令(replace, def, insert 等)现在共享完全相同的输入解析逻辑。 - -**实战示例:** +#### 1) `cp`:复制现有子树,减少手输 ```bash -# ✅ 替换表达式 -cr tree replace app.main/fn -p '2' -e 'println |hello' - -# ✅ 替换 leaf(推荐 --leaf) -cr tree replace app.main/fn -p '2,0' --leaf -e 'new-symbol' - -# ✅ 替换字符串 leaf -cr tree replace app.main/fn -p '2,1' --leaf -e '|new text' - -# ❌ 避免:用 -e 传单个 token(会变成 list) -cr tree replace app.main/fn -p '2,0' -e 'symbol' # 结果:["symbol"] +cr tree cp app.main/demo --from '3.2' -p '4' --at after ``` -### 3. Cirru 字符串和数据类型 ⭐⭐ - -**Cirru 字符串前缀:** - -| Cirru 写法 | JSON 等价 | 使用场景 | -| -------------- | -------------- | ------------ | -| `\|hello` | `"hello"` | 推荐,简洁 | -| `"hello"` | `"hello"` | 也可以 | -| `\|a b c` | `"a b c"` | 包含空格 | -| `\|[tag] text` | `"[tag] text"` | 包含特殊字符 | - -**不放心修改是否正确?** 每步后用 `tree show` 验证. - -**Tuple vs Vector:** - -```cirru -; ✅ Tuple - 用于事件、模式匹配 -:: :clipboard/read text - -; ✅ Vector - 用于 DOM 列表 -[] (button) (div) +- 含义:把路径 `3.2` 的子树复制到 `4` 后面。 +- 适合:先复用旧逻辑,再做小改。 -; ❌ 错误:用 vector 传事件 -send-to-component! $ [] :clipboard/read text -; 报错:tag-match expected tuple - -; ✅ 正确:用 tuple -send-to-component! $ :: :clipboard/read text -``` - -**记忆规则:** - -- **`::` (tuple)**: 事件、模式匹配、不可变数据结构 -- **`[]` (vector)**: DOM 元素列表、动态集合 - -### 4. 输入大小限制 ⭐⭐⭐ - -为了保证稳定性和处理速度,CLI 对单次输入的大小有限制。如果超过限制,系统会提示建议分段提交。 - -- **Cirru One-liner (`-e / --code`)**: 字数上限 **1000**。 -- **JSON 格式 (`-j / --json`, `-J`, `-e`)**: 字数上限 **2000**。 - -**大资源处理建议:** -如果需要修改复杂的长函数,不要尝试一次性替换整个定义。应先构建主体结构,使用占位符,统一写成 `{{PLACEHOLDER_FEATURE}}` 这种花括号形式,并注意避免重复,然后通过 `cr tree target-replace` 或按路径的 `cr tree replace` 做精准的分段替换。 - -补充提示:现在 `cr query def` 和 `cr tree show` 遇到大表达式时会自动输出分片结果。若你采用多阶段创建,建议从第一步就使用 `{{NAME}}` 风格占位符,这样后续在分片视图中更容易识别骨架、复制坐标并继续填充内容。 - -### 5. 命名空间操作陷阱 ⭐⭐⭐ - -**三个命令的 `-e` 期望格式完全不同,是最常见的混淆来源:** - -| 命令 | `-e` 期望内容 | 错误用法 | -| ------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------ | -| `add-ns -e ...` | **完整 `ns` 表达式**:`ns my.ns $ :require ...` | ❌ 传 import 规则(静默成功但 ns 代码损坏) | -| `imports -e ...` | **单条 import 规则**(无 `:require` 前缀):`src-ns :refer $ sym` | ❌ 带 `:require` 前缀(导致 `:require :require` 重复) | -| `add-import -e ...` | **单条 import 规则**(同上):`src-ns :refer $ sym` | 同 imports | - -**具体陷阱:** - -❌ **陷阱1:`add-ns -e` 传了 import 规则而非完整 `ns` 表达式** +#### 2) `mv`:移动/重命名定义 ```bash -# ❌ 错误:ns 代码会变成 'respo.core :refer $ defcomp'(缺 ns 关键字!) -cr edit add-ns my.ns -e 'respo.core :refer $ defcomp' - -# ✅ 正确:无代码时先建空 ns,再 add-import -cr edit add-ns my.ns -cr edit add-import my.ns -e 'respo.core :refer $ defcomp' - -# ✅ 也正确:传完整 ns 表达式(名称必须与位置参数一致) -cr edit add-ns my.ns -e 'ns my.ns $ :require respo.core :refer $ defcomp' +cr edit mv app.main/old-name app.main/new-name ``` -❌ **陷阱2:`imports -e` 带了 `:require` 前缀**(现在会报错) - -```bash -# ❌ 错误:现在会报错 "Do not include ':require' as a prefix" -cr edit imports my.ns -e ':require respo.core :refer $ sym' - -# ✅ 正确:直接传规则,不加 :require -cr edit imports my.ns -e 'respo.core :refer $ sym' -``` - -❌ **陷阱3:`add-ns -e` 中 ns 名称与位置参数不一致**(现在会报错) - -```bash -# ❌ 错误:现在会报错 "Namespace name mismatch" -cr edit add-ns my.ns -e 'ns wrong.ns $ :require ...' -``` +- 含义:定义级重命名或迁移。 +- 适合:整理命名或模块边界。 -❌ **陷阱4:想添加多条 imports 时用 `-e` 而非 `-f`** +#### 3) `wrap`:给目标套一层结构 ```bash -# ❌ 无法在单个 -e 中写多条规则(会合并为一条) -cr edit imports my.ns -e 'respo.core :refer $ div\nrespo.util.format :refer $ hsl' - -# ✅ 多条规则用文件(每行一条规则,无需 :require 前缀) -printf 'respo.core :refer $ div\nrespo.util.format :refer $ hsl\n' > /tmp/imports.cirru -cr edit imports my.ns -f /tmp/imports.cirru - -# ✅ 或用 JSON 格式 -cr edit imports my.ns -j '[["respo.core",":refer",["div"]],["respo.util.format",":refer",["hsl"]]]' - -# ✅ 或逐条 add-import(推荐,更安全) -cr edit add-import my.ns -e 'respo.core :refer $ div' -cr edit add-import my.ns -e 'respo.util.format :refer $ hsl' +cr tree wrap app.main/demo -p '5.2' -e 'when cond self' ``` -**最佳实践:优先用 `add-import`(更安全,带校验):** - -- `add-import` 会验证 source-ns 格式,有 `--overwrite` 保护 -- `imports` 全量替换,一旦格式错误会覆盖所有 imports -- 只有需要完全重置所有 imports 时才用 `imports` +- 含义:把原节点作为 `self` 嵌入新结构。 +- 适合:快速加 guard、日志、转换壳。 -### 6. 推荐工作流程 - -**基本流程(search 快速定位 ⭐⭐⭐):** +#### 4) `raise`:提升子表达式,去掉中间壳 ```bash -# 1. 快速定位(比逐层导航快10倍) -cr query search 'target' -f 'ns/def' # 或 search-expr 'fn (x)' -l 搜索结构 - -# 2. 执行修改(会显示 diff 和验证命令) -cr tree replace 'ns/def' -p '' --leaf -e '' - -# 3. 增量更新(推荐) -cr edit inc --changed ns/def -# 等待 ~300ms 后检查 -cr query error +cr tree raise app.main/demo -p '5.2.1' ``` -**新手提示:** +- 含义:用指定子节点替换其父节点。 +- 适合:去掉多余 `let/when/pipe` 包裹层。 -- 不知道目标在哪?用 `search` 或 `search-expr` 快速找到所有匹配 -- 想了解代码结构?用 `tree show` 逐层探索 -- 需要批量重命名?搜索后按提示从大到小路径依次修改 -- 不确定修改是否正确?每步后用 `tree show` 验证 - -### 7. Shell 特殊字符转义 ⭐⭐ - -Calcit 函数名中的 `?`, `->`, `!` 等字符在 bash/zsh 中有特殊含义,需要用单引号包裹: +#### 5) `rewrite`:引用原节点做结构重排 ```bash -# ❌ 错误 -cr query def app.main/valid? -cr eval '-> x (+ 1) (* 2)' - -# ✅ 正确 -cr query def 'app.main/valid?' -cr eval 'thread-first x (+ 1) (* 2)' # 用 thread-first 代替 -> +cr tree rewrite app.main/demo -p '5.2' --with self=. -e '-> self normalize emit' ``` -**建议:** 命令行中优先使用英文名称(`thread-first` 而非 `->`),更清晰且无需转义。 +- 含义:在新模板中引用原节点(`.`)。 +- 适合:复杂重构但希望保持局部语义。 -### 8. 多命令 `&&` 链式调用风险 ⭐⭐⭐ +> 实战建议:先 `target-replace/cp/wrap`,再用 `rewrite`;每步后 `tree show` 复核。 -把多个 `cr tree replace`、`cr edit def -e ...` 或其他带内联代码的命令用 `&&` 串起来,在 bash/zsh 中风险很高: +### 验证 -- 只要某一段 `-e` 内容里出现未正确转义的引号,shell 就会进入“继续等待补全输入”的状态,看起来像终端卡死 -- 前一条命令如果已经改写了内容,后一条命令即使没执行,你也可能以为整批操作已完成 - -更稳妥的做法: - -- 批量修改时逐条执行 -- 多行或含引号内容改用 `-f ` -- 需要批量脚本化时,放到独立 shell script,并先用最小样例验证 quoting +- `cr js`:快速验证当前改动可编译。 +- 全量语义回归建议:`yarn check-all`。 --- -## 🔄 完整功能开发示例 - -以下展示从零开始添加新函数的完整流程,是最常见的日常开发场景。 +## 4) 降噪与可读性建议 -### 步骤 1:确认目标命名空间和现有代码 +- 默认只看 Cirru,**必要时**才加 `-j`。 +- 先 `query def` 看大轮廓,再 `search` + `tree show` 看局部。 +- 搜索结果过多时,不要连续盲改路径;每次改后重搜一次更稳。 +- 复杂多行表达式优先 `-f `,减少 shell 转义错误。 +- 若需要最安静输出,可使用 `--no-tips`。 -```bash -# 查看命名空间列表 -cr query ns +### `Invalid path` 快速恢复模板(固定 3 步) -# 查看某个 ns 已有的定义 -cr query defs app.util +当路径报错时,不要继续猜坐标,直接走下面流程: -# 快速了解某个定义(不展开完整代码) -cr query peek 'app.util/format-date' +1. `cr query search -f ` 重新拿最新路径。 +2. `cr tree show -p ''` 核对子树上下文。 +3. 再执行 `tree replace/wrap/rewrite`。 -# 如有疑问,读取完整代码 -cr query def 'app.util/format-date' -``` +常见触发原因: -### 步骤 2:用 eval 快速验证写法 +- 前一步做了 `insert/delete/raise/unwrap`,兄弟索引已变化。 +- 把单行改成多行(尤其涉及 `$`)后,子树深度发生变化。 -在真正写入项目前,先用 `cr eval` 验证逻辑思路: +### 低噪音工作模式(推荐给 Agent) ```bash -# 验证基础函数调用 -cr eval 'string->number |123' - -# 验证带 let 的表达式 -cr eval 'let ((x 10) (y 20)) (+ x y)' +# 1) 先轻看,避免大段输出 +cr --no-tips query peek -# 验证列表操作 -cr eval 'let ((xs (list 1 2 3))) (map xs (fn (x) (* x 2)))' +# 2) 必要时才看完整定义(默认 Cirru) +cr --no-tips query def -# 加载项目依赖模块后测试 -cr eval --dep calcit.std 'str/split |hello world | ' +# 3) 用 search 定位后再 show 局部 +cr --no-tips query search '' -f +cr --no-tips tree show -p '' ``` -> 💡 `cr eval` 有类型警告时会失败退出——正好可以提前发现用法错误。 - -### 步骤 3:添加新定义 - -```bash -# 在已有命名空间中添加新函数 -cr edit def 'app.util/calculate-discount' -e 'defn calculate-discount (price rate) (* price (- 1 rate))' - -# 验证定义写入成功 -cr query def 'app.util/calculate-discount' -``` +仅在需要程序化处理时再加 `-j`,否则保持 Cirru 输出即可。 -### 步骤 4:在调用方添加 import 并使用 - -```bash -# 查看调用方当前 imports -cr query ns app.core - -# 添加 import(首选 add-import,更安全) -cr edit add-import 'app.core' -e 'app.util :refer $ calculate-discount' - -# 在函数体中使用新定义(先定位插入位置) -cr query search 'total-price' -f 'app.core/checkout' -# 输出:[3.2.1] in (let ((total-price ...)) ...) - -# 修改调用 -cr tree replace 'app.core/checkout' -p '3.2.1' -e 'calculate-discount total-price 0.1' -``` - -### 步骤 5:触发热更新并验证 +--- -```bash -# 推送增量更新(触发 watcher 热加载) -cr edit inc --changed 'app.util/calculate-discount' -cr edit inc --changed 'app.core/checkout' +## 5) 路径规则(统一) -# 等待 ~300ms 后检查是否有错误 -cr query error +- 使用点号路径:`'5.5.1.3'`。 +- `-p ''` 表示根节点,仅在明确需要根级操作时使用。 +- 输入错误路径会触发 `Invalid path`,先 `tree show` 校验上下文再改。 -# 如无错误,用 --check-only 整体验证 -cr --check-only -``` +--- -如果这次改动涉及样式、浏览器属性、字符串模板或外部接口,`cr query error` 和 `cr --check-only` 通过后,仍要继续做目标环境里的真实验收。 +## 6) 新手上手顺序(一次就够) -### 常见失误快速修复 +按顺序跑一遍即可建立手感: ```bash -# 忘记 import → unknown symbol -cr edit add-import 'app.core' -e 'app.util :refer $ calculate-discount' - -# 定义名拼写错误 → 重命名 -cr edit rename 'app.util/calculte-discount' 'calculate-discount' - -# 函数参数顺序传错 → 定位并修改调用 -cr query search 'calculate-discount' -f 'app.core/checkout' -cr tree replace 'app.core/checkout' -p '3.2.1' --leaf -e 'calculate-discount' +cr query defs app.main +cr query def app.main/main! +cr query search state -f app.main/main! +cr tree show app.main/main! -p '3.2' +cr tree replace app.main/main! -p '3.2' -e 'new-expr' +cr edit inc --changed app.main/main! +cr js ``` --- -## 💡 Calcit vs Clojure 关键差异 - -**语法层面:** +## 7) 进阶内容(已下沉) -- **只用圆括号**:Calcit 的 Cirru 语法不使用方括号 `[]` 和花括号 `{}`,统一用缩进表达结构 -- **函数前缀**:Calcit 用 `&` 区分内置函数(`&+`、`&str`)和用户定义函数 +本文件只保留高频流程。低频/进阶内容请查: -**集合函数参数顺序(易错 ⭐⭐⭐):** +- 完整进阶版 Agent 指南(从旧版完整迁移):`docs/run/agent-advanced.md` +- 运行模式、eval 细节、CLI 约束:`Agents.md` +- 语言手册与章节阅读:`cr docs list` / `cr docs read ` +- Cirru 语法细节:`cr cirru show-guide` +- traits 与运行期方法调试:`cr docs search 'trait-call'` -- **Calcit**: 集合在**第一位** → `map data fn` 或 `-> data (map fn)` -- **Clojure**: 函数在第一位 → `map fn data` 或 `->> data $ map fn` -- **症状**:`unknown data for foldl-shortcut` 报错 -- **原因**:误用 `->>` 或参数顺序错误 +--- -**其他差异:** +## 8) 一句话原则 -- **宏系统**:Calcit 更简洁,缺少 Clojure 的 reader macro(如 `#()`) -- **数据类型**:Calcit 的 Tuple (`::`) 和 Vector (`[]`) 有特定用途(见"Cirru 字符串和数据类型") +**先定位路径,再看子树,再最小替换;默认 Cirru,JSON 只在必要时启用。** --- -## 常见错误排查 +## 9) `cr` 能力地图(粗粒度) -### 快速诊断流程 +当当前模板不够用时,按下面的“能力分层”自行扩展: -当 watcher 提示有错误或行为异常时,按以下顺序排查: +- 运行与编译:`cr`, `cr js`, `cr ir`, `-w/--watch` +- 查询与定位:`cr query defs/def/search/search-expr/usages/schema/examples` +- 分析与影响评估:`cr analyze call-graph`, `cr analyze count-calls` +- 结构化编辑:`cr tree show/replace/target-replace/cp/wrap/unwrap/raise/rewrite` +- 定义级编辑:`cr edit mv/def/add-import/imports/...` +- 文档与指南:`cr docs list/read/search/agents` +- 语法学习:`cr cirru show-guide` -```bash -# 1. 查看最新错误堆栈(首选) -cr query error -# 输出示例: -# Error in app.core/process-data -# CalcitErr: unknown symbol: proess-item ← 拼写错误 -# at app.core/render → app.core/process-data → ... - -# 2. 用 --check-only 快速全量验证(不执行程序) -cr --check-only - -# 3. 用 cr eval 隔离验证单个函数写法 -cr eval 'let ((x 1)) (+ x 2)' -``` - -### 错误信息对照表 - -| 错误信息 | 原因 | 解决方法 | -| ------------------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------ | -| `Path index X out of bounds` | 路径索引已过期(操作后变化) | 重新运行 `cr query search` 获取最新路径 | -| `tag-match expected tuple` | 传入 vector 而非 tuple | 改用 `::` 语法,如 `:: :event-name data` | -| `unknown symbol: xxx` | 符号未定义或未 import | `cr query find xxx` 确认位置,`cr edit add-import` 引入 | -| `expects pairs in list for let` | `let` 绑定语法错误 | 改为 `let ((x val)) body`(双层括号) | -| `cannot be used as operator` | 末尾符号被当作函数调用 | 改用 `, acc` 前缀传递值,或用函数包裹 | -| `unknown data for foldl-shortcut` | 参数顺序错误(Calcit vs Clojure 差异) | Calcit 集合在第一位:`map data fn` | -| `Do not include ':require' as prefix` | `cr edit imports` 格式错误 | 去掉 `:require` 前缀,直接传 `src-ns :refer $ sym` | -| `Namespace name mismatch` | `add-ns -e` 名称不一致 | ns 表达式名称必须与位置参数完全一致 | -| 字符串被拆分成多个 token | 没有用 `\|` 或 `"` 包裹 | 使用 `\|complete string` 或 `"complete string` | -| `unexpected format` | Cirru 语法错误 | 用 `cr cirru parse ''` 验证语法 | -| `Type warning` 导致 eval 失败 | 类型不匹配(阻断执行) | 优先检查 `:schema` / `hint-fn` 的参数标注;局部值再用 `assert-type` 复核 | -| `schema mismatch while preprocessing definition` | `:schema` 与 `defn` / `defmacro` / 参数个数不一致 | 修正 `:kind`、`:args`、`:rest`,或让代码定义与 schema 保持一致 | -| `cr query error` 无报错但页面仍异常 | 问题不在 Calcit 语义链路,而在 CSS/DOM/业务值 | 到真实运行环境核对渲染结果、属性值和外部依赖,而不是只看 `query error` | - -### 调试常用命令 +### Agent 自学习最短路径 ```bash -# 查看完整错误栈(最详细) -cr query error - -# 检查某个定义的代码和内容 -cr query def 'ns/def' -cr tree show 'ns/def' - -# 验证 Cirru 语法 -cr cirru parse 'defn add (a b) (+ a b)' - -# 快速测试某个想法(不影响项目代码) -cr eval 'range 5' -cr eval 'let ((xs (list 1 2 3))) (map xs number->string)' - -# 检查定义是否存在 -cr query find 'my-function' -cr query defs 'my.namespace' +cr docs list +cr analyze call-graph +cr analyze count-calls +cr docs search 'tree rewrite' +cr docs read run/edit-tree.md rewrite +cr docs search 'query search-expr' ``` -> 💡 **错误文件备份**:`.calcit-error.cirru` 会保存最近一次的完整错误堆栈(包含 chain 信息),比 `cr query error` 更完整。直接用 `cat .calcit-error.cirru` 读取,或 `cr query error`(从此文件读取并格式化输出)。 +原则:先在 docs 找“最小可行命令”,再回到当前定义做局部试改与验证。 diff --git a/docs/run/agent-advanced.md b/docs/run/agent-advanced.md new file mode 100644 index 00000000..ed56917e --- /dev/null +++ b/docs/run/agent-advanced.md @@ -0,0 +1,1581 @@ +# Calcit 编程 Agent 指南 + +本文档为 AI Agent 提供 Calcit 项目的操作指南。 + +## 🚀 快速开始(新 LLM 必读) + +**硬前置步骤:在执行任何 `cr edit` / `cr tree` 修改前,必须先运行一次 `cr docs agents --full`。** + +这不是建议项,而是进入实际修改前的检查项。跳过这一步,往往会直接沿用旧用法假设,尤其容易误判 `cr tree replace -p ''`、imports 输入格式和 watcher 验收边界。 + +**核心原则:用命令行工具(不要直接编辑文件),用 search 定位(比逐层导航快 10 倍)** + +### 标准流程 + +```bash +# 搜索 → 修改 → 验证 +cr query search 'symbol' -f 'ns/def' # 1. 定位(输出:[3.2.1] in ...) +cr tree replace 'ns/def' -p '3.2.1' --leaf -e 'new' # 2. 修改 +cr tree show 'ns/def' -p '3.2.1' # 3. 验证(可选) +``` + +### 三种搜索方式 + +```bash +cr query search 'target' -f 'ns/def' # 搜索符号/字符串 +cr query search-expr 'fn (x)' -f 'ns/def' -l # 搜索代码结构 +cr tree replace-leaf 'ns/def' --pattern 'old' -e 'new' --leaf # 批量替换叶子节点 +``` + +### 效率对比 + +| 操作 | 传统方法 | search 方法 | 效率 | +| ---------- | ----------------------- | ------------------- | -------- | +| 定位符号 | 逐层 `tree show` 10+ 步 | `query search` 1 步 | **10倍** | +| 查找表达式 | 手动遍历代码 | `search-expr` 1 步 | **10倍** | +| 批量重命名 | 手动找每处 | 自动列出所有位置 | **5倍** | + +--- + +## Tips 输出分级(设计草案) + +当前已有 `--no-tips`,但在 Agent 场景下建议补充统一分级参数:`--tips-level`。 + +### 目标 + +- 默认保留必要引导,但降低噪音(首次扫读更快)。 +- 在脚本/批处理与新手教学之间提供可切换策略。 + +### 建议枚举 + +- `--tips-level minimal`(默认) + - 每次命令最多输出 1 条 tips(优先“下一步动作”)。 +- `--tips-level full` + - 输出全部 tips(教学/排障模式)。 +- `--tips-level none` + - 等价 `--no-tips`(脚本/Agent 静默模式)。 + +### 兼容建议 + +- 继续保留 `--no-tips`,内部映射到 `--tips-level none`。 +- 文档示例默认使用 `minimal` 心智模型,进阶示例再展示 `full/none`。 + +### 迁移建议 + +1. 先在 query/tree 相关子命令接入统一解析。 +2. 统一 Tips 渲染入口,避免各 handler 自行拼装。 +3. 补充回归:默认输出条数、`--no-tips` 等价行为、`full` 全量展示。 + +--- + +## ⚠️ 重要警告:禁止直接修改的文件 + +以下文件**严格禁止使用文本替换或直接编辑**: + +- **`compact.cirru`** - 这是 Calcit 程序的紧凑快照格式,必须使用 `cr edit` 相关命令进行修改 + +这两个文件的格式对空格和结构极其敏感,直接文本修改会破坏文件结构。请使用下面文档中的 CLI 命令进行代码查询和修改。 + +## Calcit 与 Cirru 的关系 + +- **Calcit** 是编程语言本身(一门类似 Clojure 的函数式编程语言) +- **Cirru** 是语法格式(缩进风格的 S-expression,类似去掉括号改用缩进的 Lisp) +- **关系**:Calcit 代码使用 Cirru 语法书写和存储 + +**具体体现:** + +- `compact.cirru` 使用 Cirru 语法存储, 尽量用 `cr edit` 和 `cr tree` 命令修改 +- `cr cirru` 工具用于 Cirru 语法与 JSON 的转换(帮助理解和生成代码) +- Cirru 语法特点: + - 用缩进代替括号(类似 Python/YAML) + - 字符串用前缀 `|` 或 `"` 标记(如 `|hello` 表示字符串 "hello") + - 单行用空格分隔元素(如 `defn add (a b) (+ a b)`) + +**类比理解:** + +- Python 语言 ← 使用 → Python 语法 +- Calcit 语言 ← 使用 → Cirru 语法 + +生成 Calcit 代码前,建议先运行 `cr cirru show-guide` 了解 Cirru 语法规则。 + +--- + +## Calcit CLI 命令 + +Calcit 程序使用 `cr` 命令: + +### 主要运行命令 + +- `cr` 或 `cr compact.cirru` - 代码解释执行,默认读取 config 执行 init-fn 定义的入口(默认单次执行后退出) +- `cr -w` 或 `cr --watch` - 解释执行监听模式(显式启用监听) +- `cr compact.cirru js` - 编译生成 JavaScript 代码(默认单次编译) +- `cr compact.cirru js -w` / `cr compact.cirru js --watch` - JS 监听编译模式 +- `cr compact.cirru ir` - 生成 program-ir.cirru(默认单次生成) +- `cr compact.cirru ir -w` / `cr compact.cirru ir --watch` - IR 监听生成模式 +- `cr -1 ` - 执行一次然后退出(兼容参数,当前默认行为已是 once) +- `cr --check-only` - 仅检查代码正确性,不执行程序 + - 对 init_fn 和 reload_fn 进行预处理验证 + - 输出:预处理进度、warnings、检查耗时 + - 用于 CI/CD 或快速验证代码修改 +- `cr js -1` - 检查代码正确性,生成 JavaScript(兼容参数,默认已是单次) +- `cr js --check-only` - 检查代码正确性,不生成 JavaScript +- `cr --no-tips ...` - 隐藏所有编辑/查询命令输出的 "Tips:" 提示行(适合脚本/Agent 使用) + - 示例:`cr --no-tips demos/compact.cirru query def calcit.core/foldl` +- `cr eval '' [--dep ...]` - 执行一段 Calcit 代码片段,用于快速验证写法 + - **不需要**项目 `compact.cirru`:core 内置函数(`range`、`+`、`map` 等)直接可用 + - 项目自定义函数不可直接 eval(代码未加载),需用 `--dep` 加载外部模块 + - `--dep` 参数可以加载 `~/.config/calcit/modules/` 中的模块(直接使用模块名),可多次使用 + - 示例:`cr eval 'range 5'`、`cr eval 'echo 1' --dep calcit.std` + +### 查询子命令 (`cr query`) + +这些命令用于查询项目信息: + +**项目全局分析:** + +- `cr analyze call-graph` - 分析从入口点开始的调用图结构 +- `cr analyze count-calls` - 统计每个定义的调用次数 + + _使用示例:_ + + ```bash + # 分析整个项目的调用图 + cr analyze call-graph + # 统计调用次数 + cr analyze count-calls + ``` + +**基础查询:** + +- `cr query ns [--deps]` - 列出项目中所有命名空间(--deps 包含依赖) +- `cr query ns ` - 读取命名空间详情(imports, 定义预览) +- `cr query defs ` - 列出命名空间中的定义 +- `cr query pkg` - 获取项目包名 +- `cr query config` - 读取项目配置(init_fn, reload_fn, version) +- `cr query error` - 读取 .calcit-error.cirru 错误堆栈文件 +- `cr query modules` - 列出项目依赖的模块(来自 compact.cirru 配置) + +**渐进式代码探索(Progressive Disclosure):** + +- `cr query peek ` - 查看定义签名(参数、文档、表达式数量),不返回完整实现体 + - 输出:Doc、Form 类型、参数列表、Body 表达式数量、首个表达式预览、Examples 数量 + - 用于快速了解函数接口,减少 token 消耗 +- `cr query def [-j]` - 读取定义的完整 Cirru 代码 + - 默认输出:Doc、Examples 数量、Cirru 格式代码 + - `-j` / `--json`:同时输出 JSON 格式(用于程序化处理) + - 推荐:LLM 直接读取 Cirru 格式即可,通常不需要 JSON +- `cr query schema [-j] [--no-tips]` - 读取定义当前的 schema + - 默认输出:Definition 标识 + schema 的 Cirru one-liner 预览 + - `-j` / `--json`:输出 schema 对应的 Cirru EDN 结构;无 schema 时输出 `nil` + - 适合在修改前确认 `:args` / `:return` / `:rest` 当前值 +- `cr query examples ` - 读取定义的示例代码 + - 输出:每个 example 的 Cirru 格式和 JSON 格式 + +**符号搜索与引用分析:** + +- `cr query find [--deps] [-f] [-n ]` - 跨命名空间搜索符号 + - 默认精确匹配:返回定义位置 + 所有引用位置(带上下文预览) + - `-f` / `--fuzzy`:模糊搜索,匹配 "namespace/definition" 格式的路径 + - `-n `:限制模糊搜索结果数量(默认 20) + - `--deps`:扩展到 `calcit.*` 内置核心命名空间(默认已包含项目 modules 依赖) +- `cr query usages [--deps]` - 查找定义的所有使用位置 + - 返回:引用该定义的所有位置(带上下文预览) + - 用于理解代码影响范围,重构前的影响分析 + - `--deps`:同上,扩展到 `calcit.*` 核心命名空间 + +**代码模式搜索(快速定位 ⭐⭐⭐):** + +- `cr query search [-f ] [-l]` - 搜索叶子节点(符号/字符串),比逐层导航快 10 倍 + - **搜索范围**:默认包含项目代码、全部 modules 依赖和 calcit.core 内置函数(无需 `--deps` 标志) + - `--entry `:额外加载 `entries..modules` 里的依赖(用于 entry 级依赖场景) + - `-f ` - 过滤到特定命名空间或定义(可缩小范围提升速度) + - `-l / --loose`:宽松匹配,包含模式 + - `-d `:限制搜索深度 + - `-p `:从指定路径开始搜索(如 `"3.2.1"`,也兼容 `"3,2,1"`) + - 返回:完整路径 + 父级上下文,多个匹配时自动显示批量替换命令 + - 示例: + - `cr query search 'println' -f app.main/main!` - 精确搜索(过滤到某定义) + - `cr query search 'comp-' -f app.ui/layout -l` - 模糊搜索(所有 comp- 开头) + - `cr query search 'task-id'` - 全项目搜索(含 modules) + +**高级结构搜索(搜索代码结构 ⭐⭐⭐):** + +- `cr query search-expr [-f ] [-l] [-j]` - 搜索结构表达式(List) + - **搜索范围**:同 `search`,默认包含全部依赖和 calcit.core + - `--entry `:同上,额外加载指定 entry 的 modules + - `-l / --loose`:宽松匹配,从头部开始的前缀匹配(嵌套表达式也支持前缀) + - `-j / --json`:将模式解析为 JSON 数组 + - 示例: + - `cr query search-expr 'fn (x)' -f app.main/process -l` - 查找函数定义 + - `cr query search-expr '>> state task-id' -l` - 查找状态访问(匹配 `>> state task-id ...` 或 `>> state`) + - `cr query search-expr 'dispatch! (:: :states)' -l` - 匹配 `dispatch! (:: :states data)` 类型的表达式 + - `cr query search-expr 'memof1-call-by' -l` - 查找记忆化调用 + +**搜索结果格式:** `[索引1.索引2...] in 父级上下文`,可配合 `cr tree show -p ''` 查看节点。逗号路径仍兼容,但文档与输出优先使用点号。**修改代码时优先用 search 命令,比逐层导航快 10 倍。** + +### LLM 辅助:动态方法提示 + +在运行时调试 trait 分派时,可使用以下内置函数(低频场景,需运行期有值后调用): + +- `&methods-of value` — 列出某值的可用方法名(返回字符串列表 `[] |.foo |.bar ...`) +- `&inspect-methods value` — 打印方法与 impl 来源(调试 trait override 链,可临时插入 pipeline) +- `&impl:origin impl` — 读取 impl record 的 trait 来源 +- `&trait-call Trait :method receiver & args` — 显式消歧:只调用属于指定 trait 的方法实现 + +> 📖 深入了解 trait 实现机制:`cr docs read traits.md` 或 `cr docs search 'trait-call'` + +### 文档子命令 (`cr docs`) + +查询 Calcit 语言文档(guidebook): + +- `cr docs search [-c ] [-f ]` - 按关键词搜索文档内容 + - `-c ` - 显示匹配行的上下文行数(默认 5) + - `-f ` - 按文件名过滤搜索结果 + - 输出:匹配行及其上下文,带行号和高亮 + - 示例:`cr docs search 'macro' -c 10` 或 `cr docs search 'defn' -f macros.md` + +- `cr docs read [ ...]` - 按 Markdown 标题阅读文档 + - 不传标题时:列出文档内所有标题 + - 传入一个或多个标题关键词时:按标题做模糊匹配并输出对应章节内容 + - 示例:`cr docs read macros.md` 或 `cr docs read run.md eval options` + +- `cr docs read-lines [-s ] [-n ]` - 按行读取文档(兼容旧行为) + - `-s ` - 起始行号(默认 0) + - `-n ` - 读取行数(默认 80) + - 输出:文档内容、当前范围、是否有更多内容 + - 示例:`cr docs read-lines intro.md -s 20 -n 30` + +- `cr docs list` - 列出所有可用文档 +- `cr docs agents [ ...] [--full]` - 读取 Agent 指南(即本文档,优先本地缓存,按天自动刷新) + - 不传标题时列出所有标题;传关键词时按标题模糊匹配输出对应章节 + +### Cirru 语法工具 (`cr cirru`) + +用于 Cirru 语法和 JSON 之间的转换: + +- `cr cirru parse ''` - 解析 Cirru 代码为 JSON +- `cr cirru format ''` - 格式化 JSON 为 Cirru 代码 +- `cr cirru parse-edn ''` - 解析 Cirru EDN 为 JSON +- `cr cirru show-guide` - 显示 Cirru 语法指南(帮助 LLM 生成正确的 Cirru 代码) + +**⚠️ 重要:生成 Cirru 代码前请先阅读语法指南** + +运行 `cr cirru show-guide` 获取完整的 Cirru 语法说明,包括: + +- `$` 操作符(单节点展开) +- `|` 前缀(字符串字面量), 这个是 Cirru 特殊的地方, 而不是直接用引号包裹 +- `,` 操作符(注释标记) +- `~` 和 `~@`(宏展开) +- 常见错误和避免方法 + +### 库管理 (`cr libs`) + +查询和了解 Calcit 官方库: + +- `cr libs` - 列出所有官方库 +- `cr libs search ` - 按关键词搜索库(搜索名称、描述、分类) +- `cr libs readme [-f ]` - 查看库的文档 + - 优先从本地 `~/.config/calcit/modules/` 读取 + - 本地不存在时从 GitHub 仓库获取 + - `-f` 参数可指定其他文档文件(如 `-f Skills.md`) + - 默认读取 `README.md` +- `cr libs scan-md ` - 扫描本地模块目录下的所有 `.md` 文件 + - 递归扫描子目录 + - 显示相对路径列表 +- `caps` - 安装/更新依赖(默认读取 `deps.cirru`,也可传自定义文件路径) + - 独立工具(非 `cr` 子命令) + - `caps`:按 `deps.cirru` 当前依赖执行更新 + - `caps add /`:添加依赖并执行默认更新流程 + - `caps remove /`:移除依赖并执行默认更新流程 + - `caps add/remove` 同时支持完整 GitHub 地址(如 `https://github.com/calcit-lang/memof`) + - `caps add -r `:写入指定分支/版本(默认 `main`) + +**查看已安装模块:** + +```bash +# 列出 ~/.config/calcit/modules/ 下所有已安装的模块 +ls ~/.config/calcit/modules/ + +# 查看当前项目配置的模块依赖 +cr query modules +``` + +### 精细代码树操作 (`cr tree`) + +⚠️ **关键警告:路径索引动态变化** + +删除或插入节点后,同级后续节点的索引会自动改变。**必须从后往前操作**或**每次修改后重新搜索路径**。 + +**核心概念:** + +- 路径格式:优先使用点号分隔的索引(如 `"3.2.1"`),逗号写法 `"3,2,1"` 仍兼容;空字符串 `""` 表示根节点 +- `-p ''` 仅表示“根节点”,**不等于推荐的整定义重写方案**;要整体替换定义时,优先使用 `cr edit def --overwrite -f ` +- 每个命令都有 `--help` 查看详细参数 +- 命令执行后会显示 "Next steps" 提示下一步操作 + +**主要操作:** + +- `cr tree show -p '' [-j]` - 查看节点 + - 默认输出:节点类型、Cirru 预览、子节点索引列表、操作提示 + - `-j` / `--json`:同时输出 JSON 格式(用于程序化处理) + - 推荐:直接查看 Cirru 格式即可,通常不需要 JSON +- `cr tree replace` - 替换节点 + - 适合局部节点替换;若目标是**整条定义**,优先改用 `cr edit def --overwrite -f `,比 `cr tree replace -p ''` 更可预期 +- `cr tree replace-leaf` - 查找并替换所有匹配的 leaf 节点(无需指定路径) + - `--pattern ` - 要搜索的模式(精确匹配 leaf 节点) + - 使用 `-e, -f, -j` 等通用参数提供替换内容 + - 自动遍历整个定义,一次性替换所有匹配项 + - 示例:`cr tree replace-leaf 'ns/def' --pattern 'old-name' -e 'new-name' --leaf` +- `cr tree target-replace` - 基于内容的唯一替换(无需指定路径,更安全 ⭐⭐⭐) + - `--pattern ` - 要搜索的模式(精确匹配 leaf 节点) + - 使用 `-e, -f, -j` 等通用参数提供替换内容 + - 逻辑:自动查找叶子节点,若唯一则替换;若不唯一则报错并列出所有位置及修改命令建议。 +- `cr tree delete -p ''` - 删除指定路径节点(⚠️ 后续同级索引自动减小) + - 示例:`cr tree delete app.core/fn -p '3,2'` +- `cr tree insert-before -p ''` / `cr tree insert-after` - 在路径节点的前/后插入兄弟节点 + - 示例:`cr tree insert-before app.core/fn -p '3,2' -e 'new-expr'` +- `cr tree insert-child -p ''` / `cr tree append-child` - 在某节点内部最前/最后插入子节点 + - 示例:`cr tree append-child app.core/fn -p '3' --leaf -e 'new-arg'` +- `cr tree swap-next -p ''` / `cr tree swap-prev` - 将节点与其下一个/上一个兄弟节点交换位置 +- `cr tree rewrite` - 用引用原节点的新结构替换节点(`--with` 必须;需引用子节点时使用) +- `cr tree wrap` - 快捷包裹节点:将 `self` 替换为原节点(`rewrite --with self=.` 的语法糖) +- `cr tree unwrap` - 将节点的所有子节点展开拼接到父节点中(拆包),原节点消失 +- `cr tree raise` - 用指定子节点替换其父节点(Paredit raise-sexp) + +**输入方式(通用):** + +- `-e ''` - 内联代码(自动识别 Cirru/JSON) +- `--leaf` - 强制作为 leaf 节点(符号或字符串) +- `-j ''` / `-f ` + +简单更新尽量用结构化的 API 操作. 多行或者带特殊符号的表达式, 可以在 `.calcit-snippets/` 创建临时文件, 然后用 `cr cirru parse` 验证语法, 最后用 `-f ` 提交, 从而减少错误率. 复杂表达式建议分段, 然后搭配 `cr tree target-replace` 命令来完成多阶段提交. + +**整体替换定义的经验规则:** + +- 局部节点修改:继续使用 `cr tree replace -p ''` +- 整条定义重写:优先使用 `cr edit def --overwrite -f ` +- 只有在你明确知道根节点替换后的结构,并且能立刻验证完整定义时,才考虑 `cr tree replace -p ''` + +**推荐工作流(高效定位 ⭐⭐⭐):** + +```bash +# ===== 方案 A:单点修改(精确定位) ===== + +# 1. 快速定位目标节点(一步到位) +cr query search 'target-symbol' -f namespace/def +# 输出:[3.2.5.1] in (fn (x) target-symbol ...) + +# 2. 直接修改(路径已知) +cr tree replace namespace/def -p '3.2.5.1' --leaf -e 'new-symbol' + +# 3. 验证结果(可选) +cr tree show namespace/def -p '3.2.5.1' + + +# ===== 方案 B:批量重命名(多处修改) ===== + +# 1. 搜索所有匹配位置 +cr query search 'old-name' -f namespace/def +# 自动显示:4 处匹配,已按路径从大到小排序 +# [3.2.5.8] [3.2.5.2] [3.1.0] [2.1] + +# 2. 按提示从后往前修改(避免路径变化) +cr tree replace namespace/def -p '3.2.5.8' --leaf -e 'new-name' +cr tree replace namespace/def -p '3.2.5.2' --leaf -e 'new-name' +# ... 继续按序修改 + +# 或:一次性替换所有匹配项 +cr tree replace-leaf namespace/def --pattern 'old-name' -e 'new-name' --leaf + + +# ===== 方案 C:基于内容的半自动替换(最推荐 ⭐⭐⭐) ===== + +# 1. 尝试基于叶子节点内容直接替换 +cr tree target-replace namespace/def --pattern 'old-symbol' -e 'new-symbol' --leaf + +# 2. 如果存在多个匹配,命令会报错并给出详细指引(包含具体路径的 replace 命令建议) +# 如果确定要全部替换,可改用 tree replace-leaf + + +# ===== 方案 D:结构搜索(查找表达式) ===== + +# 1. 搜索包含特定模式的表达式 +cr query search-expr "fn (task)" -f namespace/def -l +# 输出:[3.2.2.5.2.4.1] in (map $ fn (task) ...) + +# 2. 查看完整结构(可选) +cr tree show namespace/def -p '3.2.2.5.2.4.1' + +# 3. 修改整个表达式或子节点 +cr tree replace namespace/def -p '3.2.2.5.2.4.1.2' -e 'let ((x 1)) (+ x task)' +``` + +**关键技巧:** + +- **优先使用 `search` 系列命令**:比逐层导航快 10+ 倍,一步直达目标 +- **路径格式**:`"3.2.1"` 表示第3个子节点 → 第2个子节点 → 第1个子节点;`"3,2,1"` 仍兼容 +- **批量修改自动提示**:搜索找到多处时,自动显示路径排序和批量替换命令 +- **路径动态变化**:删除/插入后,同级后续索引会变化,按提示从后往前操作 +- **批量执行不要用 `&&` 粘成一行**:尤其当 `-e` 内容里有引号、`|` 字符串或复杂表达式时,优先逐条执行,或写入 `-f ` 避免 shell 进入未闭合引号状态 +- 所有命令都会显示 Next steps 和操作提示 + +**结构化变更示例:** + +`cr tree rewrite` 用于在替换时引用原节点及其子节点,必须传至少一个 `--with name=path`(不需要引用时直接用 `replace`)。`--with` 格式:`name=path`,`.` 表示原节点本身,数字表示子节点索引。 + +- **包裹节点**(推荐用 `wrap`,`self` 作为占位符): + + ```bash + # 将路径 "3,2" 的节点包裹在 println 中(self = 原节点) + cr tree wrap ns/def -p '3,2' -e 'println self' + + # 等价的完整写法(需要引用子节点时才用 rewrite) + cr tree rewrite ns/def -p '3,2' -e 'println self' -w 'self=.' + ``` + +- **引用原节点局部**(`rhs=2` 引用子节点索引 2): + - 假设原节点是 `+ 1 2`(路径 `3,1`),子节点索引 2 是 `2` + - 将其重构为 `* rhs 10`: + + ```bash + cr tree rewrite ns/def -p '3,1' -e '* rhs 10' -w 'rhs=2' + ``` + +- **多处重用原节点**: + + ```bash + # 将节点变为 `+ x x`(self 引用原节点本身) + cr tree rewrite ns/def -p '2' -e '+ self self' -w 'self=.' + ``` + +- **拆包节点**(`unwrap`)——将节点的所有子节点展开拼接到父节点中,原节点消失: + + ```bash + # 将路径 "3,2" 的节点拆包,所有子节点直接插入到原位置 + cr tree unwrap ns/def -p '3,2' + ``` + + 详细参数和示例使用 `cr tree --help` 查看。 + +- **提升子节点替换父节点**(`raise`)——用某子节点整体替换掉其父节点(Paredit `raise-sexp`): + + ```bash + # 路径 "3,2" 的节点整体替换掉其父节点 "3" + # 使用场景:去掉 let 外层只保留返回值,或去掉 if 只保留 then/else 分支 + cr tree raise ns/def -p '3,2' + ``` + +- **提取子表达式为新定义**(`split-def`)——将某路径的子表达式提取为同 ns 的新定义,原位替换为新名字: + + ```bash + # 将路径 "3,2" 的子表达式提取为新定义 compute-helper(同 namespace) + cr edit split-def app.util/process -p '3,2' -n compute-helper + ``` + + 详细参数使用 `cr edit split-def --help` 查看。 + +### 复杂表达式分段组装策略 (Incremental Assembly) ⭐⭐⭐ + +当需要构造非常复杂的嵌套结构(例如递归循环、多级 `let` 或 `if`)时,直接通过 `-e` 传入单行 Cirru 代码容易遇到 shell 转义、括号对齐或长度限制等问题。推荐使用**分段占位组装**策略: + +简单提示: + +- 占位符统一使用 `{{NAME}}` 风格,例如 `{{BODY}}`、`{{TRUE_BRANCH}}`; +- 大表达式可以先用 `cr query def ` 看整体分片,再用 `cr tree show -p ''` 深入某个片段; +- 真正填充时,优先用 `cr tree target-replace` 找占位符,不唯一时再退回路径替换。 + +1. **确立骨架**:先替换目标节点为一个带有占位符的简单 JSON 结构。 + + ```bash + cr tree replace ns/def -p '4.0' -j '["let", [["x", "1"]], "{{BODY}}"]' + ``` + +2. **定位占位符**:使用 `tree show` 确认占位符的具体路径。 + + ```bash + cr tree show ns/def -p '4.0' + # 输出显示 "{{BODY}}" 在索引 2,即路径 [4.0.2] + ``` + +3. **填充内容**:针对占位符路径进行下一层的精细替换。 + + ```bash + cr tree replace ns/def -p '4.0.2' -j '["if", ["=", "x", "1"], "{{TRUE_BRANCH}}", "{{FALSE_BRANCH}}"]' + ``` + +4. **递归迭代**:重复上述步骤直到所有占位符(如 `{{TRUE_BRANCH}}`、`{{FALSE_BRANCH}}`)都被替换为最终逻辑。 + +**优势:** + +- **精确性**:使用 JSON 格式 (`-j`) 可以完全避免 Cirru 缩进或括号解析的歧义。 +- **低风险**:每次只修改一小部分,出错时容易通过 `tree show` 快速定位。 +- **绕过限制**:解决某些终端对超长命令行参数的限制。 + +### 代码编辑 (`cr edit`) + +直接编辑 compact.cirru 项目代码,支持两种输入方式: + +- `--file ` 或 `-f ` - 从文件读取(默认 Cirru 格式,使用 `-J` 指定 JSON) +- `--json ` 或 `-j ` - 内联 JSON 字符串 + +额外支持“内联代码”参数: + +- `--code ` 或 `-e `:直接在命令行里传入一段代码。 + - 默认按 **Cirru 单行表达式(one-liner)** 解析。 + - 如果输入“看起来像 JSON”(例如 `-e '"abc"'`,或 `-e '["a"]'` 这类 `[...]` 且包含 `"`),则会按 JSON 解析。 + - ⚠️ 当输入看起来像 JSON 但 JSON 不合法时,会直接报错(不会回退当成 Cirru one-liner)。 + +对 `--file` 输入,还支持以下“格式开关”(与 `-J/--json-input` 类似): + +- `--leaf`:把输入当成 **leaf 节点**,直接使用 Cirru 符号或 `|text` 字符串,无需 JSON 引号。 + - 传入符号:`-e 'my-symbol'` + - 传入字符串:加 Cirru 字符串前缀 `|` 或 `"`,例如 `-e '|my string'` 或 `-e '"my string'` + +⚠️ 注意:这些开关彼此互斥(一次只用一个)。 + +**推荐简化规则(命令行更好写):** + +- **JSON(单行)**:优先用 `-j ''` 或 `-e ''`(不需要 `-J`)。 +- **Cirru 单行表达式**:用 `-e ''`(`-e` 默认按 one-liner 解析)。 +- **Cirru 多行缩进**:用 `-f file.cirru`。 +- `-J/--json-input` 主要用于 **file** 读入 JSON(如 `-f code.json -J`)。 + +补充:`-e/--code` 只有在 `[...]` 内部包含 `"` 时才会自动按 JSON 解析(例如 `-e '["a"]'`)。 +像 `-e '[]'` / `-e '[ ]'` 会默认按 Cirru one-liner 处理;如果你需要“空 JSON 数组”,用显式 JSON:`-j '[]'`。 + +如果你想在命令行里明确“这段就是 JSON”,请用 `-j ''`(`-J` 是给 file 用的)。 + +**定义操作:** + +- `cr edit format` - 不修改语义,按当前快照序列化逻辑重写 **snapshot 文件**(用于刷新格式) + - 也会把旧的 namespace `CodeEntry` 写法收敛成当前的 `NsEntry` 结构 + - 适用:普通 `compact.cirru` / 项目 snapshot 文件 + - 不适用:calcit-editor 专用的 `calcit.cirru` 结构文件 +- `cr edit def ` - 添加新定义(默认若已存在会报错;加 `--overwrite` 可强制覆盖) + - 经验语义:**不带 `--overwrite` = create-only;带 `--overwrite` = replace existing definition** + - 若当前输出文案仍显示 `Created definition`,以你的调用方式和目标是否已存在为准理解,不要把该提示字面理解为“必然新增成功” +- `cr edit rename ` - 在当前命名空间内重命名定义(不可覆盖) +- `cr edit mv-def ` - 将定义移动到另一个命名空间(跨命名空间移动) +- `cr edit cp --from -p [--at ]` - 在定义内复制 AST 节点到另一位置 +- `cr edit mv --from -p [--at ]` - 在定义内移动 AST 节点(复制后删除原位置;自动防止移入自身子树) +- `cr edit split-def -p -n ` - 将定义内某路径的子表达式提取为同命名空间内的新定义,原位置替换为新定义名称(新名称不可与已有定义重名) +- `cr edit rm-def ` - 删除定义 +- `cr edit doc ''` - 更新定义的文档 +- `cr edit schema ` - 更新定义 schema(写入前会校验 schema 结构) + - 常用输入:`-e ':: :fn $ {} (:args $ [] :number :number) (:return :number)'` + - 也支持 `-f ` / `-j ''` / `-J` / `--leaf` + - `--clear`:清空 schema,恢复为 `nil` + - 写入后会保存为直接 map 形式;后续运行与 preprocess 会用它做 `defn` / `defmacro` 一致性校验 +- `cr edit examples ` - 设置定义的示例代码(批量替换) +- `cr edit add-example ` - 添加单个示例 +- `cr edit rm-example ` - 删除指定索引的示例(0-based) + +**命名空间操作:** + +> ⚠️ **关键:各命令的 `-e` 期望格式不同,不可混用,详见下方「命名空间操作陷阱」** + +- `cr edit add-ns ` - 添加命名空间 + - 无 `-e`:创建空 ns(推荐;再用 `add-import` 逐条添加) + - `-e 'ns my.ns $ :require ...'`:需传完整 `ns` 表达式,名称必须与位置参数一致 +- `cr edit rm-ns ` - 删除命名空间 +- `cr edit imports ` - 更新导入规则(**全量替换**所有 import) + - `-e 'source-ns :refer $ sym1 sym2'`:单条规则(**不含** `:require` 前缀) + - `-f rules.cirru`:多条规则文件,每行一条(推荐多条场景) + - `-j '[["src-ns",":refer",["sym"]],...]'`:JSON 数组格式,每元素为一条规则 +- `cr edit add-import ` - 添加单个 import 规则(**不替换**已有规则) + - `-e 'source-ns :refer $ sym1 sym2'`:单条规则 + - `-o` / `--overwrite`:覆盖已存在的同名源 ns 规则 +- `cr edit rm-import ` - 移除指定来源的 import 规则 +- `cr edit ns-doc ''` - 更新命名空间文档 + +**模块和配置:** + +- `cr edit add-module ` - 添加模块依赖 +- `cr edit rm-module ` - 删除模块依赖 +- `cr edit config ` - 设置配置(key: init-fn, reload-fn, version) + +**增量变更导出:** + +- `cr edit inc` - 记录增量代码变更并导出到 `.compact-inc.cirru`,触发 watcher 热更新 + - `--added ` - 标记新增的定义 + - `--changed ` - 标记修改的定义 + - `--removed ` - 标记删除的定义 + - TIP: 使用 `cr edit mv` 移动定义后,需手动执行 `cr edit inc --removed --added ` 以更新 watcher。 + - `--added-ns ` - 标记新增的命名空间 + - `--removed-ns ` - 标记删除的命名空间 + - `--ns-updated ` - 标记命名空间导入变更 + - 配合 watcher 使用实现热更新(详见"开发调试"章节) + +使用 `--help` 参数了解详细的输入方式和参数选项。 + +--- + +## Calcit 语言基础 + +### Cirru 语法核心概念 + +**与其他 Lisp 的区别:** + +- **缩进语法**:用缩进代替括号(类似 Python/YAML),单行用空格分隔 +- **字符串前缀**:`|hello` 或 `"hello"` 表示字符串,`|` 前缀更简洁 +- **无方括号花括号**:只用圆括号概念(体现在 JSON 转换中),Cirru 文本层面无括号 + +**常见混淆点:** + +❌ **错误理解:** Calcit 字符串是 `"x"` → JSON 是 `"\"x\""` +✅ **正确理解:** Cirru `|x` → JSON `"x"`,Cirru `"x"` → JSON `"x"` + +**字符串 vs 符号的关键区分:** + +- `|Add` 或 `"Add` → **字符串**(用于显示文本、属性值等, 前缀形式区分字面量类型) +- `Add` → **符号/变量名**(Calcit 会在作用域中查找) +- 常见错误:受其他语言习惯影响,忘记加 `|` 前缀导致 `unknown symbol` 错误 + +**CLI 使用提示:** + +- 替换包含空格的字符串:`--leaf -e '|text with spaces'` 或 `-j '"text"'` +- 避免解析为列表:字符串字面量必须用 `--leaf` 或 `-j` 明确标记 + +**示例对照:** + +| Cirru 代码 | JSON 等价 | JavaScript 等价 | +| ---------------- | -------------------------------- | ------------------------ | +| `\|hello` | `"hello"` | `"hello"` | +| `"world"` | `"world"` | `"world"` | +| `\|a b c` | `"a b c"` | `"a b c"` | +| `fn (x) (+ x 1)` | `["fn", ["x"], ["+", "x", "1"]]` | `fn(x) { return x + 1 }` | + +### 数据结构:Tuple vs Vector + +Calcit 特有的两种序列类型: + +**Tuple (`::`)** - 不可变、用于模式匹配 + +```cirru +; 创建 tuple +:: :event/type data + +; 模式匹配 +tag-match event + (:event/click data) (handle-click data) + (:event/input text) (handle-input text) +``` + +**Vector (`[]`)** - 可变、用于列表操作 + +```cirru +; 创建 vector +[] item1 item2 item3 + +; DOM 列表 +div {} $ [] + button {} |Click + span {} |Text +``` + +**常见错误:** + +```cirru +; ❌ 错误:用 vector 传事件 +send-event! $ [] :clipboard/read text +; 报错:tag-match expected tuple + +; ✅ 正确:用 tuple +send-event! $ :: :clipboard/read text +``` + +### 类型标注与检查 + +Calcit 提供了静态类型分析系统,可以在预处理阶段发现潜在的类型错误。 + +#### 1. 顶层定义优先使用 `:schema`,局部函数继续使用 `hint-fn` + +现在更推荐这样分工: + +- 顶层 `defn` / `defmacro` 的参数、返回值、泛型信息,优先写到 `:schema` +- 局部 `fn` / 内部辅助函数,继续用 `hint-fn` +- `assert-type` 仍然可用,但更适合做函数体内的额外约束或中间值检查,而不是顶层定义的主标注方式 + +验证示例: + +```cirru +let + sum-items $ fn (items) + foldl items 0 $ fn (acc item) + hint-fn $ {} + :args $ [] :number :number + :return :number + &+ acc item + sum-items ([] 1 2 3) +``` + +#### 2. 返回类型标注 + +有两种方式标注函数返回类型: + +- **紧凑模式(推荐)**:紧跟在参数列表后的类型标签。 +- **正式模式**:局部 `fn` 使用 `hint-fn`(通常放在函数体开头);顶层 `defn` / `defmacro` 使用 `:schema`。 + - 泛型变量:`hint-fn $ {} (:generics $ [] 'T 'S)` + - 旧 clause 写法(如 `(hint-fn (return-type ...))` / `(generics ...)` / `(type-vars ...)`)已不再支持,会直接报错。 + +验证示例: + +```cirru +let + ; 紧凑模式 + add $ fn (a b) :number + &+ a b + ; 正式模式 + get-name $ fn (user) + hint-fn $ {} (:args $ [] :dynamic) (:return :string) + |demo + ; 泛型声明示例 + id $ fn (x) + hint-fn $ {} (:generics $ [] 'T) (:args $ [] 'T) (:return 'T) + x + add 1 2 +``` + +#### 3. 支持的类型标签 + +| 标签 | 说明 | +| ---------- | ----------------- | +| `:number` | 数字 | +| `:string` | 字符串 | +| `:bool` | 布尔值 | +| `:symbol` | 符号 | +| `:tag` | 标签 (Keyword) | +| `:list` | 列表 | +| `:map` | 哈希映射 | +| `:set` | 集合 | +| `:tuple` | Tuple | +| `:fn` | 函数 | +| `:dynamic` | 任意类型 (通配符) | + +> 约定:动态类型标注统一使用 `:dynamic`,不再使用 `:any` 或 `nil` 作为 dynamic 的显式写法。 + +**高阶函数(HOF)回调类型检查:** + +内置 HOF(`foldl`、`sort`、`filter`、`find`、`find-index`、`filter-not`、`mapcat`、`group-by` 等)的回调参数已强制要求 `:fn` 类型。传入非函数值(如数字、字符串)时会在预处理阶段触发类型警告: + +```bash +# ❌ 错误:第三个参数应为函数,传了数字 +cr eval 'foldl (list 1 2 3) 0 42' +# Type warning: expects :fn but got :number + +# ✅ 正确 +cr eval 'foldl (list 1 2 3) 0 &+' +``` + +#### 4. 复杂类型标注 + +- **可选类型**:`:: :optional :string` (可以是 string 或 nil) +- **变长参数**:在 Schema 中使用 `:rest :number` (参数列表剩余部分均为 number) +- **结构体/枚举**:使用 `defstruct` 或 `defenum` 定义的名字 + +验证示例 (使用 `let` 封装多表达式以支持 `cr eval` 验证): + +```cirru +let + ; 可选参数 + greet $ fn (name) + hint-fn $ {} (:args $ [] (:: :optional :string)) (:return :string) + str "|Hello " (or name "|Guest") + + ; 变长参数 + sum $ fn (& xs) + hint-fn $ {} (:rest :number) (:return :number) + reduce xs 0 &+ + + ; Record 约束 (使用 defstruct 定义结构体) + User $ defstruct User (:name :string) + get-name $ fn (u) + hint-fn $ {} (:args $ [] (:: :record User)) (:return :string) + get u :name + println $ greet |Alice + println $ sum 1 2 3 + println $ get-name (%{} User (:name |Bob)) +``` + +**验证类型:** 运行或者编译时会先完成校验. + +#### 5. Schema 与 `defn` / `defmacro` 一致性检查 + +如果定义带有 `:schema`,现在不仅 `cr analyze check-types` 会检查,普通运行路径也会在 **preprocess 阶段** 直接校验: + +- `:: :fn` 必须对应 `defn` +- `:: :macro` 必须对应 `defmacro` +- `:args` 的必选参数个数必须和实际参数列表一致 +- `:rest` 必须和代码里的 `&` rest 参数一致 + +这意味着下面几类命令都会在启动时直接失败,而不是等到 `analyze` 才发现: + +- `cr ` +- `cr --check-only ` +- `cr js` +- `yarn try-rs` + +复杂但正确的示例(顶层用 `:schema`,局部函数用 `hint-fn`): + +```cirru +|join-str $ %{} :CodeEntry (:doc |) + :code $ quote + defn join-str (xs0 sep) + apply-args (| xs0 true) + defn %join-str (acc xs beginning?) + hint-fn $ {} + :args $ [] :string :list :bool + :return :string + list-match xs + () acc + (x0 xss) + recur + &str:concat + if beginning? acc $ &str:concat acc sep + , x0 + , xss false + :examples $ [] + :schema $ :: :fn $ {} (:return :string) + :args $ [] :list :string +``` + +这个例子里,schema 与代码是完全对齐的: + +- `:: :fn` 对应 `defn` +- `:args` 里 2 个必选参数,对应 `(xs0 sep)` +- `:return :string` 对应整个 `join-str` 的返回值 +- 内部辅助函数 `%join-str` 不是顶层定义,所以继续用 `hint-fn` + +可以简单记忆为:**namespace 上的定义看 `:schema`,函数体内部的辅助函数看 `hint-fn`。** + +推荐工作流: + +```bash +# 先查看 calcit.core 里真实存在的 schema +cr query schema calcit.core/join-str + +# 再仿照它给自己的定义写 schema +cr edit schema app.main/my-fn -e ':: :fn $ {} (:args $ [] :list :string) (:return :string)' + +# 最后验证 +cr --check-only +cr analyze check-types +``` + +实务上,`analyze check-types` 更适合做全量巡检;普通运行路径现在会做 fail-fast 阻断。 + +### 其他易错点 + +比较容易犯的错误: + +- Calcit 中字符串通过前缀区分,`|` 和 `"` 开头表示字符串。`|x` 对应 JavaScript 字符串 `"x"`。产生 JSON 时注意不要重复包裹引号。 +- Calcit 采用 Cirru 缩进语法,可以理解成去掉跨行括号改用缩进的 Lisp 变种。用 `cr cirru parse` 和 `cr cirru format` 互相转化试验。 +- Calcit 跟 Clojure 在语义上比较像,但语法层面只用圆括号,不用方括号花括号。 + +--- + +## 开发调试 + +简单脚本可直接使用 `cr ` 执行(默认单次)。编译 JavaScript 用 `cr js` 执行一次编译。 +若需要监听模式,显式添加 `-w` / `--watch`(如 `cr -w `、`cr js -w`)。 + +Calcit snapshot 文件中 config 有 `init-fn` 和 `reload-fn` 配置: + +- 初次启动调用 `init-fn` +- 每次修改代码后调用 `reload-fn` + +**典型开发流程:** + +```bash +# 1. 启动监听模式(用户自行使用) +cr -w # 解释执行监听模式 +cr js -w # JS 编译监听模式 +cr ir -w # IR 生成监听模式 + +# 2. 修改代码后触发增量更新(详见"增量触发更新"章节) +cr edit inc --changed ns/def + +# 3. 一次性执行/编译(用于简单脚本) +cr # 执行一次 +cr js # 编译一次 +cr ir # 生成一次 IR +``` + +### 增量触发更新(推荐)⭐⭐⭐ + +当使用监听模式(`cr -w` / `cr js -w` / `cr ir -w`)开发时,推荐使用 `cr edit inc` 命令触发增量更新,而非全量重新编译/执行: + +**工作流程:** + +```bash +# 【终端 1】启动 watcher(监听模式) +cr -w # 或 cr js -w / cr ir -w + +# 【终端 2】修改代码后触发增量更新 +# 修改定义 +cr edit def app.core/my-fn -e 'defn my-fn (x) (+ x 1)' + +# 触发增量更新 +cr edit inc --changed app.core/my-fn + +# 等待 ~300ms 后查看编译结果 +cr query error +``` + +**增量更新命令参数:** + +```bash +# 新增定义 +cr edit inc --added namespace/definition + +# 修改定义 +cr edit inc --changed namespace/definition + +# 删除定义 +cr edit inc --removed namespace/definition + +# 新增命名空间 +cr edit inc --added-ns namespace + +# 删除命名空间 +cr edit inc --removed-ns namespace + +# 更新命名空间导入 +cr edit inc --ns-updated namespace + +# 组合使用(批量更新) +cr edit inc \ + --changed app.core/add \ + --changed app.core/multiply \ + --removed app.core/old-fn +``` + +**查看编译结果:** + +```bash +cr query error # 命令会显示详细的错误信息或成功状态 +``` + +`cr query error` 只能告诉你最近一次 Calcit 语义链路里有没有报错,例如解析、预处理、运行期异常;它**不能**证明浏览器 CSS、HTML 属性值、业务数据内容或外部系统配置是“合理的”。像 `|max(...)` 被误写成 `"|max(...)` 这类在 Cirru 层面仍合法的字符串,就可能通过 `cr query error`,但在浏览器渲染阶段失效。 + +**何时使用全量操作:** + +```bash +# 极少数情况:增量更新不符合预期时 +cr -1 js # 重新编译 JavaScript +cr -1 # 重新执行程序 + +# 或重启监听模式(Ctrl+C 停止后重启) +cr # 或 cr js +``` + +**增量更新优势:** 快速反馈、精确控制变更范围、watcher 保持运行状态 + +--- + +## 文档支持 + +遇到疑问时使用: + +- `cr docs search ` - 搜索 Calcit 教程内容 +- `cr docs agents [ ...] [--full]` - 读取 Agent 指南(优先本地缓存,按天自动刷新) +- `cr docs read [ ...]` - 按标题查看章节(不传标题时列标题) +- `cr docs read --full` - 直接读取整份文档内容 +- `cr docs read-lines -s -n ` - 按行读取文档 +- `cr docs list` - 查看所有可用文档 +- `cr query ns ` - 查看命名空间说明和函数文档 +- `cr query peek ` - 快速查看定义签名 +- `cr query def ` - 读取完整语法树 +- `cr query examples ` - 查看示例代码 +- `cr query find ` - 跨命名空间搜索符号 +- `cr query usages ` - 查找定义的使用位置 +- `cr query search [-f ]` - 搜索叶子节点 +- `cr query search-expr [-f ]` - 搜索结构表达式 +- `cr query error` - 查看最近的错误堆栈(仅覆盖 Calcit 语义与运行链路,不覆盖 CSS/DOM/业务值合理性) + +--- + +## 代码修改示例 + +### 添加新函数 + +```bash +# Cirru one liner +cr edit def app.core/multiply -e 'defn multiply (x y) (* x y)' +``` + +### 基本操作 + +```bash +# 添加新函数(命令会提示 Next steps) +cr edit def 'app.core/multiply' -e 'defn multiply (x y) (* x y)' + +# 替换整个定义(推荐用 overwrite,避免依赖根路径替换) +cr edit def 'app.core/multiply' --overwrite -f /tmp/multiply.cirru + +# 更新文档和示例 +cr edit doc 'app.core/multiply' '乘法函数,返回两个数的积' +cr edit add-example 'app.core/multiply' -e 'multiply 5 6' + +# 移动或重构定义 +cr edit mv 'app.core/multiply' 'app.util/multiply-numbers' +``` + +### 修改定义工作流(命令会显示子节点索引和 Next steps) + +```bash +# 1. 搜索定位 +cr query search '' -f 'ns/def' -l + +# 2. 查看节点(输出会显示索引和操作提示) +cr tree show 'ns/def' -p '' + +# 3. 执行替换(会显示 diff 和验证命令) +cr tree replace 'ns/def' -p '' --leaf -e '' + +# 4. 检查结果 +cr query error +# 若改动涉及 CSS / DOM / 浏览器行为,继续做实际渲染验证,不要把 query error 当最终验收 +# 添加命名空间(推荐:先创建空 ns,再逐条 add-import) +cr edit add-ns app.util +cr edit add-import app.util -e 'calcit.core :refer $ echo' + +# 添加导入规则(单条) +cr edit add-import app.main -e 'app.util :refer $ helper' +# 覆盖已有同名 import +cr edit add-import app.main -e 'app.util :refer $ helper util-fn' -o + +# 移除导入规则 +cr edit rm-import app.main app.util + +# 全量替换 imports(单条用 -e,多条用 -f 文件或 -j JSON) +cr edit imports app.main -e 'app.util :refer $ helper' # 单条 +cr edit imports app.main -f my-imports.cirru # 多条(每行一条规则) +cr edit imports app.main -j '[["app.lib",":as","lib"],["app.util",":refer",["helper"]]]' # JSON + +# 更新项目配置 +cr edit config init-fn app.main/main! +``` + +--- + +--- + +## 🔧 实战重构场景 + +以下是开发中最常见的局部修复和重构操作,帮助 Agent 快速找到对应命令。 + +### 提取子表达式为新定义(`edit split-def`) + +**场景:** 函数体内某个嵌套子表达式太复杂,想拆成独立的命名定义。 + +```bash +# 1. 搜索并定位目标子表达式 +cr query search-expr 'complex-call arg1' -f 'app.core/process-data' -l +# 输出示例:[3.2.1] in (let ((x ...)) ...) + +# 2. 提取为新定义(原位置自动替换为新名字 extracted-calc) +cr edit split-def 'app.core/process-data' -p '3.2.1' -n extracted-calc + +# 3. 查看结果 +cr query def 'app.core/extracted-calc' # 新定义 +cr query def 'app.core/process-data' # 原定义(原位已变成 extracted-calc) + +# 4. 如需给新定义加函数签名(用 tree replace 重构根节点) +cr tree replace 'app.core/extracted-calc' -p '' -e 'defn extracted-calc (x) body-expr' +``` + +**注意:**`split-def` 仅创建新定义并替换引用,不会自动在其他 ns 添加 import。对外暴露时记得 `cr edit add-import`。 + +### 重命名定义(`edit rename`) + +**场景:** 定义名字需要在同一命名空间内改名。 + +```bash +# 1. 确认有哪些地方引用到 +cr query usages 'app.core/old-name' + +# 2. 重命名(不允许覆盖已有定义) +cr edit rename 'app.core/old-name' 'new-name' + +# 3. 批量更新所有引用(search 会自动提示批量命令) +cr query search 'old-name' # 找到所有引用位置 +cr tree replace-leaf 'app.core/caller-fn' --pattern 'old-name' -e 'new-name' --leaf +``` + +### 迁移定义到另一命名空间(`edit mv-def`) + +**场景:** 某函数放错了命名空间,需要迁移。 + +```bash +# 移动定义 +cr edit mv-def 'app.core/helper-fn' 'app.util/helper-fn' + +# 在使用方添加 import +cr edit add-import 'app.main' -e 'app.util :refer $ helper-fn' + +# 通知 watcher(热更新场景) +cr edit inc --removed 'app.core/helper-fn' --added 'app.util/helper-fn' +``` + +### 在定义内移动 / 复制 AST 节点(`edit mv` / `edit cp`) + +**场景:** 函数体内某个子表达式需要移到另一位置,或复制用于多处。 + +```bash +# 定位节点 +cr query search-expr 'process item' -f 'app.core/main-fn' -l +# 输出:[3,1,2] + +# 移动(原位置消失) +cr edit mv 'app.core/main-fn' --from '3,1,2' -p '3,2' --at before + +# 复制(原位置保留,新位置多一份) +cr edit cp 'app.core/main-fn' --from '3,1,2' -p '3,2' --at after +``` + +### 包裹 / 拆包 / 提升节点(`tree wrap` / `tree unwrap` / `tree raise`) + +**场景:** 临时包裹一层 `println` 调试、反向拆掉包装层、或用子节点替换掉父节点。 + +```bash +# 包裹(wrap):将节点包进新表达式,self = 原节点 +cr tree wrap 'app.core/main-fn' -p '3,2' -e 'println self' + +# 包裹成 let 绑定(self = 原表达式) +cr tree wrap 'app.core/main-fn' -p '3,2' -e 'let ((result self)) result' + +# 拆包(unwrap):删除该节点,所有子节点展开到原位置 +cr tree unwrap 'app.core/main-fn' -p '3,2' + +# 提升(raise):用该子节点整体替换其父节点 +# 场景:去掉 if 只保留 then 分支,或去掉 let 只保留最终返回值 +cr tree raise 'app.core/main-fn' -p '3,2,1' +``` + +### 批量重命名局部变量(`tree replace-leaf` / `tree target-replace`) + +**场景:** 某函数内某个局部变量名需要统一改掉。 + +```bash +# 若只有一处:内容定位直接替换(最安全 ⭐) +cr tree target-replace 'app.core/process' --pattern 'old-var' -e 'new-var' --leaf + +# 若多处:一次性全部替换 +cr tree replace-leaf 'app.core/process' --pattern 'old-var' -e 'new-var' --leaf +``` + +--- + +## ⚠️ 常见陷阱和最佳实践 + +### 1. 路径索引动态变化问题 ⭐⭐⭐ + +**核心原则:** 删除/插入会改变同级后续节点索引。 + +**批量修改策略:** + +- **从后往前操作**(推荐):先删大索引,再删小索引 +- **单次操作后重新搜索**:每次修改立即用 `cr query search` 更新路径 +- **整体重写**:优先用 `cr edit def --overwrite -f `;`cr tree replace -p ''` 只保留给明确需要根节点级别改写的场景 + +命令会在路径错误时提示最长有效路径和可用子节点。 + +### 1.5 根路径整体替换的边界 ⭐⭐⭐ + +`cr tree replace -p ''` 在语义上确实是替换根节点,但在实际操作里,它更像“根 AST 节点替换”,而不是“整条定义安全重写”。当你需要完整替换一个定义体时: + +- 更推荐 `cr edit def --overwrite -f ` +- 先在文件里组织完整定义,再一次性覆盖,验证也更直接 +- 如果你已经用 `-p ''` 替换成功,仍应立刻执行 `cr query def ` 或完整运行,确认写回后的定义结构符合预期 + +经验上,`-p ''` 更适合你已经非常确定根节点结构时的精细 AST 操作,不适合作为默认“全量改写定义”的模板。 + +### 2. 输入格式参数使用速查 ⭐⭐⭐ + +**参数混淆矩阵(已全面支持 `-e` 自动识别):** + +| 场景 | 示例用法 | 解析结果 | 说明 | +| ------------------- | -------------------------------------- | ----------------------------- | --------------------------------- | +| **表达式 (Cirru)** | `-e 'defn add (a b) (+ a b)'` | `["defn", "add", ...]` (List) | 默认按 Cirru one-liner 解析 | +| **原子符号 (Leaf)** | `--leaf -e 'my-symbol'` | `"my-symbol"` (Leaf) | **推荐**,避免被包装成 list | +| **字符串 (Leaf)** | `--leaf -e '\|hello world'` | `"hello world"` (Leaf) | 符号前缀 `\|` 表示字符串 | +| **JSON 数组** | `-e '["+", "x", "1"]'` | `["+", "x", "1"]` (List) | **自动识别** (含 `[` 且有 `"`) | +| **JSON 字符串** | `-e '"my leaf"'` | `"my leaf"` (Leaf) | **自动识别** (含引用的字符串) | +| **内联 JSON** | `-j '["defn", ...]'` | `["defn", ...]` (List) | 显式按 JSON 解析,忽略 Cirru 规则 | +| **外部文件** | `-f code.cirru` (或 `-f code.json -J`) | 根据文件内容解析 | `-J` 用于标记文件内是 JSON | + +**核心规则:** + +1. **智能识别模式**:`-e / --code` 现在会自动识别 JSON。如果你传入 `["a"]` 或 `"a"`,它会直接按 JSON 处理,无需再额外加 `-J` 或 `-j`。 +2. **强制 Leaf 模式**:如果你需要确保输入是一个叶子节点(符号或字符串),请在任何地方使用 `--leaf` 开关。它会将原始输入直接作为内容,不经过任何解析。 +3. **显式 JSON 模式**:如果你想明确告诉工具“这段就是 JSON”,优先用 `-j ''`。 +4. **统一性**:`cr tree` 和 `cr edit` 的所有子命令(replace, def, insert 等)现在共享完全相同的输入解析逻辑。 + +**实战示例:** + +```bash +# ✅ 替换表达式 +cr tree replace app.main/fn -p '2' -e 'println |hello' + +# ✅ 替换 leaf(推荐 --leaf) +cr tree replace app.main/fn -p '2,0' --leaf -e 'new-symbol' + +# ✅ 替换字符串 leaf +cr tree replace app.main/fn -p '2,1' --leaf -e '|new text' + +# ❌ 避免:用 -e 传单个 token(会变成 list) +cr tree replace app.main/fn -p '2,0' -e 'symbol' # 结果:["symbol"] +``` + +### 3. Cirru 字符串和数据类型 ⭐⭐ + +**Cirru 字符串前缀:** + +| Cirru 写法 | JSON 等价 | 使用场景 | +| -------------- | -------------- | ------------ | +| `\|hello` | `"hello"` | 推荐,简洁 | +| `"hello"` | `"hello"` | 也可以 | +| `\|a b c` | `"a b c"` | 包含空格 | +| `\|[tag] text` | `"[tag] text"` | 包含特殊字符 | + +**不放心修改是否正确?** 每步后用 `tree show` 验证. + +**Tuple vs Vector:** + +```cirru +; ✅ Tuple - 用于事件、模式匹配 +:: :clipboard/read text + +; ✅ Vector - 用于 DOM 列表 +[] (button) (div) + +; ❌ 错误:用 vector 传事件 +send-to-component! $ [] :clipboard/read text +; 报错:tag-match expected tuple + +; ✅ 正确:用 tuple +send-to-component! $ :: :clipboard/read text +``` + +**记忆规则:** + +- **`::` (tuple)**: 事件、模式匹配、不可变数据结构 +- **`[]` (vector)**: DOM 元素列表、动态集合 + +### 4. 输入大小限制 ⭐⭐⭐ + +为了保证稳定性和处理速度,CLI 对单次输入的大小有限制。如果超过限制,系统会提示建议分段提交。 + +- **Cirru One-liner (`-e / --code`)**: 字数上限 **1000**。 +- **JSON 格式 (`-j / --json`, `-J`, `-e`)**: 字数上限 **2000**。 + +**大资源处理建议:** +如果需要修改复杂的长函数,不要尝试一次性替换整个定义。应先构建主体结构,使用占位符,统一写成 `{{PLACEHOLDER_FEATURE}}` 这种花括号形式,并注意避免重复,然后通过 `cr tree target-replace` 或按路径的 `cr tree replace` 做精准的分段替换。 + +补充提示:现在 `cr query def` 和 `cr tree show` 遇到大表达式时会自动输出分片结果。若你采用多阶段创建,建议从第一步就使用 `{{NAME}}` 风格占位符,这样后续在分片视图中更容易识别骨架、复制坐标并继续填充内容。 + +### 5. 命名空间操作陷阱 ⭐⭐⭐ + +**三个命令的 `-e` 期望格式完全不同,是最常见的混淆来源:** + +| 命令 | `-e` 期望内容 | 错误用法 | +| ------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------ | +| `add-ns -e ...` | **完整 `ns` 表达式**:`ns my.ns $ :require ...` | ❌ 传 import 规则(静默成功但 ns 代码损坏) | +| `imports -e ...` | **单条 import 规则**(无 `:require` 前缀):`src-ns :refer $ sym` | ❌ 带 `:require` 前缀(导致 `:require :require` 重复) | +| `add-import -e ...` | **单条 import 规则**(同上):`src-ns :refer $ sym` | 同 imports | + +**具体陷阱:** + +❌ **陷阱1:`add-ns -e` 传了 import 规则而非完整 `ns` 表达式** + +```bash +# ❌ 错误:ns 代码会变成 'respo.core :refer $ defcomp'(缺 ns 关键字!) +cr edit add-ns my.ns -e 'respo.core :refer $ defcomp' + +# ✅ 正确:无代码时先建空 ns,再 add-import +cr edit add-ns my.ns +cr edit add-import my.ns -e 'respo.core :refer $ defcomp' + +# ✅ 也正确:传完整 ns 表达式(名称必须与位置参数一致) +cr edit add-ns my.ns -e 'ns my.ns $ :require respo.core :refer $ defcomp' +``` + +❌ **陷阱2:`imports -e` 带了 `:require` 前缀**(现在会报错) + +```bash +# ❌ 错误:现在会报错 "Do not include ':require' as a prefix" +cr edit imports my.ns -e ':require respo.core :refer $ sym' + +# ✅ 正确:直接传规则,不加 :require +cr edit imports my.ns -e 'respo.core :refer $ sym' +``` + +❌ **陷阱3:`add-ns -e` 中 ns 名称与位置参数不一致**(现在会报错) + +```bash +# ❌ 错误:现在会报错 "Namespace name mismatch" +cr edit add-ns my.ns -e 'ns wrong.ns $ :require ...' +``` + +❌ **陷阱4:想添加多条 imports 时用 `-e` 而非 `-f`** + +```bash +# ❌ 无法在单个 -e 中写多条规则(会合并为一条) +cr edit imports my.ns -e 'respo.core :refer $ div\nrespo.util.format :refer $ hsl' + +# ✅ 多条规则用文件(每行一条规则,无需 :require 前缀) +printf 'respo.core :refer $ div\nrespo.util.format :refer $ hsl\n' > /tmp/imports.cirru +cr edit imports my.ns -f /tmp/imports.cirru + +# ✅ 或用 JSON 格式 +cr edit imports my.ns -j '[["respo.core",":refer",["div"]],["respo.util.format",":refer",["hsl"]]]' + +# ✅ 或逐条 add-import(推荐,更安全) +cr edit add-import my.ns -e 'respo.core :refer $ div' +cr edit add-import my.ns -e 'respo.util.format :refer $ hsl' +``` + +**最佳实践:优先用 `add-import`(更安全,带校验):** + +- `add-import` 会验证 source-ns 格式,有 `--overwrite` 保护 +- `imports` 全量替换,一旦格式错误会覆盖所有 imports +- 只有需要完全重置所有 imports 时才用 `imports` + +### 6. 推荐工作流程 + +**基本流程(search 快速定位 ⭐⭐⭐):** + +```bash +# 1. 快速定位(比逐层导航快10倍) +cr query search 'target' -f 'ns/def' # 或 search-expr 'fn (x)' -l 搜索结构 + +# 2. 执行修改(会显示 diff 和验证命令) +cr tree replace 'ns/def' -p '' --leaf -e '' + +# 3. 增量更新(推荐) +cr edit inc --changed ns/def +# 等待 ~300ms 后检查 +cr query error +``` + +**新手提示:** + +- 不知道目标在哪?用 `search` 或 `search-expr` 快速找到所有匹配 +- 想了解代码结构?用 `tree show` 逐层探索 +- 需要批量重命名?搜索后按提示从大到小路径依次修改 +- 不确定修改是否正确?每步后用 `tree show` 验证 + +### 7. Shell 特殊字符转义 ⭐⭐ + +Calcit 函数名中的 `?`, `->`, `!` 等字符在 bash/zsh 中有特殊含义,需要用单引号包裹: + +```bash +# ❌ 错误 +cr query def app.main/valid? +cr eval '-> x (+ 1) (* 2)' + +# ✅ 正确 +cr query def 'app.main/valid?' +cr eval 'thread-first x (+ 1) (* 2)' # 用 thread-first 代替 -> +``` + +**建议:** 命令行中优先使用英文名称(`thread-first` 而非 `->`),更清晰且无需转义。 + +### 8. 多命令 `&&` 链式调用风险 ⭐⭐⭐ + +把多个 `cr tree replace`、`cr edit def -e ...` 或其他带内联代码的命令用 `&&` 串起来,在 bash/zsh 中风险很高: + +- 只要某一段 `-e` 内容里出现未正确转义的引号,shell 就会进入“继续等待补全输入”的状态,看起来像终端卡死 +- 前一条命令如果已经改写了内容,后一条命令即使没执行,你也可能以为整批操作已完成 + +更稳妥的做法: + +- 批量修改时逐条执行 +- 多行或含引号内容改用 `-f ` +- 需要批量脚本化时,放到独立 shell script,并先用最小样例验证 quoting + +--- + +## 🔄 完整功能开发示例 + +以下展示从零开始添加新函数的完整流程,是最常见的日常开发场景。 + +### 步骤 1:确认目标命名空间和现有代码 + +```bash +# 查看命名空间列表 +cr query ns + +# 查看某个 ns 已有的定义 +cr query defs app.util + +# 快速了解某个定义(不展开完整代码) +cr query peek 'app.util/format-date' + +# 如有疑问,读取完整代码 +cr query def 'app.util/format-date' +``` + +### 步骤 2:用 eval 快速验证写法 + +在真正写入项目前,先用 `cr eval` 验证逻辑思路: + +```bash +# 验证基础函数调用 +cr eval 'string->number |123' + +# 验证带 let 的表达式 +cr eval 'let ((x 10) (y 20)) (+ x y)' + +# 验证列表操作 +cr eval 'let ((xs (list 1 2 3))) (map xs (fn (x) (* x 2)))' + +# 加载项目依赖模块后测试 +cr eval --dep calcit.std 'str/split |hello world | ' +``` + +> 💡 `cr eval` 有类型警告时会失败退出——正好可以提前发现用法错误。 + +### 步骤 3:添加新定义 + +```bash +# 在已有命名空间中添加新函数 +cr edit def 'app.util/calculate-discount' -e 'defn calculate-discount (price rate) (* price (- 1 rate))' + +# 验证定义写入成功 +cr query def 'app.util/calculate-discount' +``` + +### 步骤 4:在调用方添加 import 并使用 + +```bash +# 查看调用方当前 imports +cr query ns app.core + +# 添加 import(首选 add-import,更安全) +cr edit add-import 'app.core' -e 'app.util :refer $ calculate-discount' + +# 在函数体中使用新定义(先定位插入位置) +cr query search 'total-price' -f 'app.core/checkout' +# 输出:[3.2.1] in (let ((total-price ...)) ...) + +# 修改调用 +cr tree replace 'app.core/checkout' -p '3.2.1' -e 'calculate-discount total-price 0.1' +``` + +### 步骤 5:触发热更新并验证 + +```bash +# 推送增量更新(触发 watcher 热加载) +cr edit inc --changed 'app.util/calculate-discount' +cr edit inc --changed 'app.core/checkout' + +# 等待 ~300ms 后检查是否有错误 +cr query error + +# 如无错误,用 --check-only 整体验证 +cr --check-only +``` + +如果这次改动涉及样式、浏览器属性、字符串模板或外部接口,`cr query error` 和 `cr --check-only` 通过后,仍要继续做目标环境里的真实验收。 + +### 常见失误快速修复 + +```bash +# 忘记 import → unknown symbol +cr edit add-import 'app.core' -e 'app.util :refer $ calculate-discount' + +# 定义名拼写错误 → 重命名 +cr edit rename 'app.util/calculte-discount' 'calculate-discount' + +# 函数参数顺序传错 → 定位并修改调用 +cr query search 'calculate-discount' -f 'app.core/checkout' +cr tree replace 'app.core/checkout' -p '3.2.1' --leaf -e 'calculate-discount' +``` + +--- + +## 💡 Calcit vs Clojure 关键差异 + +**语法层面:** + +- **只用圆括号**:Calcit 的 Cirru 语法不使用方括号 `[]` 和花括号 `{}`,统一用缩进表达结构 +- **函数前缀**:Calcit 用 `&` 区分内置函数(`&+`、`&str`)和用户定义函数 + +**集合函数参数顺序(易错 ⭐⭐⭐):** + +- **Calcit**: 集合在**第一位** → `map data fn` 或 `-> data (map fn)` +- **Clojure**: 函数在第一位 → `map fn data` 或 `->> data $ map fn` +- **症状**:`unknown data for foldl-shortcut` 报错 +- **原因**:误用 `->>` 或参数顺序错误 + +**其他差异:** + +- **宏系统**:Calcit 更简洁,缺少 Clojure 的 reader macro(如 `#()`) +- **数据类型**:Calcit 的 Tuple (`::`) 和 Vector (`[]`) 有特定用途(见"Cirru 字符串和数据类型") + +--- + +## 常见错误排查 + +### 快速诊断流程 + +当 watcher 提示有错误或行为异常时,按以下顺序排查: + +```bash +# 1. 查看最新错误堆栈(首选) +cr query error +# 输出示例: +# Error in app.core/process-data +# CalcitErr: unknown symbol: proess-item ← 拼写错误 +# at app.core/render → app.core/process-data → ... + +# 2. 用 --check-only 快速全量验证(不执行程序) +cr --check-only + +# 3. 用 cr eval 隔离验证单个函数写法 +cr eval 'let ((x 1)) (+ x 2)' +``` + +### 错误信息对照表 + +| 错误信息 | 原因 | 解决方法 | +| ------------------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------ | +| `Path index X out of bounds` | 路径索引已过期(操作后变化) | 重新运行 `cr query search` 获取最新路径 | +| `tag-match expected tuple` | 传入 vector 而非 tuple | 改用 `::` 语法,如 `:: :event-name data` | +| `unknown symbol: xxx` | 符号未定义或未 import | `cr query find xxx` 确认位置,`cr edit add-import` 引入 | +| `expects pairs in list for let` | `let` 绑定语法错误 | 改为 `let ((x val)) body`(双层括号) | +| `cannot be used as operator` | 末尾符号被当作函数调用 | 改用 `, acc` 前缀传递值,或用函数包裹 | +| `unknown data for foldl-shortcut` | 参数顺序错误(Calcit vs Clojure 差异) | Calcit 集合在第一位:`map data fn` | +| `Do not include ':require' as prefix` | `cr edit imports` 格式错误 | 去掉 `:require` 前缀,直接传 `src-ns :refer $ sym` | +| `Namespace name mismatch` | `add-ns -e` 名称不一致 | ns 表达式名称必须与位置参数完全一致 | +| 字符串被拆分成多个 token | 没有用 `\|` 或 `"` 包裹 | 使用 `\|complete string` 或 `"complete string` | +| `unexpected format` | Cirru 语法错误 | 用 `cr cirru parse ''` 验证语法 | +| `Type warning` 导致 eval 失败 | 类型不匹配(阻断执行) | 优先检查 `:schema` / `hint-fn` 的参数标注;局部值再用 `assert-type` 复核 | +| `schema mismatch while preprocessing definition` | `:schema` 与 `defn` / `defmacro` / 参数个数不一致 | 修正 `:kind`、`:args`、`:rest`,或让代码定义与 schema 保持一致 | +| `cr query error` 无报错但页面仍异常 | 问题不在 Calcit 语义链路,而在 CSS/DOM/业务值 | 到真实运行环境核对渲染结果、属性值和外部依赖,而不是只看 `query error` | + +### 调试常用命令 + +```bash +# 查看完整错误栈(最详细) +cr query error + +# 检查某个定义的代码和内容 +cr query def 'ns/def' +cr tree show 'ns/def' + +# 验证 Cirru 语法 +cr cirru parse 'defn add (a b) (+ a b)' + +# 快速测试某个想法(不影响项目代码) +cr eval 'range 5' +cr eval 'let ((xs (list 1 2 3))) (map xs number->string)' + +# 检查定义是否存在 +cr query find 'my-function' +cr query defs 'my.namespace' +``` + +> 💡 **错误文件备份**:`.calcit-error.cirru` 会保存最近一次的完整错误堆栈(包含 chain 信息),比 `cr query error` 更完整。直接用 `cat .calcit-error.cirru` 读取,或 `cr query error`(从此文件读取并格式化输出)。 From 3aecbc66f2c727f3051e772a8efe0bf4f29e3565 Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 18 Mar 2026 18:03:33 +0800 Subject: [PATCH 21/57] feat(cli): add tips-level policy and preserve overwrite metadata --- docs/CalcitAgent.md | 22 ++++-- docs/run/agent-advanced.md | 31 ++++---- ...level-and-overwrite-schema-preservation.md | 26 +++++++ src/bin/cli_handlers/edit.rs | 15 +++- src/bin/cli_handlers/mod.rs | 2 +- src/bin/cli_handlers/query.rs | 28 ++++--- src/bin/cli_handlers/tips.rs | 73 +++++++++++++++++-- src/bin/cli_handlers/tree.rs | 12 +-- src/bin/cr.rs | 8 +- src/cli_args.rs | 10 +-- 10 files changed, 169 insertions(+), 58 deletions(-) create mode 100644 editing-history/2026-0318-1802-tips-level-and-overwrite-schema-preservation.md diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index f229450a..f0a3d140 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -97,10 +97,11 @@ cr docs agents --full - 从大索引往前改,或 - 每次修改后重新 `query search` 避免路径漂移。 - Tips 需要但应可控: - - 默认最多一条(快速扫读) - - 支持“全部/静默”模式切换(建议使用 `--tips-level` 统一控制) + - 默认只在高优先级场景展示最多一条(快速扫读) + - 需要全部提示时主动加 `--tips` + - 需要精细控制时使用 `--tips-level` -> 说明:当前 CLI 已支持 `--no-tips`。`--tips-level` 作为统一分级开关建议保留在后续实现中。 +> 说明:默认不加参数即 `minimal`(仅高优先级提示,最多 1 条);`--tips` 等价于 `full`。也支持显式 `--tips-level minimal|full|none`。 --- @@ -204,7 +205,9 @@ cr tree rewrite app.main/demo -p '5.2' --with self=. -e '-> self normalize emit' - 先 `query def` 看大轮廓,再 `search` + `tree show` 看局部。 - 搜索结果过多时,不要连续盲改路径;每次改后重搜一次更稳。 - 复杂多行表达式优先 `-f `,减少 shell 转义错误。 -- 若需要最安静输出,可使用 `--no-tips`。 +- 默认模式通常不显示 tips;仅在高优先级场景显示 1 条。 +- 若要看全部提示请加 `--tips`。 +- 若要完全静默可用 `--tips-level none`。 ### `Invalid path` 快速恢复模板(固定 3 步) @@ -223,14 +226,17 @@ cr tree rewrite app.main/demo -p '5.2' --with self=. -e '-> self normalize emit' ```bash # 1) 先轻看,避免大段输出 -cr --no-tips query peek +cr query peek # 2) 必要时才看完整定义(默认 Cirru) -cr --no-tips query def +cr query def # 3) 用 search 定位后再 show 局部 -cr --no-tips query search '' -f -cr --no-tips tree show -p '' +cr query search '' -f +cr tree show -p '' + +# 4) 需要完整提示再打开 +cr --tips query def ``` 仅在需要程序化处理时再加 `-j`,否则保持 Cirru 输出即可。 diff --git a/docs/run/agent-advanced.md b/docs/run/agent-advanced.md index ed56917e..43765c14 100644 --- a/docs/run/agent-advanced.md +++ b/docs/run/agent-advanced.md @@ -37,9 +37,9 @@ cr tree replace-leaf 'ns/def' --pattern 'old' -e 'new' --leaf # 批量替换叶 --- -## Tips 输出分级(设计草案) +## Tips 输出分级(已实现) -当前已有 `--no-tips`,但在 Agent 场景下建议补充统一分级参数:`--tips-level`。 +当前 CLI 已支持统一分级参数:`--tips-level`。 ### 目标 @@ -52,19 +52,19 @@ cr tree replace-leaf 'ns/def' --pattern 'old' -e 'new' --leaf # 批量替换叶 - 每次命令最多输出 1 条 tips(优先“下一步动作”)。 - `--tips-level full` - 输出全部 tips(教学/排障模式)。 + - 等价快捷参数:`--tips` - `--tips-level none` - - 等价 `--no-tips`(脚本/Agent 静默模式)。 + - 关闭 tips(脚本/Agent 静默模式)。 -### 兼容建议 +### 使用建议 -- 继续保留 `--no-tips`,内部映射到 `--tips-level none`。 - 文档示例默认使用 `minimal` 心智模型,进阶示例再展示 `full/none`。 -### 迁移建议 +### 落地说明 1. 先在 query/tree 相关子命令接入统一解析。 2. 统一 Tips 渲染入口,避免各 handler 自行拼装。 -3. 补充回归:默认输出条数、`--no-tips` 等价行为、`full` 全量展示。 +3. 补充回归:默认输出条数、`full` 全量展示、`none` 静默行为。 --- @@ -119,8 +119,8 @@ Calcit 程序使用 `cr` 命令: - 用于 CI/CD 或快速验证代码修改 - `cr js -1` - 检查代码正确性,生成 JavaScript(兼容参数,默认已是单次) - `cr js --check-only` - 检查代码正确性,不生成 JavaScript -- `cr --no-tips ...` - 隐藏所有编辑/查询命令输出的 "Tips:" 提示行(适合脚本/Agent 使用) - - 示例:`cr --no-tips demos/compact.cirru query def calcit.core/foldl` +- `cr --tips ...` - 主动显示完整 tips(教学/排障时) + - 示例:`cr --tips demos/compact.cirru query def calcit.core/foldl` - `cr eval '' [--dep ...]` - 执行一段 Calcit 代码片段,用于快速验证写法 - **不需要**项目 `compact.cirru`:core 内置函数(`range`、`+`、`map` 等)直接可用 - 项目自定义函数不可直接 eval(代码未加载),需用 `--dep` 加载外部模块 @@ -164,7 +164,7 @@ Calcit 程序使用 `cr` 命令: - 默认输出:Doc、Examples 数量、Cirru 格式代码 - `-j` / `--json`:同时输出 JSON 格式(用于程序化处理) - 推荐:LLM 直接读取 Cirru 格式即可,通常不需要 JSON -- `cr query schema [-j] [--no-tips]` - 读取定义当前的 schema +- `cr query schema [-j]` - 读取定义当前的 schema - 默认输出:Definition 标识 + schema 的 Cirru one-liner 预览 - `-j` / `--json`:输出 schema 对应的 Cirru EDN 结构;无 schema 时输出 `nil` - 适合在修改前确认 `:args` / `:return` / `:rest` 当前值 @@ -494,14 +494,17 @@ cr tree replace namespace/def -p '3.2.2.5.2.4.1.2' -e 'let ((x 1)) (+ x task)' ```bash cr tree show ns/def -p '4.0' - # 输出显示 "{{BODY}}" 在索引 2,即路径 [4.0.2] ``` +# 输出显示 "{{BODY}}" 在索引 2,即路径 [4.0.2] + +```` + 3. **填充内容**:针对占位符路径进行下一层的精细替换。 - ```bash - cr tree replace ns/def -p '4.0.2' -j '["if", ["=", "x", "1"], "{{TRUE_BRANCH}}", "{{FALSE_BRANCH}}"]' - ``` +```bash +cr tree replace ns/def -p '4.0.2' -j '["if", ["=", "x", "1"], "{{TRUE_BRANCH}}", "{{FALSE_BRANCH}}"]' +```` 4. **递归迭代**:重复上述步骤直到所有占位符(如 `{{TRUE_BRANCH}}`、`{{FALSE_BRANCH}}`)都被替换为最终逻辑。 diff --git a/editing-history/2026-0318-1802-tips-level-and-overwrite-schema-preservation.md b/editing-history/2026-0318-1802-tips-level-and-overwrite-schema-preservation.md new file mode 100644 index 00000000..ff156b15 --- /dev/null +++ b/editing-history/2026-0318-1802-tips-level-and-overwrite-schema-preservation.md @@ -0,0 +1,26 @@ +## Summary + +This commit finalizes two CLI workflow improvements: + +1. **Tips policy refactor** + - Added `--tips-level` support (`minimal|full|none`) as a unified switch. + - Kept `--tips` as a shortcut for full tips output. + - Updated handlers so tips rendering is centralized and priority-aware. + - Updated docs to align with the new default behavior (minimal, high-priority-first hints). + +2. **`cr edit def --overwrite` metadata safety** + - Fixed overwrite behavior to preserve existing definition metadata (`doc`, `examples`, `schema`). + - Overwrite now updates only the `code` field when the definition already exists. + - This avoids accidental schema loss during whole-definition rewrites. + +## Files touched (high level) + +- CLI args and command wiring for tips level handling. +- Query/tree/tips handler integration and output behavior updates. +- `edit` handler overwrite logic for metadata-preserving updates. +- Agent docs and advanced workflow docs reflecting the new tips model. + +## Validation notes + +- Rust formatting/lint/tests were run during implementation iterations. +- Manual end-to-end verification confirmed schema is retained after `--overwrite`. diff --git a/src/bin/cli_handlers/edit.rs b/src/bin/cli_handlers/edit.rs index c8f11b85..186dfd8e 100644 --- a/src/bin/cli_handlers/edit.rs +++ b/src/bin/cli_handlers/edit.rs @@ -163,8 +163,19 @@ fn handle_def(opts: &EditDefCommand, snapshot_file: &str) -> Result<(), String> )); } - // Create definition - let code_entry = CodeEntry::from_code(syntax_tree); + // Create or overwrite definition. + // For overwrite, preserve existing metadata (doc/examples/schema) and only replace code. + let code_entry = if exists { + if let Some(previous_entry) = file_data.defs.get(definition).cloned() { + let mut updated_entry = previous_entry; + updated_entry.code = syntax_tree; + updated_entry + } else { + CodeEntry::from_code(syntax_tree) + } + } else { + CodeEntry::from_code(syntax_tree) + }; file_data.defs.insert(definition.to_string(), code_entry); save_snapshot(&snapshot, snapshot_file)?; diff --git a/src/bin/cli_handlers/mod.rs b/src/bin/cli_handlers/mod.rs index 94cc1e62..dec653e6 100644 --- a/src/bin/cli_handlers/mod.rs +++ b/src/bin/cli_handlers/mod.rs @@ -19,6 +19,6 @@ pub use docs::handle_docs_command; pub use edit::handle_edit_command; pub use libs::handle_libs_command; pub use query::handle_query_command; -pub use tips::suppress_tips; +pub use tips::set_tips_level; pub use tree::handle_tree_command; // Re-export when needed by other modules; keep internal for now to avoid unused-import warnings diff --git a/src/bin/cli_handlers/query.rs b/src/bin/cli_handlers/query.rs index 11fdc37f..e8bf35c5 100644 --- a/src/bin/cli_handlers/query.rs +++ b/src/bin/cli_handlers/query.rs @@ -83,7 +83,7 @@ pub fn handle_query_command(cmd: &QueryCommand, input_path: &str) -> Result<(), ), QuerySubcommand::Schema(opts) => { let (ns, def) = parse_target(&opts.target)?; - handle_schema(input_path, ns, def, opts.json, opts.no_tips) + handle_schema(input_path, ns, def, opts.json) } } } @@ -691,19 +691,19 @@ fn handle_peek(input_path: &str, namespace: &str, definition: &str) -> Result<() println!("{} -", "Schema:".bold()); } - // Tips - show relevant next steps - println!("\n{}", "Tips:".bold()); - println!(" {} cr query def {}/{}", "-".dimmed(), namespace, definition); - println!(" {} cr query examples {}/{}", "-".dimmed(), namespace, definition); - println!(" {} cr query usages {}/{}", "-".dimmed(), namespace, definition); - println!(" {} cr query schema {}/{}", "-".dimmed(), namespace, definition); - println!(" {} cr edit doc {}/{} ''", "-".dimmed(), namespace, definition); + let mut tips = Tips::new(); + tips.add(format!("cr query def {namespace}/{definition}")); + tips.add(format!("cr query examples {namespace}/{definition}")); + tips.add(format!("cr query usages {namespace}/{definition}")); + tips.add(format!("cr query schema {namespace}/{definition}")); + tips.add(format!("cr edit doc {namespace}/{definition} ''")); + tips.print(); Ok(()) } /// Show definition schema -fn handle_schema(input_path: &str, namespace: &str, definition: &str, json: bool, no_tips: bool) -> Result<(), String> { +fn handle_schema(input_path: &str, namespace: &str, definition: &str, json: bool) -> Result<(), String> { let snapshot = load_snapshot(input_path)?; let file_data = snapshot @@ -735,12 +735,10 @@ fn handle_schema(input_path: &str, namespace: &str, definition: &str, json: bool println!("{} -", "Schema:".bold()); } - if !no_tips { - // Tips - println!("\n{}", "Tips:".bold()); - println!(" {} cr query peek {}/{}", "-".dimmed(), namespace, definition); - println!(" {} cr edit schema {}/{} -e '{{}} ...'", "-".dimmed(), namespace, definition); - } + let mut tips = Tips::new(); + tips.add(format!("cr query peek {namespace}/{definition}")); + tips.add(format!("cr edit schema {namespace}/{definition} -e '{{}} ...'")); + tips.print(); Ok(()) } diff --git a/src/bin/cli_handlers/tips.rs b/src/bin/cli_handlers/tips.rs index 52439781..ce7aa6f7 100644 --- a/src/bin/cli_handlers/tips.rs +++ b/src/bin/cli_handlers/tips.rs @@ -2,15 +2,60 @@ use colored::Colorize; use std::sync::atomic::{AtomicBool, Ordering}; static TIPS_SUPPRESSED: AtomicBool = AtomicBool::new(false); +static TIPS_FULL: AtomicBool = AtomicBool::new(false); -/// Suppress all tips output globally (set once at startup via --no-tips) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TipsLevel { + Minimal, + Full, + None, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum TipPriority { + Normal, + High, +} + +impl TipsLevel { + fn parse(raw: &str) -> Option { + match raw { + "minimal" => Some(Self::Minimal), + "full" => Some(Self::Full), + "none" => Some(Self::None), + _ => None, + } + } +} + +/// Suppress all tips output globally pub fn suppress_tips() { TIPS_SUPPRESSED.store(true, Ordering::Relaxed); + TIPS_FULL.store(false, Ordering::Relaxed); +} + +pub fn set_tips_level(raw: &str) -> Result<(), String> { + let level = + TipsLevel::parse(raw).ok_or_else(|| format!("Invalid --tips-level value '{raw}'. Expected one of: minimal, full, none"))?; + + match level { + TipsLevel::Minimal => { + TIPS_SUPPRESSED.store(false, Ordering::Relaxed); + TIPS_FULL.store(false, Ordering::Relaxed); + } + TipsLevel::Full => { + TIPS_SUPPRESSED.store(false, Ordering::Relaxed); + TIPS_FULL.store(true, Ordering::Relaxed); + } + TipsLevel::None => suppress_tips(), + } + + Ok(()) } /// Simple helper to collect and print tips consistently across commands pub struct Tips { - items: Vec, + items: Vec<(TipPriority, String)>, } impl Tips { @@ -19,7 +64,11 @@ impl Tips { } pub fn add(&mut self, tip: impl Into) { - self.items.push(tip.into()); + self.items.push((TipPriority::Normal, tip.into())); + } + + pub fn add_with_priority(&mut self, priority: TipPriority, tip: impl Into) { + self.items.push((priority, tip.into())); } #[allow(dead_code)] @@ -31,7 +80,7 @@ impl Tips { pub fn append(&mut self, tips: Vec) { for t in tips { - self.items.push(t); + self.items.push((TipPriority::Normal, t)); } } @@ -40,7 +89,21 @@ impl Tips { if self.items.is_empty() || TIPS_SUPPRESSED.load(Ordering::Relaxed) { return; } - println!("{}: {}", "Tips".blue().bold(), self.items.join("; ")); + + if TIPS_FULL.load(Ordering::Relaxed) { + let all = self + .items + .iter() + .map(|(_, message)| message.as_str()) + .collect::>() + .join("; "); + println!("{}: {}", "Tips".blue().bold(), all); + return; + } + + if let Some((_, message)) = self.items.iter().find(|(priority, _)| *priority == TipPriority::High) { + println!("{}: {}", "Tips".blue().bold(), message); + } } } diff --git a/src/bin/cli_handlers/tree.rs b/src/bin/cli_handlers/tree.rs index 389c0a4e..88799bc0 100644 --- a/src/bin/cli_handlers/tree.rs +++ b/src/bin/cli_handlers/tree.rs @@ -5,7 +5,7 @@ use super::chunk_display::{ChunkDisplayOptions, ChunkedDisplay, maybe_chunk_node use super::common::{ ERR_CODE_INPUT_REQUIRED, cirru_to_json, format_path, format_path_bracketed, parse_input_to_cirru, parse_path, read_code_input, }; -use super::tips::{Tips, tip_prefer_oneliner_json, tip_root_edit}; +use super::tips::{TipPriority, Tips, tip_prefer_oneliner_json, tip_root_edit}; use crate::cli_args::{ TreeAppendChildCommand, TreeCommand, TreeDeleteCommand, TreeInsertAfterCommand, TreeInsertBeforeCommand, TreeInsertChildCommand, TreeRaiseCommand, TreeReplaceCommand, TreeReplaceLeafCommand, TreeShowCommand, TreeStructuralCommand, TreeSubcommand, @@ -471,7 +471,7 @@ fn handle_replace(opts: &TreeReplaceCommand, snapshot_file: &str) -> Result<(), // Tips: root-edit guidance if let Some(t) = tip_root_edit(path.is_empty()) { let mut tips = Tips::new(); - tips.add(t); + tips.add_with_priority(TipPriority::High, t); tips.print(); } @@ -547,7 +547,7 @@ fn handle_rewrite(opts: &TreeStructuralCommand, snapshot_file: &str) -> Result<( // Tips: root-edit guidance if let Some(t) = tip_root_edit(path.is_empty()) { let mut tips = Tips::new(); - tips.add(t); + tips.add_with_priority(TipPriority::High, t); tips.print(); } @@ -847,7 +847,7 @@ fn handle_delete(opts: &TreeDeleteCommand, snapshot_file: &str) -> Result<(), St println!(); if let Some(t) = tip_root_edit(path.is_empty()) { let mut tips = Tips::new(); - tips.add(t); + tips.add_with_priority(TipPriority::High, t); tips.print(); } @@ -1101,7 +1101,7 @@ fn generic_insert_handler( println!(); if let Some(t) = tip_root_edit(path.is_empty()) { let mut tips = Tips::new(); - tips.add(t); + tips.add_with_priority(TipPriority::High, t); tips.print(); } @@ -1269,7 +1269,7 @@ fn generic_swap_handler(target: &str, path_str: &str, operation: &str, snapshot_ println!(); if let Some(t) = tip_root_edit(path.is_empty()) { let mut tips = Tips::new(); - tips.add(t); + tips.add_with_priority(TipPriority::High, t); tips.print(); } println!("{}:", "Parent after swap".green().bold()); diff --git a/src/bin/cr.rs b/src/bin/cr.rs index dd8b10e6..32856f34 100644 --- a/src/bin/cr.rs +++ b/src/bin/cr.rs @@ -42,8 +42,12 @@ fn main() -> Result<(), String> { let cli_args: ToplevelCalcit = argh::from_env(); - if cli_args.no_tips { - cli_handlers::suppress_tips(); + if let Some(level) = cli_args.tips_level.as_deref() { + cli_handlers::set_tips_level(level)?; + } + + if cli_args.tips { + cli_handlers::set_tips_level("full")?; } // Handle standalone commands that don't need full program loading diff --git a/src/cli_args.rs b/src/cli_args.rs index 8c546de0..1d7a47e5 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -52,9 +52,12 @@ pub struct ToplevelCalcit { /// print version only #[argh(switch)] pub version: bool, - /// suppress tips output in all commands + /// show full tips output in all commands #[argh(switch)] - pub no_tips: bool, + pub tips: bool, + /// control tips verbosity: minimal (default), full, none + #[argh(option)] + pub tips_level: Option, } #[derive(FromArgs, PartialEq, Debug, Clone)] @@ -273,9 +276,6 @@ pub struct QuerySchemaCommand { /// also output JSON format for programmatic consumption #[argh(switch, short = 'j')] pub json: bool, - /// do not display helpful usage tips - #[argh(switch)] - pub no_tips: bool, } #[derive(FromArgs, PartialEq, Debug, Clone)] From 631e89e9308554fabfb7cf8efbd3d12c0a01b110 Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 18 Mar 2026 20:01:51 +0800 Subject: [PATCH 22/57] feat(caps): add outdated --yes and update upgrade docs --- docs/run.md | 1 + docs/run/load-deps.md | 6 ++ docs/run/upgrade.md | 139 +++++++++++++++++++++++++++++++++++++++++ src/bin/calcit_deps.rs | 18 ++++-- 4 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 docs/run/upgrade.md diff --git a/docs/run.md b/docs/run.md index 55709f95..a77246c1 100644 --- a/docs/run.md +++ b/docs/run.md @@ -40,6 +40,7 @@ cr ir - [Hot Swapping](./run/hot-swapping.md) - [Bundle Mode](./run/bundle-mode.md) - [Entries](./run/entries.md) +- [Project Upgrade Playbook](./run/upgrade.md) ## Quick find by keyword diff --git a/docs/run/load-deps.md b/docs/run/load-deps.md index 5327cc2c..27ee8bae 100644 --- a/docs/run/load-deps.md +++ b/docs/run/load-deps.md @@ -33,6 +33,12 @@ To check outdated modules, run: caps outdated ``` +To update `deps.cirru` directly without confirmation: + +```bash +caps outdated --yes +``` + ### CLI Options ``` diff --git a/docs/run/upgrade.md b/docs/run/upgrade.md new file mode 100644 index 00000000..ae141233 --- /dev/null +++ b/docs/run/upgrade.md @@ -0,0 +1,139 @@ +# Calcit 项目升级手册(Respo / Lilac) + +本手册只关注**项目升级流程**,不展开开发实现细节。 + +适用对象:通过 Calcit CLI 运行并产出 JS 的项目(例如 Respo)。 + +--- + +## 1)升级前检查位置 + +升级前先检查以下文件与配置是否齐全: + +- 运行入口与快照:`compact.cirru` + - `:configs`(默认入口) + - `:entries`(额外入口) +- 命令入口:`README`、项目脚本、CI workflow +- Node 工具链:`package.json`、`yarn.lock`、Corepack/Yarn 版本 + +--- + +## 2)标准升级流程(建议顺序) + +下面流程按“先确认版本,再更新依赖,再验证命令链”的顺序执行。 + +### Step A:确认 Calcit CLI 版本 + +```bash +cr --version +``` + +说明:一般本机已经是较新版本,但升级前先确认一遍,避免后续误判。 + +### Step B:检查项目内 Calcit 版本对齐 + +重点检查两处是否一致、是否为目标版本: + +- `deps.cirru` 里的 `:calcit-version` +- `package.json` 里的 `@calcit/procs` + +必要时同步更新这两处,避免运行时和 JS 依赖版本错位。 + +### Step C:检查并更新依赖 + +```bash +caps outdated +caps outdated --yes +``` + +说明: + +- `caps outdated`:查看可更新项; +- `caps outdated --yes`:直接更新 `deps.cirru`(无交互确认)。 + +若依赖是固定 tag/version,仍需先改 `deps.cirru` 再执行更新。 + +### Step D:用 Yarn Berry 安装并校验 + +```bash +corepack enable +corepack prepare yarn@4.12.0 --activate +yarn --version +yarn install --immutable +``` + +说明:团队若习惯 Yarn Berry,建议固定 `packageManager` 并使用 `--immutable` 做一致性校验。 + +### Step E:从 CI workflow 提取检查命令并本地先跑 + +先看 `.github/workflows/` 里实际执行了哪些命令,然后按同顺序在本地跑一遍。 + +常见链路例如: + +```bash +caps --ci && yarn install --immutable +cr --entry +cr --entry js +cr js && yarn vite build --base=./ +``` + +### Step F:执行 package.json 里的编译相关脚本 + +如果 `package.json` 里有与编译、构建、测试相关的脚本,也应本地执行一遍,确认升级后仍可用。 + +例如: + +```bash +yarn +``` + +目标:把 CI 会跑的命令和项目脚本都在本地提前验证,减少合并后失败概率。 + +--- + +## 3)Yarn Berry 升级检查 + +### 3.1 packageManager 固定 + +```json +{ + "packageManager": "yarn@4.12.0" +} +``` + +### 3.2 CI 基础模板(GitHub Actions) + +```yaml +- uses: actions/setup-node@v6 + with: + node-version: 24 + +- name: Enable Corepack + run: | + corepack enable + corepack prepare yarn@4.12.0 --activate + yarn --version + +- name: Install deps + run: yarn install --immutable +``` + +### 3.3 lockfile 迁移 + +如果 `yarn install --immutable` 因 lockfile 格式变化失败: + +1. 先执行一次 `yarn install` 生成新格式 lockfile; +2. 再执行 `yarn install --immutable` 做严格校验。 + +--- + +## 4)升级后最小验证矩阵 + +建议至少覆盖以下 6 项: + +1. `cr --version` +2. `caps --ci outdated`(确认无遗漏项或已按预期处理) +3. `yarn install --immutable` +4. `cr js`(如果是 js 项目) +5. CI 中的入口/测试命令(`--entry` 或 `--init-fn` 链路) +6. `package.json` 中与编译/构建相关脚本 diff --git a/src/bin/calcit_deps.rs b/src/bin/calcit_deps.rs index ac7993a9..a7b73beb 100644 --- a/src/bin/calcit_deps.rs +++ b/src/bin/calcit_deps.rs @@ -95,8 +95,8 @@ pub fn main() -> Result<(), String> { } match &cli_args.subcommand { - Some(SubCommand::Outdated(_)) => { - let updated = outdated_tags(deps.dependencies, &cli_args.input)?; + Some(SubCommand::Outdated(opts)) => { + let updated = outdated_tags(deps.dependencies, &cli_args.input, opts.yes)?; if updated { // Re-read deps.cirru and download updated dependencies println!("\nDownloading updated dependencies..."); @@ -329,7 +329,11 @@ enum SubCommand { #[derive(FromArgs, PartialEq, Debug, Clone)] /// show outdated versions #[argh(subcommand, name = "outdated")] -struct OutdatedCaps {} +struct OutdatedCaps { + /// update deps.cirru directly without interactive confirmation + #[argh(switch, short = 'y', long = "yes")] + yes: bool, +} #[derive(FromArgs, PartialEq, Debug, Clone)] /// download named packages with org/repo@branch @@ -456,7 +460,7 @@ fn call_build_script(folder_path: &Path) -> Result { /// also git fetch to read latest tag from remote, /// then we can compare, get outdated version printed /// Returns true if deps.cirru was updated -fn outdated_tags(deps: HashMap, Arc>, deps_file: &str) -> Result { +fn outdated_tags(deps: HashMap, Arc>, deps_file: &str, auto_yes: bool) -> Result { print_column("package".dimmed(), "expected".dimmed(), "latest".dimmed(), "hint".dimmed()); println!(); @@ -486,6 +490,12 @@ fn outdated_tags(deps: HashMap, Arc>, deps_file: &str) -> Result Date: Thu, 19 Mar 2026 00:42:34 +0800 Subject: [PATCH 23/57] fix outdated docs on search command; tag 0.12.10 --- Cargo.lock | 2 +- Cargo.toml | 2 +- docs/run/agent-advanced.md | 36 ++++++++++++++++++++---------------- package.json | 2 +- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 175c4c38..693d9969 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.9" +version = "0.12.10" dependencies = [ "argh", "bisection_key", diff --git a/Cargo.toml b/Cargo.toml index c642d4d7..d17b9e2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.9" +version = "0.12.10" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/docs/run/agent-advanced.md b/docs/run/agent-advanced.md index 43765c14..79bcdc2c 100644 --- a/docs/run/agent-advanced.md +++ b/docs/run/agent-advanced.md @@ -23,7 +23,7 @@ cr tree show 'ns/def' -p '3.2.1' # 3. 验证(可选) ```bash cr query search 'target' -f 'ns/def' # 搜索符号/字符串 -cr query search-expr 'fn (x)' -f 'ns/def' -l # 搜索代码结构 +cr query search-expr 'fn (x)' -f 'ns/def' # 搜索代码结构 cr tree replace-leaf 'ns/def' --pattern 'old' -e 'new' --leaf # 批量替换叶子节点 ``` @@ -185,31 +185,35 @@ Calcit 程序使用 `cr` 命令: **代码模式搜索(快速定位 ⭐⭐⭐):** -- `cr query search [-f ] [-l]` - 搜索叶子节点(符号/字符串),比逐层导航快 10 倍 +- `cr query search [-f ] [--exact]` - 搜索叶子节点(符号/字符串),比逐层导航快 10 倍 - **搜索范围**:默认包含项目代码、全部 modules 依赖和 calcit.core 内置函数(无需 `--deps` 标志) - `--entry `:额外加载 `entries..modules` 里的依赖(用于 entry 级依赖场景) - `-f ` - 过滤到特定命名空间或定义(可缩小范围提升速度) - - `-l / --loose`:宽松匹配,包含模式 + - 默认即为 contains / fuzzy 匹配 + - `--exact`:仅匹配完全相等的叶子节点 - `-d `:限制搜索深度 - `-p `:从指定路径开始搜索(如 `"3.2.1"`,也兼容 `"3,2,1"`) - 返回:完整路径 + 父级上下文,多个匹配时自动显示批量替换命令 - 示例: - - `cr query search 'println' -f app.main/main!` - 精确搜索(过滤到某定义) - - `cr query search 'comp-' -f app.ui/layout -l` - 模糊搜索(所有 comp- 开头) + - `cr query search 'println' -f app.main/main!` - 默认 contains 匹配(过滤到某定义) + - `cr query search --exact 'println' -f app.main/main!` - 精确搜索 + - `cr query search 'comp-' -f app.ui/layout` - 模糊搜索(所有 comp- 开头) - `cr query search 'task-id'` - 全项目搜索(含 modules) **高级结构搜索(搜索代码结构 ⭐⭐⭐):** -- `cr query search-expr [-f ] [-l] [-j]` - 搜索结构表达式(List) +- `cr query search-expr [-f ] [--exact] [-j]` - 搜索结构表达式(List) - **搜索范围**:同 `search`,默认包含全部依赖和 calcit.core - `--entry `:同上,额外加载指定 entry 的 modules - - `-l / --loose`:宽松匹配,从头部开始的前缀匹配(嵌套表达式也支持前缀) + - 默认即为 prefix / contains 匹配(嵌套表达式也支持前缀) + - `--exact`:仅匹配结构完全一致的表达式 - `-j / --json`:将模式解析为 JSON 数组 - 示例: - - `cr query search-expr 'fn (x)' -f app.main/process -l` - 查找函数定义 - - `cr query search-expr '>> state task-id' -l` - 查找状态访问(匹配 `>> state task-id ...` 或 `>> state`) - - `cr query search-expr 'dispatch! (:: :states)' -l` - 匹配 `dispatch! (:: :states data)` 类型的表达式 - - `cr query search-expr 'memof1-call-by' -l` - 查找记忆化调用 + - `cr query search-expr 'fn (x)' -f app.main/process` - 查找函数定义 + - `cr query search-expr --exact 'fn (x)' -f app.main/process` - 仅匹配结构完全一致的表达式 + - `cr query search-expr '>> state task-id'` - 查找状态访问(匹配 `>> state task-id ...` 或 `>> state`) + - `cr query search-expr 'dispatch! (:: :states)'` - 匹配 `dispatch! (:: :states data)` 类型的表达式 + - `cr query search-expr 'memof1-call-by'` - 查找记忆化调用 **搜索结果格式:** `[索引1.索引2...] in 父级上下文`,可配合 `cr tree show -p ''` 查看节点。逗号路径仍兼容,但文档与输出优先使用点号。**修改代码时优先用 search 命令,比逐层导航快 10 倍。** @@ -400,7 +404,7 @@ cr tree target-replace namespace/def --pattern 'old-symbol' -e 'new-symbol' --le # ===== 方案 D:结构搜索(查找表达式) ===== # 1. 搜索包含特定模式的表达式 -cr query search-expr "fn (task)" -f namespace/def -l +cr query search-expr "fn (task)" -f namespace/def # 输出:[3.2.2.5.2.4.1] in (map $ fn (task) ...) # 2. 查看完整结构(可选) @@ -1028,7 +1032,7 @@ cr edit mv 'app.core/multiply' 'app.util/multiply-numbers' ```bash # 1. 搜索定位 -cr query search '' -f 'ns/def' -l +cr query search '' -f 'ns/def' # 2. 查看节点(输出会显示索引和操作提示) cr tree show 'ns/def' -p '' @@ -1074,7 +1078,7 @@ cr edit config init-fn app.main/main! ```bash # 1. 搜索并定位目标子表达式 -cr query search-expr 'complex-call arg1' -f 'app.core/process-data' -l +cr query search-expr 'complex-call arg1' -f 'app.core/process-data' # 输出示例:[3.2.1] in (let ((x ...)) ...) # 2. 提取为新定义(原位置自动替换为新名字 extracted-calc) @@ -1127,7 +1131,7 @@ cr edit inc --removed 'app.core/helper-fn' --added 'app.util/helper-fn' ```bash # 定位节点 -cr query search-expr 'process item' -f 'app.core/main-fn' -l +cr query search-expr 'process item' -f 'app.core/main-fn' # 输出:[3,1,2] # 移动(原位置消失) @@ -1351,7 +1355,7 @@ cr edit add-import my.ns -e 'respo.util.format :refer $ hsl' ```bash # 1. 快速定位(比逐层导航快10倍) -cr query search 'target' -f 'ns/def' # 或 search-expr 'fn (x)' -l 搜索结构 +cr query search 'target' -f 'ns/def' # 或 search-expr 'fn (x)' 搜索结构 # 2. 执行修改(会显示 diff 和验证命令) cr tree replace 'ns/def' -p '' --leaf -e '' diff --git a/package.json b/package.json index 168f1cb4..e47187d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.9", + "version": "0.12.10", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", From 74b88aa5dff4193714bca1c066cded9491533d59 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 19 Mar 2026 09:48:47 +0800 Subject: [PATCH 24/57] fix unexpected cyclic link; add cardo excludes --- Cargo.toml | 2 +- docs/docs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 120000 docs/docs diff --git a/Cargo.toml b/Cargo.toml index d17b9e2d..3c5e3856 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ homepage = "http://calcit-lang.org" documentation = "https://docs.rs/crate/calcit/" repository = "https://github.com/calcit-lang/calcit.rs" readme = "README.md" -exclude = ["lib/*", "calcit/*", "js-out/*", "scripts/*"] +exclude = ["lib/*", "calcit/*", "ts-src/*", "js-out/*", "scripts/*", "docs/*", "editing-history/*", "drafts/*"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/docs/docs b/docs/docs deleted file mode 120000 index 5c457d79..00000000 --- a/docs/docs +++ /dev/null @@ -1 +0,0 @@ -docs \ No newline at end of file From bef67c9225aa1ab2fae65f9b4cdd701e2c5d203a Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 19 Mar 2026 15:30:46 +0800 Subject: [PATCH 25/57] docs(agent): clarify doc roles and improve query navigation --- docs/CalcitAgent.md | 98 +++++++++++++++++++++++++++++++++++--- docs/run/agent-advanced.md | 2 + 2 files changed, 94 insertions(+), 6 deletions(-) diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index f0a3d140..3d740cfc 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -2,6 +2,18 @@ 本文档面向 Agent/LLM 的高频工作流,目标是**更快定位、最小改动、低噪音验证**。 +本文定位为“查询与局部编辑速查表”:聚焦高频命令、路径定位和最小改动模板。执行前置约束与完整边界规则以 Agents 文档为准。 + +### 查询导航(先用这个) + +- 看某个定义的大致结构:`cr query peek ` +- 看某个定义的完整实现:`cr query def ` +- 找关键词并拿可编辑路径:`cr query search -f ` +- 查进阶手册某个主题:`cr docs read agent-advanced.md ` +- 看进阶手册全文:`cr docs read agent-advanced.md --full` + +补充:仓库文件路径是 `docs/run/agent-advanced.md`,用 `cr docs read` 查询时文件名参数写 `agent-advanced.md`。 + ## Cirru 语法速览(先看这个) 结构化编辑依赖“树 + 路径”。先能读懂 Cirru,才能稳定算出路径坐标。 @@ -67,6 +79,80 @@ a - 如果把 `, d` 误写成单独一行 `d`,它可能被解析成“调用形态”,节点类型会变化,后续路径与搜索命中也可能随之变化。 - 所以:`,` 本身通常不引入额外层级;它更多是在“换行写法”下保持你想要的 AST 形态。 +#### 先理解启动文件:`compact.cirru` 的 EDN 结构(Agent 快速模型) + +Agent 切到新窗口时,优先把 `compact.cirru` 看成一个“可执行项目快照”,其顶层 EDN 结构通常是: + +```cirru +{} + :package |my-app + :configs $ {} + :init-fn |app.main/main! + :reload-fn |app.main/reload! + :modules $ [] |lilac/ |memof/ + :entries $ {} + :test $ {} + :init-fn |app.test/main! + :reload-fn |app.test/reload! + :modules $ [] |calcit-test/ + :files $ {} + |app.main $ %{} :FileEntry + :ns $ %{} :CodeEntry ... + :defs $ {} + |main! $ %{} :CodeEntry ... +``` + +字段职责可以快速记成: + +- `:package`:包名边界(影响哪些 namespace 允许被 `cr edit` 修改)。 +- `:configs`:默认运行入口(`cr` / `cr js` / `cr ir` 不指定 `--entry` 时使用)。 +- `:entries`:命名入口集合(`cr --entry ` 走这里)。 +- `:files`:源码数据库(namespace → `:ns` + `:defs`;每个定义是 `CodeEntry`,包含 code/doc/examples/schema)。 +- `:modules`:加载的外部模块路径(通常来自 `~/.config/calcit/modules/`,目录结尾 `/` 默认补 `compact.cirru`)。 + +启动解析顺序(实操最常用): + +1. `cr`:使用 `:configs` 的 `:init-fn` / `:reload-fn` / `:modules`。 +2. `cr --entry test`:切到 `:entries.test` 的配置运行。 +3. `cr --init-fn xxx`:覆盖入口函数(常用于测试链路临时指定)。 + +建议每次开工先跑 3 条,建立项目运行心智: + +```bash +cr query config +cr query ns +cr query defs +``` + +#### `deps.cirru` 与 `compact.cirru` 的关系(简版) + +给 Agent 一个最小心智就够: + +- `deps.cirru`:声明“要下载哪些外部模块 + 期望的 calcit 版本”。 +- `compact.cirru`:声明“运行时要加载哪些模块(`:modules`)+ 项目代码快照(`:files`)”。 + +常见升级动作(最少命令): + +```bash +# 1) 看本机 CLI 版本 +cr --version + +# 2) 看依赖是否有更新 +caps --ci outdated + +# 3) 直接更新 deps.cirru(无交互) +caps --ci outdated --yes + +# 4) 下载/同步模块后再编译验证 +caps --ci +cr js +``` + +实践里优先保证两件事一致: + +- `deps.cirru` 的 `:calcit-version` 与当前 `cr --version` 不要偏差太大; +- `package.json` 的 `@calcit/procs` 与当前 Calcit 版本链路保持同一代。 + #### 实操规则(最稳) 凡是改到 `$` 或 `,`(尤其是从单行改成多行)时: @@ -267,15 +353,15 @@ cr js --- -## 7) 进阶内容(已下沉) +## 7) 进阶入口(按需跳转) -本文件只保留高频流程。低频/进阶内容请查: +本文件不重复收录低频内容,遇到下列场景再跳转: -- 完整进阶版 Agent 指南(从旧版完整迁移):`docs/run/agent-advanced.md` +- 复杂重构 / 大规模替换 / rewrite 组合:`cr docs read agent-advanced.md rewrite` +- 命名空间导入、输入格式与路径漂移陷阱:`cr docs read agent-advanced.md 命名空间`、`cr docs read agent-advanced.md 输入格式` - 运行模式、eval 细节、CLI 约束:`Agents.md` -- 语言手册与章节阅读:`cr docs list` / `cr docs read ` -- Cirru 语法细节:`cr cirru show-guide` -- traits 与运行期方法调试:`cr docs search 'trait-call'` +- 浏览所有可查文档:`cr docs list` +- 语言章节与 Cirru 语法细节:`cr docs read ` / `cr cirru show-guide` --- diff --git a/docs/run/agent-advanced.md b/docs/run/agent-advanced.md index 79bcdc2c..68b0e831 100644 --- a/docs/run/agent-advanced.md +++ b/docs/run/agent-advanced.md @@ -2,6 +2,8 @@ 本文档为 AI Agent 提供 Calcit 项目的操作指南。 +本文定位为 Agents 约束与完整操作手册:覆盖硬前置步骤、命令边界、复杂重构与系统化排障。`docs/CalcitAgent.md` 用于查询与局部编辑速查,不替代本文中的约束规则。 + ## 🚀 快速开始(新 LLM 必读) **硬前置步骤:在执行任何 `cr edit` / `cr tree` 修改前,必须先运行一次 `cr docs agents --full`。** From 9b69c01bf413837bcc2b2bb779dcfb9b254bf789 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 19 Mar 2026 18:29:23 +0800 Subject: [PATCH 26/57] clarify query find command usages again --- docs/CalcitAgent.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index 3d740cfc..7e7297fd 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -9,6 +9,7 @@ - 看某个定义的大致结构:`cr query peek ` - 看某个定义的完整实现:`cr query def ` - 找关键词并拿可编辑路径:`cr query search -f ` +- 跨命名空间找符号:`cr query find `(默认就是 fuzzy;需要精确匹配时加 `--exact`) - 查进阶手册某个主题:`cr docs read agent-advanced.md ` - 看进阶手册全文:`cr docs read agent-advanced.md --full` @@ -21,7 +22,10 @@ - Cirru 是缩进风格的 S-expression,缩进层级就是树层级。 - 行内空格分隔节点;嵌套表达式是子节点。 - 常见字面量: - - `|text` 或 `"|text"`:字符串, 两者等价, 区别是后者能处理好空格. 其中 `"|t"` 在老代码也会用 "\"t" 写. + - `|text`:最常用的字符串写法。 + - 标准 one-liner 形式:`"|abc\nd"`(多行文本必须写成 `\n` 内嵌,不能直接跨行写字符串)。 + - `"|text with spaces"`:当字符串里有空格/特殊字符时,使用双引号前缀包裹整段 one-liner。 + - 双引号前缀不是通用替代:简单字符串优先 `|text`,只有在 `|...` 不够清晰时才用 `"|..."`。 - `:tag`:tag - `[]` / `{}`:集合构造 - 你在 `cr query search` 里看到的 `[5.5.1.3]`,本质是“第 5 个子节点的第 5 个子节点的第 1 个子节点的第 3 个子节点”。 @@ -186,6 +190,9 @@ cr docs agents --full - 默认只在高优先级场景展示最多一条(快速扫读) - 需要全部提示时主动加 `--tips` - 需要精细控制时使用 `--tips-level` +- 涉及 `map/filter/reduce` 的改动,优先写成显式嵌套调用(`map xs f`、`filter xs pred`),再考虑 `->`,避免宏展开后参数位置误判。 +- `query find` 不要再写 `-f/--fuzzy`(旧参数);当前默认 fuzzy,需要精确匹配时使用 `--exact`。 +- 在项目目录里用 `cr eval` 验证本项目定义时,默认不要加 `--dep ./`,避免重复加载本地模块导致 namespace 冲突。 > 说明:默认不加参数即 `minimal`(仅高优先级提示,最多 1 条);`--tips` 等价于 `full`。也支持显式 `--tips-level minimal|full|none`。 @@ -295,6 +302,19 @@ cr tree rewrite app.main/demo -p '5.2' --with self=. -e '-> self normalize emit' - 若要看全部提示请加 `--tips`。 - 若要完全静默可用 `--tips-level none`。 +### 本轮新增的稳定性约束(已验证) + +- `cr query find` 当前默认 fuzzy,不再使用 `--fuzzy` / `-f` 这类旧参数;精确匹配用 `--exact`。 +- Cirru 字符串统一按 one-liner 处理:多行文本用 `\n` 内嵌;含空格/特殊字符优先用 `"|text with spaces"`,简单字符串用 `|text`。 +- 条件分支末尾若直接返回值(尤其 `nil`)出现调用歧义时,优先改成稳定值结构(例如 sentinel map)再做过滤。 +- `cr query error` 提示旧错误堆栈时,先以本次 `cr js` / `cr --check-only` 结果为准,再决定是否继续追旧栈。 + +### 命令参数对照(易混) + +- `cr query search -f `:这里的 `-f` 是 `--filter`,用于限定搜索范围(有效)。 +- `cr query find `:默认就是 fuzzy,不再使用 `-f/--fuzzy`(无效);精确匹配用 `--exact`。 +- `cr edit def ... -f `:这里的 `-f` 是“从文件读代码输入”(与 query 的 `-f` 含义不同)。 + ### `Invalid path` 快速恢复模板(固定 3 步) 当路径报错时,不要继续猜坐标,直接走下面流程: From 15ff092e2741cd4c3b3390813ea7f22fab4f9719 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 19 Mar 2026 20:07:27 +0800 Subject: [PATCH 27/57] unify string examples; suggest structural modifying --- docs/CalcitAgent.md | 54 +++++++++++++++++++++++++++++++ docs/data/edn.md | 2 +- docs/installation/ffi-bindings.md | 6 ++-- docs/quick-reference.md | 2 +- docs/run/hot-swapping.md | 8 ++--- 5 files changed, 63 insertions(+), 9 deletions(-) diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index 7e7297fd..472e17db 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -234,6 +234,60 @@ cr js - `cr tree target-replace --pattern '' -e '' --leaf`:按内容唯一定位替换(优先)。 - `cr edit inc --changed `:增量编译当前修改定义。 +### 小改动优先 `cr tree`(避免整段重置) + +当需求只是“改少量内容或局部结构”时,**不要**先写完整文件再 `cr edit def --overwrite -f ...`。这会放大 token 消耗,也更容易引入无关漂移。 + +优先规则: + +- 只改 1~3 个节点:优先 `cr tree` 系列。 +- 仅改文本/叶子:优先 `target-replace` 或 `replace-leaf`。 +- 只调单层结构:优先 `insert-*` / `delete` / `swap-*` / `wrap` / `raise`。 +- 仅在“整段重写/新增定义/大范围重构”时,才用 `cr edit def --overwrite -f`。 + +典型场景模板: + +1. 修改文本节点(leaf) + +```bash +cr tree target-replace --pattern '|Old text' --leaf -e '|New text' +# 或 +cr tree replace-leaf --from '|Old text' --to '|New text' +``` + +2. 删除节点 + +```bash +cr tree delete -p '' +``` + +3. 一层表达式结构调整(同级顺序/包裹关系) + +```bash +cr tree swap-next -p '' +cr tree swap-prev -p '' +cr tree wrap -p '' -e 'when cond self' +cr tree raise -p '' +``` + +4. 补充节点(插入 sibling/child) + +```bash +cr tree insert-before -p '' -e '' +cr tree insert-after -p '' -e '' +cr tree insert-child -p '' -e '' +cr tree append-child -p '' -e '' +``` + +5. 每次小改后都做最小复核 + +```bash +cr tree show -p '' +cr edit inc --changed +``` + +一句话:**小改动走 `cr tree`,大改动才整段覆盖。** + ### 结构化策略(常用 5 招) 下面是“尽量不手写大段代码”的编辑策略,按风险从低到高使用。 diff --git a/docs/data/edn.md b/docs/data/edn.md index bfc6552f..fcb31ad7 100644 --- a/docs/data/edn.md +++ b/docs/data/edn.md @@ -52,7 +52,7 @@ do "|demo string" or use a single double quote for mark strings: ```cirru -do "\"demo string" +do "|demo string" ``` `\n` `\t` `\"` `\\` are supported. diff --git a/docs/installation/ffi-bindings.md b/docs/installation/ffi-bindings.md index 46f5ea86..545ebfc4 100644 --- a/docs/installation/ffi-bindings.md +++ b/docs/installation/ffi-bindings.md @@ -57,19 +57,19 @@ pub fn edn_version() -> String { Rust code is compiled into dylibs, and then Calcit could call with: ```cirru.no-check -&call-dylib-edn (get-dylib-path "\"/dylibs/libcalcit_std") "\"read_file" name +&call-dylib-edn (get-dylib-path "|/dylibs/libcalcit_std") "|read_file" name ``` first argument is the file path to that dylib. And multiple arguments are supported: ```cirru.no-check -&call-dylib-edn (get-dylib-path "\"/dylibs/libcalcit_std") "\"add_duration" (nth date 1) n k +&call-dylib-edn (get-dylib-path "|/dylibs/libcalcit_std") "|add_duration" (nth date 1) n k ``` calling a function is special, we need another function, with last argument being the callback function: ```cirru.no-check -&call-dylib-edn-fn (get-dylib-path "\"/dylibs/libcalcit_std") "\"set_timeout" t cb +&call-dylib-edn-fn (get-dylib-path "|/dylibs/libcalcit_std") "|set_timeout" t cb ``` Notice that both functions call dylibs and then library instances are cached, for better consistency and performance, with some cost in memory occupation. Linux and MacOS has different strategies loading dylibs while loaded repeatedly, so Calcit just cached them and only load once. diff --git a/docs/quick-reference.md b/docs/quick-reference.md index 5d57b7dc..988e42e4 100644 --- a/docs/quick-reference.md +++ b/docs/quick-reference.md @@ -54,7 +54,7 @@ cr eval "echo |done" ## Data Types - **Numbers**: `1`, `3.14` -- **Strings**: `|text`, `"|with spaces"`, `"\"escaped"` +- **Strings**: `|text`, `"|with spaces"`, `"\"escaped"`(old style) - **Tags**: `:keyword` (immutable strings, like Clojure keywords) - **Lists**: `[] 1 2 3` - **HashMaps**: `{} (:a 1) (:b 2)` diff --git a/docs/run/hot-swapping.md b/docs/run/hot-swapping.md index d4312aa7..99ccf791 100644 --- a/docs/run/hot-swapping.md +++ b/docs/run/hot-swapping.md @@ -40,15 +40,15 @@ There's also a `js-out/calcit.build-errors.mjs` file for hot swapping when compi ```cirru.no-check ns app.main :require - "\"./calcit.build-errors" :default build-errors - "\"bottom-tip" :default hud! + "|./calcit.build-errors" :default build-errors + "|bottom-tip" :default hud! defn reload! () $ if (nil? build-errors) do (remove-watch *reel :changes) (clear-cache!) add-watch *reel :changes $ fn (reel prev) (render-app!) reset! *reel $ refresh-reel @*reel schema/store updater - hud! "\"ok~" "\"Ok" - hud! "\"error" build-errors + hud! "|ok~" "|Ok" + hud! "|error" build-errors ``` One tricky thing to hot swap is macros. But you don't need to worry about that in newer versions. From a88debcdc1ce3079ce15338067be9bf092cd9a80 Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 20 Mar 2026 01:17:08 +0800 Subject: [PATCH 28/57] Add builtin JSON procs and runtime docs; tag 0.12.11 --- Cargo.lock | 2 +- Cargo.toml | 2 +- calcit/test.cirru | 22 + docs/CalcitAgent.md | 9 + ...320-0116-json-builtins-and-runtime-docs.md | 45 ++ package.json | 14 +- src/bin/cr.rs | 8 +- src/builtins.rs | 4 + src/builtins/json.rs | 267 +++++++++++ src/calcit/proc_name.rs | 14 + src/cirru/calcit-core.cirru | 428 ++++++++++-------- src/codegen/emit_js.rs | 4 +- ts-src/calcit.procs.mts | 87 ++++ 13 files changed, 691 insertions(+), 215 deletions(-) create mode 100644 editing-history/2026-0320-0116-json-builtins-and-runtime-docs.md create mode 100644 src/builtins/json.rs diff --git a/Cargo.lock b/Cargo.lock index 693d9969..d09e7bc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.10" +version = "0.12.11" dependencies = [ "argh", "bisection_key", diff --git a/Cargo.toml b/Cargo.toml index 3c5e3856..8f3cd88a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.10" +version = "0.12.11" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/calcit/test.cirru b/calcit/test.cirru index 87a6292a..bd58683d 100644 --- a/calcit/test.cirru +++ b/calcit/test.cirru @@ -102,6 +102,7 @@ test-set/main! test-string/main! test-edn/main! + test-json test-record/main! test-fn/main! test-tuple/main! @@ -301,6 +302,27 @@ :schema $ :: :fn {} (:return :dynamic) :args $ [] + |test-json $ %{} :CodeEntry (:doc |) + :code $ quote + fn () (log-title "|Testing JSON") + let + parsed $ json-parse "|{\"name\":\"demo\",\"items\":[1,null,true],\"meta\":{\"flag\":false}}" + assert= |demo $ get parsed :name + assert= ([] 1 nil true) (get parsed :items) + assert= false $ get (get parsed :meta) :flag + assert= + json-parse $ json-stringify + {} (:status |ok) + :items $ [] 1 true nil + {} (:status |ok) + :items $ [] 1 true nil + assert= "|\"ok\"" $ json-stringify :ok + assert= "|{\n \"a\": 1\n}" $ json-pretty + {} $ :a 1 + :examples $ [] + :schema $ :: :fn + {} (:return :dynamic) + :args $ [] |test-method $ %{} :CodeEntry (:doc |) :code $ quote fn () (log-title "|Testing method") diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index 472e17db..099dd53d 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -232,8 +232,17 @@ cr js - `cr tree replace -p '' -e ''`:替换指定节点。 - `cr tree target-replace --pattern '' -e '' --leaf`:按内容唯一定位替换(优先)。 +- `cr edit format`:按当前快照序列化逻辑重写 snapshot 文件,不改语义。 - `cr edit inc --changed `:增量编译当前修改定义。 +`edit format` 用法例子: + +```bash +cr src/cirru/calcit-core.cirru edit format +``` + +说明:`edit format` 作用于“当前输入 snapshot 文件”,在这个仓库里不要直接假设根目录有 `compact.cirru`。 + ### 小改动优先 `cr tree`(避免整段重置) 当需求只是“改少量内容或局部结构”时,**不要**先写完整文件再 `cr edit def --overwrite -f ...`。这会放大 token 消耗,也更容易引入无关漂移。 diff --git a/editing-history/2026-0320-0116-json-builtins-and-runtime-docs.md b/editing-history/2026-0320-0116-json-builtins-and-runtime-docs.md new file mode 100644 index 00000000..885f8386 --- /dev/null +++ b/editing-history/2026-0320-0116-json-builtins-and-runtime-docs.md @@ -0,0 +1,45 @@ +## Summary + +This commit moves JSON parsing and serialization into Calcit builtins and aligns the surrounding runtime/docs/tooling updates needed to ship it cleanly. + +1. Added builtin JSON runtime functions +- Introduced `json-parse`, `json-stringify`, and `json-pretty` in Rust builtin dispatch and JS runtime exports. +- Added fast arity/type checks on exposed runtime entry points. +- Normalized integer-valued numbers to encode as JSON integers in Rust, matching JS output. +- Added native tests and Calcit-level coverage for parse/stringify/pretty behavior and error paths. + +2. Updated core docs and runtime placeholder naming +- Added `calcit.core` docs/examples for the JSON runtime functions. +- Renamed the runtime placeholder spelling from `runtime-inplementation` to `runtime-implementation` across core snapshot metadata and Rust handling. +- Preserved an explicit `cr edit format` example in `docs/CalcitAgent.md`. + +3. Snapshot formatting workflow +- Re-ran `cr edit format` against the touched Cirru snapshot files: + - `src/cirru/calcit-core.cirru` + - `calcit/test.cirru` + +## Files touched (high level) + +- Rust builtin registration and implementation for JSON runtime support. +- JS runtime proc exports for JSON behavior and validation. +- Calcit core snapshot docs/examples and test snapshot updates. +- CLI/codegen placeholder spelling cleanup. +- Agent guide example for `edit format`. + +## Validation notes + +- `cargo test json_` +- `cargo test validate_runtime_impl_is_skipped -- --nocapture` +- `cargo test runtime_placeholder -- --nocapture` +- `cargo run --bin cr -- calcit/test.cirru -1` +- `yarn check-all` + +## Release size check + +Measured `target/release/cr` against `HEAD` using a detached worktree build: + +- Current: `5192960` bytes (`5.0M`) +- Base: `5174400` bytes (`4.9M`) +- Delta: `+18560` bytes (about `+18.1 KiB`) + +Conclusion: the builtin JSON work increases the release binary size slightly, but the delta is small relative to the ~5 MB target. diff --git a/package.json b/package.json index e47187d8..92e0dd23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.10", + "version": "0.12.11", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", @@ -16,14 +16,14 @@ "test-rs": "cargo test -q", "test-fail": "cargo test -q --bin cr type_fail_", "test-snippets": "cargo test -q snippets::tests", - "bench-recur-smoke": "cargo run --bin cr -- calcit/test.cirru -1 js && node --input-type=module -e \"import { test_loop } from './js-out/test-recursion.main.mjs'; const n=3000; const t0=process.hrtime.bigint(); for(let i=0;i x 0) |positive |non-positive quote $ if (empty? xs) 0 (count xs) @@ -3284,7 +3284,7 @@ {} (:rest :set) (:return :set) :args $ [] :set |is-spreading-mark? $ %{} :CodeEntry (:doc "|internal function for detecting syntax &\nSyntax: (is-spreading-mark? value)\nParams: value (any)\nReturns: boolean\nReturns true if value is the spreading mark symbol &") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :bool) @@ -3348,6 +3348,34 @@ ys $ &list:concat & xs quasiquote $ &js-object ~@ys :examples $ [] + |json-parse $ %{} :CodeEntry (:doc "|internal function for parsing JSON text\nSyntax: (json-parse text)\nParams: text (string)\nReturns: Calcit data\nParses JSON text into Calcit values. JSON object keys become tags, arrays become lists, and null becomes nil") + :code $ quote &runtime-implementation + :examples $ [] + quote $ assert= + {} $ :a 1 + json-parse "|{\"a\":1}" + quote $ assert= ([] true nil) + get (json-parse "|{\"items\":[true,null]}") :items + :schema $ :: :fn + {} (:return :dynamic) + :args $ [] :string + |json-pretty $ %{} :CodeEntry (:doc "|internal function for pretty-printing JSON\nSyntax: (json-pretty value)\nParams: value (any JSON-compatible Calcit data)\nReturns: string\nConverts Calcit data into formatted JSON text using 2-space indentation") + :code $ quote &runtime-implementation + :examples $ [] + quote $ assert= "|{\n \"a\": 1\n}" + json-pretty $ {} (:a 1) + :schema $ :: :fn + {} (:return :string) + :args $ [] :dynamic + |json-stringify $ %{} :CodeEntry (:doc "|internal function for encoding JSON\nSyntax: (json-stringify value)\nParams: value (any JSON-compatible Calcit data)\nReturns: string\nConverts Calcit data into compact JSON text. Tags and symbols are encoded as plain JSON strings") + :code $ quote &runtime-implementation + :examples $ [] + quote $ assert= "|\"ok\"" (json-stringify :ok) + quote $ assert= "|{\"a\":1}" + json-stringify $ {} (:a 1) + :schema $ :: :fn + {} (:return :string) + :args $ [] :dynamic |keys $ %{} :CodeEntry (:doc |) :code $ quote defn keys (x) @@ -3627,13 +3655,13 @@ {} (:return :bool) :args $ [] :dynamic |macroexpand $ %{} :CodeEntry (:doc "|internal syntax for expanding macros until recursive calls are resolved\nSyntax: (macroexpand expr)\nParams: expr (macro call)\nReturns: fully expanded code\nExpands macros recursively until no more macro calls remain") (:schema nil) - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] |macroexpand-1 $ %{} :CodeEntry (:doc "|internal syntax for expanding macro just once for debugging\nSyntax: (macroexpand-1 expr)\nParams: expr (macro call)\nReturns: one-level expanded code\nExpands macro only one level for debugging purposes") (:schema nil) - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] |macroexpand-all $ %{} :CodeEntry (:doc "|internal syntax for expanding macro until macros inside are resolved\nSyntax: (macroexpand-all expr)\nParams: expr (code with macros)\nReturns: fully expanded code\nExpands all macros including nested ones") (:schema nil) - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] |map $ %{} :CodeEntry (:doc "|Collection mapping function. Applies a function to each element of a list, set, or map, returning a structure of the same shape.") :code $ quote @@ -3799,7 +3827,7 @@ {} (:return :dynamic) :args $ [] :dynamic |not $ %{} :CodeEntry (:doc "|internal function for logical not\nSyntax: (not value)\nParams: value (any)\nReturns: boolean\nReturns true if value is falsy (nil or false), false otherwise") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :bool) @@ -3914,64 +3942,64 @@ {} (:return :map) :args $ [] :list |parse-cirru $ %{} :CodeEntry (:doc "|internal function for parsing Cirru\nSyntax: (parse-cirru text)\nParams: text (string)\nReturns: list\nParses Cirru syntax text into nested list structure") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :list) :args $ [] :string |parse-cirru-edn $ %{} :CodeEntry (:doc "|internal function for parsing Cirru EDN\nSyntax: (parse-cirru-edn text)\nParams: text (string)\nReturns: any\nParses Cirru EDN format text into Calcit data structures") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :dynamic) :args $ [] :string |parse-cirru-list $ %{} :CodeEntry (:doc "|internal function for parsing Cirru list\nSyntax: (parse-cirru-list text)\nParams: text (string)\nReturns: list\nParses Cirru text as a list of expressions") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :list) :args $ [] :string |parse-float $ %{} :CodeEntry (:doc "|internal function for parsing float\nSyntax: (parse-float s)\nParams: s (string)\nReturns: number or nil\nParses string as floating point number, returns nil if invalid") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} :args $ [] :string :return $ :: :optional :number |pow $ %{} :CodeEntry (:doc "|internal function for power operation\nSyntax: (pow base exponent)\nParams: base (number), exponent (number)\nReturns: number\nRaises base to the power of exponent") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :number) :args $ [] :number :number |prepend $ %{} :CodeEntry (:doc "|internal function for prepending to list\nSyntax: (prepend list element)\nParams: list (list), element (any)\nReturns: list\nReturns new list with element added at beginning") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :list) :args $ [] :list :dynamic |quasiquote $ %{} :CodeEntry (:doc "|internal syntax for quasiquote (used inside macros)\nSyntax: (quasiquote expr)\nParams: expr (code with possible unquote)\nReturns: partially quoted structure\nLike quote but allows selective unquoting with ~ and ~@") (:schema nil) - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] quote $ quasiquote (&+ ~x 1) quote $ quasiquote ([] ~x ~@xs) |quit! $ %{} :CodeEntry (:doc "|internal function for quitting program\nSyntax: (quit! exit-code)\nParams: exit-code (number, optional, defaults to 0)\nReturns: never returns (exits program)\nTerminates the program with specified exit code") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :unit) :args $ [] (:: :optional :number) |quote $ %{} :CodeEntry (:doc "|internal syntax for turning code into quoted data\nSyntax: (quote expr)\nParams: expr (any code)\nReturns: quoted data structure\nPrevents evaluation and returns code as data") (:schema nil) - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] |raise $ %{} :CodeEntry (:doc "|internal function for raising exceptions\nSyntax: (raise message)\nParams: message (string)\nReturns: never returns (throws exception)\nThrows an exception with the given message") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :dynamic) :args $ [] :string |range $ %{} :CodeEntry (:doc "|internal function for creating number ranges\nSyntax: (range start end) or (range end)\nParams: start (number, optional), end (number)\nReturns: list\nCreates list of numbers from start to end (exclusive)") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :list) @@ -3992,7 +4020,7 @@ :args $ [] :number (:: :optional :number) :return $ :: :list :number |read-file $ %{} :CodeEntry (:doc "|internal function for reading files\nSyntax: (read-file filepath)\nParams: filepath (string)\nReturns: string content or error\nReads file content as string, throws error if file not found") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :string) @@ -4039,7 +4067,7 @@ {} (:return :bool) :args $ [] :dynamic |recur $ %{} :CodeEntry (:doc "|internal function for tail recursion\nSyntax: (recur args...)\nParams: args (any, variable number)\nReturns: recur structure for tail call optimization\nEnables tail call optimization by marking recursive calls") (:schema nil) - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] |reduce $ %{} :CodeEntry (:doc "|Collection reduction operation\nFunction: Reduces a collection using a specified function, accumulating elements onto an initial value\nParams: xs (collection), x0 (initial accumulator value), f (reduction function that takes accumulator and current element)\nReturns: any type - final accumulated result\nNotes: The reduction function f should accept two parameters (accumulator, current element) and return a new accumulator value") :code $ quote @@ -4059,7 +4087,7 @@ {} (:return :bool) :args $ [] :dynamic |remove-watch $ %{} :CodeEntry (:doc "|internal function for removing atom watchers\nSyntax: (remove-watch atom key)\nParams: atom (atom), key (any)\nReturns: atom\nRemoves watcher with specified key from atom") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :unit) @@ -4085,7 +4113,7 @@ :generics $ [] 'T :return $ :: :list 'T |reset! $ %{} :CodeEntry (:doc "|internal syntax for resetting atom values\nSyntax: (reset! atom new-value)\nParams: atom (atom reference), new-value (any)\nReturns: new value\nSets atom to new value and returns it") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] quote $ ; reset! *my-atom {} $ :a 2 @@ -4133,13 +4161,13 @@ :generics $ [] 'T :return $ :: :list 'T |round $ %{} :CodeEntry (:doc "|internal function for rounding numbers\nSyntax: (round n)\nParams: n (number)\nReturns: number\nRounds number to nearest integer") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :number) :args $ [] :number |round? $ %{} :CodeEntry (:doc "|internal function for checking if number is round\nSyntax: (round? n)\nParams: n (number)\nReturns: boolean\nReturns true if number has no fractional part") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :bool) @@ -4192,7 +4220,7 @@ {} (:return :bool) :args $ [] :dynamic |sin $ %{} :CodeEntry (:doc "|internal function for sine\nSyntax: (sin n)\nParams: n (number, radians)\nReturns: number\nReturns sine of angle in radians") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :number) @@ -4240,31 +4268,31 @@ {} (:return :bool) :args $ [] :dynamic |sort $ %{} :CodeEntry (:doc "|internal function for sorting lists\nSyntax: (sort list) or (sort list comparator)\nParams: list (list), comparator (function, optional)\nReturns: list\nReturns sorted list using natural order or custom comparator") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :list) :args $ [] :list (:: :optional :fn) |split $ %{} :CodeEntry (:doc "|internal function for splitting strings\nSyntax: (split s delimiter)\nParams: s (string), delimiter (string)\nReturns: list of strings\nSplits string by delimiter into list of substrings") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :list) :args $ [] :string :string |split-lines $ %{} :CodeEntry (:doc "|internal function for splitting lines\nSyntax: (split-lines s)\nParams: s (string)\nReturns: list of strings\nSplits string by newlines into list of lines") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :list) :args $ [] :string |sqrt $ %{} :CodeEntry (:doc "|internal function for square root\nSyntax: (sqrt n)\nParams: n (number)\nReturns: number\nReturns square root of n") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :number) :args $ [] :number |starts-with? $ %{} :CodeEntry (:doc "|internal function for checking string prefix\nSyntax: (starts-with? s prefix)\nParams: s (string), prefix (string)\nReturns: boolean\nReturns true if string starts with prefix") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :bool) @@ -4484,25 +4512,25 @@ {} (:return :bool) :args $ [] :dynamic |to-lispy-string $ %{} :CodeEntry (:doc "|internal function for converting to Lisp string\nSyntax: (to-lispy-string value)\nParams: value (any)\nReturns: string\nConverts value to Lisp-style string representation") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :string) :args $ [] :dynamic |to-pairs $ %{} :CodeEntry (:doc "|internal function for converting to pairs\nSyntax: (to-pairs map)\nParams: map (map)\nReturns: set\nConverts map to an unordered set of [key value] pairs") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :set) :args $ [] :map |trim $ %{} :CodeEntry (:doc "|internal function for trimming strings\nSyntax: (trim s)\nParams: s (string)\nReturns: string\nRemoves whitespace from beginning and end of string") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :string) :args $ [] :string |try $ %{} :CodeEntry (:doc "|internal syntax for try-catch error handling\nSyntax: (try body (catch error handler))\nParams: body (expression), error (symbol), handler (expression)\nReturns: result of body or handler if error occurs\nProvides exception handling mechanism") (:schema nil) - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] |tuple? $ %{} :CodeEntry (:doc "|Predicate that checks whether a value is a tuple literal created with the `::` form.") :code $ quote @@ -4524,25 +4552,25 @@ {} (:return :string) :args $ [] :dynamic |turn-string $ %{} :CodeEntry (:doc "|internal function for converting to string\nSyntax: (turn-string value)\nParams: value (any)\nReturns: string\nConverts value to string representation") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :string) :args $ [] :dynamic |turn-symbol $ %{} :CodeEntry (:doc "|internal function for converting to symbol\nSyntax: (turn-symbol value)\nParams: value (string, tag, or symbol)\nReturns: symbol\nConverts string, tag, or existing symbol to symbol type") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :dynamic) :args $ [] :dynamic |turn-tag $ %{} :CodeEntry (:doc "|internal function for converting to tag\nSyntax: (turn-tag value)\nParams: value (string, symbol, or tag)\nReturns: tag\nConverts string, symbol, or existing tag to tag type") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :tag) :args $ [] :dynamic |type-of $ %{} :CodeEntry (:doc "|internal function for getting type of value\nSyntax: (type-of value)\nParams: value (any)\nReturns: tag representing the type\nReturns type tag like :nil, :bool, :number, :string, :list, :map, :set, :fn, etc.") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :tag) @@ -4799,7 +4827,7 @@ :schema $ :: :macro {} $ :args ([] :dynamic) |write-file $ %{} :CodeEntry (:doc "|internal function for writing files\nSyntax: (write-file filepath content)\nParams: filepath (string), content (string)\nReturns: nil or error\nWrites string content to file, creates directories if needed") - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :schema $ :: :fn {} (:return :unit) @@ -4852,10 +4880,10 @@ :schema $ :: :macro {} $ :args ([]) |~ $ %{} :CodeEntry (:doc "|internal syntax for interpolating value in macro\nSyntax: (~ expr) inside quasiquote\nParams: expr (expression to evaluate)\nReturns: evaluated expression\nUnquotes expression inside quasiquote") (:schema nil) - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] |~@ $ %{} :CodeEntry (:doc "|internal syntax for spreading interpolate value in macro\nSyntax: (~@ list-expr) inside quasiquote\nParams: list-expr (expression that evaluates to list)\nReturns: spliced list elements\nUnquotes and splices list elements inside quasiquote") (:schema nil) - :code $ quote &runtime-inplementation + :code $ quote &runtime-implementation :examples $ [] :ns $ %{} :NsEntry (:doc "|built-in function and macros in `calcit.core`") :code $ quote diff --git a/src/codegen/emit_js.rs b/src/codegen/emit_js.rs index 159f5447..c8df60f0 100644 --- a/src/codegen/emit_js.rs +++ b/src/codegen/emit_js.rs @@ -96,7 +96,7 @@ fn is_quote_head(value: &Calcit) -> bool { } fn is_runtime_placeholder_form(value: &Calcit) -> bool { - matches!(value, Calcit::Symbol { sym, .. } if sym.as_ref() == "&runtime-inplementation") + matches!(value, Calcit::Symbol { sym, .. } if sym.as_ref() == "&runtime-implementation") } fn is_runtime_placeholder_quote(value: &Calcit) -> bool { @@ -1470,7 +1470,7 @@ mod tests { fn runtime_placeholder_quote() -> Calcit { Calcit::List(Arc::new(CalcitList::from(&[ Calcit::Syntax(CalcitSyntax::Quote, Arc::from(calcit::CORE_NS)), - symbol("&runtime-inplementation"), + symbol("&runtime-implementation"), ]))) } diff --git a/ts-src/calcit.procs.mts b/ts-src/calcit.procs.mts index d7962c27..f29d42f3 100644 --- a/ts-src/calcit.procs.mts +++ b/ts-src/calcit.procs.mts @@ -1560,6 +1560,93 @@ export let parse_cirru_edn = (code: string, options: CalcitValue) => { } }; +const json_to_calcit = (value: any): CalcitValue => { + if (value == null) return null; + if (typeof value === "string") return value; + if (typeof value === "number") return value; + if (typeof value === "boolean") return value; + if (Array.isArray(value)) { + return new CalcitSliceList(value.map(json_to_calcit)); + } + if (typeof value === "object") { + const entries: CalcitValue[] = []; + for (const key of Object.keys(value)) { + entries.push(newTag(key), json_to_calcit(value[key])); + } + return new CalcitSliceMap(entries); + } + throw new Error(`Unsupported JSON value: ${value}`); +}; + +const calcit_json_key = (value: CalcitValue): string => { + if (value instanceof CalcitTag) return value.value; + if (typeof value === "string") return value; + throw new Error(`json-stringify expected object keys to be tags or strings, got: ${toString(value, true)}`); +}; + +const cirru_quote_to_json = (value: ICirruNode): any => { + if (typeof value === "string") return value; + if (Array.isArray(value)) return value.map(cirru_quote_to_json); + throw new Error(`Unsupported cirru quote node: ${value}`); +}; + +const calcit_to_json = (value: CalcitValue): any => { + if (value == null) return null; + if (typeof value === "string") return value; + if (typeof value === "number") { + if (Number.isFinite(value)) return value; + throw new Error(`json-stringify cannot encode number: ${value}`); + } + if (typeof value === "boolean") return value; + if (value instanceof CalcitTag) return value.value; + if (value instanceof CalcitSymbol) return value.value; + if (value instanceof CalcitList || value instanceof CalcitSliceList) { + return Array.from(value.items()).map(calcit_to_json); + } + if (value instanceof CalcitSet) { + return value.values().map(calcit_to_json); + } + if (value instanceof CalcitMap || value instanceof CalcitSliceMap) { + const result: Record = {}; + for (const [key, item] of value.pairs()) { + result[calcit_json_key(key)] = calcit_to_json(item); + } + return result; + } + if (value instanceof CalcitTuple) { + return [calcit_to_json(value.tag), ...value.extra.map(calcit_to_json)]; + } + if (value instanceof CalcitCirruQuote) { + return cirru_quote_to_json(value.value as ICirruNode); + } + if (value instanceof CalcitRecord) { + const result: Record = {}; + for (let idx = 0; idx < value.fields.length; idx++) { + result[value.fields[idx].value] = calcit_to_json(value.values[idx]); + } + return result; + } + throw new Error(`json-stringify cannot encode value: ${toString(value, true)}`); +}; + +export let json_parse = function (code: CalcitValue): CalcitValue { + if (arguments.length !== 1) throw new Error("json-parse expected 1 argument"); + if (typeof code !== "string") { + throw new Error(`json-parse expected a string, got: ${toString(code, true)}`); + } + return json_to_calcit(JSON.parse(code)); +}; + +export let json_stringify = function (value: CalcitValue): string { + if (arguments.length !== 1) throw new Error("json-stringify expected 1 argument"); + return JSON.stringify(calcit_to_json(value)); +}; + +export let json_pretty = function (value: CalcitValue): string { + if (arguments.length !== 1) throw new Error("json-pretty expected 1 argument"); + return JSON.stringify(calcit_to_json(value), null, 2); +}; + export let format_to_lisp = (x: CalcitValue): string => { if (x == null) { return "nil"; From 95f62d056d2fca1a28622b35b0d1ce3d3b839c90 Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 20 Mar 2026 14:38:14 +0800 Subject: [PATCH 29/57] Refine query search output and exact-match behavior --- ...1229-query-window-3-with-parent-preview.md | 72 ++ src/bin/cli_handlers/common.rs | 13 +- src/bin/cli_handlers/query.rs | 672 ++++++++++-------- src/cli_args.rs | 14 +- 4 files changed, 453 insertions(+), 318 deletions(-) create mode 100644 editing-history/2026-0320-1229-query-window-3-with-parent-preview.md diff --git a/editing-history/2026-0320-1229-query-window-3-with-parent-preview.md b/editing-history/2026-0320-1229-query-window-3-with-parent-preview.md new file mode 100644 index 00000000..4d952310 --- /dev/null +++ b/editing-history/2026-0320-1229-query-window-3-with-parent-preview.md @@ -0,0 +1,72 @@ +# Query 搜索结果分层、降噪与路径/预览优化(合并记录) + +## 变更概述 + +- 初始实现:为 `cr query` 搜索类命令引入结果分层展示(详情窗口 + 压缩输出)。 +- 参数命名收敛:将 `--prefer` 直接改为 `--detail-offset`(不保留兼容别名)。 +- 输出降噪:删除冗长提示,保留定位核心信息(定义、路径、命中片段)。 +- 路径统一:搜索输出与 `--start-path` 示例统一为点号分隔;解析层仅接受点号输入。 +- 详情窗口进一步从 5 缩小为 3,并在窗口内增加“所在表达式 + 父表达式”预览(可省略复杂父表达式)。 + +## 覆盖命令 + +- `cr query find` +- `cr query usages` +- `cr query search` +- `cr query search-expr` + +## 关键实现 + +- `src/cli_args.rs` + - 参数字段统一为 `detail_offset`。 + - 参数统一为 `#[argh(option, long = "detail-offset", default = "0")]`。 + - 参数说明更新为“3 detailed items”。 + - `query search --start-path` 示例更新为点号格式 `2.1.0`。 + +- `src/bin/cli_handlers/query.rs` + - 结果窗口逻辑: + - `DETAILED_RESULTS_WINDOW = 3` + - `detailed_window(detail_offset, total)` + - `in_detail_window(index, total, detail_offset)` + - `print_detail_window_hint(...)` + - 搜索结果降噪: + - `search/search-expr` 按命中数排序定义(高命中优先)。 + - 详情行输出为 `[path] `;窗口外仅输出压缩条数(`N matches compressed outside window`)。 + - 移除 `Next steps`/批量替换等长提示块。 + - 预览增强: + - 新增 `preview_node_oneline(...)` 并修复叶子节点空预览问题。 + - 新增 `path_parent(...)`、`get_node_at_path(...)`、`count_nodes_limited(...)`、`can_show_parent_preview(...)`、`expression_and_parent_preview(...)`。 + - 最终形态改为“单行预览”:优先显示父节点预览;无父节点时回退到当前表达式(避免重复展示两遍)。 + + - Tips 与 exact 语义修正: + - `--exact` 高优先级提示文案统一为英文:`Many matches (N); add --exact to show exact matches only`。 + - 仅在 contains 模式且命中数 `> 10` 时展示该提示;`--exact` 模式不再显示冗余提示。 + - 修复 exact 模式下的误导性高亮:只在 token 边界高亮,避免将 `states` 中的 `state` 误标为命中。 + + - 命名收敛:将旧命名 `prefer` 在函数参数/调用链/提示文案中统一改为 `detail_offset` / `detail-offset`。 + +- `src/bin/cli_handlers/common.rs` + - `parse_path(...)` 改为仅接受点号分隔;逗号输入报错并提示改用点号。 + +## 降噪策略 + +- 深度嵌套(路径过深)或分支复杂(节点过大)时,父表达式预览自动省略。 +- 详细窗口外仍然输出压缩占位,不展开上下文。 + +## 验证 + +- `cargo check --bin cr` 通过。 +- `cargo run --bin cr -- /Users/jon.chen/repo/respo/respo/compact.cirru query find --help` + - 可见 `--detail-offset` 参数。 +- `cargo run --bin cr -- /Users/jon.chen/repo/respo/respo/compact.cirru query search --help` + - `--start-path` 示例为点号格式。 +- `cargo run --bin cr -- /Users/jon.chen/repo/respo/respo/compact.cirru query search state -f respo.app.comp.todolist/comp-todolist --detail-offset 5` + - 输出降噪,路径为点号,窗口外压缩。 +- `cargo run --bin cr -- /Users/jon.chen/repo/respo/respo/compact.cirru query search state -f respo.app.comp.todolist/comp-todolist --detail-offset 0` + - 详情窗口为 `[0, 3)`,每条命中仅展示一行(优先父节点预览)。 +- `cargo run --bin cr -- /Users/jon.chen/repo/respo/respo/compact.cirru query search state -f respo.app.comp.todolist/comp-todolist --detail-offset 0 --exact` + - 精确命中不再把 `states` 视觉误判为 `state`;且不显示“add --exact”提示。 +- `cargo run --bin cr -- /Users/jon.chen/repo/respo/respo/compact.cirru query search-expr state -f respo.app.comp.todolist/comp-todolist --detail-offset 0` + - 输出样式一致(单行预览)。 +- `cargo run --bin cr -- /Users/jon.chen/repo/respo/respo/compact.cirru query search-expr state -f respo.app.comp.todolist/comp-todolist --detail-offset 0 --exact` + - 结果为 `No matches found`,符合结构精确匹配预期。 diff --git a/src/bin/cli_handlers/common.rs b/src/bin/cli_handlers/common.rs index a7c1bf90..42df97be 100644 --- a/src/bin/cli_handlers/common.rs +++ b/src/bin/cli_handlers/common.rs @@ -64,25 +64,20 @@ pub fn format_path_bracketed(path: &[usize]) -> String { } } -/// Parse path string like "2,1,0" or "2.1.0" to Vec +/// Parse path string like "2.1.0" to Vec pub fn parse_path(path_str: &str) -> Result, String> { if path_str.is_empty() { return Ok(vec![]); } - let has_comma = path_str.contains(','); - let has_dot = path_str.contains('.'); - - if has_comma && has_dot { + if path_str.contains(',') { return Err(format!( - "Invalid path '{path_str}': mixed separators are not allowed. Use either comma-separated or dot-separated coordinates." + "Invalid path '{path_str}': comma separator is no longer supported. Use dot-separated coordinates, e.g. '2.1.0'." )); } - let separator = if has_dot { '.' } else { ',' }; - path_str - .split(separator) + .split('.') .map(|s| s.trim().parse::().map_err(|e| format!("Invalid path index '{s}': {e}"))) .collect() } diff --git a/src/bin/cli_handlers/query.rs b/src/bin/cli_handlers/query.rs index e8bf35c5..31eb2f3c 100644 --- a/src/bin/cli_handlers/query.rs +++ b/src/bin/cli_handlers/query.rs @@ -3,8 +3,8 @@ //! Handles: cr query ns, defs, def, at, peek, examples, find, usages, pkg, config, error, modules use super::chunk_display::{ChunkDisplayOptions, ChunkedDisplay, maybe_chunk_node}; -use super::common::{format_path_bracketed, parse_path}; -use super::tips::{Tips, tip_prefer_oneliner_json, tip_query_defs_list, tip_query_ns_list}; +use super::common::{format_path, format_path_bracketed, parse_path}; +use super::tips::{TipPriority, Tips, tip_prefer_oneliner_json, tip_query_defs_list, tip_query_ns_list}; use calcit::CalcitTypeAnnotation; use calcit::cli_args::{QueryCommand, QueryDefCommand, QuerySubcommand}; use calcit::load_core_snapshot; @@ -24,6 +24,191 @@ type SearchResults = Vec<(String, String, Vec<(Vec, Cirru)>)>; /// Type alias for reference results: (namespace, definition, context, coordinate-path, source-label) type RefResults = Vec<(String, String, String, Vec>, &'static str)>; +struct SearchCommonOpts<'a> { + filter: Option<&'a str>, + loose: bool, + max_depth: usize, + entry: Option<&'a str>, + detail_offset: usize, +} + +const DETAILED_RESULTS_WINDOW: usize = 3; + +fn detailed_window(detail_offset: usize, total: usize) -> (usize, usize) { + if total == 0 { + return (0, 0); + } + let start = detail_offset.min(total.saturating_sub(1)); + let end = (start + DETAILED_RESULTS_WINDOW).min(total); + (start, end) +} + +fn print_detail_window_hint(total: usize, detail_offset: usize, subject: &str) { + if total > DETAILED_RESULTS_WINDOW { + let (start, end) = detailed_window(detail_offset, total); + println!( + "{}", + format!("Detail window for {subject}: [{start}, {end}) (detail-offset={detail_offset}), other entries are compressed.").dimmed() + ); + } +} + +fn in_detail_window(index: usize, total: usize, detail_offset: usize) -> bool { + if total <= DETAILED_RESULTS_WINDOW { + return true; + } + let (start, end) = detailed_window(detail_offset, total); + index >= start && index < end +} + +fn preview_node_oneline(node: &Cirru, max_len: usize) -> (String, bool) { + let text = match node { + Cirru::Leaf(s) => s.to_string(), + _ => node.format_one_liner().unwrap_or_default(), + }; + if text.is_empty() { + return ("(matched)".to_string(), false); + } + if text.len() > max_len { + (text[..max_len].to_string(), true) + } else { + (text, false) + } +} + +fn is_token_delimiter(ch: Option) -> bool { + match ch { + None => true, + Some(c) => c.is_whitespace() || matches!(c, '(' | ')' | '[' | ']' | '{' | '}' | '$' | ','), + } +} + +fn highlight_target_text(text: &str, target: Option<&str>, loose: bool) -> String { + let Some(target) = target else { + return text.to_string(); + }; + if target.is_empty() || !text.contains(target) { + return text.to_string(); + } + + if loose { + return text.replace(target, &format!("{}", target.bright_yellow().bold())); + } + + let mut highlighted = String::with_capacity(text.len()); + let mut last_index = 0; + + for (idx, _) in text.match_indices(target) { + let prev_char = text[..idx].chars().next_back(); + let next_char = text[idx + target.len()..].chars().next(); + if is_token_delimiter(prev_char) && is_token_delimiter(next_char) { + highlighted.push_str(&text[last_index..idx]); + highlighted.push_str(&format!("{}", target.bright_yellow().bold())); + last_index = idx + target.len(); + } + } + + if last_index == 0 { + text.to_string() + } else { + highlighted.push_str(&text[last_index..]); + highlighted + } +} + +fn path_parent(path: &[usize]) -> Option> { + if path.is_empty() { + None + } else { + Some(path[..path.len() - 1].to_vec()) + } +} + +fn get_node_at_path(code: &Cirru, path: &[usize]) -> Option { + if path.is_empty() { + return Some(code.clone()); + } + let mut current = code; + for &idx in path { + match current { + Cirru::List(items) => current = items.get(idx)?, + Cirru::Leaf(_) => return None, + } + } + Some(current.clone()) +} + +fn count_nodes_limited(node: &Cirru, limit: usize) -> usize { + fn walk(node: &Cirru, acc: &mut usize, limit: usize) { + if *acc >= limit { + return; + } + *acc += 1; + if let Cirru::List(items) = node { + for item in items { + if *acc >= limit { + break; + } + walk(item, acc, limit); + } + } + } + let mut acc = 0; + walk(node, &mut acc, limit); + acc +} + +fn can_show_parent_preview(expr_path: &[usize], parent_node: &Cirru) -> bool { + if expr_path.len() > 8 { + return false; + } + if let Cirru::List(items) = parent_node + && items.len() > 8 + { + return false; + } + count_nodes_limited(parent_node, 40) < 40 +} + +fn expression_and_parent_preview( + code: &Cirru, + match_path: &[usize], + matched_node: &Cirru, + highlight_target: Option<&str>, + loose: bool, +) -> ((String, bool), Vec<(String, bool)>) { + let expr_path = if matches!(matched_node, Cirru::Leaf(_)) { + path_parent(match_path).unwrap_or_else(|| match_path.to_vec()) + } else { + match_path.to_vec() + }; + + let expr_node = get_node_at_path(code, &expr_path).unwrap_or_else(|| matched_node.clone()); + let (expr_text, expr_truncated) = preview_node_oneline(&expr_node, 110); + let expr_preview = (highlight_target_text(&expr_text, highlight_target, loose), expr_truncated); + + let mut parent_previews: Vec<(String, bool)> = Vec::new(); + let mut current_path = expr_path; + + for _ in 0..2 { + let Some(parent_path) = path_parent(¤t_path) else { + break; + }; + let Some(parent_node) = get_node_at_path(code, &parent_path) else { + break; + }; + + if can_show_parent_preview(&parent_path, &parent_node) { + let (preview_text, preview_truncated) = preview_node_oneline(&parent_node, 110); + parent_previews.push((highlight_target_text(&preview_text, highlight_target, loose), preview_truncated)); + } + + current_path = parent_path; + } + + (expr_preview, parent_previews) +} + /// Parse "namespace/definition" format into (namespace, definition) /// Splits at the FIRST '/' so operator definitions like '/' and '/=' are handled correctly. fn parse_target(target: &str) -> Result<(&str, &str), String> { @@ -54,33 +239,35 @@ pub fn handle_query_command(cmd: &QueryCommand, input_path: &str) -> Result<(), } QuerySubcommand::Find(opts) => { if opts.exact { - handle_find(input_path, &opts.symbol, opts.deps) + handle_find(input_path, &opts.symbol, opts.deps, opts.detail_offset) } else { - handle_fuzzy_search(input_path, &opts.symbol, opts.deps, opts.limit) + handle_fuzzy_search(input_path, &opts.symbol, opts.deps, opts.limit, opts.detail_offset) } } QuerySubcommand::Usages(opts) => { let (ns, def) = parse_target(&opts.target)?; - handle_usages(input_path, ns, def, opts.deps) + handle_usages(input_path, ns, def, opts.deps, opts.detail_offset) + } + QuerySubcommand::Search(opts) => { + let common_opts = SearchCommonOpts { + filter: opts.filter.as_deref(), + loose: !opts.exact, + max_depth: opts.max_depth, + entry: opts.entry.as_deref(), + detail_offset: opts.detail_offset, + }; + handle_search_leaf(input_path, &opts.pattern, opts.start_path.as_deref(), &common_opts) + } + QuerySubcommand::SearchExpr(opts) => { + let common_opts = SearchCommonOpts { + filter: opts.filter.as_deref(), + loose: !opts.exact, + max_depth: opts.max_depth, + entry: opts.entry.as_deref(), + detail_offset: opts.detail_offset, + }; + handle_search_expr(input_path, &opts.pattern, opts.json, &common_opts) } - QuerySubcommand::Search(opts) => handle_search_leaf( - input_path, - &opts.pattern, - opts.filter.as_deref(), - !opts.exact, - opts.max_depth, - opts.start_path.as_deref(), - opts.entry.as_deref(), - ), - QuerySubcommand::SearchExpr(opts) => handle_search_expr( - input_path, - &opts.pattern, - opts.filter.as_deref(), - !opts.exact, - opts.max_depth, - opts.json, - opts.entry.as_deref(), - ), QuerySubcommand::Schema(opts) => { let (ns, def) = parse_target(&opts.target)?; handle_schema(input_path, ns, def, opts.json) @@ -744,7 +931,7 @@ fn handle_schema(input_path: &str, namespace: &str, definition: &str, json: bool } /// Find symbol across all namespaces -fn handle_find(input_path: &str, symbol: &str, include_deps: bool) -> Result<(), String> { +fn handle_find(input_path: &str, symbol: &str, include_deps: bool, detail_offset: usize) -> Result<(), String> { let snapshot = load_snapshot(input_path)?; let mut found_definitions: Vec<(String, String)> = vec![]; @@ -804,8 +991,13 @@ fn handle_find(input_path: &str, symbol: &str, include_deps: bool) -> Result<(), // Print definitions if !found_definitions.is_empty() { println!("{}", "Defined in:".bold().green()); - for (ns, def) in &found_definitions { - println!(" {}/{}", ns.cyan(), def.green()); + print_detail_window_hint(found_definitions.len(), detail_offset, "definitions"); + for (idx, (ns, def)) in found_definitions.iter().enumerate() { + if in_detail_window(idx, found_definitions.len(), detail_offset) { + println!(" {}/{}", ns.cyan(), def.green()); + } else { + println!(" ⋯ {}/{}", ns.dimmed(), def.dimmed()); + } } println!(); } @@ -818,7 +1010,20 @@ fn handle_find(input_path: &str, symbol: &str, include_deps: bool) -> Result<(), if !references.is_empty() { println!("{}", "Referenced in:".bold()); - for (ns, def, context, coords, source) in &references { + print_detail_window_hint(references.len(), detail_offset, "references"); + for (idx, (ns, def, context, coords, source)) in references.iter().enumerate() { + if !in_detail_window(idx, references.len(), detail_offset) { + println!( + " ⋯ {}/{} [{}] ({} path{})", + ns.dimmed(), + def.dimmed(), + source.dimmed(), + coords.len(), + if coords.len() == 1 { "" } else { "s" } + ); + continue; + } + // Show main line if !context.is_empty() { println!(" {}/{} [{}] {}", ns.cyan(), def, source.dimmed(), context.dimmed()); @@ -831,7 +1036,7 @@ fn handle_find(input_path: &str, symbol: &str, include_deps: bool) -> Result<(), let coords_parts: Vec = coords .iter() .map(|path| { - let coord_str = path.iter().map(|i| i.to_string()).collect::>().join(","); + let coord_str = format_path(path); format!("[{coord_str}]") }) .collect(); @@ -856,7 +1061,7 @@ fn handle_find(input_path: &str, symbol: &str, include_deps: bool) -> Result<(), } /// Find usages of a specific definition -fn handle_usages(input_path: &str, target_ns: &str, target_def: &str, include_deps: bool) -> Result<(), String> { +fn handle_usages(input_path: &str, target_ns: &str, target_def: &str, include_deps: bool, detail_offset: usize) -> Result<(), String> { let snapshot = load_snapshot(input_path)?; // Verify the target definition exists @@ -944,7 +1149,20 @@ fn handle_usages(input_path: &str, target_ns: &str, target_def: &str, include_de ); } else { println!(); - for (ns, def, context, coords, source) in &usages { + print_detail_window_hint(usages.len(), detail_offset, "usages"); + for (idx, (ns, def, context, coords, source)) in usages.iter().enumerate() { + if !in_detail_window(idx, usages.len(), detail_offset) { + println!( + " ⋯ {}/{} [{}] ({} path{})", + ns.dimmed(), + def.dimmed(), + source.dimmed(), + coords.len(), + if coords.len() == 1 { "" } else { "s" } + ); + continue; + } + // Show main line if !context.is_empty() { println!(" {}/{} [{}] {}", ns.cyan(), def.green(), source.dimmed(), context.dimmed()); @@ -957,7 +1175,7 @@ fn handle_usages(input_path: &str, target_ns: &str, target_def: &str, include_de let coords_parts: Vec = coords .iter() .map(|path| { - let coord_str = path.iter().map(|i| i.to_string()).collect::>().join(","); + let coord_str = format_path(path); format!("[{coord_str}]") }) .collect(); @@ -1049,7 +1267,7 @@ fn check_ns_imports(ns_code: &Cirru, target_ns: &str, _target_def: &str) -> bool /// Fuzzy search for namespace/definition by pattern /// Searches for `` in qualified names like `namespace/definition` -fn handle_fuzzy_search(input_path: &str, pattern: &str, include_deps: bool, limit: usize) -> Result<(), String> { +fn handle_fuzzy_search(input_path: &str, pattern: &str, include_deps: bool, limit: usize, detail_offset: usize) -> Result<(), String> { let snapshot = load_snapshot(input_path)?; let pattern_lower = pattern.to_lowercase(); @@ -1107,7 +1325,14 @@ fn handle_fuzzy_search(input_path: &str, pattern: &str, include_deps: bool, limi return Ok(()); } - for (ns, def, is_core) in &displayed { + print_detail_window_hint(displayed.len(), detail_offset, "search results"); + + for (idx, (ns, def, is_core)) in displayed.iter().enumerate() { + if !in_detail_window(idx, displayed.len(), detail_offset) { + println!(" ⋯ {}/{}", ns.dimmed(), def.dimmed()); + continue; + } + let qualified = format!("{}/{}", ns.cyan(), def.green()); if *is_core { println!(" {} {}", qualified, "(core)".dimmed()); @@ -1117,7 +1342,7 @@ fn handle_fuzzy_search(input_path: &str, pattern: &str, include_deps: bool, limi } if total > limit { - println!(" {} {} more results...", "...".dimmed(), total - limit); + println!(" ⋯ {} more results...", total - limit); } println!("\n{}", "Tip: Use `query def ` to view definition content.".dimmed()); @@ -1151,16 +1376,8 @@ fn fuzzy_match(text: &str, pattern: &str) -> bool { } /// Search for leaf nodes (strings) in a definition -fn handle_search_leaf( - input_path: &str, - pattern: &str, - filter: Option<&str>, - loose: bool, - max_depth: usize, - start_path: Option<&str>, - entry: Option<&str>, -) -> Result<(), String> { - let snapshot = load_snapshot_with_entry(input_path, entry)?; +fn handle_search_leaf(input_path: &str, pattern: &str, start_path: Option<&str>, common_opts: &SearchCommonOpts) -> Result<(), String> { + let snapshot = load_snapshot_with_entry(input_path, common_opts.entry)?; // Parse start_path if provided let parsed_start_path: Option> = if let Some(path_str) = start_path { @@ -1174,18 +1391,18 @@ fn handle_search_leaf( }; println!("{} Searching for:", "Search:".bold()); - if loose { + if common_opts.loose { println!(" {} (contains)", pattern.yellow()); } else { println!(" {} (exact)", pattern.yellow()); } - if let Some(filter_str) = filter { + if let Some(filter_str) = common_opts.filter { println!(" {} {}", "Filter:".dimmed(), filter_str.cyan()); } else { println!(" {} {}", "Scope:".dimmed(), "entire project".cyan()); } - if let Some(entry_name) = entry { + if let Some(entry_name) = common_opts.entry { println!(" {} {}", "Entry:".dimmed(), entry_name.cyan()); } @@ -1198,7 +1415,7 @@ fn handle_search_leaf( let mut all_results: SearchResults = Vec::new(); // Parse filter to determine scope - let (filter_ns, filter_def) = if let Some(f) = filter { + let (filter_ns, filter_def) = if let Some(f) = common_opts.filter { if f.contains('/') { let parts: Vec<&str> = f.split('/').collect(); if parts.len() == 2 { @@ -1255,7 +1472,7 @@ fn handle_search_leaf( }; let base_path = parsed_start_path.as_deref().unwrap_or(&[]); - let results = search_leaf_nodes(&search_root, pattern, loose, max_depth, base_path); + let results = search_leaf_nodes(&search_root, pattern, common_opts.loose, common_opts.max_depth, base_path); if !results.is_empty() { all_results.push((ns.clone(), def_name.clone(), results)); @@ -1267,6 +1484,8 @@ fn handle_search_leaf( if all_results.is_empty() { println!("{}", "No matches found.".yellow()); } else { + all_results.sort_by(|a, b| b.2.len().cmp(&a.2.len()).then_with(|| a.0.cmp(&b.0)).then_with(|| a.1.cmp(&b.1))); + let total_matches: usize = all_results.iter().map(|(_, _, results)| results.len()).sum(); println!( "{} {} match(es) found in {} definition(s):\n", @@ -1277,140 +1496,72 @@ fn handle_search_leaf( for (ns, def_name, results) in &all_results { println!("{} {}/{} ({} matches)", "●".cyan(), ns.dimmed(), def_name.green(), results.len()); + print_detail_window_hint(results.len(), common_opts.detail_offset, "matches"); // Load code_entry to print results if let Some(file_data) = snapshot.files.get(ns) { if let Some(code_entry) = file_data.defs.get(def_name) { - for (path, _node) in results.iter().take(20) { + let total = results.len(); + let (start, end) = detailed_window(common_opts.detail_offset, total); + let detailed_count = end.saturating_sub(start); + let compressed_count = total.saturating_sub(detailed_count); + + for (path, node) in results.iter().skip(start).take(detailed_count) { if path.is_empty() { - let content = code_entry.code.format_one_liner().unwrap_or_default(); - println!(" {} {}", "(root)".cyan(), content.dimmed()); + let (content, truncated) = preview_node_oneline(&code_entry.code, 110); + if truncated { + println!(" {} {} ⟪…⟫", "(root)".cyan(), content.dimmed()); + } else { + println!(" {} {}", "(root)".cyan(), content.dimmed()); + } } else { - let path_str = format!("[{}]", path.iter().map(|i| i.to_string()).collect::>().join(",")); - let breadcrumb = get_breadcrumb_from_code(&code_entry.code, path); - println!(" {} {}", path_str.cyan(), breadcrumb.dimmed()); - - // Get parent context - if let Some(parent) = get_parent_node_from_code(&code_entry.code, path) { - let parent_oneliner = parent.format_one_liner().unwrap_or_default(); - let display_parent = if parent_oneliner.len() > 80 { - format!("{}...", &parent_oneliner[..80]) - } else { - parent_oneliner - }; - println!(" {} {}", "in".dimmed(), display_parent.dimmed()); + let path_str = format!("[{}]", format_path(path)); + let ((expr_preview, expr_truncated), parent_previews) = + expression_and_parent_preview(&code_entry.code, path, node, Some(pattern), common_opts.loose); + let (display_preview, display_truncated) = parent_previews + .first() + .map(|(text, truncated)| (text.as_str(), *truncated)) + .unwrap_or((expr_preview.as_str(), expr_truncated)); + if display_truncated { + println!(" {} {} ⟪…⟫", path_str.cyan(), display_preview); + } else { + println!(" {} {}", path_str.cyan(), display_preview); } } } - if results.len() > 20 { - println!(" {}", format!("... and {} more", results.len() - 20).dimmed()); + if compressed_count > 0 { + println!(" {}", format!("{compressed_count} matches compressed outside window").dimmed()); } } } println!(); } - // Enhanced tips based on search context - println!("{}", "Next steps:".blue().bold()); - if all_results.len() == 1 && all_results[0].2.len() == 1 { - let (ns, def_name, results) = &all_results[0]; - let (path, _) = &results[0]; - let path_str = path.iter().map(|i| i.to_string()).collect::>().join(","); - println!(" • View node: {} '{}/{}' -p '{}'", "cr tree show".cyan(), ns, def_name, path_str); - println!( - " • Replace: {} '{}/{}' -p '{}' --leaf -e ''", - "cr tree replace".cyan(), - ns, - def_name, - path_str - ); - } else { - println!(" • View node: {} '' -p ''", "cr tree show".cyan()); - } - - // If single definition with multiple matches, suggest batch rename workflow - if all_results.len() == 1 { - let (_ns, _def_name, results) = &all_results[0]; - if results.len() > 1 { - println!(" • Batch replace: See tip below for renaming {} occurrences", results.len()); - } - } - - println!(); - - // Add batch rename tip for multiple matches in single definition - if all_results.len() == 1 && all_results[0].2.len() > 1 { - let (ns, def_name, results) = &all_results[0]; - println!("{}", "Tip for batch rename:".yellow().bold()); - println!(" Replace from largest index first to avoid path changes:"); - - // Show first command as example (in reverse order) - let mut sorted_results: Vec<_> = results.iter().collect(); - sorted_results.sort_by(|a, b| b.0.cmp(&a.0)); - - if let Some((path, _)) = sorted_results.first() { - let path_str = path.iter().map(|i| i.to_string()).collect::>().join(","); - println!( - " {} '{}/{}' -p '{}' --leaf -e ''", - "cr tree replace".cyan(), - ns, - def_name, - path_str - ); - } - - if results.len() > 1 { - println!(" {}", format!("... ({} more to replace)", results.len() - 1).dimmed()); - } - - println!(); - println!("{}", "⚠️ Important: Paths change after each modification!".yellow()); - println!( - "{}", + let mut tips = Tips::new(); + if total_matches > 10 && common_opts.loose { + tips.add_with_priority( + TipPriority::High, format!( - " Alternative: Re-search after each change: {} '{}' -f '{}/{}'", - "cr query search".cyan(), - pattern, - ns, - def_name - ) - .dimmed() - ); - } - - // Add quote reminder if pattern contains special characters - if pattern.contains('-') || pattern.contains('?') || pattern.contains('!') || pattern.contains('*') { - println!(); - println!( - "{}", - format!("Tip: Always use single quotes around names with special characters: '{pattern}'").dimmed() + "Many matches ({total_matches}); add {} to show exact matches only", + "--exact".yellow() + ), ); } + tips.print(); } Ok(()) } /// Search for structural expressions across project or in filtered scope -fn handle_search_expr( - input_path: &str, - pattern: &str, - filter: Option<&str>, - loose: bool, - max_depth: usize, - json: bool, - entry: Option<&str>, -) -> Result<(), String> { - let snapshot = load_snapshot_with_entry(input_path, entry)?; +fn handle_search_expr(input_path: &str, pattern: &str, json: bool, common_opts: &SearchCommonOpts) -> Result<(), String> { + let snapshot = load_snapshot_with_entry(input_path, common_opts.entry)?; - // Parse pattern let pattern_node = if json { - // Parse as JSON array let json_val: serde_json::Value = serde_json::from_str(pattern).map_err(|e| format!("Failed to parse JSON pattern: {e}"))?; json_to_cirru(&json_val)? } else { - // Parse as Cirru one-liner cirru_parser::parse(pattern) .map_err(|e| format!("Failed to parse Cirru pattern: {e}"))? .first() @@ -1421,26 +1572,30 @@ fn handle_search_expr( println!("{} Searching for pattern:", "Search:".bold()); let pattern_display = pattern_node.format_one_liner().unwrap_or_default(); - if loose { + let highlight_target: Option<&str> = match &pattern_node { + Cirru::Leaf(s) => Some(s.as_ref()), + _ => None, + }; + + if common_opts.loose { println!(" {} (substring match for leaf patterns)", pattern_display.yellow()); } else { println!(" {} (exact match)", pattern_display.yellow()); } - if let Some(filter_str) = filter { + if let Some(filter_str) = common_opts.filter { println!(" {} {}", "Filter:".dimmed(), filter_str.cyan()); } else { println!(" {} {}", "Scope:".dimmed(), "entire project".cyan()); } - if let Some(entry_name) = entry { + if let Some(entry_name) = common_opts.entry { println!(" {} {}", "Entry:".dimmed(), entry_name.cyan()); } println!(); let mut all_results: SearchResults = Vec::new(); - // Parse filter to determine scope - let (filter_ns, filter_def) = if let Some(f) = filter { + let (filter_ns, filter_def) = if let Some(f) = common_opts.filter { if f.contains('/') { let parts: Vec<&str> = f.split('/').collect(); if parts.len() == 2 { @@ -1455,36 +1610,32 @@ fn handle_search_expr( (None, None) }; - // Search through files for (ns, file_data) in &snapshot.files { - // Skip if namespace doesn't match filter - if let Some(filter_namespace) = filter_ns { - if ns != filter_namespace { - continue; - } + if let Some(filter_namespace) = filter_ns + && ns != filter_namespace + { + continue; } - // Search through definitions in this namespace for (def_name, code_entry) in &file_data.defs { - // Skip if definition doesn't match filter - if let Some(filter_definition) = filter_def { - if def_name != filter_definition { - continue; - } + if let Some(filter_definition) = filter_def + && def_name != filter_definition + { + continue; } - let results = search_expr_nodes(&code_entry.code, &pattern_node, loose, max_depth, &[]); - + let results = search_expr_nodes(&code_entry.code, &pattern_node, common_opts.loose, common_opts.max_depth, &[]); if !results.is_empty() { all_results.push((ns.clone(), def_name.clone(), results)); } } } - // Print results grouped by namespace/definition if all_results.is_empty() { println!("{}", "No matches found.".yellow()); } else { + all_results.sort_by(|a, b| b.2.len().cmp(&a.2.len()).then_with(|| a.0.cmp(&b.0)).then_with(|| a.1.cmp(&b.1))); + let total_matches: usize = all_results.iter().map(|(_, _, results)| results.len()).sum(); println!( "{} {} match(es) found in {} definition(s):\n", @@ -1495,93 +1646,59 @@ fn handle_search_expr( for (ns, def_name, results) in &all_results { println!("{} {}/{} ({} matches)", "●".cyan(), ns.dimmed(), def_name.green(), results.len()); + print_detail_window_hint(results.len(), common_opts.detail_offset, "matches"); - // Load code_entry to print results - if let Some(file_data) = snapshot.files.get(ns) { - if let Some(code_entry) = file_data.defs.get(def_name) { - for (path, _node) in results.iter().take(20) { - let path_str = path.iter().map(|i| i.to_string()).collect::>().join(","); - if path.is_empty() { - let content = code_entry.code.format_one_liner().unwrap_or_default(); + if let Some(file_data) = snapshot.files.get(ns) + && let Some(code_entry) = file_data.defs.get(def_name) + { + let total = results.len(); + let (start, end) = detailed_window(common_opts.detail_offset, total); + let detailed_count = end.saturating_sub(start); + let compressed_count = total.saturating_sub(detailed_count); + + for (path, node) in results.iter().skip(start).take(detailed_count) { + let path_str = format_path(path); + if path.is_empty() { + let (content, truncated) = preview_node_oneline(&code_entry.code, 110); + if truncated { + println!(" {} {} ⟪…⟫", "(root)".cyan(), content.dimmed()); + } else { println!(" {} {}", "(root)".cyan(), content.dimmed()); + } + } else { + let ((expr_preview, expr_truncated), parent_previews) = + expression_and_parent_preview(&code_entry.code, path, node, highlight_target, common_opts.loose); + let (display_preview, display_truncated) = parent_previews + .first() + .map(|(text, truncated)| (text.as_str(), *truncated)) + .unwrap_or((expr_preview.as_str(), expr_truncated)); + if display_truncated { + println!(" {} {} ⟪…⟫", format!("[{path_str}]").cyan(), display_preview); } else { - let breadcrumb = get_breadcrumb_from_code(&code_entry.code, path); - println!(" {} path: '{}' context: {}", "•".cyan(), path_str, breadcrumb.dimmed()); - - // Get parent context - if let Some(parent) = get_parent_node_from_code(&code_entry.code, path) { - let parent_oneliner = parent.format_one_liner().unwrap_or_default(); - let display_parent = if parent_oneliner.len() > 80 { - format!("{}...", &parent_oneliner[..80]) - } else { - parent_oneliner - }; - println!(" {} {}", "in:".dimmed(), display_parent.dimmed()); - } + println!(" {} {}", format!("[{path_str}]").cyan(), display_preview); } } + } - if results.len() > 20 { - println!(" {}", format!("... and {} more", results.len() - 20).dimmed()); - } + if compressed_count > 0 { + println!(" {}", format!("{compressed_count} matches compressed outside window").dimmed()); } } + println!(); } - // Enhanced tips based on search context - println!("{}", "Next steps:".blue().bold()); - if all_results.len() == 1 && all_results[0].2.len() == 1 { - let (ns, def_name, results) = &all_results[0]; - let (path, _) = &results[0]; - let path_str = path.iter().map(|i| i.to_string()).collect::>().join(","); - println!(" • View node: {} '{}/{}' -p '{}'", "cr tree show".cyan(), ns, def_name, path_str); - println!( - " • Replace: {} '{}/{}' -p '{}' -e ''", - "cr tree replace".cyan(), - ns, - def_name, - path_str + let mut tips = Tips::new(); + if total_matches > 10 && common_opts.loose { + tips.add_with_priority( + TipPriority::High, + format!( + "Many matches ({total_matches}); add {} to show exact matches only", + "--exact".yellow() + ), ); - } else { - println!(" • View node: {} '' -p ''", "cr tree show".cyan()); - } - - // If single definition with multiple matches, suggest batch replace workflow - if all_results.len() == 1 { - let (_ns, _def_name, results) = &all_results[0]; - if results.len() > 1 { - println!(" • Batch replace: See tip below for renaming {} occurrences", results.len()); - } - } - - println!(); - - // Add batch replace tip for multiple matches in single definition - if all_results.len() == 1 && all_results[0].2.len() > 1 { - let (ns, def_name, results) = &all_results[0]; - println!("{}", "Tip for batch replace:".yellow().bold()); - println!(" Replace from largest index first to avoid path changes:"); - - // Show first command as example (in reverse order) - let mut sorted_results: Vec<_> = results.iter().collect(); - sorted_results.sort_by(|a, b| b.0.cmp(&a.0)); - - if let Some((path, _)) = sorted_results.first() { - let path_str = path.iter().map(|i| i.to_string()).collect::>().join(","); - println!( - " {} '{}/{}' -p '{}' -e ''", - "cr tree replace".cyan(), - ns, - def_name, - path_str - ); - } - - if results.len() > 1 { - println!(" {}", format!("... ({} more to replace)", results.len() - 1).dimmed()); - } } + tips.print(); } Ok(()) @@ -1637,70 +1754,9 @@ fn search_leaf_nodes(node: &Cirru, pattern: &str, loose: bool, max_depth: usize, results } -/// Helper function to get parent node from code given a path -fn get_parent_node_from_code(code: &Cirru, path: &[usize]) -> Option { - if path.is_empty() { - return None; - } - let parent_path = &path[..path.len() - 1]; - if parent_path.is_empty() { - return Some(code.clone()); - } - - let mut current = code; - for &idx in parent_path { - if let Cirru::List(items) = current { - current = items.get(idx)?; - } else { - return None; - } - } - Some(current.clone()) -} - -fn get_breadcrumb_from_code(code: &Cirru, path: &[usize]) -> String { - let mut parts = Vec::new(); - let mut current = code; - - parts.push(preview_cirru_head(current)); - - for &idx in path { - if let Cirru::List(items) = current { - if let Some(next) = items.get(idx) { - current = next; - parts.push(preview_cirru_head(current)); - } else { - break; - } - } else { - break; - } - } - - parts.join(" → ") -} - -fn preview_cirru_head(node: &Cirru) -> String { - match node { - Cirru::Leaf(s) => s.to_string(), - Cirru::List(items) => { - if items.is_empty() { - "()".to_string() - } else { - match &items[0] { - Cirru::Leaf(s) => s.to_string(), - Cirru::List(_) => "(...)".to_string(), - } - } - } - } -} - -/// Search for expression nodes (structural matching) fn search_expr_nodes(node: &Cirru, pattern: &Cirru, loose: bool, max_depth: usize, current_path: &[usize]) -> Vec<(Vec, Cirru)> { let mut results = Vec::new(); - // Check depth limit if max_depth > 0 && current_path.len() >= max_depth { return results; } diff --git a/src/cli_args.rs b/src/cli_args.rs index 1d7a47e5..5b45d01a 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -379,6 +379,9 @@ pub struct QueryFindCommand { /// maximum number of results (default 20) #[argh(option, short = 'n', default = "20")] pub limit: usize, + /// start index for detailed display window (3 detailed items) + #[argh(option, long = "detail-offset", default = "0")] + pub detail_offset: usize, } #[derive(FromArgs, PartialEq, Debug, Clone)] @@ -391,6 +394,9 @@ pub struct QueryUsagesCommand { /// include dependency namespaces in search #[argh(switch)] pub deps: bool, + /// start index for detailed display window (3 detailed items) + #[argh(option, long = "detail-offset", default = "0")] + pub detail_offset: usize, } #[derive(FromArgs, PartialEq, Debug, Clone)] @@ -409,12 +415,15 @@ pub struct QuerySearchCommand { /// maximum search depth (0 = unlimited) #[argh(option, short = 'd', default = "0")] pub max_depth: usize, - /// start search from specific path (comma-separated indices, e.g. "2,1,0") + /// start search from specific path (dot-separated indices preferred, e.g. "2.1.0") #[argh(option, short = 'p', long = "start-path")] pub start_path: Option, /// include modules configured for a specific entry in `entries` #[argh(option, long = "entry")] pub entry: Option, + /// start index for detailed display window (3 detailed items) + #[argh(option, long = "detail-offset", default = "0")] + pub detail_offset: usize, } #[derive(FromArgs, PartialEq, Debug, Clone)] @@ -439,6 +448,9 @@ pub struct QuerySearchExprCommand { /// include modules configured for a specific entry in `entries` #[argh(option, long = "entry")] pub entry: Option, + /// start index for detailed display window (3 detailed items) + #[argh(option, long = "detail-offset", default = "0")] + pub detail_offset: usize, } // ═══════════════════════════════════════════════════════════════════════════════ From e9272b79743ed0d7de5d235e7e18403f2ff8d219 Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 20 Mar 2026 15:09:26 +0800 Subject: [PATCH 30/57] Add analyze js-escape helpers and document usage --- docs/CalcitAgent.md | 1 + src/bin/cr.rs | 14 ++++++++++ src/cli_args.rs | 26 ++++++++++++++++-- src/codegen/emit_js.rs | 8 ++++++ src/codegen/emit_js/symbols.rs | 50 ++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 2 deletions(-) diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index 099dd53d..6d8300de 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -10,6 +10,7 @@ - 看某个定义的完整实现:`cr query def ` - 找关键词并拿可编辑路径:`cr query search -f ` - 跨命名空间找符号:`cr query find `(默认就是 fuzzy;需要精确匹配时加 `--exact`) +- 调试 JS 变量改名:`cr analyze js-escape ''` / `cr analyze js-unescape ''`(`js-unescape` 当前为 best-effort) - 查进阶手册某个主题:`cr docs read agent-advanced.md ` - 看进阶手册全文:`cr docs read agent-advanced.md --full` diff --git a/src/bin/cr.rs b/src/bin/cr.rs index 12f21cb2..ee5eab87 100644 --- a/src/bin/cr.rs +++ b/src/bin/cr.rs @@ -252,6 +252,8 @@ fn main() -> Result<(), String> { AnalyzeSubcommand::CountCalls(count_call_options) => run_count_calls(&entries, count_call_options), AnalyzeSubcommand::CheckExamples(check_options) => run_check_examples(&check_options.ns, &snapshot), AnalyzeSubcommand::CheckTypes(check_types_options) => run_check_types(check_types_options, &snapshot), + AnalyzeSubcommand::JsEscape(options) => run_js_escape(&options.symbol), + AnalyzeSubcommand::JsUnescape(options) => run_js_unescape(&options.symbol), } } else { if !cli_args.watch { @@ -291,6 +293,18 @@ fn main() -> Result<(), String> { Ok(()) } +fn run_js_escape(symbol: &str) -> Result<(), String> { + let escaped = calcit::codegen::emit_js::escape_symbol_for_js(symbol); + println!("{escaped}"); + Ok(()) +} + +fn run_js_unescape(symbol: &str) -> Result<(), String> { + let restored = calcit::codegen::emit_js::unescape_symbol_from_js(symbol); + println!("{restored}"); + Ok(()) +} + pub fn watch_files(entries: ProgramEntries, settings: ToplevelCalcit, assets_watch: Option) { println!("\nRunning: in watch mode...\n"); let (tx, rx) = channel(); diff --git a/src/cli_args.rs b/src/cli_args.rs index 5b45d01a..60c361b5 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -69,7 +69,7 @@ pub enum CalcitCommand { EmitIr(EmitIrCommand), /// evaluate snippet Eval(EvalCommand), - /// analyze code structure (call-graph, count-calls, check-examples) + /// analyze code structure and helpers (call-graph, count-calls, check-examples) Analyze(AnalyzeCommand), /// query project information (namespaces, definitions, configs) Query(QueryCommand), @@ -130,7 +130,7 @@ pub struct EvalCommand { #[derive(FromArgs, PartialEq, Debug, Clone)] #[argh(subcommand, name = "analyze")] -/// analyze code structure (call-graph, count-calls, check-examples, check-types) +/// analyze code structure and helpers (call-graph, count-calls, check-examples, check-types, js-escape) pub struct AnalyzeCommand { #[argh(subcommand)] pub subcommand: AnalyzeSubcommand, @@ -147,6 +147,28 @@ pub enum AnalyzeSubcommand { CheckExamples(CheckExamplesCommand), /// check type-information coverage in namespace definitions CheckTypes(CheckTypesCommand), + /// escape a Calcit symbol into JavaScript-safe identifier form + JsEscape(JsEscapeCommand), + /// decode escaped JavaScript identifier back to Calcit symbol (best-effort) + JsUnescape(JsUnescapeCommand), +} + +/// escape a Calcit symbol into JavaScript-safe identifier form +#[derive(FromArgs, PartialEq, Debug, Clone)] +#[argh(subcommand, name = "js-escape")] +pub struct JsEscapeCommand { + /// original Calcit symbol + #[argh(positional)] + pub symbol: String, +} + +/// decode escaped JavaScript identifier back to Calcit symbol (best-effort) +#[derive(FromArgs, PartialEq, Debug, Clone)] +#[argh(subcommand, name = "js-unescape")] +pub struct JsUnescapeCommand { + /// escaped JavaScript identifier + #[argh(positional)] + pub symbol: String, } /// check type-information coverage in namespace definitions diff --git a/src/codegen/emit_js.rs b/src/codegen/emit_js.rs index c8df60f0..f21f3720 100644 --- a/src/codegen/emit_js.rs +++ b/src/codegen/emit_js.rs @@ -36,6 +36,14 @@ use paths::{to_js_import_name, to_mjs_filename}; use runtime::{get_proc_prefix, is_cirru_string}; use symbols::{escape_cirru_str, escape_var}; +pub fn escape_symbol_for_js(name: &str) -> String { + escape_var(name) +} + +pub fn unescape_symbol_from_js(name: &str) -> String { + symbols::unescape_var(name) +} + thread_local! { static INLINE_ALL_ARGS: Cell = const { Cell::new(false) }; } diff --git a/src/codegen/emit_js/symbols.rs b/src/codegen/emit_js/symbols.rs index d2e3a5b0..bd034d33 100644 --- a/src/codegen/emit_js/symbols.rs +++ b/src/codegen/emit_js/symbols.rs @@ -54,6 +54,49 @@ pub(super) fn escape_cirru_str(s: &str) -> String { result } +pub(crate) fn unescape_var(name: &str) -> String { + match name { + "_IF_" => return String::from("if"), + "_DO_" => return String::from("do"), + "_ELSE_" => return String::from("else"), + "_LET_" => return String::from("let"), + "_CASE_" => return String::from("case"), + "_SUB_" => return String::from("-"), + _ => {} + } + + let mut decoded = name.to_string(); + for (from, to) in [ + ("_$q_", "?"), + ("_ADD_", "+"), + ("_CRT_", "^"), + ("_$s_", "*"), + ("_$n_", "&"), + ("_$M_", "{}"), + ("_$L_", "[]"), + ("_CURL_", "{"), + ("_CURR_", "}"), + ("_SQUO_", "'"), + ("_SQRL_", "["), + ("_SQRR_", "]"), + ("_$x_", "!"), + ("_PCT_", "%"), + ("_SLSH_", "/"), + ("_$e_", "="), + ("_GT_", ">"), + ("_LT_", "<"), + ("_$o_", ":"), + ("_SCOL_", ";"), + ("_SHA_", "#"), + ("_BSL_", "\\"), + ("_DOT_", "."), + ] { + decoded = decoded.replace(from, to); + } + + decoded +} + #[cfg(test)] mod tests { use super::*; @@ -69,4 +112,11 @@ mod tests { fn escapes_cirru_string_chars() { assert_eq!(escape_cirru_str("a\nb\t\\\""), "\"a\\nb\\t\\\\\\\"\""); } + + #[test] + fn unescapes_symbols_best_effort() { + assert_eq!(unescape_var("a_b_$q_"), "a_b?"); + assert_eq!(unescape_var("_DOT_"), "."); + assert_eq!(unescape_var("_IF_"), "if"); + } } From d79b7e5b592bed446e1dbf92ae1fdaf5cd7a83c0 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 22 Mar 2026 00:42:38 +0800 Subject: [PATCH 31/57] fix path format test --- src/bin/cli_handlers/common.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bin/cli_handlers/common.rs b/src/bin/cli_handlers/common.rs index 42df97be..7dfab80f 100644 --- a/src/bin/cli_handlers/common.rs +++ b/src/bin/cli_handlers/common.rs @@ -327,8 +327,9 @@ mod tests { use super::{format_path, format_path_bracketed, format_path_with_separator, parse_path}; #[test] - fn parses_comma_separated_paths() { - assert_eq!(parse_path("3,2,1").unwrap(), vec![3, 2, 1]); + fn rejects_comma_separated_paths() { + let err = parse_path("3,2,1").unwrap_err(); + assert!(err.contains("comma separator is no longer supported")); } #[test] From 8ca1a36e70e76b7cf5d3f23d67b7121b29e6d7ac Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 23 Mar 2026 01:32:37 +0800 Subject: [PATCH 32/57] Refine tree show chunks --- docs/CalcitAgent.md | 6 +- docs/run/agent-advanced.md | 5 +- ...320-0116-json-builtins-and-runtime-docs.md | 3 + src/bin/cli_handlers/chunk_display.rs | 61 +++++++++++++++++- src/bin/cli_handlers/tree.rs | 62 +++++++++++++------ src/cli_args.rs | 3 + 6 files changed, 114 insertions(+), 26 deletions(-) diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index 6d8300de..91ea0cab 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -204,7 +204,7 @@ cr docs agents --full 1. 定位目标定义:`cr query defs ` 2. 先轻看再全看:`cr query peek `,必要时再 `cr query def ` 3. 搜关键词拿路径:`cr query search -f ` -4. 聚焦子树确认上下文:`cr tree show -p ''`(复杂时加 `-j`) +4. 聚焦子树确认上下文:`cr tree show -p ''`(复杂时可加 `-j`;大表达式默认只展开 ROOT + 一层 chunks,需要更多时加 `--chunk-expand-depth 2`) 5. 修改并验证:`cr tree replace ...` 或 `cr edit inc --changed `,然后 `cr js` ### 示例(大函数) @@ -227,7 +227,7 @@ cr js - `cr query defs `:列出命名空间定义。 - `cr query def `:查看定义(默认 Cirru)。 - `cr query search -f `:按关键词拿路径。 -- `cr tree show -p ''`:查看局部子树。 +- `cr tree show -p ''`:查看局部子树;大表达式默认只显示 ROOT 与直接 chunk,继续展开时使用 `--chunk-expand-depth `。 ### 编辑 @@ -250,7 +250,7 @@ cr src/cirru/calcit-core.cirru edit format 优先规则: -- 只改 1~3 个节点:优先 `cr tree` 系列。 +- 只改 1~10 个节点:优先 `cr tree` 系列。 - 仅改文本/叶子:优先 `target-replace` 或 `replace-leaf`。 - 只调单层结构:优先 `insert-*` / `delete` / `swap-*` / `wrap` / `raise`。 - 仅在“整段重写/新增定义/大范围重构”时,才用 `cr edit def --overwrite -f`。 diff --git a/docs/run/agent-advanced.md b/docs/run/agent-advanced.md index 68b0e831..3112b9c5 100644 --- a/docs/run/agent-advanced.md +++ b/docs/run/agent-advanced.md @@ -322,7 +322,8 @@ cr query modules **主要操作:** - `cr tree show -p '' [-j]` - 查看节点 - - 默认输出:节点类型、Cirru 预览、子节点索引列表、操作提示 + - 默认输出:节点类型、Cirru 预览、操作提示 + - 大表达式分片时默认只展开 ROOT 与直接 chunk;需要继续展开嵌套 chunk 时加 `--chunk-expand-depth ` - `-j` / `--json`:同时输出 JSON 格式(用于程序化处理) - 推荐:直接查看 Cirru 格式即可,通常不需要 JSON - `cr tree replace` - 替换节点 @@ -1282,7 +1283,7 @@ send-to-component! $ :: :clipboard/read text **大资源处理建议:** 如果需要修改复杂的长函数,不要尝试一次性替换整个定义。应先构建主体结构,使用占位符,统一写成 `{{PLACEHOLDER_FEATURE}}` 这种花括号形式,并注意避免重复,然后通过 `cr tree target-replace` 或按路径的 `cr tree replace` 做精准的分段替换。 -补充提示:现在 `cr query def` 和 `cr tree show` 遇到大表达式时会自动输出分片结果。若你采用多阶段创建,建议从第一步就使用 `{{NAME}}` 风格占位符,这样后续在分片视图中更容易识别骨架、复制坐标并继续填充内容。 +补充提示:现在 `cr query def` 和 `cr tree show` 遇到大表达式时会自动输出分片结果。`tree show` 默认只展开 ROOT 与一层 chunk;若需要继续查看 chunk 中的 chunk,可显式增加 `--chunk-expand-depth`。若你采用多阶段创建,建议从第一步就使用 `{{NAME}}` 风格占位符,这样后续在分片视图中更容易识别骨架、复制坐标并继续填充内容。 ### 5. 命名空间操作陷阱 ⭐⭐⭐ diff --git a/editing-history/2026-0320-0116-json-builtins-and-runtime-docs.md b/editing-history/2026-0320-0116-json-builtins-and-runtime-docs.md index 885f8386..895cd38f 100644 --- a/editing-history/2026-0320-0116-json-builtins-and-runtime-docs.md +++ b/editing-history/2026-0320-0116-json-builtins-and-runtime-docs.md @@ -3,17 +3,20 @@ This commit moves JSON parsing and serialization into Calcit builtins and aligns the surrounding runtime/docs/tooling updates needed to ship it cleanly. 1. Added builtin JSON runtime functions + - Introduced `json-parse`, `json-stringify`, and `json-pretty` in Rust builtin dispatch and JS runtime exports. - Added fast arity/type checks on exposed runtime entry points. - Normalized integer-valued numbers to encode as JSON integers in Rust, matching JS output. - Added native tests and Calcit-level coverage for parse/stringify/pretty behavior and error paths. 2. Updated core docs and runtime placeholder naming + - Added `calcit.core` docs/examples for the JSON runtime functions. - Renamed the runtime placeholder spelling from `runtime-inplementation` to `runtime-implementation` across core snapshot metadata and Rust handling. - Preserved an explicit `cr edit format` example in `docs/CalcitAgent.md`. 3. Snapshot formatting workflow + - Re-ran `cr edit format` against the touched Cirru snapshot files: - `src/cirru/calcit-core.cirru` - `calcit/test.cirru` diff --git a/src/bin/cli_handlers/chunk_display.rs b/src/bin/cli_handlers/chunk_display.rs index c16e7ca9..b46295fa 100644 --- a/src/bin/cli_handlers/chunk_display.rs +++ b/src/bin/cli_handlers/chunk_display.rs @@ -66,6 +66,7 @@ pub struct ChunkedDisplay { pub struct RenderedFragment { pub id: String, pub coord: String, + pub path: Vec, pub nodes: usize, pub depth: usize, pub cirru: String, @@ -180,6 +181,7 @@ fn build_ordered_decomposition(fragments: &[PendingFragment]) -> Result Result usize { + if fragment.path.is_empty() { + return 0; + } + + 1 + fragments + .iter() + .filter(|candidate| { + !candidate.path.is_empty() && candidate.path.len() < fragment.path.len() && fragment.path.starts_with(&candidate.path) + }) + .count() +} + fn pick_best_semantic_cut(root: &Cirru, options: &ChunkDisplayOptions) -> Option { let min_nodes = usize::max(6, (options.target_nodes as f64 * 0.65).floor() as usize); let profile = build_profile(root); @@ -510,16 +525,60 @@ fn format_cirru_fragment(node: &Cirru) -> Result { #[cfg(test)] mod tests { - use super::{ChunkDisplayOptions, maybe_chunk_node}; + use super::{ChunkDisplayOptions, RenderedFragment, fragment_nesting_level, maybe_chunk_node}; fn parse_first(text: &str) -> cirru_parser::Cirru { cirru_parser::parse(text).unwrap().into_iter().next().unwrap() } + fn fragment(id: &str, path: &[usize]) -> RenderedFragment { + RenderedFragment { + id: id.to_string(), + coord: if path.is_empty() { + "root".to_string() + } else { + path.iter().map(|idx| idx.to_string()).collect::>().join(".") + }, + path: path.to_vec(), + nodes: 1, + depth: 0, + cirru: id.to_string(), + } + } + #[test] fn does_not_chunk_small_nodes() { let node = parse_first("defn add (a b) &+ a b"); let options = ChunkDisplayOptions::default(); assert!(maybe_chunk_node(&node, &options).unwrap().is_none()); } + + #[test] + fn fragment_levels_hide_nested_chunks_by_default() { + let fragments = vec![ + fragment("ROOT", &[]), + fragment("L1_A", &[3]), + fragment("L1_B", &[5]), + fragment("L2_A", &[3, 2]), + fragment("L3_A", &[3, 2, 1]), + ]; + + assert_eq!(fragment_nesting_level(&fragments[0], &fragments), 0); + assert_eq!(fragment_nesting_level(&fragments[1], &fragments), 1); + assert_eq!(fragment_nesting_level(&fragments[2], &fragments), 1); + assert_eq!(fragment_nesting_level(&fragments[3], &fragments), 2); + assert_eq!(fragment_nesting_level(&fragments[4], &fragments), 3); + + let visible_default = fragments + .iter() + .filter(|fragment| fragment_nesting_level(fragment, &fragments) <= 1) + .count(); + let visible_deeper = fragments + .iter() + .filter(|fragment| fragment_nesting_level(fragment, &fragments) <= 2) + .count(); + + assert_eq!(visible_default, 3); + assert_eq!(visible_deeper, 4); + } } diff --git a/src/bin/cli_handlers/tree.rs b/src/bin/cli_handlers/tree.rs index 88799bc0..d707ca0e 100644 --- a/src/bin/cli_handlers/tree.rs +++ b/src/bin/cli_handlers/tree.rs @@ -1,7 +1,7 @@ use cirru_parser::Cirru; use colored::Colorize; -use super::chunk_display::{ChunkDisplayOptions, ChunkedDisplay, maybe_chunk_node}; +use super::chunk_display::{ChunkDisplayOptions, ChunkedDisplay, fragment_nesting_level, maybe_chunk_node}; use super::common::{ ERR_CODE_INPUT_REQUIRED, cirru_to_json, format_path, format_path_bracketed, parse_input_to_cirru, parse_path, read_code_input, }; @@ -172,7 +172,7 @@ fn show_diff_preview(old_node: &Cirru, new_node: &Cirru, operation: &str, path: output } -fn render_chunked_display(display: &ChunkedDisplay) { +fn render_chunked_display(display: &ChunkedDisplay, chunk_expand_depth: usize) -> usize { println!("{}", "Chunked preview".green().bold()); println!( "{}", @@ -188,7 +188,27 @@ fn render_chunked_display(display: &ChunkedDisplay) { ); println!(); - for fragment in &display.fragments { + let visible_fragments: Vec<_> = display + .fragments + .iter() + .filter(|fragment| fragment_nesting_level(fragment, &display.fragments) <= chunk_expand_depth) + .collect(); + + if visible_fragments.len() < display.fragments.len() { + println!( + "{}", + format!( + "showing {}/{} fragments; nested chunks beyond level {} are hidden", + visible_fragments.len(), + display.fragments.len(), + chunk_expand_depth + ) + .dimmed() + ); + println!(); + } + + for fragment in &visible_fragments { println!("{} {}", fragment.id.cyan().bold(), format!("at {}", fragment.coord).dimmed()); println!("{}", format!("nodes: {}, max depth: {}", fragment.nodes, fragment.depth).dimmed()); for line in fragment.cirru.lines() { @@ -196,6 +216,8 @@ fn render_chunked_display(display: &ChunkedDisplay) { } println!(); } + + visible_fragments.len() } // ============================================================================ @@ -345,8 +367,8 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> println!("{}: {} ({} items)", "Type".green().bold(), "list".yellow(), items.len()); println!(); - if let Some(display) = chunked_display { - render_chunked_display(&display); + let shown_fragments = if let Some(display) = chunked_display { + Some((render_chunked_display(&display, opts.chunk_expand_depth), display.fragments.len())) } else { println!("{}:", "Cirru preview".green().bold()); println!(" "); @@ -356,21 +378,8 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> println!(" {line}"); } println!(); - } - - if !items.is_empty() { - println!("{}:", "Children".green().bold()); - for (i, item) in items.iter().enumerate() { - let type_str = format_child_preview(item); - let child_path = if opts.path.is_empty() { - i.to_string() - } else { - format!("{}.{}", format_path(&path), i) - }; - println!(" [{}] {} {} -p '{}'", i, type_str.yellow(), "->".dimmed(), child_path); - } - println!(); - } + None + }; if show_json { println!("{}:", "JSON".green().bold()); @@ -397,6 +406,19 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> ); println!(); let mut tips = Tips::new(); + if let Some((shown_fragments, total_fragments)) = shown_fragments { + if shown_fragments < total_fragments { + tips.add_with_priority( + TipPriority::High, + format!( + "Showing ROOT plus {} chunk layer(s). Use {} to reveal deeper nested fragments, or {} to disable chunking.", + opts.chunk_expand_depth, + format!("--chunk-expand-depth {}", opts.chunk_expand_depth + 1).yellow(), + "--raw".yellow() + ), + ); + } + } tips.append(tip_prefer_oneliner_json(show_json)); tips.print(); diff --git a/src/cli_args.rs b/src/cli_args.rs index 60c361b5..fab6fd28 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -1172,6 +1172,9 @@ pub struct TreeShowCommand { /// only enable chunked display when total expression nodes reach this threshold #[argh(option, default = "88")] pub chunk_trigger_nodes: usize, + /// nested chunk layers to expand beyond ROOT (default 1 shows ROOT + direct chunks only) + #[argh(option, default = "1")] + pub chunk_expand_depth: usize, /// force raw subtree display without chunking #[argh(switch)] pub raw: bool, From fc2b3cb913d92bb17362023b432d7cf8c5c848db Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 23 Mar 2026 10:56:24 +0800 Subject: [PATCH 33/57] Improve command echo output --- src/bin/cli_handlers/command_echo.rs | 558 +++++++++++++++++++++++++++ src/bin/cli_handlers/docs.rs | 35 +- src/bin/cli_handlers/edit.rs | 64 +-- src/bin/cli_handlers/mod.rs | 4 +- src/bin/cli_handlers/query.rs | 54 +-- src/bin/cli_handlers/tips.rs | 10 + src/bin/cli_handlers/tree.rs | 132 ++++--- src/bin/cr.rs | 11 +- 8 files changed, 734 insertions(+), 134 deletions(-) create mode 100644 src/bin/cli_handlers/command_echo.rs diff --git a/src/bin/cli_handlers/command_echo.rs b/src/bin/cli_handlers/command_echo.rs new file mode 100644 index 00000000..a6d947f8 --- /dev/null +++ b/src/bin/cli_handlers/command_echo.rs @@ -0,0 +1,558 @@ +use calcit::cli_args::*; +use colored::Colorize; + +macro_rules! echo_items { + ($tokens:expr $(,)?) => {}; + ($tokens:expr, pos $name:literal => $value:expr $(, $($rest:tt)*)?) => {{ + push_positional($tokens, $name, $value); + echo_items!($tokens $(, $($rest)*)?); + }}; + ($tokens:expr, switch $name:literal => $value:expr $(, $($rest:tt)*)?) => {{ + push_switch($tokens, $name, $value); + echo_items!($tokens $(, $($rest)*)?); + }}; + ($tokens:expr, value $name:literal => $value:expr ; default $default:expr $(, $($rest:tt)*)?) => {{ + let value = ($value).to_string(); + push_value($tokens, $name, &value, Some($default)); + echo_items!($tokens $(, $($rest)*)?); + }}; + ($tokens:expr, value $name:literal => $value:expr $(, $($rest:tt)*)?) => {{ + let value = ($value).to_string(); + push_value($tokens, $name, &value, None); + echo_items!($tokens $(, $($rest)*)?); + }}; + ($tokens:expr, opt $name:literal => $value:expr ; default $default:expr $(, $($rest:tt)*)?) => {{ + push_optional($tokens, $name, $value, $default); + echo_items!($tokens $(, $($rest)*)?); + }}; + ($tokens:expr, opt_owned $name:literal => $value:expr ; default $default:expr $(, $($rest:tt)*)?) => {{ + push_optional_owned($tokens, $name, $value, $default); + echo_items!($tokens $(, $($rest)*)?); + }}; + ($tokens:expr, list $name:literal => $value:expr $(, $($rest:tt)*)?) => {{ + push_list($tokens, $name, $value); + echo_items!($tokens $(, $($rest)*)?); + }}; + ($tokens:expr, code_input $opts:expr $(, $($rest:tt)*)?) => {{ + let opts = &$opts; + push_code_input( + $tokens, + &CodeInputParts::new( + opts.file.as_deref(), + opts.code.as_deref(), + opts.json.as_deref(), + opts.json_input, + opts.leaf, + ), + ); + echo_items!($tokens $(, $($rest)*)?); + }}; +} + +pub fn should_echo_command(cli_args: &ToplevelCalcit) -> bool { + matches!( + cli_args.subcommand, + Some(CalcitCommand::Query(_)) + | Some(CalcitCommand::Docs(_)) + | Some(CalcitCommand::Edit(_)) + | Some(CalcitCommand::Tree(_)) + | Some(CalcitCommand::Analyze(_)) + | Some(CalcitCommand::Cirru(_)) + ) +} + +pub fn print_command_echo(cli_args: &ToplevelCalcit) { + let Some(command) = render_command_echo(cli_args) else { + return; + }; + + eprintln!("{}", format!("Command: {command}").blue().bold()); +} + +fn render_command_echo(cli_args: &ToplevelCalcit) -> Option { + let subcommand = cli_args.subcommand.as_ref()?; + let mut tokens = vec![match subcommand { + CalcitCommand::Query(cmd) => format!("cr query {}", query_name(&cmd.subcommand)), + CalcitCommand::Docs(cmd) => format!("cr docs {}", docs_name(&cmd.subcommand)), + CalcitCommand::Edit(cmd) => format!("cr edit {}", edit_name(&cmd.subcommand)), + CalcitCommand::Tree(cmd) => format!("cr tree {}", tree_name(&cmd.subcommand)), + CalcitCommand::Analyze(cmd) => format!("cr analyze {}", analyze_name(&cmd.subcommand)), + CalcitCommand::Cirru(cmd) => format!("cr cirru {}", cirru_name(&cmd.subcommand)), + _ => return None, + }]; + + match subcommand { + CalcitCommand::Query(cmd) => push_query(&mut tokens, cmd), + CalcitCommand::Docs(cmd) => push_docs(&mut tokens, cmd), + CalcitCommand::Edit(cmd) => push_edit(&mut tokens, cmd), + CalcitCommand::Tree(cmd) => push_tree(&mut tokens, cmd), + CalcitCommand::Analyze(cmd) => push_analyze(&mut tokens, cmd), + CalcitCommand::Cirru(cmd) => push_cirru(&mut tokens, cmd), + _ => return None, + } + + Some(tokens.join(" ")) +} + +fn push_query(tokens: &mut Vec, cmd: &QueryCommand) { + match &cmd.subcommand { + QuerySubcommand::Schema(opts) => echo_items!(tokens, pos "target" => &opts.target, switch "json" => opts.json), + QuerySubcommand::Ns(opts) => { + echo_items!(tokens, opt "namespace" => opts.namespace.as_deref(); default "all", switch "deps" => opts.deps) + } + QuerySubcommand::Defs(opts) => echo_items!(tokens, pos "namespace" => &opts.namespace), + QuerySubcommand::Pkg(_) | QuerySubcommand::Config(_) | QuerySubcommand::Error(_) | QuerySubcommand::Modules(_) => {} + QuerySubcommand::Def(opts) => echo_items!( + tokens, + pos "target" => &opts.target, + switch "json" => opts.json, + value "chunk-target-nodes" => opts.chunk_target_nodes; default "56", + value "chunk-max-nodes" => opts.chunk_max_nodes; default "68", + value "chunk-trigger-nodes" => opts.chunk_trigger_nodes; default "88", + switch "raw" => opts.raw + ), + QuerySubcommand::Peek(opts) => echo_items!(tokens, pos "target" => &opts.target), + QuerySubcommand::Examples(opts) => echo_items!(tokens, pos "target" => &opts.target), + QuerySubcommand::Find(opts) => echo_items!( + tokens, + pos "symbol" => &opts.symbol, + switch "deps" => opts.deps, + switch "exact" => opts.exact, + value "limit" => opts.limit; default "20", + value "detail-offset" => opts.detail_offset; default "0" + ), + QuerySubcommand::Usages(opts) => echo_items!( + tokens, + pos "target" => &opts.target, + switch "deps" => opts.deps, + value "detail-offset" => opts.detail_offset; default "0" + ), + QuerySubcommand::Search(opts) => echo_items!( + tokens, + pos "pattern" => &opts.pattern, + opt "filter" => opts.filter.as_deref(); default "none", + switch "exact" => opts.exact, + value "max-depth" => opts.max_depth; default "0", + opt "start-path" => opts.start_path.as_deref(); default "none", + opt "entry" => opts.entry.as_deref(); default "none", + value "detail-offset" => opts.detail_offset; default "0" + ), + QuerySubcommand::SearchExpr(opts) => echo_items!( + tokens, + pos "pattern" => &opts.pattern, + opt "filter" => opts.filter.as_deref(); default "none", + switch "exact" => opts.exact, + value "max-depth" => opts.max_depth; default "0", + switch "json" => opts.json, + opt "entry" => opts.entry.as_deref(); default "none", + value "detail-offset" => opts.detail_offset; default "0" + ), + } +} + +fn push_docs(tokens: &mut Vec, cmd: &DocsCommand) { + match &cmd.subcommand { + DocsSubcommand::Search(opts) => echo_items!( + tokens, + pos "keyword" => &opts.keyword, + value "context" => opts.context; default "5", + opt "filename" => opts.filename.as_deref(); default "none" + ), + DocsSubcommand::Read(opts) => echo_items!( + tokens, + pos "filename" => &opts.filename, + list "heading" => &opts.headings, + switch "no-subheadings" => opts.no_subheadings, + switch "full" => opts.full, + switch "with-lines" => opts.with_lines + ), + DocsSubcommand::Agents(opts) => echo_items!( + tokens, + list "heading" => &opts.headings, + switch "no-subheadings" => opts.no_subheadings, + switch "full" => opts.full, + switch "with-lines" => opts.with_lines, + switch "refresh" => opts.refresh + ), + DocsSubcommand::ReadLines(opts) => echo_items!( + tokens, + pos "filename" => &opts.filename, + value "start" => opts.start; default "0", + value "lines" => opts.lines; default "80" + ), + DocsSubcommand::List(_) => {} + DocsSubcommand::CheckMd(opts) => { + echo_items!(tokens, pos "file" => &opts.file, value "entry" => &opts.entry; default "demos/compact.cirru", list "dep" => &opts.dep) + } + } +} + +fn push_cirru(tokens: &mut Vec, cmd: &CirruCommand) { + match &cmd.subcommand { + CirruSubcommand::Parse(opts) => { + echo_items!(tokens, pos "code" => &opts.code, switch "expr-one" => opts.expr_one_liner, switch "validate" => opts.validate) + } + CirruSubcommand::Format(opts) => echo_items!(tokens, pos "json" => &opts.json), + CirruSubcommand::ParseEdn(opts) => echo_items!(tokens, pos "edn" => &opts.edn), + CirruSubcommand::ShowGuide(_) => {} + } +} + +fn push_analyze(tokens: &mut Vec, cmd: &AnalyzeCommand) { + match &cmd.subcommand { + AnalyzeSubcommand::CallGraph(opts) => echo_items!( + tokens, + opt "root" => opts.root.as_deref(); default "config.init-fn", + opt "ns-prefix" => opts.ns_prefix.as_deref(); default "none", + switch "include-core" => opts.include_core, + value "max-depth" => opts.max_depth; default "0", + switch "show-unused" => opts.show_unused, + value "format" => &opts.format; default "text" + ), + AnalyzeSubcommand::CountCalls(opts) => echo_items!( + tokens, + opt "root" => opts.root.as_deref(); default "config.init-fn", + opt "ns-prefix" => opts.ns_prefix.as_deref(); default "none", + switch "include-core" => opts.include_core, + value "format" => &opts.format; default "text", + value "sort" => &opts.sort; default "count" + ), + AnalyzeSubcommand::CheckExamples(opts) => echo_items!(tokens, value "ns" => &opts.ns), + AnalyzeSubcommand::CheckTypes(opts) => echo_items!( + tokens, + opt "ns" => opts.ns.as_deref(); default "none", + opt "ns-prefix" => opts.ns_prefix.as_deref(); default "none", + opt "only" => opts.only.as_deref(); default "all", + switch "deps" => opts.deps + ), + AnalyzeSubcommand::JsEscape(opts) => echo_items!(tokens, pos "symbol" => &opts.symbol), + AnalyzeSubcommand::JsUnescape(opts) => echo_items!(tokens, pos "symbol" => &opts.symbol), + } +} + +fn push_edit(tokens: &mut Vec, cmd: &EditCommand) { + match &cmd.subcommand { + EditSubcommand::Format(_) => {} + EditSubcommand::Def(opts) => { + echo_items!(tokens, pos "target" => &opts.target, code_input opts, switch "overwrite" => opts.overwrite) + } + EditSubcommand::MvDef(opts) => echo_items!(tokens, pos "source" => &opts.source, pos "target" => &opts.target), + EditSubcommand::RmDef(opts) => echo_items!(tokens, pos "target" => &opts.target), + EditSubcommand::Doc(opts) => echo_items!(tokens, pos "target" => &opts.target, pos "doc" => &opts.doc), + EditSubcommand::Schema(opts) => echo_items!(tokens, pos "target" => &opts.target, code_input opts, switch "clear" => opts.clear), + EditSubcommand::Examples(opts) => echo_items!(tokens, pos "target" => &opts.target, code_input opts, switch "clear" => opts.clear), + EditSubcommand::AddExample(opts) => { + echo_items!(tokens, pos "target" => &opts.target, opt_owned "at" => opts.at.map(|v| v.to_string()); default "append", code_input opts) + } + EditSubcommand::RmExample(opts) => echo_items!(tokens, pos "target" => &opts.target, pos "index" => &opts.index.to_string()), + EditSubcommand::AddNs(opts) => echo_items!(tokens, pos "namespace" => &opts.namespace, code_input opts), + EditSubcommand::RmNs(opts) => echo_items!(tokens, pos "namespace" => &opts.namespace), + EditSubcommand::Imports(opts) => echo_items!(tokens, pos "namespace" => &opts.namespace, code_input opts), + EditSubcommand::AddImport(opts) => { + echo_items!(tokens, pos "namespace" => &opts.namespace, code_input opts, switch "overwrite" => opts.overwrite) + } + EditSubcommand::RmImport(opts) => echo_items!(tokens, pos "namespace" => &opts.namespace, pos "source-ns" => &opts.source_ns), + EditSubcommand::NsDoc(opts) => echo_items!(tokens, pos "namespace" => &opts.namespace, pos "doc" => &opts.doc), + EditSubcommand::AddModule(opts) => echo_items!(tokens, pos "module-path" => &opts.module_path), + EditSubcommand::RmModule(opts) => echo_items!(tokens, pos "module-path" => &opts.module_path), + EditSubcommand::Config(opts) => echo_items!(tokens, pos "key" => &opts.key, pos "value" => &opts.value), + EditSubcommand::Inc(opts) => { + echo_items!(tokens, list "added-ns" => &opts.added_ns, list "removed-ns" => &opts.removed_ns, list "ns-updated" => &opts.ns_updated, list "added" => &opts.added, list "removed" => &opts.removed, list "changed" => &opts.changed) + } + EditSubcommand::Cp(opts) => { + echo_items!(tokens, pos "target" => &opts.target, value "from" => &opts.from, value "path" => &opts.path, value "at" => &opts.at; default "after") + } + EditSubcommand::Mv(opts) => { + echo_items!(tokens, pos "target" => &opts.target, value "from" => &opts.from, value "path" => &opts.path, value "at" => &opts.at; default "after") + } + EditSubcommand::Rename(opts) => echo_items!(tokens, pos "source" => &opts.source, pos "new-name" => &opts.new_name), + EditSubcommand::SplitDef(opts) => { + echo_items!(tokens, pos "target" => &opts.target, value "path" => &opts.path, value "name" => &opts.new_name) + } + } +} + +fn push_tree(tokens: &mut Vec, cmd: &TreeCommand) { + match &cmd.subcommand { + TreeSubcommand::Show(opts) => echo_items!( + tokens, + pos "target" => &opts.target, + value "path" => &opts.path, + value "depth" => opts.depth; default "2", + switch "json" => opts.json, + value "chunk-target-nodes" => opts.chunk_target_nodes; default "56", + value "chunk-max-nodes" => opts.chunk_max_nodes; default "68", + value "chunk-trigger-nodes" => opts.chunk_trigger_nodes; default "88", + value "chunk-expand-depth" => opts.chunk_expand_depth; default "1", + switch "raw" => opts.raw + ), + TreeSubcommand::Replace(opts) => { + echo_items!(tokens, pos "target" => &opts.target, value "path" => &opts.path, code_input opts, value "depth" => opts.depth; default "2") + } + TreeSubcommand::ReplaceLeaf(opts) => { + echo_items!(tokens, pos "target" => &opts.target, value "pattern" => &opts.pattern, code_input opts, value "depth" => opts.depth; default "2") + } + TreeSubcommand::Delete(opts) => { + echo_items!(tokens, pos "target" => &opts.target, value "path" => &opts.path, value "depth" => opts.depth; default "2") + } + TreeSubcommand::InsertBefore(opts) => push_tree_insert( + tokens, + &opts.target, + &opts.path, + CodeInputParts::new( + opts.file.as_deref(), + opts.code.as_deref(), + opts.json.as_deref(), + opts.json_input, + opts.leaf, + ), + opts.depth, + ), + TreeSubcommand::InsertAfter(opts) => push_tree_insert( + tokens, + &opts.target, + &opts.path, + CodeInputParts::new( + opts.file.as_deref(), + opts.code.as_deref(), + opts.json.as_deref(), + opts.json_input, + opts.leaf, + ), + opts.depth, + ), + TreeSubcommand::InsertChild(opts) => push_tree_insert( + tokens, + &opts.target, + &opts.path, + CodeInputParts::new( + opts.file.as_deref(), + opts.code.as_deref(), + opts.json.as_deref(), + opts.json_input, + opts.leaf, + ), + opts.depth, + ), + TreeSubcommand::AppendChild(opts) => push_tree_insert( + tokens, + &opts.target, + &opts.path, + CodeInputParts::new( + opts.file.as_deref(), + opts.code.as_deref(), + opts.json.as_deref(), + opts.json_input, + opts.leaf, + ), + opts.depth, + ), + TreeSubcommand::SwapNext(opts) => push_tree_path_depth(tokens, &opts.target, &opts.path, opts.depth), + TreeSubcommand::SwapPrev(opts) => push_tree_path_depth(tokens, &opts.target, &opts.path, opts.depth), + TreeSubcommand::Unwrap(opts) => push_tree_path_depth(tokens, &opts.target, &opts.path, opts.depth), + TreeSubcommand::Raise(opts) => push_tree_path_depth(tokens, &opts.target, &opts.path, opts.depth), + TreeSubcommand::Wrap(opts) => { + push_tree_path_depth(tokens, &opts.target, &opts.path, opts.depth); + echo_items!(tokens, code_input opts); + } + TreeSubcommand::TargetReplace(opts) => { + echo_items!(tokens, pos "target" => &opts.target, value "pattern" => &opts.pattern, code_input opts, value "depth" => opts.depth; default "2") + } + TreeSubcommand::Rewrite(opts) => { + push_tree_path_depth(tokens, &opts.target, &opts.path, opts.depth); + echo_items!(tokens, code_input opts, list "with" => &opts.with); + } + } +} + +struct CodeInputParts<'a> { + file: Option<&'a str>, + code: Option<&'a str>, + json: Option<&'a str>, + json_input: bool, + leaf: bool, +} + +impl<'a> CodeInputParts<'a> { + fn new(file: Option<&'a str>, code: Option<&'a str>, json: Option<&'a str>, json_input: bool, leaf: bool) -> Self { + Self { + file, + code, + json, + json_input, + leaf, + } + } +} + +fn push_tree_insert(tokens: &mut Vec, target: &str, path: &str, code_input: CodeInputParts<'_>, depth: usize) { + push_tree_path_depth(tokens, target, path, depth); + push_code_input(tokens, &code_input); +} + +fn push_tree_path_depth(tokens: &mut Vec, target: &str, path: &str, depth: usize) { + push_positional(tokens, "target", target); + push_value(tokens, "path", path, None); + push_value(tokens, "depth", &depth.to_string(), Some("2")); +} + +fn push_code_input(tokens: &mut Vec, code_input: &CodeInputParts<'_>) { + push_optional(tokens, "file", code_input.file, "none"); + push_optional(tokens, "code", code_input.code, "none"); + push_optional(tokens, "json", code_input.json, "none"); + push_switch(tokens, "json-input", code_input.json_input); + push_switch(tokens, "leaf", code_input.leaf); +} + +fn push_switch(tokens: &mut Vec, name: &str, enabled: bool) { + tokens.push(format!("--{name}={}", if enabled { "ON" } else { "OFF" })); +} + +fn push_value(tokens: &mut Vec, name: &str, value: &str, default: Option<&str>) { + let display_value = format_atom(value); + + match default { + Some(default_value) if default_value == value => { + tokens.push(format!("--{name}=({})", format_atom(default_value))); + } + _ => tokens.push(format!("--{name}={display_value}")), + } +} + +fn push_optional(tokens: &mut Vec, name: &str, value: Option<&str>, default_label: &str) { + match value { + Some(value) => tokens.push(format!("--{name}={}", format_atom(value))), + None => tokens.push(format!("--{name}=({})", format_atom(default_label))), + } +} + +fn push_optional_owned(tokens: &mut Vec, name: &str, value: Option, default_label: &str) { + match value { + Some(value) => tokens.push(format!("--{name}={}", format_atom(&value))), + None => tokens.push(format!("--{name}=({})", format_atom(default_label))), + } +} + +fn push_list(tokens: &mut Vec, name: &str, values: &[String]) { + if values.is_empty() { + tokens.push(format!("--{name}=(none)")); + } else { + for value in values { + tokens.push(format!("--{name}={}", format_atom(value))); + } + } +} + +fn push_positional(tokens: &mut Vec, name: &str, value: &str) { + tokens.push(format!("{name}={}", format_atom(value))); +} + +fn format_atom(value: &str) -> String { + if value.is_empty() { + return String::from("''"); + } + + if value.chars().any(char::is_whitespace) { + return format!("{value:?}"); + } + + value.to_owned() +} + +fn query_name(subcommand: &QuerySubcommand) -> &'static str { + match subcommand { + QuerySubcommand::Ns(_) => "ns", + QuerySubcommand::Defs(_) => "defs", + QuerySubcommand::Pkg(_) => "pkg", + QuerySubcommand::Config(_) => "config", + QuerySubcommand::Error(_) => "error", + QuerySubcommand::Modules(_) => "modules", + QuerySubcommand::Def(_) => "def", + QuerySubcommand::Peek(_) => "peek", + QuerySubcommand::Examples(_) => "examples", + QuerySubcommand::Find(_) => "find", + QuerySubcommand::Usages(_) => "usages", + QuerySubcommand::Search(_) => "search", + QuerySubcommand::SearchExpr(_) => "search-expr", + QuerySubcommand::Schema(_) => "schema", + } +} + +fn docs_name(subcommand: &DocsSubcommand) -> &'static str { + match subcommand { + DocsSubcommand::Search(_) => "search", + DocsSubcommand::Read(_) => "read", + DocsSubcommand::Agents(_) => "agents", + DocsSubcommand::ReadLines(_) => "read-lines", + DocsSubcommand::List(_) => "list", + DocsSubcommand::CheckMd(_) => "check-md", + } +} + +fn cirru_name(subcommand: &CirruSubcommand) -> &'static str { + match subcommand { + CirruSubcommand::Parse(_) => "parse", + CirruSubcommand::Format(_) => "format", + CirruSubcommand::ParseEdn(_) => "parse-edn", + CirruSubcommand::ShowGuide(_) => "show-guide", + } +} + +fn analyze_name(subcommand: &AnalyzeSubcommand) -> &'static str { + match subcommand { + AnalyzeSubcommand::CallGraph(_) => "call-graph", + AnalyzeSubcommand::CountCalls(_) => "count-calls", + AnalyzeSubcommand::CheckExamples(_) => "check-examples", + AnalyzeSubcommand::CheckTypes(_) => "check-types", + AnalyzeSubcommand::JsEscape(_) => "js-escape", + AnalyzeSubcommand::JsUnescape(_) => "js-unescape", + } +} + +fn edit_name(subcommand: &EditSubcommand) -> &'static str { + match subcommand { + EditSubcommand::Format(_) => "format", + EditSubcommand::Def(_) => "def", + EditSubcommand::MvDef(_) => "mv-def", + EditSubcommand::RmDef(_) => "rm-def", + EditSubcommand::Doc(_) => "doc", + EditSubcommand::Schema(_) => "schema", + EditSubcommand::Examples(_) => "examples", + EditSubcommand::AddExample(_) => "add-example", + EditSubcommand::RmExample(_) => "rm-example", + EditSubcommand::AddNs(_) => "add-ns", + EditSubcommand::RmNs(_) => "rm-ns", + EditSubcommand::Imports(_) => "imports", + EditSubcommand::AddImport(_) => "add-import", + EditSubcommand::RmImport(_) => "rm-import", + EditSubcommand::NsDoc(_) => "ns-doc", + EditSubcommand::AddModule(_) => "add-module", + EditSubcommand::RmModule(_) => "rm-module", + EditSubcommand::Config(_) => "config", + EditSubcommand::Inc(_) => "inc", + EditSubcommand::Cp(_) => "cp", + EditSubcommand::Mv(_) => "mv", + EditSubcommand::Rename(_) => "rename", + EditSubcommand::SplitDef(_) => "split-def", + } +} + +fn tree_name(subcommand: &TreeSubcommand) -> &'static str { + match subcommand { + TreeSubcommand::Show(_) => "show", + TreeSubcommand::Replace(_) => "replace", + TreeSubcommand::ReplaceLeaf(_) => "replace-leaf", + TreeSubcommand::Delete(_) => "delete", + TreeSubcommand::InsertBefore(_) => "insert-before", + TreeSubcommand::InsertAfter(_) => "insert-after", + TreeSubcommand::InsertChild(_) => "insert-child", + TreeSubcommand::AppendChild(_) => "append-child", + TreeSubcommand::SwapNext(_) => "swap-next", + TreeSubcommand::SwapPrev(_) => "swap-prev", + TreeSubcommand::Unwrap(_) => "unwrap", + TreeSubcommand::Raise(_) => "raise", + TreeSubcommand::Wrap(_) => "wrap", + TreeSubcommand::TargetReplace(_) => "target-replace", + TreeSubcommand::Rewrite(_) => "rewrite", + } +} diff --git a/src/bin/cli_handlers/docs.rs b/src/bin/cli_handlers/docs.rs index 2dd926b5..d3acf59c 100644 --- a/src/bin/cli_handlers/docs.rs +++ b/src/bin/cli_handlers/docs.rs @@ -21,6 +21,7 @@ use calcit::snapshot; use calcit::util; use super::markdown_read::{RenderMarkdownOptions, render_markdown_sections}; +use super::tips::command_guidance_enabled; #[derive(Debug, Clone)] pub struct GuideDoc { @@ -252,7 +253,7 @@ fn handle_search(keyword: &str, context_lines: usize, filename_filter: Option<&s if !found_any { println!("{}", "No matching content found.".yellow()); - } else { + } else if command_guidance_enabled() { println!( "{}", "Tip: Use -c to show more context lines (e.g., 'cr docs search -c 20')".dimmed() @@ -375,10 +376,12 @@ fn handle_read_lines(filename: &str, start: usize, lines_to_read: usize) -> Resu println!("{}", "End of document.".green()); } - println!( - "{}", - "Tip: Use -s -n to read specific range (e.g., 'cr docs read-lines file.md -s 20 -n 30')".dimmed() - ); + if command_guidance_enabled() { + println!( + "{}", + "Tip: Use -s -n to read specific range (e.g., 'cr docs read-lines file.md -s 20 -n 30')".dimmed() + ); + } Ok(()) } @@ -402,16 +405,18 @@ fn handle_list() -> Result<(), String> { } println!("\n{} {} topics", "Total:".dimmed(), docs.len()); - println!("{}", "Use 'cr docs read ' to list headings in a document".dimmed()); - println!( - "{}", - " 'cr docs read ' to read matched sections".dimmed() - ); - println!( - "{}", - " 'cr docs read-lines -s -n ' for line-based reading".dimmed() - ); - println!("{}", " 'cr docs search ' to search content".dimmed()); + if command_guidance_enabled() { + println!("{}", "Use 'cr docs read ' to list headings in a document".dimmed()); + println!( + "{}", + " 'cr docs read ' to read matched sections".dimmed() + ); + println!( + "{}", + " 'cr docs read-lines -s -n ' for line-based reading".dimmed() + ); + println!("{}", " 'cr docs search ' to search content".dimmed()); + } Ok(()) } diff --git a/src/bin/cli_handlers/edit.rs b/src/bin/cli_handlers/edit.rs index 186dfd8e..46c7146b 100644 --- a/src/bin/cli_handlers/edit.rs +++ b/src/bin/cli_handlers/edit.rs @@ -26,7 +26,7 @@ use std::fs; use std::sync::Arc; use super::common::{ERR_CODE_INPUT_REQUIRED, json_value_to_cirru, parse_input_to_cirru, parse_path, read_code_input}; -use super::tips::Tips; +use super::tips::{Tips, command_guidance_enabled}; /// Parse "namespace/definition" format into (namespace, definition) /// Splits at the FIRST '/' so operator definitions like '/' and '/=' are handled correctly. @@ -189,24 +189,26 @@ fn handle_def(opts: &EditDefCommand, snapshot_file: &str) -> Result<(), String> definition.cyan(), namespace ); - println!(); - println!("{}", "Next steps:".blue().bold()); - println!(" • View definition: {} '{}/{}'", "cr query def".cyan(), namespace, definition); - println!(" • Check errors: {}", "cr query error".cyan()); - println!(" • Find usages: {} '{}/{}'", "cr query usages".cyan(), namespace, definition); - println!( - " • Add to imports: {} '{}' --refer '{}'", - "cr edit add-import".cyan(), - namespace, - definition - ); - println!(); - let mut tips = Tips::new(); - tips.add(format!( - "Use single quotes around '{namespace}/{definition}' to avoid shell escaping issues." - )); - tips.add(format!("Example: cr tree show '{namespace}/{definition}'")); - tips.print(); + if command_guidance_enabled() { + println!(); + println!("{}", "Next steps:".blue().bold()); + println!(" • View definition: {} '{}/{}'", "cr query def".cyan(), namespace, definition); + println!(" • Check errors: {}", "cr query error".cyan()); + println!(" • Find usages: {} '{}/{}'", "cr query usages".cyan(), namespace, definition); + println!( + " • Add to imports: {} '{}' --refer '{}'", + "cr edit add-import".cyan(), + namespace, + definition + ); + println!(); + let mut tips = Tips::new(); + tips.add(format!( + "Use single quotes around '{namespace}/{definition}' to avoid shell escaping issues." + )); + tips.add(format!("Example: cr tree show '{namespace}/{definition}'")); + tips.print(); + } Ok(()) } @@ -449,17 +451,19 @@ fn handle_split_def(opts: &EditSplitDefCommand, snapshot_file: &str) -> Result<( definition.cyan(), new_name.cyan() ); - println!(); - println!("{}", "Next steps:".blue().bold()); - println!(" • Inspect new def: {} '{}/{}'", "cr query def".cyan(), namespace, new_name); - println!(" • Inspect source: {} '{}/{}'", "cr query def".cyan(), namespace, definition); - println!( - " • Wrap in defn: {} '{}/{}' -p '' -e 'defn {} ...'", - "cr tree replace".cyan(), - namespace, - new_name, - new_name - ); + if command_guidance_enabled() { + println!(); + println!("{}", "Next steps:".blue().bold()); + println!(" • Inspect new def: {} '{}/{}'", "cr query def".cyan(), namespace, new_name); + println!(" • Inspect source: {} '{}/{}'", "cr query def".cyan(), namespace, definition); + println!( + " • Wrap in defn: {} '{}/{}' -p '' -e 'defn {} ...'", + "cr tree replace".cyan(), + namespace, + new_name, + new_name + ); + } Ok(()) } diff --git a/src/bin/cli_handlers/mod.rs b/src/bin/cli_handlers/mod.rs index dec653e6..ac7a87d9 100644 --- a/src/bin/cli_handlers/mod.rs +++ b/src/bin/cli_handlers/mod.rs @@ -5,6 +5,7 @@ mod chunk_display; mod cirru; mod cirru_validator; +mod command_echo; mod common; mod docs; mod edit; @@ -15,10 +16,11 @@ mod tips; mod tree; pub use cirru::handle_cirru_command; +pub use command_echo::{print_command_echo, should_echo_command}; pub use docs::handle_docs_command; pub use edit::handle_edit_command; pub use libs::handle_libs_command; pub use query::handle_query_command; -pub use tips::set_tips_level; +pub use tips::{set_tips_level, suppress_command_guidance}; pub use tree::handle_tree_command; // Re-export when needed by other modules; keep internal for now to avoid unused-import warnings diff --git a/src/bin/cli_handlers/query.rs b/src/bin/cli_handlers/query.rs index 31eb2f3c..1e9c7298 100644 --- a/src/bin/cli_handlers/query.rs +++ b/src/bin/cli_handlers/query.rs @@ -4,7 +4,7 @@ use super::chunk_display::{ChunkDisplayOptions, ChunkedDisplay, maybe_chunk_node}; use super::common::{format_path, format_path_bracketed, parse_path}; -use super::tips::{TipPriority, Tips, tip_prefer_oneliner_json, tip_query_defs_list, tip_query_ns_list}; +use super::tips::{TipPriority, Tips, command_guidance_enabled, tip_prefer_oneliner_json, tip_query_defs_list, tip_query_ns_list}; use calcit::CalcitTypeAnnotation; use calcit::cli_args::{QueryCommand, QueryDefCommand, QuerySubcommand}; use calcit::load_core_snapshot; @@ -441,10 +441,12 @@ fn handle_ns_details(input_path: &str, namespace: &str) -> Result<(), String> { println!("\n{} {}", "Definitions:".bold(), file_data.defs.len()); - println!( - "\n{}", - format!("Tip: Use `cr query defs {namespace}` to list definitions.").dimmed() - ); + if command_guidance_enabled() { + println!( + "\n{}", + format!("Tip: Use `cr query defs {namespace}` to list definitions.").dimmed() + ); + } Ok(()) } @@ -532,10 +534,12 @@ fn handle_error() -> Result<(), String> { if !Path::new(error_file).exists() { println!("{}", "No .calcit-error.cirru file found.".yellow()); - println!(); - println!("{}", "Next steps:".blue().bold()); - println!(" • Start watcher: {} or {}", "cr".cyan(), "cr js".cyan()); - println!(" • Run syntax check: {}", "cr --check-only".cyan()); + if command_guidance_enabled() { + println!(); + println!("{}", "Next steps:".blue().bold()); + println!(" • Start watcher: {} or {}", "cr".cyan(), "cr js".cyan()); + println!(" • Run syntax check: {}", "cr --check-only".cyan()); + } return Ok(()); } @@ -567,13 +571,15 @@ fn handle_error() -> Result<(), String> { } else { println!("{}", "Last error stack trace:".bold().red()); println!("{content}"); - println!(); - println!("{}", "Next steps to fix:".blue().bold()); - println!(" • Search for error location: {} ''", "cr query search".cyan()); - println!(" • View definition: {} ''", "cr query def".cyan()); - println!(" • Find usages: {} ''", "cr query usages".cyan()); - println!(); - println!("{}", "Tip: After fixing, watcher will recompile automatically (~300ms).".dimmed()); + if command_guidance_enabled() { + println!(); + println!("{}", "Next steps to fix:".blue().bold()); + println!(" • Search for error location: {} ''", "cr query search".cyan()); + println!(" • View definition: {} ''", "cr query def".cyan()); + println!(" • Find usages: {} ''", "cr query usages".cyan()); + println!(); + println!("{}", "Tip: After fixing, watcher will recompile automatically (~300ms).".dimmed()); + } println!( "{}", "Note: even when this clears, non-Calcit issues like CSS strings, DOM behavior, and external integrations can still be wrong." @@ -1185,7 +1191,7 @@ fn handle_usages(input_path: &str, target_ns: &str, target_def: &str, include_de } // Tip - if !usages.is_empty() { + if !usages.is_empty() && command_guidance_enabled() { println!("\n{}", "Tip: Modifying this definition may affect the above locations.".dimmed()); } @@ -1318,10 +1324,12 @@ fn handle_fuzzy_search(input_path: &str, pattern: &str, include_deps: bool, limi if displayed.is_empty() { println!(" {}", "No matches found".dimmed()); - println!( - "\n{}", - "Tip: Try a broader pattern, or add --deps to include core namespaces.".dimmed() - ); + if command_guidance_enabled() { + println!( + "\n{}", + "Tip: Try a broader pattern, or add --deps to include core namespaces.".dimmed() + ); + } return Ok(()); } @@ -1345,7 +1353,9 @@ fn handle_fuzzy_search(input_path: &str, pattern: &str, include_deps: bool, limi println!(" ⋯ {} more results...", total - limit); } - println!("\n{}", "Tip: Use `query def ` to view definition content.".dimmed()); + if command_guidance_enabled() { + println!("\n{}", "Tip: Use `query def ` to view definition content.".dimmed()); + } Ok(()) } diff --git a/src/bin/cli_handlers/tips.rs b/src/bin/cli_handlers/tips.rs index ce7aa6f7..e22b0c37 100644 --- a/src/bin/cli_handlers/tips.rs +++ b/src/bin/cli_handlers/tips.rs @@ -3,6 +3,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; static TIPS_SUPPRESSED: AtomicBool = AtomicBool::new(false); static TIPS_FULL: AtomicBool = AtomicBool::new(false); +static COMMAND_GUIDANCE_SUPPRESSED: AtomicBool = AtomicBool::new(false); #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TipsLevel { @@ -34,6 +35,15 @@ pub fn suppress_tips() { TIPS_FULL.store(false, Ordering::Relaxed); } +pub fn suppress_command_guidance() { + COMMAND_GUIDANCE_SUPPRESSED.store(true, Ordering::Relaxed); + suppress_tips(); +} + +pub fn command_guidance_enabled() -> bool { + !COMMAND_GUIDANCE_SUPPRESSED.load(Ordering::Relaxed) +} + pub fn set_tips_level(raw: &str) -> Result<(), String> { let level = TipsLevel::parse(raw).ok_or_else(|| format!("Invalid --tips-level value '{raw}'. Expected one of: minimal, full, none"))?; diff --git a/src/bin/cli_handlers/tree.rs b/src/bin/cli_handlers/tree.rs index d707ca0e..61bb2812 100644 --- a/src/bin/cli_handlers/tree.rs +++ b/src/bin/cli_handlers/tree.rs @@ -5,7 +5,7 @@ use super::chunk_display::{ChunkDisplayOptions, ChunkedDisplay, fragment_nesting use super::common::{ ERR_CODE_INPUT_REQUIRED, cirru_to_json, format_path, format_path_bracketed, parse_input_to_cirru, parse_path, read_code_input, }; -use super::tips::{TipPriority, Tips, tip_prefer_oneliner_json, tip_root_edit}; +use super::tips::{TipPriority, Tips, command_guidance_enabled, tip_prefer_oneliner_json, tip_root_edit}; use crate::cli_args::{ TreeAppendChildCommand, TreeCommand, TreeDeleteCommand, TreeInsertAfterCommand, TreeInsertBeforeCommand, TreeInsertChildCommand, TreeRaiseCommand, TreeReplaceCommand, TreeReplaceLeafCommand, TreeShowCommand, TreeStructuralCommand, TreeSubcommand, @@ -390,21 +390,23 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> println!(); } - println!("{}: To modify this node:", "Next steps".blue().bold()); - println!( - " • Replace: {} {} -p '{}' {}", - "cr tree replace".cyan(), - opts.target, - format_path(&path), - "-e 'cirru one-liner'".dimmed() - ); - println!( - " • Delete: {} {} -p '{}'", - "cr tree delete".cyan(), - opts.target, - format_path(&path) - ); - println!(); + if command_guidance_enabled() { + println!("{}: To modify this node:", "Next steps".blue().bold()); + println!( + " • Replace: {} {} -p '{}' {}", + "cr tree replace".cyan(), + opts.target, + format_path(&path), + "-e 'cirru one-liner'".dimmed() + ); + println!( + " • Delete: {} {} -p '{}'", + "cr tree delete".cyan(), + opts.target, + format_path(&path) + ); + println!(); + } let mut tips = Tips::new(); if let Some((shown_fragments, total_fragments)) = shown_fragments { if shown_fragments < total_fragments { @@ -431,31 +433,33 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> if let Cirru::Leaf(s) = &node { println!("{}: {:?}", "Value".green().bold(), s.as_ref()); println!(); - println!("{}: To modify this leaf:", "Next steps".blue().bold()); - println!( - " • Replace: {} {} -p '{}' --leaf -e ''", - "cr tree replace".cyan(), - opts.target, - format_path(&path) - ); - if !path.is_empty() { - // Show parent path for context - let parent_path = &path[..path.len() - 1]; - let parent_path_str = format_path(parent_path); + if command_guidance_enabled() { + println!("{}: To modify this leaf:", "Next steps".blue().bold()); println!( - " • View parent: {} {} -p '{}'", - "cr tree show".cyan(), + " • Replace: {} {} -p '{}' --leaf -e ''", + "cr tree replace".cyan(), opts.target, - parent_path_str + format_path(&path) + ); + if !path.is_empty() { + // Show parent path for context + let parent_path = &path[..path.len() - 1]; + let parent_path_str = format_path(parent_path); + println!( + " • View parent: {} {} -p '{}'", + "cr tree show".cyan(), + opts.target, + parent_path_str + ); + } + println!(); + println!( + "{}: Use {} for symbols, {} for strings", + "Tip".blue().bold(), + "-e 'symbol'".yellow(), + "-e '|text'".yellow() ); } - println!(); - println!( - "{}: Use {} for symbols, {} for strings", - "Tip".blue().bold(), - "-e 'symbol'".yellow(), - "-e '|text'".yellow() - ); } } @@ -517,15 +521,17 @@ fn handle_replace(opts: &TreeReplaceCommand, snapshot_file: &str) -> Result<(), let new_node = navigate_to_path(&new_code, &path)?; println!("{}", format_preview_with_type(&new_node, 20)); println!(); - println!("{}", "Next steps:".blue().bold()); - println!( - " • Verify: {} '{}' -p '{}'", - "cr tree show".cyan(), - format_args!("{}/{}", namespace, definition), - path.iter().map(|i| i.to_string()).collect::>().join(",") - ); - println!(" • Check errors: {}", "cr query error".cyan()); - println!(" • Find usages: {} '{}/{}'", "cr query usages".cyan(), namespace, definition); + if command_guidance_enabled() { + println!("{}", "Next steps:".blue().bold()); + println!( + " • Verify: {} '{}' -p '{}'", + "cr tree show".cyan(), + format_args!("{}/{}", namespace, definition), + path.iter().map(|i| i.to_string()).collect::>().join(",") + ); + println!(" • Check errors: {}", "cr query error".cyan()); + println!(" • Find usages: {} '{}/{}'", "cr query usages".cyan(), namespace, definition); + } Ok(()) } @@ -593,15 +599,17 @@ fn handle_rewrite(opts: &TreeStructuralCommand, snapshot_file: &str) -> Result<( let new_node = navigate_to_path(&new_code, &path)?; println!("{}", format_preview_with_type(&new_node, 20)); println!(); - println!("{}", "Next steps:".blue().bold()); - println!( - " • Verify: {} '{}' -p '{}'", - "cr tree show".cyan(), - format_args!("{}/{}", namespace, definition), - path.iter().map(|i| i.to_string()).collect::>().join(",") - ); - println!(" • Check errors: {}", "cr query error".cyan()); - println!(" • Find usages: {} '{}/{}'", "cr query usages".cyan(), namespace, definition); + if command_guidance_enabled() { + println!("{}", "Next steps:".blue().bold()); + println!( + " • Verify: {} '{}' -p '{}'", + "cr tree show".cyan(), + format_args!("{}/{}", namespace, definition), + path.iter().map(|i| i.to_string()).collect::>().join(",") + ); + println!(" • Check errors: {}", "cr query error".cyan()); + println!(" • Find usages: {} '{}/{}'", "cr query usages".cyan(), namespace, definition); + } Ok(()) } @@ -700,10 +708,12 @@ fn handle_replace_leaf(opts: &TreeReplaceLeafCommand, snapshot_file: &str) -> Re format_preview_with_type(&replacement_node, 0) ); println!(); - println!("{}", "Next steps:".blue().bold()); - println!(" • Verify: {} '{}/{}'", "cr query def".cyan(), namespace, definition); - println!(" • Check errors: {}", "cr query error".cyan()); - println!(" • Find usages: {} '{}/{}'", "cr query usages".cyan(), namespace, definition); + if command_guidance_enabled() { + println!("{}", "Next steps:".blue().bold()); + println!(" • Verify: {} '{}/{}'", "cr query def".cyan(), namespace, definition); + println!(" • Check errors: {}", "cr query error".cyan()); + println!(" • Find usages: {} '{}/{}'", "cr query usages".cyan(), namespace, definition); + } Ok(()) } @@ -777,7 +787,9 @@ fn handle_target_replace(opts: &TreeTargetReplaceCommand, snapshot_file: &str) - println!(" ... and {} more", matches.len() - 10); } println!(); - println!("{}", "Tip: Use 'tree replace-leaf' if you want to replace ALL occurrences.".blue()); + if command_guidance_enabled() { + println!("{}", "Tip: Use 'tree replace-leaf' if you want to replace ALL occurrences.".blue()); + } return Err(String::new()); } diff --git a/src/bin/cr.rs b/src/bin/cr.rs index ee5eab87..e8040f76 100644 --- a/src/bin/cr.rs +++ b/src/bin/cr.rs @@ -50,6 +50,11 @@ fn main() -> Result<(), String> { cli_handlers::set_tips_level("full")?; } + if cli_handlers::should_echo_command(&cli_args) { + cli_handlers::suppress_command_guidance(); + cli_handlers::print_command_echo(&cli_args); + } + // Handle standalone commands that don't need full program loading match &cli_args.subcommand { Some(CalcitCommand::Query(query_cmd)) => { @@ -800,12 +805,6 @@ fn run_call_graph(entries: &ProgramEntries, options: &CallGraphCommand, _snapsho println!("{json}"); } else { println!("{}", calcit::call_tree::format_for_llm(&result)); - - // Helpful tips to guide follow-up commands (top 3) - println!("\n{}", "Tips".bold()); - println!("- Focus by namespace: cr analyze call-graph --ns-prefix "); - println!("- Quantify hotspots: cr analyze count-calls [--ns-prefix ] [--include-core]"); - println!("- Explore details: cr query peek | cr query def "); } Ok(()) From 28b2b585364bdd6286cb4b5ae94e81a7479c662f Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 23 Mar 2026 11:17:37 +0800 Subject: [PATCH 34/57] Trim redundant command tips --- src/bin/cli_handlers/docs.rs | 2 +- src/bin/cli_handlers/markdown_read.rs | 6 +++ src/bin/cli_handlers/query.rs | 63 +-------------------------- src/bin/cli_handlers/tips.rs | 16 ------- 4 files changed, 8 insertions(+), 79 deletions(-) diff --git a/src/bin/cli_handlers/docs.rs b/src/bin/cli_handlers/docs.rs index d3acf59c..765d2541 100644 --- a/src/bin/cli_handlers/docs.rs +++ b/src/bin/cli_handlers/docs.rs @@ -370,7 +370,7 @@ fn handle_read_lines(filename: &str, start: usize, lines_to_read: usize) -> Resu let remaining = total_lines - end; println!( "{}", - format!("More content available ({remaining} lines remaining). Use -s {end} -n {lines_to_read} to continue reading.").yellow() + format!("More content available ({remaining} lines remaining). Next start line: {end}.").yellow() ); } else { println!("{}", "End of document.".green()); diff --git a/src/bin/cli_handlers/markdown_read.rs b/src/bin/cli_handlers/markdown_read.rs index 9679f4ee..7d0de1b7 100644 --- a/src/bin/cli_handlers/markdown_read.rs +++ b/src/bin/cli_handlers/markdown_read.rs @@ -1,5 +1,7 @@ use colored::Colorize; +use super::tips::command_guidance_enabled; + pub struct RenderMarkdownOptions<'a> { pub include_subheadings: bool, pub full: bool, @@ -179,6 +181,10 @@ pub fn print_selected_sections( } pub fn print_markdown_read_tips(command_prefix: &str, with_file_option: bool) { + if !command_guidance_enabled() { + return; + } + println!( "{}", format!("Tip: Use '{command_prefix} [more-keywords...]' for fuzzy heading matching.").dimmed() diff --git a/src/bin/cli_handlers/query.rs b/src/bin/cli_handlers/query.rs index 1e9c7298..10cce5af 100644 --- a/src/bin/cli_handlers/query.rs +++ b/src/bin/cli_handlers/query.rs @@ -4,7 +4,7 @@ use super::chunk_display::{ChunkDisplayOptions, ChunkedDisplay, maybe_chunk_node}; use super::common::{format_path, format_path_bracketed, parse_path}; -use super::tips::{TipPriority, Tips, command_guidance_enabled, tip_prefer_oneliner_json, tip_query_defs_list, tip_query_ns_list}; +use super::tips::{TipPriority, Tips, command_guidance_enabled}; use calcit::CalcitTypeAnnotation; use calcit::cli_args::{QueryCommand, QueryDefCommand, QuerySubcommand}; use calcit::load_core_snapshot; @@ -413,10 +413,6 @@ fn handle_ns(input_path: &str, namespace: Option<&str>, include_deps: bool) -> R println!(" {}", ns.cyan()); } - let mut tips = Tips::new(); - tips.append(tip_query_ns_list(include_deps)); - tips.print(); - Ok(()) } @@ -441,13 +437,6 @@ fn handle_ns_details(input_path: &str, namespace: &str) -> Result<(), String> { println!("\n{} {}", "Definitions:".bold(), file_data.defs.len()); - if command_guidance_enabled() { - println!( - "\n{}", - format!("Tip: Use `cr query defs {namespace}` to list definitions.").dimmed() - ); - } - Ok(()) } @@ -484,10 +473,6 @@ fn handle_defs(input_path: &str, namespace: &str) -> Result<(), String> { } } - let mut tips = Tips::new(); - tips.append(tip_query_defs_list()); - tips.print(); - Ok(()) } @@ -631,11 +616,6 @@ fn handle_modules(input_path: &str) -> Result<(), String> { } } - // Unified tips output - let mut tips = Tips::new(); - tips.append(tip_query_ns_list(false)); - tips.print(); - Ok(()) } @@ -733,19 +713,6 @@ fn handle_def(input_path: &str, namespace: &str, definition: &str, opts: &QueryD println!("{}", serde_json::to_string(&json).unwrap()); } - let mut tips = Tips::new(); - tips.add(format!( - "Try `cr query search -f '{namespace}/{definition}'` to find coordinates of a leaf node" - )); - tips.add(format!( - "Use `cr tree show {namespace}/{definition} -p '0'` or `-p '0.1'` to explore tree for editing" - )); - if !code_entry.examples.is_empty() { - tips.add(format!("Use `cr query examples {namespace}/{definition}` to view examples")); - } - tips.append(tip_prefer_oneliner_json(opts.json)); - tips.print(); - Ok(()) } @@ -792,9 +759,6 @@ fn handle_examples(input_path: &str, namespace: &str, definition: &str) -> Resul if code_entry.examples.is_empty() { println!("\n{}", "(no examples)".dimmed()); - let mut tips = Tips::new(); - tips.add(format!("Use `cr edit examples {namespace}/{definition}` to add examples.")); - tips.print(); } else { println!("{} example(s)\n", code_entry.examples.len()); @@ -812,10 +776,6 @@ fn handle_examples(input_path: &str, namespace: &str, definition: &str) -> Resul println!(" {} {}", "JSON:".dimmed(), serde_json::to_string(&json).unwrap().dimmed()); println!(); } - - let mut tips = Tips::new(); - tips.add(format!("Use `cr edit examples {namespace}/{definition}` to modify examples.")); - tips.print(); } Ok(()) @@ -884,14 +844,6 @@ fn handle_peek(input_path: &str, namespace: &str, definition: &str) -> Result<() println!("{} -", "Schema:".bold()); } - let mut tips = Tips::new(); - tips.add(format!("cr query def {namespace}/{definition}")); - tips.add(format!("cr query examples {namespace}/{definition}")); - tips.add(format!("cr query usages {namespace}/{definition}")); - tips.add(format!("cr query schema {namespace}/{definition}")); - tips.add(format!("cr edit doc {namespace}/{definition} ''")); - tips.print(); - Ok(()) } @@ -928,11 +880,6 @@ fn handle_schema(input_path: &str, namespace: &str, definition: &str, json: bool println!("{} -", "Schema:".bold()); } - let mut tips = Tips::new(); - tips.add(format!("cr query peek {namespace}/{definition}")); - tips.add(format!("cr edit schema {namespace}/{definition} -e '{{}} ...'")); - tips.print(); - Ok(()) } @@ -1053,14 +1000,6 @@ fn handle_find(input_path: &str, symbol: &str, include_deps: bool, detail_offset if found_definitions.is_empty() && references.is_empty() { println!("{}", "No matches found.".yellow()); - let mut tips = Tips::new(); - tips.add("Try `cr query ns` to see available namespaces."); - tips.print(); - } else if !found_definitions.is_empty() { - let (first_ns, first_def) = &found_definitions[0]; - let mut tips = Tips::new(); - tips.add(format!("Use `cr query peek {first_ns}/{first_def}` to see signature.")); - tips.print(); } Ok(()) diff --git a/src/bin/cli_handlers/tips.rs b/src/bin/cli_handlers/tips.rs index e22b0c37..7807b849 100644 --- a/src/bin/cli_handlers/tips.rs +++ b/src/bin/cli_handlers/tips.rs @@ -142,19 +142,3 @@ pub fn tip_root_edit(path_is_empty: bool) -> Option { None } } - -/// Tips for `cr query ns` when listing namespaces -pub fn tip_query_ns_list(include_deps: bool) -> Vec { - let mut tips = Vec::new(); - tips.push("Use `cr query ns ` to show namespace details.".to_string()); - tips.push("Use `cr query defs ` to list definitions.".to_string()); - if !include_deps { - tips.push("Use `--deps` to include dependency and core namespaces.".to_string()); - } - tips -} - -/// Tips for `cr query defs` when showing definitions list -pub fn tip_query_defs_list() -> Vec { - vec!["Use `cr query peek ` for signature, `cr query def ` for full code.".to_string()] -} From 658392130bf2a79427841a93f6c54079239b0b49 Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 23 Mar 2026 11:38:03 +0800 Subject: [PATCH 35/57] Refine tool output around command echo --- docs/CalcitAgent.md | 8 +- docs/run/upgrade.md | 1 - ...3-1137-command-echo-tool-output-cleanup.md | 71 +++++ src/bin/cli_handlers/docs.rs | 43 +-- src/bin/cli_handlers/query.rs | 70 +---- src/bin/cli_handlers/tree.rs | 267 ++++-------------- src/bin/cr.rs | 21 +- src/bin/injection/mod.rs | 4 +- src/lib.rs | 15 +- 9 files changed, 164 insertions(+), 336 deletions(-) create mode 100644 editing-history/2026-0323-1137-command-echo-tool-output-cleanup.md diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index 91ea0cab..992e4ea5 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -35,7 +35,7 @@ 示例表达式(简化): -```cirru +```cirru.no-check defn demo (state) let result $ collect! state @@ -55,7 +55,7 @@ defn demo (state) `$` 用于把右侧表达式折叠成一个子结构,通常会让目标节点进入更深一层。 -```cirru +```cirru.no-check ; "写法 A" result $ collect! state @@ -70,7 +70,7 @@ result (collect! state) `,` 常用于告诉解析器“这里是值节点,不是再发起一次调用”。 -```cirru +```cirru.no-check ; "写法 A" a (b c) d @@ -88,7 +88,7 @@ a Agent 切到新窗口时,优先把 `compact.cirru` 看成一个“可执行项目快照”,其顶层 EDN 结构通常是: -```cirru +```cirru.no-check {} :package |my-app :configs $ {} diff --git a/docs/run/upgrade.md b/docs/run/upgrade.md index ae141233..68fe2f88 100644 --- a/docs/run/upgrade.md +++ b/docs/run/upgrade.md @@ -42,7 +42,6 @@ cr --version ### Step C:检查并更新依赖 ```bash -caps outdated caps outdated --yes ``` diff --git a/editing-history/2026-0323-1137-command-echo-tool-output-cleanup.md b/editing-history/2026-0323-1137-command-echo-tool-output-cleanup.md new file mode 100644 index 00000000..bca2581a --- /dev/null +++ b/editing-history/2026-0323-1137-command-echo-tool-output-cleanup.md @@ -0,0 +1,71 @@ +# Command Echo 主导的工具输出收敛与 tree replace 差异摘要 + +## 变更概述 + +- 将 `cr` 的命令回显进一步固定为工具主语境:前置、单行、语义化参数展示。 +- 为工具型命令引入更安静的 runtime 输出模式,避免版本、模块目录、模块加载、平台 API 注册等噪音干扰结果读取。 +- 大范围清理 `docs/query/tree` 正文里对 `target/path/pattern/entry/deps` 的重复回显,统一交给 command echo 表达。 +- 修正 `docs/CalcitAgent.md` 中示意性 Cirru 代码块的检查模式,避免 `check-md` 将结构示例误判为 runnable 程序。 +- 单独重构 `tree replace` 输出:从“Preview + From/To”双份重复,改成“Changed node + Containing expression”的单次差异摘要。 + +## 关键实现 + +- `src/bin/cr.rs` + - 在启用 command echo 时同步启用 `calcit::set_quiet_tool_output(true)`。 + - 屏蔽工具模式下的运行环境提示: + - `calcit version` + - `module folder` + - `stack trace disabled` + - `running entry` + +- `src/lib.rs` + - 新增全局 quiet flag: + - `set_quiet_tool_output(...)` + - `quiet_tool_output()` + - `load_module(...)` 在工具模式下不再打印 `loading: ...`。 + +- `src/bin/injection/mod.rs` + - `inject_platform_apis()` 在工具模式下不再打印 `registered platform APIs`。 + +- `src/bin/cli_handlers/docs.rs` + - `docs check-md` 不再重复回显 `entry/deps`。 + - 移除只服务旧回显的路径展示 helper。 + +- `src/bin/cli_handlers/query.rs` + - 删除 `query def/peek/schema/examples/usages` 中重复的 target 标题。 + - 删除 `query search/search-expr` 中重复的 pattern/filter/entry/start-path 摘要。 + - `query find` 摘要从“重复 symbol”收敛为纯结果计数。 + +- `src/bin/cli_handlers/tree.rs` + - 删除 `tree show` 与多类 tree 写操作中的冗余 follow-up 命令提示。 + - 删除成功文案里重复的 `path/target/pattern` 回显。 + - `tree replace` 改为: + - `✓ Replaced node` + - `Changed node` 下单次展示 `Before/After` + - 非 root path 额外展示 `Containing expression`,帮助快速看出修改落点。 + +- `docs/CalcitAgent.md` + - 将示意性代码块改成 `cirru.no-check`,避免 `docs check-md` 因不存在的符号或非独立程序片段失败。 + +## 输出策略结论 + +- 输入参数语境:由 `Command: ...` 统一承担。 +- 正文输出:只保留结果、差异、结构上下文、或真正新增的信息。 +- 不再在正文里重复打印 command echo 已经覆盖的 target/path/pattern/entry/deps。 + +## 验证摘要 + +- `cargo fmt` +- `cargo run --bin cr -- calcit/test.cirru analyze js-escape 'demo?'` +- `cargo run --bin cr -- calcit/test.cirru analyze js-unescape 'demo_$q_'` +- `cargo run --bin cr -- calcit/test.cirru query ns app.main` +- `cargo run --bin cr -- calcit/test.cirru query find render` +- `cargo run --bin cr -- calcit/test.cirru tree show app.main/test-json -p ''` +- `cargo run --bin cr -- calcit/test.cirru docs search chunk -f agent-advanced.md` +- `cargo run --bin cr -- demos/compact.cirru docs check-md docs/CalcitAgent.md` +- `cargo run --bin cr -- /tmp/calcit-cli-demo/compact.cirru tree replace ...` 多次人工检查 replace 输出形态 + +## 经验 + +- command echo 一旦语义化,就应把正文里的“参数再描述一遍”系统性删掉,否则会让工具输出看起来像教程而不是结果。 +- tree 类命令相比简单成功提示,更需要“差异摘要 + 所在表达式”,这样既不冗余,也能快速判断改动是否落在预期结构。 diff --git a/src/bin/cli_handlers/docs.rs b/src/bin/cli_handlers/docs.rs index 765d2541..3777c169 100644 --- a/src/bin/cli_handlers/docs.rs +++ b/src/bin/cli_handlers/docs.rs @@ -504,25 +504,6 @@ fn module_folder() -> Result { Ok(Path::new(&home).join(".config/calcit/modules/")) } -fn display_path_with_default_modules(path: &str, default_modules: &Path) -> String { - if let Ok(stripped) = Path::new(path).strip_prefix(default_modules) { - format!("/{}", stripped.display()) - } else { - path.to_owned() - } -} - -fn display_path_for_check_md(path: &str, default_modules: &Path, cwd: Option<&Path>) -> String { - let raw = Path::new(path); - if raw.is_absolute() - && let Some(current_dir) = cwd - && let Ok(stripped) = raw.strip_prefix(current_dir) - { - return stripped.display().to_string(); - } - display_path_with_default_modules(path, default_modules) -} - fn load_shared_files_for_check_md(entry: &str, deps: &[String]) -> Result, String> { ensure_runtime_initialized(); @@ -652,29 +633,7 @@ fn handle_check_md(file_path: &str, entry: &str, deps: &[String]) -> Result<(), let shared_files = load_shared_files_for_check_md(entry, deps)?; - let default_modules = module_folder()?; - let current_dir = std::env::current_dir().ok(); - - let entry_preview = display_path_for_check_md(entry, &default_modules, current_dir.as_deref()); - - let deps_preview = if deps.is_empty() { - "none".dimmed().to_string() - } else { - deps - .iter() - .map(|dep| display_path_for_check_md(dep, &default_modules, current_dir.as_deref())) - .collect::>() - .join(", ") - .dimmed() - .to_string() - }; - println!( - "{} {} (entry: {}, deps: {})", - "Checking".bold(), - file_path.cyan(), - entry_preview.dimmed(), - deps_preview - ); + println!("{} {}", "Checking".bold(), file_path.cyan()); println!("{}", "-".repeat(60).dimmed()); let mut passed = 0; diff --git a/src/bin/cli_handlers/query.rs b/src/bin/cli_handlers/query.rs index 10cce5af..df8f89ce 100644 --- a/src/bin/cli_handlers/query.rs +++ b/src/bin/cli_handlers/query.rs @@ -3,7 +3,7 @@ //! Handles: cr query ns, defs, def, at, peek, examples, find, usages, pkg, config, error, modules use super::chunk_display::{ChunkDisplayOptions, ChunkedDisplay, maybe_chunk_node}; -use super::common::{format_path, format_path_bracketed, parse_path}; +use super::common::{format_path, parse_path}; use super::tips::{TipPriority, Tips, command_guidance_enabled}; use calcit::CalcitTypeAnnotation; use calcit::cli_args::{QueryCommand, QueryDefCommand, QuerySubcommand}; @@ -425,8 +425,6 @@ fn handle_ns_details(input_path: &str, namespace: &str) -> Result<(), String> { .get(namespace) .ok_or_else(|| format!("Namespace '{namespace}' not found"))?; - println!("{} {}", "Namespace:".bold(), namespace.cyan()); - if !file_data.ns.doc.is_empty() { println!("{} {}", "Doc:".bold(), file_data.ns.doc); } @@ -451,7 +449,7 @@ fn handle_defs(input_path: &str, namespace: &str) -> Result<(), String> { let mut defs: Vec<&String> = file_data.defs.keys().collect(); defs.sort(); - println!("{} {} ({} definitions)", "Namespace:".bold(), namespace.cyan(), defs.len()); + println!("{} {}", "Definitions:".bold(), defs.len()); for def in &defs { let entry = &file_data.defs[*def]; @@ -658,8 +656,6 @@ fn handle_def(input_path: &str, namespace: &str, definition: &str, opts: &QueryD .get(definition) .ok_or_else(|| format!("Definition '{definition}' not found in namespace '{namespace}'"))?; - println!("{} {}/{}", "Definition:".bold(), namespace.cyan(), definition.green()); - if let Ok(code_data) = calcit::data::cirru::code_to_calcit(&code_entry.code, namespace, definition, vec![]) { if let Some(summary) = CalcitTypeAnnotation::summarize_code(&code_data) { println!("{} {}", "Type:".bold(), summary); @@ -755,8 +751,6 @@ fn handle_examples(input_path: &str, namespace: &str, definition: &str) -> Resul .get(definition) .ok_or_else(|| format!("Definition '{definition}' not found in namespace '{namespace}'"))?; - println!("{} {}/{}", "Examples for:".bold(), namespace.cyan(), definition.green()); - if code_entry.examples.is_empty() { println!("\n{}", "(no examples)".dimmed()); } else { @@ -795,8 +789,6 @@ fn handle_peek(input_path: &str, namespace: &str, definition: &str) -> Result<() .get(definition) .ok_or_else(|| format!("Definition '{definition}' not found in namespace '{namespace}'"))?; - println!("{} {}/{}", "Definition:".bold(), namespace.cyan(), definition.green()); - // Always show doc (even if empty) if code_entry.doc.is_empty() { println!("{} -", "Doc:".bold()); @@ -871,8 +863,6 @@ fn handle_schema(input_path: &str, namespace: &str, definition: &str, json: bool return Ok(()); } - println!("{} {}/{}", "Definition:".bold(), namespace.cyan(), definition.green()); - if let CalcitTypeAnnotation::Fn(fn_annot) = code_entry.schema.as_ref() { let cirru = snapshot::schema_edn_to_cirru(&fn_annot.to_wrapped_schema_edn())?; println!("{} {}", "Schema:".bold(), cirru.format_one_liner()?.dimmed()); @@ -934,9 +924,8 @@ fn handle_find(input_path: &str, symbol: &str, include_deps: bool, detail_offset // Print summary println!( - "{} '{}' - {} definition(s), {} reference(s)\n", - "Symbol:".bold(), - symbol.yellow(), + "{} {} definition(s), {} reference(s)\n", + "Matches:".bold(), found_definitions.len(), found_references.len().saturating_sub(found_definitions.len()) ); @@ -1079,13 +1068,7 @@ fn handle_usages(input_path: &str, target_ns: &str, target_def: &str, include_de } } - println!( - "{} {}/{} ({} usages)", - "Usages of:".bold(), - target_ns.cyan(), - target_def.green(), - usages.len() - ); + println!("{} {}", "Usages:".bold(), usages.len()); if usages.is_empty() { println!( @@ -1259,7 +1242,7 @@ fn handle_fuzzy_search(input_path: &str, pattern: &str, include_deps: bool, limi let total = results.len(); let displayed: Vec<_> = results.into_iter().take(limit).collect(); - println!("{} {} results for pattern \"{}\"", "Search:".bold(), total, pattern.yellow()); + println!("{} {} results", "Search:".bold(), total); if displayed.is_empty() { println!(" {}", "No matches found".dimmed()); @@ -1339,28 +1322,6 @@ fn handle_search_leaf(input_path: &str, pattern: &str, start_path: Option<&str>, None }; - println!("{} Searching for:", "Search:".bold()); - if common_opts.loose { - println!(" {} (contains)", pattern.yellow()); - } else { - println!(" {} (exact)", pattern.yellow()); - } - - if let Some(filter_str) = common_opts.filter { - println!(" {} {}", "Filter:".dimmed(), filter_str.cyan()); - } else { - println!(" {} {}", "Scope:".dimmed(), "entire project".cyan()); - } - if let Some(entry_name) = common_opts.entry { - println!(" {} {}", "Entry:".dimmed(), entry_name.cyan()); - } - - if let Some(ref path) = parsed_start_path { - let path_display = format_path_bracketed(path); - println!(" {} {}", "Start path:".dimmed(), path_display.cyan()); - } - println!(); - let mut all_results: SearchResults = Vec::new(); // Parse filter to determine scope @@ -1518,30 +1479,11 @@ fn handle_search_expr(input_path: &str, pattern: &str, json: bool, common_opts: .clone() }; - println!("{} Searching for pattern:", "Search:".bold()); - - let pattern_display = pattern_node.format_one_liner().unwrap_or_default(); let highlight_target: Option<&str> = match &pattern_node { Cirru::Leaf(s) => Some(s.as_ref()), _ => None, }; - if common_opts.loose { - println!(" {} (substring match for leaf patterns)", pattern_display.yellow()); - } else { - println!(" {} (exact match)", pattern_display.yellow()); - } - - if let Some(filter_str) = common_opts.filter { - println!(" {} {}", "Filter:".dimmed(), filter_str.cyan()); - } else { - println!(" {} {}", "Scope:".dimmed(), "entire project".cyan()); - } - if let Some(entry_name) = common_opts.entry { - println!(" {} {}", "Entry:".dimmed(), entry_name.cyan()); - } - println!(); - let mut all_results: SearchResults = Vec::new(); let (filter_ns, filter_def) = if let Some(f) = common_opts.filter { diff --git a/src/bin/cli_handlers/tree.rs b/src/bin/cli_handlers/tree.rs index 61bb2812..0bb22df1 100644 --- a/src/bin/cli_handlers/tree.rs +++ b/src/bin/cli_handlers/tree.rs @@ -123,6 +123,19 @@ fn format_preview_with_type(node: &Cirru, max_lines: usize) -> String { } } +fn print_preview_block(label: &str, node: &Cirru, max_lines: usize, color: &str) { + let label_text = match color { + "yellow" => label.yellow().bold(), + "green" => label.green().bold(), + "cyan" => label.cyan().bold(), + _ => label.bold(), + }; + println!("{label_text}:"); + for line in format_preview_with_type(node, max_lines).lines() { + println!(" {line}"); + } +} + /// Find the first leaf (preorder) and format for preview fn first_leaf_preview(node: &Cirru) -> Option { match node { @@ -147,15 +160,10 @@ fn format_child_preview(node: &Cirru) -> String { } /// Show a side-by-side diff preview of the change -fn show_diff_preview(old_node: &Cirru, new_node: &Cirru, operation: &str, path: &[usize]) -> String { +fn show_diff_preview(old_node: &Cirru, new_node: &Cirru, operation: &str) -> String { let mut output = String::new(); - output.push_str(&format!( - "\n{}: {} at path [{}]\n", - "Preview".blue().bold(), - operation, - format_path(path) - )); + output.push_str(&format!("\n{}: {}\n", "Preview".blue().bold(), operation)); output.push('\n'); // Show old and new side by side (simplified version) @@ -341,19 +349,6 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> } }; - // Print info - let path_display = if path.is_empty() { - "(root)".to_string() - } else { - format_path(&path) - }; - println!( - "{}: {} path: [{}]", - "Location".green().bold(), - format!("{namespace}/{definition}").cyan(), - path_display - ); - let node_type = match &node { Cirru::Leaf(_) => "leaf", Cirru::List(items) => { @@ -390,23 +385,6 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> println!(); } - if command_guidance_enabled() { - println!("{}: To modify this node:", "Next steps".blue().bold()); - println!( - " • Replace: {} {} -p '{}' {}", - "cr tree replace".cyan(), - opts.target, - format_path(&path), - "-e 'cirru one-liner'".dimmed() - ); - println!( - " • Delete: {} {} -p '{}'", - "cr tree delete".cyan(), - opts.target, - format_path(&path) - ); - println!(); - } let mut tips = Tips::new(); if let Some((shown_fragments, total_fragments)) = shown_fragments { if shown_fragments < total_fragments { @@ -434,25 +412,6 @@ fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> println!("{}: {:?}", "Value".green().bold(), s.as_ref()); println!(); if command_guidance_enabled() { - println!("{}: To modify this leaf:", "Next steps".blue().bold()); - println!( - " • Replace: {} {} -p '{}' --leaf -e ''", - "cr tree replace".cyan(), - opts.target, - format_path(&path) - ); - if !path.is_empty() { - // Show parent path for context - let parent_path = &path[..path.len() - 1]; - let parent_path_str = format_path(parent_path); - println!( - " • View parent: {} {} -p '{}'", - "cr tree show".cyan(), - opts.target, - parent_path_str - ); - } - println!(); println!( "{}: Use {} for symbols, {} for strings", "Tip".blue().bold(), @@ -491,9 +450,6 @@ fn handle_replace(opts: &TreeReplaceCommand, snapshot_file: &str) -> Result<(), // Save original for comparison let old_node = navigate_to_path(&code_entry.code, &path)?; - - // Show diff preview - println!("{}", show_diff_preview(&old_node, &new_node, "replace", &path)); // Tips: root-edit guidance if let Some(t) = tip_root_edit(path.is_empty()) { let mut tips = Tips::new(); @@ -506,31 +462,25 @@ fn handle_replace(opts: &TreeReplaceCommand, snapshot_file: &str) -> Result<(), save_snapshot(&snapshot, snapshot_file)?; - println!( - "{} Applied 'replace' at path [{}] in '{}/{}'", - "✓".green(), - path.iter().map(|i| i.to_string()).collect::>().join(","), - namespace, - definition - ); - println!(); - println!("{}:", "From".yellow().bold()); - println!("{}", format_preview_with_type(&old_node, 20)); + let replaced_node = navigate_to_path(&new_code, &path)?; + + println!("{} Replaced node", "✓".green()); println!(); - println!("{}:", "To".green().bold()); - let new_node = navigate_to_path(&new_code, &path)?; - println!("{}", format_preview_with_type(&new_node, 20)); + println!("{}", "Changed node".blue().bold()); + print_preview_block("Before", &old_node, 20, "yellow"); println!(); - if command_guidance_enabled() { - println!("{}", "Next steps:".blue().bold()); - println!( - " • Verify: {} '{}' -p '{}'", - "cr tree show".cyan(), - format_args!("{}/{}", namespace, definition), - path.iter().map(|i| i.to_string()).collect::>().join(",") - ); - println!(" • Check errors: {}", "cr query error".cyan()); - println!(" • Find usages: {} '{}/{}'", "cr query usages".cyan(), namespace, definition); + print_preview_block("After", &replaced_node, 20, "green"); + + if !path.is_empty() { + let parent_path = &path[..path.len() - 1]; + let parent_after = if parent_path.is_empty() { + new_code.clone() + } else { + navigate_to_path(&new_code, parent_path)?.clone() + }; + println!(); + println!("{}", "Containing expression".blue().bold()); + print_preview_block("After", &parent_after, 12, "cyan"); } Ok(()) @@ -571,7 +521,7 @@ fn handle_rewrite(opts: &TreeStructuralCommand, snapshot_file: &str) -> Result<( let old_node = navigate_to_path(&code_entry.code, &path)?; // Show diff preview - println!("{}", show_diff_preview(&old_node, &processed_node, "rewrite", &path)); + println!("{}", show_diff_preview(&old_node, &processed_node, "rewrite")); // Tips: root-edit guidance if let Some(t) = tip_root_edit(path.is_empty()) { let mut tips = Tips::new(); @@ -584,13 +534,7 @@ fn handle_rewrite(opts: &TreeStructuralCommand, snapshot_file: &str) -> Result<( save_snapshot(&snapshot, snapshot_file)?; - println!( - "{} Applied 'rewrite' at path [{}] in '{}/{}'", - "✓".green(), - path.iter().map(|i| i.to_string()).collect::>().join(","), - namespace, - definition - ); + println!("{} Applied 'rewrite'", "✓".green()); println!(); println!("{}:", "From".yellow().bold()); println!("{}", format_preview_with_type(&old_node, 20)); @@ -599,18 +543,6 @@ fn handle_rewrite(opts: &TreeStructuralCommand, snapshot_file: &str) -> Result<( let new_node = navigate_to_path(&new_code, &path)?; println!("{}", format_preview_with_type(&new_node, 20)); println!(); - if command_guidance_enabled() { - println!("{}", "Next steps:".blue().bold()); - println!( - " • Verify: {} '{}' -p '{}'", - "cr tree show".cyan(), - format_args!("{}/{}", namespace, definition), - path.iter().map(|i| i.to_string()).collect::>().join(",") - ); - println!(" • Check errors: {}", "cr query error".cyan()); - println!(" • Find usages: {} '{}/{}'", "cr query usages".cyan(), namespace, definition); - } - Ok(()) } @@ -643,14 +575,7 @@ fn handle_replace_leaf(opts: &TreeReplaceLeafCommand, snapshot_file: &str) -> Re return Ok(()); } - println!( - "{} Found {} match(es) for pattern '{}' in '{}/{}':", - "Search:".bold(), - matches.len(), - opts.pattern.yellow(), - namespace, - definition - ); + println!("{} {} match(es):", "Search:".bold(), matches.len()); println!(); // Show preview of matches @@ -693,13 +618,7 @@ fn handle_replace_leaf(opts: &TreeReplaceLeafCommand, snapshot_file: &str) -> Re code_entry.code = new_code; save_snapshot(&snapshot, snapshot_file)?; - println!( - "{} Replaced {} occurrence(s) in '{}/{}'", - "✓".green(), - replaced_count, - namespace, - definition - ); + println!("{} Replaced {} occurrence(s)", "✓".green(), replaced_count); println!(); println!("{}:", "Replacement".green().bold()); println!( @@ -708,13 +627,6 @@ fn handle_replace_leaf(opts: &TreeReplaceLeafCommand, snapshot_file: &str) -> Re format_preview_with_type(&replacement_node, 0) ); println!(); - if command_guidance_enabled() { - println!("{}", "Next steps:".blue().bold()); - println!(" • Verify: {} '{}/{}'", "cr query def".cyan(), namespace, definition); - println!(" • Check errors: {}", "cr query error".cyan()); - println!(" • Find usages: {} '{}/{}'", "cr query usages".cyan(), namespace, definition); - } - Ok(()) } @@ -743,21 +655,11 @@ fn handle_target_replace(opts: &TreeTargetReplaceCommand, snapshot_file: &str) - let matches = find_all_leaf_matches(&code_entry.code, &opts.pattern, &[]); if matches.is_empty() { - return Err(format!( - "No matches found for pattern '{}' in '{}/{}'", - opts.pattern, namespace, definition - )); + return Err("No matches found for target pattern".to_string()); } if matches.len() > 1 { - println!( - "{} Found {} matches for pattern '{}' in '{}/{}'.", - "Notice:".yellow().bold(), - matches.len(), - opts.pattern.yellow(), - namespace, - definition - ); + println!("{} Found {} matches.", "Notice:".yellow().bold(), matches.len()); println!("Please use specific path to replace:"); println!(); @@ -799,20 +701,14 @@ fn handle_target_replace(opts: &TreeTargetReplaceCommand, snapshot_file: &str) - let old_node = Cirru::Leaf(old_value.to_string().into()); // Show diff preview - println!("{}", show_diff_preview(&old_node, &replacement_node, "target-replace", path)); + println!("{}", show_diff_preview(&old_node, &replacement_node, "target-replace")); let new_code = apply_operation_at_path(&code_entry.code, path, "replace", Some(&replacement_node))?; code_entry.code = new_code; save_snapshot(&snapshot, snapshot_file)?; - println!( - "{} Replaced unique occurrence in '{}/{}' at path [{}]", - "✓".green(), - namespace, - definition, - path.iter().map(|i| i.to_string()).collect::>().join(",") - ); + println!("{} Replaced unique occurrence", "✓".green()); Ok(()) } @@ -868,11 +764,7 @@ fn handle_delete(opts: &TreeDeleteCommand, snapshot_file: &str) -> Result<(), St }; // Show diff preview with parent context - println!( - "\n{}: Deleting node at path [{}]", - "Preview".blue().bold(), - path.iter().map(|i| i.to_string()).collect::>().join(",") - ); + println!("\n{}: delete", "Preview".blue().bold()); println!("{}:", "Node to delete".yellow().bold()); println!("{}", format_preview_with_type(&old_node, 10)); println!(); @@ -890,13 +782,7 @@ fn handle_delete(opts: &TreeDeleteCommand, snapshot_file: &str) -> Result<(), St save_snapshot(&snapshot, snapshot_file)?; - println!( - "{} Deleted node at path [{}] in '{}/{}'", - "✓".green(), - path.iter().map(|i| i.to_string()).collect::>().join(","), - namespace, - definition - ); + println!("{} Deleted node", "✓".green()); println!(); println!("{}:", "Deleted node".yellow().bold()); println!("{}", format_preview_with_type(&old_node, 20)); @@ -1121,12 +1007,7 @@ fn generic_insert_handler( }; // Show diff preview - println!( - "\n{}: {} at path [{}]", - "Preview".blue().bold(), - operation, - path.iter().map(|i| i.to_string()).collect::>().join(",") - ); + println!("\n{}: {}", "Preview".blue().bold(), operation); println!("{}:", "Node to insert".cyan().bold()); println!("{}", format_preview_with_type(&processed_node, 8)); println!(); @@ -1144,14 +1025,7 @@ fn generic_insert_handler( save_snapshot(&snapshot, snapshot_file)?; - println!( - "{} Applied '{}' at path [{}] in '{}/{}'", - "✓".green(), - operation, - path.iter().map(|i| i.to_string()).collect::>().join(","), - namespace, - definition - ); + println!("{} Applied '{}'", "✓".green(), operation); println!(); println!("{}:", "Inserted node".cyan().bold()); println!("{}", format_preview_with_type(&processed_node, 10)); @@ -1255,14 +1129,7 @@ fn generic_swap_handler(target: &str, path_str: &str, operation: &str, snapshot_ save_snapshot(&snapshot, snapshot_file)?; - println!( - "{} Applied '{}' at path [{}] in '{}/{}'", - "✓".green(), - operation, - path.iter().map(|i| i.to_string()).collect::>().join(","), - namespace, - definition - ); + println!("{} Applied '{}'", "✓".green(), operation); println!(); // Explain what was swapped @@ -1385,15 +1252,7 @@ fn handle_unwrap(opts: &TreeUnwrapCommand, snapshot_file: &str) -> Result<(), St return Err(format!("Node at path [{}] has no children to splice", opts.path)); } - let path_str = path.iter().map(|i| i.to_string()).collect::>().join(","); - println!( - "\n{}: unwrap [{}] in '{}/{}', splicing {} children into parent", - "Preview".blue().bold(), - path_str, - namespace, - definition, - children.len() - ); + println!("\n{}: unwrap {} child(ren)", "Preview".blue().bold(), children.len()); println!("{}:", "Before".dimmed()); println!("{}", format_preview_with_type(&node, opts.depth)); println!("{} (spliced):", "After".cyan().bold()); @@ -1407,7 +1266,7 @@ fn handle_unwrap(opts: &TreeUnwrapCommand, snapshot_file: &str) -> Result<(), St save_snapshot(&snapshot, snapshot_file)?; - println!("{} Unwrapped node at [{}] in '{}/{}'", "✓".green(), path_str, namespace, definition); + println!("{} Unwrapped node", "✓".green()); Ok(()) } @@ -1438,17 +1297,7 @@ fn handle_raise(opts: &TreeRaiseCommand, snapshot_file: &str) -> Result<(), Stri let child_node = navigate_to_path(&code_entry.code, &path)?.clone(); let parent_node = navigate_to_path(&code_entry.code, parent_path)?; - let path_str = path.iter().map(|i| i.to_string()).collect::>().join(","); - let parent_path_str = parent_path.iter().map(|i| i.to_string()).collect::>().join(","); - - println!( - "\n{}: raise [{}] in '{}/{}', replacing parent [{}]", - "Preview".blue().bold(), - path_str, - namespace, - definition, - if parent_path_str.is_empty() { "(root)" } else { &parent_path_str } - ); + println!("\n{}: raise", "Preview".blue().bold()); println!("{}:", "Before (parent)".dimmed()); println!("{}", format_preview_with_type(&parent_node, opts.depth)); println!("{}:", "After (raised child)".cyan().bold()); @@ -1460,14 +1309,7 @@ fn handle_raise(opts: &TreeRaiseCommand, snapshot_file: &str) -> Result<(), Stri save_snapshot(&snapshot, snapshot_file)?; - println!( - "{} Raised node [{}] to replace parent [{}] in '{}/{}'", - "✓".green(), - path_str, - if parent_path_str.is_empty() { "(root)" } else { &parent_path_str }, - namespace, - definition - ); + println!("{} Raised node", "✓".green()); Ok(()) } @@ -1501,14 +1343,7 @@ fn handle_wrap(opts: &TreeWrapCommand, snapshot_file: &str) -> Result<(), String let new_node = process_node_with_references(&template, &references)?; - let path_str = path.iter().map(|i| i.to_string()).collect::>().join(","); - println!( - "\n{}: wrap [{}] in '{}/{}'", - "Preview".blue().bold(), - path_str, - namespace, - definition - ); + println!("\n{}: wrap", "Preview".blue().bold()); println!("{}:", "Before".dimmed()); println!("{}", format_preview_with_type(&original_node, opts.depth)); println!("{}:", "After".cyan().bold()); @@ -1520,7 +1355,7 @@ fn handle_wrap(opts: &TreeWrapCommand, snapshot_file: &str) -> Result<(), String save_snapshot(&snapshot, snapshot_file)?; - println!("{} Wrapped node at [{}] in '{}/{}'", "✓".green(), path_str, namespace, definition); + println!("{} Wrapped node", "✓".green()); Ok(()) } diff --git a/src/bin/cr.rs b/src/bin/cr.rs index e8040f76..7f3c47a1 100644 --- a/src/bin/cr.rs +++ b/src/bin/cr.rs @@ -52,6 +52,7 @@ fn main() -> Result<(), String> { if cli_handlers::should_echo_command(&cli_args) { cli_handlers::suppress_command_guidance(); + calcit::set_quiet_tool_output(true); cli_handlers::print_command_echo(&cli_args); } @@ -82,7 +83,7 @@ fn main() -> Result<(), String> { let is_eval_mode = matches!(&cli_args.subcommand, Some(CalcitCommand::Eval(_))); let assets_watch = cli_args.watch_dir.to_owned(); - if !cli_args.version { + if !cli_args.version && !calcit::quiet_tool_output() { eprintln!("{}", format!("calcit version: {}", cli_args::CALCIT_VERSION).dimmed()); } if cli_args.version { @@ -103,14 +104,18 @@ fn main() -> Result<(), String> { let module_folder = home_dir() .map(|buf| buf.as_path().join(".config/calcit/modules/")) .expect("failed to load $HOME"); - eprintln!( - "{}", - format!("module folder: {}", module_folder.to_str().expect("extract path")).dimmed() - ); + if !calcit::quiet_tool_output() { + eprintln!( + "{}", + format!("module folder: {}", module_folder.to_str().expect("extract path")).dimmed() + ); + } if cli_args.disable_stack { call_stack::set_using_stack(false); - println!("stack trace disabled.") + if !calcit::quiet_tool_output() { + println!("stack trace disabled.") + } } let input_path = PathBuf::from(&cli_args.input); @@ -153,7 +158,9 @@ fn main() -> Result<(), String> { // config in entry will overwrite default configs if let Some(entry) = cli_args.entry.to_owned() { if snapshot.entries.contains_key(entry.as_str()) { - println!("running entry: {entry}"); + if !calcit::quiet_tool_output() { + println!("running entry: {entry}"); + } snapshot.entries[entry.as_str()].clone_into(&mut snapshot.configs); } else { return Err(format!( diff --git a/src/bin/injection/mod.rs b/src/bin/injection/mod.rs index c1dd17a8..cf79a709 100644 --- a/src/bin/injection/mod.rs +++ b/src/bin/injection/mod.rs @@ -191,7 +191,9 @@ pub fn inject_platform_apis() { ); builtins::register_import_proc("async-sleep", builtins::meta::async_sleep); builtins::register_import_proc("on-control-c", on_ctrl_c); - eprintln!("{}", "registered platform APIs".dimmed()); + if !calcit::quiet_tool_output() { + eprintln!("{}", "registered platform APIs".dimmed()); + } } // &call-dylib-edn diff --git a/src/lib.rs b/src/lib.rs index 51b42a0e..9879c1b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ use std::cell::RefCell; use std::fs; use std::path::Path; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; pub use calcit::{ Calcit, CalcitErr, CalcitFnTypeAnnotation, CalcitProc, CalcitSyntax, CalcitTypeAnnotation, ProcTypeSignature, SyntaxTypeSignature, @@ -25,6 +26,16 @@ pub use calcit::{ use crate::util::string::strip_shebang; +static QUIET_TOOL_OUTPUT: AtomicBool = AtomicBool::new(false); + +pub fn set_quiet_tool_output(v: bool) { + QUIET_TOOL_OUTPUT.store(v, Ordering::Relaxed); +} + +pub fn quiet_tool_output() -> bool { + QUIET_TOOL_OUTPUT.load(Ordering::Relaxed) +} + pub fn load_core_snapshot() -> Result { // load core libs let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/calcit-core.rmp")); @@ -125,7 +136,9 @@ pub fn load_module(path: &str, base_dir: &Path, module_folder: &Path) -> Result< format!("/{file_path}") }; - println!("loading: {display_path}"); + if !quiet_tool_output() { + println!("loading: {display_path}"); + } let mut content = fs::read_to_string(&fullpath).unwrap_or_else(|_| panic!("expected Cirru snapshot {fullpath:?}")); strip_shebang(&mut content); From ffa000d2b117ab06d1dc13687bd9865acac1303a Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 23 Mar 2026 12:21:11 +0800 Subject: [PATCH 36/57] Fix IR impl dump --- src/codegen/gen_ir.rs | 70 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/src/codegen/gen_ir.rs b/src/codegen/gen_ir.rs index 3834ae4d..bb2a02dc 100644 --- a/src/codegen/gen_ir.rs +++ b/src/codegen/gen_ir.rs @@ -7,8 +7,8 @@ use std::sync::Arc; use cirru_edn::{Edn, EdnListView, format}; use crate::calcit::{ - Calcit, CalcitArgLabel, CalcitEnum, CalcitFnArgs, CalcitFnTypeAnnotation, CalcitImport, CalcitLocal, CalcitRecord, CalcitStruct, - CalcitTuple, CalcitTypeAnnotation, ImportInfo, MethodKind, + Calcit, CalcitArgLabel, CalcitEnum, CalcitFnArgs, CalcitFnTypeAnnotation, CalcitImpl, CalcitImport, CalcitLocal, CalcitRecord, + CalcitStruct, CalcitTuple, CalcitTypeAnnotation, ImportInfo, MethodKind, }; use crate::program; @@ -249,6 +249,7 @@ pub(crate) fn dump_code(code: &Calcit) -> Edn { } Calcit::Tuple(tuple) => dump_tuple_code(tuple), Calcit::Record(record) => dump_record_code(record), + Calcit::Impl(impl_def) => dump_impl_code(impl_def), Calcit::Struct(struct_def) => dump_struct_code(struct_def), Calcit::Enum(enum_def) => dump_enum_code(enum_def), Calcit::Method(method, kind) => { @@ -493,6 +494,27 @@ fn dump_record_code(record: &CalcitRecord) -> Edn { Edn::map_from_iter(entries) } +fn dump_impl_code(impl_def: &CalcitImpl) -> Edn { + let mut entries = vec![ + (Edn::tag("kind"), Edn::tag("impl")), + (Edn::tag("name"), Edn::Str(impl_def.name.ref_str().into())), + ]; + if let Some(trait_def) = impl_def.origin() { + entries.push((Edn::tag("trait"), Edn::Str(trait_def.name.ref_str().into()))); + } + + let mut fields = EdnListView::default(); + for (field, value) in impl_def.fields.iter().zip(impl_def.values.iter()) { + fields.push(Edn::map_from_iter([ + (Edn::tag("field"), Edn::Str(field.ref_str().into())), + (Edn::tag("value"), dump_code(value)), + ])); + } + entries.push((Edn::tag("fields"), fields.into())); + entries.push((Edn::tag("field-count"), Edn::Number(impl_def.fields.len() as f64))); + Edn::map_from_iter(entries) +} + fn dump_struct_code(struct_def: &CalcitStruct) -> Edn { let mut entries = vec![ (Edn::tag("kind"), Edn::tag("struct")), @@ -558,6 +580,50 @@ fn record_metadata(record: &CalcitRecord) -> Vec<(Edn, Edn)> { entries } +#[cfg(test)] +mod tests { + use super::dump_code; + use crate::calcit::{Calcit, CalcitImpl, CalcitProc}; + use cirru_edn::{Edn, EdnTag}; + use std::sync::Arc; + + #[test] + fn dumps_impl_values_for_ir() { + let value = Calcit::Impl(CalcitImpl { + name: EdnTag::new("DemoImpl"), + origin: None, + fields: Arc::new(vec![EdnTag::new("show")]), + values: Arc::new(vec![Calcit::Proc(CalcitProc::NativeStr)]), + }); + + let dumped = dump_code(&value); + let Edn::Map(entries) = dumped else { + panic!("expected impl to dump as map"); + }; + + assert_eq!(entries.get(&Edn::tag("kind")), Some(&Edn::tag("impl"))); + assert_eq!(entries.get(&Edn::tag("name")), Some(&Edn::str("DemoImpl"))); + assert_eq!(entries.get(&Edn::tag("field-count")), Some(&Edn::Number(1.0))); + + let Some(Edn::List(fields)) = entries.get(&Edn::tag("fields")) else { + panic!("expected impl fields list"); + }; + assert_eq!(fields.len(), 1); + + let Some(Edn::Map(field_entry)) = fields.iter().next() else { + panic!("expected impl field entry to be a map"); + }; + assert_eq!(field_entry.get(&Edn::tag("field")), Some(&Edn::str("show"))); + + let Some(Edn::Map(proc_entry)) = field_entry.get(&Edn::tag("value")) else { + panic!("expected impl field value to be a map"); + }; + assert_eq!(proc_entry.get(&Edn::tag("kind")), Some(&Edn::tag("proc"))); + assert_eq!(proc_entry.get(&Edn::tag("name")), Some(&Edn::str("&str"))); + assert_eq!(proc_entry.get(&Edn::tag("builtin")), Some(&Edn::Bool(true))); + } +} + fn type_tag_map(type_name: &str) -> Edn { Edn::map_from_iter([(Edn::tag("type"), Edn::tag(type_name))]) } From 431d1ae99c0b21e13d88ab578b43c318ec9dffd3 Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 23 Mar 2026 13:25:01 +0800 Subject: [PATCH 37/57] Use canonical type EDN in IR export --- src/codegen/gen_ir.rs | 151 +++++++++++------------------------------- 1 file changed, 37 insertions(+), 114 deletions(-) diff --git a/src/codegen/gen_ir.rs b/src/codegen/gen_ir.rs index bb2a02dc..c0b75cfb 100644 --- a/src/codegen/gen_ir.rs +++ b/src/codegen/gen_ir.rs @@ -7,8 +7,8 @@ use std::sync::Arc; use cirru_edn::{Edn, EdnListView, format}; use crate::calcit::{ - Calcit, CalcitArgLabel, CalcitEnum, CalcitFnArgs, CalcitFnTypeAnnotation, CalcitImpl, CalcitImport, CalcitLocal, CalcitRecord, - CalcitStruct, CalcitTuple, CalcitTypeAnnotation, ImportInfo, MethodKind, + Calcit, CalcitArgLabel, CalcitEnum, CalcitFnArgs, CalcitImpl, CalcitImport, CalcitLocal, CalcitRecord, CalcitStruct, CalcitTuple, + CalcitTypeAnnotation, ImportInfo, MethodKind, }; use crate::program; @@ -349,113 +349,7 @@ fn dump_type_list(xs: &[Arc]) -> Edn { } fn dump_type_annotation(type_info: &CalcitTypeAnnotation) -> Edn { - match type_info { - CalcitTypeAnnotation::Bool => type_tag_map("bool"), - CalcitTypeAnnotation::Number => type_tag_map("number"), - CalcitTypeAnnotation::String => type_tag_map("string"), - CalcitTypeAnnotation::Symbol => type_tag_map("symbol"), - CalcitTypeAnnotation::Tag => type_tag_map("tag"), - CalcitTypeAnnotation::List(inner) => { - let mut entries = vec![(Edn::tag("type"), Edn::tag("list"))]; - entries.push((Edn::tag("inner"), dump_type_annotation(inner.as_ref()))); - Edn::map_from_iter(entries) - } - CalcitTypeAnnotation::Map(k, v) => { - let mut entries = vec![(Edn::tag("type"), Edn::tag("map"))]; - entries.push((Edn::tag("key"), dump_type_annotation(k.as_ref()))); - entries.push((Edn::tag("value"), dump_type_annotation(v.as_ref()))); - Edn::map_from_iter(entries) - } - CalcitTypeAnnotation::DynFn => type_tag_map("fn"), - CalcitTypeAnnotation::Ref(inner) => { - let mut entries = vec![(Edn::tag("type"), Edn::tag("ref"))]; - entries.push((Edn::tag("inner"), dump_type_annotation(inner.as_ref()))); - Edn::map_from_iter(entries) - } - CalcitTypeAnnotation::Buffer => type_tag_map("buffer"), - CalcitTypeAnnotation::CirruQuote => type_tag_map("cirru-quote"), - CalcitTypeAnnotation::Record(record) => dump_struct_code(record.as_ref()), - CalcitTypeAnnotation::Tuple(tuple) => dump_enum_code(tuple.as_ref()), - CalcitTypeAnnotation::DynTuple => type_tag_map("tuple"), - CalcitTypeAnnotation::Fn(signature) => dump_function_type_annotation(signature.as_ref()), - CalcitTypeAnnotation::Set(_) => type_tag_map("set"), - CalcitTypeAnnotation::Variadic(inner) => { - let mut entries = vec![(Edn::tag("type"), Edn::tag("variadic"))]; - entries.push((Edn::tag("inner"), dump_type_annotation(inner.as_ref()))); - Edn::map_from_iter(entries) - } - CalcitTypeAnnotation::Custom(value) => Edn::map_from_iter([ - (Edn::tag("type"), Edn::tag("custom")), - (Edn::tag("value"), dump_code(value.as_ref())), - ]), - CalcitTypeAnnotation::Optional(inner) => { - let mut entries = vec![(Edn::tag("type"), Edn::tag("optional"))]; - entries.push((Edn::tag("inner"), dump_type_annotation(inner.as_ref()))); - Edn::map_from_iter(entries) - } - CalcitTypeAnnotation::Dynamic => Edn::Nil, - CalcitTypeAnnotation::TypeVar(name) => Edn::map_from_iter([ - (Edn::tag("type"), Edn::tag("type-var")), - (Edn::tag("value"), Edn::Str(name.clone())), - ]), - CalcitTypeAnnotation::TypeRef(name, args) => { - let mut entries = vec![ - (Edn::tag("type"), Edn::tag("type-ref")), - (Edn::tag("value"), Edn::Str(name.clone())), - ]; - if !args.is_empty() { - let mut args_edn = EdnListView::default(); - for arg in args.iter() { - args_edn.push(dump_type_annotation(arg.as_ref())); - } - entries.push((Edn::tag("args"), args_edn.into())); - } - Edn::map_from_iter(entries) - } - CalcitTypeAnnotation::Struct(struct_def, args) => { - let mut entries = vec![(Edn::tag("type"), Edn::tag("struct"))]; - entries.push((Edn::tag("value"), dump_struct_code(struct_def.as_ref()))); - if !args.is_empty() { - let mut args_edn = EdnListView::default(); - for arg in args.iter() { - args_edn.push(dump_type_annotation(arg.as_ref())); - } - entries.push((Edn::tag("args"), args_edn.into())); - } - Edn::map_from_iter(entries) - } - CalcitTypeAnnotation::Enum(enum_def, args) => { - let mut entries = vec![(Edn::tag("type"), Edn::tag("enum"))]; - entries.push((Edn::tag("value"), dump_enum_code(enum_def.as_ref()))); - if !args.is_empty() { - let mut args_edn = EdnListView::default(); - for arg in args.iter() { - args_edn.push(dump_type_annotation(arg.as_ref())); - } - entries.push((Edn::tag("args"), args_edn.into())); - } - Edn::map_from_iter(entries) - } - CalcitTypeAnnotation::Trait(trait_def) => Edn::map_from_iter([ - (Edn::tag("type"), Edn::tag("trait")), - (Edn::tag("value"), Edn::tag(trait_def.name.to_string())), - ]), - CalcitTypeAnnotation::TraitSet(traits) => { - let mut list = EdnListView::default(); - for trait_def in traits.iter() { - list.push(Edn::tag(trait_def.name.to_string())); - } - Edn::map_from_iter([(Edn::tag("type"), Edn::tag("traits")), (Edn::tag("value"), list.into())]) - } - CalcitTypeAnnotation::Unit => type_tag_map("unit"), - } -} - -fn dump_function_type_annotation(signature: &CalcitFnTypeAnnotation) -> Edn { - let mut entries = vec![(Edn::tag("type"), Edn::tag("fn"))]; - entries.push((Edn::tag("args"), dump_type_list(&signature.arg_types))); - entries.push((Edn::tag("return"), dump_type_annotation_opt(&signature.return_type))); - Edn::map_from_iter(entries) + type_info.to_type_edn() } fn dump_tuple_code(tuple: &CalcitTuple) -> Edn { @@ -582,8 +476,8 @@ fn record_metadata(record: &CalcitRecord) -> Vec<(Edn, Edn)> { #[cfg(test)] mod tests { - use super::dump_code; - use crate::calcit::{Calcit, CalcitImpl, CalcitProc}; + use super::{dump_code, dump_type_annotation}; + use crate::calcit::{Calcit, CalcitFnTypeAnnotation, CalcitImpl, CalcitProc, CalcitTypeAnnotation, SchemaKind}; use cirru_edn::{Edn, EdnTag}; use std::sync::Arc; @@ -622,8 +516,37 @@ mod tests { assert_eq!(proc_entry.get(&Edn::tag("name")), Some(&Edn::str("&str"))); assert_eq!(proc_entry.get(&Edn::tag("builtin")), Some(&Edn::Bool(true))); } -} -fn type_tag_map(type_name: &str) -> Edn { - Edn::map_from_iter([(Edn::tag("type"), Edn::tag(type_name))]) + #[test] + fn dumps_type_annotations_as_canonical_type_edn() { + let list_type = CalcitTypeAnnotation::List(Arc::new(CalcitTypeAnnotation::String)); + assert_eq!( + dump_type_annotation(&list_type), + Edn::tuple(Edn::tag("list"), vec![Edn::tag("string")]) + ); + + let map_type = CalcitTypeAnnotation::Map(Arc::new(CalcitTypeAnnotation::String), Arc::new(CalcitTypeAnnotation::Number)); + assert_eq!( + dump_type_annotation(&map_type), + Edn::tuple(Edn::tag("map"), vec![Edn::tag("string"), Edn::tag("number")]) + ); + + let fn_type = CalcitTypeAnnotation::Fn(Arc::new(CalcitFnTypeAnnotation { + generics: Arc::new(vec![]), + arg_types: vec![Arc::new(CalcitTypeAnnotation::String)], + return_type: Arc::new(CalcitTypeAnnotation::Bool), + fn_kind: SchemaKind::Fn, + rest_type: None, + })); + assert_eq!( + dump_type_annotation(&fn_type), + Edn::tuple( + Edn::tag("fn"), + vec![Edn::map_from_iter([ + (Edn::tag("args"), Edn::List(cirru_edn::EdnListView(vec![Edn::tag("string")]))), + (Edn::tag("return"), Edn::tag("bool")), + ])] + ) + ); + } } From 5393298d861faa11e401fda0f47406f6d9337a0b Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 24 Mar 2026 01:06:35 +0800 Subject: [PATCH 38/57] Add command echo for libs subcommands --- docs/CalcitAgent.md | 2 ++ docs/run/upgrade.md | 5 +++-- src/bin/cli_handlers/command_echo.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index 992e4ea5..d49aafd2 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -207,6 +207,8 @@ cr docs agents --full 4. 聚焦子树确认上下文:`cr tree show -p ''`(复杂时可加 `-j`;大表达式默认只展开 ROOT + 一层 chunks,需要更多时加 `--chunk-expand-depth 2`) 5. 修改并验证:`cr tree replace ...` 或 `cr edit inc --changed `,然后 `cr js` +> 修改时要求先考虑定位到坐标使用局部修改的方式, 或者结构化修改的方式, 若改动较大或不确定改动范围时再考虑整段覆盖式修改。 + ### 示例(大函数) ```bash diff --git a/docs/run/upgrade.md b/docs/run/upgrade.md index 68fe2f88..71f7b203 100644 --- a/docs/run/upgrade.md +++ b/docs/run/upgrade.md @@ -15,6 +15,7 @@ - `:entries`(额外入口) - 命令入口:`README`、项目脚本、CI workflow - Node 工具链:`package.json`、`yarn.lock`、Corepack/Yarn 版本 +- 注意 git fetch 检查最新历史, 避免基于老版本操作导致变更冲突 --- @@ -70,7 +71,7 @@ yarn install --immutable 常见链路例如: ```bash -caps --ci && yarn install --immutable +caps && yarn install --immutable cr --entry cr --entry js cr js && yarn vite build --base=./ @@ -131,7 +132,7 @@ yarn 建议至少覆盖以下 6 项: 1. `cr --version` -2. `caps --ci outdated`(确认无遗漏项或已按预期处理) +2. `caps outdated --yes`(确认无遗漏项或已按预期处理) 3. `yarn install --immutable` 4. `cr js`(如果是 js 项目) 5. CI 中的入口/测试命令(`--entry` 或 `--init-fn` 链路) diff --git a/src/bin/cli_handlers/command_echo.rs b/src/bin/cli_handlers/command_echo.rs index a6d947f8..54ccd634 100644 --- a/src/bin/cli_handlers/command_echo.rs +++ b/src/bin/cli_handlers/command_echo.rs @@ -54,6 +54,7 @@ pub fn should_echo_command(cli_args: &ToplevelCalcit) -> bool { cli_args.subcommand, Some(CalcitCommand::Query(_)) | Some(CalcitCommand::Docs(_)) + | Some(CalcitCommand::Libs(_)) | Some(CalcitCommand::Edit(_)) | Some(CalcitCommand::Tree(_)) | Some(CalcitCommand::Analyze(_)) @@ -74,6 +75,7 @@ fn render_command_echo(cli_args: &ToplevelCalcit) -> Option { let mut tokens = vec![match subcommand { CalcitCommand::Query(cmd) => format!("cr query {}", query_name(&cmd.subcommand)), CalcitCommand::Docs(cmd) => format!("cr docs {}", docs_name(&cmd.subcommand)), + CalcitCommand::Libs(cmd) => format!("cr libs {}", libs_name(cmd.subcommand.as_ref()?)), CalcitCommand::Edit(cmd) => format!("cr edit {}", edit_name(&cmd.subcommand)), CalcitCommand::Tree(cmd) => format!("cr tree {}", tree_name(&cmd.subcommand)), CalcitCommand::Analyze(cmd) => format!("cr analyze {}", analyze_name(&cmd.subcommand)), @@ -84,6 +86,7 @@ fn render_command_echo(cli_args: &ToplevelCalcit) -> Option { match subcommand { CalcitCommand::Query(cmd) => push_query(&mut tokens, cmd), CalcitCommand::Docs(cmd) => push_docs(&mut tokens, cmd), + CalcitCommand::Libs(cmd) => push_libs(&mut tokens, cmd.subcommand.as_ref()?), CalcitCommand::Edit(cmd) => push_edit(&mut tokens, cmd), CalcitCommand::Tree(cmd) => push_tree(&mut tokens, cmd), CalcitCommand::Analyze(cmd) => push_analyze(&mut tokens, cmd), @@ -187,6 +190,22 @@ fn push_docs(tokens: &mut Vec, cmd: &DocsCommand) { } } +fn push_libs(tokens: &mut Vec, subcommand: &LibsSubcommand) { + match subcommand { + LibsSubcommand::Readme(opts) => echo_items!( + tokens, + pos "package" => &opts.package, + list "heading" => &opts.headings, + opt "file" => opts.file.as_deref(); default "none", + switch "no-subheadings" => opts.no_subheadings, + switch "full" => opts.full, + switch "with-lines" => opts.with_lines + ), + LibsSubcommand::Search(opts) => echo_items!(tokens, pos "keyword" => &opts.keyword), + LibsSubcommand::ScanMd(opts) => echo_items!(tokens, pos "module" => &opts.module), + } +} + fn push_cirru(tokens: &mut Vec, cmd: &CirruCommand) { match &cmd.subcommand { CirruSubcommand::Parse(opts) => { @@ -489,6 +508,14 @@ fn docs_name(subcommand: &DocsSubcommand) -> &'static str { } } +fn libs_name(subcommand: &LibsSubcommand) -> &'static str { + match subcommand { + LibsSubcommand::Readme(_) => "readme", + LibsSubcommand::Search(_) => "search", + LibsSubcommand::ScanMd(_) => "scan-md", + } +} + fn cirru_name(subcommand: &CirruSubcommand) -> &'static str { match subcommand { CirruSubcommand::Parse(_) => "parse", From eb7e88fe1a5ffd3d43fb8090fffd019a5feff3b4 Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 24 Mar 2026 12:28:19 +0800 Subject: [PATCH 39/57] Move Cirru parse guidance and tighten agents cache refresh --- docs/CalcitAgent.md | 11 +++++++++++ docs/run/agent-advanced.md | 19 +++---------------- src/bin/cli_handlers/docs.rs | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index d49aafd2..4f0c8adf 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -20,6 +20,17 @@ 结构化编辑依赖“树 + 路径”。先能读懂 Cirru,才能稳定算出路径坐标。 +### Cirru 语法工具(`cr cirru`) + +用于 Cirru 语法和 JSON 之间的转换: + +- `cr cirru parse ''` - 解析 Cirru 代码为 JSON +- `cr cirru format ''` - 格式化 JSON 为 Cirru 代码 +- `cr cirru parse-edn ''` - 解析 Cirru EDN 为 JSON +- `cr cirru show-guide` - 显示 Cirru 语法指南(帮助生成正确的 Cirru 代码) + +**⚠️ 提示:如果你不确定某段缩进语法是否会被解析成预期结构,先运行一次 `cr cirru parse` 预检,再执行 `cr tree`/`cr edit` 修改。** + - Cirru 是缩进风格的 S-expression,缩进层级就是树层级。 - 行内空格分隔节点;嵌套表达式是子节点。 - 常见字面量: diff --git a/docs/run/agent-advanced.md b/docs/run/agent-advanced.md index 3112b9c5..84bd1c80 100644 --- a/docs/run/agent-advanced.md +++ b/docs/run/agent-advanced.md @@ -255,24 +255,11 @@ Calcit 程序使用 `cr` 命令: - `cr docs agents [ ...] [--full]` - 读取 Agent 指南(即本文档,优先本地缓存,按天自动刷新) - 不传标题时列出所有标题;传关键词时按标题模糊匹配输出对应章节 -### Cirru 语法工具 (`cr cirru`) +### Cirru 语法工具 -用于 Cirru 语法和 JSON 之间的转换: +`cr cirru parse/format/parse-edn/show-guide` 的高频命令已收敛到 `docs/CalcitAgent.md` 的「Cirru 语法速览」章节,便于在局部编辑工作流中直接查用。 -- `cr cirru parse ''` - 解析 Cirru 代码为 JSON -- `cr cirru format ''` - 格式化 JSON 为 Cirru 代码 -- `cr cirru parse-edn ''` - 解析 Cirru EDN 为 JSON -- `cr cirru show-guide` - 显示 Cirru 语法指南(帮助 LLM 生成正确的 Cirru 代码) - -**⚠️ 重要:生成 Cirru 代码前请先阅读语法指南** - -运行 `cr cirru show-guide` 获取完整的 Cirru 语法说明,包括: - -- `$` 操作符(单节点展开) -- `|` 前缀(字符串字面量), 这个是 Cirru 特殊的地方, 而不是直接用引号包裹 -- `,` 操作符(注释标记) -- `~` 和 `~@`(宏展开) -- 常见错误和避免方法 +若缩进结构不确定,先执行 `cr cirru parse ''` 预检 AST/JSON,再继续 `cr tree` 或 `cr edit` 修改。 ### 库管理 (`cr libs`) diff --git a/src/bin/cli_handlers/docs.rs b/src/bin/cli_handlers/docs.rs index 3777c169..e5232d09 100644 --- a/src/bin/cli_handlers/docs.rs +++ b/src/bin/cli_handlers/docs.rs @@ -77,7 +77,7 @@ fn needs_agents_refresh(cache_path: &Path) -> bool { let now = SystemTime::now(); match now.duration_since(modified) { - Ok(age) => age > Duration::from_secs(24 * 60 * 60), + Ok(age) => age > Duration::from_secs(1 * 60 * 60), // 1 hour Err(_) => true, } } From ac9dbcaae754d47a8e1110c727994f192d8ef7d7 Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 25 Mar 2026 01:57:09 +0800 Subject: [PATCH 40/57] Improve docs upgrade guidance --- .gitignore | 1 + docs/run/upgrade.md | 53 +++++++++++++++++++++++++++--------- src/bin/cli_handlers/docs.rs | 10 +++++-- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 9a022ebc..8908fbe6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ calcit/.compact-inc.cirru js-out/ node_modules/ +.yarn/*.gz lib builds/ diff --git a/docs/run/upgrade.md b/docs/run/upgrade.md index 71f7b203..7816f9d5 100644 --- a/docs/run/upgrade.md +++ b/docs/run/upgrade.md @@ -21,7 +21,7 @@ ## 2)标准升级流程(建议顺序) -下面流程按“先确认版本,再更新依赖,再验证命令链”的顺序执行。 +下面流程按“先确认版本,再对齐工具链,再更新依赖,最后按 CI 链路验证”的顺序执行。 ### Step A:确认 Calcit CLI 版本 @@ -31,16 +31,19 @@ cr --version 说明:一般本机已经是较新版本,但升级前先确认一遍,避免后续误判。 -### Step B:检查项目内 Calcit 版本对齐 +### Step B:先对齐项目版本与 Node 工具链 -重点检查两处是否一致、是否为目标版本: +重点先检查并对齐以下几处: - `deps.cirru` 里的 `:calcit-version` - `package.json` 里的 `@calcit/procs` +- `package.json` 里的 `packageManager` +- `.yarnrc.yml` 是否需要 `nodeLinker: node-modules` +- `.gitignore` 是否已忽略 `.yarn/*.gz`,避免 Yarn 生成的压缩状态文件入库 -必要时同步更新这两处,避免运行时和 JS 依赖版本错位。 +先把这些基础版本与工具链约定对齐,再继续更新依赖,能减少后面重复改 lockfile 或 CI 的次数。 -### Step C:检查并更新依赖 +### Step C:检查并更新 `deps.cirru` ```bash caps outdated --yes @@ -50,10 +53,21 @@ caps outdated --yes - `caps outdated`:查看可更新项; - `caps outdated --yes`:直接更新 `deps.cirru`(无交互确认)。 +- `caps`:根据当前 `deps.cirru` 下载/同步模块内容。 + +注意:`caps outdated --yes` 只负责更新 `deps.cirru`,不等于已经完成模块同步。 若依赖是固定 tag/version,仍需先改 `deps.cirru` 再执行更新。 -### Step D:用 Yarn Berry 安装并校验 +### Step D:同步模块内容 + +```bash +caps +``` + +说明:这一步才是根据当前 `deps.cirru` 下载/同步模块内容。若跳过这一步,后面的编译与安装结果容易混入旧模块状态。 + +### Step E:用 Yarn Berry 安装并校验 ```bash corepack enable @@ -64,9 +78,15 @@ yarn install --immutable 说明:团队若习惯 Yarn Berry,建议固定 `packageManager` 并使用 `--immutable` 做一致性校验。 -### Step E:从 CI workflow 提取检查命令并本地先跑 +如果项目仍依赖 `node_modules` 目录解析,还应补一个 `.yarnrc.yml`: + +```yaml +nodeLinker: node-modules +``` -先看 `.github/workflows/` 里实际执行了哪些命令,然后按同顺序在本地跑一遍。 +### Step F:从 CI workflow 和 package.json 提取检查命令并本地先跑 + +先看 `.github/workflows/` 里实际执行了哪些命令,再看 `package.json` 里是否有额外构建脚本,然后按同顺序在本地跑一遍。 常见链路例如: @@ -77,11 +97,16 @@ cr --entry js cr js && yarn vite build --base=./ ``` -### Step F:执行 package.json 里的编译相关脚本 +如果 `package.json` 里有与编译、构建、测试相关的脚本,也应本地执行一遍;如果没有额外脚本,这一步可以跳过。若项目直接通过 Vite 构建,也可直接执行: -如果 `package.json` 里有与编译、构建、测试相关的脚本,也应本地执行一遍,确认升级后仍可用。 +```bash +yarn up vite +yarn vite build --base=./ +``` + +说明:最近 Vite 有大版本更新。若项目依赖 Vite,升级时建议显式执行一次 `yarn up vite`,并在更新后重新跑 `yarn vite build --base=./` 确认没有新的构建兼容性问题。 -例如: +例如还有: ```bash yarn @@ -118,6 +143,8 @@ yarn run: yarn install --immutable ``` +说明:若项目依赖 `packageManager: "yarn@4.12.0"`,优先先执行 Corepack 激活,再让 CI 触发 Yarn。不要让 `setup-node` 的 Yarn cache 或其他 Yarn 调用早于 `corepack enable` / `corepack prepare`,否则可能误用 runner 上的全局 Yarn 1。 + ### 3.3 lockfile 迁移 如果 `yarn install --immutable` 因 lockfile 格式变化失败: @@ -134,6 +161,6 @@ yarn 1. `cr --version` 2. `caps outdated --yes`(确认无遗漏项或已按预期处理) 3. `yarn install --immutable` -4. `cr js`(如果是 js 项目) +4. `cr js`(如果是 js 项目) 5. CI 中的入口/测试命令(`--entry` 或 `--init-fn` 链路) -6. `package.json` 中与编译/构建相关脚本 +6. `package.json` 中与编译/构建相关脚本,或直接执行 `yarn vite build --base=./` diff --git a/src/bin/cli_handlers/docs.rs b/src/bin/cli_handlers/docs.rs index e5232d09..8655d0b6 100644 --- a/src/bin/cli_handlers/docs.rs +++ b/src/bin/cli_handlers/docs.rs @@ -142,10 +142,16 @@ fn get_guidebook_dir() -> Result { let docs_dir = Path::new(&home_dir).join(".config/calcit/docs"); if !docs_dir.exists() { + let calcit_repo_dir = Path::new(&home_dir).join(".config/calcit/calcit"); return Err(format!( "Guidebook documentation directory not found: {docs_dir:?}\n\n\ - To set up guidebook documentation, please run:\n\ - git clone https://github.com/calcit-lang/calcit.git && ln -s ~/.config/calcit/calcit/docs ~/.config/calcit/docs" + Download the Calcit docs repo with git, then create a symlink for the docs directory:\n\ + mkdir -p ~/.config/calcit\n\ + git clone https://github.com/calcit-lang/calcit.git {}\n\ + ln -s {}/docs {}", + calcit_repo_dir.display(), + calcit_repo_dir.display(), + docs_dir.display() )); } From 97e3fc28ba0e5b9968140e95e71fc6ceeac36a90 Mon Sep 17 00:00:00 2001 From: tiye Date: Tue, 24 Mar 2026 16:35:30 +0800 Subject: [PATCH 41/57] Remove once flag and clarify agent docs --- Agents.md | 1 - docs/CalcitAgent.md | 8 +++++++ docs/intro/overview.md | 2 +- docs/quick-reference.md | 1 - docs/run/agent-advanced.md | 6 ++--- docs/run/cli-options.md | 48 ++++---------------------------------- src/bin/cr.rs | 10 +------- src/cli_args.rs | 9 ------- 8 files changed, 16 insertions(+), 69 deletions(-) diff --git a/Agents.md b/Agents.md index bb4ab338..5686867b 100644 --- a/Agents.md +++ b/Agents.md @@ -27,7 +27,6 @@ cr docs agents --full - `cr `、`cr js`、`cr ir` 现在默认都是**单次执行**(once)。 - 需要监听时,显式传 `-w` 或 `--watch`(如 `cr -w `、`cr js -w`、`cr ir -w`)。 -- `-1/--once` 仍保留兼容,但在默认 once 行为下通常可省略。 ### cr eval 基础与常见踩坑 diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index 4f0c8adf..c9697c2b 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -80,6 +80,7 @@ result (collect! state) #### `,`:在“重起一行”场景里用于保持目标节点形态(有助于坐标稳定) `,` 常用于告诉解析器“这里是值节点,不是再发起一次调用”。 +在 Cirru 中,一行默认会被当作表达式;当你在表达式后另起一行并想表达“普通值”时,请写成 `, `(逗号后有空格),避免被解析成新的调用。 ```cirru.no-check ; "写法 A" @@ -95,6 +96,13 @@ a - 如果把 `, d` 误写成单独一行 `d`,它可能被解析成“调用形态”,节点类型会变化,后续路径与搜索命中也可能随之变化。 - 所以:`,` 本身通常不引入额外层级;它更多是在“换行写法”下保持你想要的 AST 形态。 +#### Agent 生成前自检(20 秒) + +- 字符串是否使用了 `|text` 或 `"|text with spaces"`,避免把字符串当符号。包含特殊字符需要双引号包裹. +- `let` 绑定是否是成对列表:`((name value))`,避免 `expects pairs in list for let`。 +- 分支/函数最后一行若是“值”而非调用,是否使用了 `, value`。 +- 只要对缩进有不确定,先用 `cr cirru parse ''` 看 AST,再执行结构化编辑。 + #### 先理解启动文件:`compact.cirru` 的 EDN 结构(Agent 快速模型) Agent 切到新窗口时,优先把 `compact.cirru` 看成一个“可执行项目快照”,其顶层 EDN 结构通常是: diff --git a/docs/intro/overview.md b/docs/intro/overview.md index 660f85d6..05dad67c 100644 --- a/docs/intro/overview.md +++ b/docs/intro/overview.md @@ -14,7 +14,7 @@ With the `cr` command, Calcit code can be written as an indentation-based langua - Hot code swapping -Calcit was built with hot swapping in mind. Combined with [calcit-editor](https://github.com/calcit-lang/editor), it watches code changes by default, and re-runs program on updates. For calcit-js, it works with Vite and Webpack to reload, learning from Elm, ClojureScript and React. +Calcit was built with hot swapping in mind. Combined with [calcit-editor](https://github.com/calcit-lang/editor), it watches code changes by specifying `-w`, and re-runs program on updates. For calcit-js, it works with Vite and Webpack to reload, learning from Elm, ClojureScript and React. - ES Modules Syntax diff --git a/docs/quick-reference.md b/docs/quick-reference.md index 988e42e4..6b972817 100644 --- a/docs/quick-reference.md +++ b/docs/quick-reference.md @@ -29,7 +29,6 @@ cr eval "echo |done" ### CLI Options - `--watch` / `-w` - Watch files and rerun/rebuild on changes -- `--once` / `-1` - Run once (compatibility flag; default is already once) - `--disable-stack` - Disable stack trace for errors - `--skip-arity-check` - Skip arity check in JS codegen - `--emit-path ` - Specify output path for JS (default: `js-out/`) diff --git a/docs/run/agent-advanced.md b/docs/run/agent-advanced.md index 84bd1c80..d88c93d6 100644 --- a/docs/run/agent-advanced.md +++ b/docs/run/agent-advanced.md @@ -114,12 +114,10 @@ Calcit 程序使用 `cr` 命令: - `cr compact.cirru js -w` / `cr compact.cirru js --watch` - JS 监听编译模式 - `cr compact.cirru ir` - 生成 program-ir.cirru(默认单次生成) - `cr compact.cirru ir -w` / `cr compact.cirru ir --watch` - IR 监听生成模式 -- `cr -1 ` - 执行一次然后退出(兼容参数,当前默认行为已是 once) - `cr --check-only` - 仅检查代码正确性,不执行程序 - 对 init_fn 和 reload_fn 进行预处理验证 - 输出:预处理进度、warnings、检查耗时 - 用于 CI/CD 或快速验证代码修改 -- `cr js -1` - 检查代码正确性,生成 JavaScript(兼容参数,默认已是单次) - `cr js --check-only` - 检查代码正确性,不生成 JavaScript - `cr --tips ...` - 主动显示完整 tips(教学/排障时) - 示例:`cr --tips demos/compact.cirru query def calcit.core/foldl` @@ -959,8 +957,8 @@ cr query error # 命令会显示详细的错误信息或成功状态 ```bash # 极少数情况:增量更新不符合预期时 -cr -1 js # 重新编译 JavaScript -cr -1 # 重新执行程序 +cr js # 重新编译 JavaScript +cr # 重新执行程序 # 或重启监听模式(Ctrl+C 停止后重启) cr # 或 cr js diff --git a/docs/run/cli-options.md b/docs/run/cli-options.md index a8853613..ad7cd761 100644 --- a/docs/run/cli-options.md +++ b/docs/run/cli-options.md @@ -1,40 +1,7 @@ # CLI Options ```bash -Usage: cr [] [-1] [-w] [--disable-stack] [--skip-arity-check] [--warn-dyn-method] [--emit-path ] [--init-fn ] [--reload-fn ] [--entry ] [--reload-libs] [--watch-dir ] [] [] - -Top-level command. - -Positional Arguments: - input input source file, defaults to "compact.cirru" - -Options: - -1, --once run once and quit (compatibility option) - -w, --watch watch files and rerun/rebuild on changes - --disable-stack disable stack trace for errors - --skip-arity-check - skip arity check in js codegen - --warn-dyn-method - warn on dynamic method dispatch and trait-attachment diagnostics - --emit-path entry file path, defaults to "js-out/" - --init-fn specify `init_fn` which is main function - --reload-fn specify `reload_fn` which is called after hot reload - --entry specify with config entry - --reload-libs force reloading libs data during code reload - --watch-dir specify a path to watch assets changes - --help display usage information - -Commands: - js emit JavaScript rather than interpreting - ir emit Cirru EDN representation of program to program-ir.cirru - eval run program - analyze analyze code structure (call-graph, count-calls, check-examples) - query query project information (namespaces, definitions, configs) - docs documentation tools (guidebook) - cirru Cirru syntax tools (parse, format, edn) - libs fetch available Calcit libraries from registry - edit edit project code (definitions, namespaces, modules, configs) - tree fine-grained code tree operations (view and modify AST nodes) +cr --help ``` Quick note: `cr edit format` rewrites the target snapshot using canonical serialization without changing semantics. It also normalizes legacy namespace entries that were previously serialized with `CodeEntry` into the current `NsEntry` shape. @@ -51,7 +18,7 @@ cr cr demos/compact.cirru ``` -### Run Once (--once / -1) +### Run Mode (default once) By default, `cr` runs once and exits. Use `--watch` (`-w`) to enable watch mode: @@ -60,13 +27,6 @@ cr --watch cr -w demos/compact.cirru ``` -`--once` is still available for compatibility: - -```bash -cr --once -cr -1 # shorthand -``` - ### Error Stack Trace (--disable-stack) Disables detailed stack traces in error messages, useful for cleaner output: @@ -151,13 +111,13 @@ cr js -w --emit-path dist/ cr ir -w # Testing single run -cr --once --init-fn app.test/run-tests! +cr --init-fn app.test/run-tests! # Debug mode with full stack traces cr --reload-libs # CI/CD environment -cr --once --disable-stack +cr --disable-stack ``` ## Markdown code checking diff --git a/src/bin/cr.rs b/src/bin/cr.rs index 7f3c47a1..7f9e9464 100644 --- a/src/bin/cr.rs +++ b/src/bin/cr.rs @@ -79,7 +79,7 @@ fn main() -> Result<(), String> { _ => {} } - let mut eval_once = cli_args.once; + let mut eval_once = false; let is_eval_mode = matches!(&cli_args.subcommand, Some(CalcitCommand::Eval(_))); let assets_watch = cli_args.watch_dir.to_owned(); @@ -239,10 +239,6 @@ fn main() -> Result<(), String> { // `cr js` defaults to once mode; use --watch/-w to keep watching eval_once = true; } - if js_options.once { - // kept for compatibility, force once mode - eval_once = true; - } if cli_args.skip_arity_check { codegen::set_code_gen_skip_arity_check(true); } @@ -252,10 +248,6 @@ fn main() -> Result<(), String> { // `cr ir` defaults to once mode; use --watch/-w to keep watching eval_once = true; } - if ir_options.once { - // kept for compatibility, force once mode - eval_once = true; - } run_codegen(&entries, &cli_args.emit_path, true) } else if let Some(CalcitCommand::Analyze(analyze_cmd)) = &cli_args.subcommand { eval_once = true; diff --git a/src/cli_args.rs b/src/cli_args.rs index fab6fd28..a70c141f 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -7,9 +7,6 @@ pub const CALCIT_VERSION: &str = env!("CARGO_PKG_VERSION"); pub struct ToplevelCalcit { #[argh(subcommand)] pub subcommand: Option, - /// run once and exit (kept for compatibility) - #[argh(switch, short = '1')] - pub once: bool, /// enable watch mode for direct run mode (default behavior is run once) #[argh(switch, short = 'w')] pub watch: bool, @@ -89,9 +86,6 @@ pub enum CalcitCommand { #[derive(FromArgs, PartialEq, Debug, Clone)] #[argh(subcommand, name = "js")] pub struct EmitJsCommand { - /// run once and exit (kept for compatibility) - #[argh(switch, short = '1')] - pub once: bool, /// enable watch mode (default behavior is run once) #[argh(switch, short = 'w')] pub watch: bool, @@ -104,9 +98,6 @@ pub struct EmitJsCommand { #[derive(FromArgs, PartialEq, Debug, Clone)] #[argh(subcommand, name = "ir")] pub struct EmitIrCommand { - /// run once and exit (kept for compatibility) - #[argh(switch, short = '1')] - pub once: bool, /// enable watch mode (default behavior is run once) #[argh(switch, short = 'w')] pub watch: bool, From 3c038faedb785498b44d2804a1109ee9cf52c2cc Mon Sep 17 00:00:00 2001 From: tiye Date: Wed, 25 Mar 2026 02:14:57 +0800 Subject: [PATCH 42/57] trivial fix on docs --- docs/intro.md | 2 -- docs/run/query.md | 6 +++--- docs/run/upgrade.md | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/intro.md b/docs/intro.md index fcaca847..ec1de448 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -25,8 +25,6 @@ Install Calcit via Cargo: ```bash cargo install calcit -cargo install calcit-bundler # For indentation syntax -cargo install caps-cli # For package management ``` ## Design Philosophy diff --git a/docs/run/query.md b/docs/run/query.md index f6496f37..b1e63c91 100644 --- a/docs/run/query.md +++ b/docs/run/query.md @@ -99,11 +99,11 @@ let Point $ defstruct Point (:x :number) (:y :number) p (%{} Point (:x 1) (:y 2)) do - ; Get all methods/traits implemented by a value + ; "Get all methods/traits implemented by a value" println $ &methods-of p - ; Get tag name of a record or enum + ; 'Get tag name of a record or enum' println $ &record:get-name p - ; Describe any value's internal type + ; "Describe any value's internal type" println $ &inspect-type p ``` diff --git a/docs/run/upgrade.md b/docs/run/upgrade.md index 7816f9d5..2f09250a 100644 --- a/docs/run/upgrade.md +++ b/docs/run/upgrade.md @@ -140,10 +140,10 @@ yarn yarn --version - name: Install deps - run: yarn install --immutable + run: caps --ci && yarn install --immutable ``` -说明:若项目依赖 `packageManager: "yarn@4.12.0"`,优先先执行 Corepack 激活,再让 CI 触发 Yarn。不要让 `setup-node` 的 Yarn cache 或其他 Yarn 调用早于 `corepack enable` / `corepack prepare`,否则可能误用 runner 上的全局 Yarn 1。 +说明:若项目依赖 `packageManager: "yarn@4.12.0"`,优先先执行 Corepack 激活,再让 CI 触发 Yarn。不要让 `setup-node` 的 Yarn cache 或其他 Yarn 调用早于 `corepack enable` / `corepack prepare`,否则可能误用 runner 上的全局 Yarn 1。 `caps --ci` 参数保证在 CI 加载模块时使用 HTTPS 协议,避免 CI 环境下的 SSH key 问题。 ### 3.3 lockfile 迁移 From 150e060dc1b44d0191f64e25af2e415db030dca9 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 26 Mar 2026 01:16:17 +0800 Subject: [PATCH 43/57] trivial clean on old string syntax and editor files --- calcit/editor/calcit.cirru | 820 ------------------------------------ calcit/editor/compact.cirru | 150 ------- calcit/fibo.cirru | 4 +- calcit/test-algebra.cirru | 2 +- calcit/test-gynienic.cirru | 4 +- calcit/test-hygienic.cirru | 4 +- calcit/test-recursion.cirru | 2 +- docs/ecosystem.md | 7 - 8 files changed, 8 insertions(+), 985 deletions(-) delete mode 100644 calcit/editor/calcit.cirru delete mode 100644 calcit/editor/compact.cirru diff --git a/calcit/editor/calcit.cirru b/calcit/editor/calcit.cirru deleted file mode 100644 index c4351898..00000000 --- a/calcit/editor/calcit.cirru +++ /dev/null @@ -1,820 +0,0 @@ - -{} (:entries nil) (:package |app) - :configs $ {} (:compact-output? true) (:extension |.cljs) (:init-fn |app.main/main!) (:local-ui? false) (:output |src) (:port 6001) (:reload-fn |app.main/reload!) (:version |0.0.1) - :modules $ [] - :files $ {} - |app.lib $ %{} :FileEntry - :defs $ {} - |f2 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618661020393) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661020393) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618661020393) (:by |u0) (:text |f2) - |r $ %{} :Expr (:at 1618661020393) (:by |u0) - :data $ {} - |v $ %{} :Expr (:at 1618661022794) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661024070) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618661026271) (:by |u0) (:text "|\"f2 in lib") - :examples $ [] - |f3 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618661052591) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661052591) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618661052591) (:by |u0) (:text |f3) - |r $ %{} :Expr (:at 1618661052591) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661067908) (:by |u0) (:text |x) - |v $ %{} :Expr (:at 1618661054823) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661055379) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618661061473) (:by |u0) (:text "|\"f3 in lib") - |x $ %{} :Expr (:at 1618661070479) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661071077) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618661073107) (:by |u0) (:text "|\"v:") - |r $ %{} :Leaf (:at 1618661074709) (:by |u0) (:text |x) - :examples $ [] - :ns $ %{} :NsEntry (:doc |) - :code $ %{} :Expr (:at 1618661017191) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661017191) (:by |u0) (:text |ns) - |j $ %{} :Leaf (:at 1618661017191) (:by |u0) (:text |app.lib) - |app.macro $ %{} :FileEntry - :defs $ {} - |add-by-1 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618740276250) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740281235) (:by |u0) (:text |defmacro) - |j $ %{} :Leaf (:at 1618740276250) (:by |u0) (:text |add-by-1) - |r $ %{} :Expr (:at 1618740276250) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740282976) (:by |u0) (:text |x) - |v $ %{} :Expr (:at 1618740303995) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618740308945) (:by |u0) (:text |quasiquote) - |T $ %{} :Expr (:at 1618740285475) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740286902) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618740317157) (:by |u0) (:text |~x) - |r $ %{} :Leaf (:at 1618740287700) (:by |u0) (:text |1) - :examples $ [] - |add-by-2 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618740293087) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740296031) (:by |u0) (:text |defmacro) - |j $ %{} :Leaf (:at 1618740293087) (:by |u0) (:text |add-by-2) - |r $ %{} :Expr (:at 1618740293087) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740299129) (:by |u0) (:text |x) - |v $ %{} :Expr (:at 1618740300016) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740325280) (:by |u0) (:text |quasiquote) - |j $ %{} :Expr (:at 1618740327115) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740331009) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618740354540) (:by |u0) (:text |2) - |r $ %{} :Expr (:at 1618740340237) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618740343769) (:by |u0) (:text |add-by-1) - |j $ %{} :Leaf (:at 1618740351578) (:by |u0) (:text |~x) - :examples $ [] - |add-num $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |defmacro) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-num) - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |a) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |b) - |Z $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quasiquote) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&let) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |~) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |a) - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |~) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |b) - :examples $ [] - :ns $ %{} :NsEntry (:doc |) - :code $ %{} :Expr (:at 1618663277036) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618663277036) (:by |u0) (:text |ns) - |j $ %{} :Leaf (:at 1618663277036) (:by |u0) (:text |app.macro) - |app.main $ %{} :FileEntry - :defs $ {} - |add-more $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618730350902) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730354052) (:by |u0) (:text |defmacro) - |j $ %{} :Leaf (:at 1618730350902) (:by |u0) (:text |add-more) - |r $ %{} :Expr (:at 1618730350902) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618730403604) (:by |u0) (:text |acc) - |T $ %{} :Leaf (:at 1618730358202) (:by |u0) (:text |x) - |j $ %{} :Leaf (:at 1618730359828) (:by |u0) (:text |times) - |v $ %{} :Expr (:at 1618730361081) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730362447) (:by |u0) (:text |if) - |j $ %{} :Expr (:at 1618730365650) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730370296) (:by |u0) (:text |&<) - |b $ %{} :Leaf (:at 1618730372435) (:by |u0) (:text |times) - |j $ %{} :Leaf (:at 1618730539709) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618730533225) (:by |u0) (:text |acc) - |v $ %{} :Expr (:at 1618730378436) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730381681) (:by |u0) (:text |recur) - |j $ %{} :Expr (:at 1618730466064) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618730500531) (:by |u0) (:text |quasiquote) - |T $ %{} :Expr (:at 1618730386375) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618730388781) (:by |u0) (:text |&+) - |T $ %{} :Expr (:at 1618730485628) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618730486770) (:by |u0) (:text |~) - |T $ %{} :Leaf (:at 1618730383299) (:by |u0) (:text |x) - |j $ %{} :Expr (:at 1618730488250) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618730489428) (:by |u0) (:text |~) - |T $ %{} :Leaf (:at 1618730412605) (:by |u0) (:text |acc) - |n $ %{} :Leaf (:at 1618730516278) (:by |u0) (:text |x) - |r $ %{} :Expr (:at 1618730434451) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618730435581) (:by |u0) (:text |&-) - |j $ %{} :Leaf (:at 1618730436881) (:by |u0) (:text |times) - |r $ %{} :Leaf (:at 1618730437157) (:by |u0) (:text |1) - :examples $ [] - |call-3 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618767957921) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767957921) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618767957921) (:by |u0) (:text |call-3) - |r $ %{} :Expr (:at 1618767957921) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767960551) (:by |u0) (:text |a) - |j $ %{} :Leaf (:at 1618767961787) (:by |u0) (:text |b) - |r $ %{} :Leaf (:at 1618767962162) (:by |u0) (:text |c) - |v $ %{} :Expr (:at 1618767962704) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767963282) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618767965367) (:by |u0) (:text "|\"a is:") - |r $ %{} :Leaf (:at 1618767965784) (:by |u0) (:text |a) - |x $ %{} :Expr (:at 1618767962704) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767963282) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618767969236) (:by |u0) (:text "|\"b is:") - |r $ %{} :Leaf (:at 1618767970341) (:by |u0) (:text |b) - |y $ %{} :Expr (:at 1618767962704) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767963282) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618767977407) (:by |u0) (:text "|\"c is:") - |r $ %{} :Leaf (:at 1618767973639) (:by |u0) (:text |c) - :examples $ [] - |call-macro $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618769676627) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769678801) (:by |u0) (:text |defmacro) - |j $ %{} :Leaf (:at 1618769676627) (:by |u0) (:text |call-macro) - |r $ %{} :Expr (:at 1618769676627) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769685522) (:by |u0) (:text |x0) - |j $ %{} :Leaf (:at 1618769686283) (:by |u0) (:text |&) - |r $ %{} :Leaf (:at 1618769686616) (:by |u0) (:text |xs) - |v $ %{} :Expr (:at 1618769687244) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769697898) (:by |u0) (:text |quasiquote) - |j $ %{} :Expr (:at 1618769717127) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769719548) (:by |u0) (:text |&{}) - |j $ %{} :Leaf (:at 1618769720509) (:by |u0) (:text |:a) - |n $ %{} :Expr (:at 1618769729161) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618769730971) (:by |u0) (:text |~) - |T $ %{} :Leaf (:at 1618769722734) (:by |u0) (:text |x0) - |r $ %{} :Leaf (:at 1618769723765) (:by |u0) (:text |:b) - |v $ %{} :Expr (:at 1618769809158) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618769809634) (:by |u0) (:text |[]) - |T $ %{} :Expr (:at 1618769725387) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618769865395) (:by |u0) (:text |~@) - |T $ %{} :Leaf (:at 1618769725113) (:by |u0) (:text |xs) - :examples $ [] - |call-many $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618769509051) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769509051) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618769509051) (:by |u0) (:text |call-many) - |r $ %{} :Expr (:at 1618769509051) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769511818) (:by |u0) (:text |x0) - |j $ %{} :Leaf (:at 1618769513121) (:by |u0) (:text |&) - |r $ %{} :Leaf (:at 1618769517543) (:by |u0) (:text |xs) - |t $ %{} :Expr (:at 1618769532837) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769533874) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618769535535) (:by |u0) (:text "|\"many...") - |v $ %{} :Expr (:at 1618769518829) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769519471) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618769522352) (:by |u0) (:text "|\"x0") - |r $ %{} :Leaf (:at 1618769523977) (:by |u0) (:text |x0) - |x $ %{} :Expr (:at 1618769524533) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769525175) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1618769525982) (:by |u0) (:text "|\"xs") - |r $ %{} :Leaf (:at 1618769526896) (:by |u0) (:text |xs) - :examples $ [] - |demos $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |defn) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |demos) - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |Z $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"demo") - |b $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |d $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"f1") - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |f1) - |f $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&{}) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |:a) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |:b) - |b $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |h $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |#{}) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) - |b $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text ||four) - |j $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |lib/f2) - |l $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |f3) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"arg of 3") - |n $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"quote:") - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quote) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |p $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"quo:") - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |'demo) - |Z $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quote) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |'demo) - |r $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"eval:") - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |eval) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quote) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |t $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |if) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |true) - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"true") - |v $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |if) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |false) - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"true") - |Z $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"false") - |x $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |if) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"3") - |Z $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"?") - |y $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&let) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |a) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"a is:") - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |a) - |z $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&let) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"a is none") - |zV $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&let) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |a) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |4) - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"a is:") - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |a) - |zX $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |rest) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |[]) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) - |b $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |4) - |zZ $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |type-of) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |[]) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |zb $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"result:") - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |foldl) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |[]) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) - |b $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |4) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |0) - |Z $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |defn) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |f1) - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |acc) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |x) - |Z $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"adding:") - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |acc) - |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |x) - |b $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |&+) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |acc) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |x) - |zd $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"macro:") - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-num) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |zf $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"sum:") - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |rec-sum) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |0) - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |[]) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) - |b $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |4) - |zh $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"expand-1:") - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |macroexpand-1) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quote) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-num) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |zj $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"expand:") - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |macroexpand) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quote) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-num) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |zl $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"expand:") - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |format-to-lisp) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |macroexpand) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |quote) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-more) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |0) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) - |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |8) - |zn $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"expand v:") - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-more) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |0) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |3) - |Z $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |8) - |zp $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "|\"call and call") - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |add-by-2) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |10) - |zr $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |;) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |macroexpand) - |V $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |assert=) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |1) - |X $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |2) - |zt $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |test-args) - :examples $ [] - |f1 $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |defn) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |f1) - |X $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |Z $ %{} :Expr (:at 1773136412558) (:by |sync) - :data $ {} - |T $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text |println) - |V $ %{} :Leaf (:at 1773136412558) (:by |sync) (:text "||Hello with leaf!") - :examples $ [] - |fib $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1619930459257) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930459257) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1619930459257) (:by |u0) (:text |fib) - |r $ %{} :Expr (:at 1619930459257) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930460888) (:by |u0) (:text |n) - |v $ %{} :Expr (:at 1619930461450) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930461900) (:by |u0) (:text |if) - |j $ %{} :Expr (:at 1619930462153) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930465800) (:by |u0) (:text |<) - |j $ %{} :Leaf (:at 1619930466571) (:by |u0) (:text |n) - |r $ %{} :Leaf (:at 1619930467516) (:by |u0) (:text |2) - |p $ %{} :Leaf (:at 1619976301564) (:by |u0) (:text |1) - |v $ %{} :Expr (:at 1619930469154) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930469867) (:by |u0) (:text |+) - |j $ %{} :Expr (:at 1619930471373) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930473045) (:by |u0) (:text |fib) - |j $ %{} :Expr (:at 1619930473244) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930475429) (:by |u0) (:text |-) - |j $ %{} :Leaf (:at 1619930476120) (:by |u0) (:text |n) - |r $ %{} :Leaf (:at 1619930476518) (:by |u0) (:text |1) - |r $ %{} :Expr (:at 1619930471373) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930473045) (:by |u0) (:text |fib) - |j $ %{} :Expr (:at 1619930473244) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930475429) (:by |u0) (:text |-) - |j $ %{} :Leaf (:at 1619930476120) (:by |u0) (:text |n) - |r $ %{} :Leaf (:at 1619930481371) (:by |u0) (:text |2) - :examples $ [] - |main! $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1619930570377) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930570377) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1619930570377) (:by |u0) (:text |main!) - |r $ %{} :Expr (:at 1619930570377) (:by |u0) - :data $ {} - |v $ %{} :Expr (:at 1619930574797) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619930577305) (:by |u0) (:text |demos) - |y $ %{} :Expr (:at 1619930582609) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1622292794753) (:by |u0) (:text |;) - |T $ %{} :Leaf (:at 1619930582609) (:by |u0) (:text |fib) - |j $ %{} :Leaf (:at 1619930582609) (:by |u0) (:text |10) - |yT $ %{} :Expr (:at 1622292783688) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1622292787836) (:by |u0) (:text |try-method) - |yj $ %{} :Expr (:at 1633872988484) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1633873455342) (:by |u0) (:text |;) - |T $ %{} :Leaf (:at 1633872991931) (:by |u0) (:text |show-data) - :examples $ [] - |rec-sum $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618723127970) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723127970) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618723127970) (:by |u0) (:text |rec-sum) - |r $ %{} :Expr (:at 1618723127970) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723129611) (:by |u0) (:text |acc) - |j $ %{} :Leaf (:at 1618723131566) (:by |u0) (:text |xs) - |v $ %{} :Expr (:at 1618723135708) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723136188) (:by |u0) (:text |if) - |j $ %{} :Expr (:at 1618723136714) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723138019) (:by |u0) (:text |empty?) - |j $ %{} :Leaf (:at 1618723146569) (:by |u0) (:text |xs) - |r $ %{} :Leaf (:at 1618723147576) (:by |u0) (:text |acc) - |v $ %{} :Expr (:at 1618723147929) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723151992) (:by |u0) (:text |recur) - |j $ %{} :Expr (:at 1618723153359) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723158533) (:by |u0) (:text |&+) - |j $ %{} :Leaf (:at 1618723159204) (:by |u0) (:text |acc) - |r $ %{} :Expr (:at 1618723160405) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723268153) (:by |u0) (:text |nth) - |j $ %{} :Leaf (:at 1618723162178) (:by |u0) (:text |xs) - |r $ %{} :Leaf (:at 1618723268981) (:by |u0) (:text |0) - |r $ %{} :Expr (:at 1618723164698) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618723165126) (:by |u0) (:text |rest) - |j $ %{} :Leaf (:at 1618723165879) (:by |u0) (:text |xs) - :examples $ [] - |reload! $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1619207810174) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619207810174) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1619207810174) (:by |u0) (:text |reload!) - |r $ %{} :Expr (:at 1619207810174) (:by |u0) - :data $ {} - |v $ %{} :Expr (:at 1619766026889) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1619766027788) (:by |u0) (:text |println) - |j $ %{} :Leaf (:at 1619766033570) (:by |u0) (:text "|\"reloaded 2") - |x $ %{} :Expr (:at 1619930543193) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1622292791514) (:by |u0) (:text |;) - |T $ %{} :Leaf (:at 1619930544016) (:by |u0) (:text |fib) - |j $ %{} :Leaf (:at 1619935071727) (:by |u0) (:text |40) - |y $ %{} :Expr (:at 1622292799913) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1622292800206) (:by |u0) (:text |try-method) - :examples $ [] - |show-data $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1633872992647) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633872992647) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1633872992647) (:by |u0) (:text |show-data) - |r $ %{} :Expr (:at 1633872992647) (:by |u0) - :data $ {} - |t $ %{} :Expr (:at 1633873024178) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633873031232) (:by |u0) (:text |load-console-formatter!) - |v $ %{} :Expr (:at 1633872993861) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633872996602) (:by |u0) (:text |js/console.log) - |j $ %{} :Expr (:at 1633872997079) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633873000863) (:by |u0) (:text |defrecord!) - |j $ %{} :Leaf (:at 1633873004188) (:by |u0) (:text |:Demo) - |r $ %{} :Expr (:at 1633873006952) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633873004646) (:by |u0) (:text |:a) - |j $ %{} :Leaf (:at 1633873007810) (:by |u0) (:text |1) - |v $ %{} :Expr (:at 1633873008937) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633873009838) (:by |u0) (:text |:b) - |j $ %{} :Expr (:at 1633873010851) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633873011411) (:by |u0) (:text |{}) - |j $ %{} :Expr (:at 1633873011697) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1633873012008) (:by |u0) (:text |:a) - |j $ %{} :Leaf (:at 1633873013762) (:by |u0) (:text |1) - :examples $ [] - |test-args $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1618767933203) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767933203) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1618767933203) (:by |u0) (:text |test-args) - |r $ %{} :Expr (:at 1618767933203) (:by |u0) - :data $ {} - |v $ %{} :Expr (:at 1618767936819) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767946838) (:by |u0) (:text |call-3) - |b $ %{} :Leaf (:at 1618767951283) (:by |u0) (:text |&) - |j $ %{} :Expr (:at 1618767948145) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618767948346) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618767949355) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618767949593) (:by |u0) (:text |2) - |v $ %{} :Leaf (:at 1618769480611) (:by |u0) (:text |3) - |x $ %{} :Expr (:at 1618769504303) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769507599) (:by |u0) (:text |call-many) - |j $ %{} :Leaf (:at 1618769530122) (:by |u0) (:text |1) - |y $ %{} :Expr (:at 1618769504303) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769507599) (:by |u0) (:text |call-many) - |b $ %{} :Leaf (:at 1618769543673) (:by |u0) (:text |1) - |j $ %{} :Leaf (:at 1618769540547) (:by |u0) (:text |2) - |yT $ %{} :Expr (:at 1618769504303) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769507599) (:by |u0) (:text |call-many) - |j $ %{} :Leaf (:at 1618769545875) (:by |u0) (:text |1) - |r $ %{} :Leaf (:at 1618769546500) (:by |u0) (:text |2) - |v $ %{} :Leaf (:at 1618769546751) (:by |u0) (:text |3) - |yj $ %{} :Expr (:at 1618769890713) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618769891472) (:by |u0) (:text |println) - |T $ %{} :Expr (:at 1618769885586) (:by |u0) - :data $ {} - |D $ %{} :Leaf (:at 1618769888788) (:by |u0) (:text |macroexpand) - |T $ %{} :Expr (:at 1618769673535) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618769675192) (:by |u0) (:text |call-macro) - |j $ %{} :Leaf (:at 1618769762350) (:by |u0) (:text |11) - |r $ %{} :Leaf (:at 1618769837129) (:by |u0) (:text |12) - |v $ %{} :Leaf (:at 1618769849272) (:by |u0) (:text |13) - :examples $ [] - |try-method $ %{} :CodeEntry (:doc |) - :code $ %{} :Expr (:at 1622292801677) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1622292802864) (:by |u0) (:text |defn) - |j $ %{} :Leaf (:at 1622292801677) (:by |u0) (:text |try-method) - |r $ %{} :Expr (:at 1622292801677) (:by |u0) - :data $ {} - |v $ %{} :Expr (:at 1622292803720) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1622292805545) (:by |u0) (:text |println) - |j $ %{} :Expr (:at 1622292805914) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1622292806869) (:by |u0) (:text |.count) - |j $ %{} :Expr (:at 1622292809130) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1622292811398) (:by |u0) (:text |range) - |j $ %{} :Leaf (:at 1622292816464) (:by |u0) (:text |11) - :examples $ [] - :ns $ %{} :NsEntry (:doc |) - :code $ %{} :Expr (:at 1618539507433) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618539507433) (:by |u0) (:text |ns) - |j $ %{} :Leaf (:at 1618539507433) (:by |u0) (:text |app.main) - |r $ %{} :Expr (:at 1618661030124) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661030826) (:by |u0) (:text |:require) - |j $ %{} :Expr (:at 1618661031081) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661035015) (:by |u0) (:text |app.lib) - |j $ %{} :Leaf (:at 1618661039398) (:by |u0) (:text |:as) - |r $ %{} :Leaf (:at 1618661040510) (:by |u0) (:text |lib) - |r $ %{} :Expr (:at 1618661042947) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661044709) (:by |u0) (:text |app.lib) - |j $ %{} :Leaf (:at 1618661045794) (:by |u0) (:text |:refer) - |r $ %{} :Expr (:at 1618661046024) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618661046210) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618661047074) (:by |u0) (:text |f3) - |v $ %{} :Expr (:at 1618720195824) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618720199292) (:by |u0) (:text |app.macro) - |j $ %{} :Leaf (:at 1618720200969) (:by |u0) (:text |:refer) - |r $ %{} :Expr (:at 1618720201238) (:by |u0) - :data $ {} - |T $ %{} :Leaf (:at 1618720201399) (:by |u0) (:text |[]) - |j $ %{} :Leaf (:at 1618720203059) (:by |u0) (:text |add-num) - |r $ %{} :Leaf (:at 1618740371002) (:by |u0) (:text |add-by-2) - :users $ {} - |u0 $ {} (:avatar nil) (:id |u0) (:name |chen) (:nickname |chen) (:password |d41d8cd98f00b204e9800998ecf8427e) (:theme :star-trail) diff --git a/calcit/editor/compact.cirru b/calcit/editor/compact.cirru deleted file mode 100644 index 01b329b4..00000000 --- a/calcit/editor/compact.cirru +++ /dev/null @@ -1,150 +0,0 @@ - -{} (:about "|file is generated - never edit directly; learn cr edit/tree workflows before changing") (:package |app) - :configs $ {} (:init-fn |app.main/main!) (:reload-fn |app.main/reload!) (:version |0.0.1) - :modules $ [] - :entries $ {} - :files $ {} - |app.lib $ %{} :FileEntry - :defs $ {} - |f2 $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn f2 () $ println "\"f2 in lib" - :examples $ [] - |f3 $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn f3 (x) (println "\"f3 in lib") (println "\"v:" x) - :examples $ [] - :ns $ %{} :NsEntry (:doc |) - :code $ quote (ns app.lib) - |app.macro $ %{} :FileEntry - :defs $ {} - |add-by-1 $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defmacro add-by-1 (x) - quasiquote $ &+ ~x 1 - :examples $ [] - |add-by-2 $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defmacro add-by-2 (x) - quasiquote $ &+ 2 (add-by-1 ~x) - :examples $ [] - |add-num $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defmacro add-num (a b) - quasiquote $ &let () - &+ (~ a) (~ b) - :examples $ [] - :ns $ %{} :NsEntry (:doc |) - :code $ quote (ns app.macro) - |app.main $ %{} :FileEntry - :defs $ {} - |add-more $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defmacro add-more (acc x times) - if (&< times 1) acc $ recur - quasiquote $ &+ (~ x) (~ acc) - , x (&- times 1) - :examples $ [] - |call-3 $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn call-3 (a b c) (println "\"a is:" a) (println "\"b is:" b) (println "\"c is:" c) - :examples $ [] - |call-macro $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defmacro call-macro (x0 & xs) - quasiquote $ &{} :a (~ x0) :b - [] $ ~@ xs - :examples $ [] - |call-many $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn call-many (x0 & xs) (println "\"many...") (println "\"x0" x0) (println "\"xs" xs) - :examples $ [] - |demos $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn demos () (println "\"demo") - println $ &+ 2 2 - println "\"f1" $ f1 - println $ &{} :a 1 :b 2 - println $ #{} 1 2 3 |four - lib/f2 - f3 "\"arg of 3" - println "\"quote:" $ quote (&+ 1 2) - println "\"quo:" 'demo $ quote 'demo - println "\"eval:" $ eval - quote $ &+ 1 2 - if true $ println "\"true" - if false (println "\"true") (println "\"false") - if (&+ 1 2) (println "\"3") (println "\"?") - &let (a 1) (println "\"a is:" a) - &let () $ println "\"a is none" - &let - a $ &+ 3 4 - println "\"a is:" a - println $ rest ([] 1 2 3 4) - println $ type-of ([] 1) - println "\"result:" $ foldl ([] 1 2 3 4) 0 - defn f1 (acc x) (println "\"adding:" acc x) (&+ acc x) - println "\"macro:" $ add-num 1 2 - println "\"sum:" $ rec-sum 0 ([] 1 2 3 4) - println "\"expand-1:" $ macroexpand-1 - quote $ add-num 1 2 - println "\"expand:" $ macroexpand - quote $ add-num 1 2 - println "\"expand:" $ format-to-lisp - macroexpand $ quote (add-more 0 3 8) - println "\"expand v:" $ add-more 0 3 8 - println "\"call and call" $ add-by-2 10 - ; println $ macroexpand (assert= 1 2) - test-args - :examples $ [] - |f1 $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn f1 () $ println "|Hello with leaf!" - :examples $ [] - |fib $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn fib (n) - if (< n 2) 1 $ + - fib $ - n 1 - fib $ - n 2 - :examples $ [] - |main! $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn main! () (demos) (; fib 10) (try-method) (; show-data) - :examples $ [] - |rec-sum $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn rec-sum (acc xs) - if (empty? xs) acc $ recur - &+ acc $ nth xs 0 - rest xs - :examples $ [] - |reload! $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn reload! () (println "\"reloaded 2") (; fib 40) (try-method) - :examples $ [] - |show-data $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn show-data () (load-console-formatter!) - js/console.log $ defrecord! :Demo (:a 1) - :b $ {} (:a 1) - :examples $ [] - |test-args $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn test-args () - call-3 & $ [] 1 2 3 - call-many 1 - call-many 1 2 - call-many 1 2 3 - println $ macroexpand (call-macro 11 12 13) - :examples $ [] - |try-method $ %{} :CodeEntry (:doc |) (:schema nil) - :code $ quote - defn try-method () $ println - .count $ range 11 - :examples $ [] - :ns $ %{} :NsEntry (:doc |) - :code $ quote - ns app.main $ :require (app.lib :as lib) - app.lib :refer $ [] f3 - app.macro :refer $ [] add-num add-by-2 diff --git a/calcit/fibo.cirru b/calcit/fibo.cirru index 6d13e70c..cf87a89b 100644 --- a/calcit/fibo.cirru +++ b/calcit/fibo.cirru @@ -17,7 +17,7 @@ :examples $ [] |main! $ %{} :CodeEntry (:doc |) (:schema nil) :code $ quote - defn main! () (println "\"Loaded program!") (try-fibo) + defn main! () (println "|Loaded program!") (try-fibo) :examples $ [] |reload! $ %{} :CodeEntry (:doc |) (:schema nil) :code $ quote @@ -36,7 +36,7 @@ :code $ quote defn try-fibo () $ let n 22 - println "\"fibo result:" n $ fibo n + println "|fibo result:" n $ fibo n :examples $ [] |try-prime $ %{} :CodeEntry (:doc |) (:schema nil) :code $ quote diff --git a/calcit/test-algebra.cirru b/calcit/test-algebra.cirru index 8fd58103..ed3851ab 100644 --- a/calcit/test-algebra.cirru +++ b/calcit/test-algebra.cirru @@ -58,7 +58,7 @@ :examples $ [] |main! $ %{} :CodeEntry (:doc |) :code $ quote - defn main! () (log-title "|Testing algebra") (; "\"Experimental code, to simulate usages like Monad") (test-map) (test-bind) (test-apply) (test-mappend) + defn main! () (log-title "|Testing algebra") (; "|Experimental code, to simulate usages like Monad") (test-map) (test-bind) (test-apply) (test-mappend) :examples $ [] :schema $ :: :fn {} (:return :dynamic) diff --git a/calcit/test-gynienic.cirru b/calcit/test-gynienic.cirru index cb049967..9d8b878f 100644 --- a/calcit/test-gynienic.cirru +++ b/calcit/test-gynienic.cirru @@ -11,8 +11,8 @@ defmacro add-11 (a b) let c 11 - println "\"internal c:" a b c - quasiquote $ do (println "\"c is:" c) + println "|internal c:" a b c + quasiquote $ do (println "|c is:" c) [] (~ a) (~ b) c (~ c) (add-2 8) :examples $ [] |add-2 $ %{} :CodeEntry (:doc |) diff --git a/calcit/test-hygienic.cirru b/calcit/test-hygienic.cirru index 97c711aa..6c6fdbca 100644 --- a/calcit/test-hygienic.cirru +++ b/calcit/test-hygienic.cirru @@ -11,8 +11,8 @@ defmacro add-11 (a b) let c 11 - println "\"internal c:" a b c - quasiquote $ do (println "\"c is:" c) + println "|internal c:" a b c + quasiquote $ do (println "|c is:" c) [] (~ a) (~ b) c (~ c) (add-2 8) :examples $ [] :schema $ :: :macro diff --git a/calcit/test-recursion.cirru b/calcit/test-recursion.cirru index 80e0843b..b6b6a8d7 100644 --- a/calcit/test-recursion.cirru +++ b/calcit/test-recursion.cirru @@ -12,7 +12,7 @@ |hole-series $ %{} :CodeEntry (:doc |) :code $ quote defn hole-series (x) (assert-type x :number) - if (&<= x 0) (raise "\"unexpected small number") + if (&<= x 0) (raise "|unexpected small number") if (&= x 1) 0 $ if (&= x 2) 1 let extra $ .rem x 3 diff --git a/docs/ecosystem.md b/docs/ecosystem.md index 987e62ae..f3177fae 100644 --- a/docs/ecosystem.md +++ b/docs/ecosystem.md @@ -8,21 +8,14 @@ Useful libraries are maintained at . - [Respo: virtual DOM library](https://github.com/Respo/respo.calcit) - [Phlox: virtual DOM like wrapper on top of PIXI](https://github.com/Quamolit/phlox.calcit) -- [Quaterfoil: thin virtual DOM wrapper over three.js](https://github.com/Quamolit/quatrefoil.calcit) - [Triadica: toy project rendering interactive 3D shapes with math and shader](https://github.com/Triadica/triadica-space) -- [tiny tool for drawing 3D shapes with WebGPU](https://github.com/Triadica/lagopus) - [Cumulo: template for tiny realtime apps](https://github.com/Cumulo/cumulo-workflow.calcit) -- [Quamolit: what if we make animations in React's way?](https://github.com/Quamolit/quamolit.calcit) ### Tools: - [Calcit Editor](https://github.com/calcit-lang/editor) - Structural editor for Calcit (Web-based) - [Calcit IR viewer](https://github.com/calcit-lang/calcit-ir-viewer) - Visualize IR representation - [Calcit Error viewer](https://github.com/calcit-lang/calcit-error-viewer) - Enhanced error display -- [Calcit Paint](https://github.com/calcit-lang/calcit-paint) - Experimental 2D shape editor -- [cr-mcp](https://github.com/calcit-lang/calcit) - MCP server for tool integration -- [calcit-bundler](https://www.npmjs.com/package/@calcit/bundler) - Bundle indentation syntax to compact format -- [caps-cli](https://www.npmjs.com/package/@calcit/caps) - Dependency management tool ### VS Code Integration: From 82c846f17b1547196ec9e21a12a66aedfae89f66 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 26 Mar 2026 20:14:31 +0800 Subject: [PATCH 44/57] Improve docs indexing and module lookup --- docs/CalcitAgent.md | 18 +- docs/cirru-syntax.md | 23 +- docs/data.md | 10 + docs/data/edn.md | 12 + docs/data/persistent-data.md | 10 + docs/data/string.md | 10 + docs/docs-indexing.md | 211 ++++++ docs/docs-validation.md | 113 ++++ docs/ecosystem.md | 10 + docs/features.md | 15 + docs/features/common-patterns.md | 10 + docs/features/enums.md | 10 + docs/features/error-handling.md | 10 + docs/features/hashmap.md | 10 + docs/features/imports.md | 18 + docs/features/js-interop.md | 11 + docs/features/list.md | 11 + docs/features/macros.md | 10 + docs/features/polymorphism.md | 14 + docs/features/records.md | 10 + docs/features/sets.md | 10 + docs/features/static-analysis.md | 13 + docs/features/traits.md | 10 + docs/features/tuples.md | 10 + docs/installation.md | 10 + docs/installation/ffi-bindings.md | 10 + docs/installation/github-actions.md | 10 + docs/installation/modules.md | 14 + docs/intro.md | 10 + docs/intro/from-clojure.md | 10 + docs/intro/indentation-syntax.md | 17 + docs/intro/overview.md | 10 + docs/quick-reference.md | 14 + docs/run.md | 15 + docs/run/agent-advanced.md | 15 + docs/run/bundle-mode.md | 10 + docs/run/cli-options.md | 21 + docs/run/docs-libs.md | 53 ++ docs/run/edit-tree.md | 16 + docs/run/entries.md | 11 + docs/run/eval.md | 19 + docs/run/hot-swapping.md | 13 + docs/run/load-deps.md | 10 + docs/run/query.md | 22 + docs/run/upgrade.md | 11 + docs/structural-editor.md | 11 + ...2013-docs-frontmatter-and-module-search.md | 58 ++ src/bin/cli_handlers/cirru_validator.rs | 35 +- src/bin/cli_handlers/command_echo.rs | 12 +- src/bin/cli_handlers/docs.rs | 616 +++++++++++++++--- src/bin/cli_handlers/docs_tests.rs | 433 ++++++++++++ src/cli_args.rs | 18 + 52 files changed, 1992 insertions(+), 101 deletions(-) create mode 100644 docs/docs-indexing.md create mode 100644 docs/docs-validation.md create mode 100644 editing-history/2026-0326-2013-docs-frontmatter-and-module-search.md create mode 100644 src/bin/cli_handlers/docs_tests.rs diff --git a/docs/CalcitAgent.md b/docs/CalcitAgent.md index c9697c2b..6a068366 100644 --- a/docs/CalcitAgent.md +++ b/docs/CalcitAgent.md @@ -1,3 +1,17 @@ +--- +title: "Calcit Agent 快速实践(局部查看与编辑优先)" +scope: "core" +kind: "agent" +category: "run" +aliases: + - "agent workflow" + - "llm workflow" + - "local editing guide" + - "copilot workflow" +entry_for: + - "cr docs agents" + - "cr docs read agent-advanced.md" +--- # Calcit Agent 快速实践(局部查看与编辑优先) 本文档面向 Agent/LLM 的高频工作流,目标是**更快定位、最小改动、低噪音验证**。 @@ -187,13 +201,13 @@ cr js ## 0) 硬前置步骤 -在任何 `cr edit` / `cr tree` 修改前,先执行一次: +在任何 `cr edit` / `cr tree` 修改前,如果没有命令行相关的记忆, 执行命令获取关键文档的内容: ```bash cr docs agents --full ``` -这一步不是建议项,用于避免沿用旧命令心智模型。 +这个文件默认存储在 `~/./config/calcit/Agents.md`, 后续步骤可以直接读取. --- diff --git a/docs/cirru-syntax.md b/docs/cirru-syntax.md index 16250d53..5f695903 100644 --- a/docs/cirru-syntax.md +++ b/docs/cirru-syntax.md @@ -1,3 +1,19 @@ +--- +title: "Cirru Syntax Essentials" +scope: "core" +kind: "reference" +category: "syntax" +aliases: + - "cirru syntax" + - "dollar operator" + - "comma operator" + - "string literals" +entry_for: + - "cr cirru parse" + - "cr cirru format" + - "cr cirru show-guide" +--- + ## Cirru Syntax Essentials ### 1. Indentation = Nesting @@ -89,12 +105,7 @@ defmacro when-not (cond & body) JSON equivalent: ```json -[ - "defmacro", - "when-not", - ["cond", "&", "body"], - ["quasiquote", ["if", ["not", "~cond"], ["do", "~@body"]]] -] +["defmacro", "when-not", ["cond", "&", "body"], ["quasiquote", ["if", ["not", "~cond"], ["do", "~@body"]]]] ``` ## LLM Guidance & Optimization diff --git a/docs/data.md b/docs/data.md index 3489c26b..2a8e0e5b 100644 --- a/docs/data.md +++ b/docs/data.md @@ -1,3 +1,13 @@ +--- +title: "Data Types" +scope: "core" +kind: "hub" +category: "data" +aliases: + - "data types" + - "persistent data" + - "immutable data" +--- # Data Types Calcit provides several core data types, all immutable by default for functional programming: diff --git a/docs/data/edn.md b/docs/data/edn.md index fcb31ad7..2bdc657d 100644 --- a/docs/data/edn.md +++ b/docs/data/edn.md @@ -1,3 +1,15 @@ +--- +title: "Cirru Extensible Data Notation" +scope: "core" +kind: "reference" +category: "data" +aliases: + - "cirru edn" + - "edn notation" + - "data notation" +entry_for: + - "cr cirru parse-edn" +--- # Cirru Extensible Data Notation > Data notation based on Cirru. Learnt from [Clojure EDN](https://github.com/edn-format/edn). diff --git a/docs/data/persistent-data.md b/docs/data/persistent-data.md index e1f97bae..89b3ce8d 100644 --- a/docs/data/persistent-data.md +++ b/docs/data/persistent-data.md @@ -1,3 +1,13 @@ +--- +title: "Persistent Data" +scope: "core" +kind: "reference" +category: "data" +aliases: + - "persistent collections" + - "immutable collections" + - "ternary tree" +--- # Persistent Data Calcit uses [rpds](https://github.com/orium/rpds) for HashMap and HashSet, and use [Ternary Tree](https://github.com/calcit-lang/ternary-tree.rs/) in Rust. diff --git a/docs/data/string.md b/docs/data/string.md index 7c32ea37..6e006b25 100644 --- a/docs/data/string.md +++ b/docs/data/string.md @@ -1,3 +1,13 @@ +--- +title: "String" +scope: "core" +kind: "reference" +category: "data" +aliases: + - "string literals" + - "pipe prefix" + - "quoted strings" +--- # String The way strings are represented in Calcit is a bit unique. Strings are distinguished by a prefix. For example, `|A` represents the string `A`. If the string contains spaces, you need to enclose it in double quotes, such as `"|A B"`, where `|` is the string prefix. Due to the history of the structural editor, `"` is also a string prefix, but it is special: when used inside a string, it must be escaped as `"\"A"`. This is equivalent to `|A` and also to `"|A"`. The outermost double quotes can be omitted when there is no ambiguity. diff --git a/docs/docs-indexing.md b/docs/docs-indexing.md new file mode 100644 index 00000000..c4561942 --- /dev/null +++ b/docs/docs-indexing.md @@ -0,0 +1,211 @@ +--- +title: "Documentation Indexing Spec" +scope: "core" +kind: "spec" +category: "docs" +aliases: + - "frontmatter" + - "docs metadata" + - "search indexing" +entry_for: + - "cr docs search" + - "cr docs read" + - "cr docs read-lines" +--- + +# Documentation Indexing Spec + +This page defines the documentation layout expected by `cr docs`. + +The goal is to keep docs git-friendly and readable while still giving the CLI enough structure for fast lookup. + +## Scope Layout + +There are 2 documentation scopes: + +- `core`: + - `~/.config/calcit/Agents.md` + - `~/.config/calcit/docs/` +- `module`: + - `~/.config/calcit/modules//Agents.md` + - `~/.config/calcit/modules//docs/` + +`core` docs explain the Calcit toolchain itself. +`module` docs explain APIs and workflows that belong to a specific installed module. + +## Responsibilities + +Core docs should cover: + +- Cirru syntax +- CLI behavior +- query/edit/run workflows +- language/runtime semantics + +Module docs should cover: + +- module APIs +- module-specific workflows +- module-specific conventions +- domain examples that do not belong in Calcit core docs + +## Minimal File Rules + +For module-aware lookup to work well: + +- every module should provide `Agents.md` as the module entry page when practical +- module markdown files should live under `docs/` +- file names should stay stable and descriptive +- headings should prefer task-oriented wording over abstract labels + +## Frontmatter Schema + +Each markdown file under `docs/` should provide a small frontmatter block. + +The same schema is also supported for: + +- core `Agents.md` +- module `Agents.md` +- module markdown files under `~/.config/calcit/modules//docs/` + +Required fields: + +- `title`: user-facing page title +- `scope`: `core` or `module` +- `kind`: one of `hub`, `guide`, `reference`, `spec`, `agent` +- `category`: one value from the category registry below + +### Category Registry + +Use these categories for core docs unless there is a strong reason not to: + +- `run`: CLI execution, eval, query, edit-tree, docs commands, upgrade workflow +- `features`: language features such as traits, macros, records, tuples, enums, collections +- `installation`: installation, modules directory, CI setup, runtime bindings +- `data`: data literals and data structures, such as strings, EDN, persistent collections +- `intro`: onboarding and conceptual overview pages +- `syntax`: Cirru syntax and `compact.cirru`/indentation-oriented structure docs +- `tools`: deprecated or auxiliary tooling docs, such as editor-oriented pages +- `ecosystem`: library landscape and surrounding projects +- `reference`: cross-cutting lookup pages such as cheatsheets +- `docs`: documentation system specs and indexing conventions themselves + +Category selection rules: + +- prefer an existing category over inventing a new one +- choose by the page's primary retrieval intent, not by one incidental section +- use `reference` only for genuinely cross-cutting lookup material; do not use it as a fallback for every technical page +- use `docs` only for pages about the documentation system itself +- introduce a new category only when several pages clearly form a stable family + +Optional fields: + +- `aliases`: extra phrases that users are likely to search for +- `entry_for`: commands, APIs, or task phrases that should point to this page + +Recommended shape: + +```yaml +--- +title: "CLI Code Editing" +scope: "core" +kind: "guide" +category: "run" +aliases: + - "edit tree" + - "target-replace" + - "imports" +entry_for: + - "cr tree target-replace" + - "cr edit add-import" +--- +``` + +Notes: + +- frontmatter should stay short and retrieval-oriented +- `aliases` should use phrases people actually type, not every possible synonym +- `entry_for` is best for commands, APIs, and exact task wording +- `scope` is authored explicitly even if it can be inferred from directory layout +- `Agents.md` can use the same schema; `cr docs agents` will strip frontmatter before rendering +- unknown `category` values are rejected during doc loading so the registry stays stable + +Examples: + +- `docs/api.md` +- `docs/guide/hot-swapping.md` +- `docs/apis/render!.md` + +## Search Behavior + +`cr docs search` now supports 3 scopes: + +- `--scope core` +- `--scope modules` +- `--scope all` + +It also supports narrowing to one installed module: + +```bash +cr docs search render --module respo.calcit +``` + +When `--module` is provided without `--scope`, the default scope becomes `modules`. +Without both flags, the default scope stays `core`. + +## Read Behavior + +`cr docs read` now uses the same document resolver style as `search`. + +`cr docs read-lines` uses the same resolver too. + +It can resolve a page by: + +- filename +- relative path fragment +- frontmatter `title` +- frontmatter `aliases` +- frontmatter `entry_for` + +It also supports: + +- `--scope core|modules|all` +- `--module ` + +Examples: + +```bash +cr docs read target-replace +cr docs read "CLI Code Editing" +cr docs read Respo-Agent --module respo.calcit +cr docs read-lines target-replace --start 48 --lines 8 +cr docs read-lines Respo-Agent --module respo.calcit --start 1 --lines 8 +``` + +## Recommended Authoring Style + +To improve CLI recall without introducing heavy metadata: + +- keep one topic per file when possible +- use clear headings such as `Quick start`, `Hot swapping`, `Common patterns` +- repeat important API names and command names in prose near the heading +- use `Agents.md` as a curated entry page instead of duplicating full docs +- keep frontmatter aligned with actual body content; do not use metadata to paper over weak headings +- prefer `aliases` for alternate wording, and reserve `entry_for` for explicit command/API entry points + +## Validation + +Keep executable retrieval checks in a separate validation document so this file stays normative. + +Use [docs/docs-validation.md](docs/docs-validation.md) for: + +- search ranking checks +- alias and `entry_for` checks +- `read` / `read-lines` resolver checks +- module-scope regression checks + +When changing retrieval logic or frontmatter conventions: + +- update this file if the rules changed +- update the validation document if the expected behavior changed +- rerun the validation commands from the validation document diff --git a/docs/docs-validation.md b/docs/docs-validation.md new file mode 100644 index 00000000..3deca523 --- /dev/null +++ b/docs/docs-validation.md @@ -0,0 +1,113 @@ +--- +title: "Documentation Retrieval Validation" +scope: "core" +kind: "reference" +category: "docs" +aliases: + - "docs validation" + - "search validation" + - "retrieval regression" +entry_for: + - "cr docs search" + - "cr docs read" + - "cr docs read-lines" +--- + +# Documentation Retrieval Validation + +This page contains executable validation cases for `cr docs search`, `cr docs read`, and `cr docs read-lines`. + +Use it when changing: + +- frontmatter fields and conventions +- ranking weights +- resolver behavior +- scope/module routing + +## Core Search Checks + +```bash +cr docs search polymorphism +cr docs search edit-tree -f run.md +cr docs search target-replace +cr docs search watch mode +cr docs search compact.cirru +``` + +## Module Search Checks + +```bash +cr docs search render --module respo.calcit +cr docs search clear-cache --scope modules +cr docs search defstyle --module respo.calcit +cr docs search hook --module respo.calcit +``` + +## Cross-scope Checks + +```bash +cr docs search render --scope all +cr docs search agents --scope all +``` + +## Ranking Checks + +```bash +# A topical page should beat an examples/spec page that only mentions the term +cr docs search polymorphism + +# Alias-only queries should still find the right page +cr docs search target-replace +cr docs search hot reload + +# Command-oriented phrases should point at the operational guide +cr docs search "cr eval" +cr docs search "add-import" + +# Filename/path hits should stay useful when several pages mention the same term +cr docs search traits +cr docs search docs +``` + +## Agents and Module Checks + +```bash +# Agents frontmatter should not leak into output +cr docs agents + +# Module Agents and module docs should both participate in ranking +cr docs search render --module respo.calcit +cr docs search clear-cache --scope modules +``` + +## Read Checks + +```bash +# Metadata should not leak into read output +cr docs read polymorphism.md +cr docs read edit-tree.md target-replace + +# Resolver should work with aliases, titles, and command phrases +cr docs read target-replace +cr docs read "CLI Code Editing" +cr docs read "cr eval --dep" +cr docs read "cr edit add-import" +cr docs read "query ns" +cr docs read "reload fn" +cr docs read "indentation based syntax" +``` + +## Read-Lines Checks + +```bash +cr docs read-lines target-replace --start 48 --lines 8 +cr docs read-lines Respo-Agent --module respo.calcit --start 1 --lines 8 +``` + +## Update Rule + +When behavior changes: + +1. Update [docs/docs-indexing.md](docs/docs-indexing.md) if the rule changed. +2. Update this file if the expected observable behavior changed. +3. Re-run the relevant commands above. diff --git a/docs/ecosystem.md b/docs/ecosystem.md index f3177fae..6ef5ff10 100644 --- a/docs/ecosystem.md +++ b/docs/ecosystem.md @@ -1,3 +1,13 @@ +--- +title: "Ecosystem" +scope: "core" +kind: "guide" +category: "ecosystem" +aliases: + - "libraries" + - "modules" + - "packages" +--- # Ecosystem ### Libraries: diff --git a/docs/features.md b/docs/features.md index 32aed036..018d37ae 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,3 +1,18 @@ +--- +title: "Features" +scope: "core" +kind: "hub" +category: "features" +aliases: + - "language features" + - "collections" + - "methods" + - "interop" + - "type system" +entry_for: + - "impl-traits" + - "assert-type" +--- # Features Calcit inherits most features from Clojure/ClojureScript while adding its own innovations: diff --git a/docs/features/common-patterns.md b/docs/features/common-patterns.md index f6c2ffd2..901772bb 100644 --- a/docs/features/common-patterns.md +++ b/docs/features/common-patterns.md @@ -1,3 +1,13 @@ +--- +title: "Common Patterns" +scope: "core" +kind: "guide" +category: "features" +aliases: + - "common recipes" + - "pattern examples" + - "common tasks" +--- # Common Patterns This document provides practical examples and patterns for common programming tasks in Calcit. diff --git a/docs/features/enums.md b/docs/features/enums.md index 985fab0b..2374382e 100644 --- a/docs/features/enums.md +++ b/docs/features/enums.md @@ -1,3 +1,13 @@ +--- +title: "Enums (defenum)" +scope: "core" +kind: "reference" +category: "features" +aliases: + - "defenum" + - "tagged union" + - "tagged unions" +--- # Enums (defenum) Calcit enums are tagged unions — each variant has a tag (keyword) and zero or more typed payload fields. Under the hood enums are represented as tuples with a class reference. diff --git a/docs/features/error-handling.md b/docs/features/error-handling.md index 68127786..a32fd75a 100644 --- a/docs/features/error-handling.md +++ b/docs/features/error-handling.md @@ -1,3 +1,13 @@ +--- +title: "Error Handling" +scope: "core" +kind: "guide" +category: "features" +aliases: + - "try raise" + - "exception handling" + - "errors" +--- # Error Handling Calcit uses `try` / `raise` for exception-based error handling. Errors are string values (or tags) propagated up the call stack. diff --git a/docs/features/hashmap.md b/docs/features/hashmap.md index 357bc98a..716834bb 100644 --- a/docs/features/hashmap.md +++ b/docs/features/hashmap.md @@ -1,3 +1,13 @@ +--- +title: "HashMap" +scope: "core" +kind: "reference" +category: "features" +aliases: + - "hash map" + - "map" + - "key value" +--- # HashMap Calcit HashMap is a persistent, immutable hash map. In Rust it uses [rpds::HashTrieMap](https://docs.rs/rpds/0.10.0/rpds/#hashtriemap). In JavaScript it is built on [ternary-tree](https://github.com/calcit-lang/ternary-tree.ts). diff --git a/docs/features/imports.md b/docs/features/imports.md index 2f2c7c85..ec752960 100644 --- a/docs/features/imports.md +++ b/docs/features/imports.md @@ -1,3 +1,21 @@ +--- +title: "Imports" +scope: "core" +kind: "reference" +category: "features" +aliases: + - "namespace imports" + - "require" + - "refer" + - "module imports" + - "add import" + - "edit imports" +entry_for: + - "cr edit add-import" + - "cr edit imports" + - "ns :require" +--- + # Imports Calcit loads namespaces from `compact.cirru` (the compiled representation of source files). Dependencies are tracked via `~/.config/calcit/modules/`. diff --git a/docs/features/js-interop.md b/docs/features/js-interop.md index c72dbcfe..20e7489a 100644 --- a/docs/features/js-interop.md +++ b/docs/features/js-interop.md @@ -1,3 +1,14 @@ +--- +title: "JavaScript Interop" +scope: "core" +kind: "reference" +category: "features" +aliases: + - "javascript interop" + - "js interop" + - "promise" + - "js-await" +--- # JavaScript Interop Calcit keeps JS interop syntax intentionally small. This page covers the existing core patterns: diff --git a/docs/features/list.md b/docs/features/list.md index 8e3839f5..7efbe8af 100644 --- a/docs/features/list.md +++ b/docs/features/list.md @@ -1,3 +1,14 @@ +--- +title: "List" +scope: "core" +kind: "reference" +category: "features" +aliases: + - "vector" + - "range" + - "append" + - "nth" +--- # List Calcit List is a persistent, immutable vector. In Rust it uses [ternary-tree](https://github.com/calcit-lang/ternary-tree.rs) (optimized 2-3 tree with finger-tree tricks). In JavaScript it uses a similar structure with a fast-path `CalcitSliceList` for append-heavy workloads. diff --git a/docs/features/macros.md b/docs/features/macros.md index 130598e2..d6eca4a8 100644 --- a/docs/features/macros.md +++ b/docs/features/macros.md @@ -1,3 +1,13 @@ +--- +title: "Macros" +scope: "core" +kind: "guide" +category: "features" +aliases: + - "defmacro" + - "macro expansion" + - "quote" +--- # Macros Like Clojure, Calcit uses macros to support new syntax. And macros ared evaluated during building to expand syntax tree. A `defmacro` block returns list and symbols, as well as literals: diff --git a/docs/features/polymorphism.md b/docs/features/polymorphism.md index 41bbaa93..1ab1d2bb 100644 --- a/docs/features/polymorphism.md +++ b/docs/features/polymorphism.md @@ -1,3 +1,17 @@ +--- +title: "Polymorphism" +scope: "core" +kind: "guide" +category: "features" +aliases: + - "trait dispatch" + - "method dispatch" + - "impl-traits" +entry_for: + - "impl-traits" + - "trait-call" + - "assert-traits" +--- # Polymorphism Calcit models polymorphism with traits. Traits define method capabilities and can be attached to struct/enum definitions with `impl-traits`. diff --git a/docs/features/records.md b/docs/features/records.md index e3b19f76..b60f7460 100644 --- a/docs/features/records.md +++ b/docs/features/records.md @@ -1,3 +1,13 @@ +--- +title: "Records" +scope: "core" +kind: "reference" +category: "features" +aliases: + - "record type" + - "field access" + - "struct fields" +--- # Records Calcit provides Records as a way to define structured data types with named fields, similar to structs in other languages. Records are defined with `defstruct` and instantiated with the `%{}` macro. diff --git a/docs/features/sets.md b/docs/features/sets.md index e68180b7..cdd2b8b7 100644 --- a/docs/features/sets.md +++ b/docs/features/sets.md @@ -1,3 +1,13 @@ +--- +title: "Sets" +scope: "core" +kind: "reference" +category: "features" +aliases: + - "hashset" + - "set operations" + - "unique values" +--- # Sets Calcit provides HashSet data structure for storing unordered unique elements. In Rust implementation, it uses `rpds::HashTrieSet`, while in JavaScript it uses a custom implementation based on ternary-tree. diff --git a/docs/features/static-analysis.md b/docs/features/static-analysis.md index 46e66949..ab42aa53 100644 --- a/docs/features/static-analysis.md +++ b/docs/features/static-analysis.md @@ -1,3 +1,16 @@ +--- +title: "Static Type Analysis" +scope: "core" +kind: "reference" +category: "features" +aliases: + - "type check" + - "type warning" + - "assert-type" + - "compile-time checks" +entry_for: + - "assert-type" +--- # Static Type Analysis Calcit includes a built-in static type analysis system that performs compile-time checks to catch common errors before runtime. This system operates during the preprocessing phase and provides warnings for type mismatches and other potential issues. diff --git a/docs/features/traits.md b/docs/features/traits.md index f09ed229..e5352a44 100644 --- a/docs/features/traits.md +++ b/docs/features/traits.md @@ -1,3 +1,13 @@ +--- +title: "Traits" +scope: "core" +kind: "reference" +category: "features" +aliases: + - "trait call" + - "trait impl" + - "assert-traits" +--- # Traits Calcit provides a lightweight trait system for attaching method implementations to struct/enum definitions (and using them from constructed instances and built-in types). diff --git a/docs/features/tuples.md b/docs/features/tuples.md index 176d6b45..9508e3a4 100644 --- a/docs/features/tuples.md +++ b/docs/features/tuples.md @@ -1,3 +1,13 @@ +--- +title: "Tuples" +scope: "core" +kind: "reference" +category: "features" +aliases: + - "tuple" + - "tagged tuple" + - "tuple match" +--- # Tuples Tuples in Calcit are tagged unions that can hold multiple values with a tag. They are used for representing structured data and are the foundation for records and enums. diff --git a/docs/installation.md b/docs/installation.md index c5979bfe..02f5acb4 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,3 +1,13 @@ +--- +title: "Installation" +scope: "core" +kind: "hub" +category: "installation" +aliases: + - "install calcit" + - "cargo install" + - "setup" +--- cargo install calcit # Installation diff --git a/docs/installation/ffi-bindings.md b/docs/installation/ffi-bindings.md index 545ebfc4..56b27823 100644 --- a/docs/installation/ffi-bindings.md +++ b/docs/installation/ffi-bindings.md @@ -1,3 +1,13 @@ +--- +title: "Rust bindings" +scope: "core" +kind: "reference" +category: "installation" +aliases: + - "ffi" + - "rust bindings" + - "native bindings" +--- # Rust bindings > API status: unstable. diff --git a/docs/installation/github-actions.md b/docs/installation/github-actions.md index aa354379..d742d1af 100644 --- a/docs/installation/github-actions.md +++ b/docs/installation/github-actions.md @@ -1,3 +1,13 @@ +--- +title: "GitHub Actions" +scope: "core" +kind: "reference" +category: "installation" +aliases: + - "github actions" + - "ci" + - "workflow" +--- # GitHub Actions To load Calcit `0.9.18` in a Ubuntu container: diff --git a/docs/installation/modules.md b/docs/installation/modules.md index a51782a3..9fb41976 100644 --- a/docs/installation/modules.md +++ b/docs/installation/modules.md @@ -1,3 +1,17 @@ +--- +title: "Modules directory" +scope: "core" +kind: "reference" +category: "installation" +aliases: + - "modules directory" + - "installed modules" + - "module docs" + - "caps" +entry_for: + - "caps install" + - "cr libs scan-md" +--- # Modules directory Packages are managed with `caps` command, which wraps `git clone` and `git pull` to manage modules. diff --git a/docs/intro.md b/docs/intro.md index ec1de448..f0479c53 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -1,3 +1,13 @@ +--- +title: "Introduction" +scope: "core" +kind: "hub" +category: "intro" +aliases: + - "introduction" + - "getting started" + - "language overview" +--- # Introduction Calcit is a scripting language that combines the power of Clojure-like functional programming with modern tooling and hot code swapping. diff --git a/docs/intro/from-clojure.md b/docs/intro/from-clojure.md index 30030499..bd60b720 100644 --- a/docs/intro/from-clojure.md +++ b/docs/intro/from-clojure.md @@ -1,3 +1,13 @@ +--- +title: "Features from Clojure" +scope: "core" +kind: "guide" +category: "intro" +aliases: + - "clojure dialect" + - "clojurescript dialect" + - "from clojure" +--- # Features from Clojure Calcit is mostly a ClojureScript dialect. So it should also be considered a Clojure dialect. diff --git a/docs/intro/indentation-syntax.md b/docs/intro/indentation-syntax.md index dd874b9e..15712fab 100644 --- a/docs/intro/indentation-syntax.md +++ b/docs/intro/indentation-syntax.md @@ -1,3 +1,20 @@ +--- +title: "Indentation-based Syntax" +scope: "core" +kind: "reference" +category: "syntax" +aliases: + - "indentation syntax" + - "indentation based syntax" + - "compact.cirru" + - "compact cirru" + - "bundle_calcit" + - "cirru edn" +entry_for: + - "bundle_calcit" + - "compact.cirru" +--- + ## Indentation Syntax in the MCP Server When using the MCP (Model Context Protocol) server, each documentation or code file is exposed as a key (the filename) with its content as the value. This means you can programmatically fetch, update, or analyze any file as a single value, making it easy for tools and agents to process Calcit code and documentation. Indentation-based syntax is preserved in the file content, so structure and meaning are maintained when accessed through the MCP server. diff --git a/docs/intro/overview.md b/docs/intro/overview.md index 05dad67c..1fbbfb26 100644 --- a/docs/intro/overview.md +++ b/docs/intro/overview.md @@ -1,3 +1,13 @@ +--- +title: "Overview" +scope: "core" +kind: "guide" +category: "intro" +aliases: + - "overview" + - "immutable data" + - "pattern matching" +--- # Overview - Immutable Data diff --git a/docs/quick-reference.md b/docs/quick-reference.md index 6b972817..6d932d50 100644 --- a/docs/quick-reference.md +++ b/docs/quick-reference.md @@ -1,3 +1,17 @@ +--- +title: "Quick Reference" +scope: "core" +kind: "reference" +category: "reference" +aliases: + - "cheatsheet" + - "cheat sheet" + - "quick commands" + - "quick reference" +entry_for: + - "cr --version" + - "cargo run --bin cr -- -v" +--- # Quick Reference This page provides a quick overview of key Calcit concepts and commands for rapid lookup. diff --git a/docs/run.md b/docs/run.md index a77246c1..21f3e0af 100644 --- a/docs/run.md +++ b/docs/run.md @@ -1,3 +1,18 @@ +--- +title: "Run Calcit" +scope: "core" +kind: "hub" +category: "run" +aliases: + - "run calcit" + - "watch mode" + - "entry file" + - "hot reload" +entry_for: + - "cr" + - "cr js" + - "cr ir" +--- # Run Calcit This page is a quick navigation hub. Detailed topics are split into dedicated chapters under `run/`. diff --git a/docs/run/agent-advanced.md b/docs/run/agent-advanced.md index d88c93d6..e407cba5 100644 --- a/docs/run/agent-advanced.md +++ b/docs/run/agent-advanced.md @@ -1,3 +1,18 @@ +--- +title: "Calcit 编程 Agent 指南" +scope: "core" +kind: "agent" +category: "run" +aliases: + - "agent advanced" + - "incremental edit" + - "batch rename" + - "agent playbook" +entry_for: + - "cr query search" + - "cr tree replace-leaf" + - "cr edit inc" +--- # Calcit 编程 Agent 指南 本文档为 AI Agent 提供 Calcit 项目的操作指南。 diff --git a/docs/run/bundle-mode.md b/docs/run/bundle-mode.md index 885ac88b..6a987fce 100644 --- a/docs/run/bundle-mode.md +++ b/docs/run/bundle-mode.md @@ -1,3 +1,13 @@ +--- +title: "Bundle Mode" +scope: "core" +kind: "guide" +category: "run" +aliases: + - "bundle mode" + - "single file deployment" + - "bundle" +--- # Bundle Mode Calcit programs are primarily designed to be written using the [calcit-editor](http://github.com/calcit-lang/editor), a structural editor. diff --git a/docs/run/cli-options.md b/docs/run/cli-options.md index ad7cd761..57d29a66 100644 --- a/docs/run/cli-options.md +++ b/docs/run/cli-options.md @@ -1,3 +1,24 @@ +--- +title: "CLI Options" +scope: "core" +kind: "reference" +category: "run" +aliases: + - "watch mode" + - "watch" + - "once mode" + - "check-only" + - "reload-fn" + - "reload fn" + - "watch-dir" +entry_for: + - "cr -w" + - "cr js -w" + - "cr ir -w" + - "cr --help" + - "cr --reload-fn" +--- + # CLI Options ```bash diff --git a/docs/run/docs-libs.md b/docs/run/docs-libs.md index c511090e..b1b5d2bf 100644 --- a/docs/run/docs-libs.md +++ b/docs/run/docs-libs.md @@ -1,3 +1,21 @@ +--- +title: "Documentation & Libraries" +scope: "core" +kind: "reference" +category: "run" +aliases: + - "docs" + - "libs" + - "read-lines" + - "scan-md" + - "docs read" +entry_for: + - "cr docs search" + - "cr docs read" + - "cr docs read-lines" + - "cr libs scan-md" +--- + # Documentation & Libraries Calcit includes built-in commands to navigate the language guidebook and discover community libraries. @@ -12,9 +30,16 @@ The `docs` subcommand allows you to read the language guidebook (like this one) # List all chapters in the guidebook cr docs list +# Read the local Agent guide (frontmatter, if present, is hidden automatically) +cr docs agents + # Read a specific file (fuzzy matching supported) cr docs read run.md +# Read by title/alias instead of exact filename +cr docs read target-replace +cr docs read "CLI Code Editing" + # List headings in a file (best first step before narrowing) cr docs read run.md @@ -23,6 +48,9 @@ cr docs read run.md quick start # Search for keywords across all chapters cr docs search "polymorphism" + +# Search installed module docs only +cr docs search render --module respo.calcit ``` ### Advanced Navigation (`read`) @@ -44,6 +72,12 @@ Use `read-lines` for large files where you need a specific range: ```bash # Read 50 lines starting from line 100 of common-patterns.md cr docs read-lines common-patterns.md --start 100 --lines 50 + +# Resolve by alias/title first, then read a specific range +cr docs read-lines target-replace --start 48 --lines 8 + +# Read a module document by title/alias with the same resolver +cr docs read-lines Respo-Agent --module respo.calcit --start 1 --lines 8 ``` ## Fast Navigation Patterns @@ -62,6 +96,25 @@ cr docs search trait cr docs read traits.md ``` +### Pattern 3: Search by documentation scope + +```bash +# Search only the built-in guidebook +cr docs search hot-swapping --scope core + +# Search installed modules +cr docs search clear-cache --scope modules + +# Search one installed module directly +cr docs search defstyle --module respo.calcit + +# Search module Agent/docs together and let ranking pick the better hit +cr docs search render --module respo.calcit + +# Read one module document directly with the same resolver +cr docs read Respo-Agent --module respo.calcit +``` + ## Library Discovery (`libs`) The `libs` subcommand helps you find and understand Calcit modules. diff --git a/docs/run/edit-tree.md b/docs/run/edit-tree.md index ef599762..9dfc0143 100644 --- a/docs/run/edit-tree.md +++ b/docs/run/edit-tree.md @@ -1,3 +1,19 @@ +--- +title: "CLI Code Editing (edit & tree)" +scope: "core" +kind: "reference" +category: "run" +aliases: + - "edit tree" + - "target-replace" + - "add-import" + - "tree replace" + - "tree rewrite" +entry_for: + - "cr tree target-replace" + - "cr edit add-import" + - "cr query search" +--- # CLI Code Editing (edit & tree) Calcit provides powerful CLI tools for modifying code directly without opening a text editor. These commands are optimized for both interactive use and automated scripts/agents. diff --git a/docs/run/entries.md b/docs/run/entries.md index a4007217..dd4d0e75 100644 --- a/docs/run/entries.md +++ b/docs/run/entries.md @@ -1,3 +1,14 @@ +--- +title: "Entries" +scope: "core" +kind: "reference" +category: "run" +aliases: + - "entry points" + - "init-fn" + - "reload-fn" + - "config entries" +--- # Entries By default Calcit reads `:init-fn` and `:reload-fn` inside `compact.cirru` configs. You may also specify functions, diff --git a/docs/run/eval.md b/docs/run/eval.md index 410e5207..40cfc2f0 100644 --- a/docs/run/eval.md +++ b/docs/run/eval.md @@ -1,3 +1,22 @@ +--- +title: "Run in Eval mode" +scope: "core" +kind: "reference" +category: "run" +aliases: + - "eval" + - "snippet" + - "repl" + - "type check" + - "eval dep" + - "eval module" + - "cr eval" +entry_for: + - "cr eval" + - "cr eval --dep" + - "cr eval --check-only" +--- + # Run in Eval mode Use `eval` command to evaluate code snippets from CLI: diff --git a/docs/run/hot-swapping.md b/docs/run/hot-swapping.md index 99ccf791..2189da5f 100644 --- a/docs/run/hot-swapping.md +++ b/docs/run/hot-swapping.md @@ -1,3 +1,16 @@ +--- +title: "Hot Swapping" +scope: "core" +kind: "guide" +category: "run" +aliases: + - "hot reload" + - "hot swapping" + - "compact-inc" + - "incremental compile" +entry_for: + - "cr edit inc" +--- # Hot Swapping Since there are two platforms for running Calcit, soutions for hot swapping are implemented differently. diff --git a/docs/run/load-deps.md b/docs/run/load-deps.md index 27ee8bae..b5d7e83c 100644 --- a/docs/run/load-deps.md +++ b/docs/run/load-deps.md @@ -1,3 +1,13 @@ +--- +title: "Load Dependencies" +scope: "core" +kind: "reference" +category: "run" +aliases: + - "load dependencies" + - "deps.cirru" + - "caps" +--- # Load Dependencies `caps` command is used for downloading dependencies declared in `deps.cirru`. The name "caps" stands for "Calcit Dependencies". diff --git a/docs/run/query.md b/docs/run/query.md index b1e63c91..45ecb50d 100644 --- a/docs/run/query.md +++ b/docs/run/query.md @@ -1,3 +1,25 @@ +--- +title: "Querying Definitions" +scope: "core" +kind: "reference" +category: "run" +aliases: + - "query defs" + - "query ns" + - "query def" + - "usages" + - "find symbol" + - "search-expr" + - "search expr" +entry_for: + - "cr query ns" + - "cr query defs" + - "cr query def" + - "cr query find" + - "cr query usages" + - "cr query search-expr" +--- + # Querying Definitions Calcit provides a powerful `query` subcommand to inspect code, find definitions, and analyze usages directly from the command line. diff --git a/docs/run/upgrade.md b/docs/run/upgrade.md index 2f09250a..6cb286ae 100644 --- a/docs/run/upgrade.md +++ b/docs/run/upgrade.md @@ -1,3 +1,14 @@ +--- +title: "Calcit 项目升级手册(Respo / Lilac)" +scope: "core" +kind: "guide" +category: "run" +aliases: + - "upgrade" + - "dependency migration" + - "respo upgrade" + - "lilac upgrade" +--- # Calcit 项目升级手册(Respo / Lilac) 本手册只关注**项目升级流程**,不展开开发实现细节。 diff --git a/docs/structural-editor.md b/docs/structural-editor.md index 79d10cf4..061a1758 100644 --- a/docs/structural-editor.md +++ b/docs/structural-editor.md @@ -1,3 +1,14 @@ +--- +title: "Structural Editor" +scope: "core" +kind: "reference" +category: "tools" +aliases: + - "calcit editor" + - "tree editor" + - "deprecated editor" + - "structural editing" +--- # Structural Editor > **Deprecated:** As Calcit shifts toward LLM-generated code workflows, command-line operations and type annotations have become more important. The structural editor approach is no longer recommended. Agent interfaces are preferred over direct user interaction. diff --git a/editing-history/2026-0326-2013-docs-frontmatter-and-module-search.md b/editing-history/2026-0326-2013-docs-frontmatter-and-module-search.md new file mode 100644 index 00000000..5e4b2eed --- /dev/null +++ b/editing-history/2026-0326-2013-docs-frontmatter-and-module-search.md @@ -0,0 +1,58 @@ +# Docs frontmatter 与模块检索整理 + +## 变更概述 + +- 为 `cr docs search/read/read-lines` 增加 `scope/module` 维度,支持核心文档、模块文档和跨域检索。 +- 为 core docs 建立统一的 frontmatter 规范,并补充 `title/scope/kind/category/aliases/entry_for` 元数据。 +- 将 `docs` 检索逻辑从纯文件名匹配升级为结合 frontmatter、路径、标题与条目词的综合解析与排序。 +- 给文档系统补充规范页与验证页,方便后续扩展 `cr docs` 时保持行为稳定。 +- 调整 Cirru 校验错误提示,让“其实是字符串”的场景给出更直接的修复建议。 + +## 关键实现 + +- `src/cli_args.rs` + - 为 `docs search`、`docs read`、`docs read-lines` 增加 `--scope`、`--module` 参数。 + +- `src/bin/cli_handlers/command_echo.rs` + - 同步回显新增 docs 参数,确保工具模式下能看到完整检索上下文。 + +- `src/bin/cli_handlers/docs.rs` + - 引入 `GuideDocFrontmatter`、`GuideDocScope`、`DocsSearchScope`。 + - 支持解析 Markdown frontmatter,并在加载时校验 `category`。 + - 支持从 core docs 与 `~/.config/calcit/modules//docs/` 同时加载文档。 + - 统一 `search/read/read-lines` 的查询解析逻辑,允许通过文件名、路径片段、标题、`aliases`、`entry_for` 命中目标。 + - 增加更偏向 guide/reference 页的排序策略,减少 spec/索引页抢占结果。 + - 抽出 `match_score`、`accumulate_match_score`、`parse_guide_doc` 等辅助函数,压缩主文件重复逻辑。 + +- `src/bin/cli_handlers/docs_tests.rs` + - 将 docs 相关测试从 `docs.rs` 拆出,单独维护解析、排序、模块加载与校验场景。 + +- `docs/docs-indexing.md` + - 固化 frontmatter 字段、分类注册表、scope 布局与 authoring 约束。 + +- `docs/docs-validation.md` + - 记录 `cr docs` 的可执行回归命令,覆盖 `search/read/read-lines` 与 module scope。 + +- `src/bin/cli_handlers/cirru_validator.rs` + - 当 token 更像“应写成字符串的文本”时,提示使用 `|text` 或 `"|text with spaces"`,减少样式值与括号文本误写的排查成本。 + +## 文档整理原则 + +- `category` 只允许稳定注册值,避免搜索元数据失控膨胀。 +- `aliases` 主要承接用户会直接输入的别名或术语,`entry_for` 主要承接命令/API/任务入口。 +- `Agents.md` 与普通 docs 页复用同一套 frontmatter 机制,但渲染时默认隐藏元数据正文。 +- 模块领域知识保留在模块 docs 中,Calcit core 只提供通用索引与加载能力,不内置 Respo 特殊语义。 + +## 验证 + +- `cargo fmt` +- `cargo test docs --bin cr` +- 手动验证: + - `cargo run --bin cr -- docs search target-replace` + - `cargo run --bin cr -- docs search 'cr eval'` + - `cargo run --bin cr -- docs read polymorphism.md` + +## 后续经验 + +- docs 元数据一旦进入 CLI 行为,就应该配套规范页与验证页,否则后续加字段很容易出现“文档能写、检索却不稳定”的分叉。 +- 模块文档检索要尽量和 core docs 走同一套 resolver,这样使用者不需要记两套命令心智模型。 \ No newline at end of file diff --git a/src/bin/cli_handlers/cirru_validator.rs b/src/bin/cli_handlers/cirru_validator.rs index 44b53dff..01241c23 100644 --- a/src/bin/cli_handlers/cirru_validator.rs +++ b/src/bin/cli_handlers/cirru_validator.rs @@ -51,7 +51,7 @@ fn validate_leaf(s: &Arc, path: &[usize], in_comment: bool) -> Result<(), S return Err(format!( "Invalid tag at path [{}]: Tags cannot contain spaces\n\ Found: {:?}\n\ - Hint: Tags like :tag should be single tokens without spaces", + Hint: Tags like :tag should be single tokens without spaces; if this is literal text, use a string such as |text or \"|text with spaces\"", format_path(path), text )); @@ -91,7 +91,7 @@ fn validate_leaf(s: &Arc, path: &[usize], in_comment: bool) -> Result<(), S return Err(format!( "Invalid leaf node at path [{}]: Contains parentheses which are structural characters\n\ Found: {:?}\n\ - Hint: Parentheses ( ) are only for list structure, not leaf content", + Hint: Parentheses ( ) are only for list structure, not leaf content; if you need literal parentheses in text, wrap the value as a string such as |text(with-parens) or \"|text with (parens)\"", format_path(path), text )); @@ -111,7 +111,7 @@ fn validate_leaf(s: &Arc, path: &[usize], in_comment: bool) -> Result<(), S return Err(format!( "Invalid number format at path [{}]: Starts with digit but cannot be parsed as number\n\ Found: {:?}\n\ - Hint: Valid formats include: 123, -456, 3.14, 1e10, 0x1F, 0b1010, 0o77", + Hint: Valid formats include: 123, -456, 3.14, 1e10, 0x1F, 0b1010, 0o77; if this is a literal token rather than a number, wrap it as a string", format_path(path), text )); @@ -128,7 +128,7 @@ fn validate_leaf(s: &Arc, path: &[usize], in_comment: bool) -> Result<(), S return Err(format!( "Invalid number format at path [{}]: Starts with {}{} but cannot be parsed as number\n\ Found: {:?}\n\ - Hint: Valid formats include: +123, -456, +3.14, -1e10", + Hint: Valid formats include: +123, -456, +3.14, -1e10; if this is not meant to be a number, write it as a string instead of a bare leaf", format_path(path), first_char, second_char, @@ -145,7 +145,7 @@ fn validate_leaf(s: &Arc, path: &[usize], in_comment: bool) -> Result<(), S return Err(format!( "Suspicious leaf node at path [{}]: Contains spaces but is not a string\n\ Found: {:?}\n\ - Hint: If this is meant to be a string, prefix with | or \"\n\ + Hint: If this is meant to be a string, prefix with | for simple text, or use \"|...\" for one-line text with spaces/special characters\n\ If it's multiple tokens, it should be a list (separate expressions)", format_path(path), text @@ -313,6 +313,31 @@ mod tests { assert!(result.unwrap_err().contains("cannot be parsed as number")); } + #[test] + fn test_invalid_non_numeric_token_starting_with_digits_has_string_hint() { + let result = validate_cirru_syntax(&leaf("100vh")); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("literal token rather than a number")); + } + + #[test] + fn test_parentheses_hint_mentions_literal_parentheses_strings() { + let result = validate_cirru_syntax(&leaf("text(with-parens)")); + assert!(result.is_err()); + let message = result.unwrap_err(); + assert!(message.contains("literal parentheses in text")); + assert!(message.contains("|text(with-parens)")); + } + + #[test] + fn test_parentheses_hint_mentions_string_wrapping() { + let result = validate_cirru_syntax(&leaf("text with (parens)")); + assert!(result.is_err()); + let message = result.unwrap_err(); + assert!(message.contains("wrap the value as a string") || message.contains("string such as")); + assert!(message.contains("\"|text with (parens)\"")); + } + #[test] fn test_valid_symbols() { assert!(validate_cirru_syntax(&leaf("defn")).is_ok()); diff --git a/src/bin/cli_handlers/command_echo.rs b/src/bin/cli_handlers/command_echo.rs index 54ccd634..9c1adc7a 100644 --- a/src/bin/cli_handlers/command_echo.rs +++ b/src/bin/cli_handlers/command_echo.rs @@ -159,7 +159,9 @@ fn push_docs(tokens: &mut Vec, cmd: &DocsCommand) { tokens, pos "keyword" => &opts.keyword, value "context" => opts.context; default "5", - opt "filename" => opts.filename.as_deref(); default "none" + opt "filename" => opts.filename.as_deref(); default "none", + opt "scope" => opts.scope.as_deref(); default "none", + opt "module" => opts.module.as_deref(); default "none" ), DocsSubcommand::Read(opts) => echo_items!( tokens, @@ -167,7 +169,9 @@ fn push_docs(tokens: &mut Vec, cmd: &DocsCommand) { list "heading" => &opts.headings, switch "no-subheadings" => opts.no_subheadings, switch "full" => opts.full, - switch "with-lines" => opts.with_lines + switch "with-lines" => opts.with_lines, + opt "scope" => opts.scope.as_deref(); default "none", + opt "module" => opts.module.as_deref(); default "none" ), DocsSubcommand::Agents(opts) => echo_items!( tokens, @@ -181,7 +185,9 @@ fn push_docs(tokens: &mut Vec, cmd: &DocsCommand) { tokens, pos "filename" => &opts.filename, value "start" => opts.start; default "0", - value "lines" => opts.lines; default "80" + value "lines" => opts.lines; default "80", + opt "scope" => opts.scope.as_deref(); default "none", + opt "module" => opts.module.as_deref(); default "none" ), DocsSubcommand::List(_) => {} DocsSubcommand::CheckMd(opts) => { diff --git a/src/bin/cli_handlers/docs.rs b/src/bin/cli_handlers/docs.rs index 8655d0b6..eaf29e5c 100644 --- a/src/bin/cli_handlers/docs.rs +++ b/src/bin/cli_handlers/docs.rs @@ -5,7 +5,7 @@ use calcit::cli_args::{DocsCommand, DocsSubcommand}; use colored::Colorize; use std::cell::RefCell; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -23,11 +23,97 @@ use calcit::util; use super::markdown_read::{RenderMarkdownOptions, render_markdown_sections}; use super::tips::command_guidance_enabled; +const VALID_DOC_CATEGORIES: &[&str] = &[ + "run", + "features", + "installation", + "data", + "intro", + "syntax", + "tools", + "ecosystem", + "reference", + "docs", +]; + #[derive(Debug, Clone)] pub struct GuideDoc { filename: String, path: String, content: String, + scope: GuideDocScope, + frontmatter: GuideDocFrontmatter, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +struct GuideDocFrontmatter { + title: Option, + scope: Option, + kind: Option, + category: Option, + aliases: Vec, + entry_for: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum GuideDocScope { + Core, + Module(String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DocsSearchScope { + Core, + Modules, + All, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct SearchResult { + doc_index: usize, + merged_ranges: Vec<(usize, usize)>, + score: usize, +} + +impl GuideDoc { + fn display_title(&self) -> String { + let base_title = self.frontmatter.title.as_deref().unwrap_or(&self.filename); + match &self.scope { + GuideDocScope::Core => base_title.to_string(), + GuideDocScope::Module(module) => format!("{} [module:{}]", base_title, module), + } + } +} + +fn match_score(value: &str, query_lower: &str, exact_score: usize, contains_score: usize) -> usize { + let value_lower = value.to_lowercase(); + if value_lower == query_lower { + exact_score + } else if value_lower.contains(query_lower) { + contains_score + } else { + 0 + } +} + +fn accumulate_match_score(total: &mut usize, matched: &mut bool, value: &str, query_lower: &str, exact: usize, contains: usize) { + let score = match_score(value, query_lower, exact, contains); + if score > 0 { + *total += score; + *matched = true; + } +} + +fn parse_guide_doc(filename: String, path: String, raw_content: &str, scope: GuideDocScope) -> Result { + let (frontmatter, content) = parse_doc_frontmatter(raw_content); + validate_doc_frontmatter(&path, &frontmatter)?; + Ok(GuideDoc { + filename, + path, + content, + scope, + frontmatter, + }) } struct ReadRenderOptions<'a> { @@ -44,10 +130,30 @@ struct ReadRenderOptions<'a> { pub fn handle_docs_command(cmd: &DocsCommand) -> Result<(), String> { match &cmd.subcommand { - DocsSubcommand::Search(opts) => handle_search(&opts.keyword, opts.context, opts.filename.as_deref()), - DocsSubcommand::Read(opts) => handle_read(&opts.filename, &opts.headings, !opts.no_subheadings, opts.full, opts.with_lines), + DocsSubcommand::Search(opts) => handle_search( + &opts.keyword, + opts.context, + opts.filename.as_deref(), + opts.scope.as_deref(), + opts.module.as_deref(), + ), + DocsSubcommand::Read(opts) => handle_read( + &opts.filename, + &opts.headings, + !opts.no_subheadings, + opts.full, + opts.with_lines, + opts.scope.as_deref(), + opts.module.as_deref(), + ), DocsSubcommand::Agents(opts) => handle_agents(&opts.headings, !opts.no_subheadings, opts.full, opts.with_lines, opts.refresh), - DocsSubcommand::ReadLines(opts) => handle_read_lines(&opts.filename, opts.start, opts.lines), + DocsSubcommand::ReadLines(opts) => handle_read_lines( + &opts.filename, + opts.start, + opts.lines, + opts.scope.as_deref(), + opts.module.as_deref(), + ), DocsSubcommand::List(_) => handle_list(), DocsSubcommand::CheckMd(opts) => handle_check_md(&opts.file, &opts.entry, &opts.dep), } @@ -77,7 +183,7 @@ fn needs_agents_refresh(cache_path: &Path) -> bool { let now = SystemTime::now(); match now.duration_since(modified) { - Ok(age) => age > Duration::from_secs(1 * 60 * 60), // 1 hour + Ok(age) => age > Duration::from_secs(60 * 60), Err(_) => true, } } @@ -130,25 +236,51 @@ fn handle_read_content(options: ReadRenderOptions<'_>) -> Result<(), String> { ) } -fn find_doc_by_filename<'a>(guide_docs: &'a HashMap, filename: &str) -> Result<&'a GuideDoc, String> { +fn score_doc_query(doc: &GuideDoc, query_lower: &str) -> usize { + let mut score: usize = 0; + score += match_score(&doc.filename, query_lower, 320, 180); + score += match_score(&doc.path, query_lower, 280, 140); + + if let Some(title) = &doc.frontmatter.title { + score += match_score(title, query_lower, 260, 150); + } + + for alias in &doc.frontmatter.aliases { + score += match_score(alias, query_lower, 220, 120); + } + + for entry in &doc.frontmatter.entry_for { + score += match_score(entry, query_lower, 180, 96); + } + + score.saturating_add_signed(score_doc_shape(doc)) +} + +fn find_doc_by_query<'a>(guide_docs: &'a [GuideDoc], query: &str) -> Result<&'a GuideDoc, String> { + let query_lower = query.to_lowercase(); guide_docs - .values() - .find(|d| d.filename == filename || d.filename.contains(filename)) - .ok_or_else(|| format!("Document '{filename}' not found. Use 'cr docs list' to see available documents.")) + .iter() + .filter_map(|doc| { + let score = score_doc_query(doc, &query_lower); + (score > 0).then_some((doc, score)) + }) + .max_by(|(left_doc, left_score), (right_doc, right_score)| { + left_score.cmp(right_score).then_with(|| right_doc.path.cmp(&left_doc.path)) + }) + .map(|(doc, _)| doc) + .ok_or_else(|| { + format!("Document '{query}' not found. Use 'cr docs list' or 'cr docs search {query}' to locate matching documents.") + }) } -fn get_guidebook_dir() -> Result { +fn get_guidebook_dir() -> Result { let home_dir = std::env::var("HOME").map_err(|_| "Unable to get HOME environment variable")?; let docs_dir = Path::new(&home_dir).join(".config/calcit/docs"); if !docs_dir.exists() { let calcit_repo_dir = Path::new(&home_dir).join(".config/calcit/calcit"); return Err(format!( - "Guidebook documentation directory not found: {docs_dir:?}\n\n\ - Download the Calcit docs repo with git, then create a symlink for the docs directory:\n\ - mkdir -p ~/.config/calcit\n\ - git clone https://github.com/calcit-lang/calcit.git {}\n\ - ln -s {}/docs {}", + "Guidebook documentation directory not found: {docs_dir:?}\n\nDownload the Calcit docs repo with git, then create a symlink for the docs directory:\nmkdir -p ~/.config/calcit\ngit clone https://github.com/calcit-lang/calcit.git {}\nln -s {}/docs {}", calcit_repo_dir.display(), calcit_repo_dir.display(), docs_dir.display() @@ -158,96 +290,393 @@ fn get_guidebook_dir() -> Result { Ok(docs_dir) } -fn load_guidebook_docs() -> Result, String> { - let docs_dir = get_guidebook_dir()?; - let mut guide_docs = HashMap::new(); - - fn visit_dir(dir: &Path, base_dir: &Path, docs: &mut HashMap) -> Result<(), String> { - for entry in fs::read_dir(dir).map_err(|e| format!("Failed to read directory: {e}"))? { - let entry = entry.map_err(|e| format!("Failed to read directory entry: {e}"))?; - let path = entry.path(); - - if path.is_dir() { - visit_dir(&path, base_dir, docs)?; - } else if path.extension().and_then(|s| s.to_str()) == Some("md") { - let content = fs::read_to_string(&path).map_err(|e| format!("Failed to read file {path:?}: {e}"))?; - let relative_path = path.strip_prefix(base_dir).map_err(|_| "Unable to get relative path")?; - let filename = path.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(); - - docs.insert( - filename.clone(), - GuideDoc { - filename, - path: relative_path.to_string_lossy().to_string(), - content, - }, - ); +fn trim_frontmatter_value(raw: &str) -> String { + let value = raw.trim(); + if value.len() >= 2 { + let first = value.chars().next().unwrap_or_default(); + let last = value.chars().last().unwrap_or_default(); + if (first == '"' && last == '"') || (first == '\'' && last == '\'') { + return value[1..value.len() - 1].to_string(); + } + } + value.to_string() +} + +fn validate_doc_frontmatter(path: &str, frontmatter: &GuideDocFrontmatter) -> Result<(), String> { + if let Some(category) = frontmatter.category.as_deref() { + if !VALID_DOC_CATEGORIES.contains(&category) { + return Err(format!( + "Invalid frontmatter category '{category}' in {path}. Use one of: {}. See docs/docs-indexing.md.", + VALID_DOC_CATEGORIES.join(", ") + )); + } + } + + Ok(()) +} + +fn parse_doc_frontmatter(raw: &str) -> (GuideDocFrontmatter, String) { + if !raw.starts_with("---\n") { + return (GuideDocFrontmatter::default(), raw.to_string()); + } + + let mut frontmatter = GuideDocFrontmatter::default(); + let mut body_lines = Vec::new(); + let mut in_frontmatter = true; + let mut saw_closing = false; + let mut active_list: Option<&str> = None; + + for (index, line) in raw.lines().enumerate() { + if index == 0 { + continue; + } + + if in_frontmatter { + if line.trim() == "---" { + in_frontmatter = false; + saw_closing = true; + active_list = None; + continue; } + + let trimmed = line.trim(); + if let Some(item) = trimmed.strip_prefix("- ") { + let value = trim_frontmatter_value(item); + match active_list { + Some("aliases") => frontmatter.aliases.push(value), + Some("entry_for") => frontmatter.entry_for.push(value), + _ => {} + } + continue; + } + + if let Some((key, value)) = trimmed.split_once(':') { + let key = key.trim(); + let value = value.trim(); + active_list = None; + match key { + "title" => frontmatter.title = Some(trim_frontmatter_value(value)), + "scope" => frontmatter.scope = Some(trim_frontmatter_value(value)), + "kind" => frontmatter.kind = Some(trim_frontmatter_value(value)), + "category" => frontmatter.category = Some(trim_frontmatter_value(value)), + "aliases" if value.is_empty() => active_list = Some("aliases"), + "entry_for" if value.is_empty() => active_list = Some("entry_for"), + "aliases" => frontmatter.aliases.push(trim_frontmatter_value(value)), + "entry_for" => frontmatter.entry_for.push(trim_frontmatter_value(value)), + _ => {} + } + continue; + } + + active_list = None; + } else { + body_lines.push(line); } - Ok(()) } - visit_dir(&docs_dir, &docs_dir, &mut guide_docs)?; + if !saw_closing { + return (GuideDocFrontmatter::default(), raw.to_string()); + } + + let body = body_lines.join("\n").trim_start_matches('\n').to_string(); + (frontmatter, body) +} + +fn visit_markdown_dir(dir: &Path, base_dir: &Path, docs: &mut Vec, scope: &GuideDocScope) -> Result<(), String> { + for entry in fs::read_dir(dir).map_err(|e| format!("Failed to read directory: {e}"))? { + let entry = entry.map_err(|e| format!("Failed to read directory entry: {e}"))?; + let path = entry.path(); + + if path.is_dir() { + visit_markdown_dir(&path, base_dir, docs, scope)?; + } else if path.extension().and_then(|s| s.to_str()) == Some("md") { + let raw_content = fs::read_to_string(&path).map_err(|e| format!("Failed to read file {path:?}: {e}"))?; + let relative_path = path.strip_prefix(base_dir).map_err(|_| "Unable to get relative path")?; + let filename = path.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(); + let relative_path_str = relative_path.to_string_lossy().to_string(); + docs.push(parse_guide_doc(filename, relative_path_str, &raw_content, scope.clone())?); + } + } + Ok(()) +} + +fn load_guidebook_docs() -> Result, String> { + let docs_dir = get_guidebook_dir()?; + let mut guide_docs = Vec::new(); + visit_markdown_dir(&docs_dir, &docs_dir, &mut guide_docs, &GuideDocScope::Core)?; Ok(guide_docs) } -fn handle_search(keyword: &str, context_lines: usize, filename_filter: Option<&str>) -> Result<(), String> { - let guide_docs = load_guidebook_docs()?; +fn load_module_docs_from_dir(modules_dir: &Path, module_filter: Option<&str>) -> Result, String> { + let mut docs = Vec::new(); + let mut seen_modules = HashSet::new(); + + for entry in fs::read_dir(modules_dir).map_err(|e| format!("Failed to read modules directory {modules_dir:?}: {e}"))? { + let entry = entry.map_err(|e| format!("Failed to read modules directory entry: {e}"))?; + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let module_name = match path.file_name().and_then(|s| s.to_str()) { + Some(name) if !name.starts_with('.') => name.to_string(), + _ => continue, + }; + + if let Some(filter) = module_filter { + if module_name != filter { + continue; + } + } + + seen_modules.insert(module_name.clone()); + let scope = GuideDocScope::Module(module_name.clone()); + + let agents_path = path.join("Agents.md"); + if agents_path.exists() { + let raw_content = fs::read_to_string(&agents_path).map_err(|e| format!("Failed to read file {agents_path:?}: {e}"))?; + let agents_doc_path = format!("{}/Agents.md", module_name); + docs.push(parse_guide_doc( + "Agents.md".to_string(), + agents_doc_path, + &raw_content, + scope.clone(), + )?); + } + + let docs_path = path.join("docs"); + if docs_path.exists() { + visit_markdown_dir(&docs_path, &path, &mut docs, &scope)?; + } + } + + if let Some(filter) = module_filter { + if !seen_modules.contains(filter) { + return Err(format!( + "Module '{filter}' not found under {:?}. Use 'cr libs scan-md ' or inspect ~/.config/calcit/modules/.", + modules_dir + )); + } + } + + Ok(docs) +} + +fn load_module_docs(module_filter: Option<&str>) -> Result, String> { + let modules_dir = module_folder()?; + load_module_docs_from_dir(&modules_dir, module_filter) +} + +fn resolve_search_scope(scope: Option<&str>, module_filter: Option<&str>) -> Result { + match (scope, module_filter) { + (None, Some(_)) => Ok(DocsSearchScope::Modules), + (None, None) => Ok(DocsSearchScope::Core), + (Some(value), _) => match value { + "core" => Ok(DocsSearchScope::Core), + "modules" => Ok(DocsSearchScope::Modules), + "all" => Ok(DocsSearchScope::All), + other => Err(format!("Invalid docs search scope '{other}'. Use one of: core, modules, all.")), + }, + } +} + +fn collect_search_docs(scope: DocsSearchScope, module_filter: Option<&str>) -> Result, String> { + let mut docs = Vec::new(); + match scope { + DocsSearchScope::Core => docs.extend(load_guidebook_docs()?), + DocsSearchScope::Modules => docs.extend(load_module_docs(module_filter)?), + DocsSearchScope::All => { + docs.extend(load_guidebook_docs()?); + docs.extend(load_module_docs(module_filter)?); + } + } + docs.sort_by(|a, b| a.path.cmp(&b.path)); + Ok(docs) +} + +fn score_metadata_hit(doc: &GuideDoc, keyword_lower: &str) -> (usize, bool) { + let mut score = 0; + let mut matched = false; + + if let Some(title) = &doc.frontmatter.title { + accumulate_match_score(&mut score, &mut matched, title, keyword_lower, 240, 160); + } + + for alias in &doc.frontmatter.aliases { + accumulate_match_score(&mut score, &mut matched, alias, keyword_lower, 220, 120); + } + + for entry in &doc.frontmatter.entry_for { + accumulate_match_score(&mut score, &mut matched, entry, keyword_lower, 180, 90); + } + + (score, matched) +} + +fn score_doc_shape(doc: &GuideDoc) -> isize { + let kind_score = match doc.frontmatter.kind.as_deref() { + Some("reference") => 18, + Some("guide") => 14, + Some("hub") => 8, + Some("agent") => 4, + Some("spec") => -6, + Some(_) | None => 0, + }; + + let category_score = match doc.frontmatter.category.as_deref() { + Some("run") | Some("features") | Some("installation") | Some("data") | Some("intro") => 4, + Some("docs") => -2, + Some(_) | None => 0, + }; + + let scope_score = match (&doc.scope, doc.frontmatter.scope.as_deref()) { + (GuideDocScope::Module(_), Some("module")) => 4, + (GuideDocScope::Core, Some("core")) => 2, + _ => 0, + }; + + kind_score + category_score + scope_score +} + +fn score_search_hit(doc: &GuideDoc, lines: &[&str], keyword_lower: &str, matching_lines: &[usize]) -> usize { + let (mut score, _) = score_metadata_hit(doc, keyword_lower); + score = score.saturating_add_signed(score_doc_shape(doc)); + score += match_score(&doc.filename, keyword_lower, 140, 80); + score += match_score(&doc.path, keyword_lower, 120, 36); + + for line_index in matching_lines { + let line = lines[*line_index]; + let lower = line.to_lowercase(); + let trimmed = line.trim_start(); + let trimmed_lower = lower.trim_start(); + + if trimmed.starts_with('#') { + score += 48; + } else { + score += 10; + } + + if trimmed_lower == keyword_lower { + score += 36; + } + + if trimmed_lower.starts_with(keyword_lower) { + score += 18; + } + + if lower.contains(&format!("`{keyword_lower}`")) { + score += 14; + } - let mut found_any = false; + if *line_index < 40 { + score += 6; + } + } - for doc in guide_docs.values() { - // Skip SUMMARY files + score +} + +fn merge_ranges(mut matching_ranges: Vec<(usize, usize)>) -> Vec<(usize, usize)> { + matching_ranges.sort_by_key(|range| range.0); + let mut merged_ranges: Vec<(usize, usize)> = Vec::new(); + + for (start, end) in matching_ranges { + if let Some(last) = merged_ranges.last_mut() { + if start <= last.1 { + last.1 = last.1.max(end); + continue; + } + } + merged_ranges.push((start, end)); + } + + merged_ranges +} + +fn collect_search_results( + guide_docs: &[GuideDoc], + keyword_lower: &str, + context_lines: usize, + filename_filter: Option<&str>, +) -> Vec { + let mut results = Vec::new(); + + for (doc_index, doc) in guide_docs.iter().enumerate() { if doc.filename.to_uppercase().contains("SUMMARY") { continue; } - // Apply filename filter if provided if let Some(filter) = filename_filter { - if !doc.filename.contains(filter) { + if !doc.filename.contains(filter) && !doc.path.contains(filter) { continue; } } let lines: Vec<&str> = doc.content.lines().collect(); let mut matching_ranges: Vec<(usize, usize)> = Vec::new(); + let mut matching_lines: Vec = Vec::new(); + let (_, metadata_matched) = score_metadata_hit(doc, keyword_lower); - // Find all matching lines for (line_num, line) in lines.iter().enumerate() { - if line.contains(keyword) { + if line.to_lowercase().contains(keyword_lower) { let start = line_num.saturating_sub(context_lines); let end = (line_num + context_lines + 1).min(lines.len()); matching_ranges.push((start, end)); + matching_lines.push(line_num); } } - if matching_ranges.is_empty() { + if matching_ranges.is_empty() && !metadata_matched { continue; } - found_any = true; - - // Merge overlapping ranges - matching_ranges.sort_by_key(|r| r.0); - let mut merged_ranges: Vec<(usize, usize)> = Vec::new(); - for (start, end) in matching_ranges { - if let Some(last) = merged_ranges.last_mut() { - if start <= last.1 { - last.1 = last.1.max(end); - continue; - } + if matching_ranges.is_empty() { + let preview_end = lines.len().min(context_lines.saturating_mul(2).max(6)); + if preview_end > 0 { + matching_ranges.push((0, preview_end)); } - merged_ranges.push((start, end)); } - // Display matches - println!("{} ({})", doc.filename.cyan().bold(), doc.path.dimmed()); + results.push(SearchResult { + doc_index, + merged_ranges: merge_ranges(matching_ranges), + score: score_search_hit(doc, &lines, keyword_lower, &matching_lines), + }); + } + + results.sort_by(|a, b| { + b.score + .cmp(&a.score) + .then_with(|| guide_docs[a.doc_index].path.cmp(&guide_docs[b.doc_index].path)) + }); + + results +} + +fn handle_search( + keyword: &str, + context_lines: usize, + filename_filter: Option<&str>, + scope: Option<&str>, + module_filter: Option<&str>, +) -> Result<(), String> { + let resolved_scope = resolve_search_scope(scope, module_filter)?; + let guide_docs = collect_search_docs(resolved_scope, module_filter)?; + let keyword_lower = keyword.to_lowercase(); + + let results = collect_search_results(&guide_docs, &keyword_lower, context_lines, filename_filter); + + for result in &results { + let doc = &guide_docs[result.doc_index]; + let lines: Vec<&str> = doc.content.lines().collect(); + + println!("{} ({})", doc.display_title().cyan().bold(), doc.path.dimmed()); println!("{}", "-".repeat(60).dimmed()); - for (start, end) in merged_ranges { - for (idx, line) in lines[start..end].iter().enumerate() { - let line_num = start + idx + 1; - if line.contains(keyword) { + for (start, end) in &result.merged_ranges { + for (idx, line) in lines[*start..*end].iter().enumerate() { + let line_num = *start + idx + 1; + if line.to_lowercase().contains(&keyword_lower) { println!("{} {}", format!("{line_num:4}:").yellow(), line); } else { println!("{} {}", format!("{line_num:4}:").dimmed(), line.dimmed()); @@ -257,7 +686,7 @@ fn handle_search(keyword: &str, context_lines: usize, filename_filter: Option<&s } } - if !found_any { + if results.is_empty() { println!("{}", "No matching content found.".yellow()); } else if command_guidance_enabled() { println!( @@ -270,6 +699,12 @@ fn handle_search(keyword: &str, context_lines: usize, filename_filter: Option<&s " Use -f to filter by file (e.g., 'cr docs search -f syntax.md')".dimmed() ); } + if scope.is_none() { + println!( + "{}", + " Use --scope modules|all or --module to search installed module docs.".dimmed() + ); + } } Ok(()) @@ -291,6 +726,8 @@ fn handle_agents( } } let content = fs::read_to_string(&cache_path).map_err(|e| format!("Failed to read Agents cache {cache_path:?}: {e}"))?; + let (frontmatter, content) = parse_doc_frontmatter(&content); + validate_doc_frontmatter(&cache_path.to_string_lossy(), &frontmatter)?; let byte_len = content.len(); let line_len = content.lines().count(); @@ -304,7 +741,7 @@ fn handle_agents( let cache_display = cache_path.to_string_lossy().to_string(); handle_read_content(ReadRenderOptions { - display_title: "Agents.md", + display_title: frontmatter.title.as_deref().unwrap_or("Agents.md"), display_path: &cache_display, command_hint: "agents", no_match_error: "No heading matched in Agents.md. Run 'cr docs agents' to list available headings.", @@ -322,11 +759,14 @@ fn handle_read( include_subheadings: bool, full: bool, with_lines: bool, + scope: Option<&str>, + module_filter: Option<&str>, ) -> Result<(), String> { - let guide_docs = load_guidebook_docs()?; - let doc = find_doc_by_filename(&guide_docs, filename)?; + let resolved_scope = resolve_search_scope(scope, module_filter)?; + let guide_docs = collect_search_docs(resolved_scope, module_filter)?; + let doc = find_doc_by_query(&guide_docs, filename)?; let result = handle_read_content(ReadRenderOptions { - display_title: &doc.filename, + display_title: &doc.display_title(), display_path: &doc.path, command_hint: "read", no_match_error: "No heading matched in document.", @@ -347,25 +787,29 @@ fn handle_read( result } -fn handle_read_lines(filename: &str, start: usize, lines_to_read: usize) -> Result<(), String> { - let guide_docs = load_guidebook_docs()?; - - let doc = find_doc_by_filename(&guide_docs, filename)?; +fn handle_read_lines( + filename: &str, + start: usize, + lines_to_read: usize, + scope: Option<&str>, + module_filter: Option<&str>, +) -> Result<(), String> { + let resolved_scope = resolve_search_scope(scope, module_filter)?; + let guide_docs = collect_search_docs(resolved_scope, module_filter)?; + let doc = find_doc_by_query(&guide_docs, filename)?; let all_lines: Vec<&str> = doc.content.lines().collect(); let total_lines = all_lines.len(); let end = (start + lines_to_read).min(total_lines); - println!("{} ({})", doc.filename.cyan().bold(), doc.path.dimmed()); + println!("{} ({})", doc.display_title().cyan().bold(), doc.path.dimmed()); println!("{}", "=".repeat(60).dimmed()); - // Display lines with line numbers for (idx, line) in all_lines[start..end].iter().enumerate() { let line_num = start + idx + 1; println!("{} {}", format!("{line_num:4}:").dimmed(), line); } - // Show tips println!(); println!( "{}", @@ -397,7 +841,7 @@ fn handle_list() -> Result<(), String> { println!("{}", "Available Guidebook Documentation:".bold()); - let mut docs: Vec<&GuideDoc> = guide_docs.values().collect(); + let mut docs: Vec<&GuideDoc> = guide_docs.iter().collect(); docs.sort_by_key(|d| &d.path); for doc in &docs { @@ -427,6 +871,10 @@ fn handle_list() -> Result<(), String> { Ok(()) } +#[cfg(test)] +#[path = "docs_tests.rs"] +mod tests; + /// Check mode for a cirru code block in markdown. /// /// - `cirru` → Run: parse + preprocess + eval @@ -465,7 +913,7 @@ fn extract_cirru_blocks(content: &str) -> Vec<(usize, CirruCheckMode, String)> { if !in_block && !in_non_cirru_block && trimmed.starts_with("```") { if trimmed == "```cirru" { in_block = true; - block_start_line = idx + 1; // 1-based + block_start_line = idx + 1; block_mode = CirruCheckMode::Run; block_lines.clear(); } else if trimmed == "```cirru.no-run" { @@ -479,7 +927,6 @@ fn extract_cirru_blocks(content: &str) -> Vec<(usize, CirruCheckMode, String)> { block_mode = CirruCheckMode::NoCheck; block_lines.clear(); } else { - // other fenced block (```json, ```bash, etc.) — skip entirely in_non_cirru_block = true; } } else if (in_block || in_non_cirru_block) && trimmed == "```" { @@ -661,7 +1108,6 @@ fn handle_check_md(file_path: &str, entry: &str, deps: &[String]) -> Result<(), } for (line_num, mode, code) in &blocks { - // Show a brief preview of the code block let preview: String = code.lines().next().unwrap_or("").chars().take(60).collect(); let preview_suffix = if code.lines().count() > 1 || preview.len() >= 60 { "..." diff --git a/src/bin/cli_handlers/docs_tests.rs b/src/bin/cli_handlers/docs_tests.rs new file mode 100644 index 00000000..5a92da81 --- /dev/null +++ b/src/bin/cli_handlers/docs_tests.rs @@ -0,0 +1,433 @@ +use super::{ + DocsSearchScope, GuideDoc, GuideDocFrontmatter, GuideDocScope, collect_search_results, find_doc_by_query, load_module_docs_from_dir, + parse_doc_frontmatter, resolve_search_scope, score_doc_query, score_doc_shape, validate_doc_frontmatter, +}; +use std::fs; +use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn unique_temp_dir(label: &str) -> std::path::PathBuf { + let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + std::env::temp_dir().join(format!("calcit-docs-{label}-{nanos}")) +} + +fn write_file(path: &Path, content: &str) { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(path, content).unwrap(); +} + +#[test] +fn resolve_scope_defaults_to_core_without_module() { + assert_eq!(resolve_search_scope(None, None).unwrap(), DocsSearchScope::Core); +} + +#[test] +fn resolve_scope_defaults_to_modules_with_module_filter() { + assert_eq!(resolve_search_scope(None, Some("respo.calcit")).unwrap(), DocsSearchScope::Modules); +} + +#[test] +fn resolve_scope_accepts_all_variants() { + assert_eq!(resolve_search_scope(Some("core"), None).unwrap(), DocsSearchScope::Core); + assert_eq!(resolve_search_scope(Some("modules"), None).unwrap(), DocsSearchScope::Modules); + assert_eq!(resolve_search_scope(Some("all"), None).unwrap(), DocsSearchScope::All); +} + +#[test] +fn load_module_docs_from_dir_reads_agents_and_docs() { + let root = unique_temp_dir("modules-read"); + let modules_dir = root.join("modules"); + write_file(&modules_dir.join("respo.calcit/Agents.md"), "# Respo Agent\n"); + write_file(&modules_dir.join("respo.calcit/docs/api.md"), "# API\nrender!\n"); + write_file(&modules_dir.join("memof/Agents.md"), "# Memof Agent\n"); + + let docs = load_module_docs_from_dir(&modules_dir, Some("respo.calcit")).unwrap(); + assert_eq!(docs.len(), 2); + assert!(docs.iter().any(|doc| doc.path == "respo.calcit/Agents.md")); + assert!(docs.iter().any(|doc| doc.path == "docs/api.md")); + + fs::remove_dir_all(root).unwrap(); +} + +#[test] +fn load_module_docs_from_dir_errors_on_missing_module() { + let root = unique_temp_dir("modules-missing"); + let modules_dir = root.join("modules"); + fs::create_dir_all(&modules_dir).unwrap(); + + let err = load_module_docs_from_dir(&modules_dir, Some("missing.module")).unwrap_err(); + assert!(err.contains("Module 'missing.module' not found")); + + fs::remove_dir_all(root).unwrap(); +} + +#[test] +fn collect_search_results_prefers_heading_matches_over_example_mentions() { + let docs = vec![ + GuideDoc { + filename: "docs-indexing.md".to_string(), + path: "docs-indexing.md".to_string(), + content: "# Documentation Indexing\n\n```bash\ncr docs search polymorphism\n```\n".to_string(), + scope: GuideDocScope::Core, + frontmatter: GuideDocFrontmatter::default(), + }, + GuideDoc { + filename: "features.md".to_string(), + path: "features.md".to_string(), + content: "# Polymorphism\n\nPolymorphism allows generic behavior.\n".to_string(), + scope: GuideDocScope::Core, + frontmatter: GuideDocFrontmatter::default(), + }, + ]; + + let results = collect_search_results(&docs, "polymorphism", 2, None); + assert_eq!(results.len(), 2); + assert_eq!(docs[results[0].doc_index].filename, "features.md"); +} + +#[test] +fn collect_search_results_prefers_filename_matches() { + let docs = vec![ + GuideDoc { + filename: "traits.md".to_string(), + path: "traits.md".to_string(), + content: "# Notes\n\nTraits discussion appears here.\n".to_string(), + scope: GuideDocScope::Core, + frontmatter: GuideDocFrontmatter::default(), + }, + GuideDoc { + filename: "guide.md".to_string(), + path: "guide.md".to_string(), + content: "# Traits\n\nTraits overview.\n".to_string(), + scope: GuideDocScope::Core, + frontmatter: GuideDocFrontmatter::default(), + }, + ]; + + let results = collect_search_results(&docs, "traits", 2, None); + assert_eq!(results.len(), 2); + assert_eq!(docs[results[0].doc_index].filename, "traits.md"); +} + +#[test] +fn parse_doc_frontmatter_extracts_lists_and_strips_header() { + let raw = "---\ntitle: \"Run Calcit\"\nkind: hub\ncategory: run\naliases:\n - watch mode\n - eval\nentry_for:\n - cr eval\n---\n# Run Calcit\n\nBody text.\n"; + let (frontmatter, body) = parse_doc_frontmatter(raw); + + assert_eq!(frontmatter.title.as_deref(), Some("Run Calcit")); + assert_eq!(frontmatter.scope.as_deref(), None); + assert_eq!(frontmatter.kind.as_deref(), Some("hub")); + assert_eq!(frontmatter.category.as_deref(), Some("run")); + assert_eq!(frontmatter.aliases, vec!["watch mode", "eval"]); + assert_eq!(frontmatter.entry_for, vec!["cr eval"]); + assert!(body.starts_with("# Run Calcit")); +} + +#[test] +fn collect_search_results_uses_alias_matches_without_body_hits() { + let docs = vec![GuideDoc { + filename: "edit-tree.md".to_string(), + path: "run/edit-tree.md".to_string(), + content: "# CLI Code Editing\n\nFocused chapter for tree editing.\n".to_string(), + scope: GuideDocScope::Core, + frontmatter: GuideDocFrontmatter { + title: Some("CLI Code Editing".to_string()), + scope: Some("core".to_string()), + kind: Some("guide".to_string()), + category: Some("run".to_string()), + aliases: vec!["target-replace".to_string(), "tree replace".to_string()], + entry_for: vec!["cr tree target-replace".to_string()], + }, + }]; + + let results = collect_search_results(&docs, "target-replace", 2, None); + assert_eq!(results.len(), 1); + assert_eq!(docs[results[0].doc_index].filename, "edit-tree.md"); +} + +#[test] +fn collect_search_results_prefers_guide_over_spec_on_same_metadata_hit() { + let docs = vec![ + GuideDoc { + filename: "docs-indexing.md".to_string(), + path: "docs-indexing.md".to_string(), + content: "# Documentation Indexing Spec\n\nBody.\n".to_string(), + scope: GuideDocScope::Core, + frontmatter: GuideDocFrontmatter { + title: Some("Documentation Indexing Spec".to_string()), + scope: Some("core".to_string()), + kind: Some("spec".to_string()), + category: Some("docs".to_string()), + aliases: vec!["target-replace".to_string()], + entry_for: vec![], + }, + }, + GuideDoc { + filename: "edit-tree.md".to_string(), + path: "run/edit-tree.md".to_string(), + content: "# CLI Code Editing\n\nBody.\n".to_string(), + scope: GuideDocScope::Core, + frontmatter: GuideDocFrontmatter { + title: Some("CLI Code Editing".to_string()), + scope: Some("core".to_string()), + kind: Some("guide".to_string()), + category: Some("run".to_string()), + aliases: vec!["target-replace".to_string()], + entry_for: vec![], + }, + }, + ]; + + let results = collect_search_results(&docs, "target-replace", 2, None); + assert_eq!(docs[results[0].doc_index].filename, "edit-tree.md"); +} + +#[test] +fn score_doc_shape_prefers_guides_to_specs() { + let guide = GuideDoc { + filename: "guide.md".to_string(), + path: "guide.md".to_string(), + content: "# Guide\n".to_string(), + scope: GuideDocScope::Core, + frontmatter: GuideDocFrontmatter { + title: None, + scope: Some("core".to_string()), + kind: Some("guide".to_string()), + category: Some("run".to_string()), + aliases: vec![], + entry_for: vec![], + }, + }; + let spec = GuideDoc { + filename: "spec.md".to_string(), + path: "spec.md".to_string(), + content: "# Spec\n".to_string(), + scope: GuideDocScope::Core, + frontmatter: GuideDocFrontmatter { + title: None, + scope: Some("core".to_string()), + kind: Some("spec".to_string()), + category: Some("docs".to_string()), + aliases: vec![], + entry_for: vec![], + }, + }; + + assert!(score_doc_shape(&guide) > score_doc_shape(&spec)); +} + +#[test] +fn parse_doc_frontmatter_reads_scope_field() { + let raw = "---\ntitle: \"Module API\"\nscope: \"module\"\nkind: reference\ncategory: api\n---\n# Module API\n"; + let (frontmatter, body) = parse_doc_frontmatter(raw); + assert_eq!(frontmatter.scope.as_deref(), Some("module")); + assert_eq!(frontmatter.kind.as_deref(), Some("reference")); + assert!(body.starts_with("# Module API")); +} + +#[test] +fn find_doc_by_query_matches_aliases_and_titles() { + let docs = vec![ + GuideDoc { + filename: "edit-tree.md".to_string(), + path: "run/edit-tree.md".to_string(), + content: "# CLI Code Editing\n".to_string(), + scope: GuideDocScope::Core, + frontmatter: GuideDocFrontmatter { + title: Some("CLI Code Editing".to_string()), + scope: Some("core".to_string()), + kind: Some("guide".to_string()), + category: Some("run".to_string()), + aliases: vec!["target-replace".to_string()], + entry_for: vec!["cr tree target-replace".to_string()], + }, + }, + GuideDoc { + filename: "docs-indexing.md".to_string(), + path: "docs-indexing.md".to_string(), + content: "# Documentation Indexing Spec\n".to_string(), + scope: GuideDocScope::Core, + frontmatter: GuideDocFrontmatter { + title: Some("Documentation Indexing Spec".to_string()), + scope: Some("core".to_string()), + kind: Some("spec".to_string()), + category: Some("docs".to_string()), + aliases: vec!["search indexing".to_string()], + entry_for: vec![], + }, + }, + ]; + + assert_eq!(find_doc_by_query(&docs, "target-replace").unwrap().filename, "edit-tree.md"); + assert_eq!(find_doc_by_query(&docs, "CLI Code Editing").unwrap().filename, "edit-tree.md"); +} + +#[test] +fn score_doc_query_prefers_filename_exact_match() { + let doc = GuideDoc { + filename: "polymorphism.md".to_string(), + path: "features/polymorphism.md".to_string(), + content: "# Polymorphism\n".to_string(), + scope: GuideDocScope::Core, + frontmatter: GuideDocFrontmatter { + title: Some("Polymorphism".to_string()), + scope: Some("core".to_string()), + kind: Some("guide".to_string()), + category: Some("features".to_string()), + aliases: vec!["trait dispatch".to_string()], + entry_for: vec![], + }, + }; + + assert!(score_doc_query(&doc, "polymorphism.md") > score_doc_query(&doc, "trait dispatch")); +} + +#[test] +fn find_doc_by_query_can_resolve_module_agents_title() { + let docs = vec![GuideDoc { + filename: "Agents.md".to_string(), + path: "respo.calcit/Agents.md".to_string(), + content: "# Respo Development Guide for LLM Agents\n".to_string(), + scope: GuideDocScope::Module("respo.calcit".to_string()), + frontmatter: GuideDocFrontmatter { + title: Some("Respo-Agent.md".to_string()), + scope: Some("module".to_string()), + kind: Some("agent".to_string()), + category: Some("docs".to_string()), + aliases: vec!["Respo-Agent".to_string()], + entry_for: vec!["render!".to_string()], + }, + }]; + + assert_eq!(find_doc_by_query(&docs, "Respo-Agent").unwrap().path, "respo.calcit/Agents.md"); +} + +#[test] +fn collect_search_results_prefers_module_style_guide_for_defstyle_query() { + let docs = vec![ + GuideDoc { + filename: "styles.md".to_string(), + path: "docs/guide/styles.md".to_string(), + content: "## Styles\n\nStatic style guide for Respo.\n".to_string(), + scope: GuideDocScope::Module("respo.calcit".to_string()), + frontmatter: GuideDocFrontmatter { + title: Some("Styles".to_string()), + scope: Some("module".to_string()), + kind: Some("guide".to_string()), + category: Some("ecosystem".to_string()), + aliases: vec!["defstyle".to_string(), ":class-name".to_string(), "style map".to_string()], + entry_for: vec!["style extraction".to_string(), "static styles".to_string()], + }, + }, + GuideDoc { + filename: "api.md".to_string(), + path: "docs/api.md".to_string(), + content: "## Respo API\n\ndefcomp render! clear-cache!\n".to_string(), + scope: GuideDocScope::Module("respo.calcit".to_string()), + frontmatter: GuideDocFrontmatter { + title: Some("Respo API".to_string()), + scope: Some("module".to_string()), + kind: Some("overview".to_string()), + category: Some("reference".to_string()), + aliases: vec!["respo api".to_string()], + entry_for: vec!["api reference".to_string()], + }, + }, + ]; + + let results = collect_search_results(&docs, "defstyle", 2, None); + assert_eq!(results.len(), 1); + assert_eq!(docs[results[0].doc_index].filename, "styles.md"); +} + +#[test] +fn find_doc_by_query_matches_module_entry_for_terms() { + let docs = vec![GuideDoc { + filename: "server-rendering.md".to_string(), + path: "docs/guide/server-rendering.md".to_string(), + content: "## Server Rendering\n\nSSR flow for Respo.\n".to_string(), + scope: GuideDocScope::Module("respo.calcit".to_string()), + frontmatter: GuideDocFrontmatter { + title: Some("Server Rendering".to_string()), + scope: Some("module".to_string()), + kind: Some("guide".to_string()), + category: Some("ecosystem".to_string()), + aliases: vec!["SSR".to_string(), "server side rendering".to_string()], + entry_for: vec!["realize-ssr!".to_string(), "hydrate app".to_string()], + }, + }]; + + assert_eq!( + find_doc_by_query(&docs, "server side rendering").unwrap().filename, + "server-rendering.md" + ); + assert_eq!(find_doc_by_query(&docs, "hydrate app").unwrap().filename, "server-rendering.md"); +} + +#[test] +fn find_doc_by_query_matches_symbol_aliases_for_module_docs() { + let docs = vec![GuideDoc { + filename: "pick-states.md".to_string(), + path: "docs/apis/pick-states.md".to_string(), + content: "## >>\n\nPick nested states.\n".to_string(), + scope: GuideDocScope::Module("respo.calcit".to_string()), + frontmatter: GuideDocFrontmatter { + title: Some(">>".to_string()), + scope: Some("module".to_string()), + kind: Some("reference".to_string()), + category: Some("reference".to_string()), + aliases: vec!["pick-states".to_string(), ">>".to_string(), "states cursor".to_string()], + entry_for: vec!["state cursor".to_string(), "local states".to_string()], + }, + }]; + + assert_eq!(find_doc_by_query(&docs, ">>").unwrap().filename, "pick-states.md"); + assert_eq!(find_doc_by_query(&docs, "state cursor").unwrap().filename, "pick-states.md"); +} + +#[test] +fn validate_doc_frontmatter_accepts_registered_category() { + let frontmatter = GuideDocFrontmatter { + title: Some("Styles".to_string()), + scope: Some("module".to_string()), + kind: Some("guide".to_string()), + category: Some("ecosystem".to_string()), + aliases: vec![], + entry_for: vec![], + }; + + validate_doc_frontmatter("docs/guide/styles.md", &frontmatter).unwrap(); +} + +#[test] +fn validate_doc_frontmatter_rejects_unknown_category() { + let frontmatter = GuideDocFrontmatter { + title: Some("Broken".to_string()), + scope: Some("core".to_string()), + kind: Some("guide".to_string()), + category: Some("api".to_string()), + aliases: vec![], + entry_for: vec![], + }; + + let err = validate_doc_frontmatter("docs/broken.md", &frontmatter).unwrap_err(); + assert!(err.contains("Invalid frontmatter category 'api'")); + assert!(err.contains("docs/docs-indexing.md")); +} + +#[test] +fn load_module_docs_from_dir_rejects_invalid_category() { + let root = unique_temp_dir("modules-invalid-category"); + let modules_dir = root.join("modules"); + write_file( + &modules_dir.join("respo.calcit/docs/api.md"), + "---\ntitle: \"API\"\nscope: \"module\"\nkind: reference\ncategory: api\n---\n# API\n", + ); + + let err = load_module_docs_from_dir(&modules_dir, Some("respo.calcit")).unwrap_err(); + assert!(err.contains("Invalid frontmatter category 'api'")); + + fs::remove_dir_all(root).unwrap(); +} diff --git a/src/cli_args.rs b/src/cli_args.rs index a70c141f..8259a597 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -508,6 +508,12 @@ pub struct DocsSearchCommand { /// filter by filename (optional) #[argh(option, short = 'f')] pub filename: Option, + /// search scope: core, modules, or all (default: core; with --module defaults to modules) + #[argh(option)] + pub scope: Option, + /// search docs for a specific installed module (e.g. respo.calcit) + #[argh(option)] + pub module: Option, } #[derive(FromArgs, PartialEq, Debug, Clone)] @@ -529,6 +535,12 @@ pub struct DocsReadCommand { /// show line numbers in heading list and section titles #[argh(switch)] pub with_lines: bool, + /// read scope: core, modules, or all (default: core; with --module defaults to modules) + #[argh(option)] + pub scope: Option, + /// read docs from a specific installed module (e.g. respo.calcit) + #[argh(option)] + pub module: Option, } #[derive(FromArgs, PartialEq, Debug, Clone)] @@ -565,6 +577,12 @@ pub struct DocsReadLinesCommand { /// number of lines to read (default: 80) #[argh(option, short = 'n', default = "80")] pub lines: usize, + /// read scope: core, modules, or all (default: core; with --module defaults to modules) + #[argh(option)] + pub scope: Option, + /// read docs from a specific installed module (e.g. respo.calcit) + #[argh(option)] + pub module: Option, } #[derive(FromArgs, PartialEq, Debug, Clone)] From 9b70f544b707a330771b6e6dd54cdc7f6b6de2b3 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 26 Mar 2026 22:24:01 +0800 Subject: [PATCH 45/57] Avoid recursive evaluation in preprocess type inference; tag 0.12.12 --- Cargo.lock | 2 +- Cargo.toml | 2 +- package.json | 2 +- src/bin/cli_handlers/docs.rs | 7 +++---- src/runner/preprocess.rs | 8 ++++---- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d09e7bc8..f368b6a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.11" +version = "0.12.12" dependencies = [ "argh", "bisection_key", diff --git a/Cargo.toml b/Cargo.toml index 8f3cd88a..d5cadd20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.11" +version = "0.12.12" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/package.json b/package.json index 92e0dd23..36a12ad9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.11", + "version": "0.12.12", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", diff --git a/src/bin/cli_handlers/docs.rs b/src/bin/cli_handlers/docs.rs index eaf29e5c..c564356b 100644 --- a/src/bin/cli_handlers/docs.rs +++ b/src/bin/cli_handlers/docs.rs @@ -80,7 +80,7 @@ impl GuideDoc { let base_title = self.frontmatter.title.as_deref().unwrap_or(&self.filename); match &self.scope { GuideDocScope::Core => base_title.to_string(), - GuideDocScope::Module(module) => format!("{} [module:{}]", base_title, module), + GuideDocScope::Module(module) => format!("{base_title} [module:{module}]"), } } } @@ -435,7 +435,7 @@ fn load_module_docs_from_dir(modules_dir: &Path, module_filter: Option<&str>) -> let agents_path = path.join("Agents.md"); if agents_path.exists() { let raw_content = fs::read_to_string(&agents_path).map_err(|e| format!("Failed to read file {agents_path:?}: {e}"))?; - let agents_doc_path = format!("{}/Agents.md", module_name); + let agents_doc_path = format!("{module_name}/Agents.md"); docs.push(parse_guide_doc( "Agents.md".to_string(), agents_doc_path, @@ -453,8 +453,7 @@ fn load_module_docs_from_dir(modules_dir: &Path, module_filter: Option<&str>) -> if let Some(filter) = module_filter { if !seen_modules.contains(filter) { return Err(format!( - "Module '{filter}' not found under {:?}. Use 'cr libs scan-md ' or inspect ~/.config/calcit/modules/.", - modules_dir + "Module '{filter}' not found under {modules_dir:?}. Use 'cr libs scan-md ' or inspect ~/.config/calcit/modules/." )); } } diff --git a/src/runner/preprocess.rs b/src/runner/preprocess.rs index 708e51fc..4b02a288 100644 --- a/src/runner/preprocess.rs +++ b/src/runner/preprocess.rs @@ -2347,11 +2347,11 @@ fn infer_return_type_from_compiled_callable( call_expr: &CalcitList, scope_types: &ScopeTypes, ) -> Option> { - let compiled_value = program::resolve_compiled_executable_def(ns, def, &CallStackList::default()) - .ok() - .flatten()?; + let compiled = program::lookup_compiled_def(ns, def)?; - match compiled_value { + // Avoid evaluating compiled payloads during preprocess type inference. + // Evaluating function code here can recurse back into preprocess and overflow stack. + match compiled.preprocessed_code { Calcit::Fn { info, .. } => { if let Some(resolved) = resolve_generic_return_type(&info, call_expr.iter().skip(1), scope_types) { return Some(resolved); From 5323714bbbada74d617d088821dea5e53dc0603e Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 26 Mar 2026 22:33:33 +0800 Subject: [PATCH 46/57] --once option removed; fix outdated case --- .github/workflows/publish.yaml | 5 ++--- .github/workflows/test.yaml | 5 ++--- editing-history/2026-0225-1234-watch-mode-default-once.md | 1 - 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index f7f003f3..38690a65 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -34,14 +34,13 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: cargo test - - run: cargo run --bin cr calcit/editor/compact.cirru --once - - run: cargo run --bin cr calcit/test.cirru --once + - run: cargo run --bin cr calcit/test.cirru - name: "try js" run: > yarn && yarn tsc - && cargo run --bin cr calcit/test.cirru --once js + && cargo run --bin cr calcit/test.cirru js && ln -s ../../ node_modules/@calcit/procs && cp -v scripts/main.mjs js-out/ && node js-out/main.mjs diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 765fd7b5..e3b9c55e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -36,8 +36,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: cargo test - - run: cargo run --bin cr calcit/editor/compact.cirru --once - - run: cargo run --bin cr calcit/test.cirru --once + - run: cargo run --bin cr calcit/test.cirru - uses: giraffate/clippy-action@v1 with: @@ -48,7 +47,7 @@ jobs: run: > yarn && yarn tsc - && cargo run --bin cr calcit/test.cirru --once js + && cargo run --bin cr calcit/test.cirru js && ln -s ../../ node_modules/@calcit/procs && cp -v scripts/main.mjs js-out/ && node js-out/main.mjs diff --git a/editing-history/2026-0225-1234-watch-mode-default-once.md b/editing-history/2026-0225-1234-watch-mode-default-once.md index a62ef76a..3d5e8a11 100644 --- a/editing-history/2026-0225-1234-watch-mode-default-once.md +++ b/editing-history/2026-0225-1234-watch-mode-default-once.md @@ -2,5 +2,4 @@ - Unified run mode behavior for `cr`, `cr js`, and `cr ir`: default to once. - Added explicit `-w/--watch` switches for top-level direct run and `ir` subcommand. -- Kept `-1/--once` for backward compatibility. - Updated watch-mode related docs in `Agents.md` and `docs/CalcitAgent.md`. From b6a151590a83157bc1a9a9f415eb0b7484012f01 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 26 Mar 2026 23:44:09 +0800 Subject: [PATCH 47/57] bump 0.12.13 --- Cargo.lock | 2 +- Cargo.toml | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f368b6a9..7668f668 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.12" +version = "0.12.13" dependencies = [ "argh", "bisection_key", diff --git a/Cargo.toml b/Cargo.toml index d5cadd20..c516b7b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.12" +version = "0.12.13" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/package.json b/package.json index 36a12ad9..3aff5d73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.12", + "version": "0.12.13", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", From 75b7d6fa3219b5b228c98f5fdbead0997198ce3d Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 27 Mar 2026 00:49:28 +0800 Subject: [PATCH 48/57] improve upgrademd --- docs/run/upgrade.md | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/run/upgrade.md b/docs/run/upgrade.md index 6cb286ae..9873ed1e 100644 --- a/docs/run/upgrade.md +++ b/docs/run/upgrade.md @@ -9,6 +9,7 @@ aliases: - "respo upgrade" - "lilac upgrade" --- + # Calcit 项目升级手册(Respo / Lilac) 本手册只关注**项目升级流程**,不展开开发实现细节。 @@ -27,6 +28,7 @@ aliases: - 命令入口:`README`、项目脚本、CI workflow - Node 工具链:`package.json`、`yarn.lock`、Corepack/Yarn 版本 - 注意 git fetch 检查最新历史, 避免基于老版本操作导致变更冲突 +- 结构化编辑优先使用 `cr edit` / `cr tree`;若直接改过 `compact.cirru`,提交前执行一次 `cr edit format` --- @@ -34,6 +36,22 @@ aliases: 下面流程按“先确认版本,再对齐工具链,再更新依赖,最后按 CI 链路验证”的顺序执行。 +### 快速命令清单 + +```bash +cr --version +caps outdated --yes +caps +corepack enable +corepack prepare yarn@4.12.0 --activate +yarn install +yarn install --immutable +cr js +yarn vite build --base=./ +``` + +说明:`yarn install` 只在 lockfile 迁移或依赖变更时需要;平时可直接从 `yarn install --immutable` 开始。 + ### Step A:确认 Calcit CLI 版本 ```bash @@ -50,7 +68,7 @@ cr --version - `package.json` 里的 `@calcit/procs` - `package.json` 里的 `packageManager` - `.yarnrc.yml` 是否需要 `nodeLinker: node-modules` -- `.gitignore` 是否已忽略 `.yarn/*.gz`,避免 Yarn 生成的压缩状态文件入库 +- `.gitignore` 是否已忽略 `.yarn/*.gz`(避免 Yarn 压缩状态文件入库) 先把这些基础版本与工具链约定对齐,再继续更新依赖,能减少后面重复改 lockfile 或 CI 的次数。 @@ -60,15 +78,7 @@ cr --version caps outdated --yes ``` -说明: - -- `caps outdated`:查看可更新项; -- `caps outdated --yes`:直接更新 `deps.cirru`(无交互确认)。 -- `caps`:根据当前 `deps.cirru` 下载/同步模块内容。 - -注意:`caps outdated --yes` 只负责更新 `deps.cirru`,不等于已经完成模块同步。 - -若依赖是固定 tag/version,仍需先改 `deps.cirru` 再执行更新。 +说明:`caps outdated --yes` 用于更新 `deps.cirru`,不负责模块下载。 ### Step D:同步模块内容 @@ -76,7 +86,7 @@ caps outdated --yes caps ``` -说明:这一步才是根据当前 `deps.cirru` 下载/同步模块内容。若跳过这一步,后面的编译与安装结果容易混入旧模块状态。 +说明:这一步才会按当前 `deps.cirru` 下载/同步模块内容。 ### Step E:用 Yarn Berry 安装并校验 @@ -108,14 +118,14 @@ cr --entry js cr js && yarn vite build --base=./ ``` -如果 `package.json` 里有与编译、构建、测试相关的脚本,也应本地执行一遍;如果没有额外脚本,这一步可以跳过。若项目直接通过 Vite 构建,也可直接执行: +如果 `package.json` 里有编译、构建、测试相关脚本,也应本地执行一遍;没有额外脚本可跳过。若项目直接通过 Vite 构建,可执行: ```bash yarn up vite yarn vite build --base=./ ``` -说明:最近 Vite 有大版本更新。若项目依赖 Vite,升级时建议显式执行一次 `yarn up vite`,并在更新后重新跑 `yarn vite build --base=./` 确认没有新的构建兼容性问题。 +说明:若项目依赖 Vite,升级时建议显式执行一次 `yarn up vite`,并重跑构建确认兼容性。 例如还有: From 75c9b269d81cad0e510489198b5f70d143fe2536 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 29 Mar 2026 13:35:57 +0800 Subject: [PATCH 49/57] improve focus of error stack displaying --- src/bin/cli_handlers/tree.rs | 2 +- src/calcit.rs | 12 ++++++- src/call_stack.rs | 65 +++++++++++++++++++++++++++++------- src/runner/preprocess.rs | 2 +- src/snapshot.rs | 65 ++++++++++++++++++++++++++++++++++++ 5 files changed, 131 insertions(+), 15 deletions(-) diff --git a/src/bin/cli_handlers/tree.rs b/src/bin/cli_handlers/tree.rs index 0bb22df1..aa624a32 100644 --- a/src/bin/cli_handlers/tree.rs +++ b/src/bin/cli_handlers/tree.rs @@ -1138,7 +1138,7 @@ fn generic_swap_handler(target: &str, path_str: &str, operation: &str, snapshot_ let parent_display = if parent_path.is_empty() { "root".to_string() } else { - format!("[{}]", parent_path.iter().map(|i| i.to_string()).collect::>().join(",")) + format!("[{}]", parent_path.iter().map(|i| i.to_string()).collect::>().join(".")) }; match operation { diff --git a/src/calcit.rs b/src/calcit.rs index 196843e2..ed15fcfb 100644 --- a/src/calcit.rs +++ b/src/calcit.rs @@ -1094,7 +1094,7 @@ impl fmt::Display for NodeLocation { "{}/{} [{}]", self.ns, self.def, - self.coord.iter().map(|x| x.to_string()).collect::>().join(",") + self.coord.iter().map(|x| x.to_string()).collect::>().join(".") ) } } @@ -1497,4 +1497,14 @@ mod tests { assert_eq!(calcit_hash(&value), calcit_hash(&cloned)); } } + + #[test] + fn node_location_uses_dot_separator() { + let loc = NodeLocation::new( + Arc::from("app.comp.sidebar"), + Arc::from("comp-sidebar"), + Arc::from(vec![3, 2, 1, 0]), + ); + assert_eq!(loc.to_string(), "app.comp.sidebar/comp-sidebar [3.2.1.0]"); + } } diff --git a/src/call_stack.rs b/src/call_stack.rs index 97249f8d..ece61673 100644 --- a/src/call_stack.rs +++ b/src/call_stack.rs @@ -154,6 +154,50 @@ pub fn display_stack_with_docs( .map(|s| Arc::new(NodeLocation::new(s.ns.to_owned(), s.def.to_owned(), Arc::new(vec![])))) }) }); + let mut stack_rows: Vec<(usize, &CalcitStack, Option)> = vec![]; + for (idx, s) in stack.0.iter().enumerate() { + let stack_location = find_location_in_calcit(&s.code).or_else(|| s.args.iter().find_map(find_location_in_calcit)); + stack_rows.push((idx, s, stack_location)); + } + + let current_package = fallback_location + .as_deref() + .map(|l| root_ns(&l.ns).to_string()) + .or_else(|| { + stack_rows.iter().find_map(|(_, _, loc)| { + loc + .as_ref() + .and_then(|l| (!is_calcit_ns(&l.ns)).then(|| root_ns(&l.ns).to_string())) + }) + }) + .or_else(|| { + stack_rows + .iter() + .find_map(|(_, s, _)| (!is_calcit_ns(&s.ns)).then(|| root_ns(&s.ns).to_string())) + }) + .unwrap_or_else(|| String::from(crate::calcit::CORE_NS)); + + stack_rows.sort_by_key(|(idx, s, loc)| { + let ns_for_priority = loc.as_ref().map(|l| l.ns.as_ref()).unwrap_or(s.ns.as_ref()); + let priority = if root_ns(ns_for_priority) == current_package { + 0 + } else if is_calcit_ns(ns_for_priority) { + 2 + } else { + 1 + }; + (priority, *idx) + }); + + eprintln!("\nStack:"); + for (_, s, stack_location) in &stack_rows { + let is_macro = s.kind == StackKind::Macro; + match stack_location { + Some(l) => eprintln!(" {}/{}{} @ {l}", s.ns, s.def, if is_macro { "\t ~macro" } else { "" }), + None => eprintln!(" {}/{}{}", s.ns, s.def, if is_macro { "\t ~macro" } else { "" }), + } + } + eprintln!("\nFailure: {failure}"); if let Some(l) = fallback_location.as_deref() { eprintln!(" at {l}"); @@ -169,25 +213,14 @@ pub fn display_stack_with_docs( } } } - eprintln!("\ncall stack:"); - - for s in &stack.0 { - let is_macro = s.kind == StackKind::Macro; - let stack_location = find_location_in_calcit(&s.code).or_else(|| s.args.iter().find_map(find_location_in_calcit)); - match stack_location { - Some(l) => eprintln!(" {}/{}{} @ {l}", s.ns, s.def, if is_macro { "\t ~macro" } else { "" }), - None => eprintln!(" {}/{}{}", s.ns, s.def, if is_macro { "\t ~macro" } else { "" }), - } - } let mut stack_list = EdnListView::default(); - for s in &stack.0 { + for (_, s, stack_location) in &stack_rows { let mut args = EdnListView::default(); for v in s.args.iter() { let edn_val = edn::calcit_to_edn(v)?; args.push(edn::sanitize_edn_for_format(&edn_val)); } - let stack_location = find_location_in_calcit(&s.code).or_else(|| s.args.iter().find_map(find_location_in_calcit)); let mut info_map = vec![ (Edn::tag("def"), format!("{}/{}", s.ns, s.def).into()), (Edn::tag("code"), cirru::calcit_to_cirru(&s.code)?.into()), @@ -238,6 +271,14 @@ pub fn display_stack_with_docs( Ok(()) } +fn root_ns(ns: &str) -> &str { + ns.split('.').next().unwrap_or(ns) +} + +fn is_calcit_ns(ns: &str) -> bool { + ns == crate::calcit::CORE_NS || ns.starts_with("calcit.") +} + const ERROR_SNAPSHOT: &str = ".calcit-error.cirru"; fn find_location_in_calcit(v: &Calcit) -> Option { diff --git a/src/runner/preprocess.rs b/src/runner/preprocess.rs index 4b02a288..dda0ebad 100644 --- a/src/runner/preprocess.rs +++ b/src/runner/preprocess.rs @@ -806,7 +806,7 @@ fn preprocess_list_call( let loc = head.get_location().or_else(|| first_arg.get_location()); if let Some(l) = loc { - let coord_repr = l.coord.iter().map(|c| c.to_string()).collect::>().join(","); + let coord_repr = l.coord.iter().map(|c| c.to_string()).collect::>().join("."); eprintln!( "[&inspect-type] in {}/{} [{}]\n {} => {}", l.ns, diff --git a/src/snapshot.rs b/src/snapshot.rs index 18919007..2c3e4bc7 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -1391,12 +1391,63 @@ pub fn render_snapshot_content(snapshot: &Snapshot) -> Result { // Format Edn as Cirru string let content = cirru_edn::format(&edn_data, true).map_err(|e| format!("Failed to format snapshot as Cirru: {e}"))?; + let content = normalize_pipe_prefixed_strings(&content); validate_serialized_snapshot_content(&content)?; Ok(content) } +fn normalize_pipe_prefixed_strings(content: &str) -> String { + let mut out = String::with_capacity(content.len()); + let bytes = content.as_bytes(); + let mut i = 0; + + while i < bytes.len() { + let ch = bytes[i] as char; + let at_token_start = i == 0 || is_leaf_boundary(bytes[i - 1] as char); + + if at_token_start && ch == '"' { + let mut j = i + 1; + while j < bytes.len() { + let c = bytes[j] as char; + if c == '\n' || c == '\r' || c == ')' { + break; + } + j += 1; + } + + let token = &content[i + 1..j]; + if can_use_pipe_prefix(token) { + out.push('|'); + out.push_str(token); + } else { + out.push('"'); + out.push_str(token); + } + i = j; + continue; + } + + out.push(ch); + i += 1; + } + + out +} + +fn is_leaf_boundary(ch: char) -> bool { + ch.is_whitespace() || ch == '(' || ch == ')' +} + +fn can_use_pipe_prefix(token: &str) -> bool { + !token.is_empty() + && !token.contains('\\') + && token + .chars() + .all(|c| !c.is_whitespace() && c != '(' && c != ')' && c != '"' && c != '\'') +} + /// Save snapshot to compact.cirru file /// This is a shared utility function used by CLI edit commands pub fn save_snapshot_to_file>(compact_cirru_path: P, snapshot: &Snapshot) -> Result<(), String> { @@ -1416,6 +1467,20 @@ mod tests { use std::fs; + #[test] + fn normalizes_simple_quoted_tokens_to_pipe_prefix() { + let input = "{} (:a \"&) (:b \"56px) (:c \"hello-world)"; + let output = normalize_pipe_prefixed_strings(input); + assert_eq!(output, "{} (:a |&) (:b |56px) (:c |hello-world)"); + } + + #[test] + fn keeps_quoted_tokens_when_pipe_prefix_is_unsafe() { + let input = "{} (:a \"hello world) (:b \"line\\nfeed) (:c \"x(y))"; + let output = normalize_pipe_prefixed_strings(input); + assert_eq!(output, input); + } + #[test] fn test_examples_field_parsing() { // 读取实际的 calcit-core.cirru 文件 From 48a89a57dab507a1d7069fffe9bc1d55473f0834 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 29 Mar 2026 22:48:31 +0800 Subject: [PATCH 50/57] fix string syntax in edit format --- Cargo.lock | 42 ++---------------- Cargo.toml | 4 +- calcit/test-string.cirru | 2 +- src/snapshot.rs | 96 ++++++++++++++++++---------------------- 4 files changed, 49 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7668f668..1a398e2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,26 +61,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bincode" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" -dependencies = [ - "bincode_derive", - "serde", - "unty", -] - -[[package]] -name = "bincode_derive" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" -dependencies = [ - "virtue", -] - [[package]] name = "bisection_key" version = "0.0.1" @@ -155,11 +135,10 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "cirru_edn" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f4f45b12dd75820a21e7209e7eae7d81f4aafa0bb0b04c547fe2609c12e0db" +checksum = "b47f4173f6758afa15d0ad76bb9013a17b8a7da89a05e2b2a60cbc6b0c071ee9" dependencies = [ - "bincode", "cirru_parser", "cjk", "hex", @@ -168,11 +147,10 @@ dependencies = [ [[package]] name = "cirru_parser" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf1a19f30e275287096a29e5b5981cf7416b38ab4a8782f8183a39424a1d35d" +checksum = "6cd74f684ca880e0f809554c676d1cede0bc688a0b184b62adef58f5e52b22ed" dependencies = [ - "bincode", "serde", ] @@ -827,12 +805,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "unty" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" - [[package]] name = "ureq" version = "3.2.0" @@ -868,12 +840,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "virtue" -version = "0.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" - [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index c516b7b6..fd20e933 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,14 +32,14 @@ strum = "0.25" strum_macros = "0.25" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -cirru_edn = "0.7.3" +cirru_edn = "0.7.4" cirru_parser = "0.2.3" bisection_key = "0.0.1" ureq = "3.2.0" rmp-serde = "1.3.0" [build-dependencies] -cirru_edn = "0.7.3" +cirru_edn = "0.7.4" rmp-serde = "1.3.0" serde = { version = "1.0", features = ["derive"] } cirru_parser = "0.2.3" diff --git a/calcit/test-string.cirru b/calcit/test-string.cirru index ea4f3b98..1f110851 100644 --- a/calcit/test-string.cirru +++ b/calcit/test-string.cirru @@ -222,7 +222,7 @@ :code $ quote fn () (log-title "|Test blank?") assert-detect identity $ blank? | - assert-detect identity $ blank? "\"" + assert-detect identity $ blank? | assert-detect identity $ blank? "| " assert-detect identity $ blank? "| " assert-detect identity $ blank? "|\n" diff --git a/src/snapshot.rs b/src/snapshot.rs index 2c3e4bc7..c97e00e0 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -1389,63 +1389,27 @@ pub fn render_snapshot_content(snapshot: &Snapshot) -> Result { let edn_data = Edn::from(edn_map); - // Format Edn as Cirru string - let content = cirru_edn::format(&edn_data, true).map_err(|e| format!("Failed to format snapshot as Cirru: {e}"))?; - let content = normalize_pipe_prefixed_strings(&content); + // Normalize on AST directly, avoiding parse-after-format roundtrip. + let normalized = normalize_pipe_prefixed_leaf(edn_data.cirru()); + let content = cirru_parser::format(std::slice::from_ref(&normalized), true.into()) + .map_err(|e| format!("Failed to format snapshot as Cirru: {e}"))?; validate_serialized_snapshot_content(&content)?; Ok(content) } -fn normalize_pipe_prefixed_strings(content: &str) -> String { - let mut out = String::with_capacity(content.len()); - let bytes = content.as_bytes(); - let mut i = 0; - - while i < bytes.len() { - let ch = bytes[i] as char; - let at_token_start = i == 0 || is_leaf_boundary(bytes[i - 1] as char); - - if at_token_start && ch == '"' { - let mut j = i + 1; - while j < bytes.len() { - let c = bytes[j] as char; - if c == '\n' || c == '\r' || c == ')' { - break; - } - j += 1; - } - - let token = &content[i + 1..j]; - if can_use_pipe_prefix(token) { - out.push('|'); - out.push_str(token); +fn normalize_pipe_prefixed_leaf(node: Cirru) -> Cirru { + match node { + Cirru::Leaf(token) => { + if let Some(rest) = token.strip_prefix('"') { + Cirru::leaf(format!("|{rest}")) } else { - out.push('"'); - out.push_str(token); + Cirru::Leaf(token) } - i = j; - continue; } - - out.push(ch); - i += 1; + Cirru::List(items) => Cirru::List(items.into_iter().map(normalize_pipe_prefixed_leaf).collect()), } - - out -} - -fn is_leaf_boundary(ch: char) -> bool { - ch.is_whitespace() || ch == '(' || ch == ')' -} - -fn can_use_pipe_prefix(token: &str) -> bool { - !token.is_empty() - && !token.contains('\\') - && token - .chars() - .all(|c| !c.is_whitespace() && c != '(' && c != ')' && c != '"' && c != '\'') } /// Save snapshot to compact.cirru file @@ -1469,16 +1433,40 @@ mod tests { #[test] fn normalizes_simple_quoted_tokens_to_pipe_prefix() { - let input = "{} (:a \"&) (:b \"56px) (:c \"hello-world)"; - let output = normalize_pipe_prefixed_strings(input); - assert_eq!(output, "{} (:a |&) (:b |56px) (:c |hello-world)"); + let input = "{} (:a \"|&\") (:b \"|56px\") (:c \"|hello-world\")"; + let nodes = cirru_parser::parse(input).expect("input should parse"); + let output_node = normalize_pipe_prefixed_leaf(nodes[0].to_owned()); + let output = cirru_parser::format(std::slice::from_ref(&output_node), true.into()).expect("output should format"); + assert_eq!(output.trim(), "{} (:a |&) (:b |56px) (:c |hello-world)"); } #[test] - fn keeps_quoted_tokens_when_pipe_prefix_is_unsafe() { - let input = "{} (:a \"hello world) (:b \"line\\nfeed) (:c \"x(y))"; - let output = normalize_pipe_prefixed_strings(input); - assert_eq!(output, input); + fn normalizes_all_quote_prefixed_leaves_from_ast() { + let input = "{} (:a \"|hello world\") (:b \"|line\\nfeed\") (:c \"|x(y)\")"; + let nodes = cirru_parser::parse(input).expect("input should parse"); + let output_node = normalize_pipe_prefixed_leaf(nodes[0].to_owned()); + let output = cirru_parser::format(std::slice::from_ref(&output_node), true.into()).expect("output should format"); + + let nodes = cirru_parser::parse(&output).expect("normalized output should still be parseable"); + let Cirru::List(root_items) = &nodes[0] else { + panic!("expected one root list"); + }; + + for pair in root_items.iter().skip(1) { + let Cirru::List(pair_items) = pair else { + continue; + }; + if pair_items.len() < 2 { + continue; + } + let Cirru::Leaf(value) = &pair_items[1] else { + continue; + }; + assert!( + value.starts_with('|'), + "expected string leaf to be normalized to pipe-prefix in AST, got: {value}" + ); + } } #[test] From 97f0d0044c2eff76e9a3c19f8e1efce1c10f546e Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 29 Mar 2026 23:36:01 +0800 Subject: [PATCH 51/57] bump 0.12.14 --- Cargo.lock | 2 +- Cargo.toml | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a398e2c..6df5ebed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,7 +87,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calcit" -version = "0.12.13" +version = "0.12.14" dependencies = [ "argh", "bisection_key", diff --git a/Cargo.toml b/Cargo.toml index fd20e933..224cd2f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "calcit" -version = "0.12.13" +version = "0.12.14" authors = ["jiyinyiyong "] edition = "2024" license = "MIT" diff --git a/package.json b/package.json index 3aff5d73..0fe4b341 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@calcit/procs", - "version": "0.12.13", + "version": "0.12.14", "main": "./lib/calcit.procs.mjs", "devDependencies": { "@types/node": "^25.0.9", From 85bd2ceb0f1c84c94115b2e44efd46cf638944f2 Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 30 Mar 2026 00:42:16 +0800 Subject: [PATCH 52/57] docs: replace hardcoded versions with dynamic shell variables in ffi-upgrade-guide --- docs/installation/ffi-upgrade-guide.md | 143 +++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 docs/installation/ffi-upgrade-guide.md diff --git a/docs/installation/ffi-upgrade-guide.md b/docs/installation/ffi-upgrade-guide.md new file mode 100644 index 00000000..917ecd1f --- /dev/null +++ b/docs/installation/ffi-upgrade-guide.md @@ -0,0 +1,143 @@ +--- +title: "FFI 项目升级手册" +scope: "core" +kind: "guide" +category: "installation" +aliases: + - "ffi upgrade" + - "dylib upgrade" +--- + +# FFI 项目升级手册 + +本文描述将 Calcit FFI 动态库项目(dylib 工程)升级到最新依赖版本的完整流程,基于实际升级经验整理。 + +## 两个关键版本号 + +每个 FFI 项目需要同步维护两处版本号: + +| 文件 | 字段 | 说明 | +| ------------ | ------------------------- | ----------------------------------------------------------- | +| `Cargo.toml` | `cirru_edn = "x.y.z"` | 必须与运行时 Calcit 二进制所链接的 `cirru_edn` 版本完全一致 | +| `deps.cirru` | `:calcit-version \|x.y.z` | 必须与 `cr --version` 输出一致,CI 会用它校验 | + +两者不一致都会导致 CI 失败或运行时 `dlsym failed`。 + +## 升级流程 + +### 0. 导出版本变量(所有后续步骤均复用) + +```bash +CR_VER=$(cr --version | awk '{print $NF}') +EDN_VER=$(cargo search cirru_edn --limit 1 | grep '^cirru_edn' | awk -F'"' '{print $2}') +PARSER_VER=$(cargo search cirru_parser --limit 1 | grep '^cirru_parser' | awk -F'"' '{print $2}') +echo "cr=$CR_VER cirru_edn=$EDN_VER cirru_parser=$PARSER_VER" +``` + +也可以用 crates.io API 查询: + +```bash +EDN_VER=$(curl -s 'https://crates.io/api/v1/crates/cirru_edn' \ + -H 'User-Agent: upgrade-script' \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['crate']['max_stable_version'])") +``` + +### 1. 更新 Cargo.toml + +```bash +sed -i "s/^cirru_edn = .*/cirru_edn = \"$EDN_VER\"/" Cargo.toml +sed -i "s/^cirru_parser = .*/cirru_parser = \"$PARSER_VER\"/" Cargo.toml +``` + +同时将 `[package] version` bump 一个 patch 版本(若项目有版本号发布需求)。 + +### 2. 更新 deps.cirru + +```bash +sed -i "s/:calcit-version |.*/:calcit-version |$CR_VER/" deps.cirru +``` + +确保与 `cr --version` 输出完全对应。 + +### 3. 本地构建验证 + +```bash +cargo build --release +rm -rf dylibs/* && mkdir -p dylibs && cp target/release/*.* dylibs/ +cr compact.cirru +``` + +三步缺一不可:构建 → 复制产物 → 运行验证。如果只更新了 `target/release/` 而未复制到 `dylibs/`,运行时仍会加载旧库。 + +### 4. 提交、打标签并推送 + +```bash +PKG_VER=$(cargo metadata --no-deps --format-version 1 \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['packages'][0]['version'])") + +git add Cargo.toml Cargo.lock deps.cirru +git commit -m "chore: upgrade cirru_edn $EDN_VER, cirru_parser $PARSER_VER; bump version to $PKG_VER" +git tag "$PKG_VER" +git push origin +git push origin "$PKG_VER" +``` + +### 5. 创建 PR 和 Release + +```bash +gh pr create \ + --title "chore: upgrade to cirru_edn $EDN_VER" \ + --body "- cirru_edn → $EDN_VER\n- cirru_parser → $PARSER_VER\n- deps.cirru calcit-version → $CR_VER" +# 或复用已有 PR,直接推送即可触发新的 CI run + +gh release create "$PKG_VER" --title "$PKG_VER" \ + --notes "upgrade cirru_edn $EDN_VER, cirru_parser $PARSER_VER" \ + --target +``` + +### 6. 检查 CI 状态 + +```bash +gh pr checks +``` + +期望输出:`All checks were successful` + +## 常见问题 + +### CI 报版本不匹配 + +先检查 `deps.cirru` 中 `:calcit-version` 是否与当前 `cr --version` 一致。 +这是最常见的失败原因,频繁升级 calcit 时容易被遗漏。 + +### dlsym failed + +按顺序排查: + +1. `edn_version()` 函数是否已导出 +2. `#[unsafe(no_mangle)]`(Rust 2024 edition)是否存在 +3. `dylibs/` 中是否已复制最新产物(`cp target/release/*.* dylibs/`) + +### amend 后需要重打 tag + +```bash +git tag -d "$PKG_VER" +git tag "$PKG_VER" +git push origin "$PKG_VER" --force +``` + +## 查看各项目当前状态 + +通过 GitHub CLI 快速检查所有 FFI 项目的最新版本和 CI 状态: + +```bash +for repo in calcit-lang/calcit-std calcit-lang/dylib-workflow \ + calcit-lang/calcit-fetch calcit-lang/calcit-http \ + calcit-lang/calcit-regex calcit-lang/calcit-wss \ + calcit-lang/calcit-command calcit-lang/calcit-clipboard \ + calcit-lang/calcit-wasmtime calcit-lang/calcit-fswatch \ + calcit-lang/calcit-graphviz; do + echo "=== $repo ===" + gh release list --repo "$repo" --limit 1 +done +``` From 5ad912e4e24a193a2542e38fbce3f9d2ff1bd25f Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 27 Mar 2026 10:58:17 +0800 Subject: [PATCH 53/57] forgot to include history --- .../2026-0326-2013-docs-frontmatter-and-module-search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editing-history/2026-0326-2013-docs-frontmatter-and-module-search.md b/editing-history/2026-0326-2013-docs-frontmatter-and-module-search.md index 5e4b2eed..a4bf925a 100644 --- a/editing-history/2026-0326-2013-docs-frontmatter-and-module-search.md +++ b/editing-history/2026-0326-2013-docs-frontmatter-and-module-search.md @@ -55,4 +55,4 @@ ## 后续经验 - docs 元数据一旦进入 CLI 行为,就应该配套规范页与验证页,否则后续加字段很容易出现“文档能写、检索却不稳定”的分叉。 -- 模块文档检索要尽量和 core docs 走同一套 resolver,这样使用者不需要记两套命令心智模型。 \ No newline at end of file +- 模块文档检索要尽量和 core docs 走同一套 resolver,这样使用者不需要记两套命令心智模型。 From 27cb8087fd9950b762800e433d315cfc48df4071 Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 30 Mar 2026 11:17:24 +0800 Subject: [PATCH 54/57] caps: also modify calcit version --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/bin/calcit_deps.rs | 40 +++++++++++++++++++++++++++++++--------- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6df5ebed..6dadc477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,7 @@ dependencies = [ "notify-debouncer-mini", "rmp-serde", "rpds", + "semver", "serde", "serde_json", "strum", @@ -658,6 +659,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index 224cd2f4..92d8539c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ cirru_parser = "0.2.3" bisection_key = "0.0.1" ureq = "3.2.0" rmp-serde = "1.3.0" +semver = "1.0" [build-dependencies] cirru_edn = "0.7.4" diff --git a/src/bin/calcit_deps.rs b/src/bin/calcit_deps.rs index a7b73beb..345cdd93 100644 --- a/src/bin/calcit_deps.rs +++ b/src/bin/calcit_deps.rs @@ -10,6 +10,7 @@ use argh::{self, FromArgs}; use cirru_edn::Edn; use colored::*; use git::*; +use semver::Version; use std::{ collections::HashMap, fs, @@ -96,7 +97,7 @@ pub fn main() -> Result<(), String> { match &cli_args.subcommand { Some(SubCommand::Outdated(opts)) => { - let updated = outdated_tags(deps.dependencies, &cli_args.input, opts.yes)?; + let updated = outdated_tags(deps, &cli_args.input, opts.yes)?; if updated { // Re-read deps.cirru and download updated dependencies println!("\nDownloading updated dependencies..."); @@ -460,14 +461,14 @@ fn call_build_script(folder_path: &Path) -> Result { /// also git fetch to read latest tag from remote, /// then we can compare, get outdated version printed /// Returns true if deps.cirru was updated -fn outdated_tags(deps: HashMap, Arc>, deps_file: &str, auto_yes: bool) -> Result { +fn outdated_tags(deps: PackageDeps, deps_file: &str, auto_yes: bool) -> Result { print_column("package".dimmed(), "expected".dimmed(), "latest".dimmed(), "hint".dimmed()); println!(); let mut outdated_packages = Vec::new(); let mut children = vec![]; - for (org_and_folder, version) in deps { + for (org_and_folder, version) in &deps.dependencies { let org_and_folder_clone = org_and_folder.clone(); let version_clone = version.clone(); let ret = thread::spawn(move || { @@ -478,7 +479,7 @@ fn outdated_tags(deps: HashMap, Arc>, deps_file: &str, auto_yes: b } ret.ok() }); - children.push((org_and_folder, version, ret)); + children.push((org_and_folder.clone(), version.clone(), ret)); } for (org_and_folder, version, child) in children { @@ -489,15 +490,28 @@ fn outdated_tags(deps: HashMap, Arc>, deps_file: &str, auto_yes: b } } - if !outdated_packages.is_empty() { + let calcit_version_upgrade = deps.calcit_version.as_ref().and_then(|version| { + let expected = Version::parse(version).ok()?; + let current = Version::parse(CALCIT_VERSION).ok()?; + if expected < current { Some(version.to_owned()) } else { None } + }); + + if !outdated_packages.is_empty() || calcit_version_upgrade.is_some() { if auto_yes { - update_deps_file(&outdated_packages, deps_file)?; + update_deps_file(&outdated_packages, calcit_version_upgrade.as_deref(), deps_file)?; println!("deps.cirru updated successfully!"); return Ok(true); } println!(); - print!("Found {} outdated package(s). Update deps.cirru? (y/N): ", outdated_packages.len()); + let mut changes = Vec::new(); + if !outdated_packages.is_empty() { + changes.push(format!("{} outdated package(s)", outdated_packages.len())); + } + if let Some(version) = &calcit_version_upgrade { + changes.push(format!("calcit-version {} -> {}", version, CALCIT_VERSION)); + } + print!("Found {}. Update deps.cirru? (y/N): ", changes.join(", ")); std::io::stdout().flush().map_err(|e| e.to_string())?; let mut input = String::new(); @@ -505,7 +519,7 @@ fn outdated_tags(deps: HashMap, Arc>, deps_file: &str, auto_yes: b let input = input.trim(); if input.is_empty() || input.to_lowercase() == "y" || input.to_lowercase() == "yes" { - update_deps_file(&outdated_packages, deps_file)?; + update_deps_file(&outdated_packages, calcit_version_upgrade.as_deref(), deps_file)?; println!("deps.cirru updated successfully!"); return Ok(true); } @@ -543,7 +557,11 @@ fn show_package_versions(org_and_folder: Arc, version: Arc) -> Result< } } -fn update_deps_file(outdated_packages: &[(Arc, Arc, String)], deps_file: &str) -> Result<(), String> { +fn update_deps_file( + outdated_packages: &[(Arc, Arc, String)], + calcit_version_upgrade: Option<&str>, + deps_file: &str, +) -> Result<(), String> { if !Path::new(deps_file).exists() { return Err("deps.cirru file not found".to_string()); } @@ -556,6 +574,10 @@ fn update_deps_file(outdated_packages: &[(Arc, Arc, String)], deps_fil })?; let mut deps: PackageDeps = parsed.try_into()?; + if let Some(version) = calcit_version_upgrade { + deps.calcit_version = Some(version.to_string()); + } + // Update the dependencies in the parsed structure for (org_and_folder, _old_version, new_version) in outdated_packages { deps.dependencies.insert(org_and_folder.clone(), new_version.clone().into()); From a5345627c563b93f70e499d5a1539b7f46d4b5f8 Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 30 Mar 2026 15:48:45 +0800 Subject: [PATCH 55/57] invalid argument usage of syntax nodes --- src/bin/calcit_deps.rs | 2 +- src/bin/cr.rs | 2 -- src/codegen/emit_js.rs | 75 ++++++++++++++++++++++++++++++++++++++--- ts-src/calcit.procs.mts | 2 +- 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/bin/calcit_deps.rs b/src/bin/calcit_deps.rs index 345cdd93..86bc7787 100644 --- a/src/bin/calcit_deps.rs +++ b/src/bin/calcit_deps.rs @@ -509,7 +509,7 @@ fn outdated_tags(deps: PackageDeps, deps_file: &str, auto_yes: bool) -> Result {}", version, CALCIT_VERSION)); + changes.push(format!("calcit-version {version} -> {CALCIT_VERSION}")); } print!("Found {}. Update deps.cirru? (y/N): ", changes.join(", ")); std::io::stdout().flush().map_err(|e| e.to_string())?; diff --git a/src/bin/cr.rs b/src/bin/cr.rs index 7f9e9464..a29646c8 100644 --- a/src/bin/cr.rs +++ b/src/bin/cr.rs @@ -575,7 +575,6 @@ fn run_codegen(entries: &ProgramEntries, emit_path: &str, ir_mode: bool) -> Resu match codegen::gen_ir::emit_ir(&entries.init_fn, &entries.reload_fn, emit_path) { Ok(_) => (), Err(failure) => { - eprintln!("\nfailed codegen, {failure}"); call_stack::display_stack_with_docs(&failure, &gen_stack::get_gen_stack(), None, None)?; return Err(failure); } @@ -585,7 +584,6 @@ fn run_codegen(entries: &ProgramEntries, emit_path: &str, ir_mode: bool) -> Resu match codegen::emit_js::emit_js(&entries.init_ns, emit_path) { Ok(_) => (), Err(failure) => { - eprintln!("\nfailed codegen, {failure}"); call_stack::display_stack_with_docs(&failure, &gen_stack::get_gen_stack(), None, None)?; return Err(failure); } diff --git a/src/codegen/emit_js.rs b/src/codegen/emit_js.rs index f21f3720..7d1279be 100644 --- a/src/codegen/emit_js.rs +++ b/src/codegen/emit_js.rs @@ -199,6 +199,12 @@ fn indent_block(body: &str, indent: &str) -> String { .join("\n") } +fn raw_syntax_codegen_error(syntax: &CalcitSyntax) -> String { + format!( + "invalid JS codegen: raw syntax node `{syntax}` cannot be emitted as a standalone JS value. LLM hint: special forms must start an expression, for example `(if cond a b)`, or appear at the beginning of a line / after `$`, instead of being left as a separate argument node." + ) +} + fn to_js_code( xs: &Calcit, ns: &str, @@ -294,10 +300,7 @@ fn to_js_code( info.def_ns, info.name, info.usage.used_in_impl )) } - Calcit::Syntax(s, ..) => { - let proc_prefix = get_proc_prefix(ns); - Ok(format!("{proc_prefix}{}", escape_var(s.as_ref()))) - } + Calcit::Syntax(s, ..) => Err(raw_syntax_codegen_error(s)), Calcit::Str(s) => Ok(escape_cirru_str(s)), Calcit::Bool(b) => Ok(b.to_string()), Calcit::Number(n) => Ok(n.to_string()), @@ -419,10 +422,23 @@ fn gen_call_code( } (_, _) => Err(format!("try expected 2 nodes, got: {body}")), }, + CalcitSyntax::Eval => { + let (prelude, args_code) = + gen_call_args_with_temps(&body, ns, local_defs, file_imports, tags, return_label.is_some(), inline_all)?; + let call_code = format!("{proc_prefix}{}({args_code})", escape_var("eval")); + Ok(wrap_call_with_prelude(prelude, call_code, return_label, detect_await(&body))) + } + CalcitSyntax::Reset => { + let (prelude, args_code) = + gen_call_args_with_temps(&body, ns, local_defs, file_imports, tags, return_label.is_some(), inline_all)?; + let call_code = format!("{proc_prefix}{}({args_code})", escape_var("reset!")); + Ok(wrap_call_with_prelude(prelude, call_code, return_label, detect_await(&body))) + } // for `&call-spread`, just translate as normal call CalcitSyntax::CallSpread => gen_call_code(&body, ns, local_defs, xs, file_imports, tags, return_label), CalcitSyntax::HintFn => Ok(format!("{return_code}null")), CalcitSyntax::AssertType => Ok(format!("{return_code}null")), + CalcitSyntax::AssertTraits => Ok(format!("{return_code}null")), _ => { let (prelude, args_code) = gen_call_args_with_temps(&body, ns, local_defs, file_imports, tags, return_label.is_some(), inline_all)?; @@ -1546,4 +1562,55 @@ mod tests { let compiled = compiled_def_for_codegen_test(program::CompiledDefKind::LazyValue, None); assert!(should_skip_core_def_codegen("eval", &compiled)); } + + #[test] + fn raw_syntax_nodes_fail_js_codegen_with_llm_hint() { + let local_defs: HashSet> = HashSet::new(); + let file_imports = RefCell::new(ImportsDict::new()); + let tags = RefCell::new(HashSet::new()); + + let failure = to_js_code( + &Calcit::Syntax(CalcitSyntax::If, Arc::from("tests.emit-js")), + "tests.emit-js", + &local_defs, + &file_imports, + &tags, + None, + ) + .expect_err("raw syntax should be rejected in JS codegen"); + + assert!(failure.contains("raw syntax node `if`"), "unexpected error: {failure}"); + assert!(failure.contains("LLM hint"), "unexpected error: {failure}"); + } + + #[test] + fn reset_syntax_call_codegen_uses_runtime_proc() { + let local_defs: HashSet> = HashSet::new(); + let file_imports = RefCell::new(ImportsDict::new()); + let tags = RefCell::new(HashSet::new()); + let form = Calcit::List(Arc::new(CalcitList::from(&[ + Calcit::Syntax(CalcitSyntax::Reset, Arc::from("tests.emit-js")), + symbol("state"), + Calcit::Number(1.0), + ]))); + + let code = to_js_code(&form, "tests.emit-js", &local_defs, &file_imports, &tags, None).expect("reset! should compile"); + + assert_eq!(code, "$clt.reset_$x_(state, 1)"); + } + + #[test] + fn eval_syntax_call_codegen_uses_runtime_proc() { + let local_defs: HashSet> = HashSet::new(); + let file_imports = RefCell::new(ImportsDict::new()); + let tags = RefCell::new(HashSet::new()); + let form = Calcit::List(Arc::new(CalcitList::from(&[ + Calcit::Syntax(CalcitSyntax::Eval, Arc::from("tests.emit-js")), + symbol("code"), + ]))); + + let code = to_js_code(&form, "tests.emit-js", &local_defs, &file_imports, &tags, None).expect("eval should compile"); + + assert_eq!(code, "$clt.eval(code)"); + } } diff --git a/ts-src/calcit.procs.mts b/ts-src/calcit.procs.mts index f29d42f3..a905a813 100644 --- a/ts-src/calcit.procs.mts +++ b/ts-src/calcit.procs.mts @@ -22,7 +22,7 @@ import { } from "./calcit-data.mjs"; import { CalcitRef } from "./js-ref.mjs"; -import { fieldsEqual, CalcitRecord } from "./js-record.mjs"; +import { CalcitRecord } from "./js-record.mjs"; import { CalcitImpl } from "./js-impl.mjs"; import { CalcitStruct } from "./js-struct.mjs"; import { CalcitEnum } from "./js-enum.mjs"; From 81b8dce7dc453d025974cc1c18a527a01ed5ea26 Mon Sep 17 00:00:00 2001 From: tiye Date: Sat, 4 Apr 2026 01:21:02 +0800 Subject: [PATCH 56/57] improve hint-fn warning --- src/codegen/emit_js.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/codegen/emit_js.rs b/src/codegen/emit_js.rs index 7d1279be..60b32c2b 100644 --- a/src/codegen/emit_js.rs +++ b/src/codegen/emit_js.rs @@ -1157,6 +1157,11 @@ fn gen_js_func( if is_hint { if hinted_async(xs) { async_prefix = String::from("async ") + } else if xs.len() > 1 && !xs.iter().skip(1).any(is_schema_map_form) { + eprintln!( + "[Warn] hint-fn args not in recognized schema map form in {}/{name}; correct usage: `hint-fn $ {{}} (:async true)`", + passed_defs.ns + ); } continue; } @@ -1261,6 +1266,21 @@ fn hinted_async(xs: &CalcitList) -> bool { xs.iter().skip(1).any(schema_marks_async) } +/// Returns true when a value is in the schema map form recognised by hint-fn: +/// either an already-evaluated `Calcit::Map` or a list-literal starting with +/// `{}` / `NativeMap`. Used to distinguish valid schema annotations (which +/// should never warn) from malformed async hints. +fn is_schema_map_form(form: &Calcit) -> bool { + match form { + Calcit::Map(_) => true, + Calcit::List(list) => { + matches!(list.first(), Some(Calcit::Symbol { sym, .. }) if sym.as_ref() == "{}") + || matches!(list.first(), Some(Calcit::Proc(CalcitProc::NativeMap))) + } + _ => false, + } +} + fn extract_preprocessed_fn_parts(code: &Calcit) -> Result<(CalcitFnArgs, Vec), String> { let Calcit::List(items) = code else { return Err(format!("expected preprocessed defn list, got: {code}")); From b48eaebd23f1c27b3712862e6ee275b35fc74aaf Mon Sep 17 00:00:00 2001 From: tiye Date: Sat, 4 Apr 2026 01:40:10 +0800 Subject: [PATCH 57/57] validation during configuring version --- src/bin/cli_handlers/edit.rs | 16 ++++++++++++++++ src/calcit/list.rs | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/bin/cli_handlers/edit.rs b/src/bin/cli_handlers/edit.rs index 46c7146b..2387d1fc 100644 --- a/src/bin/cli_handlers/edit.rs +++ b/src/bin/cli_handlers/edit.rs @@ -1512,6 +1512,22 @@ fn handle_config(opts: &EditConfigCommand, snapshot_file: &str) -> Result<(), St snapshot.configs.reload_fn = opts.value.clone(); } "version" => { + let v = opts.value.as_str(); + if v.starts_with('|') { + return Err(format!( + "Invalid version '{v}': do not include the '|' Cirru string prefix; use bare semver, e.g. '0.0.17'" + )); + } + // Validate semver-like format (x.y.z with optional pre-release suffix) + let is_valid_semver = { + let parts: Vec<&str> = v.splitn(4, '.').collect(); + parts.len() >= 3 && parts.iter().take(3).all(|p| !p.is_empty() && p.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false)) + }; + if !is_valid_semver { + return Err(format!( + "Invalid version '{v}': expected semver format, e.g. '0.0.17'" + )); + } snapshot.configs.version = opts.value.clone(); } _ => { diff --git a/src/calcit/list.rs b/src/calcit/list.rs index eb067e6b..a233f219 100644 --- a/src/calcit/list.rs +++ b/src/calcit/list.rs @@ -392,7 +392,7 @@ impl CalcitList { } } - pub fn iter(&self) -> CalcitListIterator { + pub fn iter(&self) -> CalcitListIterator<'_> { CalcitListIterator { value: self, index: 0,