diff --git a/docs/tools-deps.md b/docs/tools-deps.md index ed6f9e4..8bce643 100644 --- a/docs/tools-deps.md +++ b/docs/tools-deps.md @@ -96,11 +96,13 @@ a C compiler; `JOLT_CHEZ_CSV` overrides the auto-detected `csv/` dir. `--opt` turns on the inference/flatten/scalar-replace passes; the default `release` mode is const-fold only. -`release` and `--opt` are closed-world: a call between the app's own functions -binds to its target directly, skipping the var lookup and generic dispatch a -runtime call pays. That assumes app vars are final — mark one `^:redef` (or -`^:dynamic`) to keep it redefinable and indirect. `--dev` keeps everything -indirect/open. Calls into `clojure.core` stay indirect in every mode. +`--direct-link` (or `:jolt/build {:direct-link true}`) opts into a closed world: a +call between the app's own functions binds to its target directly, skipping the var +lookup and generic dispatch a runtime call pays — at the cost of runtime +redefinition of those vars and `eval`/`load-string`. It's off by default, so +ordinary builds (including `release` and `--opt`) stay dynamically linked. A var +marked `^:redef` or `^:dynamic` stays indirect even under `--direct-link`, and calls +into `clojure.core` stay indirect in every mode. ## Limitations diff --git a/host/chez/build-smoke.sh b/host/chez/build-smoke.sh index 9187211..eca8402 100755 --- a/host/chez/build-smoke.sh +++ b/host/chez/build-smoke.sh @@ -63,4 +63,19 @@ if [ "$got_opt" != "$want" ]; then echo "--- got ----"; echo "$got_opt" exit 1 fi -echo "build smoke: passed (release + optimized)" + +# Closed-world direct-linking (opt-in): same result, and the cross-namespace call +# (app.core -> app.util/shout) must lower to a direct jv$ binding, not var-deref. +if ! JOLT_PWD="$app" bin/joltc build -m app.core -o "$out" --direct-link >/dev/null 2>&1; then + echo " FAIL: jolt build --direct-link exited non-zero"; exit 1 +fi +got_dl="$(cd / && "$out" alpha bb ccc 2>&1)" +if [ "$got_dl" != "$want" ]; then + echo " FAIL: --direct-link binary output mismatch" + echo "--- got ----"; echo "$got_dl" + exit 1 +fi +if ! grep -q '(jv\$app.util\$shout' "$out.build/flat.ss"; then + echo " FAIL: --direct-link did not emit a direct app->app call"; exit 1 +fi +echo "build smoke: passed (release + optimized + direct-link)" diff --git a/host/chez/build.ss b/host/chez/build.ss index f37cc83..fc8e8da 100644 --- a/host/chez/build.ss +++ b/host/chez/build.ss @@ -211,7 +211,10 @@ ;; natives: encoded :jolt/native libs to load at startup. embed-dirs: dirs whose ;; files bake into the binary (single-file). ext-roots: project-relative io/resource ;; roots resolved at runtime against JOLT_PWD (ship-alongside resources). -(define (build-binary entry-ns out-path mode natives embed-dirs ext-roots) +;; direct-link?: opt-in closed-world direct-linking (app->app calls bind directly, +;; no runtime redefinition). Off by default in every mode — release stays +;; dynamically linked. +(define (build-binary entry-ns out-path mode natives embed-dirs ext-roots direct-link?) (bld-check-toolchain) ;; 1. record app namespaces in dependency order as they finish loading. (let ((app-order '())) @@ -224,14 +227,15 @@ (error 'jolt-build (string-append "no source namespace loaded for " entry-ns " — is it on the source roots?"))) ;; 2. emit each app namespace. `optimized` turns on the inference + flatten - ;; + scalar-replace passes (closed world); release/dev get const-fold only. - ;; release + optimized are closed-world: turn on direct-linking so app->app - ;; calls bind directly (dev stays open/indirect). The defined-set accumulates - ;; across the dependency-ordered namespaces, so a dep's defs are direct-linkable - ;; by the time the entry that calls them is emitted. + ;; + scalar-replace passes; release/dev get const-fold only. + ;; direct-link? (opt-in) commits to a closed world: app->app calls bind + ;; directly, giving up runtime redefinition of those vars. Off by default in + ;; every mode. The defined-set accumulates across the dependency-ordered + ;; namespaces, so a dep's defs are direct-linkable by the time the entry that + ;; calls them is emitted. (set-optimize! (string=? mode "optimized")) - (let ((dl (not (string=? mode "dev")))) - ((var-deref "jolt.backend-scheme" "set-direct-link!") dl) + (when direct-link? + ((var-deref "jolt.backend-scheme" "set-direct-link!") #t) ((var-deref "jolt.backend-scheme" "direct-link-reset!"))) (let* ((app-strs (apply append (map (lambda (nf) (bld-emit-ns (car nf) (read-file-string (cdr nf)))) @@ -325,9 +329,9 @@ (display (string-append "jolt build: wrote " out-path "\n")))))) (def-var! "jolt.host" "build-binary" - (lambda (entry out mode natives embed-dirs ext-roots) + (lambda (entry out mode natives embed-dirs ext-roots direct-link?) (build-binary (jolt-str-render-one entry) (jolt-str-render-one out) (jolt-str-render-one mode) - natives embed-dirs ext-roots) + natives embed-dirs ext-roots (jolt-truthy? direct-link?)) jolt-nil)) diff --git a/jolt-core/jolt/main.clj b/jolt-core/jolt/main.clj index 7bf8993..090a495 100644 --- a/jolt-core/jolt/main.clj +++ b/jolt-core/jolt/main.clj @@ -115,10 +115,13 @@ (map? task) (do (apply-project! resolved) (apply-main-opts (:main-opts task) more)) :else (throw (ex-info (str "bad task " name) {}))))) -;; build [-m NS | FILE] [-o OUT] [--opt | --dev] — AOT-compile the app into a -;; standalone executable. Resolves deps + roots like `run`, then hands the entry -;; namespace to the host build driver (jolt.host/build-binary, defined by +;; build [-m NS | FILE] [-o OUT] [--opt | --dev] [--direct-link] — AOT-compile the +;; app into a standalone executable. Resolves deps + roots like `run`, then hands the +;; entry namespace to the host build driver (jolt.host/build-binary, defined by ;; build.ss). Default mode is release; --opt selects optimized, --dev unoptimized. +;; --direct-link (or deps.edn :jolt/build {:direct-link true}) opts into closed-world +;; direct-linking: app->app calls bind directly, giving up runtime redefinition of +;; those vars and eval/load-string. Off by default — release stays dynamically linked. ;; Encode a deps.edn :jolt/native spec for the build launcher, resolving the ;; current platform's candidate list now (the binary runs on this OS). Each entry ;; becomes a vector the launcher (build.ss) reads: ["process"] for the running @@ -133,7 +136,7 @@ (into [(if (:optional spec) "opt" "req")] cands))))))) (defn- cmd-build [more] - (let [{:keys [project-paths embed-dirs] :as resolved} + (let [{:keys [project-paths embed-dirs build] :as resolved} (deps/resolve-project (project-dir))] (apply-project! resolved) (let [opts (loop [a more, entry nil, out nil] @@ -164,10 +167,13 @@ (nil? o) (str pdir "/target/" (if (= mode "dev") "debug" "release") "/" proj) (str/starts-with? o "/") o :else (str pdir "/" o))) - natives (encode-natives (:natives resolved))] + natives (encode-natives (:natives resolved)) + ;; closed-world direct-linking is opt-in: the --direct-link flag or a + ;; deps.edn :jolt/build {:direct-link true}. Off otherwise. + direct-link? (boolean (or (some #{"--direct-link"} more) (:direct-link build)))] ;; embed-dirs (absolute) are walked + baked into the binary by the driver; ;; project-paths (relative) become runtime io/resource roots (ship-alongside). - (jolt.host/build-binary entry out mode natives embed-dirs project-paths))))) + (jolt.host/build-binary entry out mode natives embed-dirs project-paths direct-link?))))) (defn- nrepl [more] ;; resolve the project (deps on the roots, native libs loaded), then start the @@ -185,7 +191,7 @@ (println "usage: jolt [args]") (println " run -m NS [args] resolve deps.edn, load NS, call its -main") (println " run FILE load a Clojure file") - (println " build -m NS [-o OUT] [--opt|--dev] compile a standalone binary") + (println " build -m NS [-o OUT] [--opt|--dev] [--direct-link] compile a standalone binary") (println " -M:alias [args] run the alias's :main-opts") (println " -A:alias [args] add the alias's paths/deps") (println " repl start a line REPL")