feat(compile): Phase 2 — native ops + direct calls (fib30 50s -> 0.076s, ~660x)

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.
This commit is contained in:
Yogthos 2026-06-05 18:00:46 -04:00
parent f74af5dbc5
commit 3cf303578e
4 changed files with 59 additions and 16 deletions

View file

@ -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)))