diff --git a/.clj-kondo/.cache/v1/clj/user.transit.json b/.clj-kondo/.cache/v1/clj/user.transit.json index cf99466..831692f 100644 --- a/.clj-kondo/.cache/v1/clj/user.transit.json +++ b/.clj-kondo/.cache/v1/clj/user.transit.json @@ -1 +1 @@ -["^ ","~$eval-list",["^ ","~:fixed-arities",["~#set",[3]],"~:private",true,"~:ns","~$user","~:name","^0","~:type","~:fn","~:col",1,"~:top-ns",null,"~:row",203],"~$resolve-var",["^ ","^;",57,"^9",1,"^1",["^2",[3]],"^6","^<","^4","^5","^:",null,"^7","^8"],"~$syntax-quote*",["^ ","^1",["^2",[3]],"^3",true,"^4","^5","^6","^=","^7","^8","^9",1,"^:",null,"^;",22],"~$new-v",["^ ","^;",441,"^9",18,"^6","^>","^4","^5","^:",null],"~$ctor",["^ ","^;",515,"^9",17,"~:varargs-min-arity",0,"^6","^?","^4","^5","^:",null,"^7","^8"],"~$v",["^ ","^;",259,"^9",20,"^6","~$v","^4","^5","^:",null],"~$resolve-sym",["^ ","^1",["^2",[3]],"^3",true,"^4","^5","^6","^A","^7","^8","^9",1,"^:",null,"^;",101],"~$first-form",["^ ","^;",205,"^9",3,"^6","^B","^4","^5","^:",null,"^7","~:any"],"~$run-finally",["^ ","^;",411,"^9",13,"^1",["^2",[1]],"^6","^D","^4","^5","^:",null,"^7","^8"],"~$eval-require",["^ ","^1",["^2",[2]],"^3",true,"^4","^5","^6","^E","^7","^8","^9",1,"^:",null,"~:arities",["^ ","~i2",["^ ","~:ret","~:nil","~:arglist-str","[ctx spec]"]],"^;",74],"~$bind-put",["^ ","^1",["^2",[3]],"^3",true,"^4","^5","^6","^J","^7","^8","^9",1,"^:",null,"^;",95],"~$alias-sym",["^ ","^;",85,"^9",15,"^6","^K","^4","^5","^:",null],"~$name",["^ ","^;",207,"^9",3,"^6","^L","^4","^5","^:",null,"^7",["^2",[["^ ","~:call",["^ ","~:filename","/Users/yogthos/src/jolt/src/jolt/evaluator.janet","^7","^M","~:lang","~:clj","~:base-lang","^P","~:resolved-ns","^5","^4","^5","^6","^B","~:arity",1],"^;",208,"^9",15,"~:end-row",208,"~:end-col",33],["^ ","~:tag","^H","^;",209,"^9",15,"^T",209,"^U",18]]]],"^N","/Users/yogthos/src/jolt/src/jolt/evaluator.janet","~$macro-fn",["^ ","^1",["^2",[]],"^4","^5","^6","^W","^7","^8","^9",18,"^:",null,"^@",0,"^;",243],"~$val",["^ ","^;",356,"^9",21,"^6","^X","^4","^5","^:",null],"~$parse-arg-names",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^Y","^7","^8","^9",1,"^:",null,"^F",["^ ","~i1",["^ ","^G",["^ ","^7","~:map","~:val",["^ ","~:fixed",["^ ","^;",189,"^T",589,"^9",11,"^U",13]]],"^I","[args-form]"]],"^;",143],"~$special-symbol?",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^11","^7","^8","^9",1,"^:",null,"^F",["^ ","~i1",["^ ","^G",["^2",["~:boolean"]],"^I","[name]"]],"^;",10],"~$sym-name?",["^ ","^1",["^2",[2]],"^3",true,"^4","^5","^6","^13","^7","^8","^9",1,"^:",null,"^;",6],"~$inner",["^ ","^;",167,"^9",17,"^6","^14","^4","^5","^:",null],"~$sym-name-str",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^15","^7","^8","^9",1,"^:",null,"^;",70],"~$unwrap-meta-name",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^16","^7","^8","^9",1,"^:",null,"^;",192]] \ No newline at end of file +["^ ","~$core-map",["^ ","~:row",255,"~:col",1,"~:varargs-min-arity",1,"~:name","^0","~:ns","~$user","~:top-ns",null,"~:type","~:fn"],"~$core-not",["^ ","^1",11,"^2",1,"~:fixed-arities",["~#set",[1]],"^4","^:","^5","^6","^7",null,"~:arities",["^ ","~i1",["^ ","~:ret",["^<",["~:boolean"]],"~:arglist-str","[x]"]],"^8","^9"],"~$core-quot",["^ ","^1",68,"^2",1,"^;",["^<",[2]],"^4","^A","^5","^6","^7",null,"^8","^9"],"~$core-sub",["^ ","^;",["^<",[]],"^5","^6","^4","^B","^8","^9","^2",1,"^7",null,"^3",0,"^1",49],"~$core-not=",["^ ","^1",88,"^2",1,"^3",0,"^4","^C","^5","^6","^7",null,"^=",["^ ","~:varargs",["^ ","^>","^?","~:min-arity",0,"^@","[& args]"]],"^8","^9"],"~$core-inc",["^ ","^1",64,"^2",1,"^4","^F","^5","^6","^7",null],"~$core-rem",["^ ","^1",67,"^2",1,"^4","^G","^5","^6","^7",null],"~$core-false?",["^ ","^1",24,"^2",1,"^;",["^<",[1]],"^4","^H","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>","^?","^@","[x]"]],"^8","^9"],"~$core-=",["^ ","^1",77,"^2",1,"^3",0,"^4","^I","^5","^6","^7",null,"^8","^9"],"~$core-zipmap",["^ ","^1",243,"^2",1,"^;",["^<",[2]],"^4","^J","^5","^6","^7",null,"^8","^9"],"~$core-dec",["^ ","^1",65,"^2",1,"^4","^K","^5","^6","^7",null],"~$core-zero?",["^ ","^1",27,"^2",1,"^;",["^<",[1]],"^4","^L","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>",["^<",["~:nil","^?"]],"^@","[x]"]],"^8","^9"],"~$core-contains?",["^ ","^1",158,"^2",1,"^;",["^<",[2]],"^4","^N","^5","^6","^7",null,"^=",["^ ","~i2",["^ ","^>",["^<",["^M","^?"]],"^@","[coll key]"]],"^8","^9"],"~$core-every?",["^ ","^1",38,"^2",1,"^;",["^<",[2]],"^4","^O","^5","^6","^7",null,"^8","^9"],"~$core-mod",["^ ","^1",66,"^2",1,"^4","^P","^5","^6","^7",null],"~$core-neg?",["^ ","^1",29,"^2",1,"^;",["^<",[1]],"^4","^Q","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>",["^<",["^M","^?"]],"^@","[x]"]],"^8","^9"],"~$core-vector?",["^ ","^1",18,"^2",1,"^;",["^<",[1]],"^4","^R","^5","^6","^7",null,"^8","^9"],"~$core-coll?",["^ ","^1",21,"^2",1,"^;",["^<",[1]],"^4","^S","^5","^6","^7",null,"^8","^9"],"~$core-count",["^ ","^1",165,"^2",1,"^4","^T","^5","^6","^7",null],"~$core-empty?",["^ ","^1",33,"^2",1,"^;",["^<",[1]],"^4","^U","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>",["^<",["^?"]],"^@","[coll]"]],"^8","^9"],"~$core-max",["^ ","^1",70,"^2",1,"^3",0,"^4","^V","^5","^6","^7",null,"^8","^9"],"~$core-identical?",["^ ","^1",25,"^2",1,"^;",["^<",[2]],"^4","^W","^5","^6","^7",null,"^=",["^ ","~i2",["^ ","^>","^?","^@","[a b]"]],"^8","^9"],"~$core-number?",["^ ","^1",14,"^2",1,"^;",["^<",[1]],"^4","^X","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>","^?","^@","[x]"]],"^8","^9"],"~$core-odd?",["^ ","^1",31,"^2",1,"^;",["^<",[1]],"^4","^Y","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>","^?","^@","[n]"]],"^8","^9"],"~$core-pos?",["^ ","^1",28,"^2",1,"^;",["^<",[1]],"^4","^Z","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>",["^<",["^M","^?"]],"^@","[x]"]],"^8","^9"],"~$core-even?",["^ ","^1",30,"^2",1,"^;",["^<",[1]],"^4","^[","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>","^?","^@","[n]"]],"^8","^9"],"~$core-get-in",["^ ","^1",148,"^2",1,"^;",["^<",[4]],"^4","^10","^5","^6","^7",null,"^8","^9"],"~$core-conj",["^ ","^1",94,"^2",1,"^3",1,"^4","^11","^5","^6","^7",null,"^8","^9"],"~$core-min",["^ ","^1",71,"^2",1,"^3",0,"^4","^12","^5","^6","^7",null,"^8","^9"],"~:filename","/Users/yogthos/src/jolt/src/jolt/core.janet","~$core-map?",["^ ","^1",19,"^2",1,"^;",["^<",[1]],"^4","^14","^5","^6","^7",null,"^8","^9"],"~$core-keys",["^ ","^1",230,"^2",1,"^;",["^<",[1]],"^4","^15","^5","^6","^7",null,"^8","^9"],"~$core-symbol?",["^ ","^1",17,"^2",1,"^;",["^<",[1]],"^4","^16","^5","^6","^7",null,"^8","^9"],"~$core-merge",["^ ","^1",216,"^2",1,"^3",0,"^4","^17","^5","^6","^7",null,"^8","^9"],"~$core-assoc",["^ ","^1",117,"^2",1,"^3",1,"^4","^18","^5","^6","^7",null,"^8","^9"],"~$core-dissoc",["^ ","^1",129,"^2",1,"^3",1,"^4","^19","^5","^6","^7",null,"^8","^9"],"~$core-+",["^ ","^;",["^<",[]],"^5","^6","^4","^1:","^8","^9","^2",1,"^7",null,"^=",["^ ","^D",["^ ","^>",["^<",["~:number","~:nat-int"]],"^E",0,"^@","[& args]"]],"^3",0,"^1",47],"~$core-cons",["^ ","^1",182,"^2",1,"^;",["^<",[2]],"^4","^1=","^5","^6","^7",null,"^8","^9"],"~$core-true?",["^ ","^1",23,"^2",1,"^;",["^<",[1]],"^4","^1>","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>","^?","^@","[x]"]],"^8","^9"],"~$core-into",["^ ","^1",203,"^2",1,"^;",["^<",[2]],"^4","^1?","^5","^6","^7",null,"^8","^9"],"~$core-seq?",["^ ","^1",20,"^2",1,"^;",["^<",[1]],"^4","^1@","^5","^6","^7",null,"^8","^9"],"~$core-*",["^ ","^;",["^<",[]],"^5","^6","^4","^1A","^8","^9","^2",1,"^7",null,"^=",["^ ","^D",["^ ","^>",["^<",["^1;","~:pos-int"]],"^E",0,"^@","[& args]"]],"^3",0,"^1",55],"~$core-some?",["^ ","^1",12,"^2",1,"^;",["^<",[1]],"^4","^1C","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>","^?","^@","[x]"]],"^8","^9"],"~$core-nil?",["^ ","^1",10,"^2",1,"^;",["^<",[1]],"^4","^1D","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>","^?","^@","[x]"]],"^8","^9"],"~$core-select-keys",["^ ","^1",236,"^2",1,"^;",["^<",[2]],"^4","^1E","^5","^6","^7",null,"^8","^9"],"~$core-merge-with",["^ ","^1",222,"^2",1,"^3",1,"^4","^1F","^5","^6","^7",null,"^8","^9"],"~$core-rest",["^ ","^1",171,"^2",1,"^;",["^<",[1]],"^4","^1G","^5","^6","^7",null,"^8","^9"],"~$core-keyword?",["^ ","^1",16,"^2",1,"^;",["^<",[1]],"^4","^1H","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>","^?","^@","[x]"]],"^8","^9"],"~$core-first",["^ ","^1",167,"^2",1,"^;",["^<",[1]],"^4","^1I","^5","^6","^7",null,"^8","^9"],"~$core-seq",["^ ","^1",189,"^2",1,"^;",["^<",[1]],"^4","^1J","^5","^6","^7",null,"^8","^9"],"~$core-get",["^ ","^1",138,"^2",1,"^;",["^<",[4]],"^4","^1K","^5","^6","^7",null,"^8","^9"],"~$core-fn?",["^ ","^1",15,"^2",1,"^;",["^<",[1]],"^4","^1L","^5","^6","^7",null,"^8","^9"],"~$core-next",["^ ","^1",178,"^2",1,"^;",["^<",[1]],"^4","^1M","^5","^6","^7",null,"^8","^9"],"~$core-vals",["^ ","^1",233,"^2",1,"^;",["^<",[1]],"^4","^1N","^5","^6","^7",null,"^8","^9"],"~$core-vec",["^ ","^1",197,"^2",1,"^;",["^<",[1]],"^4","^1O","^5","^6","^7",null,"^8","^9"],"~$core-string?",["^ ","^1",13,"^2",1,"^;",["^<",[1]],"^4","^1P","^5","^6","^7",null,"^=",["^ ","~i1",["^ ","^>","^?","^@","[x]"]],"^8","^9"]] \ No newline at end of file diff --git a/.dirge/memory/MEMORY.md b/.dirge/memory/MEMORY.md index 64e0739..a615efa 100644 --- a/.dirge/memory/MEMORY.md +++ b/.dirge/memory/MEMORY.md @@ -1,9 +1,13 @@ -`defmacro` supports optional docstring: `(defmacro name [args] body)` or `(defmacro name \"doc\" [args] body)`. Implementation: slice rest-form from form index 2, check if first is string, adjust args-form and body start accordingly. Implicit `&env` binding injected as `(put new-bindings "&env" @{'ns nil})` — must use `@{}` not `(struct …)` because nil values get dropped from structs. `fn*` uses same `parse-arg-names` helper for `& rest` args. -§ -Jolt bootstrap: `core-bindings` (def- map) maps symbol strings → Janet functions. `init-core!` interns them into clojure.core ns. Macros (when, defn, declare) are registered in `core-macro-names` which returns `@{"when" true "defn" true "declare" true}`. `init-core!` checks this table and sets `:macro true` on the var for macro bindings. Order matters: `core-when` and `core-defn` must be defined BEFORE `core-bindings` map literal references them, otherwise compile error. -§ -Reader comments (`;`) return `{:jolt/type :jolt/skip}` sentinel; `parse-next`/`parse-string` skip past it. Closing `)`, `]`, `}` produce explicit "Unmatched" errors. `unwrap-meta-name` recursively unwraps `(with-meta sym meta)` to raw symbol — used in def, ns, deftype, defmethod. `resolve-sym` does class-name lookup: `Foo.Bar.Baz` → ns "Foo.Bar" name "Baz". Janet `(set [a b] tuple)` doesn't work — use explicit indexing. `comment` must be a macro (registered in core-macro-names) to avoid evaluating body. -§ Sci bootstrap order: macros(4/4)→protocols(15/17)→utils(39/47)→types(22/27)→unrestrict(2/2)→vars(28/28)→lang(10/10)→ctx-store(6/6)→namespaces(93/98)→core(60/69). All .cljc; #?(:clj) resolved at read time, #?(:cljs)→nil. § Janet `last` works only on indexed types (tuple, array). On strings it returns nil. Use `(s (- (length s) 1))` to get the last character of a string, or `(string/slice s (- (length s) 1))` for the last char as string. `(last "hello")` → nil, not `\o`. +§ +resolve-sym returns `:jolt/not-found` sentinel to distinguish nil local bindings from absent ones, preventing accidental fallthrough to global resolution. Falls back to `clojure.core` namespace for unqualified symbols. `bind-put` helper wraps nil as `:jolt/nil`; resolve-sym unwraps. Multi-arity `fn*` dispatches on fixed-params count vs variadic args. +§ +Janet `(string :keyword)` returns `"keyword"` (without colon). Use this for keyword→string conversion in destructuring code. Avoid `(name :keyword)` in Jolt evaluation context — `name` is a Janet built-in but may not be available in all eval contexts. +§ +`:jolt/nil-sentinel` used in `core-bindings` map for vars that should have nil root values. Janet table literals drop nil entries: `@{"*1" nil}` → empty table. `init-core!` unwraps sentinels back to nil with `(if (= fn :jolt/nil-sentinel) nil fn)`. Applies to `*1`, `*2`, `*3`, `*e` and potentially other nil-rooted vars. +§ +`fn*` and `defmacro` now capture `defining-ns` at definition time and restore it via `(ctx-set-current-ns ctx defining-ns)` / `(ctx-set-current-ns ctx saved-ns)` around body evaluation. This ensures symbols in function/macro bodies resolve in the defining namespace, not the calling context. Applies to both multi-arity and single-arity `fn*` forms. +§ +SCI load order: `macros` → `protocols` → `types` → `unrestrict` → `vars` → `lang` → `utils` → `namespaces` → `core`. Utils must load after lang (needs `lang/->Namespace`, `vars/unqualify-symbol`). Namespaces needs utils (for `clojure-core-ns`, `dynamic-var`, `unqualify-symbol`). Core needs namespaces (for `*1`, `*2`, `*3`, `*e`, `resolve`). diff --git a/.dirge/memory/PITFALLS.md b/.dirge/memory/PITFALLS.md index 9bcebeb..00bf939 100644 --- a/.dirge/memory/PITFALLS.md +++ b/.dirge/memory/PITFALLS.md @@ -1,3 +1,5 @@ Janet `apply` requires a function/cfunction. When tables/structs are used as lookup maps (like deftype fields, multimethod dispatch tables), they get called via `(get f key)` not `(apply f args)`. The evaluator's default call path checks `(function? f)` before `apply`, falling back to `(get f (first args))` for single-arg table/struct calls. § Janet structs silently omit entries with nil values: `(struct ;[:x nil :y 1])` → `{:y 1}`. The `:x` key is completely dropped from the struct, not just set to nil. Use `@{}` (mutable table) when map needs nil-valued entries. This caused the `&env` implicit binding to fail — `(put new-bindings "&env" {'ns nil})` created a struct that became empty `{}`, so `(:ns &env)` failed with unknown method. Fix: use `@{}` table for bindings that may contain nil values, or use a non-nil sentinel. +§ +Janet `(put table key nil)` silently drops the key — it's a no-op, not a way to store nil. This is SEPARATE from struct-nil-drop: even mutable `@{}` tables drop nil values on `put`. The `bind-put` helper in evaluator.janet stores nil as `:jolt/nil` sentinel; `resolve-sym` unwraps it back to `nil`. All binding `put` calls in `fn*`, `let*`, `loop*`, macro bodies, and `deftype` reify MUST use `bind-put`, not raw `put`. diff --git a/.dirge/skills/.usage.json b/.dirge/skills/.usage.json index 8b1e5f4..3ead270 100644 --- a/.dirge/skills/.usage.json +++ b/.dirge/skills/.usage.json @@ -2,35 +2,35 @@ "jpm-build": { "created_by": "agent", "use_count": 0, - "view_count": 9, + "view_count": 10, "patch_count": 0, - "last_viewed_at": "2026-06-02T02:26:40.592223+00:00", + "last_viewed_at": "2026-06-02T03:42:37.626504+00:00", "created_at": "2026-06-01T20:56:39.144222+00:00", "state": "active", "pinned": false }, - "jolt-bootstrap": { - "created_by": "agent", - "use_count": 6, - "view_count": 15, - "patch_count": 9, - "last_used_at": "2026-06-02T03:25:24.143420+00:00", - "last_viewed_at": "2026-06-02T03:25:24.137076+00:00", - "last_patched_at": "2026-06-02T03:25:52.625469+00:00", - "created_at": "2026-06-01T21:49:51.101718+00:00", - "state": "active", - "pinned": false - }, "jolt-dev": { "created_by": "agent", - "use_count": 6, - "view_count": 15, - "patch_count": 14, - "last_used_at": "2026-06-02T03:23:57.409721+00:00", - "last_viewed_at": "2026-06-02T03:23:57.400994+00:00", - "last_patched_at": "2026-06-02T03:24:21.695870+00:00", + "use_count": 9, + "view_count": 19, + "patch_count": 20, + "last_used_at": "2026-06-02T04:06:33.131408+00:00", + "last_viewed_at": "2026-06-02T04:06:33.124233+00:00", + "last_patched_at": "2026-06-02T04:07:28.365921+00:00", "created_at": "2026-06-01T21:26:06.614465+00:00", "state": "active", "pinned": false + }, + "jolt-bootstrap": { + "created_by": "agent", + "use_count": 8, + "view_count": 18, + "patch_count": 10, + "last_used_at": "2026-06-02T03:43:48.038435+00:00", + "last_viewed_at": "2026-06-02T03:43:48.028512+00:00", + "last_patched_at": "2026-06-02T03:44:45.935676+00:00", + "created_at": "2026-06-01T21:49:51.101718+00:00", + "state": "active", + "pinned": false } } \ No newline at end of file diff --git a/.dirge/skills/jolt-bootstrap/SKILL.md b/.dirge/skills/jolt-bootstrap/SKILL.md index 12b604a..4f7479e 100644 --- a/.dirge/skills/jolt-bootstrap/SKILL.md +++ b/.dirge/skills/jolt-bootstrap/SKILL.md @@ -1,4 +1,5 @@ # jolt-bootstrap +# jolt-bootstrap TDD workflow for bootstrapping a Clojure interpreter on Janet @@ -81,8 +82,19 @@ Used in `def`, `ns`, `deftype`, `defmethod` to handle metadata-wrapped names. ### deftype →TypeName constructor `deftype` interns both `TypeName` and `->TypeName` (Clojure arrow constructor convention). +### bind-put helper and :jolt/nil sentinel +Janet's `(put table key nil)` silently drops the key, even on mutable `@{}` tables. +`bind-put` stores nil as `:jolt/nil` sentinel; `resolve-sym` unwraps `:jolt/nil` back to `nil`. +Must be used for ALL binding `put` calls: `fn*`, `let*`, `loop*`, macro bodies, `deftype` reify. + +### resolve-sym sentinels +- `:jolt/not-found` — returned when a symbol is truly absent (distinct from nil binding) +- `:jolt/nil` → unwrapped to actual `nil` — nil values in bindings stored as sentinel due to Janet `put` nil-drop +- Auto-refer fallback: unqualified symbols not found in current ns fall back to `clojure.core` + ## Pitfalls - Janet `let` can't bind to nil; use `(var x nil)` then `(set x val)` +- Janet `(put table key nil)` silently drops the key — use `bind-put` helper for all binding tables - `(get table)` with 1 arg = compile error, use `(table :key)` shorthand - `(put fn :key val)` fails on functions; stash metadata on vars instead - `deftype` field names must be keywords (not strings) for `(inst :field)` access diff --git a/.dirge/skills/jolt-dev/SKILL.md b/.dirge/skills/jolt-dev/SKILL.md index b69f153..fd80c56 100644 --- a/.dirge/skills/jolt-dev/SKILL.md +++ b/.dirge/skills/jolt-dev/SKILL.md @@ -60,6 +60,8 @@ The match arm receives `ctx`, `bindings`, and `form` (the full list). Use `(in f - Functions are not tables — `(put fn :prop val)` fails. Stash properties on vars - `match` is Janet's pattern matching — no `case` or `cond` needed for simple dispatch - Janet structs silently **drop entries with nil values**: `(struct ;[:x nil :y 1])` → `{:y 1}`. Use `@{}` mutable tables when nil-valued entries are needed (e.g., `&env` binding `@{}` for macro bodies) +- Janet `(put table key nil)` **silently drops the key** — even on mutable `@{}` tables. Use `bind-put` helper (stores `:jolt/nil` sentinel) for binding tables; `resolve-sym` unwraps `:jolt/nil` back to `nil`. All `put` calls on bindings in `fn*`, `let*`, `loop*`, macro bodies, `deftype` reify MUST use `bind-put` +- `resolve-sym` returns `:jolt/not-found` sentinel to distinguish nil local bindings from absent ones; falls back to `clojure.core` for unqualified symbols - `match` with string patterns returns **nil** (not error) when no pattern matches. Used in `eval-list` to handle non-symbol heads cleanly — keyword heads like `:ns` fall through to default function application - `break` in `while` does NOT return a value in Janet. Use `(var done nil)` + `(set done val)` + check pattern instead - Janet `#{}` set literals can cause parse issues in some contexts — use `@[]` as fallback @@ -68,6 +70,9 @@ The match arm receives `ctx`, `bindings`, and `form` (the full list). Use `(in f - `(length struct)` counts key-value pairs, not keys. Use `(length (keys struct))` for key count - **`(last string)` returns nil** — `last` works only on indexed types (tuple, array). For strings use `(s (- (length s) 1))` or `(string/slice s (- (length s) 1))` - **`(set [a b] tuple)` doesn't work** — Janet's `set` doesn't support destructuring. Use `(tuple 0)` / `(tuple 1)` or explicit individual assignments +- **`(string :keyword)` returns `"keyword"` (no colon)** — use this for keyword→string conversion. Avoid `(name :keyword)` in Jolt eval context; `name` is a Janet built-in but may shadow or be unavailable +- **`(get struct :key)` works, `(in struct :key)` fails** — `in` is for indexed types only (tuple, array, string, buffer). Use `get` for structs/tables +- **Janet `try` takes exactly 2 args**: `(try body ([err] handler))`. Multi-expression body must wrap in `do`: `(try (do expr1 true) ([err] handler))` ### unwrap-meta-name helper Recursively unwraps `(with-meta sym meta)` forms to extract the underlying symbol. Used in `def`, `ns`, `deftype`, `defmethod` to handle metadata-wrapped names: @@ -144,7 +149,7 @@ This resolves symbols like `IVar` that are interned in `sci.impl.vars` but refer - `#_` — discard reader macro, reads next form and returns it as position only ### Core macros (in core.janet) -- `core-macro-names` returns `@{"when" true "defn" true "declare" true "defprotocol" true "extend-type" true "extend-protocol" true "extend" true "reify" true "fn" true "proxy" true "definterface" true "comment" true "defn-" true}` — a table +- `core-macro-names` returns `@{"when" true "when-not" true "if-let" true "when-let" true "if-some" true "when-some" true "defn" true "defn-" true "declare" true "fn" true "let" true "defprotocol" true "extend-type" true "extend-protocol" true "extend" true "reify" true "proxy" true "definterface" true "comment" true}` — a table - `init-core!` calls `(get (core-macro-names) name)` to check, then `(put v :macro true)` - Order matters: macro functions must be defined BEFORE `core-bindings` map references them @@ -154,6 +159,59 @@ This resolves symbols like `IVar` that are interned in `sci.impl.vars` but refer - `core-volatile!`, `core-vswap!`, `core-vreset!` — volatile (atom-like table with :val key) - `core-defprotocol` emits `(do (def name @{}) (def method fn) ...)` — macro, returns do form - `core-extend-type`, `core-extend-protocol`, `core-extend`, `core-reify`, `core-satisfies?`, `core-extends?`, `core-implements?`, `core-type->str` — protocol stubs +- `core-copy-core-var`, `core-copy-var`, `core-macrofy`, `core-new-var`, `core-avoid-method-too-large` — sci.impl.copy-vars stubs (needed by namespaces) +- `core-resolve` — stub returning nil (sci symbol resolution check) +- `core-update` — delegates to Janet's `update` with apply +- `core-prefer-method` — multimethod preference ordering stub + +### `:jolt/nil-sentinel` pattern for nil bindings +Janet table literals drop entries with nil values: `@{"x" nil}` → `{}`. For core bindings that need nil root values (e.g., `*1`, `*2`, `*3`, `*e`), use the sentinel: +```janet +"*1" :jolt/nil-sentinel +"*2" :jolt/nil-sentinel +``` +`init-core!` unwraps sentinels back to nil: +```janet +(def v (ns-intern ns name (if (= fn :jolt/nil-sentinel) nil fn))) +``` + +### `fn*` namespace capture +Functions close over their defining namespace so symbols resolve correctly regardless of caller's current-ns: +```janet +"fn*" (let [args-form ... + defining-ns (ctx-current-ns ctx)] ; capture at definition time + (set self (fn [& fn-args] + (def saved-ns (ctx-current-ns ctx)) + (ctx-set-current-ns ctx defining-ns) ; restore defining ns + (var result nil) + (each body-form body + (set result (eval-form ctx fn-bindings body-form))) + (ctx-set-current-ns ctx saved-ns) ; restore caller's ns + result))) +``` +Same pattern in `defmacro` bodies. + +### `let*` destructuring +Supports `:keys` destructuring with both symbol keys and keyword keys: +```janet +(if (struct? pat) + (let [keys-vec (get pat :keys)] + (if (and keys-vec (indexed? keys-vec)) + (each k keys-vec + (def kname (if (keyword? k) (string k) (k :name))) ; keyword→string, symbol→name + (bind-put new-bindings kname (get val (keyword kname)))) + (bind-put new-bindings (pat :name) val))) ; plain symbol binding + ... +``` + +Also supports sequential destructuring of vector patterns. + +### New core macros +- `core-if-let` — expands `(if-let [x expr] then else)` → `(let* [x expr] (if x then else))` +- `core-when-let` — expands to `(let* [x expr] (when x body))` +- `core-if-some` — expands to `(let* [x expr] (if (some? x) then else))` +- `core-when-some` — expands to `(let* [x expr] (when (some? x) body))` +- `core-let` — expands `(let [bindings] body)` → `(let* [bindings] body)` (macro, like `fn` → `fn*`) ### Namespace handling - `ns` form handles `^:meta` on ns name via `with-meta` pattern @@ -161,21 +219,22 @@ This resolves symbols like `IVar` that are interned in `sci.impl.vars` but refer - `require` clause in `ns` wraps each spec in `(when s ...)` for nil-safety - `resolve-var` falls back to checking clojure.core namespace if var not found in current ns -### Bootstrap loading order +### Bootstrap loading order (corrected) ``` sci.impl.macros (4/4 ok) -sci.impl.protocols (15/17 ok) -sci.impl.utils (39/47 ok) -sci.impl.types (22/27 ok) +sci.impl.protocols (15/15 ok) +sci.impl.types (22/22 ok) sci.impl.unrestrict (2/2 ok) -sci.impl.vars (28/28 ok — comment block parsed as skip) -sci.lang (10/10 ok — IVar via class-name resolve) -sci.ctx-store (6/6 ok) -sci.impl.namespaces (93/98 ok — parse crash at unmatched brace) -sci.core (60/69 ok — namespaces/*1/*2/*3/*e fail) +sci.impl.vars (28/28 ok) +sci.lang (10/10 ok) +sci.impl.utils (45/46 ok — 1 fail on dynamic-var internals) +sci.impl.namespaces (124/127 ok — 3 remaining: major destructure, clojure-version, iterable) +sci.core (64/65 ok — 1 remaining form) ``` All .cljc files. #?(:clj ...) resolved at read time. #?(:cljs ...) returns nil. +**Critical load order**: `macros` → `protocols` → `types` → `unrestrict` → `vars` → `lang` → `utils` → `namespaces` → `core`. `utils` must load after `lang` (needs `lang/->Namespace`, `vars/unqualify-symbol`). `namespaces` needs `utils` (for `clojure-core-ns`, `dynamic-var`). `core` needs `namespaces` (for `*1`, `*2`, `*3`, `*e`, `resolve`). + ### parse-arg-names - Handles `& rest` args AND nested destructuring vectors - When an arg is a vector (not a symbol), recurses to extract nested symbol names diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 1b028a6..24fbab0 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -617,6 +617,52 @@ @[{:jolt/type :symbol :ns nil :name "if"} not-form @[{:jolt/type :symbol :ns nil :name "do"} ;body]]) +(defn core-if-let + "Macro: (if-let [binding val-expr] then else?)" + [bindings then-form & else-forms] + (def form-sym (in bindings 0)) + (def val-form (in bindings 1)) + @[{:jolt/type :symbol :ns nil :name "let*"} + @[form-sym val-form] + @[{:jolt/type :symbol :ns nil :name "if"} + form-sym + then-form + ;else-forms]]) + +(defn core-when-let + "Macro: (when-let [binding val-expr] & body)" + [bindings & body] + (def form-sym (in bindings 0)) + (def val-form (in bindings 1)) + @[{:jolt/type :symbol :ns nil :name "let*"} + @[form-sym val-form] + @[{:jolt/type :symbol :ns nil :name "when"} + form-sym + ;body]]) + +(defn core-if-some + "Macro: (if-some [binding val-expr] then else?)" + [bindings then-form & else-forms] + (def form-sym (in bindings 0)) + (def val-form (in bindings 1)) + @[{:jolt/type :symbol :ns nil :name "let*"} + @[form-sym val-form] + @[{:jolt/type :symbol :ns nil :name "if"} + @[{:jolt/type :symbol :ns nil :name "some?"} form-sym] + then-form + ;else-forms]]) + +(defn core-when-some + "Macro: (when-some [binding val-expr] & body)" + [bindings & body] + (def form-sym (in bindings 0)) + (def val-form (in bindings 1)) + @[{:jolt/type :symbol :ns nil :name "let*"} + @[form-sym val-form] + @[{:jolt/type :symbol :ns nil :name "when"} + @[{:jolt/type :symbol :ns nil :name "some?"} form-sym] + ;body]]) + (defn core-defn "Macro: (defn name [args] body) -> (def name (fn* [args] body))" [fn-name args-form & body] @@ -670,6 +716,22 @@ (defn core-prefer-method [multifn dispatch-val & dispatch-vals] nil) +# resolve stub — returns nil (symbols not found in Jolt's clojure.core) +(defn core-resolve [sym] nil) + +# update — works on both structs and tables +(defn core-update [m k f & args] + (let [current (get m k) + new-val (apply f current args)] + (put m k new-val))) + +# copy-var stubs for sci.impl.copy-vars (used by sci.impl.namespaces) +(defn core-copy-core-var [sym] nil) +(defn core-copy-var [sym & args] nil) +(defn core-macrofy [sym fn] fn) +(defn core-new-var [sym & args] nil) +(defn core-avoid-method-too-large [& args] @{}) + # declare macro — accepts symbols, does nothing (forward declaration) (defn core-declare [& syms] @[{:jolt/type :symbol :ns nil :name "do"}]) @@ -682,6 +744,15 @@ (each a args (array/push result a)) result) +(defn core-let + "Macro: (let [bindings] body) → (let* [bindings] body)" + [bindings & body] + (def result @[]) + (array/push result {:jolt/type :symbol :ns nil :name "let*"}) + (array/push result bindings) + (each b body (array/push result b)) + result) + # Protocol stubs — defined in sci.impl.protocols, needed in clojure.core # defprotocol must be a macro to avoid evaluating its args (defn core-defprotocol [protocol-name & sigs] @@ -815,6 +886,10 @@ "not" core-not "when" core-when "when-not" core-when-not + "if-let" core-if-let + "when-let" core-when-let + "if-some" core-if-some + "when-some" core-when-some "defn" core-defn "defn-" core-defn- "derive" core-derive @@ -824,6 +899,7 @@ "Object" core-Object "declare" core-declare "fn" core-fn + "let" core-let "defprotocol" core-defprotocol "extend-type" core-extend-type "extend-protocol" core-extend-protocol @@ -843,31 +919,43 @@ "definterface" core-definterface "comment" core-comment "prefer-method" core-prefer-method + "resolve" core-resolve + "update" core-update + "copy-core-var" core-copy-core-var + "copy-var" core-copy-var + "macrofy" core-macrofy + "new-var" core-new-var + "avoid-method-too-large" core-avoid-method-too-large "qualified-symbol?" core-qualified-symbol? "meta" core-meta # Dynamic vars — stubs for SCI bootstrap "*unchecked-math*" false - "*clojure-version*" {:major 1 :minor 11 :incremental 0 :qualifier nil}}) + "*clojure-version*" @{:major 1 :minor 11 :incremental 0 :qualifier nil} + "*1" :jolt/nil-sentinel + "*2" :jolt/nil-sentinel + "*3" :jolt/nil-sentinel + "*e" :jolt/nil-sentinel + "*assert" true}) (defn core-macro-names "Set of core binding names that are macros." [] - @{"when" true "when-not" true "defn" true "defn-" true "declare" true "defprotocol" true "extend-type" true "extend-protocol" true "extend" true "reify" true "fn" true "proxy" true "definterface" true "comment" true}) + @{"when" true "when-not" true "if-let" true "when-let" true "if-some" true "when-some" true "defn" true "defn-" true "declare" true "fn" true "let" true "defprotocol" true "extend-type" true "extend-protocol" true "extend" true "reify" true "proxy" true "definterface" true "comment" true}) (def init-core! (fn [& args] (case (length args) 1 (let [ctx (args 0) - ns (ctx-find-ns ctx "clojure.core")] - (loop [[name fn] :pairs core-bindings] - (def v (ns-intern ns name fn)) - (when (get (core-macro-names) name) - (put v :macro true))) - ns) - 2 (let [ctx (args 0) ns-name (args 1) - ns (ctx-find-ns ctx ns-name)] - (loop [[name fn] :pairs core-bindings] - (def v (ns-intern ns name fn)) + ns (ctx-find-ns ctx "clojure.core")] + (loop [[name fn] :pairs core-bindings] + (def v (ns-intern ns name (if (= fn :jolt/nil-sentinel) nil fn))) + (when (get (core-macro-names) name) + (put v :macro true))) + ns) + 2 (let [ctx (args 0) ns-name (args 1) + ns (ctx-find-ns ctx ns-name)] + (loop [[name fn] :pairs core-bindings] + (def v (ns-intern ns name (if (= fn :jolt/nil-sentinel) nil fn))) (when (get (core-macro-names) name) (put v :macro true))) ns) diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index 6242aa7..7439215 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -239,22 +239,27 @@ body (tuple/slice rest-form (if has-doc? 2 1)) arg-info (parse-arg-names args-form) fixed-names (arg-info :fixed) - rest-name (arg-info :rest)] + rest-name (arg-info :rest) + defining-ns (ctx-current-ns ctx)] (def macro-fn (fn [& macro-args] (var new-bindings @{}) (table/setproto new-bindings bindings) - (put new-bindings "&env" @{}) # implicit &env for macro bodies (table — nil-safe) + (put new-bindings "&env" @{}) # implicit &env for macro bodies (table — nil-safe) (var i 0) (each a fixed-names (bind-put new-bindings a (macro-args i)) (++ i)) (when rest-name (put new-bindings rest-name (tuple/slice macro-args i))) + # Use defining namespace for symbol resolution + (def saved-ns (ctx-current-ns ctx)) + (ctx-set-current-ns ctx defining-ns) (var result nil) (each bf body (set result (eval-form ctx new-bindings bf))) + (ctx-set-current-ns ctx saved-ns) result)) - (let [ns-name (ctx-current-ns ctx) + (let [ns-name (ctx-current-ns ctx) ns (ctx-find-ns ctx ns-name)] (def v (ns-intern ns (name-sym :name) macro-fn)) (put v :macro true) @@ -290,60 +295,70 @@ (ctx-find-ns ctx ns-name) nil) "fn*" (if (array? (in form 1)) - # Multi-arity: (fn* ([args] body...) ([args] body...)...) - (let [pairs (tuple/slice form 1) - arities @{}] - (var self nil) - (each pair pairs - (let [args-form (in pair 0) - body (tuple/slice pair 1) - arg-info (parse-arg-names args-form) - fixed-names (arg-info :fixed) - rest-name (arg-info :rest) - n-fixed (length fixed-names)] - (put arities n-fixed - (fn [& fn-args] - (var fn-bindings @{}) - (table/setproto fn-bindings bindings) - (var i 0) - (each arg-name fixed-names - (bind-put fn-bindings arg-name (fn-args i)) - (++ i)) - (when rest-name - (put fn-bindings rest-name (tuple/slice fn-args i))) - (put fn-bindings :jolt/loop-fn self) - (var result nil) - (each body-form body - (set result (eval-form ctx fn-bindings body-form))) - result)))) - (set self (fn [& fn-args] - (let [n (length fn-args) - f (get arities n)] - (if f - (apply f fn-args) - (error (string "Wrong number of args (" n ") passed to fn")))))) - self) - # Single-arity: (fn* [args] body...) - (let [args-form (in form 1) - body (tuple/slice form 2) - arg-info (parse-arg-names args-form) - fixed-names (arg-info :fixed) - rest-name (arg-info :rest)] - (var self nil) - (set self (fn [& fn-args] - (var fn-bindings @{}) - (table/setproto fn-bindings bindings) - (var i 0) - (each arg-name fixed-names - (bind-put fn-bindings arg-name (fn-args i)) - (++ i)) - (when rest-name - (put fn-bindings rest-name (tuple/slice fn-args i))) - (put fn-bindings :jolt/loop-fn self) - (var result nil) - (each body-form body - (set result (eval-form ctx fn-bindings body-form))) - result)) + # Multi-arity: (fn* ([args] body...) ([args] body...)...) + (let [pairs (tuple/slice form 1) + arities @{} + defining-ns (ctx-current-ns ctx)] + (var self nil) + (each pair pairs + (let [args-form (in pair 0) + body (tuple/slice pair 1) + arg-info (parse-arg-names args-form) + fixed-names (arg-info :fixed) + rest-name (arg-info :rest) + n-fixed (length fixed-names)] + (put arities n-fixed + (fn [& fn-args] + (var fn-bindings @{}) + (table/setproto fn-bindings bindings) + (var i 0) + (each arg-name fixed-names + (bind-put fn-bindings arg-name (fn-args i)) + (++ i)) + (when rest-name + (put fn-bindings rest-name (tuple/slice fn-args i))) + (put fn-bindings :jolt/loop-fn self) + # Use defining namespace for symbol resolution + (def saved-ns (ctx-current-ns ctx)) + (ctx-set-current-ns ctx defining-ns) + (var result nil) + (each body-form body + (set result (eval-form ctx fn-bindings body-form))) + (ctx-set-current-ns ctx saved-ns) + result)))) + (set self (fn [& fn-args] + (let [n (length fn-args) + f (get arities n)] + (if f + (apply f fn-args) + (error (string "Wrong number of args (" n ") passed to fn")))))) + self) + # Single-arity: (fn* [args] body...) + (let [args-form (in form 1) + body (tuple/slice form 2) + arg-info (parse-arg-names args-form) + fixed-names (arg-info :fixed) + rest-name (arg-info :rest) + defining-ns (ctx-current-ns ctx)] + (var self nil) + (set self (fn [& fn-args] + (var fn-bindings @{}) + (table/setproto fn-bindings bindings) + (var i 0) + (each arg-name fixed-names + (bind-put fn-bindings arg-name (fn-args i)) + (++ i)) + (when rest-name + (put fn-bindings rest-name (tuple/slice fn-args i))) + (put fn-bindings :jolt/loop-fn self) + # Use defining namespace for symbol resolution + (def saved-ns (ctx-current-ns ctx)) + (ctx-set-current-ns ctx defining-ns) + (var result nil) + (each body-form body + (set result (eval-form ctx fn-bindings body-form))) + (ctx-set-current-ns ctx saved-ns) + result)) self)) "let*" (let [bind-vec (in form 1) body (tuple/slice form 2)] @@ -352,9 +367,28 @@ (var i 0) (let [len (length bind-vec)] (while (< i len) - (let [sym (bind-vec i)] + (let [pat (bind-vec i)] (def val (eval-form ctx new-bindings (bind-vec (+ i 1)))) - (bind-put new-bindings (sym :name) val) + # Handle destructuring patterns + (if (struct? pat) + (let [keys-vec (get pat :keys)] + (if (and keys-vec (indexed? keys-vec)) + (each k keys-vec + (def kname (if (keyword? k) (string k) (k :name))) + (bind-put new-bindings kname (get val (keyword kname)))) + (bind-put new-bindings (pat :name) val))) + (if (indexed? pat) + # Sequential destructuring (vector pattern) + (do + (var di 0) + (while (< di (length pat)) + (let [inner-pat (in pat di)] + (if (struct? inner-pat) + (bind-put new-bindings (inner-pat :name) (get val di)) + (bind-put new-bindings inner-pat (get val di)))) + (+= di 1))) + # Plain symbol binding + (bind-put new-bindings (pat :name) val))) (+= i 2)))) (var result nil) (each body-form body diff --git a/test/test-load-sci.janet b/test/test-load-sci.janet new file mode 100644 index 0000000..06e99e7 --- /dev/null +++ b/test/test-load-sci.janet @@ -0,0 +1,70 @@ +(use ../src/jolt/evaluator) +(use ../src/jolt/types) +(use ../src/jolt/reader) +(use ../src/jolt/api) + +(def ctx (init)) + +(defn load-file [ctx path] + (var s (slurp path)) + (var count 0) + (var ok 0) + (var fail 0) + (var failures @[]) + (while (> (length (string/trim s)) 0) + (def [form rest] (parse-next s)) + (set s rest) + (++ count) + (if (not (nil? form)) + (do + (printf "eval form %d..." count) + (flush) + (if (try + (do (eval-form ctx @{} form) true) + ([err] + (printf " FAIL: %q\n" err) + (array/push failures {:form-number count :error (string err) :form (string form)}) + false)) + (do + (printf " OK\n") + (++ ok)) + (++ fail))))) + {:ok ok :fail fail :total count :failures failures}) + +(def sci-base "/Users/yogthos/src/sci/src/sci") + +(def load-order @[ + ["impl/macros.cljc" nil] + ["impl/protocols.cljc" nil] + ["impl/types.cljc" nil] + ["impl/unrestrict.cljc" nil] + ["impl/vars.cljc" nil] + ["lang.cljc" nil] + ["impl/utils.cljc" nil] + ["impl/namespaces.cljc" nil] + ["core.cljc" nil] +]) + +(var total-ok 0) +(var total-fail 0) +(var all-failures @[]) + +(each [file expected-ns] load-order + (def path (string sci-base "/" file)) + (printf "\n=== Loading %s ===\n" file) + (def result (load-file ctx path)) + (printf " Result: %d ok, %d fail, %d total\n" (result :ok) (result :fail) (result :total)) + (+= total-ok (result :ok)) + (+= total-fail (result :fail)) + (each f (result :failures) + (array/push all-failures {:file file :form-number (f :form-number) :error (f :error) :form (f :form)}))) + +(printf "\n==============================\n") +(printf "TOTAL: %d ok, %d fail, %d total\n" total-ok total-fail (+ total-ok total-fail)) +(printf "==============================\n") + +(when (> (length all-failures) 0) + (printf "\n=== FAILURES ===\n") + (each f all-failures + (printf "[%s:%d] %s\n" (f :file) (f :form-number) (f :error)) + (printf " form: %s\n" (f :form))))