Initial commit: Jolt — Clojure interpreter on Janet

- PEG-based Clojure reader (symbols, keywords, numbers, strings, lists,
  vectors, maps, sets, quote forms, reader macros, metadata)
- Tree-walking evaluator (quote, do, if, def, fn*, let*, loop*/recur,
  syntax-quote/unquote/unquote-splicing, macro system, ns/require/in-ns)
- 95+ clojure.core functions (predicates, math, collections, seq ops,
  higher-order, atoms, I/O)
- Public API (init, eval-string, eval-string*)
- REPL (jolt/main.janet)
- 7 test suites, all green
- MIT license
This commit is contained in:
Yogthos 2026-06-01 16:48:56 -04:00
commit cdcf569506
16 changed files with 2696 additions and 0 deletions

37
src/jolt/api.janet Normal file
View file

@ -0,0 +1,37 @@
# Jolt Public API
# High-level interface for the Clojure-on-Janet interpreter.
(use ./types)
(use ./reader)
(use ./evaluator)
(use ./core)
(defn init
"Create a new Jolt evaluation context, optionally with opts.
(init) — empty context with clojure.core loaded
(init opts) — context with opts and clojure.core loaded
opts may contain:
:namespaces — map of {ns-name → {sym → value, ...}, ...}"
[&opt opts]
(default opts nil)
(let [ctx (make-ctx opts)]
(init-core! ctx)
ctx))
(defn eval-string
"Evaluate a Clojure source string in a Jolt context.
(eval-string ctx s) → value
Returns the result of evaluating the first form in s."
[ctx s]
(let [form (parse-string s)]
(eval-form ctx @{} form)))
(defn eval-string*
"Evaluate a Clojure source string in a Jolt context.
Like eval-string but with explicit bindings.
(eval-string* ctx s bindings) → value"
[ctx s bindings]
(let [form (parse-string s)]
(eval-form ctx bindings form)))

709
src/jolt/core.janet Normal file
View file

@ -0,0 +1,709 @@
# Jolt Core Library
# Clojure-compatible core functions for the Jolt interpreter.
(use ./types)
# ============================================================
# Predicates
# ============================================================
(defn core-nil? [x] (nil? x))
(defn core-some? [x] (not (nil? x)))
(defn core-string? [x] (string? x))
(defn core-number? [x] (number? x))
(defn core-fn? [x] (or (function? x) (cfunction? x)))
(defn core-keyword? [x] (keyword? x))
(defn core-symbol? [x] (and (struct? x) (= :symbol (x :jolt/type))))
(defn core-vector? [x] (tuple? x))
(defn core-map? [x] (struct? x))
(defn core-seq? [x] (or (array? x) (tuple? x)))
(defn core-coll? [x] (or (array? x) (tuple? x) (struct? x)))
(defn core-true? [x] (= true x))
(defn core-false? [x] (= false x))
(defn core-identical? [a b] (= a b))
(defn core-zero? [x] (and (number? x) (= x 0)))
(defn core-pos? [x] (and (number? x) (> x 0)))
(defn core-neg? [x] (and (number? x) (< x 0)))
(defn core-even? [n] (= 0 (% n 2)))
(defn core-odd? [n] (not= 0 (% n 2)))
(defn core-empty? [coll]
(if (nil? coll) true
(if (struct? coll) (= 0 (length (keys coll)))
(= 0 (length coll)))))
(defn core-every? [pred coll]
(var result true)
(each x coll (if (not (pred x)) (do (set result false) (break))))
result)
# ============================================================
# Math — Clojure semantics (variadic, / with one arg = reciprocal)
# ============================================================
(def core-+ (fn [& args] (if (= 0 (length args)) 0 (+ ;args))))
(def core-sub
(fn [& args]
(if (= 0 (length args))
(error "Wrong number of args (0) passed to: -")
(apply - args))))
(def core-* (fn [& args] (if (= 0 (length args)) 1 (* ;args))))
(def core-/
(fn [& args]
(case (length args)
0 (error "Wrong number of args (0) passed to: /")
1 (/ 1 (args 0))
(apply / args))))
(def core-inc inc)
(def core-dec dec)
(def core-mod %)
(def core-rem %)
(def core-quot (fn [n d] (math/floor (/ n d))))
(defn core-max [& args] (apply max args))
(defn core-min [& args] (apply min args))
# ============================================================
# Comparison
# ============================================================
(defn core-= [& args]
(if (< (length args) 2) true
(do
(var ok true)
(var i 0)
(while (and ok (< i (dec (length args))))
(if (not (deep= (args i) (args (+ i 1))))
(set ok false))
(++ i))
ok)))
(defn core-not= [& args] (not (apply core-= args)))
# ============================================================
# Collections
# ============================================================
(defn core-conj [coll & xs]
(if (tuple? coll)
# vector: add to end
(tuple/slice (tuple ;(array/concat (array/slice coll) xs)))
(if (array? coll)
# list: add to front (reverse xs, push each)
(do
(var result coll)
(var i 0)
(while (< i (length xs))
(set result (array/insert result 0 (xs i)))
(++ i))
result)
# struct/map: add [k v] pairs
(do
(var result coll)
(var i 0)
(while (< i (length xs))
(let [pair (xs i)]
(set result (merge result {(pair 0) (pair 1)})))
(++ i))
result))))
(defn core-assoc [m & kvs]
(var result @{})
(when m
(each k (if (struct? m) (keys m) (keys (table ;(pairs m))))
(put result k (get m k))))
(var i 0)
(while (< i (length kvs))
(let [k (kvs i) v (kvs (+ i 1))]
(put result k v)
(+= i 2)))
(if (struct? m) (table/to-struct result) result))
(defn core-dissoc [m & ks]
(var result @{})
(each k (keys m)
(var in-ks false)
(each k2 ks
(if (deep= k k2) (do (set in-ks true) (break))))
(if (not in-ks) (put result k (m k))))
(if (struct? m) (table/to-struct result) result))
(defn core-get [m k &opt default]
(default default nil)
(if (nil? m) default
(if (or (struct? m) (table? m))
(let [v (m k)]
(if (nil? v) default v))
(if (and (or (tuple? m) (array? m)) (number? k) (>= k 0) (< k (length m)))
(in m k)
default))))
(defn core-get-in [m ks &opt default]
(default default nil)
(var current m)
(var i 0)
(while (< i (length ks))
(if (nil? current) (break))
(set current (core-get current (ks i)))
(++ i))
(if (nil? current) default current))
(defn core-contains? [coll key]
(if (struct? coll) (not (nil? (coll key)))
(if (table? coll) (not (nil? (coll key)))
(if (or (tuple? coll) (array? coll))
(and (number? key) (>= key 0) (< key (length coll)))
false))))
(def core-count length)
(defn core-first [coll]
(if (or (nil? coll) (= 0 (length coll))) nil
(in coll 0)))
(defn core-rest [coll]
(if (or (nil? coll) (= 0 (length coll)))
@[]
(if (tuple? coll)
(tuple/slice coll 1)
(array/slice coll 1))))
(defn core-next [coll]
(let [r (core-rest coll)]
(if (= 0 (length r)) nil r)))
(defn core-cons [x coll]
(if (nil? coll)
@[x]
(if (tuple? coll)
(tuple/slice (tuple ;(array/insert (array/slice coll) 0 x)))
(array/insert coll 0 x))))
(defn core-seq [coll]
(if (or (nil? coll) (and (or (tuple? coll) (array? coll)) (= 0 (length coll))))
nil
(if (tuple? coll) (tuple/slice coll)
(if (string? coll) (map |(string/from-bytes $) (string/bytes coll))
(if (struct? coll) (tuple ;(keys coll))
coll)))))
(defn core-vec [coll]
(if (tuple? coll) coll
(if (array? coll) (tuple ;coll)
(if (struct? coll) (tuple ;(map |(in (kvs coll) (+ (* $ 2) 1)) (range (/ (length (kvs coll)) 2))))
(tuple)))))
(defn core-into [to from]
(if (tuple? to)
(tuple/slice (tuple ;(array/concat (array/slice to) (if (tuple? from) from (array/slice from)))))
(if (array? to)
(array/concat to from)
(if (struct? to)
(do
(var result to)
(each [k v] (pairs from)
(set result (merge result {k v})))
result)
to))))
(defn core-merge [& maps]
(var result (struct))
(each m maps
(set result (merge result m)))
result)
(defn core-merge-with [f & maps]
(var result @{})
(each m maps
(each k (keys m)
(let [existing (result k)]
(put result k (if (nil? existing) (m k) (f existing (m k)))))))
(table/to-struct result))
(defn core-keys [m]
(tuple ;(keys m)))
(defn core-vals [m]
(tuple ;(map |(m $) (keys m))))
(defn core-select-keys [m ks]
(var result @{})
(each k ks
(let [v (core-get m k)]
(if (not (nil? v)) (put result k v))))
(if (struct? m) (table/to-struct result) result))
(defn core-zipmap [ks vs]
(var result @{})
(var i 0)
(while (and (< i (length ks)) (< i (length vs)))
(put result (ks i) (vs i))
(++ i))
(table/to-struct result))
# ============================================================
# Sequence operations
# ============================================================
(defn core-map [f & colls]
(let [first-coll (colls 0)
result (if (= 1 (length colls))
(array ;(map f first-coll))
(do
(var res @[])
(var idxs @{})
(each _ first-coll (array/push idxs 0))
(var done false)
(while (not done)
(var args @[])
(var i 0)
(while (< i (length colls))
(let [c (colls i) j (idxs i)]
(if (>= j (length c))
(do (set done true) (break))
(array/push args (c j))))
(++ i))
(if (not done) (array/push res (apply f args)))
(var k 0)
(while (< k (length colls))
(set (idxs k) (+ (idxs k) 1))
(++ k)))
res))]
(if (tuple? first-coll) (tuple/slice (tuple ;result)) result)))
(defn core-filter [pred coll]
(var result @[])
(each x coll
(if (pred x) (array/push result x)))
(if (tuple? coll) (tuple/slice (tuple ;result)) result))
(defn core-remove [pred coll]
(core-filter (fn [x] (not (pred x))) coll))
(def core-reduce
(fn [& args]
(case (length args)
2 (let [f (args 0) coll (args 1)]
(if (= 0 (length coll))
(f)
(do
(var acc (coll 0))
(var i 1)
(while (< i (length coll))
(set acc (f acc (coll i)))
(++ i))
acc)))
3 (let [f (args 0) val (args 1) coll (args 2)]
(var acc val)
(each x coll (set acc (f acc x)))
acc)
(error "Wrong number of args passed to: reduce"))))
(defn core-take [n coll]
(var result @[])
(var i 0)
(while (and (< i n) (< i (length coll)))
(array/push result (coll i))
(++ i))
(if (tuple? coll) (tuple/slice (tuple ;result)) result))
(defn core-drop [n coll]
(if (tuple? coll)
(tuple/slice coll (min n (length coll)))
(array/slice coll (min n (length coll)))))
(defn core-take-while [pred coll]
(var result @[])
(each x coll
(if (pred x) (array/push result x) (break)))
(if (tuple? coll) (tuple/slice (tuple ;result)) result))
(defn core-drop-while [pred coll]
(var start 0)
(while (and (< start (length coll)) (pred (coll start)))
(++ start))
(if (tuple? coll)
(tuple/slice coll start)
(array/slice coll start)))
(defn core-concat [& colls]
(var result @[])
(each c colls
(each x c (array/push result x)))
result)
(defn core-reverse [coll]
(var result @[])
(var i (dec (length coll)))
(while (>= i 0)
(array/push result (coll i))
(-- i))
(if (tuple? coll) (tuple/slice (tuple ;result)) result))
(defn core-sort [coll]
(let [arr (if (tuple? coll) (array/slice coll) coll)
sorted (sort arr)]
(if (tuple? coll) (tuple/slice (tuple ;sorted)) sorted)))
(defn core-sort-by [keyfn coll]
(let [arr (if (tuple? coll) (array/slice coll) coll)
sorted (sort-by keyfn arr)]
(if (tuple? coll) (tuple/slice (tuple ;sorted)) sorted)))
(defn core-distinct [coll]
(var seen @{})
(var result @[])
(each x coll
(if (nil? (seen x))
(do
(put seen x true)
(array/push result x))))
(if (tuple? coll) (tuple/slice (tuple ;result)) result))
(defn core-group-by [f coll]
(var result @{})
(each x coll
(let [k (f x)]
(put result k (array/push (core-get result k @[]) x))))
result)
(defn core-frequencies [coll]
(core-group-by identity coll))
(defn core-partition [n coll]
(var result @[])
(var i 0)
(while (< i (length coll))
(var part @[])
(var j 0)
(while (and (< j n) (< (+ i j) (length coll)))
(array/push part (coll (+ i j)))
(++ j))
(if (= (length part) n) (array/push result (tuple/slice (tuple ;part))))
(+= i n))
result)
(defn core-partition-by [f coll]
(var result @[])
(var part @[])
(var last-k nil)
(each x coll
(let [k (f x)]
(if (and last-k (deep= k last-k))
(array/push part x)
(do
(if (> (length part) 0) (array/push result (tuple/slice (tuple ;part))))
(set part @[x])
(set last-k k)))))
(if (> (length part) 0) (array/push result (tuple/slice (tuple ;part))))
result)
# ============================================================
# Sequence generators
# ============================================================
(def core-range
(fn [& args]
(let [start (if (> (length args) 1) (args 0) 0)
end (if (> (length args) 1) (args 1) (args 0))
step (if (> (length args) 2) (args 2) 1)]
(var result @[])
(var i start)
(while (if (pos? step) (< i end) (> i end))
(array/push result i)
(+= i step))
(tuple/slice (tuple ;result)))))
(def core-repeat (fn [n x]
(var result @[])
(var i 0)
(while (< i n)
(array/push result x)
(++ i))
result))
(defn core-repeatedly [n f]
(var result @[])
(var i 0)
(while (< i n)
(array/push result (f))
(++ i))
result)
# ============================================================
# Higher-order functions
# ============================================================
(def core-identity (fn [x] x))
(def core-constantly (fn [x] (fn [& _] x)))
(defn core-complement [f]
(fn [& args] (not (apply f args))))
(def core-comp
(fn [& fs]
(case (length fs)
0 identity
1 (fs 0)
2 (let [f (fs 0) g (fs 1)] (fn [& args] (f (apply g args))))
(let [f (last fs)
gs (array/slice fs 0 (dec (length fs)))]
(fn [& args]
(var result (apply (last gs) args))
(var i (- (length gs) 2))
(while (>= i 0)
(set result ((gs i) result))
(-- i))
(f result))))))
(defn core-partial [f & args]
(fn [& more] (apply f (array/concat (array/slice args) more))))
(defn core-juxt [& fs]
(fn [& args]
(tuple ;(map |(apply $ args) fs))))
(defn core-memoize [f]
(var cache @{})
(fn [& args]
(let [key (tuple ;args)]
(if-let [v (get cache key)]
v
(let [result (apply f args)]
(put cache key result)
result)))))
# ============================================================
# Collection constructors
# ============================================================
(defn core-vector [& xs] (tuple ;xs))
(defn core-hash-map [& kvs]
(var result @{})
(var i 0)
(while (< i (length kvs))
(put result (kvs i) (kvs (+ i 1)))
(+= i 2))
(table/to-struct result))
(defn core-array-map [& kvs]
(var result @{})
(var i 0)
(while (< i (length kvs))
(put result (kvs i) (kvs (+ i 1)))
(+= i 2))
(table/to-struct result))
(defn core-hash-set [& xs]
(var result @{})
(each x xs (put result x true))
{:jolt/type :jolt/set :value (tuple ;(keys result))})
(defn core-set [coll]
(apply core-hash-set (if (tuple? coll) (array/slice coll) coll)))
(defn core-list [& xs]
(array ;xs))
# ============================================================
# String functions
# ============================================================
(defn core-str [& xs]
(if (= 0 (length xs)) ""
(do
(var result @[])
(each x xs
(if (nil? x) nil # skip nil
(array/push result (if (string? x) x (string x)))))
(string/join result ""))))
(def core-subs
(fn [& args]
(case (length args)
2 (string/slice (args 0) (args 1))
3 (string/slice (args 0) (args 1) (args 2))
(error "Wrong number of args passed to: subs"))))
# ============================================================
# I/O — minimal wrappers
# ============================================================
(def core-print print)
(def core-println (fn [& xs] (apply print xs) (print "\n") nil))
(defn core-pr [& xs]
(var i 0)
(while (< i (length xs))
(if (> i 0) (prin " "))
(prin (xs i))
(++ i))
nil)
(defn core-prn [& xs]
(apply core-pr xs)
(print "\n")
nil)
# ============================================================
# Atom
# ============================================================
(defn core-atom [val]
@{:jolt/type :jolt/atom :value val :watches @{}})
(defn core-atom? [x]
(and (table? x) (= :jolt/atom (x :jolt/type))))
(defn core-deref [ref]
(cond
# Jolt atom
(and (table? ref) (= :jolt/atom (ref :jolt/type)))
(ref :value)
# Jolt var (from types.janet)
(and (table? ref) (= :jolt/var (ref :jolt/type)))
(ref :root)
# default: return as-is
ref))
(defn core-reset! [atm val]
(put atm :value val)
val)
(defn core-swap! [atm f & args]
(let [new-val (apply f (atm :value) args)]
(put atm :value new-val)
new-val))
# ============================================================
# Threading macros (as regular functions? No, as macros in Clojure)
# These need to be defined as macros in the Jolt namespace system.
# For now, skip — they need proper macro definition via the evaluator.
# ============================================================
# ============================================================
# Initialization — intern everything into a context's namespace
# ============================================================
(def- core-bindings
"Map of symbol name → function for all core functions."
@{"nil?" core-nil?
"some?" core-some?
"string?" core-string?
"number?" core-number?
"fn?" core-fn?
"keyword?" core-keyword?
"symbol?" core-symbol?
"vector?" core-vector?
"map?" core-map?
"seq?" core-seq?
"coll?" core-coll?
"true?" core-true?
"false?" core-false?
"identical?" core-identical?
"zero?" core-zero?
"pos?" core-pos?
"neg?" core-neg?
"even?" core-even?
"odd?" core-odd?
"empty?" core-empty?
"every?" core-every?
"+" core-+
"-" core-sub
"*" core-*
"/" core-/
"inc" core-inc
"dec" core-dec
"mod" core-mod
"rem" core-rem
"quot" core-quot
"max" core-max
"min" core-min
"=" core-=
"not=" core-not=
"conj" core-conj
"assoc" core-assoc
"dissoc" core-dissoc
"get" core-get
"get-in" core-get-in
"contains?" core-contains?
"count" core-count
"first" core-first
"rest" core-rest
"next" core-next
"cons" core-cons
"seq" core-seq
"vec" core-vec
"into" core-into
"merge" core-merge
"merge-with" core-merge-with
"keys" core-keys
"vals" core-vals
"select-keys" core-select-keys
"zipmap" core-zipmap
"map" core-map
"filter" core-filter
"remove" core-remove
"reduce" core-reduce
"take" core-take
"drop" core-drop
"take-while" core-take-while
"drop-while" core-drop-while
"concat" core-concat
"reverse" core-reverse
"sort" core-sort
"sort-by" core-sort-by
"distinct" core-distinct
"group-by" core-group-by
"frequencies" core-frequencies
"partition" core-partition
"partition-by" core-partition-by
"range" core-range
"repeat" core-repeat
"repeatedly" core-repeatedly
"identity" core-identity
"constantly" core-constantly
"complement" core-complement
"comp" core-comp
"partial" core-partial
"juxt" core-juxt
"memoize" core-memoize
"vector" core-vector
"hash-map" core-hash-map
"array-map" core-array-map
"hash-set" core-hash-set
"set" core-set
"list" core-list
"str" core-str
"subs" core-subs
"print" core-print
"println" core-println
"pr" core-pr
"prn" core-prn
"atom" core-atom
"atom?" core-atom?
"deref" core-deref
"reset!" core-reset!
"swap!" core-swap!})
(def init-core!
(fn [& args]
(case (length args)
1 (let [ctx (args 0)
ns (ctx-find-ns ctx "clojure.core")]
(loop [[name fn] :pairs core-bindings]
(ns-intern ns name fn))
ns)
2 (let [ctx (args 0) ns-name (args 1)
ns (ctx-find-ns ctx ns-name)]
(loop [[name fn] :pairs core-bindings]
(ns-intern ns name fn))
ns)
(error "Wrong number of args passed to: init-core!"))))

264
src/jolt/evaluator.janet Normal file
View file

@ -0,0 +1,264 @@
# Jolt Evaluator
# Direct interpreter for Clojure forms on Janet.
(use ./types)
(defn- sym-name?
[sym-s name-str]
(and (struct? sym-s) (= :symbol (sym-s :jolt/type)) (= name-str (sym-s :name))))
(defn- special-symbol?
[name]
(or (= name "quote") (= name "syntax-quote") (= name "unquote")
(= name "unquote-splicing") (= name "do") (= name "if")
(= name "def") (= name "fn*") (= name "let*") (= name "loop*")
(= name "recur")))
(var eval-form nil)
(defn- syntax-quote*
[ctx bindings form]
(cond
(and (array? form) (> (length form) 0) (sym-name? (first form) "unquote"))
(eval-form ctx bindings (in form 1))
(and (array? form) (> (length form) 0) (sym-name? (first form) "unquote-splicing"))
(error "~@ used outside of a list or vector in syntax-quote")
(or (number? form) (string? form) (keyword? form) (nil? form) (= true form) (= false form))
form
(and (struct? form) (= :symbol (form :jolt/type)))
(if (nil? (form :ns))
(if (special-symbol? (form :name)) form
{:jolt/type :symbol :ns (ctx-current-ns ctx) :name (form :name)})
form)
(tuple? form)
(do (var result @[]) (var i 0) (while (< i (length form))
(let [item (in form i)]
(if (and (array? item) (> (length item) 0) (sym-name? (first item) "unquote-splicing"))
(each v (eval-form ctx bindings (in item 1)) (array/push result v))
(array/push result (syntax-quote* ctx bindings item))))
(++ i)) (tuple ;result))
(array? form)
(do (var result @[]) (var i 0) (while (< i (length form))
(let [item (in form i)]
(if (and (array? item) (> (length item) 0) (sym-name? (first item) "unquote-splicing"))
(each v (eval-form ctx bindings (in item 1)) (array/push result v))
(array/push result (syntax-quote* ctx bindings item))))
(++ i)) result)
(and (struct? form) (get form :jolt/type)) form
(struct? form)
(do (var kvs @[]) (each k (keys form)
(array/push kvs (syntax-quote* ctx bindings k))
(array/push kvs (syntax-quote* ctx bindings (get form k)))) (struct ;kvs))
form))
(defn- resolve-var
[ctx bindings sym-s]
(let [name (sym-s :name) ns (sym-s :ns)]
(if (not (nil? ns))
(let [target-ns (ctx-find-ns ctx ns)] (ns-find target-ns name))
(if (get bindings name) nil
(let [current-ns (ctx-current-ns ctx) ns (ctx-find-ns ctx current-ns)] (ns-find ns name))))))
(defn- sym-name-str
[sym-s]
(if (sym-s :ns) (string (sym-s :ns) "/" (sym-s :name)) (sym-s :name)))
(defn- eval-require
[ctx spec]
(let [ns-sym (in spec 0)
ns-name (sym-name-str ns-sym)]
(var alias nil)
(var i 1)
(let [slen (length spec)]
(while (< i slen)
(let [item (in spec i)]
(if (or (= item :as) (and (struct? item) (= :symbol (item :jolt/type)) (= "as" (item :name))))
(do
(def alias-sym (in spec (+ i 1)))
(set alias (alias-sym :name))
(set i slen))
(++ i)))))
(ctx-find-ns ctx ns-name)
(when alias
(let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx))]
(ns-import current-ns alias ns-name)))
nil))
(defn- resolve-sym
[ctx bindings sym-s]
(let [name (sym-s :name) ns (sym-s :ns)]
(if (not (nil? ns))
(let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx)) aliased-ns (ns-import-lookup current-ns ns)]
(if aliased-ns
(let [target-ns (ctx-find-ns ctx aliased-ns) v (ns-find target-ns name)]
(if v (var-get v) (error (string "Unable to resolve symbol: " ns "/" name))))
(let [target-ns (ctx-find-ns ctx ns) v (ns-find target-ns name)]
(if v (var-get v) (error (string "Unable to resolve symbol: " ns "/" name))))))
(let [local (get bindings name)]
(if (not (nil? local)) local
(let [current-ns (ctx-current-ns ctx) ns (ctx-find-ns ctx current-ns) v (ns-find ns name)]
(if v (var-get v)
# Check clojure.core as auto-referred fallback
(let [core-ns (ctx-find-ns ctx "clojure.core")
core-v (ns-find core-ns name)]
(if core-v
(var-get core-v)
# Fall back to Janet's global environment
(let [root-env (fiber/getenv (fiber/current))
entry (in root-env (symbol name))]
(if (not (nil? entry))
(if (table? entry) (entry :value) entry)
(error (string "Unable to resolve symbol: " name)))))))))))))
# Dispatch a special form by its string name. Each branch is a standalone
# expression that returns the value directly — no cond, no nested if chains.
# We use a local function per form and call the matching one.
(defn- eval-list
[ctx bindings form]
(def first-form (first form))
(def name (first-form :name))
(match name
"quote" (in form 1)
"syntax-quote" (syntax-quote* ctx bindings (in form 1))
"unquote" (error "Unquote not valid outside of syntax-quote")
"unquote-splicing" (error "Unquote-splicing not valid outside of syntax-quote")
"do" (do
(var result nil)
(var i 1)
(let [len (length form)]
(while (< i len)
(set result (eval-form ctx bindings (in form i)))
(++ i)))
result)
"if" (let [test-val (eval-form ctx bindings (in form 1))]
(if (and (not (nil? test-val)) (not (= false test-val)))
(eval-form ctx bindings (in form 2))
(if (> (length form) 3) (eval-form ctx bindings (in form 3)) nil)))
"def" (let [name-sym (in form 1)
val (eval-form ctx bindings (in form 2))
ns-name (ctx-current-ns ctx)
ns (ctx-find-ns ctx ns-name)]
(ns-intern ns (name-sym :name) val)
(var-get (ns-intern ns (name-sym :name))))
"ns" (let [ns-name (sym-name-str (in form 1))
clauses (tuple/slice form 2)]
(ctx-set-current-ns ctx ns-name)
(ctx-find-ns ctx ns-name)
(var result nil)
(var i 0)
(let [clen (length clauses)]
(while (< i clen)
(let [clause (in clauses i)]
(if (and (array? clause) (> (length clause) 0) (= :require (first clause)))
(let [specs (tuple/slice clause 1)
slen (length specs)]
(var j 0)
(while (< j slen)
(let [s (in specs j)]
(eval-require ctx s))
(++ j))
(set i (+ i 1)))
(do (set result clause) (++ i))))))
result)
"require" (let [spec (eval-form ctx bindings (in form 1))]
(if (and (tuple? spec) (> (length spec) 0))
(eval-require ctx spec)
(error "require expects a vector spec")))
"in-ns" (let [ns-name (sym-name-str (in form 1))]
(ctx-set-current-ns ctx ns-name)
(ctx-find-ns ctx ns-name)
nil)
"fn*" (let [args-form (in form 1)
body (tuple/slice form 2)
arg-names (map |($ :name) args-form)]
(var self nil)
(set self (fn [& fn-args]
(var fn-bindings @{})
(table/setproto fn-bindings bindings)
(var i 0)
(each arg-name arg-names
(put fn-bindings arg-name (fn-args i))
(++ i))
(put fn-bindings :jolt/loop-fn self)
(var result nil)
(each body-form body
(set result (eval-form ctx fn-bindings body-form)))
result))
self)
"let*" (let [bind-vec (in form 1)
body (tuple/slice form 2)]
(var new-bindings @{})
(table/setproto new-bindings bindings)
(var i 0)
(let [len (length bind-vec)]
(while (< i len)
(let [sym (bind-vec i)
val (eval-form ctx new-bindings (bind-vec (+ i 1)))]
(put new-bindings (sym :name) val)
(+= i 2))))
(var result nil)
(each body-form body
(set result (eval-form ctx new-bindings body-form)))
result)
"loop*" (let [bind-vec (in form 1)
body (tuple/slice form 2)
init-vals @[]
sym-names @[]]
(var i 0)
(while (< i (length bind-vec))
(array/push init-vals (eval-form ctx bindings (bind-vec (+ i 1))))
(array/push sym-names ((bind-vec i) :name))
(+= i 2))
(var loop-fn nil)
(set loop-fn (fn [& args]
(var loop-bindings @{})
(table/setproto loop-bindings bindings)
(var j 0)
(each sn sym-names
(put loop-bindings sn (args j))
(++ j))
(put loop-bindings :jolt/loop-fn loop-fn)
(var result nil)
(each body-form body
(set result (eval-form ctx loop-bindings body-form)))
result))
(apply loop-fn init-vals))
"recur" (let [loop-fn (get bindings :jolt/loop-fn)]
(if (nil? loop-fn)
(error "recur used outside of loop* or fn*")
(let [args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(apply loop-fn args))))
# default: function application — check for macros
(if (and (struct? first-form) (= :symbol (first-form :jolt/type)))
(let [v (resolve-var ctx bindings first-form)]
(if (and v (var-macro? v))
(let [macro-fn (var-get v)
args (tuple/slice form 1)]
(eval-form ctx bindings (apply macro-fn args)))
(let [f (eval-form ctx bindings first-form)
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(apply f args))))
(let [f (eval-form ctx bindings first-form)
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(apply f args)))))
(set eval-form (fn [ctx bindings form]
(cond
(nil? form) nil
(number? form) form
(string? form) form
(keyword? form) form
(bytes? form) form
(buffer? form) form
(tuple? form) (tuple/slice (map |(eval-form ctx bindings $) form))
(struct? form)
(if (= :symbol (form :jolt/type))
(resolve-sym ctx bindings form)
(if (get form :jolt/type)
(error (string "Unexpected tagged form: " (form :jolt/type)))
form))
(array? form)
(if (= 0 (length form))
@[]
(eval-list ctx bindings form))
form)))

34
src/jolt/main.janet Normal file
View file

@ -0,0 +1,34 @@
# Jolt REPL
# Read-eval-print loop for Clojure expressions.
(use ./api)
(use ./types)
(def ctx (init))
(defn read-line [prompt]
(prin prompt)
(flush)
(let [line (file/read stdin :line)]
(if line (string/trim line) nil)))
(defn print-value [v]
(if (nil? v)
(print "nil")
(if (and (table? v) (= :jolt/var (v :jolt/type)))
(printf "#'%s/%s" (ctx-current-ns ctx) ((var-name v) :name))
(print v))))
(print "Jolt — Clojure on Janet")
(print "Type (exit) to quit.\n")
(var running true)
(while running
(let [line (read-line (string (ctx-current-ns ctx) "=> "))]
(if (nil? line) (set running false)
(if (= line "(exit)") (set running false)
(if (not (= "" line))
(try
(print-value (eval-string ctx line))
([err]
(eprint "Error: " err))))))))

383
src/jolt/reader.janet Normal file
View file

@ -0,0 +1,383 @@
# Jolt Clojure Reader
# Recursive descent parser for Clojure source text.
# Output convention:
# Symbols foo, foo/bar → {:jolt/type :symbol :ns "foo" :name "bar"}
# Keywords :foo, :foo/bar → Janet keyword :foo, :foo/bar
# Lists (a b c) → Janet array @[a b c]
# Vectors [a b c] → Janet tuple [a b c]
# Maps {:a 1} → Janet struct {:a 1}
# Sets #{1 2} → tagged struct {:jolt/type :jolt/set :value [1 2]}
# Forward declaration for mutual recursion
(var read-form nil)
(def whitespace-chars " \t\n\r,")
(defn whitespace? [c]
(or (= c 32) # space
(= c 10) # \n
(= c 9) # \t
(= c 13) # \r
(= c 44))) # comma
(defn skip-whitespace [s pos]
(if (and (< pos (length s))
(whitespace? (s pos)))
(skip-whitespace s (+ pos 1))
pos))
(defn digit? [c]
(and (>= c 48) (<= c 57)))
(defn hex-digit? [c]
(or (and (>= c 48) (<= c 57))
(and (>= c 65) (<= c 70))
(and (>= c 97) (<= c 102))))
(defn symbol-start? [c]
(or (and (>= c 65) (<= c 90)) # A-Z
(and (>= c 97) (<= c 122)) # a-z
(= c 42) # *
(= c 43) # +
(= c 33) # !
(= c 95) # _
(= c 45) # -
(= c 63) # ?
(= c 46) # .
(= c 60) # <
(= c 62) # >
(= c 61) # =
(= c 38) # &
(= c 124) # |
(= c 36) # $
(= c 37) # %
(= c 47))) # /
(defn symbol-char? [c]
(or (symbol-start? c)
(digit? c)
(= c 35) # #
(= c 39) # '
(= c 58))) # :
(defn read-symbol-name [s pos end]
(if (and (< end (length s))
(symbol-char? (s end)))
(read-symbol-name s pos (+ end 1))
end))
(defn make-symbol
"Create a Jolt symbol struct."
[name]
(let [slash (string/find "/" name)]
(if slash
{:jolt/type :symbol
:ns (string/slice name 0 slash)
:name (string/slice name (+ slash 1))}
{:jolt/type :symbol
:ns nil
:name name})))
(defn sym
"Convenience to create a symbol during testing."
[name]
(make-symbol name))
(defn read-symbol [s pos]
(let [end (read-symbol-name s pos pos)]
(if (= end pos)
(error (string "Unrecognized character at " pos ": " (string/from-bytes (s pos))))
(let [name (string/slice s pos end)]
(if (= name "nil") [nil end]
(if (= name "true") [true end]
(if (= name "false") [false end]
[(make-symbol name) end])))))))
(defn read-keyword-name [s pos end]
(if (and (< end (length s))
(symbol-char? (s end)))
(read-keyword-name s pos (+ end 1))
end))
(defn read-keyword [s pos]
# pos is at the first colon
(if (and (< (+ pos 1) (length s)) (= (s (+ pos 1)) 58))
# ::foo/bar — auto-resolved keyword
(let [start (+ pos 2)
end (read-keyword-name s start start)
name (string/slice s start end)]
[(keyword name) end])
# :foo or :foo/bar
(let [start (+ pos 1)
end (read-keyword-name s start start)
name (string/slice s start end)]
[(keyword name) end])))
(defn escape-char [c]
(if (= c 110) "\n"
(if (= c 116) "\t"
(if (= c 114) "\r"
(if (= c 92) "\\"
(if (= c 34) "\""
(string/from-bytes c)))))))
(defn read-string-chars [s pos end chars]
(if (>= pos end)
(error "Unterminated string")
(let [c (s pos)]
(if (= c 92) # backslash
(let [next-pos (+ pos 1)]
(if (>= next-pos end)
(error "Unterminated escape")
(read-string-chars s (+ pos 2) end
(array/push chars (escape-char (s next-pos))))))
(if (= c 34) # closing quote
[pos chars]
(read-string-chars s (+ pos 1) end
(array/push chars (string/from-bytes c))))))))
(defn read-string [s pos]
# pos is at opening double-quote
(let [end (length s)
[new-pos chars] (read-string-chars s (+ pos 1) end @[])]
[(string/join chars "") (+ new-pos 1)]))
(defn read-hex-digits [s pos end]
(if (and (< end (length s)) (hex-digit? (s end)))
(read-hex-digits s pos (+ end 1))
end))
(defn read-digits [s pos end]
(if (and (< end (length s)) (digit? (s end)))
(read-digits s pos (+ end 1))
end))
(defn read-fractional [s pos end]
(if (and (< end (length s)) (digit? (s end)))
(read-fractional s pos (+ end 1))
end))
(defn read-number [s pos]
(var start pos) # start is mutable for sign handling
(var neg false)
# optional sign
(if (and (< pos (length s)) (= (s pos) 45))
(do (set start (+ pos 1)) (set neg true)))
(let [pos start
hex? (and (< (+ pos 1) (length s))
(= (s pos) 48) (= (s (+ pos 1)) 120))
start (if hex? (+ pos 2) pos)
end (if hex?
(read-hex-digits s start start)
(read-digits s start start))]
(if (= end start) (error (string "Expected number at " pos)))
# check for fractional part
(if (and (not hex?)
(< end (length s))
(= (s end) 46))
(let [frac-start (+ end 1)
frac-end (read-fractional s frac-start frac-start)]
(if (= frac-end frac-start) (error "Expected digit after ."))
(let [num-str (string/slice s start frac-end)
val (scan-number num-str)]
[(if neg (- val) val) frac-end]))
# integer or hex
(let [num-str (string/slice s start end)
val (if hex?
(string/format "0x%s" num-str)
num-str)
val (scan-number val)]
[(if neg (- val) val) end]))))
(defn read-list [s pos]
# pos is at opening paren
(defn read-list-items [s pos items]
(let [pos (skip-whitespace s pos)]
(if (>= pos (length s))
(error "Unterminated list"))
(if (= (s pos) 41) # )
[items (+ pos 1)]
(let [[form new-pos] (read-form s pos)]
(read-list-items s new-pos (array/push items form))))))
(read-list-items s (+ pos 1) @[]))
(defn read-vector [s pos]
# pos is at opening bracket
(defn read-vec-items [s pos items]
(let [pos (skip-whitespace s pos)]
(if (>= pos (length s))
(error "Unterminated vector"))
(if (= (s pos) 93) # ]
[(tuple/slice (tuple ;items)) (+ pos 1)]
(let [[form new-pos] (read-form s pos)]
(read-vec-items s new-pos (array/push items form))))))
(read-vec-items s (+ pos 1) @[]))
(defn read-map [s pos]
# pos is at opening brace
(defn read-kvs [s pos kvs]
(let [pos (skip-whitespace s pos)]
(if (>= pos (length s))
(error "Unterminated map"))
(if (= (s pos) 125) # }
[(struct ;kvs) (+ pos 1)]
(let [[key new-pos] (read-form s pos)
pos (skip-whitespace s new-pos)
[val new-pos2] (read-form s pos)]
(read-kvs s new-pos2 (-> kvs (array/push key) (array/push val)))))))
(read-kvs s (+ pos 1) @[]))
(defn read-set [s pos]
# pos is at #, next char is {
(defn read-set-items [s pos items]
(let [pos (skip-whitespace s pos)]
(if (>= pos (length s))
(error "Unterminated set"))
(if (= (s pos) 125) # }
[{:jolt/type :jolt/set :value (tuple/slice (tuple ;items))} (+ pos 1)]
(let [[form new-pos] (read-form s pos)]
(read-set-items s new-pos (array/push items form))))))
(read-set-items s (+ pos 2) @[]))
(defn read-char-name-end [s pos]
(if (and (< pos (length s)) (symbol-char? (s pos)))
(read-char-name-end s (+ pos 1))
pos))
(defn read-char [s pos]
# pos is at backslash
(let [end (read-char-name-end s (+ pos 1))
char-name (string/slice s (+ pos 1) end)]
[{:jolt/type :char :name char-name} end]))
(defn read-anon-fn [s pos]
# pos is at #, next char is (
(let [[form new-pos] (read-form s (+ pos 1))]
[(array/insert form 0 (sym "fn*")) new-pos]))
(defn read-reader-conditional [s pos]
# pos is at #, next char is ?
(let [[form new-pos] (read-form s (+ pos 2))]
[{:jolt/type :jolt/reader-conditional :clauses form} new-pos]))
(defn read-var-quote [s pos]
# pos is at #, next char is '
(let [[form new-pos] (read-form s (+ pos 2))]
[(array (sym "var") form) new-pos]))
(defn read-dispatch [s pos]
# pos is at #
(if (>= (+ pos 1) (length s))
(error "Unexpected end after #"))
(let [c (s (+ pos 1))]
(cond
(= c 123) (read-set s pos) # #{
(= c 40) (read-anon-fn s pos) # #(
(= c 63) (read-reader-conditional s pos) # #?
(= c 95) (let [[_ new-pos] (read-form s (+ pos 2))] # #_
(read-form s new-pos))
(= c 39) (read-var-quote s pos) # #'
# unknown dispatch — tagged literal
(let [end (read-symbol-name s pos pos)
tag (string/slice s pos end)
[form new-pos] (read-form s end)]
[{:jolt/type :jolt/tagged :tag (keyword tag) :form form} new-pos]))))
(defn read-quote [s pos new-pos token-sym]
(let [[form final-pos] (read-form s new-pos)]
[(array token-sym form) final-pos]))
(defn read-meta [s pos]
# pos is at ^
(let [[meta-form new-pos] (read-form s (+ pos 1))
[form new-pos2] (read-form s new-pos)]
[(array (sym "with-meta") form meta-form) new-pos2]))
(defn read-until-newline [s pos]
(if (or (>= pos (length s)) (= (s pos) 10))
pos
(read-until-newline s (+ pos 1))))
(set read-form (fn [s pos]
(let [pos (skip-whitespace s pos)]
(if (>= pos (length s))
[nil pos]
(let [c (s pos)]
(cond
# comment
(= c 59)
(let [line-end (read-until-newline s pos)]
(read-form s line-end))
# dispatch
(= c 35)
(read-dispatch s pos)
# string
(= c 34)
(read-string s pos)
# list
(= c 40)
(read-list s pos)
# vector
(= c 91)
(read-vector s pos)
# map
(= c 123)
(read-map s pos)
# keyword
(= c 58)
(read-keyword s pos)
# quote
(= c 39)
(read-quote s pos (+ pos 1) (sym "quote"))
# syntax-quote / backtick
(= c 96)
(read-quote s pos (+ pos 1) (sym "syntax-quote"))
# unquote ~
(= c 126)
(if (and (< (+ pos 1) (length s)) (= (s (+ pos 1)) 64))
(read-quote s pos (+ pos 2) (sym "unquote-splicing"))
(read-quote s pos (+ pos 1) (sym "unquote")))
# deref
(= c 64)
(read-quote s pos (+ pos 1) (sym "deref"))
# metadata
(= c 94)
(read-meta s pos)
# character
(= c 92)
(read-char s pos)
# number or symbol
(if (or (digit? c)
(and (= c 45) (< (+ pos 1) (length s)) (digit? (s (+ pos 1))))
(and (= c 43) (< (+ pos 1) (length s)) (digit? (s (+ pos 1)))))
(read-number s pos)
(read-symbol s pos))))))))
(defn parse-string
"Parse a Clojure source string and return the first form."
[s]
(let [[form _] (read-form s 0)]
form))
(defn parse-next
"Parse the first form from a string. Returns [form remaining-string]."
[s]
(let [[form pos] (read-form s 0)]
[form (string/slice s pos)]))

276
src/jolt/types.janet Normal file
View file

@ -0,0 +1,276 @@
# Jolt Types
# Core types for the Clojure-on-Janet interpreter.
#
# Types:
# JoltVar — mutable container with metadata (like Clojure Var)
# JoltNamespace — namespace with symbol→var mappings and imports
# JoltContext — evaluation context (env atom, namespaces)
#
# Symbols are represented as {:jolt/type :symbol :ns <string-or-nil> :name <string>}
# as produced by the reader.
# ============================================================
# Symbol helpers
# ============================================================
(defn sym?
"Check if x is a Jolt symbol struct."
[x]
(and (struct? x) (= :symbol (x :jolt/type))))
# ============================================================
# Var
# ============================================================
(def- binding-stack @[]) # stack of {var → value} tables for thread-local bindings
(defn push-thread-bindings
"Push a frame of dynamic var bindings. Takes a struct of var→value."
[bindings]
(array/push binding-stack bindings))
(defn pop-thread-bindings
"Pop the most recent frame of dynamic var bindings."
[]
(array/pop binding-stack))
(defn make-var
"Create a new Jolt Var.
(make-var name) — unbound var
(make-var name init-val) — var with root binding
(make-var name init-val meta) — var with root and metadata
name is a symbol struct {:jolt/type :symbol ...}"
[name &opt init-val meta]
(default init-val nil)
(default meta nil)
(let [m (if meta meta {:name name})
result @{:jolt/type :jolt/var
:name name
:root init-val
:meta m
:dynamic (if meta (get meta :dynamic) false)
:macro (if meta (get meta :macro) false)
:ns (if meta (get meta :ns) nil)}]
result))
(defn var?
"Check if x is a Jolt Var."
[x]
(and (table? x) (= :jolt/var (x :jolt/type))))
(defn var-dynamic?
"Check if var is marked :dynamic."
[v]
(v :dynamic))
(defn var-macro?
"Check if var is marked :macro."
[v]
(v :macro))
(defn var-name
"Return the symbol name of the var."
[v]
(v :name))
(defn var-meta
"Return the metadata of the var."
[v]
(v :meta))
(defn var-ns
"Return the namespace of the var."
[v]
(v :ns))
(defn var-get
"Deref the var. If the var is dynamic and has a thread-local binding, return that.
Otherwise return the root binding."
[v]
# walk binding stack top-down for this var
(var result nil)
(var i (dec (length binding-stack)))
(while (>= i 0)
(let [frame (in binding-stack i)
val (get frame v)]
(if (not (nil? val))
(do
(set result (if (var? val) (var-get val) val))
(set i -1))
(-- i))))
(if (not (nil? result)) result (v :root)))
(defn var-set
"Set the root binding of a var."
[v val]
(put v :root val))
(defn alter-var-root
"Atomically alter the root binding of v by applying f to current value plus args."
[v f & args]
(let [new-val (f (v :root) ;args)]
(put v :root new-val)))
(defn with-meta
"Return a new var with updated metadata. The original var is unchanged."
[v meta]
# build new meta as a table first (to allow adding keys), then convert
(let [new-meta-table (merge @{} (v :meta) meta)
# convert to struct by extracting all keys
new-meta (table/to-struct new-meta-table)]
@{:jolt/type :jolt/var
:name (v :name)
:root (v :root)
:meta new-meta
:dynamic (v :dynamic)
:macro (v :macro)
:ns (v :ns)}))
(defn bind-root
"Set the root binding (internal, same as var-set)."
[v val]
(put v :root val))
# ============================================================
# Namespace
# ============================================================
(defn make-ns
"Create a new namespace.
(make-ns name) — empty namespace
name is a symbol struct {:jolt/type :symbol ...}"
[name]
(struct
:jolt/type :jolt/namespace
:name name
:mappings @{}
:imports @{}
:aliases @{}))
(defn ns?
"Check if x is a Jolt Namespace."
[x]
(and (struct? x) (= :jolt/namespace (x :jolt/type))))
(defn ns-name
"Return the name symbol of a namespace."
[ns]
(ns :name))
(defn ns-map
"Return the mappings table (symbol → var) for a namespace."
[ns]
(ns :mappings))
(defn ns-intern
"Find or create a var named by sym in namespace ns, setting root binding to val if given.
(ns-intern ns sym) — find or create unbound var
(ns-intern ns sym val) — find or create with root binding"
[ns sym &opt val]
(default val nil)
(let [mappings (ns :mappings)
existing (get mappings sym)]
(if existing
(do
(when (not (nil? val))
(bind-root existing val))
existing)
(let [v (make-var sym val {:ns ns :name sym})]
(put mappings sym v)
v))))
(defn ns-find
"Find a var by symbol in the namespace. Returns nil if not found."
[ns sym]
(get (ns :mappings) sym))
(defn ns-unmap
"Remove a mapping by symbol from the namespace."
[ns sym]
(put (ns :mappings) sym nil))
(defn ns-resolve
"Resolve a symbol in a namespace. Looks in own mappings first,
then aliases. Returns the var or nil."
[ns sym]
(or (ns-find ns sym)
(let [qualified? (sym? sym)]
(when qualified?
# qualified symbol: look up via alias
(let [alias-ns (get (ns :aliases) (sym :ns))]
(when alias-ns
(ns-find alias-ns (sym :name))))))))
(defn ns-import
"Add an import to the namespace. class-name is local symbol, fq-class-name is the full qualified name."
[ns class-name fq-class-name]
(put (ns :imports) class-name fq-class-name))
(defn ns-import-lookup
"Look up an import in the namespace. Returns the full qualified name or nil."
[ns class-name]
(get (ns :imports) class-name))
(defn ns-add-alias
"Add an alias from alias-sym to target-ns."
[ns alias-sym target-ns]
(put (ns :aliases) alias-sym target-ns))
# ============================================================
# Context
# ============================================================
(defn ctx-find-ns
"Find or create a namespace in the context by name symbol."
[ctx ns-sym]
(let [env (ctx :env)
namespaces (env :namespaces)]
(or (get namespaces ns-sym)
(let [ns (make-ns ns-sym)]
(put namespaces ns-sym ns)
ns))))
(defn make-ctx
"Create a new evaluation context.
(make-ctx) — empty context with 'user namespace
(make-ctx opts) — context with initial namespaces from opts
opts may contain:
:namespaces — struct of {ns-symbol → {sym → value, ...}, ...}"
[&opt opts]
(default opts nil)
(let [env @{:namespaces @{}
:class->opts @{}
:current-ns "user"}
# create the user namespace via a partial context
_ (ctx-find-ns {:env env} "user")]
# initialize from opts
(when opts
(when-let [ns-opts (get opts :namespaces)]
(loop [[ns-sym mappings] :pairs ns-opts]
(let [ns (ctx-find-ns {:env env} ns-sym)]
(loop [[sym val] :pairs mappings]
(ns-intern ns sym val))))))
{:jolt/type :jolt/context
:env env}))
(defn ctx?
"Check if x is a Jolt Context."
[x]
(and (struct? x) (= :jolt/context (x :jolt/type))))
(defn ctx-env
"Return the env atom from the context."
[ctx]
(ctx :env))
(defn ctx-current-ns
"Get the current namespace symbol."
[ctx]
(get (ctx :env) :current-ns))
(defn ctx-set-current-ns
"Set the current namespace symbol."
[ctx ns-sym]
(put (ctx :env) :current-ns ns-sym))