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.
5 KiB
deps.edn support — design notes
How Jolt loads pure-Clojure libraries from a deps.edn, and why it's built the
way it is. For how to use it, see building-and-deps.md.
Scope, decided up front:
- git + local deps only — no Maven/
~/.m2resolution. - pure
clj/cljc— anything needing the JVM won't load or run; expected. - no classpath abstraction —
requirejust needs to find a dep's namespaces; "the classpath" is an ordered list of source directories. - piggyback on jpm — reuse jpm's git fetch + cache; don't write a package manager.
- separate tool — resolution lives in
jolt-deps, beside the runtime, the wayjpmsits besidejanet. Thejoltruntime knows nothing about deps.edn.
How jpm handles dependencies
jpm's package code (jpm/pm.janet) splits into a fetch half and a build half,
and we use only the first:
resolve-bundlenormalizes a dep spec to{:url :tag :type :shallow}, accepting:url/:repo+:tag/:sha/:commit/:ref. A deps.edn{:git/url … :git/sha …}maps straight onto it.download-bundle url :git tag shallowclones into a content-addressed cache (<modpath>/.cache/git_<tag>_<sanitized-url>) and returns the path —git init+remote add+ fetch + reset, plus submodules. No build step.bundle-installis the half we skip: it then runsproject.janetbuild rules, which a Clojure lib doesn't have. It's cleanly separable from the clone.
So jpm gives us git resolution and a cache for free; calling download-bundle
needs jpm/config/load-default first (it sets gitpath and the cache dyns).
How it works
src/jolt/deps.janet reads deps.edn (Janet parses it directly — EDN and Janet
syntax overlap for the :deps/:paths subset), then walks :deps:
:git/url(+:git/shaor:git/tag) →resolve-bundle+download-bundleintojpm_tree/.cache;:local/root→ the path as-is;:mvn/*and anything else → ignored.
Each resolved dependency contributes its own :paths (default ["src"]) as
source roots, and we recurse into its deps.edn for transitive deps. The result
is a de-duplicated, ordered list of directories. resolve-deps-cached memoizes
that list in the tree keyed on a hash of deps.edn, so an unchanged file doesn't
re-fetch. jpm is loaded lazily (require, not import) so it's pulled in only
when resolving — never embedded in a built binary.
The loader (evaluator.janet/find-ns-file) resolves a namespace by searching the
context's :source-paths in order (the stdlib src/jolt first), trying <ns>.clj
then <ns>.cljc. Extra roots come from JOLT_PATH or init's :paths option.
jolt-deps (src/jolt/deps_cli.janet, its own declare-executable) ties it
together: it resolves the roots and runs the jolt binary with them on
JOLT_PATH. The runtime's only dependency interface is that env var.
Gotcha worth remembering: the jolt CLI's context is built into its image at
build time, so JOLT_PATH is applied at runtime in main, not in init (whose
env read would be frozen at build).
Limitations
- Pure
clj/cljconly — JVM interop, host classes, and unimplementedclojure.corecorners fail. Coverage is per-function: a namespace can load with most functions working and a few not. - Source only; compiled
.classfiles in a git dep are ignored. - git
:git/shamust be a full SHA (git fetchcan't resolve a short one).
Status
-
Loader source roots — done.
find-ns-file+:source-paths,.clj/.cljc,JOLT_PATH/:paths. Test:test/integration/deps-loader-test.janet. -
Resolve git/local deps via jpm — done.
deps.janet+ thejolt-depstool. Test:test/integration/deps-resolve-test.janet. -
Bundling (uberscript) — done.
jolt uberscript OUT -m NSrequiresNS, records the load order the loader emits (a dependency finishes loading before the file that required it, so it's topological), and concatenates the source into one.cljthat runs on a plainjolt— no deps, no jpm.jolt-deps uberscriptresolves first. Test:test/integration/uberscript-test.janet. (This is the practical form of "compile the used namespaces into the build"; embedding into a custom binary image is a heavier variant we haven't needed.) -
Conformance — harness in place:
test/integration/deps-conformance-test.janetresolves real pure-cljcgit libs and reports load/run status (network-gated behindJOLT_CONFORMANCE=1, so CI stays offline). First run:medley— loads and runs.cuerdas— now loads (it used\{, a one-char literal the reader rejected; fixed). A function (kebab) still hits a separate runtime gap — a smaller, per-function issue rather than a whole-namespace failure.stuartsierra/dependency—Long/MAX_VALUE: JVM interop, so out of scope (it isn't actually pure-cljc).
The loop from here is the same as the clojure-test-suite battery: add libs, see what breaks, fix interpreter/reader gaps.