reader: #?@ empty splice fix (nil→@[]), #? nil→:jolt/skip, map reader handles #_/#?@ in K/V evaluator: unwrap-meta-name helper, deftype interns ->Name too, dot-suffix fix core: comment/prefer-method stubs, *unchecked-math*/*clojure-version* dynamic vars
4.1 KiB
| name | description |
|---|---|
| jolt-bootstrap | TDD workflow for bootstrapping a Clojure interpreter on Janet |
Bootstrapping a Clojure interpreter on Janet
Prerequisites
- Janet ≥ 1.36, jpm
- Target Clojure sources (e.g. sci) to load
- Jolt sources in
src/jolt/, tests intest/
TDD Loop
- Write a failing test in
test/<feature>-test.janetusing(use ../src/jolt/...)relative paths - Run with
janet test/<file>.janet(faster thanjpm testfor iteration) - If test involves
init(which loads clojure.core), also(use ../src/jolt/api) - Implement in
src/jolt/<module>.janet - Run test → see failure message → fix → repeat
- After passing:
jpm testto ensure no regressions
Current bootstrap progress
Loaded (all .cljc, #? resolved at read time):
sci.impl.macros— 4/4 (ns, defmacro deftime, defmacro usetime, deftime(? macro))sci.impl.protocols— 15/17sci.impl.utils— 39/47sci.impl.types— 22/27sci.impl.unrestrict— 2/2sci.impl.vars— 28/28 (comment block parses via :jolt/skip sentinel)sci.lang— 10/10 (IVar resolves via class-name pattern lookup)sci.ctx-store— 6/6sci.impl.namespaces— 93/98 (parse crash at unmatched brace)sci.core— 60/69 (namespaces/*1/*2/*3/*e unresolved)
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, .
Reader: #?(:clj ...), #?@(:clj ...) with splicing, #_ discard (returns :jolt/skip sentinel), #\ var-quote, ^ metadata. Comments ; skip via :jolt/skip. Closing delimiters ), ], } produce explicit "Unmatched" errors.
Core macros: when, defn (with docstring), defn-, declare, fn (wraps fn*), defprotocol, extend-type, extend-protocol, extend, reify, proxy, definterface, comment (ignores body), prefer-method (stub)
Key utilities: unwrap-meta-name — recursively unwraps (with-meta sym meta) to extract raw symbol. Used in def, ns, deftype, defmethod.
Class-name resolution: unqualified symbols with dots (Foo.Bar.Baz) are resolved by splitting at last dot into ns+name.
Key patterns
Symbol structs
{:jolt/type :symbol :ns <string-or-nil> :name <string>}
Macro intern marks var
(def v (ns-intern ns name macro-fn))
(put v :macro true)
Reader conditional #?
Resolves at read time: scans for :clj keyword, picks next form.
#?@ wraps resolved form in :jolt/splice struct for list/vec/set splicing.
Callable forms check
(if (function? f)
(apply f args)
(get f (first args))) ; table/struct lookup
Pitfalls
- Janet
letcan't bind to nil; use(var x nil)then(set x val) (get table)with 1 arg = compile error, use(table :key)shorthand(put fn :key val)fails on functions; stash metadata on vars insteaddeftypefield names must be keywords (not strings) for(inst :field)accessdefnplaced aftercore-bindingsthat reference it → compile error; order matters- Janet's
trymacro:(try body ([err] handler))— catch clause is tuple[binding body...] core-macro-namesis a zero-arg fn returning a table:(get (core-macro-names) name). Don't call it as(core-macro-names name)— that's arity mismatch- Janet
#{}sets can cause parse issues — use@[]instead for stub collections breakinwhiledoesn't return a value in Janet — use(var done nil)+(while (and cond (not done)) ... (set done result))pattern insteadread-reader-conditionalfor#?(:cljs X)with no:cljbranch returns[nil new-pos]. For#?@(:cljs X)wrapping, nil gets wrapped in splice struct with@[nil]items#_discard now works in lists, vectors, and sets — wraps the skipped form in{:jolt/type :jolt/skip}and readers check for thisread-regexnow works with(var done nil)pattern to return value from while loop#?@splicing inside vectors — if the resolved :clj branch is itself a vector, items are extracted and spliced. Works for both lists and vectors.