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
tryform:(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)).tryseparate from([err]:(try body-form ([err] handler))is valid —body-formand([err]on same line.(string :kw)converts keyword to string. Janet has nonamefunction.(put {:x 1} :y 2)errors — structs don't supportput. Use@{}tables for mutable maps.(put table key nil)silently drops the key. Usebind-put+:jolt/nil-sentinelpattern.vardeclares mutable locals;def/letare 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
defnin Janet: Use(fn [& args] (case (length args) ...))pattern.defn-?doesn't exist. indexed?vsarray?: Reader produces tuples for vectors, arrays for lists. Checkindexed?for vector patterns.matchreturns nil on no match — used ineval-listfor non-symbol head fallthrough.(length struct)counts pairs, not keys. Use(length (keys struct)).(first struct)calls:jolt/typemethod — use(get struct :key).
Special Form Checklist
To add a new special form:
- Add the name to
special-symbol?insrc/jolt/evaluator.janet - Add a match arm in
eval-list - 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
&envbinding:(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:
bind-puthelper inevaluator.janetstores nil as:jolt/nilsentinelresolve-symunwraps:jolt/nilback tonil- Dynamic vars with nil values (
*1,*2,*3,*e) use:jolt/nil-sentinelincore-bindings init-core!checks for:jolt/nil-sentineland passes actual nil tons-intern