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

5.4 KiB

jolt-dev

Jolt development workflow — build, test, special form patterns, Janet gotchas

Build & Test

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 .gitignored.

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

(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