From 6ea43fb6237536ac5ab874eeebb9c0d3a6eb5eae Mon Sep 17 00:00:00 2001 From: Yogthos Date: Fri, 5 Jun 2026 22:53:20 -0400 Subject: [PATCH] deps phase 1: loader searches multiple source roots The loader resolved a namespace against one hardcoded root (src/jolt, .clj only). Now it searches an ordered list (ctx env :source-paths, stdlib first) and tries .clj then .cljc. init adds roots from the :paths opt and JOLT_PATH. This is the foundation for loading deps.edn libraries; verified loading medley from an added root. No change to stdlib loading (3913 suite baseline held). --- doc/tools-deps.md | 18 +++++----- src/jolt/api.janet | 9 ++++- src/jolt/evaluator.janet | 35 ++++++++++++------ src/jolt/types.janet | 3 ++ test/integration/deps-loader-test.janet | 48 +++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 21 deletions(-) create mode 100644 test/integration/deps-loader-test.janet diff --git a/doc/tools-deps.md b/doc/tools-deps.md index 2b05aba..8f41ea7 100644 --- a/doc/tools-deps.md +++ b/doc/tools-deps.md @@ -91,11 +91,10 @@ already does path-based namespace lookup — we widen it from one root to a few. jpm stays the build tool for the Jolt binary; this lives beside it and *calls into* `jpm/pm` for the git/cache work (import the module at dev/build time — jpm -is always present then; the shipped binary doesn't need it). Open question: -whether to depend on jpm's internal `pm` functions (not a stable public API) or -shell `git` ourselves into the same cache layout. Reusing `pm` is less code and -shares jpm's cache; shelling git is ~15 lines and avoids the internal-API risk. -Leaning toward reusing `pm` first. +is always present then; the shipped binary doesn't need it). **Decision: reuse +`jpm/pm`** (`resolve-bundle` + `download-bundle`, after `jpm/config/load-default`) +rather than shelling `git` ourselves — less code, and it shares jpm's cache. The +internal-API risk is acceptable since it's only used at dev/build time. ## Limitations @@ -107,10 +106,11 @@ Leaning toward reusing `pm` first. ## Plan -1. **Loader roots.** Generalize `maybe-require-ns` to search an ordered root list - and try `.clj` + `.cljc`; add a `JOLT_PATH`/`--path` to set roots by hand. - Point it at a checked-out lib's `src` and load it. Independently testable, - unblocks the rest. +1. **Loader roots.** *(done)* `maybe-require-ns` now searches an ordered root + list (`ctx.env :source-paths`, stdlib first) trying `.clj` then `.cljc`; + `init` adds roots from the `:paths` opt and `JOLT_PATH` (colon-separated). + Verified loading a real lib (medley) from an added root. See + `test/integration/deps-loader-test.janet`. 2. **Resolve git deps via jpm.** Read `deps.edn`, resolve `:git/*` (+ `:local/root`) through `jpm/pm` into `jpm_tree/.cache`, recurse for transitive git deps, register the roots. `jolt deps` to resolve/print; auto on startup diff --git a/src/jolt/api.janet b/src/jolt/api.janet index 4e59165..215ec6b 100644 --- a/src/jolt/api.janet +++ b/src/jolt/api.janet @@ -30,10 +30,17 @@ opts may contain: :namespaces — map of {ns-name → {sym → value, ...}, ...} :mutable? — use Janet mutable data structures instead of persistent - :compile? — enable compilation of Clojure forms to Janet" + :compile? — enable compilation of Clojure forms to Janet + :paths — extra source roots to search for namespaces (after the stdlib)" [&opt opts] (default opts {}) (let [ctx (make-ctx opts)] + # Extra source roots: opts :paths, then JOLT_PATH (colon-separated). These are + # searched after the stdlib so (require ...) finds deps.edn-resolved libs. + (let [roots (get (ctx :env) :source-paths)] + (each p (get opts :paths []) (array/push roots p)) + (when-let [jp (os/getenv "JOLT_PATH")] + (each p (string/split ":" jp) (when (> (length p) 0) (array/push roots p))))) # Collection representation (persistent vs mutable) is selected at BUILD time # via JOLT_MUTABLE (see config.janet); init-core! registers vec/vector/conj/ # etc. that produce the mode-appropriate values, so nothing extra to load. diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index 7213f7a..c3b5a5b 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -185,12 +185,24 @@ [sym-s] (if (sym-s :ns) (string (sym-s :ns) "/" (sym-s :name)) (sym-s :name))) -(defn- ns->path - "Map a namespace name to its Jolt stdlib source path (dots->dirs, dashes->_)." +(defn- ns->relpath + "Namespace name to its file-relative path (dots->dirs, dashes->_), no extension." [ns-name] - (string "src/jolt/" - (string/replace-all "." "/" (string/replace-all "-" "_" ns-name)) - ".clj")) + (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-file "Parse and evaluate every form in a .clj file in the given context." @@ -202,17 +214,18 @@ (when (not (nil? f)) (eval-form ctx @{} f)))) (defn- maybe-require-ns - "If namespace ns-name isn't populated yet and a stdlib source file exists, - load it. Restores the current namespace afterwards (the file's `ns` form + "If namespace ns-name isn't populated yet and source for it exists on the + context's source roots, load it. 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= ns-name "clojure.core")) - (let [path (ns->path ns-name)] - (when (os/stat path) + (let [path (find-ns-file ctx ns-name)] + (when path (let [saved (ctx-current-ns ctx)] - # Jolt stdlib files have no `ns` form; switch into the target ns so - # their defs intern there, then restore. + # 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) (load-ns-file ctx path) (ctx-set-current-ns ctx saved))))))) diff --git a/src/jolt/types.janet b/src/jolt/types.janet index 96cb1d9..41ab0fe 100644 --- a/src/jolt/types.janet +++ b/src/jolt/types.janet @@ -366,6 +366,9 @@ :class->opts @{} :current-ns "user" :compile? compile? + # Ordered roots searched (after the stdlib) to resolve a namespace + # to a .clj/.cljc file. deps.edn resolution appends dep src dirs. + :source-paths @["src/jolt"] :compiled-cache @{} :type-registry @{} :data-readers (let [dr @{}] diff --git a/test/integration/deps-loader-test.janet b/test/integration/deps-loader-test.janet new file mode 100644 index 0000000..521a36a --- /dev/null +++ b/test/integration/deps-loader-test.janet @@ -0,0 +1,48 @@ +# The loader resolves namespaces against an ordered list of source roots (the +# stdlib first, then deps.edn-resolved dirs), trying .clj then .cljc. This is the +# foundation for loading Clojure libraries via deps.edn — here we point a root at +# a hand-written "library" and require it. + +(use ../../src/jolt/api) +(use ../../src/jolt/types) + +(def tmp (string (os/getenv "TMPDIR") "jolt-deps-test-" (os/time))) +(os/mkdir tmp) +(os/mkdir (string tmp "/mylib")) +(os/mkdir (string tmp "/other")) + +# a .cljc library with its own ns form and a require of a sibling ns +(spit (string tmp "/mylib/core.cljc") + "(ns mylib.core (:require [other.util :as u]))\n(defn double [x] (* 2 x))\n(defn doubled-inc [x] (u/inc1 (double x)))\n") +# a .clj sibling, with a dash in the name (-> underscore in the path) +(os/mkdir (string tmp "/other")) +(spit (string tmp "/other/util.clj") "(ns other.util)\n(defn inc1 [x] (+ x 1))\n") + +(def ctx (init {:paths [tmp]})) +(ctx-set-current-ns ctx "user") + +(var fails 0) +(defn check [label expr expected] + (let [r (protect (eval-string ctx expr)) + got (if (r 0) (normalize-pvecs (r 1)) (string "ERR " (r 1)))] + (if (= got expected) + (print " ok " label) + (do (++ fails) (printf " FAIL %s: want %q, got %q" label expected got))))) + +# require a .cljc lib from the added root and call it +(check "require .cljc lib" "(do (require (quote [mylib.core :as m])) (m/double 21))" 42) +# transitive require (mylib.core requires other.util) resolved from the root too +(check "transitive .clj require" "(mylib.core/doubled-inc 10)" 21) +# the stdlib still resolves from its default root +(check "stdlib still loads" "(do (require (quote [jolt.interop :as j])) (j/janet-type 1))" :number) + +# clean up +(defn rmrf [p] + (if (= :directory (os/stat p :mode)) + (do (each e (os/dir p) (rmrf (string p "/" e))) (os/rmdir p)) + (os/rm p))) +(rmrf tmp) + +(if (> fails 0) + (error (string "deps-loader-test: " fails " failing check(s)")) + (print "\nAll deps-loader tests passed!"))