* Make the benchmark harness build optimized binaries on Chez bench/run.sh was Janet-era: it invoked a 'jolt' binary and set JOLT_DIRECT_LINK/JOLT_WHOLE_PROGRAM, none of which exist on Chez, where 'joltc run -m' runs fully unoptimized (direct-link and inline default off). So the suite was measuring jolt's unoptimized path. run.sh now compiles each benchmark to an optimized AOT binary (joltc build --direct-link --opt) and times it against JVM Clojure on the same portable source, auto-detecting the Chez kernel dev files like build-smoke.sh. Adds bench/deps.edn so joltc resolves the namespaces, NO_JVM to skip the reference. mandelbrot.clj dropped its jolt.png require so the JVM reference can run it; the picture demo moved to mandelbrot_png.clj (jolt-only). README scorecard refreshed with current Chez numbers and the two-regime read (compute ~8-10x substrate floor; dispatch/alloc ~120-330x architectural gaps the passes don't touch). Stale 'jolt -m' header lines point at bench/run.sh. * Emit direct self-calls for named-fn self-recursion A self-recursive call to a named fn compiled to (jolt-invoke fib ...) instead of a direct (fib ...): emit-invoke handled a :local callee only when it was NOT a known proc, so a :local that IS in *known-procs* (the letrec-bound self-name) fell through to the :else jolt-invoke branch. Now a :local known proc emits a direct Scheme call — no jolt-invoke, no per-call arg-list consing; case-lambda handles arity. fib 30: 63.3ms -> 4.7ms (faster than JVM Clojure's 7.1ms; was 9x slower). The win is on every self-recursive non-loop fn, including the compiler's own. No semantic change — selfhost holds, make test green, shakesmoke/buildsmoke byte-identical. Re-mint (backend is seed). Corpus rows pin self-recursion across fixed/multi/ variadic arities. * Intern no-ns keywords without per-call allocation (keyword #f name) built a fresh combined-key string (string-append) on every call just to do the intern-table lookup — ~80 bytes of garbage per (:kw x), map literal, keyword arg, etc. A no-ns keyword now interns in a table keyed by the name string directly, so a lookup of an already-interned keyword is one hashtable-ref with no allocation. The ns table keeps the combined key; both share the keyword-t khash (equal-hash of the combined key) so hash values are unchanged. Small time win on its own (the field-read dispatch dominates hot record code — see jolt-unx4) but removes per-call keyword allocation everywhere. Runtime .ss, no re-mint; identity/=/hash unchanged, make test green. * Fast record field reads: single eq? scan, skip the get-arm walk (:field rec) / (get rec :field) lowers to (jolt-get rec kw), which walked the get-arm list to reach the jrec arm, then did jrec-has? + jrec-lookup — TWO linear scans, each comparing keys through the generic jolt=2 equality dispatcher. Field keys are interned keywords, so: - jrec-key=? compares a keyword query by eq? (jolt=2 only for non-keyword keys), - jrec-ref does ONE scan (vs has?+lookup) and runs a deftype's ILookup valAt only when the field is genuinely absent (present-nil still returns nil, not default), - jolt-get-dispatch checks jrec? first, skipping the get-arm walk for the hottest get target. jrec-lookup/jrec-has? (used by =, contains?, etc.) get the fast compare too. binary-trees 135x->18.9x, dispatch 121x->26.4x, mono-dispatch 327x->108x vs JVM. Runtime .ss (collections.ss + records.ss), no re-mint; make test + shakesmoke + buildsmoke green, record get/assoc/keys/=/count semantics unchanged. --------- Co-authored-by: Yogthos <yogthos@gmail.com>
125 lines
6.2 KiB
Scheme
125 lines
6.2 KiB
Scheme
;; Jolt value model on Chez Scheme.
|
|
;;
|
|
;; The irreducible value layer the self-hosted RT rests on. Maps Clojure's value
|
|
;; types onto Chez natives where possible, and adds records only where Chez lacks
|
|
;; a distinct type (nil sentinel, keywords, ns-bearing symbols). Loaded into an
|
|
;; env that has already (import (chezscheme)).
|
|
;;
|
|
;; Design notes:
|
|
;; - nil is a UNIQUE sentinel, distinct from #f and '() (the classic Lisp-on-Lisp
|
|
;; trap). jolt false -> Chez #f, jolt true -> #t.
|
|
;; - Chez's numeric tower IS Clojure's: long->exact integer, double->flonum,
|
|
;; ratio->exact rational, bigint->bignum. Clojure `=` is exactness-aware:
|
|
;; (= 1 1.0) is FALSE.
|
|
|
|
;; --- nil ---------------------------------------------------------------------
|
|
(define-record-type jolt-nil-t (fields) (nongenerative jolt-nil-v1))
|
|
(define jolt-nil (make-jolt-nil-t))
|
|
(define (jolt-nil? x) (jolt-nil-t? x))
|
|
|
|
;; --- truthiness: only nil and false are falsey -------------------------------
|
|
(define (jolt-truthy? x) (not (or (jolt-nil? x) (eq? x #f))))
|
|
|
|
;; --- keywords: interned so identity works; optional namespace ----------------
|
|
(define-record-type keyword-t (fields ns name khash) (nongenerative keyword-v1))
|
|
(define keyword-table (make-hashtable string-hash string=?))
|
|
;; The common no-ns keyword is interned in a table keyed by NAME directly, so a
|
|
;; lookup of an already-interned :kw (the hot case — every (:kw x), map literal,
|
|
;; keyword arg) is one hashtable-ref with NO allocation. The ns table keeps the
|
|
;; combined key. Both share the keyword-t khash (equal-hash of the combined key),
|
|
;; so hash values are unchanged.
|
|
(define keyword-table-bare (make-hashtable string-hash string=?))
|
|
;; NUL separator can't occur in a keyword ns/name, so the intern key is
|
|
;; unambiguous (a "/" separator would collide ns="a" name="b/c" with ns="a/b").
|
|
(define (keyword-intern-key ns name) (string-append (or ns "") "\x0;" name))
|
|
(define (keyword ns name)
|
|
(if ns
|
|
(let ((k (keyword-intern-key ns name)))
|
|
(or (hashtable-ref keyword-table k #f)
|
|
(let ((kw (make-keyword-t ns name (equal-hash k))))
|
|
(hashtable-set! keyword-table k kw)
|
|
kw)))
|
|
(or (hashtable-ref keyword-table-bare name #f)
|
|
(let ((kw (make-keyword-t #f name (equal-hash (keyword-intern-key #f name)))))
|
|
(hashtable-set! keyword-table-bare name kw)
|
|
kw))))
|
|
(define (keyword? x) (keyword-t? x))
|
|
|
|
;; --- symbols: ns + name + meta; NOT interned (meta varies), = by ns/name ------
|
|
(define-record-type symbol-t (fields ns name meta) (nongenerative symbol-v1))
|
|
(define (jolt-symbol ns name) (make-symbol-t ns name jolt-nil))
|
|
(define (jolt-symbol/meta ns name meta) (make-symbol-t ns name meta))
|
|
(define (jolt-symbol? x) (symbol-t? x))
|
|
|
|
;; chars/strings: Chez natives (strings treated immutable).
|
|
|
|
;; --- jolt equality (Clojure =) — scalars + collections ----------------------
|
|
;; A host shim registers a type's equality via register-eq-arm! instead of
|
|
;; set!-wrapping jolt=2 (cf. register-hash-arm!). An arm is (pred . handler), both
|
|
;; (a b): the arm applies when pred holds (typically either arg is the type), and
|
|
;; handler returns the #t/#f result. Arms are checked before the base scalar/coll
|
|
;; cases; the entry is stable.
|
|
(define jolt-eq-arms '())
|
|
(define (register-eq-arm! pred handler)
|
|
(set! jolt-eq-arms (cons (cons pred handler) jolt-eq-arms)))
|
|
(define (jolt=2-base a b)
|
|
(cond
|
|
((and (jolt-nil? a) (jolt-nil? b)) #t)
|
|
((or (jolt-nil? a) (jolt-nil? b)) #f)
|
|
((and (number? a) (number? b)) ; exactness-aware
|
|
(and (eq? (exact? a) (exact? b)) (= a b)))
|
|
((and (keyword-t? a) (keyword-t? b)) (eq? a b)) ; interned
|
|
((and (symbol-t? a) (symbol-t? b))
|
|
(and (equal? (symbol-t-ns a) (symbol-t-ns b))
|
|
(string=? (symbol-t-name a) (symbol-t-name b))))
|
|
((and (char? a) (char? b)) (char=? a b))
|
|
((and (string? a) (string? b)) (string=? a b))
|
|
((and (boolean? a) (boolean? b)) (eq? a b))
|
|
;; sequential (vector / list / lazy seq) compare element-wise, cross-type:
|
|
;; (= [1 2 3] (list 1 2 3)) is true. Forward to seq.ss (loaded by rt.ss).
|
|
((and (jolt-sequential? a) (jolt-sequential? b)) (seq=? a b))
|
|
((or (jolt-sequential? a) (jolt-sequential? b)) #f)
|
|
;; other collections (map/set): forward to collections.ss.
|
|
((and (jolt-coll? a) (jolt-coll? b)) (jolt-coll=? a b))
|
|
(else (eq? a b))))
|
|
(define (jolt=2 a b)
|
|
(let loop ((as jolt-eq-arms))
|
|
(cond ((null? as) (jolt=2-base a b))
|
|
(((caar as) a b) ((cdar as) a b))
|
|
(else (loop (cdr as))))))
|
|
(define (jolt= a . rest)
|
|
(let loop ((a a) (rest rest))
|
|
(cond ((null? rest) #t)
|
|
((jolt=2 a (car rest)) (loop (car rest) (cdr rest)))
|
|
(else #f))))
|
|
|
|
;; --- jolt hash — consistent with jolt= (for the HAMT) -----------------------
|
|
;; A host shim (records, host-table, inst-time, …) registers its type's hash via
|
|
;; register-hash-arm! instead of set!-wrapping jolt-hash — the arms are disjoint
|
|
;; types, checked before the base cases, so the full behavior is gathered here plus
|
|
;; the registry rather than scattered across a set! chain (cf. register-str-render!).
|
|
(define jolt-hash-arms '())
|
|
(define (register-hash-arm! pred handler)
|
|
(set! jolt-hash-arms (cons (cons pred handler) jolt-hash-arms)))
|
|
(define (jolt-hash-base x)
|
|
(cond
|
|
((jolt-nil? x) 0)
|
|
((keyword-t? x) (keyword-t-khash x))
|
|
((symbol-t? x) (equal-hash (cons (symbol-t-ns x) (symbol-t-name x))))
|
|
;; distinguish inexact from exact (1 and 1.0 are not jolt=); guard non-finite
|
|
;; (inexact->exact would error on NaN/inf)
|
|
((number? x) (if (exact? x) (equal-hash x)
|
|
(if (and (flonum? x) (or (nan? x) (infinite? x)))
|
|
(equal-hash (cons 'inexact (number->string x)))
|
|
(equal-hash (cons 'inexact (inexact->exact x))))))
|
|
((string? x) (string-hash x))
|
|
((char? x) (char->integer x))
|
|
((boolean? x) (if x 1 2))
|
|
((jolt-sequential? x) (seq-hash x)) ; vector/list/seq hash alike (forward to seq.ss)
|
|
((jolt-coll? x) (jolt-coll-hash x)) ; map/set; forward to collections.ss
|
|
(else (equal-hash x))))
|
|
(define (jolt-hash x)
|
|
(let loop ((as jolt-hash-arms))
|
|
(cond ((null? as) (jolt-hash-base x))
|
|
(((caar as) x) ((cdar as) x))
|
|
(else (loop (cdr as))))))
|