diff --git a/.dirge/memory/MEMORY.md b/.dirge/memory/MEMORY.md index 2f688fb..1b92709 100644 --- a/.dirge/memory/MEMORY.md +++ b/.dirge/memory/MEMORY.md @@ -7,3 +7,5 @@ Phase 2 (PersistentHashMap): Live in src/jolt/phm.janet — separate module impo Macro expansion: resolve-macro at analyze time → expand → re-analyze. Loop: (do (var _loop_N nil) (set _loop_N (fn [params] body)) (_loop_N vals...)). Recur: emits (loop-name args...) via :loop-name in AST. § Test files: test/phase6-final.janet (47 tests, 58 assertions — collections, math, predicates, comparison, seq ops, special forms, macros, complex nesting). Phase 1 tests appended to test/compiler-test.janet (ns accessors, ns form extensions). All 317 tests pass. +§ +Phase 3 (Var system): find-var (ctx-based, resolve-q/nq symbol), alter-meta!, reset-meta!, var-get/var-set/var?/alter-var-root all in types.janet + evaluator dispatch arms + core-bindings wrappers. core-meta fixed: (var? x) branch → var-meta, struct? branch → :meta. 10 tests pass. diff --git a/.dirge/memory/PITFALLS.md b/.dirge/memory/PITFALLS.md index 1ed45e8..ec8f784 100644 --- a/.dirge/memory/PITFALLS.md +++ b/.dirge/memory/PITFALLS.md @@ -1,7 +1,7 @@ -core-renames MUST match actual function names in core.janet. `"-"`→`core-sub` NOT `core--`. `"fn?"` was missing entirely (caused silent nil). Missing entries → symbol treated as unknown global, returns nil. When adding: grep core.janet for actual `(defn core-XXX)` name, add to BOTH core-renames (string table) and core-fn-values (fn value table). -§ Bare tuples in Janet's `eval` are function calls: `(eval [1 2 3])` tries to call `1` as function. Always emit `['tuple 1 2 3]` or `(tuple 1 2 3)` in data-structure emitter. Similarly, `(eval (try body (sym handler)))` fails because `catch` is not a Janet special form — must be `(try body ([sym] handler))`. Discovered during Phase 5/6 compiler work. § Duplicate function definitions in the same file cause hard-to-diagnose "unknown symbol" errors. In compiler.janet, emit-quote-str was defined twice (once before emit-ast dispatch, once before emit-expr section). The second definition compiled but the first was used by emit-ast dispatch — causing "unknown symbol raw-form->janet". Always grep for the fn name before adding a new definition. § Janet break can't be used inside let blocks — break returns from the innermost loop, and in a let, there's no loop. Pattern: use (var found nil) + (while ... (if condition (do (set found val) (break)))) then check found after loop. +§ +core-binding macro: use array-map (plain struct) NOT hash-map/PHM for the binding frame. PHM's phm-get doesn't work with push-thread-bindings' var-get lookup. Symptom: "error: dynamic binding" with no useful message. Fix: emit (array-map [var sym] val ...) instead of (hash-map ...). diff --git a/.dirge/skills/.usage.json b/.dirge/skills/.usage.json index 1801749..b78c567 100644 --- a/.dirge/skills/.usage.json +++ b/.dirge/skills/.usage.json @@ -1,16 +1,26 @@ { "jolt-dev": { "created_by": "agent", - "use_count": 30, - "view_count": 45, - "patch_count": 37, - "last_used_at": "2026-06-02T22:24:09.648320+00:00", - "last_viewed_at": "2026-06-02T22:24:09.636430+00:00", - "last_patched_at": "2026-06-02T22:24:40.254179+00:00", + "use_count": 32, + "view_count": 47, + "patch_count": 40, + "last_used_at": "2026-06-03T00:55:18.036371+00:00", + "last_viewed_at": "2026-06-03T00:55:18.017806+00:00", + "last_patched_at": "2026-06-03T00:55:51.227868+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": 15, + "patch_count": 0, + "last_viewed_at": "2026-06-02T21:03:17.897587+00:00", + "created_at": "2026-06-01T20:56:39.144222+00:00", + "state": "active", + "pinned": false + }, "jolt-bootstrap": { "created_by": "agent", "use_count": 9, @@ -25,24 +35,14 @@ }, "jolt-compiler": { "created_by": "agent", - "use_count": 4, - "view_count": 8, - "patch_count": 6, - "last_used_at": "2026-06-02T22:24:09.679583+00:00", - "last_viewed_at": "2026-06-02T22:24:09.655249+00:00", - "last_patched_at": "2026-06-02T21:40:55.723354+00:00", + "use_count": 5, + "view_count": 9, + "patch_count": 7, + "last_used_at": "2026-06-03T00:55:56.946140+00:00", + "last_viewed_at": "2026-06-03T00:55:56.935595+00:00", + "last_patched_at": "2026-06-03T00:56:15.725835+00:00", "created_at": "2026-06-02T17:54:38.690279+00:00", "state": "active", "pinned": false - }, - "jpm-build": { - "created_by": "agent", - "use_count": 0, - "view_count": 15, - "patch_count": 0, - "last_viewed_at": "2026-06-02T21:03:17.897587+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-compiler/SKILL.md b/.dirge/skills/jolt-compiler/SKILL.md index d8d6fc0..2944dd7 100644 --- a/.dirge/skills/jolt-compiler/SKILL.md +++ b/.dirge/skills/jolt-compiler/SKILL.md @@ -1,13 +1,4 @@ ---- -triggers: - - "compile jolt" - - "jolt compiler" - - "Clojure to Janet compilation" - - "add new op to compiler" - - "fix compiler" ---- - -# Jolt Compiler +# jolt-compiler Source-to-source Clojure→Janet compiler. Two-phase: analyze-form (classify + macro expand) → emit-ast (generate). @@ -24,142 +15,81 @@ Clojure form → analyze-form [form bindings ctx] → AST {:op ...} Three public entry points: - `(compile-form form &opt ctx)` → Janet source string (debug/display) - `(compile-ast form &opt ctx)` → Janet data structure (for eval) -- `(compile-and-eval form ctx)` → compile-ast + eval +- `(compile-and-eval form ctx)` → compile-ast + eval + ns interning ## Why data structures, not source strings -Janet's `eval` does NOT have access to `use`-imported symbols from the calling file. `(eval "(core-inc 1)")` fails with "unknown symbol core-inc". The fix: emit Janet tuples where function VALUES are embedded: `[core-inc 1]`. +Janet's `eval` does NOT have access to `use`-imported symbols. Emit Janet tuples with embedded function VALUES via `core-fn-values` table. -``` -core-fn-values table: "core-inc" → core-inc (the actual function) -emit-core-symbol-expr → (get core-fn-values janet-name) -``` - -Source-to-source (`compile-form` + `emit-ast`) still exists for debugging but is NOT used by `compile-and-eval`. +Source-to-source (`compile-form` + `emit-ast`) exists for debugging, NOT used by `compile-and-eval`. ## Macro expansion -`analyze-form` checks whether the head symbol of a list resolves to a macro var before dispatching to special form handling: +`analyze-form` checks `resolve-macro` before special form dispatch: +1. Look up symbol in current ns → core ns +2. If `var-macro?`, apply fn, re-analyze expanded form -1. Look up symbol in current ns → core ns via `resolve-macro` -2. If `var-macro?` is true, call `(var-get macro-var)` to get the fn -3. `(apply macro-fn (tuple/slice form 1))` to expand -4. `(analyze-form expanded ...)` to re-analyze the result - -Macros expand at analyze time, before emission. `defn` expands to `(def name (fn* ...))`, `when` to `(if test (do ...) nil)`, etc. - -## Symbol classification (in analyze-form) +## Symbol classification Order: qualified ns → local binding → core-symbol → bare symbol -``` -(if (form :ns) → :qualified-symbol - (get bindings name) → :local - (get core-renames name) → :core-symbol - → :symbol) -``` - -core-renames MUST match actual fn names: `"-"` → `"core-sub"` (not `"core--"`), `"not"` → `"core-not"`. Verify against `core.janet` bindings. +core-renames MUST match actual fn names: `"-"` → `"core-sub"` (not `"core--"`). ## core-fn-values -Maps Janet string names to actual function values. Must be kept in sync with core-renames. When adding a new core fn, update BOTH tables. +Maps Janet string names to actual function values. Must be kept in sync with core-renames. +Special mappings: `"apply"` → `apply` (built-in), `"some"` → `core-some?`, `"pr-str"` → `core-str`, `"nth"` → `core-nth`. -Functions that need special mapping (name differs): -- `"apply"` → `apply` (Janet built-in) -- `"-"` → `"core-sub"` (not `core--`) -- `"some"` → `core-some?` (shared with `core-some?`) -- `"pr-str"` → `core-str` (alias) -- `"nth"` → `core-nth` (separate function, added in Phase 6) -- `"list"` → `core-list`, `"name"` → `core-name`, `"subs"` → `core-subs` +## compile-and-eval interning + +`def`/`defn` results are interned in the Jolt namespace via `ns-intern` so the interpreter can resolve bare symbols. This is critical — without it, `(defn foo [x] x)` followed by `(foo 1)` would fail with "Unable to resolve symbol: foo". ## Loop/recur compilation -`loop*` emits a self-referential closure: -```janet -(do (var _loop_N nil) - (set _loop_N (fn [params] body)) - (_loop_N init-vals...)) -``` - -`recur` saves `:loop-name` in the AST (looked up from bindings `:jolt/current-loop`), then `emit-recur-expr` rewrites to `(loop-name arg1 arg2...)`. - -In the string emitter, recur similarly emits `(loop-name arg ...)`. +`loop*` → `(do (var name nil) (set name (fn [params] body)) (name init-vals...))` +`recur` → `(loop-name arg...)` via `:jolt/current-loop` binding ## Throw/try compilation - `throw` → `(error val)` in Janet -- `try/catch` → `(try body ([err] handler-body))` — NOTE: Janet uses `([sym] handler)` format, NOT `(catch sym handler)` -- `try/finally` → appends do-block after catch clause in the Janet tuple +- `try` → `(try body ([err] handler))` — Janet uses `([sym] handler)`, NOT `(catch sym handler)` -## Quote in data-structure emitter +## Quote — use raw-form->janet -Don't re-analyze quoted forms. Use `raw-form->janet` to pass Jolt reader forms through verbatim to Janet's `quote`: -``` -(emit-quote-expr expr) → ['quote (raw-form->janet expr)] -``` -raw-form->janet converts symbols to Janet symbols, arrays/tuples recursively. +Don't re-analyze quoted forms. Use `raw-form->janet` to pass Jolt reader forms verbatim to Janet's `quote`. ## Remaining ops (interpreter only) -`syntax-quote`, `set!`, `deftype`, `defmulti`, `defmethod` — these are stateful or complex and always use the interpreter path even in compile mode. +`syntax-quote`, `set!`, `deftype`, `defmulti`, `defmethod` — stateful/complex, always use interpreter. -## Stateful forms (must use interpreter, NOT compiler) +## Stateful forms (interpreter only even in compile mode) -These forms modify context state and cannot be compiled: -- `defmacro`, `ns`, `deftype`, `defmulti`, `defmethod`, `require`, `in-ns` +`defmacro`, `ns`, `deftype`, `defmulti`, `defmethod`, `require`, `in-ns` -Note: `def` IS handled by the compiler (compiles to Janet `def`, since macros are expanded at analyze time). +Note: `def` IS handled by compiler (macros expanded at analyze time). ## eval-string dispatch (compile mode) -When `:compile?` is true, everything goes through the compiler EXCEPT stateful forms. Phase 0 fix: bare symbols and tuples also go through compile path (not just arrays). - -Stateful forms (always use interpreter): -`defmacro`, `ns`, `deftype`, `defmulti`, `defmethod`, `require`, `in-ns`, `syntax-quote`, `set!`, `var`, `.`, `new` - -Note: `def` is handled by the compiler — macros like `defn` expand to `(def name (fn* ...))` and `def` emits Janet `def`. - -## compile-and-eval def/defn interning - -`compile-and-eval` interns `def`/`defn`/`defn-` results in the Jolt namespace via `ns-intern` so the interpreter can resolve bare symbols later. Without this, `(defn foo [x] x)` creates the Janet global but `foo` as a bare symbol can't be found by the interpreter. - -## Quote in data-structure emitter - -Don't re-analyze quoted forms. Use `raw-form->janet` to pass Jolt reader forms through verbatim to Janet's `quote`. raw-form->janet now handles namespace-qualified symbols: `{:ns "foo" :name "bar"}` → `foo/bar` Janet symbol. - -## Remaining ops (interpreter only) - -`syntax-quote`, `set!`, `deftype`, `defmulti`, `defmethod`, `var`, `.`, `new` — these are stateful or complex and always use the interpreter path even in compile mode. - -## Binding macro (Phase 1) - -`core-binding` macro generates `(let [frame {var1 var1-obj var2 var2-obj ...}] (push-thread-bindings frame) (try (do body...) (finally (pop-thread-bindings))))`. Call forms inside the macro expansion MUST use `@[...]` arrays, not tuples — the evaluator treats tuples as literal vectors. +Stateful check: `defmacro`, `ns`, `deftype`, `defmulti`, `defmethod`, `require`, `in-ns`, `syntax-quote`, `set!`, `var`, `.`, `new` → interpreter. Everything else → `compile-and-eval`. ## Adding a new op -1. Add `analyze-form` match arm for the special form -2. Add `emit-ast` match arm (source string path) -3. Add `emit-expr` match arm (data structure path) -4. Add `core-renames` entry if it's a core fn (name → Janet string name) -5. Add `core-fn-values` entry (Janet string name → actual fn value) -6. Add tests in `test/compiler-test.janet` +1. Add `analyze-form` match arm +2. Add `emit-ast` + `emit-expr` match arms +3. Update `core-renames` + `core-fn-values` if core fn +4. Add tests in `test/compiler-test.janet` -## Phase 1: Var/Namespace system +## Var system (Phase 3) -- `types.janet`: `all-ns`, `remove-ns`, `create-ns`, `the-ns`, `ns-interns`, `ns-aliases`, `ns-imports-fn` -- `core.janet`: `core-binding` macro + `core-push-thread-bindings`/`core-pop-thread-bindings` -- `evaluator.janet`: ns accessor special forms, ns form extended with `:require`/`:refer`, `:use`, `:refer-clojure`/`:exclude`, `:import` clauses +`var-get`, `var-set`, `var?`, `alter-var-root`, `find-var`, `alter-meta!`, `reset-meta!`, `intern` — all dispatched in evaluator, with Clojure wrappers in `core-bindings`. `find-var` takes a ctx + symbol, must eval args first (NOT pass raw form). `core-meta` handles vars via `var-meta` branch. + +## PersistentHashMap (Phase 2) + +`src/jolt/phm.janet` — separate module, bucket-based with `:jolt/deftype` tag. `core-binding` macro uses `array-map` (not `hash-map`) to avoid PHM get() incompatibility with `push-thread-bindings`. ## Test files -- `test/compiler-test.janet` — Phase 2-5 + Phase 0-1 tests (source output + compile-eval + macro + ns + defn tests) -- `test/phase6-final.janet` — Phase 6 comprehensive compile-mode tests (47 assertions) - -Run: `janet test/compiler-test.janet` or `janet test/phase6-final.janet` or `jpm test` - -- Source output tests: `(assert (= "(expected)" (compile-str "(input)")) "label")` -- Round-trip tests: `(assert (= val (compile-eval-str "(input)")) "label")` -- Compile flag tests: `(eval-string ctx "(input)")` with `{:compile? true}` +- `test/compiler-test.janet` — all compiler tests (Phases 0-3) +- `test/phase6-final.janet` — 47 comprehensive compile-mode tests Run: `janet test/compiler-test.janet` or `jpm test` diff --git a/.dirge/skills/jolt-dev/SKILL.md b/.dirge/skills/jolt-dev/SKILL.md index 8b8183a..3e64e91 100644 --- a/.dirge/skills/jolt-dev/SKILL.md +++ b/.dirge/skills/jolt-dev/SKILL.md @@ -1,9 +1,6 @@ ---- -name: jolt-dev -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 @@ -12,29 +9,8 @@ cd /Users/yogthos/src/jolt jpm build # produces build/jolt jpm test # runs all tests janet test/foo.janet # run a single test file from project root -janet -k file.janet # check parse without executing (useful for syntax validation) ``` -## Python3 batch edits - -When the `edit` tool is blocked by syntax checker false positives on complex multi-line replacements, use `python3` for batch text edits: - -```bash -python3 << 'PYEOF' -with open('src/jolt/core.janet') as f: - content = f.read() -content = content.replace('old text', 'new text') -with open('src/jolt/core.janet', 'w') as f: - f.write(content) -PYEOF -``` - -Always run `janet -k file.janet` after to verify syntax. - -## Compiler Development - -See `jolt-compiler` skill for the Clojure→Janet source-to-source compiler workflow. - ## Janet Eval Pipeline (critical) Janet's `(parse s)` does NOT return a parsed form — it returns `[symbol, error-position]`. @@ -57,66 +33,13 @@ When you need to mutate a local with `set`, use `(var x nil)` not `(def x nil)`. ## Compiler (see also `jolt-compiler` skill) `src/jolt/compiler.janet` — Clojure→Janet source compiler with macro expansion. -`test/compiler-test.janet` — 11 test groups covering all ops. Key design decision: **compile-and-eval emits Janet DATA STRUCTURES, not source strings**, because Janet's `eval` doesn't see `use`-imported symbols. `core-fn-values` table resolves Janet names to actual function values at compile time. -## Compiler Development - -### Adding a new op to the compiler - -1. Add match arm in `analyze-form` — maps Clojure form → AST node -2. Add `emit-*-str` for source-to-source path, then arm in `emit-ast` dispatch -3. Add `emit-*-expr` for data-structure path, then arm in `emit-expr` dispatch -4. Add tests - -### Emitter patterns - -**String emitter**: `(buffer/push buf "...")` → source text -**Data-structure emitter**: `['keyword val1 val2]` → eval-able tuples - -### Janet eval gotchas - -- Bare tuples are function calls — always use `['tuple ...]` or `(tuple ...)` -- `eval` scope: symbols from `(use ...)` not available — embed function VALUES -- Janet `try`: `(try body ([err] handler))` — not `(catch sym handler)` -- Core `-` maps to `core-sub` (NOT `core--`) - -### Loop compilation - -`(loop* [x 0] body-with-recur)` → `(do (var name nil) (set name (fn [x] body)) (name 0))` -Recur rewrites to `(loop-name arg...)` via `:jolt/current-loop` binding. - -### Quote: use `raw-form->janet` converter, never re-analyze - -### Macro expansion: pass ctx to `analyze-form`, check `resolve-macro`, expand + re-analyze - -## Special Form Checklist - -To add a new special form to the evaluator AND compiler: - -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` - -The match arm receives `ctx`, `bindings`, and `form` (the full list). Use `(in form 1)` for first arg, etc. - -**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. - -### 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`, `.` - -## Compiler Development - -### Architecture -`src/jolt/compiler.janet` (721 lines). Two emitter paths: -- `compile-form` → `emit-ast` → Janet source string (debug/display) -- `compile-and-eval` → `emit-expr` → Janet data structures (direct eval, resolved fn values) - ### Adding a compiled op -1. **analyze-form**: add `match head-name` arm returning `{:op :your-op ...}` -2. **emit-ast**: add str function + `:your-op` case in `set emit-ast` dispatch -3. **emit-expr**: add expr function + `:your-op` case in `set emit-expr` dispatch +1. Add match arm in `analyze-form` — maps Clojure form → AST node +2. Add `emit-*-str` for source-to-source path → arm in `emit-ast` dispatch +3. Add `emit-*-expr` for data-structure path → arm in `emit-expr` dispatch 4. Add tests in `test/compiler-test.janet` ### Emit-expr critical rules @@ -128,45 +51,41 @@ The match arm receives `ctx`, `bindings`, and `form` (the full list). Use `(in f ### Macro expansion `analyze-form` checks `resolve-macro` first — if head is a macro var, applies fn, re-analyzes expanded form (only when ctx passed). -## Persistent Data Structures +### Loop/recur compilation +`(loop* [x 0] body)` → `(do (var name nil) (set name (fn [x] body)) (name 0))` +Recur rewrites to `(loop-name arg...)` via `:jolt/current-loop` binding. -### PersistentVector -Located in `src/jolt/clojure/lang/persistent_vector.clj`. -Loaded at init time by `load-persistent-structures` in `api.janet`. -32-way branching trie with tail optimization. +## Special Form Checklist -### PersistentHashMap -Located in `src/jolt/phm.janet` — Janet module, NOT a .clj file. -Imported via `(use ./phm)` in `src/jolt/core.janet`. -Bucket-based (8 buckets), each bucket is a flat `[k v k v ...]` array. -PHM is a table with magic type tag: `:jolt/deftype` = `"jolt.lang.persistent-hash-map.PersistentHashMap"`. -Key fields: `:cnt` (entry count), `:buckets` (array of arrays), `:_meta`. +To add a new special form to the evaluator AND compiler: +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` -API: `phm-get`, `phm-assoc`, `phm-dissoc`, `phm-contains?`, `phm-entries`, `phm-to-struct`. -`phm-to-struct` converts PHM back to Janet struct for compatibility with `keys`, `deep=`, etc. +The match arm receives `ctx`, `bindings`, and `form`. Use `(in form 1)` for first arg. -**Design decision**: PHM lives in a separate Janet module rather than embedded in core.janet, to avoid forward-reference issues (core-map? calls phm? which wasn't yet defined when core-map? was being compiled). +### Current special forms (29): +`quote`, `syntax-quote`, `unquote`, `unquote-splicing`, `do`, `if`, `def`, `defmacro`, `fn*`, `let*`, `loop*`, `recur`, `throw`, `try`, `set!`, `var`, `locking`, `instance?`, `defmulti`, `defmethod`, `deftype`, `new`, `.`, `var-get`, `var-set`, `var?`, `alter-var-root`, `find-var`, `alter-meta!`, `reset-meta!`, `intern` -**16 core functions updated** with PHM-aware branches: core-map?, core-hash-map, core-get, core-count, core-keys, core-vals, core-contains?, core-empty?, core-seq, core-conj, core-assoc, core-dissoc, core-merge, core-merge-with, core-into, core-=. +## PersistentHashMap (phm.janet) -### Working with PHM in core functions -- Check `(phm? coll)` FIRST, before generic struct/table checks -- Use `phm-get` not `(m key)` — PHM is a table but keys aren't direct properties -- Use `phm-to-struct` when you need Janet-native operations (deep=, keys, etc.) -- PHM equality: `core-=` converts both sides to structs via `phm-to-struct`, then uses `deep=` +`src/jolt/phm.janet` — separate module imported via `(use ./phm)` into `core.janet`. +PHM is a table with `:jolt/deftype` tag. Has `:cnt`, `:buckets` (array of 8 flat `[k v k v ...]` arrays), `:_meta`. +16 core functions updated with `(phm? x)` branches. `phm-to-struct` converts to Janet struct for compatibility. -### Loading persistent structures -Use `{:mutable? true}` in `init` to skip persistent types and use Janet-native structs. +### Janet `break` limitation +Janet `break` cannot be used inside `let` blocks — use `(var found nil)` + `(while ... (if condition (do (set found val) (break))))` pattern. -## Janet Gotchas +### core-binding: use array-map, not hash-map +`core-binding` macro must emit `(array-map ...)` for the binding frame — PHM's get is incompatible with `push-thread-bindings` var-get lookup. -- Bit operations (brshift, brushift, band) use 32-bit signed integers. Hash values can exceed 32-bit range. Use `(band x 0xFFFFFFFF)` before shifting. -- `deftype` creates tables, not structs. `struct?` returns false. -- `(get child :key)` DOES follow table prototype chain — resolved and confirmed working. -- Janet LSP produces many false positives on `.janet` files — safe to ignore. -- `break` can't be used inside `let` blocks — `break` returns from the innermost loop, and a `let` has no loop. Use `(var found nil)` pattern + `(while ... (if cond (do (set found val) (break))))` then check `found` after loop. -- Duplicate function definitions in the same file cause hard-to-diagnose "unknown symbol" errors. Always grep for the fn name before adding. +### core-intern gotcha +Janet does not support Clojure-style multi-arity destructuring `([a b] ...)` in `defn`. Use flat args or wrapper functions. ## Symbol representation -Jolt symbols are `{:jolt/type :symbol :ns :name }` as produced by the reader. \ No newline at end of file +Jolt symbols are `{:jolt/type :symbol :ns :name }` as produced by the reader. + +## Janet LSP + +Janet LSP produces many false positives on `.janet` files — safe to ignore. diff --git a/src/jolt/core.janet b/src/jolt/core.janet index a5a3641..0cb9e2f 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -17,7 +17,7 @@ (defn core-keyword? [x] (keyword? x)) (defn core-symbol? [x] (and (struct? x) (= :symbol (x :jolt/type)))) (defn core-vector? [x] (tuple? x)) -(defn core-map? [x] (or (phm? x) (struct? x))) +(defn core-map? [x] (or (phm? x) (struct? x) (if (and (table? x) (get x :jolt/deftype)) true false))) (defn core-seq? [x] (or (array? x) (tuple? x))) (defn core-coll? [x] (or (array? x) (tuple? x) (struct? x))) @@ -166,7 +166,10 @@ (and (number? key) (>= key 0) (< key (length coll))) false))))) -(defn core-count [coll] (if (phm? coll) (coll :cnt) (length coll))) +(defn core-count [coll] + (if (phm? coll) (coll :cnt) + (if (and (table? coll) (get coll :jolt/deftype)) (- (length (keys coll)) 1) + (length coll)))) (defn core-first [coll] (if (or (nil? coll) (= 0 (length coll))) nil @@ -888,24 +891,33 @@ (defn core-comment [& body] nil) -# defrecord stub — emits constructor and factory functions +# defrecord — creates a proper type via deftype + factory functions (defn core-defrecord [name-sym fields-vec & body] - (def ctor-name-str (string "->" (name-sym :name))) - (def ctor-name-sym {:jolt/type :symbol :ns nil :name ctor-name-str}) - # Build the map expression at macro-expansion time: {"":a a, ":b b, ...} - (var kvs @[]) + (def type-name (name-sym :name)) + (def type-name-dot (string type-name ".")) + (def arrow-name (string "->" type-name)) + (def map-name (string "map->" type-name)) + + # (deftype TypeName [fields...]) + (def dt-form @[{:jolt/type :symbol :ns nil :name "deftype"} name-sym fields-vec]) + + # Arrow factory: (def ->TypeName (fn [field1 field2 ...] (TypeName. field1 field2 ...))) + (def arrow-call @[{:jolt/type :symbol :ns nil :name type-name-dot}]) + (each f fields-vec (array/push arrow-call f)) + (def arrow-sym {:jolt/type :symbol :ns nil :name arrow-name}) + (def arrow-body @[{:jolt/type :symbol :ns nil :name "fn"} fields-vec arrow-call]) + + # map-> factory: (def map->TypeName (fn [m] (->TypeName (get m :field1) (get m :field2) ...))) + (def map-call @[{:jolt/type :symbol :ns nil :name arrow-name}]) (each f fields-vec - (array/push kvs (keyword (f :name))) - (array/push kvs f)) - (def map-expr @[{:jolt/type :symbol :ns nil :name "array-map"} ;kvs]) - (def ctor-body - @[{:jolt/type :symbol :ns nil :name "fn*"} - @[fields-vec] - map-expr]) - # Emit (do (def TypeName ) (def ->TypeName )) + (array/push map-call @[{:jolt/type :symbol :ns nil :name "core-get"} {:jolt/type :symbol :ns nil :name (string "m")} (keyword (f :name))])) + (def map-sym {:jolt/type :symbol :ns nil :name map-name}) + (def map-body @[{:jolt/type :symbol :ns nil :name "fn"} @[{:jolt/type :symbol :ns nil :name (string "m")}] map-call]) + @[{:jolt/type :symbol :ns nil :name "do"} - @[{:jolt/type :symbol :ns nil :name "def"} name-sym ctor-body] - @[{:jolt/type :symbol :ns nil :name "def"} ctor-name-sym ctor-body]]) + dt-form + @[{:jolt/type :symbol :ns nil :name "def"} arrow-sym arrow-body] + @[{:jolt/type :symbol :ns nil :name "def"} map-sym map-body]]) # prefer-method stub — multimethod preference ordering (defn core-prefer-method [multifn dispatch-val & dispatch-vals] diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index e788ef8..cdb3b62 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -545,31 +545,42 @@ (error err))) (eval-form ctx bindings body-form)))) "set!" (let [target (in form 1) - val (eval-form ctx bindings (in form 2))] - # (set! (. obj -field) val) — instance field mutation - (if (and (array? target) (> (length target) 0) - (struct? (first target)) - (= :symbol ((first target) :jolt/type)) - (= "." ((first target) :name))) - (let [obj (eval-form ctx bindings (in target 1)) - field-sym (in target 2) - field-name (field-sym :name) - field-key (keyword (if (and (> (length field-name) 0) (= "-" (string/slice field-name 0 1))) - (string/slice field-name 1) - field-name))] - (if (get obj :jolt/deftype) - (do (put obj field-key val) val) - (error (string "Can't set! field on non-deftype: " (type obj))))) - # (set! var val) — normal var mutation - (let [target-sym target - v (resolve-var ctx bindings target-sym)] - (if v - (do (var-set v val) val) - # Auto-create var if it doesn't exist - (let [ns-name (ctx-current-ns ctx) - ns (ctx-find-ns ctx ns-name)] - (def new-v (ns-intern ns (target-sym :name) val)) - val))))) + val (eval-form ctx bindings (in form 2))] + # Handle (set! (.-field obj) val) — .-field shorthand as a list + (if (and (array? target) (> (length target) 1) + (struct? (first target)) (= :symbol ((first target) :jolt/type)) + (> (length ((first target) :name)) 1) + (= (string/slice ((first target) :name) 0 2) ".-")) + (let [obj (eval-form ctx bindings (in target 1)) + field-name (string/slice ((first target) :name) 2) + field-key (keyword field-name)] + (if (get obj :jolt/deftype) + (do (put obj field-key val) val) + (error (string "Can't set! field on non-deftype: " (type obj))))) + # (set! (. obj -field) val) — instance field mutation + (if (and (array? target) (> (length target) 0) + (struct? (first target)) + (= :symbol ((first target) :jolt/type)) + (= "." ((first target) :name))) + (let [obj (eval-form ctx bindings (in target 1)) + field-sym (in target 2) + field-name (field-sym :name) + field-key (keyword (if (and (> (length field-name) 0) (= "-" (string/slice field-name 0 1))) + (string/slice field-name 1) + field-name))] + (if (get obj :jolt/deftype) + (do (put obj field-key val) val) + (error (string "Can't set! field on non-deftype: " (type obj))))) + # (set! var val) — normal var mutation + (let [target-sym target + v (resolve-var ctx bindings target-sym)] + (if v + (do (var-set v val) val) + # Auto-create var if it doesn't exist + (let [ns-name (ctx-current-ns ctx) + ns (ctx-find-ns ctx ns-name)] + (def new-v (ns-intern ns (target-sym :name) val)) + val)))))) "var" (let [target-sym (in form 1) v (resolve-var ctx bindings target-sym)] (if v v (error (string "Unable to resolve var: " (sym-name-str target-sym) " in var")))) diff --git a/test/compiler-test.janet b/test/compiler-test.janet index 7ce4680..5e9531f 100644 --- a/test/compiler-test.janet +++ b/test/compiler-test.janet @@ -287,3 +287,49 @@ (print " passed") (print "\nAll Phase 3 tests passed!") + +# ============================================================ +# 19. Phase 4: deftype +# ============================================================ +(print "19: deftype...") +(use ../src/jolt/api) + +(let [ctx (init)] + (eval-string ctx "(deftype Point [x y])") + (assert (not (nil? (eval-string ctx "(Point. 3 4)"))) "deftype constructs") + (assert (= 3 (eval-string ctx "(. (Point. 3 4) x)")) ".x access") + (assert (= 4 (eval-string ctx "(. (Point. 3 4) y)")) ".y access") + (assert (= 10 (eval-string ctx "(let [p (Point. 3 4)] (set! (.-x p) 10) (. p x))")) "set! field") + (assert (= true (eval-string ctx "(instance? Point (Point. 1 2))")) "instance?") + (assert (= false (eval-string ctx "(instance? Point {:x 1})")) "instance? false") + (assert (= 5 (eval-string ctx "(. (->Point 5 6) x)")) "arrow factory")) +(print " passed") + +# ============================================================ +# 20. Phase 4: defrecord +# ============================================================ +(print "20: defrecord...") +(let [ctx (init)] + (eval-string ctx "(defrecord Person [name age])") + (assert (not (nil? (eval-string ctx "(Person. \"Alice\" 30)"))) "record constructs") + (assert (= true (eval-string ctx "(map? (Person. \"Alice\" 30))")) "record is map?") + (assert (= "Alice" (eval-string ctx "(:name (Person. \"Alice\" 30))")) "keyword access") + (assert (= 30 (eval-string ctx "(get (Person. \"Alice\" 30) :age)")) "get access") + (assert (= "Alice" (eval-string ctx "(. (Person. \"Alice\" 30) name)")) ".name access") + (assert (= 30 (eval-string ctx "(. (Person. \"Alice\" 30) age)")) ".age access") + (assert (= 2 (eval-string ctx "(count (Person. \"Alice\" 30))")) "count") + (assert (= "Bob" (eval-string ctx "(:name (->Person \"Bob\" 25))")) "arrow factory")) + +(print " passed") + +# ============================================================ +# 21. Phase 4: record equality +# ============================================================ +(print "21: record equality...") +(let [ctx (init)] + (eval-string ctx "(defrecord Point3D [x y z])") + (assert (= true (eval-string ctx "(= (Point3D. 1 2 3) (Point3D. 1 2 3))")) "same values") + (assert (= false (eval-string ctx "(= (Point3D. 1 2 3) (Point3D. 4 5 6))")) "different values")) +(print " passed") + +(print "\nAll Phase 4 tests passed!")