From 3cf303578eac065994bed95894759fbbf26dec72 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Fri, 5 Jun 2026 18:00:46 -0400 Subject: [PATCH] =?UTF-8?q?feat(compile):=20Phase=202=20=E2=80=94=20native?= =?UTF-8?q?=20ops=20+=20direct=20calls=20(fib30=2050s=20->=200.076s,=20~66?= =?UTF-8?q?0x)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes unlock native Janet speed in compile mode: - Hot numeric primitives (+ - * < > <= >=) emit as native Janet SYMBOLS rather than the variadic core fns, so Janet's compiler uses its arithmetic/compare opcodes. = / not= / quot / rem / mod / division stay as core fns (their semantics differ from Janet's). Trade-off: the strict non-number checks are relaxed under compilation (documented perf-mode divergence). - emit-invoke emits a DIRECT call (f arg...) when the callee is a function reference (core/local/symbol/fn), instead of wrapping every call in jolt-call. jolt-call is kept only for keyword/collection literals in call position ((:k m), ({:a 1} :a)) so IFn dispatch still works. compiled fib(30): 3.4s -> 0.076s (native ceiling), faster than jank's 0.8s. Updated compiler-test string assertions (core-+ -> +); compile-mode-test gains native-op + IFn-dispatch cases; README documents compile mode. jpm test green. --- README.md | 12 +++++++ src/jolt/compiler.janet | 45 +++++++++++++++++------- test/integration/compile-mode-test.janet | 12 ++++++- test/unit/compiler-test.janet | 6 ++-- 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 45a36ce..f649ca8 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,18 @@ hello 42 `(init)` returns a context with `clojure.core` loaded. Each context is isolated; use separate contexts for separate environments. +### Compilation + +By default Jolt tree-walks the interpreter. Passing `:compile?` compiles each form to Janet — `def`/`defn` persist in a per-context Janet environment and resolve across forms, hot numeric primitives (`+ - * < > <= >=`) emit native Janet ops, and function calls compile to direct calls (keyword/map/set still dispatch as IFn). For compute-heavy code this is dramatically faster — recursive `fib(30)` runs in ~0.08 s compiled vs ~50 s interpreted (≈600×), at native Janet speed: + +```janet +(def ctx (init {:compile? true})) +(eval-string ctx "(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))") +(eval-string ctx "(fib 30)") ; → 832040, fast +``` + +Compile mode is opt-in and still maturing: context-modifying forms (`ns`/`defmacro`/`deftype`/multimethods/…) always interpret, and the numeric-op inlining relaxes the strict non-number checks (e.g. `(< nil 1)`). Constructs the compiler doesn't yet handle fall back to errors rather than the interpreter (a hybrid fallback is planned). + ## Host interop Jolt exposes CLJS-style host interop through `.` on any Janet table or struct — a field holding a function is called with the receiver as the first argument: diff --git a/src/jolt/compiler.janet b/src/jolt/compiler.janet index 90684f5..3a3babe 100644 --- a/src/jolt/compiler.janet +++ b/src/jolt/compiler.janet @@ -27,18 +27,24 @@ (make-env jolt-runtime-env))) (def- core-renames - @{"+" "core-+" - "-" "core-sub" - "*" "core-*" + # Compile mode emits NATIVE Janet ops for the hot numeric primitives (+,-,* + # and the comparisons), which match Jolt's semantics for numbers and are + # ~10-20x faster than the variadic core fns. Trade-off: the strict non-number + # checks (e.g. (< nil 1) throwing) are relaxed under compilation — a + # documented perf-mode divergence. = / not= / quot / rem / mod / division stay + # as core fns (their semantics differ from Janet's). + @{"+" "+" + "-" "-" + "*" "*" "/" "core-/" "inc" "core-inc" "dec" "core-dec" "=" "core-=" "not=" "core-not=" - "<" "core-<" - ">" "core->" - "<=" "core-<=" - ">=" "core->=" + "<" "<" + ">" ">" + "<=" "<=" + ">=" ">=" "nil?" "core-nil?" "not" "core-not" "some?" "core-some?" @@ -732,9 +738,16 @@ (defn- emit-symbol-expr [name] (symbol name)) (defn- emit-local-expr [name] (symbol name)) +# Native Janet numeric ops: emit them as SYMBOLS (not inlined fn values) so +# Janet's compiler recognizes the primitive and uses its fast arithmetic/compare +# opcode rather than a function call. +(def- native-ops @{"+" true "-" true "*" true "<" true ">" true "<=" true ">=" true}) + (defn- emit-core-symbol-expr [janet-name] - (or (get core-fn-values janet-name) - (error (string "Core fn not found: " janet-name)))) + (if (get native-ops janet-name) + (symbol janet-name) + (or (get core-fn-values janet-name) + (error (string "Core fn not found: " janet-name))))) (defn- emit-qualified-symbol-expr [ns name] (error (string "Cannot eval qualified symbol at compile time: " ns "/" name))) @@ -809,9 +822,17 @@ (tuple/slice (tuple ;exprs))) (defn- emit-invoke-expr [f-ast args] - # Embed the jolt-call function value directly (like core symbols) so compiled - # invocations dispatch real fns AND IFn collections at runtime. - (def exprs @[jolt-call (emit-expr f-ast)]) + # Emit a DIRECT Janet call (f arg…) when the callee is a function reference — + # a core op/fn, a local/global symbol, or an fn literal — so native ops keep + # their fast opcodes and recursion is a direct call. Fall back to jolt-call + # only when the head is a keyword/collection literal in call position (an IFn + # that needs runtime lookup), e.g. (:k m) or ({:a 1} :a). + (def direct (case (f-ast :op) + :core-symbol true :symbol true :local true + :qualified-symbol true :fn true + false)) + (def f (emit-expr f-ast)) + (def exprs (if direct @[f] @[jolt-call f])) (each arg args (array/push exprs (emit-expr arg))) (tuple/slice (tuple ;exprs))) diff --git a/test/integration/compile-mode-test.janet b/test/integration/compile-mode-test.janet index 1b06cbc..b83aa75 100644 --- a/test/integration/compile-mode-test.janet +++ b/test/integration/compile-mode-test.janet @@ -88,7 +88,17 @@ (eval-string ctx "(defn sum-sq [a b] (+ (sq a) (sq b)))") (assert (= 25 (ct-eval ctx "(sum-sq 3 4)")) "defn calling earlier defn") (eval-string ctx "(def base 100)") - (assert (= 142 (ct-eval ctx "(+ base 42)")) "compiled def referenced later")) + (assert (= 142 (ct-eval ctx "(+ base 42)")) "compiled def referenced later") + + # Phase 2: native ops are emitted directly (fast), but IFn values in call + # position (keyword/map/set) still dispatch via the runtime. + (print " native ops + IFn dispatch...") + (assert (= 10 (ct-eval ctx "(+ 1 2 3 4)")) "n-ary +") + (assert (= true (ct-eval ctx "(< 1 2 3)")) "n-ary <") + (assert (= 1 (ct-eval ctx "(:a {:a 1})")) "keyword as fn") + (assert (= 1 (ct-eval ctx "({:a 1} :a)")) "map as fn") + (assert (= 2 (ct-eval ctx "(#{1 2 3} 2)")) "set as fn") + (assert (= true (ct-eval ctx "(= [1 2] [1 2])")) "= is value equality, not core-= bypass")) # Context isolation: a def in one compiled context is invisible in another. (let [a (init {:compile? true}) b (init {:compile? true})] diff --git a/test/unit/compiler-test.janet b/test/unit/compiler-test.janet index 7de1183..788e3ba 100644 --- a/test/unit/compiler-test.janet +++ b/test/unit/compiler-test.janet @@ -62,7 +62,7 @@ # ============================================================ (print "6: let...") (assert (= "(let [x 1] (core-inc x))" (compile-str "(let* [x 1] (inc x))")) "let single binding") -(assert (= "(let [x 1 y 2] (core-+ x y))" (compile-str "(let* [x 1 y 2] (+ x y))")) "let two bindings") +(assert (= "(let [x 1 y 2] (+ x y))" (compile-str "(let* [x 1 y 2] (+ x y))")) "let two bindings") (assert (= "(let [x (core-inc 1)] (core-inc x))" (compile-str "(let* [x (inc 1)] (inc x))")) "let with fn in binding") (print " passed") @@ -71,8 +71,8 @@ # ============================================================ (print "7: invoke...") (assert (= "(core-inc 1)" (compile-str "(inc 1)")) "inc call") -(assert (= "(core-+ 1 2)" (compile-str "(+ 1 2)")) "+ call") -(assert (= "(core-+ (core-inc 1) 2)" (compile-str "(+ (inc 1) 2)")) "nested calls") +(assert (= "(+ 1 2)" (compile-str "(+ 1 2)")) "+ call") +(assert (= "(+ (core-inc 1) 2)" (compile-str "(+ (inc 1) 2)")) "nested calls") (assert (= "(core-map core-inc (core-vec 1 2 3))" (compile-str "(map inc (vec 1 2 3))")) "multi-arg call") (print " passed")