A typo'd symbol used to auto-intern an unbound var and die later as
'Cannot call nil as a function' with no hint which symbol. Now:
$ jolt -e '(undefined-fn 1)'
Error: Unable to resolve symbol: undefined-fn in this context
The analyzer's :unresolved fallthrough now punts to the interpreter
(whose resolver raises the message above when the form runs) instead of
emitting a var-ref that interned the var. A punt rather than a hard
throw because runtime-interning forms (defmulti's setup) legitimately
reference the var they're about to create from a nested do.
Pulling that thread surfaced three real bugs the leniency was masking:
- h-resolve-global resolved unqualified symbols against ctx-current-ns,
which during analysis is jolt.analyzer — so user-ns vars NEVER
resolved through it; the lenient arm happened to emit the right ns.
Now resolves against the compile ns like the qualified branch.
- Top-level (do ...) wasn't split: Clojure compiles and EVALS each
child in sequence so earlier children's runtime effects (defmulti's
intern) are visible while later children compile. eval-toplevel now
splits.
- The stdlib itself had forward references the auto-intern hid:
10-seq's transducers used vreset!/vswap! from 20-coll (moved to
10-seq); in 20-coll qualified-ident?/realized?/list*/underive
referenced defs declared later in the file (reordered); sorted? and
partition-all are genuinely later-tier and got (declare ...).
Test rows updated where they encoded the old leniency: ir-passes'
dead-branch row (unresolved in a dead branch is an error, as in
Clojure), compile-mode's ctx-isolation row (other ctx now errors
instead of reading nil), cli rows assert the new message. Gate green,
conformance 335/335 x3, suite 4718 steady, bench within noise.
42 lines
2 KiB
Text
42 lines
2 KiB
Text
# IR pass pipeline (jolt-2om, nanopass-lite): jolt.passes/run-passes applies
|
|
# pure IR->IR rewrites between the analyzer and the back end. The first pass
|
|
# is constant folding — it computes with the ACTUAL jolt fns, so folded
|
|
# results match runtime semantics by construction.
|
|
(import ../../src/jolt/api :as api)
|
|
(import ../../src/jolt/backend :as backend)
|
|
(import ../../src/jolt/reader :as reader)
|
|
|
|
(print "IR passes (constant folding)...")
|
|
(def ctx (api/init-cached {:compile? true}))
|
|
(defn ir [src] (backend/analyze-form ctx (reader/parse-string src)))
|
|
|
|
(defn check-const [src want]
|
|
(def n (ir src))
|
|
(assert (= :const (n :op)) (string src " folds to a constant"))
|
|
(assert (= want (n :val)) (string src " folds to " (string/format "%q" want))))
|
|
|
|
(check-const "(+ 1 2 3)" 6)
|
|
(check-const "(* 2 (+ 3 4))" 14)
|
|
(check-const "(quot 7 2)" 3)
|
|
(check-const "(mod -7 3)" 2)
|
|
(check-const "(if (< 1 2) :yes :no)" :yes)
|
|
# dead-branch elimination: the untaken branch never evaluates (it must still
|
|
# RESOLVE — unresolved symbols are analysis errors as in Clojure, jolt-2o7.3)
|
|
(check-const "(if false (throw (ex-info \"boom\" {})) 2)" 2)
|
|
# an unresolvable symbol errors even in a dead branch (analysis precedes folding)
|
|
(assert (not ((protect (ir "(if false (this-would-not-resolve) 2)")) 0))
|
|
"unresolved symbol errors even in a dead branch")
|
|
|
|
# non-constants stay calls; folding must be conservative. `xq` is a real var
|
|
# (defined here) so it resolves, but its VALUE must not be folded in.
|
|
(api/eval-string ctx "(def xq 1)")
|
|
(assert (= :invoke ((ir "(+ xq 2)") :op)) "var ref stays a call")
|
|
(assert (= :invoke ((ir "(mod xq 0)") :op)) "non-const args stay calls")
|
|
# a fold that would THROW is left for runtime
|
|
(assert (= :invoke ((ir "(mod 5 0)") :op)) "throwing fold left to runtime")
|
|
|
|
# and the folded code evaluates identically (3-mode conformance covers the
|
|
# broader matrix; this pins a couple end-to-end)
|
|
(assert (= 6 (api/eval-string ctx "(+ 1 2 3)")) "folded eval")
|
|
(assert (= :yes (api/eval-string ctx "(if (< 1 2) :yes :no)")) "folded if eval")
|
|
(print "IR passes passed!")
|