- types.janet: find-var (ctx-based var resolution), alter-meta!, reset-meta! - evaluator.janet: 10 new special-form dispatch arms for var ops - core.janet: 6 new Clojure-level wrappers in core-bindings - core-meta: add var-aware branch (was struct-only) - core-binding macro: use array-map instead of hash-map (PHM incompatibility) - 8 new tests (17: var system, 18: var metadata) — all pass - 317 total tests, 0 failures
7.5 KiB
| name | description |
|---|---|
| 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
janet -k file.janet # check parse without executing (useful for syntax validation)
Python3 batch edits
When the edit tool is blocked by syntax checker false positives on complex multi-line replacements, use python3 for batch text edits:
python3 << 'PYEOF'
with open('src/jolt/core.janet') as f:
content = f.read()
content = content.replace('old text', 'new text')
with open('src/jolt/core.janet', 'w') as f:
f.write(content)
PYEOF
Always run janet -k file.janet after to verify syntax.
Compiler Development
See jolt-compiler skill for the Clojure→Janet source-to-source compiler workflow.
Janet Eval Pipeline (critical)
Janet's (parse s) does NOT return a parsed form — it returns [symbol, error-position].
For evaluating Janet source strings, use the parser pipeline:
(def p (parser/new))
(parser/consume p source)
(parser/eof p) # REQUIRED — otherwise produce returns nil
(def form (parser/produce p))
(eval form)
Never try (eval [if true 1 2]) — Janet's eval doesn't recognize special forms in tuple data structures.
var vs def
When you need to mutate a local with set, use (var x nil) not (def x nil). def creates constants.
Compiler (see also jolt-compiler skill)
src/jolt/compiler.janet — Clojure→Janet source compiler with macro expansion.
test/compiler-test.janet — 11 test groups covering all ops.
Key design decision: compile-and-eval emits Janet DATA STRUCTURES, not source strings, because Janet's eval doesn't see use-imported symbols. core-fn-values table resolves Janet names to actual function values at compile time.
Compiler Development
Adding a new op to the compiler
- Add match arm in
analyze-form— maps Clojure form → AST node - Add
emit-*-strfor source-to-source path, then arm inemit-astdispatch - Add
emit-*-exprfor data-structure path, then arm inemit-exprdispatch - Add tests
Emitter patterns
String emitter: (buffer/push buf "...") → source text
Data-structure emitter: ['keyword val1 val2] → eval-able tuples
Janet eval gotchas
- Bare tuples are function calls — always use
['tuple ...]or(tuple ...) evalscope: symbols from(use ...)not available — embed function VALUES- Janet
try:(try body ([err] handler))— not(catch sym handler) - Core
-maps tocore-sub(NOTcore--)
Loop compilation
(loop* [x 0] body-with-recur) → (do (var name nil) (set name (fn [x] body)) (name 0))
Recur rewrites to (loop-name arg...) via :jolt/current-loop binding.
Quote: use raw-form->janet converter, never re-analyze
Macro expansion: pass ctx to analyze-form, check resolve-macro, expand + re-analyze
Special Form Checklist
To add a new special form to the evaluator AND compiler:
- 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.
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, .
Compiler Development
Architecture
src/jolt/compiler.janet (721 lines). Two emitter paths:
compile-form→emit-ast→ Janet source string (debug/display)compile-and-eval→emit-expr→ Janet data structures (direct eval, resolved fn values)
Adding a compiled op
- analyze-form: add
match head-namearm returning{:op :your-op ...} - emit-ast: add str function +
:your-opcase inset emit-astdispatch - emit-expr: add expr function +
:your-opcase inset emit-exprdispatch - Add tests in
test/compiler-test.janet
Emit-expr critical rules
- Vectors: wrap with
['tuple ...]— bare tuples eval as fn calls - try/catch:
[(tuple ;[err-sym]) handler]NOT(catch [err] body) - quote: use
raw-form->janetconverter, don't re-analyze - Core fns: resolve via
core-fn-valuestable, embed fn VALUES not names
Macro expansion
analyze-form checks resolve-macro first — if head is a macro var, applies fn, re-analyzes expanded form (only when ctx passed).
Persistent Data Structures
PersistentVector
Located in src/jolt/clojure/lang/persistent_vector.clj.
Loaded at init time by load-persistent-structures in api.janet.
32-way branching trie with tail optimization.
PersistentHashMap
Located in src/jolt/phm.janet — Janet module, NOT a .clj file.
Imported via (use ./phm) in src/jolt/core.janet.
Bucket-based (8 buckets), each bucket is a flat [k v k v ...] array.
PHM is a table with magic type tag: :jolt/deftype = "jolt.lang.persistent-hash-map.PersistentHashMap".
Key fields: :cnt (entry count), :buckets (array of arrays), :_meta.
API: phm-get, phm-assoc, phm-dissoc, phm-contains?, phm-entries, phm-to-struct.
phm-to-struct converts PHM back to Janet struct for compatibility with keys, deep=, etc.
Design decision: PHM lives in a separate Janet module rather than embedded in core.janet, to avoid forward-reference issues (core-map? calls phm? which wasn't yet defined when core-map? was being compiled).
16 core functions updated with PHM-aware branches: core-map?, core-hash-map, core-get, core-count, core-keys, core-vals, core-contains?, core-empty?, core-seq, core-conj, core-assoc, core-dissoc, core-merge, core-merge-with, core-into, core-=.
Working with PHM in core functions
- Check
(phm? coll)FIRST, before generic struct/table checks - Use
phm-getnot(m key)— PHM is a table but keys aren't direct properties - Use
phm-to-structwhen you need Janet-native operations (deep=, keys, etc.) - PHM equality:
core-=converts both sides to structs viaphm-to-struct, then usesdeep=
Loading persistent structures
Use {:mutable? true} in init to skip persistent types and use Janet-native structs.
Janet Gotchas
- Bit operations (brshift, brushift, band) use 32-bit signed integers. Hash values can exceed 32-bit range. Use
(band x 0xFFFFFFFF)before shifting. deftypecreates tables, not structs.struct?returns false.(get child :key)DOES follow table prototype chain — resolved and confirmed working.- Janet LSP produces many false positives on
.janetfiles — safe to ignore. breakcan't be used insideletblocks —breakreturns from the innermost loop, and alethas no loop. Use(var found nil)pattern +(while ... (if cond (do (set found val) (break))))then checkfoundafter loop.- Duplicate function definitions in the same file cause hard-to-diagnose "unknown symbol" errors. Always grep for the fn name before adding.
Symbol representation
Jolt symbols are {:jolt/type :symbol :ns <string-or-nil> :name <string>} as produced by the reader.