Commit graph

288 commits

Author SHA1 Message Date
Yogthos
c975f5d1c3 perf: generation-guarded cache for multimethod hierarchy dispatch
The expensive multimethod path is the hierarchy fallback: when a dispatch value
isn't a direct method key, mm-fn walks every key with isa? (derive-based
dispatch). That resolution is now cached per dispatch value on the multimethod
var (:jolt/dispatch-cache), cleared in place by defmethod/prefer-method/
remove-method/remove-all-methods so a redefinition can't be served stale. Direct
(get methods dv) hits stay uncached — already a single lookup.

Test cases added to dispatch-cache-test (compile, interpret, aot-core off):
hierarchy hit is cached, a newly-added specific method is seen, removal re-exposes
the fallback. Conformance 218/218; full suite green.
2026-06-06 18:36:11 -04:00
Yogthos
e02ccab4c0 test: bootstrap fixpoint (stage1 == stage2 == stage3 behavioral parity)
Soundness gate for self-hosting (jolt-d0r). stage1 = analyzer built by the Janet
bootstrap; stage2 = analyzer rebuilt by compiling jolt.ir + jolt.analyzer through
stage1 (self-host); stage3 = the same self-rebuild again. A corpus of programs
must produce identical results across all three.

Compared BEHAVIORALLY rather than by emitted code: emitted Janet forms embed live
setter/getter closures (identity varies per compile) and the IR carries
representation-level gensyms and pvec internals, so structural equality fights the
representation. Behavioral parity is the property that actually matters and is
representation-independent.

Also corrects the battery baseline 3920 -> 3919: 3920 was a lucky run; 3919 is the
stable value (the 9 timeout-prone tests can shift the count by one).
2026-06-06 18:27:14 -04:00
Yogthos
3638521e2d core: move juxt/every-pred/some-fn to Clojure
Three pure function combinators into the collection tier, as compositions of
mapv/every?/some/apply. Three more core-* primitives + table entries gone.
Battery 3916 -> 3920 (the canonical defs are more correct than the prior Janet
ones — some-fn returns the matching value, every-pred is properly boolean).
Conformance 218/218 all modes.
2026-06-06 18:09:42 -04:00
Yogthos
57b8ffcb9b core: move frequencies/group-by/not-empty/filterv/max-key/min-key to Clojure
New collection tier (core/20-coll.clj): six pure, eager fns expressed as
compositions of frozen primitives (reduce/assoc/get/conj/filter/vec). Six more
core-* primitives + table entries gone from the Janet kernel.

Two semantics to preserve, caught by the suite:
- max-key/min-key use the canonical multi-arity form (strict </> on the first
  pair, <=/>= in the fold) to reproduce the JVM IEEE-754 NaN behavior; a single
  reduce got the NaN cases wrong.
- frequencies/group-by base on (hash-map), not the {} literal: a struct map
  doesn't canonicalize collection keys across representations ({:a 1} literal vs
  (hash-map :a 1)), a PHM does — same reason the Janet impl used make-phm.

Conformance 218/218 all modes; battery holds 3916.
2026-06-06 18:02:34 -04:00
Yogthos
279c8e176b perf: generation-guarded inline cache for host-value protocol dispatch
Host-value protocol dispatch (a protocol extended to Number/String/… called on a
host value) recomputed the candidate type-tag list and walked the registry — up
to ~15 map lookups plus an array alloc — on every call. It now caches
(most-specific-host-tag, protocol, method) -> fn, guarded by a registry
generation that bumps in register-protocol-method; re-extending a protocol
invalidates the cache. The single-lookup fast paths (deftype protocol dispatch,
direct multimethod dispatch) are untouched — they're already cheap, so no cache
overhead there. The multimethod hierarchy-fallback cache (narrower: only
derive-based dispatch) is deferred.

Test in test/integration/dispatch-cache-test.janet covers correctness under
re-extension in all three modes. Conformance 218/218; battery 3916.
2026-06-06 17:38:44 -04:00
Yogthos
db08cecde8 perf: var-deref fast path; drop the memoized-getter indirection
var-get short-circuits to the root when no dynamic bindings are active (the
common case — the binding stack is only non-empty inside a `binding` block),
skipping the stack walk on every global deref.

The compiled indirect deref now emits (var-get (quote cell)) instead of routing
through a per-var memoized getter closure — one fewer call and no per-var closure
alloc. The cell is quoted so it embeds by reference: a bare table in argument
position is re-evaluated as a constructor, which deep-copies the cell (and any
atom in its root) on every call — that silently broke swap!/atoms/self-ref
lazy-seqs until the quote. Conformance 218/218 all three modes; battery 3916.
2026-06-06 17:31:16 -04:00
Yogthos
a3cc035782 compiler: direct-linking (call-site/unit, Clojure model) + :aot-core? flag
Calls compile direct (target value embedded) iff the compiling unit has
direct-linking on, the target isn't ^:redef/^:dynamic, and it's an
already-defined fn; otherwise indirect (live var deref, redefinable). Direct
emission is guarded to function values only — embedding a non-function would make
Janet evaluate it as code.

:aot-core? (init opt, default true; JOLT_AOT_CORE=0 disables) compiles the core
tiers with direct-linking on so core->core calls are direct. User/REPL code
compiles indirect by default, so redefining functions in the REPL works with no
annotation, exactly like Clojure. :aot-core? false makes core indirect too — the
whole language becomes redefinable. ^:redef / ^:dynamic on a target force it
indirect even inside a direct-linked unit.

Also fixes a pre-existing gap this surfaced: compiled def dropped ALL var
metadata (so ^:dynamic was silently lost under compilation, and ^:redef couldn't
work). def metadata now flows analyzer -> IR -> back end (new form-sym-meta host
fn, def-node :meta, var-setter-meta) and is applied to the cell.

Matrix test in test/integration/direct-linking-test.janet. Conformance 218/218
in all three modes under both :aot-core? true and false; battery holds 3916;
binary rebuilt.
2026-06-06 17:13:54 -04:00
Yogthos
a2805c4f51 vars: add per-var generation counter, bumped on root change
Foundation for the linking-mode work (jolt-5ed). Every var cell carries a :gen
that bumps on bind-root / var-set(root) / alter-var-root — i.e. on redefinition.
Behavior-neutral on its own; it's the substrate the upcoming work needs:
direct-linked (sealed) call sites can detect staleness, and redefinition-aware
dispatch caches can invalidate per-var (only a redefined var's callers miss,
not a global flush). Full suite green, conformance 218/218 in all three modes.
2026-06-06 16:47:44 -04:00
Yogthos
28ef15ea4b core: staged-bootstrap kernel tier; move second/peek/subvec/mapv/update to Clojure
First fractal turn of the multi-stage bootstrap (jolt-tzo). The Clojure part of
clojure.core is now loaded in ordered tiers under jolt-core/clojure/core/. The
kernel tier (00-kernel: second/peek/subvec/mapv/update) holds the structural fns
the self-hosted compiler itself uses; in compile mode it is bootstrap-compiled
into clojure.core BEFORE the analyzer is built, so the analyzer binds those names
to the Clojure definitions instead of a not-yet-defined forward ref. That removes
the circularity that previously forced these to stay in Janet — the five core-*
primitives and their init-core! entries are gone.

Mechanism: backend/bootstrap-load-source (generalized from compile-load) builds a
source string into a target ns via the bootstrap; rebuild-compiler! recompiles
the self-hosted compiler against the current core (the rail for future turns,
exercised by the new fixpoint test). api/load-core-overlay! walks the ordered
tiers, bootstrap-loading kernel tiers and self-hosting the rest.

Also fixes a latent evaluator bug surfaced by the move: a fn rebinds current-ns
to its defining ns while it runs and restores on normal return, but a fn that
THREW unwound past its own restore, leaking the ns. try now restores the
try-entry ns on the catch/finally path, so referred-symbol resolution survives a
caught error (this was breaking is/testing in the suite harness after any thrown
assertion). Net battery +3 (3913 -> 3916); conformance 218/218 in all three
modes; binary builds and runs the embedded tiers.
2026-06-06 13:15:17 -04:00
Yogthos
a6f0f8a6fe core: move last/butlast to the Clojure overlay
Continue the kernel shrink. last/butlast become canonical Clojure defs
(pure first/next/loop/recur) in the core.clj overlay, dropping two
realize-for-iteration uses from the Janet kernel.

second was the obvious next candidate but can't move: the self-hosted
compiler front-end (analyzer.clj) calls it, and the compiler has to
compile the overlay, so anything it uses must already exist as a Janet
primitive. Documented that as a third clause of the safe-to-move rule.

Suite green, clojure-test-suite battery holds 3913 pass / 58 clean,
binary builds and runs the embedded overlay.
2026-06-06 12:08:00 -04:00
Yogthos
c8810ee4aa core: move nfirst to the Clojure overlay
Same leaf-fn migration as ffirst/fnext/nnext. Full suite green, battery 3913.
2026-06-06 11:49:09 -04:00
Yogthos
30a308f3ef core: start moving clojure.core to Clojure (overlay loaded at init)
Phase 4 kernel-shrink seam. jolt-core/clojure/core.clj holds the Clojure portion
of clojure.core; api/init loads it into the clojure.core namespace after
init-core! interns the Janet primitives, routed through eval-toplevel so it
compiles via the self-hosted pipeline (or interprets when :compile? is off).

First three fns moved off the Janet side: ffirst, fnext, nnext (leaf, no internal
callers). Same results compiled or interpreted; full suite green; battery holds
3913.
2026-06-06 11:45:31 -04:00
Yogthos
984af02532 loader: drop write-only compiled-form cache and its unused helpers
compiled?/get-compiled-forms/clear-compiled-cache were never called, and
load-ns only ever wrote to :compiled-cache with no reader. All dead once the
self-host path landed. Removed the helpers, the cache writes, and the
:compiled-cache env field.
2026-06-06 11:40:27 -04:00
Yogthos
497970029d self-host: make it the only compile path; drop the bootstrap as a selectable path
eval-toplevel now routes every compiled form through backend/compile-and-eval
(analyzer -> IR -> Janet back end). The bootstrap compiler (compile-ast) stays
only to bootstrap jolt.ir/jolt.analyzer in backend/compile-load. Dropped the
:selfhost? flag and JOLT_SELFHOST env plumbing — self-host is the default.

Three correctness fixes the full-suite-under-self-host sweep exposed:
- analyzer/back end: recur into a variadic arity now compiles. The arity fn
  gets an ordinary positional rest param (the collected seq), with a dispatch
  wrapper, so (recur fixed... rest-seq) is a clean self-call like Clojure --
  instead of punting to the interpreter, which hangs on this (jolt-4df).
- host_iface: interop heads (.foo/.-field/Foo.) and ns-management forms
  (all-ns, create-ns, resolve, ...) now mark uncompilable so they fall back
  instead of compiling to a bad call.
- back end: named-fn self-reference ((fn self [n] ... (self ...))) binds the
  fn name via a var.

Full unit+integration suite green; clojure-test-suite battery holds 3913.
2026-06-06 11:34:02 -04:00
Yogthos
34b880f0d6 conformance: guard the self-hosted pipeline too (218/218 in all three modes)
conformance-test now runs every case under interpret, the bootstrap compiler, AND
the self-hosted pipeline (portable Clojure analyzer -> IR -> Janet back end). All
three pass 218/218, so the self-hosted compiler can't silently regress in CI.
2026-06-06 10:29:36 -04:00
Yogthos
3afd1c6142 self-host: analyzer compiles to fast bytecode (fib(30) 0.52s)
The self-hosted compiler now genuinely compiles to native Janet bytecode rather
than falling back to the interpreter:
- bootstrap fixes that let it compile the analyzer: (def x) with no init, and an
  empty (do) (declare expands to one) no longer error;
- analyzer reordered so only the mutually-recursive  is forward declared
  (a deeply-nested forward ref to analyze-fn/analyze-try miscompiled to nil);
- back end emits native Janet ops for hot primitives (+,-,*,<,>,<=,>=,inc,dec)
  on clojure.core refs, like the bootstrap's core-renames.

Result: self-hosted fib(30) runs in 0.52s (was ~50s interpreted). Full
clojure-test-suite via JOLT_SELFHOST runs compiled at 3869 pass (some compiled-
vs-interpreted divergences remain to chase toward full parity — jolt-4xc).
Default suite green; conformance 218/218; jank-conformance up to 135.
2026-06-06 10:25:05 -04:00
Yogthos
06d5f51d4a self-host: compile the analyzer (full-suite parity, fast); fix interp multi-arity
The portable analyzer now refers the host contract + IR ctors unqualified (host
form predicates renamed form-* to dodge core-renames), so the bootstrap compiles
it via its plain :var path — no qualified-ref compilation needed. ensure-analyzer
compile-loads jolt.ir + jolt.analyzer as native bytecode.

Result: the self-hosted pipeline (portable Clojure analyzer -> IR -> Janet back
end) runs the FULL clojure-test-suite at 3913 pass — parity with the interpreter
baseline — and fast (no timeouts), via JOLT_SELFHOST=1. Conformance 218/218.

Also fixes a real interpreter bug: multi-arity dispatch stored the variadic clause
by fixed-count (colliding) and only matched exact counts, so (f a b c..) on an
[a b & more] clause threw. Now the variadic clause dispatches for any count >= its
fixed arity.

Remaining (jolt-4xc): the compiled analyzer still errors analyzing a multi-arity
fn literal (falls back to the interpreter, now correct); compiling that is the
last coverage gap before flipping the self-hosted pipeline on by default.
2026-06-06 09:59:28 -04:00
Yogthos
98bf389f82 compiler: resolve :as aliases in macro detection; fix analyzer compile-ns
- compiler/resolve-macro now resolves :as aliases (like resolve-var), so aliased
  macro calls (t/is) are recognized and compiled via expansion instead of falling
  back. Compile-mode suite holds at 3932.
- host/current-ns reads a backend-stashed :compile-ns rather than ctx-current-ns:
  the interpreter rebinds current-ns to a fn's defining ns while it runs, so the
  interpreted analyzer (defined in jolt.analyzer) would otherwise mis-resolve the
  user's compile ns. Fixes def/intern targeting the wrong namespace.

Analyzer stays interpreted (compiling it needs qualified-ref compilation, which
regresses the clojure.test shim — jolt-4xc). Self-host pipeline correct (218/218),
full suite green.
2026-06-06 09:48:52 -04:00
Yogthos
b3a80507cc destructuring via shared expander; fix sequential let + nth nil-default
core-let now expands destructuring (Clojure's destructure algorithm: nth/nthnext/
get over gensym roots) so both the interpreter and the compiler see only plain
bindings — the compiler compiles destructuring for free, and it's a step toward
moving destructuring out of the kernel. Handles vector (& rest, :as, nested), map
(:keys/:strs/:syms incl. namespaced, :as, :or, explicit pairs).

Two real bugs fixed in passing:
- compiler let*/loop* analyzed all binding inits in the OUTER scope, so a later
  binding couldn't see an earlier one ((let [x 1 y x] y) miscompiled). Now scope
  accumulates across bindings.
- core-nth treated an explicit nil not-found as 'no default' and threw on out of
  bounds; (nth coll i nil) now returns nil (Clojure semantics), via arity.

Baselines held: conformance 218/218 both modes, clojure-test-suite 3913 interp /
3932 compile, destructuring-spec green.
2026-06-06 09:36:36 -04:00
Yogthos
b6e41bf499 self-host: JOLT_SELFHOST suite mode; keep analyzer interpreted for now
suite-worker honors JOLT_SELFHOST=1 to route the clojure-test-suite through the
self-hosted pipeline (for validating once the analyzer compiles).

The analyzer stays interpreted: compiling it via the bootstrap miscompiles
(qualified refs regressed compile mode 3932->1181; compile-loading yields
corrupted IR) — tracked in jolt-4xc. Correctness is unaffected (the self-hosted
pipeline passes 218/218 conformance); only speed is pending.
2026-06-06 06:41:36 -04:00
Yogthos
f9df794475 self-host: compile loop/recur, recur-in-fn, and try
Analyzer threads an env (locals + recur target) instead of a bare locals set, and
compiles loop*/recur, recur directly in a fixed-arity fn (each arity's name is its
recur target -> self-call), and try/catch/finally (-> Janet try + defer). recur
into a variadic arity isn't a recur target, so it falls back to the interpreter
rather than miscompiling the rest seq. Still 218/218 conformance via the pipeline,
now with these forms compiling natively instead of interpreting.
2026-06-06 06:26:22 -04:00
Yogthos
5624e99eb6 self-host: analyzer reaches full conformance parity via hybrid fallback
The Clojure analyzer + Janet back end now pass all 218 conformance cases (vs the
interpreter) through the self-hosted pipeline:
- host contract gains special? (the full special-form/uncompilable set), so the
  analyzer falls back for forms it doesn't compile instead of miscompiling them
  as calls;
- qualified symbol refs that don't resolve to a var (janet/…, Math/…, host
  interop) and tagged literals (#"regex", #inst) and set literals throw
  uncompilable -> interpreter;
- the back end's compile-and-eval is now hybrid: only the compile step is
  guarded, runtime errors propagate.

Still interpreter-routed (coverage to grow next): loop/recur, recur in fn, try,
sets, destructuring.
2026-06-06 06:19:45 -04:00
Yogthos
849449aada self-host: working portable analyzer -> IR -> Janet back end pipeline
The Clojure-in-Clojure front end now compiles and runs a real subset end to end:
arithmetic, if/do/let, vector/map literals, def + name-based global refs, fn,
defn (via macroexpand), recursion through the var cell, multi-arity, variadic,
and higher-order calls. Proven by test/integration/self-host-test.janet.

Pieces:
- jolt-core/jolt/analyzer.clj — PORTABLE Clojure analyzer: reader form -> IR,
  depending only on the host contract (jolt.host), never on Janet.
- src/jolt/host_iface.janet — Janet impl of the jolt.host contract (form
  introspection + resolve/macro/current-ns), installed into each ctx.
- src/jolt/backend.janet — Janet back end: IR -> Janet form -> eval; resolves
  name-based :var nodes to cells and reuses runtime helpers.

Host Janet code lives in src/jolt/ (not a host/janet/ dir): Janet resolves
relative imports per file, so cross-dir ../../src/jolt/* imports load second
instances of compiler/types/core and corrupt state. The portability boundary is
the jolt.host namespace contract + jolt-core/, not the directory.

Notes: gensym in the back end must not be Janet's (shadowed by Jolt's via use);
a jolt bug — (into #{} coll) doesn't add elements — was worked around with reduce
conj in the analyzer (filed separately).
2026-06-06 06:11:42 -04:00
Yogthos
b1ac427bdd self-host: scaffold portable jolt-core + host/core architecture
Adds doc/self-hosting-architecture.md: the portability design (portable front
end + host back end + host runtime, seam = a minimal host protocol + the IR),
grounded in the CLJS/Clojure-in-Clojure prior art. Decisions locked: minimal host
protocol as the seam; physical split with a jolt-core/ source root.

Adds jolt-core/ as a portable Clojure source root (dev path + embedded into the
binary alongside the rest of the stdlib) and jolt-core/jolt/ir.clj — host-neutral
IR node constructors (vars referenced by name, no host values embedded). Verified
it loads and runs under both interpreter and compiler.
2026-06-06 05:44:12 -04:00
Yogthos
6c61d445fb docs: compile-by-default, hybrid fallback, AOT; mark staged path 1/2/5 done
README and the self-hosting design doc now describe the current pipeline:
compile-by-default with an always-correct interpreter fallback, var-cell late
binding, parity validation, and AOT images. The staged path marks
var-indirection, hybrid+coverage, and compile-by-default+AOT done; self-hosting
the compiler and moving core to Clojure remain.
2026-06-06 03:00:30 -04:00
Yogthos
877373b7e6 aot: marshal compiled namespaces to bytecode images
Adds src/jolt/aot.janet: save-ns / load-ns-image marshal a namespace's compiled
var cells to a Janet bytecode image and load them back, skipping
parse/analyze/emit/compile on reload.

Compiled functions close over core fns (cfunctions) that can't marshal by value,
so we marshal against a dictionary built from the baked-in runtime env
(env-lookup jolt-runtime-env, which chains to the Janet boot env): core fns and
builtins are referenced by name, only user bytecode and its var cells are
serialized. The runtime env is identical at save and load time (same binary), so
the dictionaries match.

Test round-trips a namespace (constant, fn over it, core-fn user, recursion) into
a fresh context and confirms the loaded vars run and stay redefinable.
2026-06-06 02:57:32 -04:00
Yogthos
62c5060093 compile by default in the shipped runtime
The jolt binary now compiles each form to Janet bytecode by default (REPL, file,
-e, -m, nrepl). The hybrid path keeps it correct — forms the compiler can't
handle fall back to the interpreter — and it's validated at parity with the
interpreter across the conformance suite (218/218) and the clojure-test-suite
(3932 pass / 124 error vs 3913 / 125). JOLT_INTERPRET=1 forces the interpreter.
2026-06-06 02:50:59 -04:00
Yogthos
918e2798db compiler: resolve Janet-env fallback symbols; validate full suite under compile
analyze-form now mirrors resolve-sym's full resolution order: jolt var (current
ns / clojure.core), then the runtime/Janet env, then a pending forward-ref cell.
Previously a name that resolves only via the Janet-env fallback (int?, type, …)
interned an empty var and miscompiled; now it emits the runtime binding directly,
matching the interpreter.

suite-worker honors JOLT_COMPILE=1 to run the whole clojure-test-suite through
the compile path. Under compilation it now passes 3932 / errors 124 — at parity
with the interpreter baseline (3913 / 125) across 4600+ assertions, confirming
the hybrid compile path doesn't diverge from the interpreter.
2026-06-06 02:49:53 -04:00
Yogthos
6297a65617 compiler: fix compile-mode correctness (conformance 87->218/218)
Running the conformance suite under compile mode surfaced many forms that
silently miscompiled (the hybrid fallback only catches compile-time errors,
not wrong results). Fixes:

- Global var resolution now mirrors the interpreter's resolve-var: current ns
  (which holds refers) then clojure.core, instead of interning an empty var in
  the current ns. This was the big one — every core fn not in core-renames
  (iterate, update, subvec, reductions, ...) derefed to nil from a user ns.
- Map literals evaluate their keys and values (were emitted as quoted data);
  build via build-map-literal, mirroring the interpreter (struct, or phm when a
  key is a collection).
- Vector literals build a mode-appropriate jolt vector via make-vec (pvec when
  immutable, array when mutable) instead of a bare Janet tuple, so compiled and
  interpreted vectors share one representation (type-strict ops like rseq
  rejected tuples).
- Core fn values resolve dynamically from the runtime env instead of a
  hand-maintained table that had drifted: core-apply mapped to Janet's native
  apply (rejects pvec tails), core-some mapped to core-some?. Removed the table
  and the bogus some rename.
- analyze-form throws uncompilable on interpreter special forms it doesn't
  implement and on definitional/host macros (deftype, defprotocol, reify,
  binding, letfn, read-string, regex/tagged literals, ...), so they fall back
  to the interpreter instead of miscompiling — including nested in compiled
  forms.

conformance-test.janet now runs every case under both interpreter and compiler
so compile-mode correctness is guarded in CI.
2026-06-06 02:41:18 -04:00
Yogthos
acf724b638 loader: share compile/interpret routing between load-ns and eval-one
Moves the per-form routing (compile when :compile?, stateful forms always
interpret, interpreter fallback for uncompilable forms) into loader as
eval-toplevel, used by both load-ns and api/eval-one. Previously load-ns's
compile path called compile-and-eval directly with no fallback and no
stateful-form handling, so it would have broken ns/require/defmacro under
compile-by-default.

Array forms whose head is not a symbol (e.g. ((fn ...) args)) compile like
any other call, matching the prior eval-one behavior.
2026-06-06 02:19:21 -04:00
Yogthos
2872f17c59 compiler: compile multi-arity, named, and variadic fns + recur-in-fn
Rewrites fn* analysis/emit. Each arity compiles to a named Janet fn whose
name is its recur target, so recur is a self-call (Janet tail-calls it) —
this fixes recur used directly inside a fn, which previously miscompiled to
a runtime error that the hybrid fallback couldn't catch.

Multi-arity fns dispatch on arg count: fixed arities match exactly, the
variadic arity matches >= its fixed count. The rest param is an ordinary
param holding a seq, so (recur fixed... rest-seq) into a variadic arity
works as in Clojure. Single fixed-arity fns keep the direct, dispatch-free
shape so the hot path stays fast.

Destructuring params still route to the interpreter via uncompilable.
Tests cover named recursion, multi-arity (incl. variadic clause), recur in
fn, and recur into a variadic arity.
2026-06-06 01:56:01 -04:00
Yogthos
145035d424 compiler phase 2: hybrid interpret/compile fallback
eval-one now tries to compile each form and falls back to the interpreter
for anything the compiler can't handle, instead of erroring or silently
miscompiling. Only the compile step is guarded, so runtime errors in
compiled code still propagate (no double-eval of side effects, no hidden
errors).

The compiler now throws jolt/uncompilable on the forms it would otherwise
miscompile — destructuring let/loop bindings and fn params, multi-arity
fns, and named fns — so they route to the interpreter and produce correct
results. These become compile targets in a later optimization pass.

Adds eval-compiled to split compile from eval; compile-mode tests cover
destructuring, multi-arity, named-fn recursion, and error propagation.
2026-06-06 01:43:40 -04:00
Yogthos
70711b4489 compiler phase 1: var-indirection for redefinable compiled globals
Compiled global references now deref through the Jolt var cell instead of an
early-bound Janet symbol, so redefining a def/defn is visible to already-compiled
callers (Janet early-binds plain symbols). A global ref analyzes to :op :var
(resolving/creating the cell); a def sets that same cell's root. Because Janet
copies table constants but references functions, we embed memoized getter/setter
closures over the cell rather than the cell itself. This also subsumes the old
named-fn recursion rewrite and the separate ns-intern.

ns-intern now stores the namespace *name* (string) in a var, not the ns table, so
var cells aren't cyclic (required for embedding).

fib(30) compiled ~0.5s (the late-binding cost vs phase 2's direct-linked 0.08s;
direct-linking for hot calls is a later optimization). Suite green; cross-form,
recursion, and context isolation preserved; redefinition now works.
2026-06-06 01:33:47 -04:00
Yogthos
614962d535 Research notes: self-hosting compiler design
Survey (Clojure/JVM var indirection + direct linking, ClojureScript self-host,
nanopass, Guile) and a recommended path to a self-hosting Clojure-in-Clojure
compiler emitting Janet bytecode while keeping REPL redefinition. Key finding:
Janet early-binds top-level refs, so compiled globals must deref through var
cells to stay redefinable. Recommends var-indirection first, then hybrid
fallback, then self-hosting the compiler and shrinking the Janet kernel.
2026-06-06 01:13:51 -04:00
Dmitri Sotnikov
607779866e
Merge pull request #9 from jolt-lang/selmer-io-destructuring
Destructuring compliance, deps.edn comments, clojure.java.io shim
2026-06-06 13:02:10 +08:00
Yogthos
41f78e218f destructuring compliance, deps.edn comments, clojure.java.io shim
Destructuring (drove by trying to load Selmer):
- defmacro params now destructure like fn (parse-params + destructure-bind), so
  [& [a & more :as all]] and {:keys ...} work in macro arglists.
- map-destructuring a sequential value treats it as keyword args (alternating
  k/v, or a trailing map) — the [& {:keys [...]}] form, in fns and macros.
- :keys/:syms accept namespaced symbols (x/y): looks up the namespaced key,
  binds the bare local.

deps.edn: read with Jolt's reader instead of Janet's parse, so EDN ';' line
comments are handled (Janet treats ';' as splice).

clojure.java.io: the shim was at clojure/java_io.clj (ns clojure.java-io, never
the clojure.java.io that code requires) and used bare-qualified Janet calls that
the janet.* bridge no longer resolves. Moved to clojure/java/io.clj and rewritten
on janet.file/os: file/reader/writer/resource/copy/delete-file/make-parents.

Specs in destructuring-spec; destructuring note in the ebnf.
2026-06-06 01:01:04 -04:00
Dmitri Sotnikov
48a5f6258f
Merge pull request #8 from jolt-lang/cuerdas-kebab
Support type hints; coherent metadata
2026-06-06 12:37:19 +08:00
Yogthos
761a2b8f72 support type hints; make metadata coherent
The reader expanded ^X form to (with-meta form X), which evaluated the tag (so
^String errored 'Unable to resolve symbol: String') and, as a param, was no
longer a bare symbol (so the arg bound to nil). Now a keyword/symbol/string hint
on a symbol attaches to the symbol's :meta and the symbol stays bare, so type
hints are transparent in params, lets, and bodies. Map metadata still uses a
runtime with-meta form.

meta now returns a symbol's :meta, and def applies the name's metadata
(^:dynamic, ^:private, ^Type tag, ^{:doc}) to the var, so (meta (var x)) is
consistent. Specs in metadata-spec; grammar note in the ebnf.

README notes regex \p{...} as unsupported (separate from this).
2026-06-06 00:36:10 -04:00
Yogthos
f749ec3c19 remove unused svg 2026-06-06 00:12:09 -04:00
Dmitri Sotnikov
3061c246d3
Merge pull request #7 from jolt-lang/reader-char-literals
reader: one-char literals for non-symbol chars (\{ \( \, \%)
2026-06-06 12:11:14 +08:00
Yogthos
d724aa7069 reader: one-char literals for non-symbol chars (\{ \( \, \%)
read-char read the char name with symbol-char?, so a literal whose char isn't a
symbol char (\{, \(, \), \,, \%, …) came back as an empty name and errored
'Unsupported character'. Now a single non-symbol char after \ is taken as a
one-character literal of that char. Surfaced by funcool/cuerdas (uses \{), which
now loads. Spec cases added.
2026-06-06 00:10:17 -04:00
Dmitri Sotnikov
b3a145f124
Merge pull request #6 from jolt-lang/bake-stdlib
Bake the .clj stdlib into the binary
2026-06-06 11:51:58 +08:00
Yogthos
f3f2c63ee7 bake the .clj stdlib into the binary
The Clojure stdlib namespaces (clojure.string/set/walk/edn/zip, jolt.http/
interop/shell/nrepl) were loaded cwd-relative, so the built binary couldn't
load them outside the repo. Now stdlib_embed slurps them all at build time into
a {ns -> source} map frozen in the image, and the loader falls back to it when a
namespace isn't found on disk. The artifact is self-contained — clojure.string
works from any directory.

Drops the one-off nREPL source embed (jolt.nrepl is covered by this now). The
baked-in stdlib is excluded from uberscript bundles since it's part of the
runtime. clojure.core is unchanged (Janet, auto-referred).

Test: embedded-stdlib-test requires from the embedded copy with FS roots cleared.
2026-06-05 23:51:03 -04:00
Dmitri Sotnikov
701f75da69
Merge pull request #5 from jolt-lang/deps-phase34
deps phases 3-4: uberscript bundling + conformance harness
2026-06-06 11:38:27 +08:00
Yogthos
30c4bb0dc2 deps phases 3-4: uberscript bundling + conformance harness
Phase 3 — bundling. `jolt uberscript OUT -m NS` requires NS, uses the loader's
recorded load order (a dep finishes loading before its requirer, so it's
topological) to concatenate the used namespaces into one .clj that runs on a
plain jolt with no deps/jpm. `jolt-deps uberscript` resolves first. The loader
records loaded file paths when ctx env has :loaded-files.

Phase 4 — conformance. deps-conformance-test resolves real pure-cljc git libs
and reports load/run status, gated behind JOLT_CONFORMANCE so CI stays offline.
First run: medley works; cuerdas hits a reader gap (filed); stuartsierra/
dependency uses Long/MAX_VALUE (JVM interop, out of scope).

Tests: uberscript-test, deps-conformance-test (skips without the flag).
2026-06-05 23:36:40 -04:00
Dmitri Sotnikov
6a1706df10
Merge pull request #4 from jolt-lang/nrepl-port-cleanup
nrepl: remove .nrepl-port on exit
2026-06-06 11:25:36 +08:00
Yogthos
becc94431c nrepl: remove .nrepl-port on exit
Clean up the .nrepl-port file when the server stops — via defer on a clean
unwind and SIGINT/SIGTERM handlers for Ctrl-C. A SIGKILL still can't be caught,
so it may occasionally be left behind.
2026-06-05 23:24:54 -04:00
Dmitri Sotnikov
39049d6dba
Merge pull request #3 from jolt-lang/cli-flags
CLI: babashka-style flags (--version, --eval, --main, nrepl-server)
2026-06-06 11:21:15 +08:00
Yogthos
ed9d5fc998 cli: babashka-style flags (--version, --eval, --main, nrepl-server)
Add --version/version, -e/--eval, -f/--file, -m/--main (require NS and apply
its -main to *command-line-args*), and nrepl-server/--nrepl-server taking an
optional [host:]port (nrepl stays an alias). repl/help/-h round it out.

Also rewrite doc/tools-deps.md as design notes for the now-implemented deps
support (it still read as pre-implementation research), and note the new flags
in the README. Test: cli-test runs the flags from source.
2026-06-05 23:20:29 -04:00
Dmitri Sotnikov
f5205e8c0b
Merge pull request #2 from jolt-lang/deps-research
deps.edn support: load Clojure git deps via a separate jolt-deps tool
2026-06-06 11:12:31 +08:00