cgen: build-time AOT — native fns without a toolchain on the target (jolt-a7ds) (#148)

Splits native codegen into a build phase (needs cc) and a deploy phase (none):

- gen-c-module/compile-module compile MANY numeric-leaf fns into ONE native
  module (the AOT shape), generalizing the one-fn-per-.so JIT path.
- Backend :cgen-collect? records each numeric-leaf defn's IR while the app loads
  as bytecode; cgen/aot-build compiles them into one module and write-manifest
  persists {sopath, [{ns name sym}]}.
- Backend :cgen-prebuilt + cgen/load-aot: the deploy run loads the prebuilt .so
  (via the native builtin, no cc) and installs each cfunction as the var root
  with the same timing as the JIT path, so callers direct-link to native code.
- toolchain-available? no longer crashes when cc is off PATH (os/execute raises
  on a missing exe) — a toolchain-less target now gets false.

Proven end-to-end in two processes (spike/native/aot-demo.janet): build with cc,
then deploy with cc removed from PATH -> count-point still native, mandelbrot
3288753 at 12.4ms (full 18x). Test: test/integration/cgen-aot-test.janet. Default
path unchanged; the modes are opt-in. Gate green (118 files).

Remaining for a literal single binary: fuse the .so + manifest into the jpm exe.

Co-authored-by: Yogthos <yogthos@gmail.com>
This commit is contained in:
Dmitri Sotnikov 2026-06-16 18:09:22 +00:00 committed by GitHub
parent ce3c7df24b
commit bffb492c1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 282 additions and 43 deletions

View file

@ -0,0 +1,59 @@
# Native codegen AOT build/deploy (jolt-a7ds): compile an app's numeric-leaf fns
# into ONE native module at build time, then deploy with NO cc — load the
# prebuilt module and install the cfunctions as var roots. Proves the build-time
# path that removes the runtime-toolchain dependency. Skips where cc/janet.h are
# absent.
(import ../../src/jolt/api :as api)
(import ../../src/jolt/cgen :as cgen)
(print "Native codegen AOT build/deploy (jolt-a7ds)...")
(var failures 0)
(defn check [label ok] (unless ok (++ failures) (eprintf " FAIL: %s" label)))
(def cp-src "(defn count-point [cr ci cap] (loop [i 0 zr 0.0 zi 0.0] (if (or (>= i cap) (> (+ (* zr zr) (* zi zi)) 4.0)) i (recur (inc i) (+ (- (* zr zr) (* zi zi)) cr) (+ (* 2.0 (* zr zi)) ci)))))")
(def run-src "(defn run [n] (let [cap 200 nd (* 1.0 n)] (loop [y 0 acc 0] (if (< y n) (let [ci (- (/ (* 2.0 y) nd) 1.0) row (loop [x 0 a 0] (if (< x n) (let [cr (- (/ (* 2.0 x) nd) 1.5)] (recur (inc x) (+ a (count-point cr ci cap)))) a))] (recur (inc y) (+ acc row))) acc))))")
(if (cgen/toolchain-available?)
(do
# --- build phase: collect numeric-leaf fns, compile one module, write manifest
(def bctx (api/init-cached {:compile? true}))
(put (bctx :env) :direct-linking? true)
(put (bctx :env) :cgen-collect? true)
(api/eval-string bctx "(ns aot)")
(api/eval-string bctx cp-src)
(api/eval-string bctx run-src)
(def collected (get (bctx :env) :cgen-collected))
(check "collected exactly the numeric-leaf fn (count-point, not run)"
(and collected (= 1 (length collected))
(= "count-point" ((first collected) :name))))
(def manifest-path (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-aot-test.jdn"))
(def build (cgen/aot-build collected {:dir "build/cgen-aot-test"}))
(check "aot-build produced a module" (and build (build :sopath)))
(cgen/write-manifest manifest-path build)
# --- deploy phase: fresh ctx, load prebuilt (NO cc), install roots, run
(def dctx (api/init-cached {:compile? true}))
(put (dctx :env) :direct-linking? true)
(def prebuilt (cgen/load-aot manifest-path))
(check "load-aot maps the qname to a cfunction"
(cfunction? (get prebuilt "aot/count-point")))
(put (dctx :env) :cgen-prebuilt prebuilt)
(api/eval-string dctx "(ns aot)")
(api/eval-string dctx cp-src)
(api/eval-string dctx run-src)
(check "deployed count-point root is the prebuilt cfunction"
(cfunction? (api/eval-string dctx "count-point")))
(check "deployed run stays bytecode" (not (cfunction? (api/eval-string dctx "run"))))
(check "AOT-deployed mandelbrot computes the right total"
(= 3288753 (api/eval-string dctx "(run 200)")))
# a fn NOT in the manifest must stay bytecode in deploy
(api/eval-string dctx "(defn sq [x] (* x x))")
(check "unlisted numeric-leaf stays bytecode in deploy (no cc)"
(not (cfunction? (api/eval-string dctx "sq")))))
(print " (toolchain absent — skipping AOT legs)"))
(if (= 0 failures)
(print "All tests passed.")
(do (eprintf "%d cgen-aot check(s) failed" failures) (os/exit 1)))