errors: unresolved symbols error with Clojure's message (jolt-2o7.3)
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.
This commit is contained in:
parent
3fd0af33b6
commit
c230e70ed7
9 changed files with 133 additions and 64 deletions
|
|
@ -50,9 +50,16 @@
|
|||
(check "arity error names the fn"
|
||||
(run-err "-e" "(defn afn [x] x) (afn 1 2)")
|
||||
(has "Wrong number of args (2) passed to: user/afn"))
|
||||
(check "nil-call hints at undefined symbol"
|
||||
(check "nil-call (nil value) keeps the hint"
|
||||
(run-err "-e" "(def x nil) (x 1)")
|
||||
(has "Cannot call nil as a function"))
|
||||
# round 3: typos die at resolve time with Clojure's message, not as nil-calls
|
||||
(check "unresolved symbol named at resolve time"
|
||||
(run-err "-e" "(undefined-fn 1)")
|
||||
(has "undefined (misspelled?) symbol"))
|
||||
(has "Unable to resolve symbol: undefined-fn in this context"))
|
||||
(check "typo inside fn body also resolves to the message"
|
||||
(run-err "-e" "(defn f [] (no-such 1)) (f)")
|
||||
(has "Unable to resolve symbol: no-such"))
|
||||
(check "trace shows the user's call chain"
|
||||
(run-err "-e" "(defn inner [x] (let [r (+ x :k)] r)) (defn outer [x] (let [v (inner x)] v)) (outer 1)")
|
||||
(fn [s] (and (string/find "at user/inner" s) (string/find "at user/outer" s))))
|
||||
|
|
|
|||
|
|
@ -124,12 +124,15 @@
|
|||
"runtime error in compiled code propagates"))
|
||||
|
||||
# Context isolation: a def in one compiled context is invisible in another. With
|
||||
# var-indirection each context has its own var cells, so b's `secret` is a
|
||||
# distinct, unbound var (nil) rather than a's 7.
|
||||
# var-indirection each context has its own var cells, so in b `secret` does not
|
||||
# resolve at all ('Unable to resolve symbol', jolt-2o7.3) rather than seeing a's 7.
|
||||
(let [a (init-cached {:compile? true}) b (init-cached {:compile? true})]
|
||||
(eval-string a "(def secret 7)")
|
||||
(assert (= 7 (ct-eval a "secret")) "def visible in its own ctx")
|
||||
(assert (nil? (ct-eval b "secret")) "def isolated to its ctx"))
|
||||
(def r (protect (ct-eval b "secret")))
|
||||
(assert (and (not (r 0))
|
||||
(string/find "Unable to resolve symbol: secret" (string/format "%q" (r 1))))
|
||||
"def isolated to its ctx (unresolved there)"))
|
||||
|
||||
# Redefinition is visible to already-compiled callers (var-indirection).
|
||||
(let [c (init-cached {:compile? true})]
|
||||
|
|
|
|||
|
|
@ -20,12 +20,18 @@
|
|||
(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
|
||||
(check-const "(if false (this-would-not-resolve) 2)" 2)
|
||||
# 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
|
||||
(assert (= :invoke ((ir "(+ x 2)") :op)) "free var stays a call")
|
||||
(assert (= :invoke ((ir "(mod x 0)") :op)) "non-const args stay calls")
|
||||
# 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")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue