From 7112632f2f289cda930a5be17dc0d8e0fb652773 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Tue, 2 Jun 2026 08:43:27 -0400 Subject: [PATCH] chore: update memory/skills --- .clj-kondo/.cache/v1/clj/user.transit.json | 2 +- .dirge/memory/MEMORY.md | 14 +- .dirge/memory/PITFALLS.md | 4 +- .dirge/skills/.usage.json | 32 +- .dirge/skills/jolt-dev/SKILL.md | 350 ++++----------------- test/test-ctor.janet | 34 ++ 6 files changed, 125 insertions(+), 311 deletions(-) create mode 100644 test/test-ctor.janet diff --git a/.clj-kondo/.cache/v1/clj/user.transit.json b/.clj-kondo/.cache/v1/clj/user.transit.json index 831692f..054fe00 100644 --- a/.clj-kondo/.cache/v1/clj/user.transit.json +++ b/.clj-kondo/.cache/v1/clj/user.transit.json @@ -1 +1 @@ -["^ ","~$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 +["^ ","~$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",["^ ","^;",475,"^9",18,"^6","^>","^4","^5","^:",null],"~$ctor",["^ ","^;",549,"^9",17,"~:varargs-min-arity",0,"^6","^?","^4","^5","^:",null,"^7","^8"],"~$v",["^ ","^;",264,"^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",["^ ","^;",445,"^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,"^;",244],"~$kname",["^ ","^;",377,"^9",29,"^6","^X","^4","^5","^:",null,"^7","^C"],"~$val",["^ ","^;",371,"^9",21,"^6","^Y","^4","^5","^:",null],"~$parse-arg-names",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^Z","^7","^8","^9",1,"^:",null,"^F",["^ ","~i1",["^ ","^G",["^ ","^7","~:map","~:val",["^ ","~:fixed",["^ ","^;",189,"^T",623,"^9",11,"^U",13]]],"^I","[args-form]"]],"^;",143],"~$special-symbol?",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^12","^7","^8","^9",1,"^:",null,"^F",["^ ","~i1",["^ ","^G",["^2",["~:boolean"]],"^I","[name]"]],"^;",10],"~$sym-name?",["^ ","^1",["^2",[2]],"^3",true,"^4","^5","^6","^14","^7","^8","^9",1,"^:",null,"^;",6],"~$inner",["^ ","^;",167,"^9",17,"^6","^15","^4","^5","^:",null],"~$saved-ns",["^ ","^;",255,"^9",20,"^6","^16","^4","^5","^:",null],"~$sym-name-str",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^17","^7","^8","^9",1,"^:",null,"^;",70],"~$unwrap-meta-name",["^ ","^1",["^2",[1]],"^3",true,"^4","^5","^6","^18","^7","^8","^9",1,"^:",null,"^;",192]] \ No newline at end of file diff --git a/.dirge/memory/MEMORY.md b/.dirge/memory/MEMORY.md index d9eae86..d2284bc 100644 --- a/.dirge/memory/MEMORY.md +++ b/.dirge/memory/MEMORY.md @@ -1,13 +1,11 @@ -`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. -§ -Janet `(string :keyword)` works but `(name :keyword)` does not — Janet has no `name` function. Use `(string kw)` to convert keywords to strings. -§ -Janet `try` form: `(try body ([err] handler))` — the `([err] handler)` clause must be on ONE line. Multi-line handler clauses cause "unexpected closing delimiter" parse errors. Correct: `(try (do-stuff) ([err] nil))`. Wrong: `(try (do-stuff) ([err] nil))` with `nil` on next line. -§ -SCI depends on edamame (external Clojure parser) for `sci.core/eval-string`. The read path is: `eval-string → interpreter/eval-string* → parser/parse-next → edamame.core/parse-string`. Jolt's reader can't directly replace this without a shim. SCI also requires `clojure.tools.reader.reader-types` (indexing-push-back-reader, string-push-back-reader). -§ `:keys` destructuring in `let*` uses `:keys` keyword (not `"keys"` string) to look up the vector of keys: `{:keys [a b]}` → `(get pat :keys)` returns `(a b)` tuple where each is a keyword. Bind each using `(get val (keyword kname))`. § SCI eval-string pipeline requires 4 internal namespaces not loaded by ns :require: sci.impl.interpreter, sci.impl.parser, sci.impl.analyzer, sci.impl.opts. Their source files must be loaded separately. After loading all 9 SCI source files, these namespaces have 0 bindings. eval-string callable but fails with "Unable to resolve symbol" because it needs these internals. § Edamame shim lives in core.janet, embedded alongside core bindings. Uses `make-string-reader` to create `@{:s str :pos 0 :line 1 :col 1}` reader tables. `shim-edamame-eof` returns `:edamame/eof` keyword. `init-edamame-shim!` takes `ctx`, `parse-str` (e.g. Jolt's `parse-string`), and `read-f` (e.g. Jolt's `read-form`) as arguments to avoid requiring `./reader` from `core.janet`. Line/col tracking increments on newline (chr 10). +§ +SCI added as git submodule at `vendor/sci` (github.com/borkdude/sci). Path to SCI sources: `vendor/sci/src/sci/`. The original `/Users/yogthos/src/sci` path is now superseded. +§ +Architecture decision: Jolt is a Janet-hosted SCI with minimal bootstrapping. Jolt's evaluator + reader form the runtime; SCI's `clojure.core` namespace is populated by loading all 9 SCI source files. `sci.core/eval-string` is replaced with a Jolt-native version: `(defn jolt-eval-string [s &opt opts] (eval-form ctx @{} @[{:jolt/type :symbol :ns nil :name "do"} (parse-string s)]))`. This bypasses SCI's internal interpreter/parser/analyzer pipeline entirely, avoiding the edamame dependency. +§ +gensym in core.janet: uses `@{}` mutable table counter `gensym_counter`. Takes optional prefix string (default "G__"). `core-doto` uses gensym for the object symbol, expands to `(let* [sym obj] (. sym method args)... sym)`. `core-defrecord` generates `->TypeName` positional constructor. `core-name` returns string for keywords (`(string kw)`) or symbol name field. diff --git a/.dirge/memory/PITFALLS.md b/.dirge/memory/PITFALLS.md index 00bf939..435a11c 100644 --- a/.dirge/memory/PITFALLS.md +++ b/.dirge/memory/PITFALLS.md @@ -1,5 +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`. +§ +Janet `try` syntax: the error handler clause `([err] handler)` must be on ONE line. Splitting `([err]\n handler)` causes "unexpected closing delimiter )" parse error at runtime. This is a Janet parser limitation, not a Jolt issue. Fix: always write `(try body ([err] handler-body))` on one line, or use `(do ...)` for multi-line handlers: `(try body ([err] (do ...)))`. diff --git a/.dirge/skills/.usage.json b/.dirge/skills/.usage.json index a2d94c4..f2a1e2f 100644 --- a/.dirge/skills/.usage.json +++ b/.dirge/skills/.usage.json @@ -1,4 +1,14 @@ { + "jpm-build": { + "created_by": "agent", + "use_count": 0, + "view_count": 10, + "patch_count": 0, + "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": 8, @@ -13,24 +23,14 @@ }, "jolt-dev": { "created_by": "agent", - "use_count": 12, - "view_count": 22, - "patch_count": 23, - "last_used_at": "2026-06-02T05:19:05.951411+00:00", - "last_viewed_at": "2026-06-02T05:19:05.942586+00:00", - "last_patched_at": "2026-06-02T05:19:19.223335+00:00", + "use_count": 13, + "view_count": 23, + "patch_count": 25, + "last_used_at": "2026-06-02T05:43:03.107672+00:00", + "last_viewed_at": "2026-06-02T05:43:03.094267+00:00", + "last_patched_at": "2026-06-02T05:43:43.591823+00:00", "created_at": "2026-06-01T21:26:06.614465+00:00", "state": "active", "pinned": false - }, - "jpm-build": { - "created_by": "agent", - "use_count": 0, - "view_count": 10, - "patch_count": 0, - "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 } } \ No newline at end of file diff --git a/.dirge/skills/jolt-dev/SKILL.md b/.dirge/skills/jolt-dev/SKILL.md index 6e260d8..c0bee6f 100644 --- a/.dirge/skills/jolt-dev/SKILL.md +++ b/.dirge/skills/jolt-dev/SKILL.md @@ -1,278 +1,27 @@ ---- -description: Jolt development workflow — build, test, special form patterns, Janet gotchas ---- +# jolt-dev -# Jolt Development +Jolt development workflow — build, test, special form patterns, Janet gotchas ## Build & Test ```bash cd /Users/yogthos/src/jolt -jpm build # produces build/jolt -jpm test # runs all tests +jpm build # produces build/jolt (gitignored) +jpm test # runs all tests (test-load-sci times out occasionally) janet test/foo.janet # run a single test file from project root ``` -## Special Form Checklist +Build artifacts are in `build/` which is `.gitignore`d. -To add a new special form to the evaluator: +## Architecture -1. Add the name to `special-symbol?` in `src/jolt/evaluator.janet` -2. Add a match arm in `eval-list` (the match on `name`) -3. Add tests in `test/evaluator-test.janet` +Jolt is a **Janet-hosted SCI** — Jolt's reader + evaluator form the runtime, SCI provides the standard library. Load order: macros → protocols → types → unrestrict → vars → lang → utils → namespaces → core (all 317 forms, 0 failures). Source: `vendor/sci/src/sci/` (git submodule). -The match arm receives `ctx`, `bindings`, and `form` (the full list). Use `(in form 1)` for first arg, etc. +`sci.core/eval-string` is replaced with Jolt-native: `(eval-form ctx @{} @[{:jolt/type :symbol :ns nil :name "do"} (parse-string s)])`. This bypasses SCI's interpreter/parser/analyzer pipeline. SCI internal namespaces (interpreter, parser, analyzer, opts) have 0 bindings after loading — they require loading their own source files. -**Non-symbol heads** (keywords, etc.): `eval-list` first checks `(and (struct? first-form) (= :symbol (...)))` before extracting `name`. If not a symbol, falls through to default function application. This means `(:ns &env)` works because the head `:ns` is a keyword, not a symbol, so it's evaluated and called as a lookup. +## SCI Submodule -### Current special forms (22): -`quote`, `syntax-quote`, `unquote`, `unquote-splicing`, `do`, `if`, `def`, `defmacro`, `fn*`, `let*`, `loop*`, `recur`, `throw`, `try`, `set!`, `var`, `locking`, `instance?`, `defmulti`, `defmethod`, `deftype`, `new`, `.` - -### let* destructuring -`:keys` pattern: `{:keys [a b]}` — `(get pat :keys)` returns tuple of keywords. Use `(string k)` to convert keyword to string name, `(keyword kname)` for lookup. -Sequential: `[a b c]` — indexed pattern, iterate with `(length pat)`, bind each position via `(in pat di)` -Plain symbol: bind name directly with `bind-put` - -### let macro (→ let*) -Macro `let` expands to `let*`: `(let [bindings] body)` → `(let* [bindings] body)` -Registered as macro in `core-macro-names` so evaluator expands before `()let*` special-form dispatch. -Needed because SCI/clojure.core use `let` not `let*`. - -### Janet `try` format — CRITICAL -The catch clause `([err] handler)` MUST be on ONE line. Multi-line causes "unexpected closing delimiter )" parse errors. -```janet -# Correct — single line -(try (do-x) ([err] nil)) -# Correct — multi-statement with do -(try (do-x) ([err] (do (log-err) nil))) -# WRONG — newline before nil causes parse error -(try (do-x) ([err] - nil)) -``` -This is a Janet parser limitation, not a Jolt issue. The `([err] ...)` clause is a tuple literal and Janet requires it on one line. - -### Macro registration pattern -- Define macro fn in `core.janet` → add to `core-bindings` map → add to `core-macro-names` table -- Order matters: `core-macro-names` is a literal referencing fn names, so fns must be defined BEFORE the def- map - -### Namespace capture for closures -`fn*` and `defmacro` capture `defining-ns` from `(ctx-current-ns ctx)` at definition time. -During body eval, save/restore: `(ctx-set-current-ns ctx defining-ns)` before eval, restore after. -This ensures symbols resolve in the defining namespace, not whatever ns is current when the closure is called. - -### defmacro details -- Supports optional docstring: `(defmacro name [args] body)` or `(defmacro name "doc" [args] body)` -- Implementation: `(tuple/slice form 2)` → check if first is string → adjust args-form and body start -- Implicit `&env` binding: `(put new-bindings "&env" @{})` — table, not struct (nil-safe) -- Uses `parse-arg-names` for `& rest` arg handling - -### defmulti / defmethod -- `defmulti` stores the methods table on the **var** via `(put v :jolt/methods methods)`, NOT on the function -- Janet `put` on a function value fails silently -- `defmethod` retrieves methods via `(get mm-var :jolt/methods)` using `resolve-var` to get the var first - -### set! -- If var doesn't exist, auto-creates it (`ns-intern`) rather than erroring -- Needed for sci's `(set! *warn-on-reflection* true)` - -### defmethod (auto-create) -- If the multimethod var doesn't exist yet, auto-creates it with a dummy fn and empty methods table -- This allows sci's `defmethod print-method` to work before `defmulti print-method` is defined - -### deftype -- Handles `^:meta` on type name via `with-meta` pattern -- Fields vector handles `^:meta` annotations and `^Type` hints — extracts actual symbol name -- Produces a table with `:jolt/deftype "ns.TypeName"` and keyword-keyed fields - -## Janet Gotchas - -- `var` declares mutable locals that can be `set` later; `def`/`let` are immutable -- `let` cannot bind to `nil` — use `(var x nil)` instead of `(let [x nil] ...)` -- `(get table key)` needs 2 args minimum — for single-arg checks use `(table :key)` -- 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 -- `(first struct)` calls `:jolt/type` method — use `(get struct :key)` instead of positional access -- Janet `(struct ;[:x nil])` silently drops the nil entry — the map becomes `{}`. Use `@{}` tables for nil-safe entries -- `(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)` → `"keyword"`** — Janet has no `name` function. Use `(string kw)` to convert keyword to string. `(name :kw)` is NOT a Janet built-in. -- **`try` handler must be one line** — `(try body ([err] handler))` — the `([err] ...)` clause is a tuple literal, Janet requires it all on one line. Multi-line handlers cause parse errors. -- **`put` doesn't work on structs** — `(put {:a 1} :b 2)` errors. Use `@{}` tables for mutable maps. -- **`(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: -```janet -(defn- unwrap-meta-name [form] - (if (and (array? form) (> (length form) 0) - (struct? (in form 0)) - (= :symbol ((in form 0) :jolt/type)) - (= "with-meta" ((in form 0) :name))) - (unwrap-meta-name (in form 1)) - form)) -``` - -### Reader map k/v handling -The map reader must handle three special value types in both key and value positions: -- `:jolt/skip` — discarded form (comment, `#_`, nil `#?(:cljs ...)`): skip the K/V pair entirely -- `:jolt/splice` — `#?@` splicing: concat items into the kvs array. If splice has 0 items (nil `:cljs` branch), don't push the key - -### `#?(:cljs X)` returns nil → `:jolt/skip` -The non-splicing `#?` reader now returns `{:jolt/type :jolt/skip}` for nil results (e.g., `#?(:cljs X)` on CLJ). This prevents orphaned nil keys/values in maps and lists. `parse-next` and `parse-string` skip past skip markers to return the next real form. - -### deftype →TypeName constructor -`deftype` now interns both the type name and `->TypeName` arrow constructor (Clojure convention). The dot-suffix constructor check uses `(sym-name (- (length sym-name) 1))` instead of broken `(last sym-name)`. - -## Comment Handling - -Comments `;` in `read-form` return `{:jolt/type :jolt/skip}` sentinel: -```janet -(= c 59) # ; -[{:jolt/type :jolt/skip} line-end]) -``` - -`parse-next` and `parse-string` both skip over `:jolt/skip` results: -- `parse-next` uses inner `parse-next-loop` that recurses on skip -- `parse-string` recurses on skip to return next non-comment form - -`read-map` checks both key AND value positions for `:jolt/skip` to skip `#_`-discarded entries. - -Closing delimiters `)`, `]`, `}` in `read-form` produce explicit errors: -```janet -(= c 41) (error (string "Unmatched closing paren at " pos)) -``` - -This prevents them from falling through to `read-symbol` which gave "Unrecognized character". - -## `unwrap-meta-name` Utility - -Recursively unwraps `(with-meta sym meta)` forms to extract the underlying symbol: -```janet -(defn- unwrap-meta-name [form] - (if (and (array? form) (> (length form) 0) - (struct? (in form 0)) - (= :symbol ((in form 0) :jolt/type)) - (= "with-meta" ((in form 0) :name))) - (unwrap-meta-name (in form 1)) - form)) -``` - -Used in: `def`, `ns` (ns name), `deftype` (type name + field names), `defmethod` (arg names). -Replaced duplicated `with-meta` unwrapping code in each of these forms. - -## Bootstrap Patterns - -### Class-name resolution in resolve-sym - -When a simple symbol (unqualified) isn't found in current ns or clojure.core, checks for dotted class-name pattern: -`Foo.Bar.Baz` → finds last dot → ns "Foo.Bar", name "Baz" → tries ns-resolve. -This resolves symbols like `IVar` that are interned in `sci.impl.vars` but referred to unqualified from `sci.lang`. - -### Reader conditionals -- `#?(:clj expr :cljs expr2)` — resolved at read time by `read-reader-conditional` -- `#?@(:clj expr :cljs expr2)` — splicing variant, wraps resolved items in `{:jolt/type :jolt/splice :items ...}` -- List/vector/set readers check for splice and flatten items -- `#_` — discard reader macro, reads next form and returns it as position only - -### Core macros (in core.janet) -- `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 - -### Core stubs for sci bootstrap -- `core-derive`, `core-isa?`, `core-ancestors`, `core-descendants` — minimal hierarchy -- `core-Object`, `core-Thread`, `core-ThreadLocal`, `core-IllegalStateException` — JVM class stubs -- `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 -- `def` form also handles `^:meta` on def name (extracts name-sym from `(with-meta Name meta)`) -- `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 (corrected) -``` -sci.impl.macros (4/4 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) -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 +SCI is at `vendor/sci` (git submodule, github.com/borkdude/sci). Load order: macros → protocols → types → unrestrict → vars → lang → utils → namespaces → core ## Project Structure @@ -281,47 +30,80 @@ src/jolt/ types.janet — Var, Namespace, Context, symbol helpers reader.janet — recursive descent parser for Clojure syntax evaluator.janet — tree-walking interpreter (eval-form, eval-list, syntax-quote*) - core.janet — 95+ clojure.core functions (map, filter, reduce, etc.) + core.janet — clojure.core bindings: predicates, math, collections, macros, stubs api.janet — public API: init, eval-string, eval-string* - main.janet — REPL entry point with (defn main [&]) + main.janet — REPL entry point test/ - evaluator-test.janet — special form tests (22 forms tested) - reader-test.janet — parser tests (includes #?, #?@, #_) + evaluator-test.janet — special form tests + reader-test.janet — parser tests core-test.janet — core library tests macro-test.janet — syntax-quote and macro tests namespace-test.janet — ns, require, in-ns tests types-test.janet — Var and Namespace tests api-test.janet — public API tests - bootstrap-test.janet — loads sci.impl.macros (deftime, usetime, ?) - test-load-sci.janet — loads all sci files, counts ok/fail - test-eval.janet — end-to-end sci.core/eval-string test + bootstrap-test.janet — loads sci.impl.macros + test-load-sci.janet — loads all sci files + tests eval-string ``` -## Edamame Shim for SCI eval-string +## Janet Gotchas -SCI's `eval-string` depends on `edamame.core` (parser) and `clojure.tools.reader.reader-types`. Instead of loading the real edamame (its parser.cljc has character-literal edge cases), inject a shim in `core.janet`: +- **`try` form**: `(try body ([err] handler))` — the `([err] handler)` must be ONE line. Multi-line break after `([err]` causes "unexpected closing delimiter )" parse error. Correct: `(try (do-stuff) ([err] nil))`. +- **`try` separate from `([err]`**: `(try body-form ([err] handler))` is valid — `body-form` and `([err]` on same line. +- **`(string :kw)`** converts keyword to string. Janet has no `name` function. +- **`(put {:x 1} :y 2)`** errors — structs don't support `put`. Use `@{}` tables for mutable maps. +- **`(put table key nil)`** silently drops the key. Use `bind-put` + `:jolt/nil-sentinel` pattern. +- **`var`** declares mutable locals; `def`/`let` are immutable. +- **`(set [a b] tuple)`** doesn't work — use explicit indexing `(tuple 0)` / `(tuple 1)`. +- **`(last "string")`** returns nil — works only on indexed types. +- **Multi-arity `defn` in Janet**: Use `(fn [& args] (case (length args) ...))` pattern. `defn-?` doesn't exist. +- **`indexed?` vs `array?`**: Reader produces tuples for vectors, arrays for lists. Check `indexed?` for vector patterns. +- **`match`** returns nil on no match — used in `eval-list` for non-symbol head fallthrough. +- **`(length struct)`** counts pairs, not keys. Use `(length (keys struct))`. +- **`(first struct)`** calls `:jolt/type` method — use `(get struct :key)`. +## Special Form Checklist + +To add a new special form: +1. Add the name to `special-symbol?` in `src/jolt/evaluator.janet` +2. Add a match arm in `eval-list` +3. Add tests in `test/evaluator-test.janet` + +Current: `quote`, `syntax-quote`, `unquote`, `unquote-splicing`, `do`, `if`, `def`, `defmacro`, `fn*`, `let*`, `loop*`, `recur`, `throw`, `try`, `set!`, `var`, `locking`, `instance?`, `defmulti`, `defmethod`, `deftype`, `new`, `.` + +## Core Macros and Functions + +### gensym ```janet -(defn init-edamame-shim! [ctx parse-str read-f] - ...) +(def gensym_counter @{:val 0}) +(defn gensym [&opt prefix-string] + (default prefix-string "G__") + (def n (get gensym_counter :val)) + (put gensym_counter :val (+ n 1)) + {:jolt/type :symbol :ns nil :name (string prefix-string n)}) ``` -Called after SCI sources load: `(init-edamame-shim! ctx parse-string read-form)`. Takes reader functions as arguments to avoid circular module dependencies (core.janet doesn't use ./reader). +### core-name +Returns name string of keyword, symbol, or string. Uses `(string kw)` for keywords, `.name` field for symbols. -Shim creates `edamame.core` ns with: `eof`, `normalize-opts`, `reader`, `parse-string`, `parse-string-all`, `parse-next`, `continue`. -Also creates `clojure.tools.reader.reader-types` ns with: `indexing-push-back-reader`, `string-push-back-reader`, `source-logging-reader?`, `get-line-number`, `get-column-number`. +### Registered macros in core-macro-names +`when`, `when-not`, `if-let`, `when-let`, `if-some`, `when-some`, `doto`, `defn`, `defn-`, `declare`, `fn`, `let`, `defrecord`, `defprotocol`, `extend-type`, `extend-protocol`, `extend`, `reify`, `proxy`, `definterface`, `comment` -### Remaining gap +### defrecord stub +Generates `->TypeName` positional constructor. Expands to `(do (def TypeName (fn* [fields] ...)) (def ->TypeName ...))`. -SCI's `eval-string` calls into `sci.impl.interpreter/eval-string*` which requires 4 internal namespaces NOT loaded by Jolt's `ns :require` handler: `sci.impl.interpreter`, `sci.impl.parser`, `sci.impl.analyzer`, `sci.impl.opts`. Their source files must be loaded separately. +### doto macro +Uses `gensym` for the object symbol. Expands to `(let* [sym obj] (. sym method args)... sym)`. -## Module Dependency Gotcha +### defmacro details +- Supports optional docstring: `(defmacro name [args] body)` or `(defmacro name "doc" [args] body)` +- Implicit `&env` binding: `(put new-bindings "&env" @{})` — table, not struct (nil-safe) +- Capture defining namespace for symbol resolution in macro bodies -Don't create standalone shim files that `(use ./reader)` if test files also use reader — Janet resolves modules at compile time and circular dependencies cause "Unmatched closing bracket" errors at unrelated file positions. Embed shims in existing modules (e.g. `core.janet`) and pass reader functions as arguments. +## Janet Table Nil-Drop Workaround -## `let*` Destructuring - -`:keys` destructuring: `{:keys [a b]}` → `(get pat :keys)` returns `(a b)` tuple where each is a keyword. Bind each using `(get val (keyword kname))`. Use `(string kw)` (not `(name kw)`) to convert keywords to strings — Janet has no `name` function. - -Sequential destructuring: `[a b c]` pattern → iterate by index, bind `(get val di)` for each element. +`(put table key nil)` silently drops the key. Workflow: +1. `bind-put` helper in `evaluator.janet` stores nil as `:jolt/nil` sentinel +2. `resolve-sym` unwraps `:jolt/nil` back to `nil` +3. Dynamic vars with nil values (`*1`, `*2`, `*3`, `*e`) use `:jolt/nil-sentinel` in `core-bindings` +4. `init-core!` checks for `:jolt/nil-sentinel` and passes actual nil to `ns-intern` \ No newline at end of file diff --git a/test/test-ctor.janet b/test/test-ctor.janet new file mode 100644 index 0000000..a93e35c --- /dev/null +++ b/test/test-ctor.janet @@ -0,0 +1,34 @@ +(use ../src/jolt/types) +(use ../src/jolt/api) +(use ../src/jolt/reader) +(use ../src/jolt/evaluator) + +(def ctx (init)) +(def sci-base "vendor/sci/src/sci") + +(defn load [ctx path] + (var s (slurp path)) + (while (> (length (string/trim s)) 0) + (def [form rest] (parse-next s)) + (set s rest) + (when (not (nil? form)) + (try (eval-form ctx @{} form) ([err] nil)))))) + +(each f ["impl/macros.cljc" "impl/protocols.cljc" "impl/types.cljc" "impl/unrestrict.cljc" "impl/vars.cljc" "lang.cljc" "impl/utils.cljc" "impl/namespaces.cljc"] + (load ctx (string sci-base "/" f))) + +# Check if utils/new-var exists +(def utils-ns (ctx-find-ns ctx "sci.impl.utils")) +(def new-var-v (if utils-ns (ns-find utils-ns "new-var") nil)) +(printf "sci.impl.utils/new-var: %q\n" new-var-v) + +# Check parser ns aliases +(def parser-ns (ctx-find-ns ctx "sci.impl.parser")) +(if parser-ns + (do + (printf "parser aliases: %q\n" (parser-ns :aliases)) + (printf "parser imports: %q\n" (parser-ns :imports)))) + +# Try resolving utils/new-var through alias +(printf "\nResolve utils/new-var: %q\n" + (try (eval-string ctx "utils/new-var") ([err] (string "ERR: " err))))