Refactor phase 2a: split evaluator.janet + explode eval-list into named handlers

evaluator.janet was a 2597-line file with a 680-line eval-list. Split into
cluster modules behind a re-export aggregator (same pattern as core):

  eval_base     forward vars, syntax-quote, ns-loading, registries, jolt-invoke
  eval_resolve  symbol/var resolution, params, destructuring, class lookup
  eval_runtime  protocols, multimethods, deftype/reify, install-stateful-fns!
  eval_special  the special forms (eval-list dispatch)

evaluator.janet stays the module every consumer imports: it loads the clusters
in dependency order and re-exports their defs (import :prefix "" :export true),
so the five (use ./evaluator) consumers are unchanged. It still owns the
eval-form entry that ties resolution + special forms + map/coll evaluation.

In eval_special, the giant eval-list match is exploded: each multi-line arm is
now a named (defn eval-<form> [ctx bindings form] ...) — eval-def, eval-fn*,
eval-let*, eval-loop*, eval-try, eval-set!, eval-dot, etc. — and eval-list is a
thin dispatch table over them. "where is try handled" is now `grep eval-try`.

Order-preserving (statically verified: no symbol used before its cluster loads;
zero backward refs). 27 helpers shared across clusters are now public so `use`
shares them. The two near-duplicate .method dot blocks are NOT merged here — that
is a behavior-sensitive dedup tracked separately (jolt-eos3); this PR is pure
moves + the mechanical eval-list explosion, no behavior change.

Gate green: conformance 355x3, clojure-test-suite 4718 pass (>= 4695 baseline),
full jpm test exit 0.
This commit is contained in:
Yogthos 2026-06-15 02:29:22 -04:00
parent ea5c3452de
commit 0a531dd1e8
5 changed files with 2610 additions and 2506 deletions

564
src/jolt/eval_base.janet Normal file
View file

@ -0,0 +1,564 @@
# Jolt Evaluator — base: forward vars, syntax-quote, ns-loading, registries
# Extracted from evaluator.janet (jolt-oudv, phase 2a split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./pv)
(use ./plist)
(use ./config)
(use ./reader)
(use ./regex)
# The env this module was loaded under — proto-chains to the Janet root env;
# the janet/* interop bridge falls back to it inside env-less fibers.
(def module-load-env (fiber/getenv (fiber/current)))
# jpm-module autoload: a janet.<module>/<name> reference whose module isn't
# in the env is satisfied by requiring it from the jpm module path on first
# use — (janet.spork.http/server ...) just works when spork is installed,
# and the same goes for any jpm module. Loaded bindings are cached here
# (and failures negatively cached, so a missing module errors fast).
(def janet-bridge-extras @{})
(def janet-bridge-failed @{})
(defn bridge-autoload
"jname is spork.http/server-shaped: require spork/http, cache its public
bindings under the dotted prefix, return the one asked for (nil when the
module is missing or has no such binding)."
[jname]
(def slash (string/find "/" jname))
(when slash
(def mod-ns (string/slice jname 0 slash))
(unless (get janet-bridge-failed mod-ns)
(def mod-path (string/replace-all "." "/" mod-ns))
(def r (protect (require mod-path)))
(if (r 0)
(eachp [sym entry] (r 1)
(when (and (symbol? sym) (table? entry) (not (get entry :private)))
(put janet-bridge-extras (string mod-ns "/" sym) (get entry :value))))
(put janet-bridge-failed mod-ns true))))
(in janet-bridge-extras jname))
(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 "defmacro") (= name "fn*") (= name "let*") (= name "loop*")
(= name "recur") (= name "throw") (= name "try")
(= name "set!") (= name "var")
(= name "eval")
(= name "new") (= name ".")
# var-get/var-set/var?/alter-var-root/alter-meta!/reset-meta! are plain
# clojure.core fns (core-bindings); find-var/intern are ctx-capturing fns
# (install-stateful-fns!) — no longer special forms (Stage 2 tier 6).
# locking/instance?/satisfies?/defonce/read-string/macroexpand-1 and the
# multimethod table ops are overlay macros / clojure.core fns now
# (Stage 2 tier 6c) — not special forms.
))
(var eval-form nil)
# Macro expansion cache (interpreter): a macro CALL form expands ONCE and the
# result is reused — macroexpansion is a compile-time step with zero runtime cost,
# the proper Lisp model. Keyed by the call form's identity (a fn body re-evaluates
# the same form arrays each call). Also gives compile-once gensym semantics (a
# foo# auto-gensym is fixed across calls, unlike per-call re-expansion). Cleared
# when a macro is (re)defined so stale expansions don't linger.
(def macro-cache @{})
# Compile hook for macro expanders: set by the api to (fn [ctx args-form body] ->
# compiled-janet-fn | nil). When set and the body is compilable (no &env/&form,
# analyzer available), defmacro uses the compiled expander instead of the
# interpreted closure — macro expansion at native speed, zero runtime cost.
(var macro-compile-hook nil)
(defn form-uses-sym? [form nm]
(cond
(and (struct? form) (= :symbol (form :jolt/type))) (= nm (form :name))
(or (array? form) (tuple? form))
(do (var found false) (each x form (when (form-uses-sym? x nm) (set found true) (break))) found)
(and (struct? form) (nil? (form :jolt/type)))
(do (var found false) (each k (keys form)
(when (or (form-uses-sym? k nm) (form-uses-sym? (get form k) nm)) (set found true) (break))) found)
false))
# A transient is a tagged mutable table @{:jolt/type :jolt/transient :kind ...}.
(defn- jolt-transient? [x]
(and (table? x) (= :jolt/transient (get x :jolt/type))))
# Read-only lookup over a transient (vector index / map key / set membership),
# mirroring core-get. Map/set backing tables are keyed by the same canon used
# by phm, so canonicalize collection keys here too.
(defn- transient-lookup [t k default]
(case (t :kind)
:vector (let [a (t :arr)]
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length a)))
(in a k) default))
:map (let [e (get (t :tbl) (canon k))] (if (nil? e) default (in e 1)))
:set (if (nil? (get (t :tbl) (canon k))) default k)
default))
(defn coll-lookup
"Clojure `get` semantics over a jolt collection, used for collection-as-IFn."
[coll k default]
(cond
(jolt-transient? coll) (transient-lookup coll k default)
(shape-rec? coll) (shape-get coll k default)
# sorted colls are tables — without this arm they fell into the raw
# table-get branch and (:k (sorted-map ...)) was always nil (jolt-4vr spec)
(and (table? coll) (or (= :jolt/sorted-map (coll :jolt/type))
(= :jolt/sorted-set (coll :jolt/type))))
((get (coll :ops) :get) coll k default)
(phm? coll) (phm-get coll k default)
(set? coll) (if (phs-contains? coll k) k default)
(pvec? coll)
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (pv-count coll)))
(pv-nth coll k) default)
(or (tuple? coll) (array? coll))
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length coll)))
(in coll k) default)
(or (struct? coll) (table? coll))
(let [v (get coll k :jolt/not-found)]
(if (= v :jolt/not-found) default v))
(nil? coll) default
default))
(defn jolt-invoke
"Apply f to already-evaluated args. Handles real functions and Clojure's
IFn collections: vectors (index lookup), maps/sets/keywords/symbols (get),
and deftype/record values implementing IFn. `args` is an array."
[ctx f args]
(cond
(or (function? f) (cfunction? f)) (apply f args)
(jolt-transient? f) (transient-lookup f (get args 0) (get args 1))
# a record shape-rec is callable: IFn impl if it has one, else map-like
# field access. A plain (non-record) shape-rec is just field access.
(shape-rec? f)
(let [tag (record-tag f)
ifn (when tag (find-protocol-method ctx tag "IFn" "-invoke"))]
(if ifn (apply ifn f args) (shape-get f (get args 0) (get args 1))))
(keyword? f) (coll-lookup (get args 0) f (get args 1))
(and (struct? f) (= :symbol (f :jolt/type)))
(coll-lookup (get args 0) f (get args 1))
(and (table? f) (or (= :jolt/sorted-map (f :jolt/type))
(= :jolt/sorted-set (f :jolt/type))))
# the overlay-attached :get op (comparator-based lookup, like Clojure)
((get (f :ops) :get) f (get args 0) (get args 1))
(phm? f) (phm-get f (get args 0) (get args 1))
(set? f) (if (phs-contains? f (get args 0)) (get args 0) (get args 1))
(pvec? f)
(let [k (get args 0)]
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (pv-count f)))
(pv-nth f k)
(error (string "Index " k " out of bounds for vector of length " (pv-count f)))))
(or (tuple? f) (array? f))
(let [k (get args 0)]
(if (and (number? k) (= k (math/floor k)) (>= k 0) (< k (length f)))
(in f k)
(error (string "Index " k " out of bounds for vector of length " (length f)))))
# Map literal only (struct with no :jolt/type). A tagged struct (char/etc.)
# is not callable — symbols are handled above; chars fall through to the error.
(and (struct? f) (nil? (get f :jolt/type)))
(let [v (get f (get args 0) :jolt/not-found)]
(if (= v :jolt/not-found) (get args 1) v))
(and (table? f) (get f :jolt/deftype))
(let [ifn-fn (find-protocol-method ctx (get f :jolt/deftype) "IFn" "-invoke")]
(if ifn-fn (apply ifn-fn f args)
(if (and (get f :jolt/protocol-methods) (get (f :jolt/protocol-methods) :-invoke))
(apply (get (f :jolt/protocol-methods) :-invoke) f args)
# No IFn impl: fall back to map-like field access, e.g. (point :x)
(let [v (get f (get args 0) :jolt/not-found)]
(if (= v :jolt/not-found) (get args 1) v)))))
(and (table? f) (get f :jolt/protocol-methods))
(let [invoke-fn (get (f :jolt/protocol-methods) :-invoke)]
(if invoke-fn (apply invoke-fn f args)
(error (string "Cannot call " (type f) " as a function"))))
(error (string "Cannot call " (type f) " as a function"))))
(defn- sq-symbol
"Resolve a symbol inside syntax-quote. `foo#` becomes a stable auto-gensym
(per-expansion, via gsmap); special forms are left unqualified; a clojure.core
name is fully qualified to clojure.core/ (matching Clojure, for hygiene); other
symbols are qualified to the current namespace so they resolve when the macro is
used elsewhere."
[ctx form gsmap]
(if (nil? (form :ns))
(let [nm (form :name)]
(cond
(string/has-suffix? "#" nm)
(or (get gsmap nm)
(let [g {:jolt/type :symbol :ns nil
:name (string (string/slice nm 0 -2) "__" (string (gensym)) "__auto")}]
(put gsmap nm g) g))
(special-symbol? nm) form
(ns-find (ctx-find-ns ctx "clojure.core") nm)
{:jolt/type :symbol :ns "clojure.core" :name nm}
# Unresolved -> qualify to the namespace being COMPILED when set (the
# analyzer runs interpreted in jolt.analyzer, so ctx-current-ns is wrong
# mid-compile — the same seam resolve-var/h-current-ns use). Matters when
# a macro expander's template is lowered while a symbol it references is
# not yet defined (deftype's extend-type, defined later in the same tier):
# it must qualify to the macro's home ns, not jolt.analyzer.
{:jolt/type :symbol
:ns (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx))
:name nm}))
# Alias-qualified (impl/foo): resolve the alias to its target namespace so the
# emitted symbol resolves at the macro's USE site, which has no such alias
# (jolt-9av). Matches Clojure's syntax-quote. A real ns name (not an alias)
# has no entry and is left as written.
(let [cur (ctx-find-ns ctx (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx)))
target (and cur (or (ns-alias-lookup cur (form :ns))
(ns-import-lookup cur (form :ns))))]
(if target
{:jolt/type :symbol :ns target :name (form :name)}
form))))
(defn d-realize
"Realize a lazy-seq to an array for positional destructuring / splicing; pass
others (pvec/plist coerced to array, everything else unchanged). nil is an
empty seq, as everywhere in Clojure — ~@nil splices nothing (an interpreted
macro's empty & rest binds nil, which used to blow up `each`)."
[val]
(if (nil? val) @[]
(if (pvec? val) (pv->array val)
(if (plist? val) (pl->array val)
(if (lazy-seq? val)
(do
(var items @[]) (var cur val) (var go true)
(while go
(let [cell (realize-ls cur)]
(if (or (nil? cell) (= :jolt/pending cell) (= 0 (length cell)))
(set go false)
(do (array/push items (in cell 0))
(let [rt (in cell 1)]
(if (nil? rt) (set go false) (set cur (ls-rest-cached cur rt))))))))
items)
val)))))
(defn syntax-quote*
[ctx bindings form &opt gsmap]
(default gsmap @{})
(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)))
(sq-symbol ctx form gsmap)
(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"))
(let [sv (eval-form ctx bindings (in item 1))]
(each v (d-realize sv) (array/push result v)))
(array/push result (syntax-quote* ctx bindings item gsmap))))
(++ 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"))
(let [sv (eval-form ctx bindings (in item 1))]
(each v (d-realize sv) (array/push result v)))
(array/push result (syntax-quote* ctx bindings item gsmap))))
(++ i)) result)
# set literal: lower each element (processing ~/~@) and rebuild a set.
(and (struct? form) (= :jolt/set (form :jolt/type)))
(do (var result @[])
(each item (form :value)
(if (and (array? item) (> (length item) 0) (sym-name? (first item) "unquote-splicing"))
(let [sv (eval-form ctx bindings (in item 1))]
(each v (d-realize sv) (array/push result v)))
(array/push result (syntax-quote* ctx bindings item gsmap))))
(make-phs ;result))
(and (struct? form) (get form :jolt/type)) form
(struct? form)
(do (var kvs @[])
(def order (form-kv-order form))
(if order
(each x order (array/push kvs (syntax-quote* ctx bindings x gsmap)))
(each k (keys form)
(array/push kvs (syntax-quote* ctx bindings k gsmap))
(array/push kvs (syntax-quote* ctx bindings (get form k) gsmap))))
# keep carrying source order through nested syntax-quote (jolt-p3c)
(struct/with-proto (struct :jolt/kv-order (tuple/slice kvs)) ;kvs))
form))
# Syntax-quote LOWERING: instead of evaluating a `(...) form to a value (what
# syntax-quote* does), produce equivalent CONSTRUCTION CODE so a backtick body is
# plain compilable code (read -> macroexpand -> compile, zero runtime cost).
# Mirrors syntax-quote*/sq-symbol exactly; the canonical algorithm is
# tools.reader's syntax-quote*/expand-list. List forms build via __sqcat (-> array),
# vectors via __sqvec (-> tuple), maps via __sqmap; symbols become (quote resolved);
# ~ leaves the expr in place, ~@ passes the seq straight to __sqcat for splicing.
(defn- sqsym* [nm] {:jolt/type :symbol :ns nil :name nm})
(var syntax-quote-lower nil)
(defn- sq-lower-part [ctx item gsmap]
(if (and (array? item) (> (length item) 0) (sym-name? (first item) "unquote-splicing"))
(in item 1)
@[(sqsym* "__sq1") (syntax-quote-lower ctx item gsmap)]))
(set syntax-quote-lower
(fn syntax-quote-lower [ctx form &opt gsmap]
(default gsmap @{})
(cond
(and (array? form) (> (length form) 0) (sym-name? (first form) "unquote"))
(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)))
@[(sqsym* "quote") (sq-symbol ctx form gsmap)]
(array? form)
(array/concat @[(sqsym* "__sqcat")] (map (fn [it] (sq-lower-part ctx it gsmap)) form))
(tuple? form)
(array/concat @[(sqsym* "__sqvec")] (map (fn [it] (sq-lower-part ctx it gsmap)) form))
# set literal: lower each element (so ~/~@ are processed) and rebuild a set.
(and (struct? form) (= :jolt/set (form :jolt/type)))
(array/concat @[(sqsym* "__sqset")] (map (fn [it] (sq-lower-part ctx it gsmap)) (form :value)))
# other tagged structs (chars): returned as-is (no recursion)
(and (struct? form) (get form :jolt/type))
@[(sqsym* "quote") form]
(struct? form)
(do (var parts @[(sqsym* "__sqmap")])
(def order (form-kv-order form))
(if order
(each x order (array/push parts (syntax-quote-lower ctx x gsmap)))
(each k (keys form)
(array/push parts (syntax-quote-lower ctx k gsmap))
(array/push parts (syntax-quote-lower ctx (get form k) gsmap))))
parts)
@[(sqsym* "quote") form])))
(defn resolve-var
[ctx bindings sym-s]
(let [name (sym-s :name) ns (sym-s :ns)]
(if (not (nil? ns))
# Resolve ns aliases (e.g. `p/thrown?` where `p` is a require :as alias) so
# aliased refs/macros resolve. During compilation the analyzer (interpreted,
# in jolt.analyzer) rebinds ctx-current-ns to its own ns, so look up the alias
# against the COMPILE ns (:compile-ns, the user's ns) when set — otherwise an
# aliased ref like g/foo wouldn't resolve mid-compile. Same ns h-current-ns uses.
(let [cur-name (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx))
current-ns (ctx-find-ns ctx cur-name)
aliased-ns (or (ns-alias-lookup current-ns ns) (ns-import-lookup current-ns ns))
target-ns (ctx-find-ns ctx (or aliased-ns 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)
v (ns-find ns name)]
(if v v
(let [core-ns (ctx-find-ns ctx "clojure.core")]
(ns-find core-ns name))))))))
(defn sym-name-str
[sym-s]
(if (sym-s :ns) (string (sym-s :ns) "/" (sym-s :name)) (sym-s :name)))
(defn- ns->relpath
"Namespace name to its file-relative path (dots->dirs, dashes->_), no extension."
[ns-name]
(string/replace-all "." "/" (string/replace-all "-" "_" ns-name)))
(defn- find-ns-file
"Search the context's source roots (stdlib first, then deps.edn dirs) for the
namespace's source, trying .clj then .cljc. Returns the path or nil."
[ctx ns-name]
(let [rel (ns->relpath ns-name)
roots (or (get (ctx :env) :source-paths) @["src/jolt"])]
(var found nil)
(each root roots
(each ext [".clj" ".cljc"]
(when (nil? found)
(let [p (string root "/" rel ext)]
(when (os/stat p) (set found p))))))
found))
(defn- load-ns-source
"Parse and evaluate every form of a namespace's source in the given context.
Routes through the loader's eval-toplevel when the api has installed it
(the :toplevel-eval hook) so REQUIRED namespaces compile like everything
else — without it they ran interpreted-only: slower, and their fns were
anonymous closures in stack traces (jolt-2o7.1)."
[ctx src &opt file]
(default file "<source>")
(def toplevel (get (ctx :env) :toplevel-eval))
# a require runs nested inside an outer file's eval; save/restore the outer
# checker source so its later forms still convert offsets correctly (jolt-fqy)
(def checking (or (checker-enabled?) (get (ctx :env) :inline?)))
(def saved-src (and checking (get (ctx :env) :tc-source)))
(def saved-file (and checking (get (ctx :env) :tc-file)))
(when checking
(track-positions! true)
(put (ctx :env) :tc-source src)
(put (ctx :env) :tc-file file))
(defer (when checking
(put (ctx :env) :tc-source saved-src)
(put (ctx :env) :tc-file saved-file))
(each [f line] (parse-all-positioned src file)
(try
(if toplevel (toplevel ctx f) (eval-form ctx @{} f))
([err fib]
# innermost failing form wins; files unwound through form the
# 'while loading …' chain (mirrors loader/eval-forms-positioned,
# which this can't import — circularity) (jolt-2o7.4)
(def env (ctx :env))
(when (nil? (get env :error-pos))
(put env :error-pos {:file file :line line}))
(when (nil? (get env :error-loading)) (put env :error-loading @[]))
(def chain (get env :error-loading))
(when (not= (last chain) file) (array/push chain file))
(propagate err fib))))))
(defn maybe-require-ns
"If namespace ns-name isn't populated yet, load its source — from a file on the
context's source roots, else from the stdlib baked into the image. Restores the
current namespace afterwards (a library's own `ns` form, or our manual switch
for ns-form-less stdlib files, changes it). No-op for already-loaded namespaces."
[ctx ns-name]
(let [ns (ctx-find-ns ctx ns-name)]
(when (and (= 0 (length (ns :mappings)))
(not (get (get (ctx :env) :loaded-namespaces @{}) ns-name))
(not= ns-name "clojure.core"))
(let [path (find-ns-file ctx ns-name)
embedded (get (get (ctx :env) :embedded-sources @{}) ns-name)
stdlib? (not (nil? embedded))]
# Clojure throws FileNotFoundException here; succeeding silently leaves
# an empty namespace behind and defers the failure to the first
# unresolved symbol, far from the actual cause (a typo, a missing
# JOLT_PATH root). Best-effort loaders (the SCI bootstrap, which loads
# clj-targeted sources whose requires can't all exist on this host)
# opt out via :lenient-require? on the env.
(when (and (nil? path) (nil? embedded)
(not (get (ctx :env) :lenient-require?)))
(error (string "Could not locate " ns-name
" on the context's source paths (JOLT_PATH / :paths)")))
(when (or path embedded)
(let [saved (ctx-current-ns ctx)]
# Stdlib files have no `ns` form, so switch into the target ns first
# (their defs intern there); a library's own `ns` form overrides this.
(ctx-set-current-ns ctx ns-name)
(if path
(load-ns-source ctx (slurp path) path)
(load-ns-source ctx embedded (string ns-name " (stdlib)")))
# Inter-procedural collection-type inference (jolt-767): once the whole
# unit is loaded, run the closed-world fixpoint + recompile so param-
# dependent lookups specialize. Only in optimization mode; best-effort
# (a failure here must not break loading). Hook installed by the api to
# avoid an evaluator->backend circular import.
(when (get (ctx :env) :inline?)
(if (and (get (ctx :env) :whole-program?)
(not (get (ctx :env) :infer-program-done?)))
# whole-program (jolt-t34): defer — record the ns and run ONE
# fixpoint over all units later (the closed-world pass sees every
# caller, so cross-ns param types propagate). Once that batch pass
# has run (infer-program-done?), a ns loaded later — a lazy require
# inside -main — can't join it, so fall back to per-ns inference.
(let [lst (or (get (ctx :env) :inferred-nses)
(let [a @[]] (put (ctx :env) :inferred-nses a) a))]
(array/push lst ns-name))
(when-let [iu (get (ctx :env) :infer-unit!)]
(protect (iu ctx ns-name)))))
# Record load order for tooling (uberscript): a dependency finishes
# loading before its requirer, so this is topological. Skip the
# baked-in stdlib — it's part of the runtime, not something to bundle.
(when (and path (not stdlib?))
(when-let [lf (get (ctx :env) :loaded-files)] (array/push lf path)))
(ctx-set-current-ns ctx saved)))))))
(defn eval-require
[ctx spec]
(let [ns-sym (in spec 0)
ns-name (sym-name-str ns-sym)]
(var alias nil)
(var refer-syms nil)
(var i 1)
(let [slen (length spec)]
# Scan ALL options — a spec may carry both :as and :refer, e.g.
# [clojure.string :as str :refer [blank?]]; don't stop at the first.
(while (< i slen)
(let [item (in spec i)]
(cond
(or (= item :as) (and (struct? item) (= :symbol (item :jolt/type)) (= "as" (item :name))))
(do (set alias ((in spec (+ i 1)) :name)) (+= i 2))
(or (= item :refer) (and (struct? item) (= :symbol (item :jolt/type)) (= "refer" (item :name))))
(do (set refer-syms (in spec (+ i 1))) (+= i 2))
(++ i)))))
(maybe-require-ns ctx ns-name)
(when alias
(let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx))]
(ns-add-alias current-ns alias ns-name)))
(when refer-syms
(let [source-ns (ctx-find-ns ctx ns-name)
target-ns (ctx-find-ns ctx (ctx-current-ns ctx))]
(if (or (= refer-syms :all)
(and (struct? refer-syms) (= :symbol (refer-syms :jolt/type))
(= "all" (refer-syms :name))))
# :refer :all — share EVERY var (this used to each over the :all
# keyword itself and silently refer nothing; selmer's
# [selmer.util :refer :all] left *tag-open* & co unresolved)
(eachp [nm v] (source-ns :mappings)
(put (target-ns :mappings) nm v))
(each refer-sym refer-syms
(let [name (if (struct? refer-sym) (refer-sym :name) refer-sym)
v (ns-find source-ns name)]
(when v
# Share the SOURCE var (the Clojure model): macro-ness travels with
# it and source-ns redefinitions propagate to the referer.
(put (target-ns :mappings) name v)))))))
nil))
(defn bind-put
"Put a value into bindings. Uses :jolt/nil sentinel for nil values
because Janet's (put table key nil) silently drops the key."
[bindings key value]
(put bindings key (if (nil? value) :jolt/nil value)))
(defn binding-get
"Get a value from bindings, walking the prototype chain."
[bindings name]
(var result :jolt/not-found)
(var t bindings)
(while (not (nil? t))
(when (in t name)
(set result (in t name))
(break))
(set t (table/getproto t)))
result)
# Pluggable host-class shims (java.time etc. register here at module load):
# class-statics: "ClassName" -> {"member" value-or-fn} (Foo/bar resolution)
# tagged-methods: :jolt/tag -> {"method" (fn [self args...])} ((.m obj) dispatch)
(def class-statics @{})
(def tagged-methods @{})
(defn register-class-statics! [class-name tbl] (put class-statics class-name tbl))
(defn register-tagged-methods! [tag tbl] (put tagged-methods tag tbl))
# Constructor shims: (ClassName. args) resolves ClassName as a value, so the
# ctor fns are interned as clojure.core vars at init (install-stateful-fns!).
(def class-ctors @{})
(defn register-class-ctor! [nm f] (put class-ctors nm f))
# java.util.Iterator shim: (.iterator coll) gives a jolt iterator over any
# seqable, with (.hasNext it) / (.next it). Some Clojure libs (e.g. hiccup's
# iterate!) loop with the Java Iterator protocol; this makes that work over jolt
# collections. The realizer (core/realize-for-iteration, which handles every
# collection type) is late-bound because core loads after this file.
(var coll-realizer nil)
(defn set-coll-realizer! [f] (set coll-realizer f))
# Late-bound (wired in api): routes a Java collection-interop method call
# (.nth/.count/.valAt/.seq …) on a jolt persistent collection to the clojure.core
# equivalent. Returns :jolt/ci-none when it doesn't apply. Lets clj-targeted libs
# (malli) that use .nth/.count on vectors/maps in their :clj branches work.
(var coll-interop nil)
(defn set-coll-interop! [f] (set coll-interop f))

443
src/jolt/eval_resolve.janet Normal file
View file

@ -0,0 +1,443 @@
# Jolt Evaluator — symbol/var resolution, params, destructuring, class lookup
# Extracted from evaluator.janet (jolt-oudv, phase 2a split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./pv)
(use ./plist)
(use ./config)
(use ./reader)
(use ./regex)
(use ./eval_base)
(register-tagged-methods! :jolt/iterator
@{"hasNext" (fn [self] (< (self :pos) (length (self :items))))
"next" (fn [self]
(def x (in (self :items) (self :pos)))
(put self :pos (+ 1 (self :pos)))
x)})
# Class names evaluate to their CANONICAL NAME STRING — the same value
# core-class returns — so (defmethod m String ...) keys match a
# (defmulti m (comp class :body)) dispatch (ring.util.request does this).
# `new` resolves the actual constructor from class-ctors by short name.
(def class-canonical-names
@{"String" "java.lang.String" "Number" "java.lang.Number"
"Boolean" "java.lang.Boolean" "Long" "java.lang.Long"
"Integer" "java.lang.Integer" "Double" "java.lang.Double"
"InputStream" "java.io.InputStream" "OutputStream" "java.io.OutputStream"
"File" "java.io.File" "Reader" "java.io.Reader" "Writer" "java.io.Writer"
"ISeq" "clojure.lang.ISeq" "Keyword" "clojure.lang.Keyword"
"Symbol" "clojure.lang.Symbol" "MapEntry" "clojure.lang.MapEntry"
"StringReader" "java.io.StringReader" "StringWriter" "java.io.StringWriter"
"StringBuilder" "java.lang.StringBuilder"
"StringTokenizer" "java.util.StringTokenizer"
"Charset" "java.nio.charset.Charset" "Base64" "java.util.Base64"
"Exception" "java.lang.Exception"
"IllegalArgumentException" "java.lang.IllegalArgumentException"
"InterruptedException" "java.lang.InterruptedException"
"Throwable" "java.lang.Throwable"})
# A class used as a VALUE should evaluate to what (clojure.core/type instance)
# returns for its instances, so a registry keyed by class (e.g. malli's
# class-schemas) matches a value's (type ...). For jolt's native tagged types the
# class maps to its :jolt/type keyword — Pattern <-> a compiled regex.
(def- class-value-overrides
@{"Pattern" :jolt/regex "java.util.regex.Pattern" :jolt/regex})
(defn class-value-for
"The value a class-name symbol evaluates to: a type override, else its canonical
name string."
[nm]
(or (get class-value-overrides nm)
(get class-canonical-names nm)
# qualified already, or unknown: the name itself is the token
nm))
(defn ctor-for-class-token
"Constructor fn for a class token (a canonical-name string): try the full
name, then the short name after the last dot."
[tok]
(or (in class-ctors tok)
(let [parts (string/split "." tok)]
(in class-ctors (last parts)))))
# java.lang.String method surface for clj-compat interop: (.toLowerCase s),
# (.indexOf s x), ... — the methods portable cljc libraries actually call.
# Case mapping is ASCII (the whole engine is byte-based); indexOf returns -1
# on miss, as on the JVM.
(defn- str-needle [x]
(cond
(and (struct? x) (= :jolt/char (get x :jolt/type))) (string/from-bytes (x :ch))
# (.indexOf s 61): an int needle is a char CODE on the JVM, not its decimal
# text (ring-codec splits k=v pairs this way)
(number? x) (string/from-bytes (math/trunc x))
(string x)))
# java.lang.Number surface (ring-codec: (.byteValue (Integer/valueOf s 16))).
(def number-methods
{"byteValue" (fn [n] (let [b (band (math/trunc n) 0xff)] (if (> b 127) (- b 256) b)))
"shortValue" (fn [n] (let [v (band (math/trunc n) 0xffff)] (if (> v 32767) (- v 65536) v)))
"intValue" (fn [n] (math/trunc n))
"longValue" (fn [n] (math/trunc n))
"floatValue" (fn [n] (* 1.0 n))
"doubleValue" (fn [n] (* 1.0 n))
"toString" (fn [n &opt radix] (if (= radix 16) (string/format "%x" (math/trunc n)) (string n)))})
# Universal java.lang.Object / exception / persistent-collection methods that
# reitit's :clj branches call on non-string targets: (.getMessage e),
# (.assoc m k v), (.get m k). Consulted in the method-dispatch fallthrough.
(def object-methods
{"getMessage" (fn [e] (cond (and (table? e) (= :jolt/ex-info (get e :jolt/type))) (get e :message)
(string? e) e
(string e)))
"getCause" (fn [e] (and (table? e) (get e :cause)))
"toString" (fn [x] (string x))
"equals" (fn [a b] (deep= a b))
"hashCode" (fn [x] (hash x))
# (.iterator coll) -> a jolt iterator (see :jolt/iterator above). Materializes
# the collection to an indexable array via the late-bound core realizer.
"iterator" (fn [coll] @{:jolt/type :jolt/iterator :pos 0
:items (if coll-realizer (coll-realizer coll) @[])})})
(def string-methods
{"getBytes" (fn [s &opt charset] (buffer s))
"toString" (fn [s] s)
"toLowerCase" (fn [s] (string/ascii-lower s))
"toUpperCase" (fn [s] (string/ascii-upper s))
"trim" (fn [s] (string/trim s))
"intern" (fn [s] s)
# file-path surface: io/file returns plain path strings, so the java.io.File
# / java.net.URL methods selmer's template cache calls land here
"toURI" (fn [s] s)
"toURL" (fn [s] s)
"getPath" (fn [s] s)
"getName" (fn [s] (if-let [i (string/find "/" (string/reverse s))]
(string/slice s (- (length s) i)) s))
"exists" (fn [s] (not (nil? (os/stat s))))
"lastModified" (fn [s] (if-let [st (os/stat s)] (math/floor (* 1000 (st :modified))) 0))
# JVM String.split takes a REGEX string; trailing empties dropped like the JVM
"split" (fn [s re &opt limit]
(def parts (re-split (re-pattern re) s))
(while (and (> (length parts) 0) (= "" (last parts)))
(array/pop parts))
parts)
"length" (fn [s] (length s))
"isEmpty" (fn [s] (= 0 (length s)))
"charAt" (fn [s i] {:jolt/type :jolt/char :ch (s i)})
"codePointAt" (fn [s i] (s i))
"indexOf" (fn [s x &opt from] (or (string/find (str-needle x) s (or from 0)) -1))
"lastIndexOf" (fn [s x]
(let [n (str-needle x)]
(var found -1) (var i 0)
(while (< i (length s))
(let [f (string/find n s i)]
(if f (do (set found f) (set i (+ f 1))) (set i (length s)))))
found))
"substring" (fn [s start &opt end] (string/slice s start end))
"startsWith" (fn [s p] (string/has-prefix? p s))
"endsWith" (fn [s p] (string/has-suffix? p s))
"contains" (fn [s sub] (not (nil? (string/find (str-needle sub) s))))
"concat" (fn [s o] (string s o))
"replace" (fn [s a b] (string/replace-all (str-needle a) (str-needle b) s))
"replaceAll" (fn [s regex replacement] (re-replace-all (re-pattern regex) s replacement))
"replaceFirst" (fn [s regex replacement] (re-replace-first (re-pattern regex) s replacement))
"matches" (fn [s regex] (not (nil? (re-matches (re-pattern regex) s))))
"compareTo" (fn [s o] (cond (< s o) -1 (> s o) 1 0))
"equalsIgnoreCase" (fn [s o] (= (string/ascii-lower s) (string/ascii-lower (string o))))})
(defn resolve-sym
[ctx bindings sym-s]
(let [name (sym-s :name) ns (sym-s :ns)]
# Math/Thread/System/Long and every other class resolve through the generic
# class-statics registry (host_interop registers them at load); no special-case.
(if (get class-statics ns)
(let [v (get (get class-statics ns) name)]
(if (nil? v) (error (string "Unsupported member: " ns "/" name)) v))
(if (not (nil? ns))
(let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx))
aliased-ns (or (ns-alias-lookup current-ns ns) (ns-import-lookup current-ns ns))
target-ns (ctx-find-ns ctx (or aliased-ns ns))
v (and target-ns (ns-find target-ns name))]
(if v (var-get v)
# Explicit Janet interop. The `janet` namespace segment marks every
# crossing into host code, where Clojure semantics no longer hold:
# janet/<name> -> Janet root binding (janet/slurp, janet/type)
# janet.<module>/<name> -> Janet module binding (janet.net/server,
# janet.os/clock)
# This makes the whole Janet stdlib reachable from Clojure while keeping
# the interop boundary visible at the call site.
(if (or (= ns "janet") (string/has-prefix? "janet." ns))
(let [jname (if (= ns "janet") name (string (string/slice ns 6) "/" name))
# worker fibers may carry no env (fiber/new without :e inherit)
# — fall back to the env captured at module load
# four-step resolution: the runtime fiber's env (when it
# has one), the evaluator's module env (worker/connection
# fibers carry a foreign or empty env — net/server handler
# fibers resolve janet/struct through here), the autoload
# cache, then a jpm-module require on first miss
entry (or (when-let [fe (fiber/getenv (fiber/current))]
(in fe (symbol jname)))
(in module-load-env (symbol jname))
(in janet-bridge-extras jname)
(bridge-autoload jname))]
(if (not (nil? entry))
(if (table? entry) (entry :value) entry)
(error (string "Unable to resolve Janet symbol: " jname))))
# syntax-quote ns-qualifies bare class names inside macros
# (selmer.util/StringBuilder); class names never belong to an ns —
# fall back to the constructor / statics shims before giving up.
(if (or (in class-ctors name) (get class-canonical-names name) (get class-value-overrides name))
(class-value-for name)
(error (string "Unable to resolve symbol: " ns "/" name))))))
# Use :jolt/not-found sentinel to distinguish nil binding from absent binding
(let [local (get bindings name :jolt/not-found-1)
local (if (= local :jolt/not-found-1) (binding-get bindings name) local)]
(if (not= local :jolt/not-found)
(if (= local :jolt/nil) nil 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)
# Try class-name resolution: Foo.Bar.Baz -> ns "Foo.Bar", name "Baz"
(let [dot-idx (string/find "." name)]
(if dot-idx
(let [last-dot (do
(var idx dot-idx)
(var next-dot (string/find "." name (+ idx 1)))
(while (not (nil? next-dot))
(set idx next-dot)
(set next-dot (string/find "." name (+ idx 1))))
idx)
class-ns (string/slice name 0 last-dot)
class-name (string/slice name (+ last-dot 1))]
(let [target-ns (ctx-find-ns ctx class-ns) tv (ns-find target-ns class-name)]
(if tv (var-get tv) tv)))
# No implicit Janet fallback (Stage 3): an unresolved
# Clojure symbol is an error. Host access is the explicit
# janet/ prefix above.
(if (or (in class-ctors name) (get class-canonical-names name) (get class-value-overrides name))
(class-value-for name)
(error (string "Unable to resolve symbol: " name " in this context")))))))))))))))
(defn- parse-arg-names
"Parse a parameter vector, handling & rest args.
Returns {:fixed [names...] :rest name-or-nil :all [names...]}"
[args-form]
(var fixed @[])
(var rest-name nil)
(var i 0)
(while (< i (length args-form))
(let [a (in args-form i)]
(if (and (struct? a) (= :symbol (a :jolt/type)) (= "&" (a :name)))
(do
(+= i 1)
(if (< i (length args-form))
(do
(set rest-name ((in args-form i) :name))
(+= i 1))
(error "& without argument in parameter list")))
(do
(if (and (struct? a) (= :symbol (a :jolt/type)))
(array/push fixed (a :name))
# destructuring form: recurse into it
(when (indexed? a)
(var di 0)
(while (< di (length a))
(def inner (in a di))
(if (and (struct? inner) (= :symbol (inner :jolt/type)) (= "&" (inner :name)))
(do
(+= di 1)
(if (< di (length a))
(do
(set rest-name ((in a di) :name))
(+= di 1))
(error "& without argument in parameter list")))
(do
(if (and (struct? inner) (= :symbol (inner :jolt/type)))
(array/push fixed (inner :name))
# nested destructuring - extract names
(when (indexed? inner)
(each sym inner
(when (and (struct? sym) (= :symbol (sym :jolt/type)))
(array/push fixed (sym :name))))))
(+= di 1))))))
(+= i 1)))))
(var all @[])
(each n fixed (array/push all n))
(if rest-name (array/push all rest-name))
{:fixed (tuple/slice (tuple ;fixed)) :rest rest-name :all (tuple/slice (tuple ;all))})
# ============================================================
# Destructuring (Clojure-compatible, recursive)
# ============================================================
(defn parse-params
"Parse a parameter vector into raw patterns: {:fixed [pat...] :rest pat-or-nil}.
Unlike parse-arg-names, patterns are kept intact (not flattened) so they can
be destructured against the corresponding argument."
[args-form]
(var fixed @[])
(var rest-pat nil)
(var i 0)
(while (< i (length args-form))
(let [a (in args-form i)]
(if (and (struct? a) (= :symbol (a :jolt/type)) (= "&" (a :name)))
(do (+= i 1)
(when (< i (length args-form)) (set rest-pat (in args-form i)))
(+= i 1))
(do (array/push fixed a) (+= i 1)))))
{:fixed (tuple/slice (tuple ;fixed)) :rest rest-pat})
(defn rest-args-val
"What a rest param binds to: nil when no args remain (Clojure semantics —
(fn [& r]) called with nothing gives r = nil, never an empty seq)."
[args i]
(when (> (length args) i) (tuple/slice args i)))
(defn plain-sym? [p] (and (struct? p) (= :symbol (p :jolt/type))))
(defn require-symbol-params
"fn* is a primitive: its params must be plain symbols. The fn/defn MACROS desugar
destructuring into plain params + a body let before emitting fn*, so fn* never
legitimately sees a pattern — matching Clojure, where (fn* [[a b]] ...) is the
compile error 'fn params must be Symbols'. Enforcing it here keeps the interpreter
consistent with the self-hosted analyzer (which also requires plain fn* params)
and with Clojure, instead of leniently destructuring a form Clojure rejects."
[param-info]
(each p (param-info :fixed)
(unless (plain-sym? p) (error "fn params must be Symbols")))
(let [r (param-info :rest)]
(when (and r (not (plain-sym? r))) (error "fn params must be Symbols"))))
(defn- d-get
"Look up key k in a map-like value (phm/struct/table/nil)."
[m k]
(cond
(phm? m) (phm-get m k)
(or (struct? m) (table? m)) (get m k)
true nil))
(defn- find-or-default
"Find the :or default expression for binding name nm, or :jolt/none."
[or-map nm]
(var result :jolt/none)
(when or-map
(each k (keys or-map)
(when (and (struct? k) (= :symbol (k :jolt/type)) (= nm (k :name)))
(set result (get or-map k)))))
result)
(var destructure-bind nil)
(set destructure-bind
(fn dbind [ctx bindings pat val]
(cond
# plain symbol
(and (struct? pat) (= :symbol (pat :jolt/type)))
(bind-put bindings (pat :name) val)
# sequential pattern (vector of sub-patterns)
(indexed? pat)
(let [rv (d-realize val)
seqable? (indexed? rv)]
(var di 0) (var vi 0)
(def n (length pat))
(while (< di n)
(let [elem (in pat di)]
(cond
# & rest
(and (struct? elem) (= :symbol (elem :jolt/type)) (= "&" (elem :name)))
(do
# rest binds a seq (jolt list = array), per Clojure semantics.
# For lazy-seqs, preserve laziness: walk vi steps via ls-rest
# instead of slicing the eagerly-realized array.
(destructure-bind ctx bindings (in pat (+ di 1))
(if (lazy-seq? val)
(do
(var c val) (var i 0)
(while (< i vi)
(let [nxt (ls-rest c)]
(if (nil? nxt) (break)
(do (set c nxt) (++ i)))))
c)
(if (and seqable? (< vi (length rv)))
(array/slice (if (tuple? rv) (array/slice rv) rv) vi)
@[])))
(set di (+ di 2)))
# :as whole
(= elem :as)
(do
(destructure-bind ctx bindings (in pat (+ di 1)) val)
(set di (+ di 2)))
# positional element
true
(do
(destructure-bind ctx bindings elem
(if (and seqable? (< vi (length rv))) (in rv vi) nil))
(+= di 1) (+= vi 1))))))
# map pattern (struct/table that isn't a symbol)
(or (struct? pat) (table? pat))
(let [rv (d-realize val)
# Destructuring a sequential value as a map treats it as kwargs:
# alternating k/v pairs, or a single trailing map (Clojure's
# `[& {:keys ...}]`). A real map value is used as-is.
mval (if (and (indexed? rv) (not (or (struct? rv) (table? rv))))
(if (and (= 1 (length rv))
(let [e (in rv 0)] (or (struct? e) (table? e) (phm? e))))
(in rv 0)
(let [m @{}]
(var i 0)
(while (< (+ i 1) (length rv))
(put m (in rv i) (in rv (+ i 1)))
(+= i 2))
m))
val)]
(def or-map (get pat :or))
(def as-sym (get pat :as))
(when as-sym (destructure-bind ctx bindings as-sym mval))
# :keys (keyword), :strs (string), :syms (symbol). A namespaced symbol
# in :keys/:syms (x/y) looks up the namespaced key but binds local y.
(each spec [[:keys :kw] [:strs :str] [:syms :sym]]
(let [kw (in spec 0) kind (in spec 1) names (get pat kw)]
(when (and names (indexed? names))
(each s names
(let [sym? (and (struct? s) (= :symbol (s :jolt/type)))
local (if sym? (s :name) (string s))
nsp (and sym? (s :ns))
key (case kind
:kw (keyword (if nsp (string nsp "/" local) local))
:str local
:sym {:jolt/type :symbol :ns nsp :name local})
v (d-get mval key)
v (if (nil? v)
(let [d (find-or-default or-map local)]
(if (= d :jolt/none) nil (eval-form ctx bindings d)))
v)]
(bind-put bindings local v))))))
# direct {local-pattern key-expr} entries (local may itself be a
# nested vector/map pattern). Special keys are keywords; skip them.
(each k (keys pat)
(when (not (keyword? k))
(let [key-val (eval-form ctx bindings (get pat k))
v (d-get mval key-val)]
(if (and (struct? k) (= :symbol (k :jolt/type)))
# symbol target: apply :or default if missing
(let [nm (k :name)
v (if (nil? v)
(let [d (find-or-default or-map nm)]
(if (= d :jolt/none) nil (eval-form ctx bindings d)))
v)]
(bind-put bindings nm v))
# nested pattern target
(destructure-bind ctx bindings k v))))))
true (error (string "Unsupported destructuring pattern: " (string/format "%q" pat))))))
# ---- host-type protocol extension (extend-protocol String/Number/... ) ----
(def host-type-names
{"Long" true "Integer" true "Short" true "Byte" true "BigInteger" true "BigInt" true
"Double" true "Float" true "Number" true "BigDecimal" true "Ratio" true
"String" true "CharSequence" true "Boolean" true "Character" true
"Keyword" true "Symbol" true "Object" true "IFn" true "Fn" true
"PersistentVector" true "PersistentList" true "PersistentHashMap" true
"PersistentHashSet" true "IPersistentMap" true "IPersistentVector" true
"IPersistentSet" true "IPersistentCollection" true "ISeq" true "Atom" true "nil" true
# java.util interfaces + seq types ring & friends extend on
"Map" true "Set" true "List" true "Collection" true "LazySeq" true
"APersistentMap" true})

845
src/jolt/eval_runtime.janet Normal file
View file

@ -0,0 +1,845 @@
# Jolt Evaluator — protocols, multimethods, deftype/reify, stateful fn install
# Extracted from evaluator.janet (jolt-oudv, phase 2a split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./pv)
(use ./plist)
(use ./config)
(use ./reader)
(use ./regex)
(use ./eval_base)
(use ./eval_resolve)
(defn- canonical-host-tag
"If type-name names a host type (optionally java.*/clojure.lang.* qualified),
return its bare canonical name; else nil (it's a deftype/record name)."
[type-name]
(let [base (cond
(string/has-prefix? "java.lang." type-name) (string/slice type-name 10)
(string/has-prefix? "java.util." type-name) (string/slice type-name 10)
(string/has-prefix? "clojure.lang." type-name) (string/slice type-name 13)
type-name)]
(if (get host-type-names base) base nil)))
(defn- value-host-tags
"Candidate host type-tags for a runtime value, most-specific first."
[obj]
(cond
(number? obj) ["Long" "Integer" "Number" "Double" "Object"]
(string? obj) ["String" "CharSequence" "Object"]
(or (= true obj) (= false obj)) ["Boolean" "Object"]
(keyword? obj) ["Keyword" "Object"]
(and (struct? obj) (= :jolt/char (get obj :jolt/type))) ["Character" "Object"]
(and (struct? obj) (= :symbol (get obj :jolt/type))) ["Symbol" "Object"]
(plist? obj) ["PersistentList" "IPersistentList" "IPersistentCollection" "ISeq" "List" "Collection" "Object"]
(lazy-seq? obj) ["LazySeq" "ISeq" "IPersistentCollection" "Collection" "Object"]
# maps: phm / plain struct / sorted / records — java.util.Map covers them
# all in ring-style extend-protocol clauses
(or (phm? obj)
(shape-rec? obj) # plain shape maps AND records — both map-like
(and (struct? obj) (nil? (get obj :jolt/type)))
(and (table? obj) (or (get obj :jolt/deftype)
(= :jolt/sorted-map (get obj :jolt/type)))))
["PersistentHashMap" "APersistentMap" "IPersistentMap" "Map" "IPersistentCollection" "Object"]
(or (set? obj) (and (table? obj) (= :jolt/sorted-set (get obj :jolt/type))))
["PersistentHashSet" "IPersistentSet" "Set" "IPersistentCollection" "Object"]
(or (tuple? obj) (array? obj) (pvec? obj)) ["PersistentVector" "IPersistentVector" "IPersistentCollection" "ISeq" "Object"]
(or (function? obj) (cfunction? obj)) ["IFn" "Fn" "Object"]
(nil? obj) ["nil" "Object"]
["Object"]))
# ---------------------------------------------------------------------------
# Stateful primitives as ordinary fns (Stage 2 jolt-eaa). These mutate/read the
# per-ctx protocol registry, so they need ctx. They're interned into clojure.core
# as closures over ctx (install-stateful-fns!), which makes them resolve + COMPILE
# as plain :var invokes — the back end embeds the per-ctx var cell, and the closure
# captures ctx so a compiled protocol dispatcher works even when called later.
# Both the interpreter and compiled code call these same closures; there is no
# longer a special-form handler for them. proto/method/type names arrive as
# STRINGS (the defprotocol/extend-type macros pass (name sym), not the symbol).
(defn protocol-dispatch-impl [ctx proto-name method-name obj rest-args]
# an empty jolt rest arg is NIL (Clojure semantics); janet apply needs a tuple
(default rest-args [])
(def type-tag (or (record-tag obj)
(if (and (table? obj) (get obj :jolt/protocol-methods)) (get obj :jolt/deftype))))
(if (and (table? obj) (get obj :jolt/protocol-methods))
(let [reified-fns (get obj :jolt/protocol-methods)
f (get reified-fns (keyword method-name))]
(if f (apply f obj rest-args)
(error (string "No reified method " method-name " for " type-tag))))
(if type-tag
(let [f (find-protocol-method ctx type-tag proto-name method-name)]
(if f (apply f obj rest-args)
(error (string "No method " method-name " in " proto-name " for " type-tag))))
# host value: try candidate host type-tags (Long/String/Object/...), with a
# generation-guarded inline cache (same walk for every value of a host class).
(let [env (ctx :env)
reg-gen (or (get env :type-registry-gen) 0)
pc (let [c (get env :proto-dispatch-cache)]
(if (and c (= (c :gen) reg-gen)) c
(let [n @{:gen reg-gen :map @{}}]
(put env :proto-dispatch-cache n) n)))
cands (value-host-tags obj)
ckey [(first cands) proto-name method-name]
cached (get (pc :map) ckey)
found (if (nil? cached)
(let [f (do (var r nil)
(each tag cands
(when (nil? r)
(set r (find-protocol-method ctx tag proto-name method-name))))
r)]
(put (pc :map) ckey (if f f :jolt/none))
f)
(if (= cached :jolt/none) nil cached))]
(if found (apply found obj rest-args)
(error (string "No dispatch for " method-name " on " (type obj))))))))
(defn register-method-impl [ctx type-name proto-name method-name f]
# host types register under a bare canonical tag; deftype/record names stay
# namespace-qualified to the ns the (extend-)type form runs in.
(def host (canonical-host-tag type-name))
(def type-tag (if host host (string (ctx-current-ns ctx) "." type-name)))
(register-protocol-method ctx type-tag proto-name method-name f))
(defn make-reified-impl [ctx methods-map & rest-args]
# methods-map is the EVALUATED {keyword fn} map (a phm when compiled, a struct/
# table when interpreted) — the fn* literals are already fns, just store them.
# proto-names are the (short) names of every protocol the reify implements.
(def proto-names (if (and (= 1 (length rest-args)) (indexed? (in rest-args 0)))
(in rest-args 0) # wiring passed the rest tuple as one arg
rest-args))
(def obj @{:jolt/deftype (string "reified-" (if (> (length proto-names) 0) (in proto-names 0) ""))
:jolt/protocols (tuple ;proto-names)
:jolt/protocol-methods @{}})
(def pairs (if (phm? methods-map)
(phm-entries methods-map)
(map (fn [k] [k (get methods-map k)]) (keys methods-map))))
(each p pairs (put (obj :jolt/protocol-methods) (in p 0) (in p 1)))
obj)
(defn require-impl
"(require '[ns :as a :refer [...]] ...) — load + alias/refer each spec. A fn, so
the args (quoted specs) arrive evaluated. Varargs (Clojure-compatible); each spec
is a vector [ns & opts] or a bare ns symbol (treated as [ns])."
[ctx & specs]
(each spec specs
(let [s (if (pvec? spec) (pv->array spec) spec)]
(cond
(and (indexed? s) (> (length s) 0)) (eval-require ctx s)
(and (struct? s) (= :symbol (s :jolt/type))) (eval-require ctx @[s])
(error "require expects a vector spec or a namespace symbol"))))
nil)
(defn in-ns-impl
"(in-ns 'foo) — switch the current namespace (creating it if needed). A fn; the
quoted symbol arrives evaluated."
[ctx sym]
(def ns-name (if (and (struct? sym) (= :symbol (sym :jolt/type))) (sym :name) (string sym)))
(def the-ns-obj (ctx-find-ns ctx ns-name))
# An ns entered in-session counts as loaded (Clojure's ns macro commutes the
# name into *loaded-libs*), so a later require/use of it must not try to load
# a file — see maybe-require-ns. Namespace objects are immutable structs, so
# the set lives on the env.
(def loaded (or (get (ctx :env) :loaded-namespaces)
(let [t @{}] (put (ctx :env) :loaded-namespaces t) t)))
(put loaded ns-name true)
(ctx-set-current-ns ctx ns-name)
the-ns-obj)
(defn use-impl
"(use '[ns ...] ...) — refer ALL public vars of each used ns into the CURRENT ns.
A fn; quoted specs arrive evaluated. Each spec is a ns symbol or a [ns & opts]
vector (a pvec/tuple, not a Janet array — coerce, then take the head as the ns)."
[ctx & specs]
(def target-ns (ctx-find-ns ctx (ctx-current-ns ctx)))
(each s specs
(let [spec (if (pvec? s) (pv->array s) s)
ns-sym (if (indexed? spec) (in spec 0) spec)
src-name (sym-name-str ns-sym)]
(maybe-require-ns ctx src-name)
(let [source-ns (ctx-find-ns ctx src-name)]
# Refer maps the SOURCE VAR itself (the Clojure model): redefinitions in
# the source ns propagate, the :macro flag travels for free, and
# ns-refers can identify refers by the var's home :ns.
(loop [[sym v] :pairs (source-ns :mappings)]
(put (target-ns :mappings) sym v)))))
nil)
(defn import-impl
"(import 'pkg.Class ...) — register the short class name as an alias of the fully
qualified name in the current ns. A fn; quoted class symbols arrive evaluated."
[ctx & class-specs]
(def ns (ctx-find-ns ctx (ctx-current-ns ctx)))
(defn sym-name [x] (if (and (struct? x) (= :symbol (x :jolt/type))) (x :name) (string x)))
(defn import-one [class-name &opt pkg]
(def last-dot (do (var idx -1) (var pos 0)
(while (< pos (length class-name))
(when (= (class-name pos) 46) (set idx pos)) (++ pos))
idx))
(def short-name (if (>= last-dot 0) (string/slice class-name (+ last-dot 1)) class-name))
(def pkg-name (cond pkg pkg (>= last-dot 0) (string/slice class-name 0 last-dot) nil))
(ns-import ns short-name class-name)
# a deftype "class" lives as a ctor var in its defining jolt ns — share it
# (the JVM import makes (TextNode. ...) resolvable; this is our analog)
(when pkg-name
(when-let [src-ns (get ((ctx :env) :namespaces) pkg-name)
v (ns-find src-ns short-name)]
(put (ns :mappings) short-name v))))
(each class-spec class-specs
(if (or (array? class-spec) (tuple? class-spec)
(and (table? class-spec) (= :jolt/pvec (class-spec :jolt/type))))
# vector spec: [pkg Class1 Class2 ...]
(let [items (if (table? class-spec) (pv->array class-spec) class-spec)
pkg (sym-name (in items 0))]
(for i 1 (length items)
(import-one (string pkg "." (sym-name (in items i))) pkg)))
(import-one (sym-name class-spec))))
nil)
(defn refer-clojure-impl
"(refer-clojure :exclude [a b]) — currently only :exclude is honored: unmap the
excluded names from the current ns. A fn; quoted args arrive evaluated."
[ctx & args]
(when (and (>= (length args) 2) (= (in args 0) :exclude))
(let [ns (ctx-find-ns ctx (ctx-current-ns ctx))
excl (in args 1)]
(each sym excl
(ns-unmap ns (if (and (struct? sym) (= :symbol (sym :jolt/type))) (sym :name) (string sym))))))
nil)
# Multimethod value -> its var. methods/get-method take the multimethod VALUE
# (Clojure semantics) and recover the var (hence :jolt/methods) through this,
# which works from a compiled fn in any namespace — resolving the symbol at call
# time in the current ns did not (a bare multifn ref in its defining ns saw an
# empty table once defmethods lived in other namespaces; migratus hit this).
(def multi-registry @{})
(defn defmulti-setup
"(defmulti name dispatch & opts) — intern a multimethod var. A fn; name arrives
quoted, dispatch + opts (:default key, :hierarchy h) arrive evaluated. The
defmulti macro is the thin wrapper. Builds the dispatch closure over the method
table (shared with the var's :jolt/methods so defmethod adds to it)."
[ctx name-sym dispatch-raw & opts]
(def dispatch-fn (if (keyword? dispatch-raw) (fn [x] (get x dispatch-raw)) dispatch-raw))
(def default-key
(do (var dv :default) (var i 0)
(while (< i (length opts))
(if (= :default (in opts i)) (do (set dv (in opts (+ i 1))) (set i (length opts))) (+= i 2)))
dv))
(def hierarchy
(do (var h nil) (var i 0)
(while (< i (length opts))
(if (= :hierarchy (in opts i)) (do (set h (in opts (+ i 1))) (set i (length opts))) (+= i 2)))
h))
(def ns (ctx-find-ns ctx (ctx-current-ns ctx)))
(def methods @{})
(def isa-cache @[nil])
(def dispatch-cache @{})
# the prefers table, shared with the var (prefer-method-setup mutates it)
(def v-box @[nil])
(def mm-fn
(fn [& args]
(let [dv* (apply dispatch-fn args)
dv (if (nil? dv*) :jolt/nil-sentinel dv*)
method (get methods dv)]
(if method
(apply method args)
(let [cached (get dispatch-cache dv)]
(if cached
(apply cached args)
# isa? is the OVERLAY's (the hierarchy system is pure Clojure now,
# stage 3); resolve its var lazily, once. A :hierarchy option is an
# atom (deref per dispatch, like Clojure's var) or a plain map.
(let [isa-fn (do
(when (nil? (isa-cache 0))
(put isa-cache 0
(var-get (ns-find (ctx-find-ns ctx "clojure.core") "isa?"))))
(isa-cache 0))
h (if hierarchy
(if (and (table? hierarchy) (= :jolt/atom (get hierarchy :jolt/type)))
(hierarchy :value)
hierarchy)
nil)
# Collect EVERY isa-matching method key, then pick the
# dominant one: x dominates y when x is prefer-method'd
# over y (direct preference) or (isa? x y). Two matches
# with no dominant is an ambiguity ERROR, as in Clojure —
# this used to silently take whichever key the table
# yielded first, ignoring prefer-method (jolt-heo).
found (do
(def matches @[])
(each k (keys methods)
(when (if h (isa-fn h dv k) (isa-fn dv k))
(array/push matches k)))
(defn pref? [x y]
(def px (get (or (get v-box 0) @{}) x))
(and px (not (nil? (get px y)))))
(defn dom? [x y]
(or (pref? x y) (if h (isa-fn h x y) (isa-fn x y))))
(case (length matches)
0 nil
1 (get methods (in matches 0))
(do
(var best (in matches 0))
(var i 1)
(while (< i (length matches))
(when (dom? (in matches i) best) (set best (in matches i)))
(++ i))
(var amb nil)
(each k matches
(when (and (nil? amb) (not (deep= k best)) (not (dom? best k)))
(set amb k)))
(when amb
(error (string "Multiple methods in multimethod '" (name-sym :name)
"' match dispatch value — neither is preferred")))
(get methods best))))]
(if found
(do (put dispatch-cache dv found) (apply found args))
(let [dm (get methods default-key)]
(if dm (apply dm args)
(error (string "No method in multimethod " (name-sym :name)
" for dispatch value: " dv))))))))))))
(def v (ns-intern ns (name-sym :name) mm-fn))
# pre-create the prefers store so the dispatch closure and
# prefer-method-setup share one table
(def prefs-tbl (or (get v :jolt/prefers)
(do (put v :jolt/prefers @{}) (get v :jolt/prefers))))
(put v-box 0 prefs-tbl)
(put v :jolt/methods methods)
(put v :jolt/dispatch-cache dispatch-cache)
(put v :jolt/default default-key)
(when hierarchy (put v :jolt/hierarchy hierarchy))
(put multi-registry mm-fn v)
(var-get v))
(defn defmethod-setup
"(defmethod mm dispatch-val impl) — add a method to a multimethod. A fn; mm
arrives quoted, dispatch-val evaluated, impl is the COMPILED method fn (the
defmethod macro builds (fn …)). Auto-creates the multimethod if it's missing."
[ctx mm-sym dispatch-val impl]
(def mm-var
(or (resolve-var ctx @{} mm-sym)
(let [ns (ctx-find-ns ctx (ctx-current-ns ctx))
stub (fn [& args] nil)]
(def v (ns-intern ns (mm-sym :name) stub))
(put v :jolt/methods @{})
(put multi-registry stub v)
v)))
(def methods (or (get mm-var :jolt/methods) (let [m @{}] (put mm-var :jolt/methods m) m)))
# nil is a legal dispatch value (ring's body-string keys a method on it);
# janet tables can't hold nil keys, so it rides the sentinel
(put methods (if (nil? dispatch-val) :jolt/nil-sentinel dispatch-val) impl)
(let [dc (get mm-var :jolt/dispatch-cache)]
(when dc (each k (keys dc) (put dc k nil))))
mm-var)
(defn- hint-cross-ns-key
"Resolve a record-typed field hint (\"Vec3\", \"v/Vec3\", \"rt.vec/Vec3\") to the
home namespace's ctor key (\"rt.vec/->Vec3\") when the type is defined in a
DIFFERENT namespace and referred/aliased into the one being defined. The local
current-ns/->Type lookup misses those; this resolves the hint name through the
ns's :refer/:as bindings to the type var, then maps its root ctor value back to
the home key via the ctor-value index. Using the ctor VALUE, not the var's :ns,
is what makes :refer work — a :refer re-interns a fresh var whose :ns is the
referring ns, but its root is the same shared ctor closure. nil if unresolved."
[ctx t cix]
# Resolve against the COMPILE ns (the user ns being analyzed), not ctx-current-ns
# — during compilation the analyzer rebinds ctx-current-ns to jolt.analyzer, so a
# bare referred name would otherwise miss. Qualified alias/Name resolves the alias
# against the compile ns; a bare name looks up the compile ns's own mappings
# (which include :refer-interned vars).
(def cur-name (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx)))
(def cur-ns (ctx-find-ns ctx cur-name))
(def slash (string/find "/" t))
(def v (when cur-ns
(if slash
(let [a (string/slice t 0 slash) nm (string/slice t (inc slash))
home (or (ns-alias-lookup cur-ns a) (ns-import-lookup cur-ns a))]
(when home (ns-find (ctx-find-ns ctx home) nm)))
(ns-find cur-ns t))))
(when (and v (table? v)) (get cix (v :root))))
(defn record-hint-ctor-key
"Resolve a record-type hint NAME (as written on a ^Type field/param — bare,
aliased, or fully qualified) to its home ctor key in the record-shapes registry
(\"rt.vec/->Vec3\"), or nil if it is not a known record type. Local
current-ns/->Name wins; otherwise cross-ns via the ctor-value index. Public so
the analyzer (through jolt.host) can type a ^Type PARAM hint exactly as a field
hint resolves, which is what carries a record param's type across a namespace
boundary without whole-program inference."
[ctx name]
(def rs (get (ctx :env) :record-shapes))
(when rs
(def cur (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx)))
(def local (string cur "/->" name))
(if (get rs local)
local
(let [cix (get (ctx :env) :record-ctor-index)]
(when cix (hint-cross-ns-key ctx name cix))))))
(defn make-deftype-ctor-impl
"Build a deftype constructor closure. The ns-qualified type tag is baked at
definition time (this runs during the deftype's (def …), in the type's ns), so
instances carry a stable tag matching what extend-type registers methods under.
field-kws is the [:f1 :f2 …] keyword vector; the ctor maps positional args to
those keys. A ctx-capturing closure (make-deftype-ctor) is the public handle."
[ctx type-name-sym field-kws &opt field-tags]
(def type-tag (string (ctx-current-ns ctx) "." (type-name-sym :name)))
(def kws (d-realize field-kws))
# per-field type hints (jolt-3ko): a tuple parallel to kws — "Vec3" (a record
# type name), "num", or nil. The inference resolves these to the field's exact
# type so reading a field back carries it (a nested record stays typed).
(def tags (if field-tags (d-realize field-tags) (array/new-filled (length kws))))
# The ctor closure itself. Built FIRST so it can be indexed by value below.
# Records are shape-recs when shapes are active (:shapes? = direct-link, where
# the inference proves the reads) — the whole field-access pipeline handles
# them; otherwise the original :jolt/deftype tables. Read at ctor-BUILD time so
# a type is consistently one representation or the other.
(def the-ctor
(if (get (ctx :env) :shapes?)
(fn [& args] (make-record type-tag kws args))
(fn [& args]
(var inst @{:jolt/deftype type-tag})
(var i 0) (each kw kws (put inst kw (in args i)) (++ i))
inst)))
# jolt-t34: register this record's ctor return shape (DECLARED field order) so
# the inference types (->Name ...) as a struct of these fields and field reads
# on the result bare-index. Keyed by the ctor var-key "ns/->Name" to match how
# the IR names the call head. Harmless when records aren't shaped (sidx gated).
(let [rs (or (get (ctx :env) :record-shapes)
(let [t @{}] (put (ctx :env) :record-shapes t) t))
# ctor-value index: maps each ctor closure to its rs key, so a ^Type hint
# in another namespace can resolve home through the type var's root value
# (jolt-3ko cross-ns hints; see hint-cross-ns-key).
cix (or (get (ctx :env) :record-ctor-index)
(let [t @{}] (put (ctx :env) :record-ctor-index t) t))
# resolve a record-typed hint ("Vec3") to its ctor-key ("ns/->Vec3") so
# the inference resolves it with a direct lookup. "num" stays as-is; a
# local def wins; else try cross-ns resolution; an unresolved name (not a
# known record type) stays bare -> :any.
resolved (map (fn [t]
(cond (nil? t) nil
(= t "num") "num"
(let [ck (string (ctx-current-ns ctx) "/->" t)]
(if (get rs ck) ck
(or (hint-cross-ns-key ctx t cix) t)))))
tags)]
(put rs (string (ctx-current-ns ctx) "/->" (type-name-sym :name))
{:fields (tuple ;kws) :type type-tag :tags (tuple ;resolved)})
(put cix the-ctor (string (ctx-current-ns ctx) "/->" (type-name-sym :name))))
the-ctor)
(defn install-stateful-fns!
"Intern ctx-capturing closures for the stateful primitives into clojure.core, so
both the interpreter and the compiler reach them as ordinary fns. Called by
api/init after init-core! and before the overlay loads (the protocol macros
expand to calls of these)."
[ctx]
(def core (ctx-find-ns ctx "clojure.core"))
# current-ns get/set for compiled code (emit-try restores the ns on a caught
# throw — an interpreted fn that throws leaves ctx-current-ns set to its
# defining ns, since it can't restore on unwind; the interpreted try already
# repairs this, the compiled try did not, leaking the ns past a catch).
(ns-intern core "__current-ns" (fn [] (ctx-current-ns ctx)))
(ns-intern core "__set-current-ns!" (fn [ns-sym] (ctx-set-current-ns ctx ns-sym) nil))
(ns-intern core "protocol-dispatch"
(fn [proto-name method-name obj rest-args]
(protocol-dispatch-impl ctx proto-name method-name obj rest-args)))
# Devirtualization registry (jolt-41m): defprotocol calls this at load so the
# inference can recognize a protocol-method call site. Maps the method's
# var-key "ns/method" -> [proto-name method-name].
(ns-intern core "register-protocol-methods!"
(fn [proto-name method-names]
(def reg (or (get (ctx :env) :protocol-methods)
(let [t @{}] (put (ctx :env) :protocol-methods t) t)))
(def ns (ctx-current-ns ctx))
(each m (d-realize method-names) (put reg (string ns "/" m) (tuple proto-name m)))
nil))
(ns-intern core "extenders"
(fn [proto]
# All type-tags whose registry entry implements this protocol, as symbols
# (closest analog to Clojure's class list); nil when none.
(let [pname (get (get proto :name) :name)
registry (get (ctx :env) :type-registry)
out @[]]
(each tag (keys registry)
(when (get (get registry tag) pname)
(array/push out {:jolt/type :symbol :ns nil :name tag})))
(if (empty? out) nil (tuple ;out)))))
(ns-intern core "register-method"
(fn [type-name proto-name method-name f]
(register-method-impl ctx type-name proto-name method-name f)))
(ns-intern core "make-reified"
(fn [methods-map & proto-names] (make-reified-impl ctx methods-map proto-names)))
# Host-class shim registration, exposed to Clojure so a library can mirror a
# Java class jolt doesn't ship (e.g. reitit.Trie). __register-class-statics!
# makes (Class/method ...) resolve; __register-class-methods! makes (.method
# tagged-value ...) dispatch; __register-class-ctor! makes (Class. ...) build.
# Reader-conditional feature toggle, exposed to Clojure so a namespace can
# load a clj-targeted library (e.g. reitit, under :clj) WITHOUT forcing the
# whole process to :clj — set features, require the lib, restore. Returns the
# previous feature set (a list of name strings) for restoration.
(ns-intern core "__reader-features"
(fn [] (tuple ;(map (fn [k] (string k)) (keys reader-features)))))
(ns-intern core "__reader-features-set!"
(fn [names]
# names arrives as a jolt vector (pvec) or list — coerce to a janet array
(def arr (cond (pvec? names) (pv->array names)
(or (tuple? names) (array? names)) names
@[names]))
(reader-features-set! (map (fn [n] (if (keyword? n) n (string n))) arr))
nil))
(ns-intern core "__register-class-statics!"
(fn [nm tbl] (register-class-statics! nm tbl) nil))
(ns-intern core "__register-class-methods!"
(fn [tag tbl] (register-tagged-methods! tag tbl) nil))
(ns-intern core "__register-class-ctor!"
(fn [nm f] (register-class-ctor! nm f) (ns-intern core nm (class-value-for nm)) nil))
(ns-intern core "require" (fn [& specs] (require-impl ctx ;specs)))
(ns-intern core "in-ns" (fn [sym] (in-ns-impl ctx sym)))
(ns-intern core "use" (fn [& specs] (use-impl ctx ;specs)))
(ns-intern core "import" (fn [& specs] (import-impl ctx ;specs)))
(ns-intern core "refer-clojure" (fn [& args] (refer-clojure-impl ctx ;args)))
(ns-intern core "defmulti-setup" (fn [name-sym dispatch & opts] (defmulti-setup ctx name-sym dispatch ;opts)))
(ns-intern core "defmethod-setup" (fn [mm-sym dval impl] (defmethod-setup ctx mm-sym dval impl)))
(ns-intern core "make-deftype-ctor" (fn [name-sym field-kws &opt field-tags] (make-deftype-ctor-impl ctx name-sym field-kws field-tags)))
# Var/namespace lookups that need the ctx (the rest of the var fns — var-get/
# var-set/var?/alter-var-root/alter-meta!/reset-meta! — are plain core-bindings).
(ns-intern core "find-var" (fn [sym] (find-var ctx sym)))
# *ns*: the current-namespace dynamic var. Its root is kept in sync by
# ctx-set-current-ns via the cached var table (env :ns-var); a thread
# binding (binding [*ns* ...]) shadows the root through var-get as usual.
(def ns-var (ns-intern core "*ns*" (ctx-find-ns ctx (ctx-current-ns ctx))))
(put ns-var :dynamic true)
(put (ctx :env) :ns-var ns-var)
(ns-intern core "intern"
(fn [ns-name sym-name &opt val]
(def ns (ctx-find-ns ctx (if (struct? ns-name) (ns-name :name) ns-name)))
(ns-intern ns (if (struct? sym-name) (sym-name :name) sym-name) val)))
# --- ns introspection (Stage 2 tier 6b) — evaluated-arg Clojure semantics.
# A namespace designator is an ns object (passes through) or a symbol/string
# naming one. find-ns is a pure lookup (nil when absent); create-ns creates
# (ctx-find-ns is create-on-demand). The optional-arg forms default to the
# current ns, preserving the prior 0-arg interpreter behavior.
(def ns-name-of (fn [x]
(cond
(and (struct? x) (= :symbol (x :jolt/type))) (x :name)
(string? x) x
(keyword? x) (string x)
nil)))
(def ns-of (fn [x]
(if (= :jolt/namespace (get x :jolt/type))
x
(let [nm (ns-name-of x)]
(if nm (get (get (ctx :env) :namespaces) nm) nil)))))
(def ns-or-current (fn [x]
(if (nil? x)
(ctx-find-ns ctx (ctx-current-ns ctx))
(or (ns-of x) (error (string "No namespace: " (ns-name-of x)))))))
(ns-intern core "find-ns" (fn [x] (ns-of x)))
(ns-intern core "create-ns" (fn [x] (ctx-find-ns ctx (ns-name-of x))))
(ns-intern core "remove-ns" (fn [x] (remove-ns ctx (ns-name-of x))))
(ns-intern core "all-ns" (fn [] (all-ns ctx)))
(ns-intern core "the-ns" (fn [&opt x] (ns-or-current x)))
# interns/imports return a jolt MAP (struct), not the live host table — so
# count/seq/keys work on them, and callers can't mutate the ns through them.
(ns-intern core "ns-interns" (fn [&opt x] (table/to-struct ((ns-or-current x) :mappings))))
# {alias-symbol -> namespace object}, Clojure's shape, from the string store.
(ns-intern core "ns-aliases"
(fn [&opt x]
(def ns (ns-or-current x))
(def out @{})
(eachp [a target] (ns :aliases)
(put out {:jolt/type :symbol :ns nil :name a} (ctx-find-ns ctx target)))
(table/to-struct out)))
(ns-intern core "ns-imports" (fn [&opt x] (table/to-struct ((ns-or-current x) :imports))))
# (ns-resolve ns sym) -> the var or nil. Unqualified syms look in ns's own
# mappings; ns-qualified syms resolve through ns's aliases. (types/ns-resolve
# keys ns-find with the symbol struct instead of its name string, so it never
# finds anything — do the lookup here.)
(ns-intern core "ns-resolve"
(fn [ns-d sym]
(def ns (ns-or-current ns-d))
(def nm (if (struct? sym) (sym :name) (string sym)))
(def nsp (if (struct? sym) (sym :ns) nil))
(if nsp
(let [target (or (ns-alias-lookup ns nsp) nsp)
target-ns (ctx-find-ns ctx target)]
(when target-ns (ns-find target-ns nm)))
(ns-find ns nm))))
(ns-intern core "resolve"
(fn [sym]
(when (and (struct? sym) (= :symbol (sym :jolt/type)))
(def r (protect (resolve-var ctx @{} sym)))
(if (r 0) (r 1) nil))))
# refer: bring another ns's public vars into the current ns. Reuses use-impl's
# refer-all behavior; the :only/:exclude/:rename filters are not yet honored.
(ns-intern core "refer" (fn [ns-sym & filters] (use-impl ctx ns-sym)))
# --- dispatch-table / type fns (Stage 2 tier 6c) ------------------------
# A multimethod's method table lives on its VAR (the value is the dispatch
# closure), so the overlay macros pass the NAME quoted — the defmulti/
# defmethod pattern — and these resolve the var. prefer-method auto-creates
# a missing multimethod (matching the prior interpreter arm).
(def mm-var-of (fn [mm-sym auto-create?]
(def r (protect (resolve-var ctx @{} mm-sym)))
(def found (if (r 0) (r 1) nil))
(if found
found
(when auto-create?
(def ns (ctx-find-ns ctx (ctx-current-ns ctx)))
(def stub (fn [& args] nil))
(def nv (ns-intern ns (mm-sym :name) stub))
(put nv :jolt/methods @{})
(put multi-registry stub nv)
nv))))
(def clear-dispatch-cache! (fn [mm-var]
(let [dc (get mm-var :jolt/dispatch-cache)]
(when dc (each k (keys dc) (put dc k nil))))))
(ns-intern core "prefer-method-setup"
(fn [mm-sym dval-a dval-b]
(def mm-var (mm-var-of mm-sym true))
(def prefs (or (get mm-var :jolt/prefers)
(do (put mm-var :jolt/prefers @{}) (mm-var :jolt/prefers))))
# {x -> {y true ...}}: x is preferred over each y (Clojure's {x #{y}})
(def sub (or (get prefs dval-a)
(do (put prefs dval-a @{}) (get prefs dval-a))))
(put sub dval-b true)
(clear-dispatch-cache! mm-var)
mm-var))
(ns-intern core "remove-method-setup"
(fn [mm-sym dval]
(def dval (if (nil? dval) :jolt/nil-sentinel dval))
(def mm-var (mm-var-of mm-sym false))
(when mm-var
(let [methods (get mm-var :jolt/methods)]
(when methods (put methods dval nil)))
(clear-dispatch-cache! mm-var))
mm-var))
(ns-intern core "remove-all-methods-setup"
(fn [mm-sym]
(def mm-var (mm-var-of mm-sym false))
(when mm-var
# clear IN PLACE: the dispatch closure captured this table at defmulti
# time, so swapping in a fresh one leaves dispatch seeing stale methods
(let [methods (get mm-var :jolt/methods)]
(when methods (each k (keys methods) (put methods k nil))))
(clear-dispatch-cache! mm-var))
mm-var))
(ns-intern core "prefers-setup"
(fn [mm-sym]
(def mm-var (mm-var-of mm-sym false))
(or (and mm-var (get mm-var :jolt/prefers)) {})))
# methods/get-method receive the multimethod VALUE (Clojure semantics): map it
# back to its var via multi-registry. A symbol arg still works (mm-var-of), for
# any caller that passes one.
(def mm-var-of-val (fn [mm]
(if (function? mm) (get multi-registry mm) (mm-var-of mm false))))
(ns-intern core "get-method-setup"
(fn [mm dval]
(def dval (if (nil? dval) :jolt/nil-sentinel dval))
(def mm-var (mm-var-of-val mm))
(when mm-var
(let [methods (get mm-var :jolt/methods)]
(or (get methods dval) (get methods :default))))))
(ns-intern core "methods-setup"
(fn [mm]
(def mm-var (mm-var-of-val mm))
(when mm-var
# a jolt map, not the live host table (and phm so vector dispatch
# values look up by value, same reason build-eval-map promotes)
(var m (make-phm))
(let [tbl (get mm-var :jolt/methods)]
(when tbl (each k (keys tbl) (set m (phm-assoc m k (get tbl k))))))
m)))
# satisfies?: evaluated protocol value + instance. Recognizes a reify the same
# way instance? does — by the protocols it records on itself (a reify's methods
# are instance-local, so they aren't in the global type registry that
# type-satisfies? consults).
(ns-intern core "satisfies?"
(fn [proto obj]
(def pn (proto :name))
(def pn-str (if (struct? pn) (pn :name) pn))
(def protos (if (table? obj) (get obj :jolt/protocols)))
(def type-tag (or (record-tag obj)
(if (and (table? obj) (get obj :jolt/protocol-methods))
(get obj :jolt/deftype))))
(cond
(and protos (string? pn-str)
(truthy? (some (fn [p] (= (last (string/split "." p))
(last (string/split "." pn-str))))
protos))) true
type-tag (type-satisfies? ctx type-tag pn-str)
false)))
# instance?: the overlay macro passes the TYPE NAME quoted (class names don't
# evaluate to values on jolt); the value arg arrives evaluated.
(ns-intern core "instance-check"
(fn [type-sym val]
(if (record-tag val)
(let [type-tag (record-tag val)
type-name (type-sym :name)]
(or (= type-tag type-name)
(and (> (length type-tag) (length type-name))
(= (string/slice type-tag (- (length type-tag) (length type-name)))
type-name))
# instance? of a PROTOCOL works like satisfies?: a reify implementing
# it is an instance. The reify records every protocol it implements
# (short names); (instance? a.b.Proto x) passes a qualified name, so
# match by short name against any of them. (malli relies on this.)
(let [protos (if (table? val) (get val :jolt/protocols))
tn-short (last (string/split "." type-name))]
(and protos (truthy? (some (fn [p] (= (last (string/split "." p)) tn-short)) protos))))))
(match (type-sym :name)
"Number" (number? val)
"java.lang.Number" (number? val)
"Long" (number? val)
"java.lang.Long" (number? val)
"Integer" (number? val)
"Double" (number? val)
"String" (string? val)
"java.lang.String" (string? val)
"Boolean" (or (= true val) (= false val))
"Keyword" (keyword? val)
# regex patterns (cuerdas-style (instance? Pattern x) checks)
"Pattern" (and (table? val) (= :jolt/regex (val :jolt/type)))
"java.util.regex.Pattern" (and (table? val) (= :jolt/regex (val :jolt/type)))
"Character" (and (struct? val) (= :jolt/char (get val :jolt/type)))
"java.lang.Character" (and (struct? val) (= :jolt/char (get val :jolt/type)))
# java.time shims (host_interop.janet); #inst IS java.util.Date in Clojure
"java.util.Date" (and (struct? val) (= :jolt/inst (get val :jolt/type)))
"Date" (and (struct? val) (= :jolt/inst (get val :jolt/type)))
"Instant" (and (table? val) (= :jolt/instant (get val :jolt/type)))
"java.time.Instant" (and (table? val) (= :jolt/instant (get val :jolt/type)))
"LocalDateTime" (and (table? val) (= :jolt/local-dt (get val :jolt/type)))
"java.time.LocalDateTime" (and (table? val) (= :jolt/local-dt (get val :jolt/type)))
"ZonedDateTime" (and (table? val) (= :jolt/zoned-dt (get val :jolt/type)))
"java.time.ZonedDateTime" (and (table? val) (= :jolt/zoned-dt (get val :jolt/type)))
"LocalTime" false
"LocalDate" false
"java.sql.Time" false
"java.sql.Timestamp" false
"java.sql.Date" false
"DateTimeFormatter" (and (table? val) (= :jolt/dt-formatter (get val :jolt/type)))
"URL" (and (table? val) (= :jolt/url (get val :jolt/type)))
"java.net.URL" (and (table? val) (= :jolt/url (get val :jolt/type)))
# next.jdbc host shim: a wrapped jdbc.core connection (core.janet).
# migratus's do-commands only runs SQL through its (instance? Connection)
# branch, so the wrapped conn must answer true here.
"Connection" (and (table? val) (= :jolt/jdbc-conn (get val :jolt/type)))
"java.sql.Connection" (and (table? val) (= :jolt/jdbc-conn (get val :jolt/type)))
# java.io.File model (jolt-hjw): io/file and (File. …) build :jolt/file,
# so migratus's (instance? File migration-dir) takes the filesystem path.
"File" (and (table? val) (= :jolt/file (get val :jolt/type)))
"java.io.File" (and (table? val) (= :jolt/file (get val :jolt/type)))
# JVM char[] class — (Class/forName "[C"); jolt char arrays are Janet
# arrays of char structs
"[C" (and (array? val)
(or (= 0 (length val))
(and (struct? (val 0)) (= :jolt/char ((val 0) :jolt/type)))))
"clojure.lang.Atom" (and (table? val) (= :jolt/atom (val :jolt/type)))
"clojure.lang.Volatile" (and (table? val) (= :jolt/volatile (val :jolt/type)))
"clojure.lang.Delay" (and (table? val) (= :jolt/delay (val :jolt/type)))
"clojure.lang.IPersistentMap" (or (phm? val) (struct? val))
"clojure.lang.IPersistentVector" (or (tuple? val) (pvec? val))
"clojure.lang.IPersistentSet" (set? val)
"Object" true
false))))
# Reader / expansion as plain fns: read-string parses one form; macroexpand-1
# expands a (quoted, already-evaluated) call form once via its macro var.
(ns-intern core "read-string" (fn [s] (parse-string s)))
# The *in* reader family's host seams. __stdin-read-line: one line from real
# stdin, newline stripped, nil at EOF. __parse-next: one form off a string ->
# [form rest-of-string], nil when only whitespace remains. *in*, read-line,
# read, with-in-str, and line-seq are Clojure over these (core/50-io.clj).
# The loader's registered source roots (the closest thing to a classpath) —
# io/resource searches these for relative resource paths.
# registered constructor shims: the NAME evaluates to the canonical class
# string (so class-dispatch defmultis match); `new` finds the ctor fn.
(eachp [nm f] class-ctors (ns-intern core nm (class-value-for nm)))
# dispatch-only type names (no ctor): InputStream, File, ISeq, ...
(eachp [nm canon] class-canonical-names
(unless (or (in class-ctors nm) (ns-find core nm))
(ns-intern core nm canon)))
(ns-intern core "__source-roots"
(fn [] (tuple ;(get (ctx :env) :source-paths))))
(ns-intern core "__stdin-read-line"
(fn []
(let [l (file/read stdin :line)]
(if (nil? l) nil
(let [s (string l)]
(if (string/has-suffix? "\n" s) (string/slice s 0 -2) s))))))
(ns-intern core "__parse-next"
(fn [s]
(if (= 0 (length (string/trim s))) nil
(let [r (parse-next s)] (tuple (r 0) (r 1))))))
(def expand-1 (fn [the-form]
(if (and (array? the-form) (> (length the-form) 0)
(struct? (first the-form)) (= :symbol ((first the-form) :jolt/type)))
(let [v (resolve-var ctx @{} (first the-form))]
(if (and v (var-macro? v))
(apply (var-get v) (tuple/slice the-form 1))
the-form))
the-form)))
(ns-intern core "macroexpand-1" expand-1)
# Apply a registered data reader to an already-read form (EDN built-in tags
# #uuid/#inst and any registered reader). Throws on an unknown tag.
(ns-intern core "__read-tagged"
(fn [tag form]
(def data-readers (get (ctx :env) :data-readers))
(def reader-fn (if data-readers (get data-readers tag)))
(if reader-fn
(reader-fn form)
(error (string "No reader function for tag " tag)))))
# macroexpand: expand repeatedly until the head is no longer a macro (the
# form's SUBFORMS are not expanded, matching Clojure).
(ns-intern core "macroexpand"
(fn [the-form]
(var cur the-form)
(var nxt (expand-1 cur))
(while (not= cur nxt) (set cur nxt) (set nxt (expand-1 cur)))
cur))
# alias bookkeeping is UNIFIED (jolt-ark): :aliases (alias-name string ->
# ns-name string) is the one store, read by resolution and ns-aliases;
# :imports holds class imports only.
(ns-intern core "alias"
(fn [alias-sym ns-sym]
(def cur (ctx-find-ns ctx (ctx-current-ns ctx)))
(ns-add-alias cur (alias-sym :name) (ns-sym :name))
nil))
(ns-intern core "ns-unalias"
(fn [ns-d alias-sym]
(def ns (ns-or-current ns-d))
(put (ns :aliases) (alias-sym :name) nil)
nil))
# ns-publics: {symbol -> var} (jolt has no private vars, so publics = interns).
# Keys are symbol structs (value-hashed), matching Clojure's symbol keys.
(def mappings->symbol-map (fn [ns pred]
(var m (make-phm))
(loop [[nm v] :pairs (ns :mappings)]
(when (pred nm v)
(set m (phm-assoc m {:jolt/type :symbol :ns nil :name nm} v))))
m))
(ns-intern core "ns-publics"
(fn [&opt ns-d]
(mappings->symbol-map (ns-or-current ns-d) (fn [nm v] true))))
# ns-map: all mappings (interns + refers; jolt has no class imports in maps).
(ns-intern core "ns-map"
(fn [&opt ns-d]
(mappings->symbol-map (ns-or-current ns-d) (fn [nm v] true))))
# ns-refers: mappings whose var's HOME ns differs from this ns (copied in by
# refer/use/require :refer).
(ns-intern core "ns-refers"
(fn [&opt ns-d]
(def ns (ns-or-current ns-d))
(def my-name (ns :name))
(mappings->symbol-map ns (fn [nm v]
(and (table? v) (not= (get v :ns) my-name))))))
(ns-intern core "ns-unmap"
(fn [ns-d sym]
(def ns (ns-or-current ns-d))
(put (ns :mappings) (if (struct? sym) (sym :name) (string sym)) nil)
nil))
core)
# Dispatch a special form by its string name.

748
src/jolt/eval_special.janet Normal file
View file

@ -0,0 +1,748 @@
# Jolt Evaluator — special forms (eval-list dispatch)
# Extracted from evaluator.janet (jolt-oudv, phase 2a split).
(use ./types)
(use ./phm)
(use ./phs)
(use ./lazyseq)
(use ./pv)
(use ./plist)
(use ./config)
(use ./reader)
(use ./regex)
(use ./eval_base)
(use ./eval_resolve)
(use ./eval_runtime)
(defn- unwrap-meta-name
"Recursively unwrap (with-meta sym meta) forms to extract the underlying symbol.
Returns the symbol struct, or the original form if it's not a with-meta wrapper."
[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))
# --- special-form handlers (exploded from eval-list, jolt-oudv) ---
(defn eval-do [ctx bindings form]
(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))
(defn eval-if [ctx bindings form]
(do
# 2 or 3 argument forms only (spec 03-special-forms X1)
(when (or (< (length form) 3) (> (length form) 4))
(error (string "Wrong number of args (" (dec (length form)) ") passed to: 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)))))
(defn eval-def [ctx bindings form]
(let [raw-name (in form 1)
name-sym (unwrap-meta-name raw-name)
# Metadata on the name: keyword/type-hint metadata rides on the
# symbol (:meta); a ^{:map} reads as a with-meta form we evaluate.
sym-meta (or (and (struct? name-sym) (get name-sym :meta)) {})
wm-meta (if (and (array? raw-name) (> (length raw-name) 0)
(sym-name? (first raw-name) "with-meta"))
(let [mv (protect (eval-form ctx bindings (last raw-name)))]
(if (and (mv 0) (or (table? (mv 1)) (struct? (mv 1)))) (mv 1) {}))
{})
name-meta (merge wm-meta sym-meta)
dynamic? (truthy? (get name-meta :dynamic))
ns-name (ctx-current-ns ctx)
ns (ctx-find-ns ctx ns-name)
# Create var first (unbound) so self-referencing defs resolve
v (ns-intern ns (name-sym :name))]
# (def name) with no init interns the var and leaves any existing
# root binding alone (Clojure semantics — this is what declare
# expands to, so compiled forward refs bind to the var instead of
# falling through to a like-named host builtin).
(if (= 2 (length form))
(do
(when (not (empty? name-meta))
(put v :meta (merge (or (get v :meta) {}) name-meta)))
(when dynamic? (put v :dynamic true))
v)
(let [# (def name docstring value): docstring form 2, value form 3
has-doc (and (> (length form) 3) (string? (in form 2)))
val-form (in form (if has-doc 3 2))
val (eval-form ctx bindings val-form)]
(bind-root v val)
# Staged bootstrap (jolt-4j3): pre/at-kernel overlay defns load
# interpreted; stash the fn source so backend/recompile-defns! can
# compile them once the analyzer is alive — the defn analog of
# :macro-src. Only set while api/load-core-overlay! loads the early
# tiers (the flag scopes it away from user code).
(when (and (get (ctx :env) :stash-defn-src?)
(function? val)
(array? val-form) (> (length val-form) 0)
(or (sym-name? (first val-form) "fn")
(sym-name? (first val-form) "fn*")))
(put v :defn-src val-form))
(let [extra (if has-doc (merge name-meta {:doc (in form 2)}) name-meta)]
(when (not (empty? extra))
(put v :meta (merge (or (get v :meta) {}) extra))))
(when dynamic?
(put v :dynamic true))
# def returns the var (Clojure semantics); REPL prints #'ns/name
v))))
(defn eval-defmacro [ctx bindings form]
(let [# ^{:map} metadata on the name reads as a (with-meta sym …)
# form (jolt-8w2); unwrap to the bare symbol like def does.
name-sym (unwrap-meta-name (in form 1))
after-name (tuple/slice form 2)
# Skip an optional leading docstring (string) then an optional
# attr-map (a struct that is not a symbol — a map literal reads
# as a struct), matching defn. Real macros use both, e.g.
# (defmacro info "doc" {:arglists '(...)} [& args] …).
a1 (if (and (> (length after-name) 0) (string? (first after-name)))
(tuple/slice after-name 1) after-name)
after-meta (if (and (> (length a1) 0)
(struct? (first a1))
(not= :symbol (get (first a1) :jolt/type)))
(tuple/slice a1 1) a1)
# What remains is either a params VECTOR (tuple) + body, or one
# or more arity CLAUSES (each a list, i.e. a janet array). Build
# a uniform arity list [{:params … :body …} …].
multi? (and (> (length after-meta) 0) (array? (first after-meta)))
arities (if multi?
(map (fn [cl] {:params (first cl) :body (tuple/slice cl 1)})
after-meta)
@[{:params (first after-meta) :body (tuple/slice after-meta 1)}])
defining-ns (ctx-current-ns ctx)]
(def interp-fn (fn [& macro-args]
(def n (length macro-args))
# Pick the arity: an exact fixed-count match wins; otherwise the
# first variadic arity that accepts n args (Clojure fn dispatch).
(var chosen nil)
(each ar arities
(def pi (parse-params (ar :params)))
(when (and (nil? chosen) (not (pi :rest)) (= n (length (pi :fixed))))
(set chosen [pi (ar :body)])))
(when (nil? chosen)
(each ar arities
(def pi (parse-params (ar :params)))
(when (and (nil? chosen) (pi :rest) (>= n (length (pi :fixed))))
(set chosen [pi (ar :body)]))))
(when (nil? chosen)
(error (string "no matching arity for macro " (name-sym :name)
" (" n " args)")))
(def pi (chosen 0))
(def body (chosen 1))
(var new-bindings @{})
(table/setproto new-bindings bindings)
(put new-bindings "&env" @{}) # implicit &env for macro bodies (table — nil-safe)
(var i 0)
# Destructure macro params (like fn), so [& [a & more :as all]]
# and {:keys …} rest forms work in macro arglists.
(each pat (pi :fixed)
(destructure-bind ctx new-bindings pat (macro-args i))
(++ i))
(when (pi :rest)
(destructure-bind ctx new-bindings (pi :rest) (rest-args-val macro-args i)))
# Use defining namespace for symbol resolution
(def saved-ns (ctx-current-ns ctx))
(ctx-set-current-ns ctx defining-ns)
# Plain trailing restore (NOT defer/try — those build a fiber per
# call and blow the C stack on deep interpreted recursion). An
# unwinding throw is repaired once at the TOP-LEVEL boundary
# (loader/eval-toplevel restores the ns on error).
(var result nil)
(each bf body
(set result (eval-form ctx new-bindings bf)))
(ctx-set-current-ns ctx saved-ns)
result))
# A COMPILED expander (native-speed) is only built for the
# single-arity case (the compile hook + recompile path take one
# [args body]); multi-arity macros use the interpreted expander.
(def single? (= 1 (length arities)))
(def args-form (and single? ((first arities) :params)))
(def body (and single? ((first arities) :body)))
(def uses-env (do (var u false)
(each ar arities
(when (or (form-uses-sym? (ar :body) "&env")
(form-uses-sym? (ar :body) "&form"))
(set u true)))
u))
(def compiled-fn
(when (and macro-compile-hook single? (not uses-env))
(macro-compile-hook ctx args-form body)))
(def macro-fn (or compiled-fn interp-fn))
(let [ns-name (ctx-current-ns ctx)
ns (ctx-find-ns ctx ns-name)]
(def v (ns-intern ns (name-sym :name) macro-fn))
(put v :macro true)
# Stash the expander source so backend/recompile-macros! can
# compile it once the analyzer is alive (staged bootstrap): a
# macro defined WHILE the analyzer is still being built gets an
# interpreted closure now, a compiled expander later. uses-env
# macros stay interpreted (the compiled fn* has no &env/&form);
# multi-arity macros keep the interpreted dispatch (no single
# [args body] to recompile).
(when single? (put v :macro-src @[args-form body]))
(put v :macro-uses-env uses-env)
(when compiled-fn (put v :macro-compiled true))
# A (re)defined macro invalidates any cached expansions.
(table/clear macro-cache)
(var-get v))))
(defn eval-fn* [ctx bindings form]
(let [# optional name: (fn* name [args] ...) / (fn* name ([args] ...)...)
named? (and (struct? (in form 1)) (= :symbol ((in form 1) :jolt/type)))
fn-name (if named? ((in form 1) :name) nil)
form (if named? (array/concat @[(in form 0)] (tuple/slice form 2)) form)]
(if (array? (in form 1))
# Multi-arity: (fn* ([args] body...) ([args] body...)...)
(let [pairs (tuple/slice form 1)
arities @{}
defining-ns (ctx-current-ns ctx)]
(var self nil)
# The (single) variadic clause is dispatched separately: it handles
# any arg count >= its fixed count. Storing it in `arities` by
# fixed-count would collide with a same-fixed-count fixed clause and
# only match that exact count.
(var variadic-fn nil)
(var variadic-min 0)
(each pair pairs
(let [args-form (in pair 0)
body (tuple/slice pair 1)
param-info (parse-params args-form)
_ (require-symbol-params param-info)
fixed-pats (param-info :fixed)
rest-pat (param-info :rest)
n-fixed (length fixed-pats)
# recur-entry: where (recur ...) re-enters THIS arity. For
# a fixed arity it's the dispatcher (exact count re-selects
# it). For the VARIADIC arity, recur takes n-fixed + 1 args
# with the LAST bound DIRECTLY as the rest seq (Clojure) —
# re-entering through the varargs collector would wrap it
# in a fresh 1-element rest list and the seq never empties
# (the jolt-4df hang).
recur-entry-box @[nil]
run-clause (fn [fn-bindings]
(put fn-bindings :jolt/loop-fn (in recur-entry-box 0))
(when fn-name (bind-put fn-bindings fn-name self))
# Use defining namespace for symbol resolution
(def saved-ns (ctx-current-ns ctx))
(ctx-set-current-ns ctx defining-ns)
# Plain trailing restore (NOT defer/try — those build a fiber per
# call and blow the C stack on deep interpreted recursion). An
# unwinding throw is repaired once at the TOP-LEVEL boundary
# (loader/eval-toplevel restores the ns on error).
(var result nil)
(each body-form body
(set result (eval-form ctx fn-bindings body-form)))
(ctx-set-current-ns ctx saved-ns)
result)
f (fn [& fn-args]
(var fn-bindings @{})
(table/setproto fn-bindings bindings)
(var i 0)
(each pat fixed-pats
(destructure-bind ctx fn-bindings pat (fn-args i))
(++ i))
(when rest-pat
(destructure-bind ctx fn-bindings rest-pat (rest-args-val fn-args i)))
(run-clause fn-bindings))]
(if rest-pat
(do
(put recur-entry-box 0
(fn [& recur-args]
(var fn-bindings @{})
(table/setproto fn-bindings bindings)
(var i 0)
(each pat fixed-pats
(destructure-bind ctx fn-bindings pat (recur-args i))
(++ i))
(destructure-bind ctx fn-bindings rest-pat (get recur-args i))
(run-clause fn-bindings)))
(set variadic-fn f) (set variadic-min n-fixed))
(do
(put recur-entry-box 0 (fn [& recur-args] (apply self recur-args)))
(put arities n-fixed f)))))
(set self (fn [& fn-args]
(let [n (length fn-args)
f (get arities n)]
(cond
f (apply f fn-args)
(and variadic-fn (>= n variadic-min)) (apply variadic-fn fn-args)
(error (string "Wrong number of args (" n ") passed to: "
(or fn-name "fn")))))))
self)
# Single-arity: (fn* [args] body...)
(let [args-form (in form 1)
body (tuple/slice form 2)
param-info (parse-params args-form)
_ (require-symbol-params param-info)
fixed-pats (param-info :fixed)
rest-pat (param-info :rest)
defining-ns (ctx-current-ns ctx)]
(var self nil)
(var recur-entry nil)
(def run-body (fn [fn-bindings]
(put fn-bindings :jolt/loop-fn recur-entry)
(when fn-name (bind-put fn-bindings fn-name self))
# Use defining namespace for symbol resolution
(def saved-ns (ctx-current-ns ctx))
(ctx-set-current-ns ctx defining-ns)
# Plain trailing restore (NOT defer/try — those build a fiber per
# call and blow the C stack on deep interpreted recursion). An
# unwinding throw is repaired once at the TOP-LEVEL boundary
# (loader/eval-toplevel restores the ns on error).
(var result nil)
(each body-form body
(set result (eval-form ctx fn-bindings body-form)))
(ctx-set-current-ns ctx saved-ns)
result))
(def n-fixed (length fixed-pats))
(set self (fn [& fn-args]
# ArityException semantics (jolt-6xn): a fixed arity takes
# exactly its params, a variadic one at least its fixed params.
# The compiled path enforces this natively (janet fn arity);
# this keeps the interpreter oracle in agreement.
(def n (length fn-args))
(when (if rest-pat (< n n-fixed) (not= n n-fixed))
(error (string "Wrong number of args (" n ") passed to: "
(or fn-name "fn"))))
(var fn-bindings @{})
(table/setproto fn-bindings bindings)
(var i 0)
(each pat fixed-pats
(destructure-bind ctx fn-bindings pat (fn-args i))
(++ i))
(when rest-pat
(destructure-bind ctx fn-bindings rest-pat (rest-args-val fn-args i)))
(run-body fn-bindings)))
# recur re-enters here: for a variadic fn it takes n-fixed + 1
# args, the LAST bound DIRECTLY as the rest seq (Clojure) — going
# back through the varargs collector wrapped the seq in a fresh
# 1-element rest list, so it never emptied (the jolt-4df hang).
(set recur-entry
(if rest-pat
(fn [& recur-args]
(var fn-bindings @{})
(table/setproto fn-bindings bindings)
(var i 0)
(each pat fixed-pats
(destructure-bind ctx fn-bindings pat (recur-args i))
(++ i))
(destructure-bind ctx fn-bindings rest-pat (get recur-args i))
(run-body fn-bindings))
self))
self))))
(defn eval-let* [ctx bindings form]
(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 [pat (bind-vec i)]
# let* is a primitive (the let macro desugars destructuring);
# its binding names must be plain symbols, as in Clojure.
(unless (plain-sym? pat) (error "Bad binding form, expected symbol"))
(def val (eval-form ctx new-bindings (bind-vec (+ i 1))))
(destructure-bind ctx new-bindings pat val)
(+= i 2))))
(var result nil)
(each body-form body
(set result (eval-form ctx new-bindings body-form)))
result))
(defn eval-loop* [ctx bindings form]
(let [bind-vec (in form 1)
body (tuple/slice form 2)
init-vals @[]
patterns @[]
# Inits are evaluated sequentially in an accumulating scope (like
# let*), so a later init can reference an earlier binding —
# matching Clojure's loop.
seq-bindings @{}]
(table/setproto seq-bindings bindings)
(var i 0)
(while (< i (length bind-vec))
# loop* is a primitive (the loop macro desugars destructuring);
# its binding names must be plain symbols, as in Clojure.
(unless (plain-sym? (bind-vec i)) (error "Bad binding form, expected symbol"))
(def v (eval-form ctx seq-bindings (bind-vec (+ i 1))))
(bind-put seq-bindings ((bind-vec i) :name) v)
(array/push init-vals v)
(array/push patterns (bind-vec i))
(+= i 2))
(var loop-fn nil)
(set loop-fn (fn [& args]
(var loop-bindings @{})
(table/setproto loop-bindings bindings)
(var j 0)
(each pat patterns
(destructure-bind ctx loop-bindings pat (in 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)))
(defn eval-recur [ctx bindings form]
(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)))))
(defn eval-try [ctx bindings form]
(let [# The body is EVERY form between `try` and the first catch/finally
# clause (not just form 1 — a multi-form body before the clauses,
# e.g. (try (foo) (bar) (catch …)), dropped all but the first).
forms (tuple/slice form 1)
clause? (fn [c]
(and (array? c) (> (length c) 0)
(struct? (first c)) (= :symbol ((first c) :jolt/type))
(or (= "catch" ((first c) :name))
(= "finally" ((first c) :name)))))
split (do (var k 0)
(while (and (< k (length forms)) (not (clause? (in forms k)))) (++ k))
k)
body-forms (tuple/slice forms 0 split)
clauses (tuple/slice forms split)
# current-ns is dynamic state. The interpreter rebinds it to a
# fn's defining ns while that fn runs and restores it on normal
# return, but a fn that THROWS unwinds past its own restore — so
# the ns can leak. try is the unwind boundary: restore the ns that
# was current at try entry before running catch/finally, so caught
# code (and the harness's is/thrown?) sees the right namespace.
try-ns (ctx-current-ns ctx)]
(var catch-sym nil)
(var catch-body nil)
(var finally-body nil)
(each clause clauses
(when (and (array? clause) (> (length clause) 0))
(let [head (first clause)]
(when (and (struct? head) (= :symbol (head :jolt/type)))
(match (head :name)
"catch" (do
(set catch-sym (in clause 2))
(set catch-body (tuple/slice clause 3)))
"finally" (set finally-body (tuple/slice clause 1)))))))
(defn eval-body []
(var result nil)
(each bf body-forms (set result (eval-form ctx bindings bf)))
result)
(defn run-finally []
(when finally-body
(each fb finally-body (eval-form ctx bindings fb))))
(defn run-protected []
(if catch-sym
(try
(eval-body)
([err]
(ctx-set-current-ns ctx try-ns)
(var new-bindings @{})
(table/setproto new-bindings bindings)
# bind the originally-thrown value (unwrap the :jolt/exception
# envelope) so (catch … e (throw e)) rethrows the same value
# rather than nesting another envelope
(def caught
(if (and (or (table? err) (struct? err)) (= :jolt/exception (get err :jolt/type)))
(get err :value)
err))
(put new-bindings (catch-sym :name) caught)
(var result nil)
(each cb catch-body
(set result (eval-form ctx new-bindings cb)))
result))
# no catch: restore the ns on an unwinding error, then re-raise
(try (eval-body) ([err] (ctx-set-current-ns ctx try-ns) (error err)))))
# finally ALWAYS runs (success, caught error, or rethrow) — defer so it
# fires even if a catch body throws. Without a finally, just run.
(if finally-body
(defer (run-finally) (run-protected))
(run-protected))))
(defn eval-set! [ctx bindings form]
(let [target (in form 1)
val (eval-form ctx bindings (in form 2))]
# Handle (set! (.-field obj) val) — .-field shorthand as a list
(if (and (array? target) (> (length target) 1)
(struct? (first target)) (= :symbol ((first target) :jolt/type))
(> (length ((first target) :name)) 1)
(= (string/slice ((first target) :name) 0 2) ".-"))
(let [obj (eval-form ctx bindings (in target 1))
field-name (string/slice ((first target) :name) 2)
field-key (keyword field-name)]
(if (get obj :jolt/deftype)
(do (put obj field-key val) val)
(error (string "Can't set! field on non-deftype: " (type obj)))))
# (set! (. obj -field) val) — instance field mutation
(if (and (array? target) (> (length target) 0)
(struct? (first target))
(= :symbol ((first target) :jolt/type))
(= "." ((first target) :name)))
(let [obj (eval-form ctx bindings (in target 1))
field-sym (in target 2)
field-name (field-sym :name)
field-key (keyword (if (and (> (length field-name) 0) (= "-" (string/slice field-name 0 1)))
(string/slice field-name 1)
field-name))]
(if (get obj :jolt/deftype)
(do (put obj field-key val) val)
(error (string "Can't set! field on non-deftype: " (type obj)))))
# (set! var val) — normal var mutation
(let [target-sym target
v (resolve-var ctx bindings target-sym)]
(if v
(do (var-set v val) val)
# Auto-create var if it doesn't exist
(let [ns-name (ctx-current-ns ctx)
ns (ctx-find-ns ctx ns-name)]
(def new-v (ns-intern ns (target-sym :name) val))
val)))))))
(defn eval-new [ctx bindings form]
(let [type-sym (in form 1)
args (map |(eval-form ctx bindings $) (tuple/slice form 2))
ctor (eval-form ctx bindings type-sym)
ctor (if (string? ctor) (or (ctor-for-class-token ctor) ctor) ctor)]
(apply ctor args)))
(defn eval-dot [ctx bindings form]
(let [target (eval-form ctx bindings (in form 1))
member-raw (in form 2)
# Resolve member name: symbols have :name, keywords use string, strings as-is
member-name (if (and (struct? member-raw) (= :symbol (member-raw :jolt/type)))
(member-raw :name)
(if (keyword? member-raw)
(string member-raw)
member-raw))
field-name (if (and (string? member-name) (> (length member-name) 0) (= "-" (string/slice member-name 0 1)))
(string/slice member-name 1)
member-name)]
(if (> (length form) 3)
# method call: (. obj method args...)
(let [args (map |(eval-form ctx bindings $) (tuple/slice form 3))]
(if (or (string? target) (buffer? target))
(let [m (get string-methods field-name)]
(if m
(m (string target) ;args)
(if-let [om (get object-methods field-name)]
(om (string target) ;args)
(error (string "Unsupported String method ." field-name)))))
(if (and (number? target) (get number-methods field-name))
((get number-methods field-name) target ;args)
(if (and (get object-methods field-name)
(not (and (table? target) (get tagged-methods (get target :jolt/type)))))
((get object-methods field-name) target ;args)
# registered shim objects (java.time etc.): tag-keyed method tables
(if (and (or (table? target) (struct? target))
(get tagged-methods (get target :jolt/type)))
(let [m (get (get tagged-methods (get target :jolt/type)) field-name)]
(if m
(m target ;args)
(error (string "Unsupported method ." field-name " on " (string (get target :jolt/type))))))
(if (record-tag target)
# deftype/reify methods live in the protocol registry (or the
# instance's reified-fns table), not on the instance. get is safe
# on a shape-rec tuple (returns nil for the method/protocol keys).
(let [method-key (keyword field-name)
own (get target method-key)
reified (get (get target :jolt/protocol-methods) method-key)
m (cond
(or (function? own) (cfunction? own)) own
(or (function? reified) (cfunction? reified)) reified
(find-method-any-protocol ctx (record-tag target) field-name))]
(if m
(apply m target args)
(error (string "No method ." field-name " on " (record-tag target)))))
# Janet-native interop: try field lookup + call
(if (or (table? target) (struct? target))
(let [method (get target (keyword field-name))]
(if (or (function? method) (cfunction? method))
(method target ;args)
# If stored as fn* form (array), compile to function then call
(if (array? method)
(let [method-fn (eval-form ctx bindings method)]
(if (or (function? method-fn) (cfunction? method-fn))
(method-fn target ;args)
(error (string "Cannot call non-function " field-name " on " (type target)))))
(let [r (if coll-interop (coll-interop target field-name args) :jolt/ci-none)]
(if (= r :jolt/ci-none)
(error (string "Cannot call non-function " field-name " on " (type target)))
r)))))
(error (string "Cannot call method " field-name " on " (type target))))))))))
# (. obj member) with no extra args: a symbol member naming a
# function is a zero-arg method call (receiver passed as self);
# a keyword or `-field` member is plain field access. Strings get
# the java.lang.String surface (clj-compat: (.toLowerCase s) ...).
(if (or (string? target) (buffer? target))
(let [m (get string-methods field-name)]
(if m
(m (string target))
(if-let [om (get object-methods field-name)]
(om (string target))
(error (string "Unsupported String method ." field-name)))))
(if (and (number? target) (get number-methods field-name))
((get number-methods field-name) target)
(if (and (get object-methods field-name)
(not (and (table? target) (get tagged-methods (get target :jolt/type))
(get (get tagged-methods (get target :jolt/type)) field-name))))
((get object-methods field-name) target)
(if (and (or (table? target) (struct? target))
(get tagged-methods (get target :jolt/type))
(get (get tagged-methods (get target :jolt/type)) field-name))
((get (get tagged-methods (get target :jolt/type)) field-name) target)
# zero-arg Java collection interop (.count/.seq/… on a jolt collection)
# before field lookup — coll-interop returns :jolt/ci-none if not its kind
(let [ci (if coll-interop (coll-interop target field-name @[]) :jolt/ci-none)]
(if (not= ci :jolt/ci-none) ci
(let [v (if (record-tag target)
(coll-lookup target (keyword field-name) nil)
(get target (keyword field-name)))]
(if (and (struct? member-raw) (= :symbol (member-raw :jolt/type))
(not (string/has-prefix? "-" member-name)))
(cond
(or (function? v) (cfunction? v)) (v target)
# zero-arg deftype/reify method via the protocol registry
(record-tag target)
(let [reified (get (get target :jolt/protocol-methods) (keyword field-name))
m (if (or (function? reified) (cfunction? reified)) reified
(find-method-any-protocol ctx (record-tag target) field-name))]
(if m (m target) v))
# value stored as an unevaluated fn* form: compile then call
(array? v) (let [f (eval-form ctx bindings v)]
(if (or (function? f) (cfunction? f)) (f target) f))
v)
v)))))))))))
(defn eval-list
[ctx bindings form]
(def first-form (first form))
# Safe name extraction: non-symbol heads (e.g. keywords) fall through to default.
# A head qualified to a NON-core namespace (e.g. clojure.edn/read-string) must
# resolve to that var, not the like-named clojure.core special form — so only
# unqualified or clojure.core-qualified heads dispatch as special forms.
(def name (if (and (struct? first-form) (= :symbol (first-form :jolt/type)))
(let [ns (first-form :ns)]
(if (or (nil? ns) (= ns "clojure.core")) (first-form :name) nil))
nil))
(match name
"quote" (in form 1)
# Interpreter builds the form directly (self-contained, no core dependency).
# The COMPILE path instead lowers syntax-quote to construction code (via
# syntax-quote-lower) so a backtick body is compilable; the two are kept in
# sync and cross-checked by conformance (interpret vs compile modes).
"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")
"eval" (eval-form ctx bindings (eval-form ctx bindings (in form 1)))
# read-string/macroexpand-1 are ctx-capturing clojure.core fns and defonce
# an overlay macro now (Stage 2 tier 6c) — no special-form arms.
"do" (eval-do ctx bindings form)
"if" (eval-if ctx bindings form)
"def" (eval-def ctx bindings form)
"defmacro" (eval-defmacro ctx bindings form)
# ns is now a macro (clojure.core, 30-macros) expanding to in-ns + require/use/
# import/refer-clojure calls — all ctx-capturing fns — so it compiles. No
# special-form arm; an (ns ...) head falls through to the macro-expansion path.
# require / in-ns are now ordinary clojure.core fns (install-stateful-fns!) —
# no special-form arm; they compile + interpret as plain invokes.
# all-ns/the-ns/create-ns/remove-ns/ns-interns/ns-aliases/ns-imports/
# ns-resolve/resolve/find-ns/refer are ctx-capturing clojure.core fns now
# (install-stateful-fns!) with evaluated-arg Clojure semantics — they fall
# through to the function-call default and compile as plain invokes
# (Stage 2 tier 6b).
"fn*" (eval-fn* ctx bindings form)
"let*" (eval-let* ctx bindings form)
"loop*" (eval-loop* ctx bindings form)
"recur" (eval-recur ctx bindings form)
"throw" (let [val (eval-form ctx bindings (in form 1))]
(error {:jolt/type :jolt/exception :value val}))
"try" (eval-try ctx bindings form)
"set!" (eval-set! ctx bindings form)
"var" (let [target-sym (in form 1)
v (resolve-var ctx bindings target-sym)]
(if v v (error (string "Unable to resolve var: " (sym-name-str target-sym) " in var"))))
# var-get/var-set/var?/alter-var-root/alter-meta!/reset-meta! are plain
# clojure.core fns; find-var/intern are ctx-capturing clojure.core fns
# (install-stateful-fns!) — they fall through to the function-call default
# and compile as ordinary invokes (Stage 2 tier 6).
# set?/disj are plain clojure.core fns now (core-set?/core-disj) — no longer
# special-cased here, the analyzer, or compiler.janet (jolt-g3h).
# protocol-dispatch / register-method / make-reified are now ordinary
# clojure.core fns (install-stateful-fns!) — the defprotocol/extend-type/reify
# macros call them with name STRINGS, so they compile + interpret as plain
# invokes (no special-form arms).
# satisfies?/instance?/locking and the multimethod table ops
# (prefer-method/remove-method/remove-all-methods/get-method/methods) are
# clojure.core fns / overlay macros now (Stage 2 tier 6c) — no special arms.
# deftype is now a macro (30-macros) over make-deftype-ctor + extend-type —
# compiles as a plain (do …); no special-form arm.
"new" (eval-new ctx bindings form)
"." (eval-dot ctx bindings form)
# default: function application — check for macros
(if (and (struct? first-form) (= :symbol (first-form :jolt/type)))
(let [sym-name (first-form :name)]
# Handle .-fieldName accessor: (.-cnt obj) → (. obj -cnt)
(if (and (> (length sym-name) 1) (= (string/slice sym-name 0 2) ".-")
(> (length form) 1))
(let [field-name (string/slice sym-name 2)
target (eval-form ctx bindings (in form 1))]
(get target (keyword field-name)))
# (.method obj args...) sugar -> (. obj method args...): desugar and
# re-enter the dot special form (which holds the String surface, the
# deftype method path, and the map-fn fallback).
(if (and (> (length sym-name) 1)
(= (string/slice sym-name 0 1) ".")
(not= sym-name "..")
(> (length form) 1))
(eval-form ctx bindings
(array/concat @[{:jolt/type :symbol :ns nil :name "."}
(in form 1)
{:jolt/type :symbol :ns nil :name (string/slice sym-name 1)}]
(tuple/slice form 2)))
# Handle ClassName. constructor syntax (".." is the member-threading
# macro, not a constructor named ".")
(if (and (> (length sym-name) 1) (not= sym-name "..")
(= (sym-name (- (length sym-name) 1)) 46))
(let [type-name (string/slice sym-name 0 (- (length sym-name) 1))
type-sym {:jolt/type :symbol :ns (first-form :ns) :name type-name}
ctor (eval-form ctx bindings type-sym)
# class names evaluate to canonical-name STRINGS now; the
# constructor itself comes from the ctor registry
ctor (if (string? ctor) (or (ctor-for-class-token ctor) ctor) ctor)
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(apply ctor args))
(let [v (resolve-var ctx bindings first-form)]
(if (and v (var-macro? v))
# Expand once (cached by call-form identity), then evaluate the
# macro-free expansion with the current bindings each call.
(let [cached (in macro-cache form)]
(if (not (nil? cached))
(eval-form ctx bindings cached)
(let [expanded (apply (var-get v) (tuple/slice form 1))]
(put macro-cache form expanded)
(eval-form ctx bindings expanded))))
(let [f (eval-form ctx bindings first-form)
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(jolt-invoke ctx f args))))))))
(let [f (eval-form ctx bindings first-form)
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
(jolt-invoke ctx f args)))))
# Build a map value from an array of evaluated [k v k v ...]. A phm (not a Janet
# struct) is used when a key is a collection (value-based hashing) OR a key/value
# is nil (Janet structs drop nil; phm preserves it, matching Clojure). The common
# scalar/nil-free case stays a struct.

File diff suppressed because it is too large Load diff