- Reorder load sequence: types→unrestrict→vars→lang→utils→namespaces→core
- Add if-let/when-let/if-some/when-some/let macros and register in core-macro-names
- Add stubs: resolve, update, copy-core-var, copy-var, macrofy, new-var, avoid-method-too-large
- Add dynamic vars: *1, *2, *3, *e, *assert with nil-sentinel
- Fix core-update: use put instead of Janet's update (struct-incompatible)
- Change *clojure-version* from struct to table (mutable for update)
- Fix core-avoid-method-too-large: return @{} not nil
- Fix init-core! nil-sentinel unwrapping
- Fix let* destructuring for {:keys [...]} and sequential patterns
- Fix fn*/defmacro: capture defining ns for symbol resolution in closures
5.8 KiB
jolt-bootstrap
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
All files parse cleanly. Eval status:
| File | Forms | Eval OK | Failures |
|---|---|---|---|
| sci.impl.macros | 4/4 | 4 | 0 |
| sci.impl.protocols | 15/17 | 15 | 2 (resolution stubs) |
| sci.impl.utils | 39/47 | 39 | 8 (multi-arity, missing deps) |
| sci.impl.types | 22/27 | 22 | 5 (resolution stubs) |
| sci.impl.unrestrict | 2/2 | 2 | 0 |
| sci.impl.vars | 28/28 | 28 | 0 |
| sci.lang | 10/10 | 10 | 0 |
| sci.ctx-store | 6/6 | 6 | 0 |
| sci.impl.namespaces | 93/98 | 93 | 5 (missing copy-core-var dep) |
| sci.core | 60/69 | 60 | 4 (*1/*2/*3/*e unresolved) |
Loading order: macros → protocols → types → unrestrict → vars(27/28, skip comment block) → lang → utils → ctx-store → namespaces → core
Added special forms: quote, syntax-quote, unquote, unquote-splicing, do, if, def, defmacro, fn*, let*, loop*, recur, throw, try, set!, var, locking, instance?, defmulti, defmethod, deftype, new, . (22 total)
Core additions: when (macro), defn (macro with docstring), declare (macro), fn (macro — wraps fn*), Object (interop stub), derive, isa?, ancestors, descendants (hierarchy stubs), defprotocol (macro), extend-type, extend-protocol (macro), extend (macro), reify, satisfies?, extends?, implements?, type->str, comment (macro), prefer-method (stub), unchecked-math (false), clojure-version ({:major 1 :minor 11})
Reader: #?(:clj ...), #?@(:clj ...) with splicing, #_ discard, #\ var-quote, ^ metadata, ; comments → skip, nil #?(:cljs ...) → skip (non-splicing), empty #?@(:cljs ...) → empty splice, unmatched )]} → explicit errors
Current blockers
sci.impl.copy-varsnot yet loaded — needed forcopy-core-var,copy-var,macrofy,new-varsci.impl.resolve,sci.impl.cljs,sci.impl.multimethods,sci.impl.deftypenot yet loaded- Multi-arity function dispatch edge cases in utils (forms 36-37, 44-45)
- ~9 remaining eval failures across namespaces (5) and sci.core (4) — all tracing back to missing deps
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.
Nil results (e.g. #?(:cljs X) on CLJ) now return {:jolt/type :jolt/skip} for non-splicing
and {:jolt/type :jolt/splice :items @[]} for splicing — preventing orphaned keys in maps.
Callable forms check
(if (function? f)
(apply f args)
(get f (first args))) ; table/struct lookup
unwrap-meta-name helper
Recursively unwraps (with-meta sym meta) to extract the underlying symbol.
Used in def, ns, deftype, defmethod to handle metadata-wrapped names.
deftype →TypeName constructor
deftype interns both TypeName and ->TypeName (Clojure arrow constructor convention).
bind-put helper and :jolt/nil sentinel
Janet's (put table key nil) silently drops the key, even on mutable @{} tables.
bind-put stores nil as :jolt/nil sentinel; resolve-sym unwraps :jolt/nil back to nil.
Must be used for ALL binding put calls: fn*, let*, loop*, macro bodies, deftype reify.
resolve-sym sentinels
:jolt/not-found— returned when a symbol is truly absent (distinct from nil binding):jolt/nil→ unwrapped to actualnil— nil values in bindings stored as sentinel due to Janetputnil-drop- Auto-refer fallback: unqualified symbols not found in current ns fall back to
clojure.core
Pitfalls
- Janet
letcan't bind to nil; use(var x nil)then(set x val) - Janet
(put table key nil)silently drops the key — usebind-puthelper for all binding tables (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 instead(last string)returns nil —lastworks only on indexed types. Use(s (- (length s) 1))for last char of string(set [a b] tuple)doesn't work — Janet'ssetdoesn't support destructuring. Use(tuple 0)/(tuple 1)#_discard works in lists, vectors, sets, and maps — wraps skipped form in{:jolt/type :jolt/skip}and readers check for this- Map reader must handle
:jolt/skipand:jolt/splicein both key and value positions commentmacro must be registered incore-macro-namesto avoid evaluating its body