PersistentVector: 17-form .clj source, fully working.
- 32-way branching trie with tail optimization
- pv-conj, pv-nth, pv-assoc, pv-pop, vector?, vector constructor
- instance? check works on deftype tables
- .-field accessor syntax for deftype fields
api.janet: :mutable? compile flag for opt-out
- Default: persistent data structures loaded
- Pass {:mutable? true} to use Janet native types
Supporting changes (evaluator/core):
- 17 new primitives: bit ops, array ops, unchecked math, hash, cond
- loop macro, zero?, dec/inc, defn multi-arity
- instance? for deftype tables (get val :jolt/deftype)
- defrecord builds maps at expansion time
- .-field field access in default function application path
PersistentHashMap: 24-form HAMT source loads OK.
- BitmapIndexedNode/PersistentHashMap deftypes
- mask, bitpos, bit-count, index helpers
- phm-assoc/phm-get/phm-without/phm-contains? stubs
- bmn-assoc insert path structured, bitmap propagation wip
13 KiB
jolt-dev
Jolt development workflow — build, test, special form patterns, Janet gotchas
Jolt Development
Build & Test
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
Special Form Checklist
To add a new special form to the evaluator:
- Add the name to
special-symbol?insrc/jolt/evaluator.janet - Add a match arm in
eval-list(the match onname) - 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. This means (:ns &env) works because the head :ns is a keyword, not a symbol, so it's evaluated and called as a lookup.
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, .
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
&envbinding:(put new-bindings "&env" @{})— table, not struct (nil-safe) - Uses
parse-arg-namesfor& restarg handling
defmulti / defmethod
defmultistores the methods table on the var via(put v :jolt/methods methods), NOT on the function- Janet
puton a function value fails silently defmethodretrieves methods via(get mm-var :jolt/methods)usingresolve-varto 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-methodto work beforedefmulti print-methodis defined
deftype
- Handles
^:metaon type name viawith-metapattern - Fields vector handles
^:metaannotations and^Typehints — extracts actual symbol name - Produces a table with
:jolt/deftype "ns.TypeName"and keyword-keyed fields
Janet Gotchas
vardeclares mutable locals that can besetlater;def/letare immutableletcannot bind tonil— 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 matchis Janet's pattern matching — nocaseorcondneeded 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.,&envbinding@{}for macro bodies) matchwith string patterns returns nil (not error) when no pattern matches. Used ineval-listto handle non-symbol heads cleanly — keyword heads like:nsfall through to default function applicationbreakinwhiledoes 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/typemethod — 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 —lastworks 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'ssetdoesn't support destructuring. Use(tuple 0)/(tuple 1)or explicit individual assignments(get table key)does NOT walk prototypes set bytable/setproto. Use a custom walker:(var t bindings) (while t (when (in t key) (set result (in t key)) (break)) (set t (table/getproto t))).(in table key)DOES walk prototypes.returnfrom loop insidefnbody doesn't work — use mutablevar result+breakpattern instead.- Bit-shift functions:
blshift(left),brshift(right),brushift(unsigned right). NOTlshift/rshift/urshift.brushifton negative numbers errors. (hash x)built-in works on strings, keywords, numbers, tuples, structs, tables. Used directly as corehashbinding.
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:
(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:cljsbranch), 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).
fn* and defmacro namespace capture
fn* and defmacro must capture defining namespace at definition time and restore it during body evaluation:
(let [defining-ns (ctx-current-ns ctx)]
...closure body...
(def saved-ns (ctx-current-ns ctx))
(ctx-set-current-ns ctx defining-ns)
...eval body...
(ctx-set-current-ns ctx saved-ns))
Applies to single-arity, multi-arity fn*, and defmacro. Without this, closures evaluate in whatever ns happens to be current at call time.
Persistent data structures
- ClojureScript defines persistent vector as deftype in
cljs/core.cljc:PersistentVector(cnt, shift, root, tail, _meta),VectorNode(arr). 32-way branching trie with tail optimization. - Mutable arrays via
object-array,int-array,aset,aget,aclone,alength,to-array. Bit ops:bit-and/or/xor/not,bit-shift-left/right,unsigned-bit-shift-right. intcoercion viamath/trunc.unchecked-inc/dec/add/subtractfor fast path..cljsource files insrc/jolt/clojure/lang/. Loaded by Jolt reader/evaluator at init.
instance? and set! for deftype
instance?checks:jolt/deftypetag. Matches by suffix:(instance? PersistentVector x)→ check if(x :jolt/deftype)ends with"PersistentVector".set!handles(set! (. obj -field) val)for deftype field mutation. Strips leading-, converts to keyword,(put obj field-key val).
and / or macros
(and x y) → (let* [and__x x] (if and__x (and y) and__x))
(or x y) → (let* [or__x x] (if or__x or__x (or y)))
Registered as macros in core-macro-names.
defrecord macro
Builds key-value pairs at macro expansion time using array-map:
(var kvs @[])
(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])
Comment Handling
Comments ; in read-form return {:jolt/type :jolt/skip} sentinel:
(= c 59) # ;
[{:jolt/type :jolt/skip} line-end])
parse-next and parse-string both skip over :jolt/skip results:
parse-nextuses innerparse-next-loopthat recurses on skipparse-stringrecurses 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:
(= 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:
(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 byread-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-namesreturns@{"when" true "defn" true "declare" true "defprotocol" true "extend-type" true "extend-protocol" true "extend" true "reify" true "fn" true "proxy" true "definterface" true "comment" true "defn-" true}— a tableinit-core!calls(get (core-macro-names) name)to check, then(put v :macro true)- Order matters: macro functions must be defined BEFORE
core-bindingsmap references them
Core stubs for sci bootstrap
core-derive,core-isa?,core-ancestors,core-descendants— minimal hierarchycore-Object,core-Thread,core-ThreadLocal,core-IllegalStateException— JVM class stubscore-volatile!,core-vswap!,core-vreset!— volatile (atom-like table with :val key)core-defprotocolemits(do (def name @{}) (def method fn) ...)— macro, returns do formcore-extend-type,core-extend-protocol,core-extend,core-reify,core-satisfies?,core-extends?,core-implements?,core-type->str— protocol stubs
Namespace handling
nsform handles^:metaon ns name viawith-metapatterndefform also handles^:metaon def name (extracts name-sym from(with-meta Name meta))requireclause innswraps each spec in(when s ...)for nil-safetyresolve-varfalls back to checking clojure.core namespace if var not found in current ns
Bootstrap loading order
sci.impl.macros (4/4 ok)
sci.impl.protocols (15/17 ok)
sci.impl.utils (39/47 ok)
sci.impl.types (22/27 ok)
sci.impl.unrestrict (2/2 ok)
sci.impl.vars (28/28 ok — comment block parsed as skip)
sci.lang (10/10 ok — IVar via class-name resolve)
sci.ctx-store (6/6 ok)
sci.impl.namespaces (93/98 ok — parse crash at unmatched brace)
sci.core (60/69 ok — namespaces/*1/*2/*3/*e fail)
All .cljc files. #?(:clj ...) resolved at read time. #?(:cljs ...) returns nil.
parse-arg-names
- Handles
& restargs AND nested destructuring vectors - When an arg is a vector (not a symbol), recurses to extract nested symbol names
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 — 95+ clojure.core functions (map, filter, reduce, etc.)
api.janet — public API: init, eval-string, eval-string*
main.janet — REPL entry point with (defn main [&])
test/
evaluator-test.janet — special form tests (22 forms tested)
reader-test.janet — parser tests (includes #?, #?@, #_)
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