self-host: working portable analyzer -> IR -> Janet back end pipeline
The Clojure-in-Clojure front end now compiles and runs a real subset end to end:
arithmetic, if/do/let, vector/map literals, def + name-based global refs, fn,
defn (via macroexpand), recursion through the var cell, multi-arity, variadic,
and higher-order calls. Proven by test/integration/self-host-test.janet.
Pieces:
- jolt-core/jolt/analyzer.clj — PORTABLE Clojure analyzer: reader form -> IR,
depending only on the host contract (jolt.host), never on Janet.
- src/jolt/host_iface.janet — Janet impl of the jolt.host contract (form
introspection + resolve/macro/current-ns), installed into each ctx.
- src/jolt/backend.janet — Janet back end: IR -> Janet form -> eval; resolves
name-based :var nodes to cells and reuses runtime helpers.
Host Janet code lives in src/jolt/ (not a host/janet/ dir): Janet resolves
relative imports per file, so cross-dir ../../src/jolt/* imports load second
instances of compiler/types/core and corrupt state. The portability boundary is
the jolt.host namespace contract + jolt-core/, not the directory.
Notes: gensym in the back end must not be Janet's (shadowed by Jolt's via use);
a jolt bug — (into #{} coll) doesn't add elements — was worked around with reduce
conj in the analyzer (filed separately).
This commit is contained in:
parent
b1ac427bdd
commit
849449aada
6 changed files with 438 additions and 0 deletions
|
|
@ -11,6 +11,7 @@
|
|||
(use ./loader)
|
||||
(use ./async)
|
||||
(import ./stdlib_embed :as stdlib-embed)
|
||||
(import ./host_iface :as host)
|
||||
|
||||
(defn normalize-pvecs
|
||||
"Deep-convert any sequential (pvec/tuple/array) to a Janet tuple. Test helper
|
||||
|
|
@ -53,6 +54,8 @@
|
|||
# clojure.core.async (channels + go blocks on Janet fibers); pre-populated
|
||||
# so (require '[clojure.core.async ...]) finds it and applies :as/:refer.
|
||||
(install-async! ctx)
|
||||
# Host contract (ns jolt.host): the seam the portable jolt-core compiler calls.
|
||||
(host/install! ctx)
|
||||
ctx))
|
||||
|
||||
(defn eval-one
|
||||
|
|
|
|||
141
src/jolt/backend.janet
Normal file
141
src/jolt/backend.janet
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# Janet back end: host-neutral IR (from jolt.analyzer) -> Janet form -> bytecode.
|
||||
#
|
||||
# Host-specific by definition (it targets Janet). It resolves name-based :var
|
||||
# nodes to Janet var cells and reuses runtime helpers (jolt-call, make-vec,
|
||||
# build-map-literal). The portable front end (jolt.analyzer) never sees any of
|
||||
# this; a different runtime provides its own back end against the same IR.
|
||||
#
|
||||
# In src/jolt/ (not host/janet/) for the same module-resolution reason as
|
||||
# host_iface — see that file's header.
|
||||
|
||||
(use ./types)
|
||||
(use ./core)
|
||||
(import ./compiler :as comp)
|
||||
(use ./evaluator)
|
||||
(import ./reader :as r)
|
||||
|
||||
# Var late-binding: deref/set through the cell via a memoized closure so compiled
|
||||
# code sees redefinition (Janet early-binds plain symbols). Same scheme as the
|
||||
# bootstrap compiler.
|
||||
(defn- var-getter [cell]
|
||||
(or (get cell :jolt/getter)
|
||||
(let [g (fn [] (var-get cell))] (put cell :jolt/getter g) g)))
|
||||
(defn- var-setter [cell]
|
||||
(or (get cell :jolt/setter)
|
||||
(let [s (fn [v] (bind-root cell v) cell)] (put cell :jolt/setter s) s)))
|
||||
|
||||
(defn- cell-for [ctx ns-name nm]
|
||||
(ns-intern (ctx-find-ns ctx ns-name) nm))
|
||||
|
||||
# Fresh Janet symbol for back-end-introduced bindings (arity dispatch). NOT
|
||||
# Janet's `gensym` — `(use ./core)` shadows it with Jolt's, which returns a jolt
|
||||
# symbol struct (invalid in a Janet param position).
|
||||
(var- gsym-counter 0)
|
||||
(defn- gsym [] (def s (symbol "_be$" gsym-counter)) (++ gsym-counter) s)
|
||||
|
||||
(var emit nil)
|
||||
|
||||
(defn- emit-seq [ctx node]
|
||||
(def out @['do])
|
||||
(each s (vview (node :statements)) (array/push out (emit ctx s)))
|
||||
(array/push out (emit ctx (node :ret)))
|
||||
(tuple/slice out))
|
||||
|
||||
(defn- emit-let [ctx node]
|
||||
(def binds @[])
|
||||
(each pair (vview (node :bindings))
|
||||
(def p (vview pair))
|
||||
(array/push binds (symbol (in p 0)))
|
||||
(array/push binds (emit ctx (in p 1))))
|
||||
['let (tuple/slice binds) (emit ctx (node :body))])
|
||||
|
||||
(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))))
|
||||
['fn (tuple/slice ps) (emit ctx (ar :body))])
|
||||
|
||||
(defn- emit-fn [ctx node]
|
||||
(def arities (vview (node :arities)))
|
||||
(if (= 1 (length arities))
|
||||
(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).
|
||||
(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 ['error "wrong number of args passed to fn"])
|
||||
['fn ['& jargs]
|
||||
['do ['def nsym ['length jargs]] (tuple/slice cf)]])))
|
||||
|
||||
(defn- direct-call? [fnode]
|
||||
(case (fnode :op) :var true :local true :fn true :host true false))
|
||||
|
||||
(defn- emit-invoke [ctx node]
|
||||
(def f (emit ctx (node :fn)))
|
||||
(def args (map |(emit ctx $) (vview (node :args))))
|
||||
(if (direct-call? (node :fn))
|
||||
(tuple f ;args)
|
||||
(tuple jolt-call f ;args)))
|
||||
|
||||
(defn- emit-vector [ctx node]
|
||||
(def items (map |(emit ctx $) (vview (node :items))))
|
||||
(tuple make-vec (tuple/slice (array/concat @['tuple] items))))
|
||||
|
||||
(defn- emit-map [ctx node]
|
||||
(def args @[comp/build-map-literal])
|
||||
(each pair (vview (node :pairs))
|
||||
(def p (vview pair))
|
||||
(array/push args (emit ctx (in p 0)))
|
||||
(array/push args (emit ctx (in p 1))))
|
||||
(tuple/slice args))
|
||||
|
||||
(set emit
|
||||
(fn emit [ctx node]
|
||||
(case (node :op)
|
||||
:const (node :val)
|
||||
:local (symbol (node :name))
|
||||
:host (symbol (node :name))
|
||||
:var (tuple (var-getter (cell-for ctx (node :ns) (node :name))))
|
||||
:if ['if (emit ctx (node :test)) (emit ctx (node :then)) (emit ctx (node :else))]
|
||||
:do (emit-seq ctx node)
|
||||
:throw ['error (emit ctx (node :expr))]
|
||||
:def (tuple (var-setter (cell-for ctx (node :ns) (node :name))) (emit ctx (node :init)))
|
||||
:let (emit-let ctx node)
|
||||
:fn (emit-fn ctx node)
|
||||
:invoke (emit-invoke ctx node)
|
||||
:vector (emit-vector ctx node)
|
||||
:map (emit-map ctx node)
|
||||
:quote ['quote (node :form)]
|
||||
(error (string "backend: unhandled op " (node :op))))))
|
||||
|
||||
(defn emit-ir
|
||||
"IR node -> Janet form (public entry for the back end)."
|
||||
[ctx node]
|
||||
(emit ctx node))
|
||||
|
||||
# --- pipeline wiring (the self-hosted compile path) ---
|
||||
|
||||
(defn- ensure-analyzer [ctx]
|
||||
# Load jolt.analyzer (and transitively jolt.ir) once; jolt.host is pre-installed
|
||||
# by host/install! so its require is a no-op.
|
||||
(when (= 0 (length ((ctx-find-ns ctx "jolt.analyzer") :mappings)))
|
||||
(eval-form ctx @{} (r/parse-string "(require '[jolt.analyzer])"))))
|
||||
|
||||
(defn analyze-form
|
||||
"Run the portable Clojure analyzer (jolt.analyzer/analyze) on a reader form,
|
||||
returning host-neutral IR."
|
||||
[ctx form]
|
||||
(ensure-analyzer ctx)
|
||||
(def av (ns-find (ctx-find-ns ctx "jolt.analyzer") "analyze"))
|
||||
((var-get av) ctx form))
|
||||
|
||||
(defn compile-and-eval
|
||||
"Self-hosted compile path: analyze (portable Clojure) -> IR -> Janet -> eval."
|
||||
[ctx form]
|
||||
(eval (emit-ir ctx (analyze-form ctx form)) (comp/ctx-janet-env ctx)))
|
||||
101
src/jolt/host_iface.janet
Normal file
101
src/jolt/host_iface.janet
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Janet implementation of the Jolt host contract (ns `jolt.host`).
|
||||
#
|
||||
# This is the seam between the portable jolt-core (analyzer/IR/core, pure Clojure
|
||||
# under jolt-core/) and the Janet runtime. jolt-core calls ONLY these functions —
|
||||
# never Janet directly. Re-hosting Jolt to another runtime means reimplementing
|
||||
# this contract (+ the back end and RT) for that runtime.
|
||||
#
|
||||
# Lives in src/jolt/ (with the rest of the Janet host) rather than a separate
|
||||
# host/janet/ dir: Janet resolves relative imports per-file, so a host/janet
|
||||
# module importing ../../src/jolt/* loads SECOND instances of compiler/types/core
|
||||
# (inconsistent state). The portability boundary is the `jolt.host` namespace
|
||||
# contract + jolt-core/, not the directory.
|
||||
#
|
||||
# Two groups:
|
||||
# 1. Form introspection — reader forms are host-specific (the reader is the
|
||||
# host's), so shape predicates/accessors live here. Returns jolt values the
|
||||
# analyzer walks with ordinary Clojure.
|
||||
# 2. Compile-time environment — resolve symbols to vars/macros, expand macros,
|
||||
# the current namespace. These take ctx (an opaque host handle).
|
||||
|
||||
(use ./types)
|
||||
(use ./evaluator)
|
||||
(use ./core)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Form introspection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
(defn h-sym? [form] (and (struct? form) (= :symbol (form :jolt/type))))
|
||||
(defn h-sym-name [form] (form :name))
|
||||
(defn h-sym-ns [form] (form :ns))
|
||||
|
||||
(defn h-list? [form] (array? form)) # a call / list (reader: array)
|
||||
(defn h-vector? [form] (tuple? form)) # a vector literal (reader: tuple)
|
||||
(defn h-map? [form] (and (struct? form) (nil? (form :jolt/type))))
|
||||
(defn h-set? [form] (and (struct? form) (= :jolt/set (form :jolt/type))))
|
||||
(defn h-char? [form] (and (struct? form) (= :jolt/char (form :jolt/type))))
|
||||
|
||||
(defn h-literal? [form]
|
||||
(or (nil? form) (boolean? form) (number? form) (string? form)
|
||||
(keyword? form) (h-char? form)))
|
||||
|
||||
# Items of a list/vector as a jolt vector, so the analyzer walks them with Clojure.
|
||||
(defn h-elements [form] (make-vec form))
|
||||
(defn h-vector-items [form] (make-vec form))
|
||||
(defn h-map-pairs [form]
|
||||
(make-vec (map (fn [k] (make-vec [k (get form k)])) (keys form))))
|
||||
(defn h-set-items [form] (make-vec (form :value)))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compile-time environment
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
(defn h-current-ns [ctx] (ctx-current-ns ctx))
|
||||
|
||||
(defn h-macro? [ctx sym]
|
||||
(let [v (resolve-var ctx @{} sym)]
|
||||
(if (and v (var-macro? v)) true false)))
|
||||
|
||||
(defn h-expand-1 [ctx form]
|
||||
(let [head (in form 0)
|
||||
v (resolve-var ctx @{} head)
|
||||
macro-fn (var-get v)]
|
||||
(apply macro-fn (tuple/slice form 1))))
|
||||
|
||||
# Classify a global (non-local) symbol reference:
|
||||
# {:kind :var :ns NS :name NAME} — a Jolt var (current ns / clojure.core)
|
||||
# {:kind :host :name NAME} — resolves only via the host env (+, int?, …),
|
||||
# same fallback the interpreter's resolve-sym uses
|
||||
# {:kind :unresolved :name NAME} — not yet defined (forward reference)
|
||||
(defn h-resolve-global [ctx sym]
|
||||
(let [v (resolve-var ctx @{} sym)]
|
||||
(if v
|
||||
{:kind :var :ns (var-ns v) :name (var-name v)}
|
||||
(let [nm (sym :name)
|
||||
entry (in (fiber/getenv (fiber/current)) (symbol nm))]
|
||||
(if (not (nil? entry))
|
||||
{:kind :host :name nm}
|
||||
{:kind :unresolved :name nm})))))
|
||||
|
||||
(defn h-intern! [ctx ns-name nm]
|
||||
(ns-intern (ctx-find-ns ctx ns-name) nm)
|
||||
nil)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Installation: bind these fns as vars in the `jolt.host` namespace so jolt-core
|
||||
# can call them. Idempotent per context.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
(def- exports
|
||||
{"sym?" h-sym? "sym-name" h-sym-name "sym-ns" h-sym-ns
|
||||
"list?" h-list? "vector?" h-vector? "map?" h-map? "set?" h-set? "char?" h-char?
|
||||
"literal?" h-literal? "elements" h-elements "vector-items" h-vector-items
|
||||
"map-pairs" h-map-pairs "set-items" h-set-items
|
||||
"current-ns" h-current-ns "macro?" h-macro? "expand-1" h-expand-1
|
||||
"resolve-global" h-resolve-global "intern!" h-intern!})
|
||||
|
||||
(defn install! [ctx]
|
||||
(def ns (ctx-find-ns ctx "jolt.host"))
|
||||
(eachp [nm f] exports (ns-intern ns nm f))
|
||||
ns)
|
||||
Loading…
Add table
Add a link
Reference in a new issue