jolt/test/integration/cgen-test.janet
Dmitri Sotnikov ce3c7df24b
cgen: content-address and cache the compiled .so (jolt-ihdp) (#147)
compile-fn now keys the .so on a hash of the generated C + the Janet ABI + the
platform, in a persistent cache dir (default jolt-cgen under TMPDIR, override
with JOLT_CGEN_CACHE_DIR; JOLT_CGEN_NO_CACHE=1 forces a rebuild). cc runs only on
the first build of a given fn; later runs with the same source reuse the cached
.so, so the per-startup compile cost is paid once.

mandelbrot 100 whole-process wall: cold ~0.71s -> warm ~0.21s (the ~0.5s cc
cost). These cache knobs don't shape output, so they stay out of
ctx-shaping-env-vars (same as the image-cache knobs). Test asserts the .so is
content-addressed and a second compile hits the cache without the source .c.

Co-authored-by: Yogthos <yogthos@gmail.com>
2026-06-16 17:37:35 +00:00

86 lines
4.3 KiB
Text

# Native codegen (jolt-ihdp): IR -> C for numeric-leaf fns. Pins that the C
# translator (1) classifies candidates correctly and (2) produces a native fn
# whose results match the bytecode/interpreted fn over the mandelbrot grid.
# Skips cleanly where the C toolchain (cc + janet.h) is absent — the rest of the
# gate still runs.
(import ../../src/jolt/api :as api)
(import ../../src/jolt/backend :as backend)
(import ../../src/jolt/reader :as reader)
(import ../../src/jolt/cgen :as cgen)
(print "Native codegen IR->C (jolt-ihdp)...")
(def ctx (api/init-cached {:compile? true}))
(put (ctx :env) :direct-linking? true)
(put (ctx :env) :inline? true)
(api/eval-string ctx "(ns cgentest)")
(defn ir-of [src] (backend/analyze-form ctx (reader/parse-string src)))
(def count-point-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)))))")
(var failures 0)
(defn check [label ok] (unless ok (++ failures) (eprintf " FAIL: %s" label)))
# --- classification ---
(check "count-point is a numeric leaf" (cgen/numeric-leaf? (ir-of count-point-src)))
(check "fn calling a non-native fn is NOT a leaf"
(not (cgen/numeric-leaf? (ir-of "(defn f [x] (str x))"))))
(check "fn building a collection is NOT a leaf"
(not (cgen/numeric-leaf? (ir-of "(defn f [x] [x x])"))))
(check "plain numeric expr fn IS a leaf"
(cgen/numeric-leaf? (ir-of "(defn sq [x] (* x x))")))
# --- C generation is well-formed (smoke: contains the wrapper + a loop) ---
(def c-src (cgen/gen-c-fn (ir-of count-point-src) "count_point"))
(check "emits a cfunction wrapper" (string/find "cfun_count_point" c-src))
(check "lowers the loop to a C while" (string/find "while (" c-src))
(check "unboxes params with janet_getnumber" (string/find "janet_getnumber" c-src))
# --- behavioral equivalence (only where the toolchain is present) ---
(if (cgen/toolchain-available?)
(do
# start from a clean cache dir so the content-addressed-file count is exact
(when (os/stat "build/cgen-test")
(each f (os/dir "build/cgen-test") (os/rm (string "build/cgen-test/" f))))
(api/eval-string ctx count-point-src)
(def bc-cp (api/eval-string ctx "count-point")) # the compiled/interpreted fn
(def c-cp (cgen/compile-fn (ir-of count-point-src)
{:dir "build/cgen-test" :name "count_point_test"}))
(defn callable? [x] (or (function? x) (cfunction? x)))
(check "compile-fn returns a callable" (callable? c-cp))
(when (callable? c-cp)
# spot points spanning escape-fast .. in-set
(var ptmatch true)
(each [cr ci] [[2.0 2.0] [0.5 0.5] [-0.5 0.6] [0.28 0.0] [-0.74 0.1] [-0.5 0.0] [0.0 0.0]]
(unless (= (c-cp cr ci 200) (bc-cp cr ci 200)) (set ptmatch false)))
(check "C count-point matches bytecode on sample points" ptmatch)
# full mandelbrot grid total
(defn grid-total [cp n]
(def cap 200) (def nd (* 1.0 n)) (var acc 0) (var y 0)
(while (< y n)
(def ci (- (/ (* 2.0 y) nd) 1.0)) (var x 0) (var a 0)
(while (< x n)
(def cr (- (/ (* 2.0 x) nd) 1.5)) (set a (+ a (cp cr ci cap))) (++ x))
(set acc (+ acc a)) (++ y))
acc)
(check "C and bytecode agree on the full n=80 grid total"
(= (grid-total c-cp 80) (grid-total bc-cp 80)))
# caching: the .so is content-addressed, so a second compile of the same
# fn reuses it. Drop the generated .c (keep the .so) then recompile: a
# cache hit loads the existing .so without needing cc/source again.
(def cache-files (filter |(string/has-suffix? ".so" $) (os/dir "build/cgen-test")))
(check "produced a content-addressed .so" (= 1 (length cache-files)))
(each f (os/dir "build/cgen-test")
(when (string/has-suffix? ".c" f) (os/rm (string "build/cgen-test/" f))))
(def c-cp2 (cgen/compile-fn (ir-of count-point-src)
{:dir "build/cgen-test" :name "count_point_test"}))
(check "cache hit recompiles without the source .c"
(and (callable? c-cp2) (= (c-cp2 -0.5 0.0 200) 200)))))
(print " (toolchain absent — skipping behavioral equivalence)"))
(if (= 0 failures)
(print "All tests passed.")
(do (eprintf "%d cgen check(s) failed" failures) (os/exit 1)))