jolt/.dirge/skills/jolt-dev/SKILL.md
2026-06-02 08:43:27 -04:00

109 lines
No EOL
5.4 KiB
Markdown

# jolt-dev
Jolt development workflow — build, test, special form patterns, Janet gotchas
## Build & Test
```bash
cd /Users/yogthos/src/jolt
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
```
Build artifacts are in `build/` which is `.gitignore`d.
## Architecture
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).
`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.
## SCI Submodule
SCI is at `vendor/sci` (git submodule, github.com/borkdude/sci). Load order: macros → protocols → types → unrestrict → vars → lang → utils → namespaces → core
## Project Structure
```
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 — clojure.core bindings: predicates, math, collections, macros, stubs
api.janet — public API: init, eval-string, eval-string*
main.janet — REPL entry point
test/
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
test-load-sci.janet — loads all sci files + tests eval-string
```
## Janet Gotchas
- **`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
(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)})
```
### core-name
Returns name string of keyword, symbol, or string. Uses `(string kw)` for keywords, `.name` field for symbols.
### 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`
### defrecord stub
Generates `->TypeName` positional constructor. Expands to `(do (def TypeName (fn* [fields] ...)) (def ->TypeName ...))`.
### doto macro
Uses `gensym` for the object symbol. Expands to `(let* [sym obj] (. sym method args)... sym)`.
### 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
## Janet Table Nil-Drop Workaround
`(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`