self-host: make it the only compile path; drop the bootstrap as a selectable path
eval-toplevel now routes every compiled form through backend/compile-and-eval (analyzer -> IR -> Janet back end). The bootstrap compiler (compile-ast) stays only to bootstrap jolt.ir/jolt.analyzer in backend/compile-load. Dropped the :selfhost? flag and JOLT_SELFHOST env plumbing — self-host is the default. Three correctness fixes the full-suite-under-self-host sweep exposed: - analyzer/back end: recur into a variadic arity now compiles. The arity fn gets an ordinary positional rest param (the collected seq), with a dispatch wrapper, so (recur fixed... rest-seq) is a clean self-call like Clojure -- instead of punting to the interpreter, which hangs on this (jolt-4df). - host_iface: interop heads (.foo/.-field/Foo.) and ns-management forms (all-ns, create-ns, resolve, ...) now mark uncompilable so they fall back instead of compiling to a bad call. - back end: named-fn self-reference ((fn self [n] ... (self ...))) binds the fn name via a var. Full unit+integration suite green; clojure-test-suite battery holds 3913.
This commit is contained in:
parent
34b880f0d6
commit
497970029d
7 changed files with 93 additions and 27 deletions
|
|
@ -32,7 +32,8 @@
|
|||
opts may contain:
|
||||
:namespaces — map of {ns-name → {sym → value, ...}, ...}
|
||||
:mutable? — use Janet mutable data structures instead of persistent
|
||||
:compile? — enable compilation of Clojure forms to Janet
|
||||
:compile? — compile Clojure forms via the self-hosted pipeline (analyzer ->
|
||||
IR -> Janet back end), falling back to the interpreter as needed
|
||||
:paths — extra source roots to search for namespaces (after the stdlib)"
|
||||
[&opt opts]
|
||||
(default opts {})
|
||||
|
|
|
|||
|
|
@ -49,15 +49,25 @@
|
|||
(array/push binds (emit ctx (in p 1))))
|
||||
['let (tuple/slice binds) (emit ctx (node :body))])
|
||||
|
||||
# A named Janet fn whose name is the arity's recur target, so recur is a
|
||||
# self-call (Janet tail-calls it).
|
||||
# An arity compiles to a named Janet fn whose name is its recur target, so recur
|
||||
# is a self-call (Janet tail-calls it). The rest param is an ORDINARY positional
|
||||
# param holding a seq (not Janet `&`), so `(recur fixed... rest-seq)` re-enters
|
||||
# the way Clojure recur into a variadic arity does (rebinds the rest seq directly,
|
||||
# no re-collection). The dispatch wrapper (emit-fn-body) collects the call's args.
|
||||
(defn- emit-arity-fn [ctx ar]
|
||||
(def ps @[])
|
||||
(each pn (vview (ar :params)) (array/push ps (symbol pn)))
|
||||
(when (ar :rest) (array/push ps '&) (array/push ps (symbol (ar :rest))))
|
||||
(if (ar :recur-name)
|
||||
['fn (symbol (ar :recur-name)) (tuple/slice ps) (emit ctx (ar :body))]
|
||||
['fn (tuple/slice ps) (emit ctx (ar :body))]))
|
||||
(when (ar :rest) (array/push ps (symbol (ar :rest))))
|
||||
['fn (symbol (ar :recur-name)) (tuple/slice ps) (emit ctx (ar :body))])
|
||||
|
||||
# Invoke an arity's fn with args pulled from the dispatch tuple: fixed params by
|
||||
# index, rest as a slice from n-fixed on.
|
||||
(defn- emit-arity-invoke [ctx ar jargs]
|
||||
(def nfixed (length (vview (ar :params))))
|
||||
(def call @[(emit-arity-fn ctx ar)])
|
||||
(for i 0 nfixed (array/push call ['in jargs i]))
|
||||
(when (ar :rest) (array/push call ['tuple/slice jargs nfixed]))
|
||||
(tuple/slice call))
|
||||
|
||||
(defn- emit-loop [ctx node]
|
||||
(def L (symbol (node :recur-name)))
|
||||
|
|
@ -86,24 +96,43 @@
|
|||
['defer (emit ctx (node :finally)) core]
|
||||
core))
|
||||
|
||||
(defn- emit-fn [ctx node]
|
||||
(defn- emit-fn-body [ctx node]
|
||||
(def arities (vview (node :arities)))
|
||||
(if (= 1 (length arities))
|
||||
(def multi (> (length arities) 1))
|
||||
(cond
|
||||
# Single fixed arity (the hot case): emit the arity fn directly — its name is
|
||||
# the recur target, no dispatch overhead.
|
||||
(and (not multi) (not ((first arities) :rest)))
|
||||
(emit-arity-fn ctx (first arities))
|
||||
# Multi-arity: dispatch on arg count; fixed arities match exactly, the
|
||||
# variadic one matches >= its fixed count. apply spreads the captured args
|
||||
# into the chosen arity fn (whose own & collects any rest).
|
||||
# Single variadic arity: a thin wrapper collects the call's args so the rest
|
||||
# seq can be built, then hands off to the arity fn.
|
||||
(not multi)
|
||||
(let [jargs (gsym)]
|
||||
['fn ['& jargs] (emit-arity-invoke ctx (first arities) jargs)])
|
||||
# Multi-arity: dispatch on arg count. Fixed arities match exactly; the (one)
|
||||
# variadic arity matches >= its fixed count.
|
||||
(let [jargs (gsym)
|
||||
nsym (gsym)
|
||||
cf @['cond]]
|
||||
(each ar arities
|
||||
(def nfixed (length (vview (ar :params))))
|
||||
(array/push cf (if (ar :rest) [>= nsym nfixed] [= nsym nfixed]))
|
||||
(array/push cf [apply (emit-arity-fn ctx ar) jargs]))
|
||||
(array/push cf (emit-arity-invoke ctx ar jargs)))
|
||||
(array/push cf ['error "wrong number of args passed to fn"])
|
||||
['fn ['& jargs]
|
||||
['do ['def nsym ['length jargs]] (tuple/slice cf)]])))
|
||||
|
||||
# A named fn (fn self [..] .. (self ..)) references itself by name. The analyzer
|
||||
# binds that name as a local; bind it here to the fn value via a var (set before
|
||||
# any call, so the captured closure sees it — same scheme as emit-loop). recur
|
||||
# stays a separate self-call to the arity fn; this only covers by-name self-refs.
|
||||
(defn- emit-fn [ctx node]
|
||||
(def body (emit-fn-body ctx node))
|
||||
(if (node :name)
|
||||
(let [s (symbol (node :name))]
|
||||
['do ['var s nil] ['set s body] s])
|
||||
body))
|
||||
|
||||
(defn- direct-call? [fnode]
|
||||
(case (fnode :op) :var true :local true :fn true :host true false))
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@
|
|||
"alter-meta!" "reset-meta!" "disj" "set?" "satisfies?"
|
||||
"protocol-dispatch" "register-method" "make-reified" "prefer-method"
|
||||
"remove-method" "remove-all-methods" "get-method" "methods"
|
||||
# ns-management forms dispatched by the interpreter (not core vars)
|
||||
"create-ns" "remove-ns" "find-ns" "all-ns" "the-ns" "resolve"
|
||||
"ns-resolve" "ns-aliases" "ns-imports" "ns-interns"
|
||||
"read-string" "macroexpand-1" "defonce" "ns" "in-ns" "require"
|
||||
"import" "use" "refer" "defrecord" "defprotocol" "definterface"
|
||||
"reify" "proxy" "extend-type" "extend-protocol" "extend" "gen-class"
|
||||
|
|
@ -72,7 +75,18 @@
|
|||
(put t n true))
|
||||
t))
|
||||
|
||||
(defn h-special? [name] (if (get special-names name) true false))
|
||||
# Interop-shaped heads the interpreter lowers but the back end doesn't model:
|
||||
# (.method obj …) / (.-field obj) — member access (name starts with ".")
|
||||
# (Foo. …) — constructor (name ends with "." )
|
||||
# Treated as special so the analyzer marks them uncompilable and falls back.
|
||||
(defn- interop-head? [name]
|
||||
(def n (length name))
|
||||
(and (> n 1)
|
||||
(or (= (string/slice name 0 1) ".")
|
||||
(= (string/slice name (- n 1)) "."))))
|
||||
|
||||
(defn h-special? [name]
|
||||
(if (or (get special-names name) (interop-head? name)) true false))
|
||||
|
||||
# The namespace being compiled. NOT ctx-current-ns directly: the interpreter
|
||||
# rebinds current-ns to a fn's defining ns while that fn runs, so an interpreted
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
# Supports in-memory bytecode caching when :compile? is enabled.
|
||||
|
||||
(use ./reader)
|
||||
(use ./compiler)
|
||||
(use ./evaluator)
|
||||
(import ./backend :as backend)
|
||||
|
||||
# Stateful / context-modifying forms always interpret: they mutate the context
|
||||
# (namespaces, macros, types, multimethods, dynamic vars, …) in ways the compiler
|
||||
|
|
@ -25,15 +25,12 @@
|
|||
|
||||
(defn eval-toplevel
|
||||
"Evaluate one top-level form for ctx, honoring :compile?. Stateful forms always
|
||||
interpret; otherwise the form is compiled and run, falling back to the
|
||||
interpreter when the compiler can't handle it. Only the compile step is guarded
|
||||
— runtime errors in compiled code propagate (no double-eval, no hidden errors)."
|
||||
interpret; otherwise the form runs through the self-hosted compile pipeline
|
||||
(portable Clojure analyzer -> IR -> Janet back end), which falls back to the
|
||||
interpreter for forms it can't compile. Only the compile step is guarded —
|
||||
runtime errors in compiled code propagate (no double-eval, no hidden errors)."
|
||||
[ctx form]
|
||||
(defn try-compile []
|
||||
(let [compiled (protect (compile-ast form ctx))]
|
||||
(if (compiled 0)
|
||||
(eval-compiled (compiled 1) ctx)
|
||||
(eval-form ctx @{} form))))
|
||||
(defn try-compile [] (backend/compile-and-eval ctx form))
|
||||
(if (get (ctx :env) :compile?)
|
||||
(if (array? form)
|
||||
# A call/list: compile it unless its head is a stateful special form.
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@
|
|||
|
||||
(def jolt-version "0.1.0")
|
||||
|
||||
# Compile by default: the shipped runtime compiles each form to Janet bytecode
|
||||
# (hybrid — forms the compiler can't handle fall back to the interpreter, so the
|
||||
# result always matches the interpreter; see compiler.janet / loader/eval-toplevel).
|
||||
# Compile by default: the shipped runtime runs each form through the self-hosted
|
||||
# pipeline (portable Clojure analyzer -> IR -> Janet back end) to native bytecode
|
||||
# (hybrid — forms the analyzer can't compile fall back to the interpreter, so the
|
||||
# result always matches the interpreter; see backend.janet / loader/eval-toplevel).
|
||||
# Set JOLT_INTERPRET=1 to force the tree-walking interpreter (debugging / A-B).
|
||||
(def compile-default? (not (= "1" (os/getenv "JOLT_INTERPRET"))))
|
||||
(def ctx (init {:compile? compile-default?}))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue