feat: negative/never types — calling a non-function, wrong-arity (jolt-wwy)
Two provably-wrong cases the inference already has the facts for, closing the
last RFC 0006 open question:
- Calling a non-function. At an :invoke whose callee is provably :num or :str
(the only non-callable types — keywords/maps/vectors/sets are IFn), report
"cannot call a number as a function". Default level (no closed-world: the
callee type is inferred at the call site). Covers (5 1), ("hi" 0),
((+ 1 2) :k), a let-bound number, and a var holding a number (via vtype-box
in direct-link). A union is non-callable only when every member is, so
((if c 1 :k) x) is accepted (:kw is callable). Verified zero false positives
on the ray tracer, which calls maps/keywords/vectors as fns throughout.
- Wrong arity to a user fn. The registered single-fixed-arity sig (jolt-zo1)
makes a mismatched arg count provably throw; reported under the
JOLT_TYPE_CHECK_USER opt-in (same closed-world boundary; ^:redef/variadic
skipped). Caught at compile time before the runtime arity error.
Both fold into the existing infer walk, carry :pos for file:line:col, and keep
no-false-positives. Gate green, suite 4718, conformance 335/335, runtime bench
even (compile-time only).
This commit is contained in:
parent
e071d09170
commit
328f88636e
3 changed files with 100 additions and 23 deletions
|
|
@ -860,7 +860,7 @@
|
|||
(def ^:private elem-fns #{"rand-nth" "first" "peek" "last" "nth" "fnext" "second"})
|
||||
|
||||
;; the checker's emission points, defined after infer but referenced from it
|
||||
(declare check-invoke check-user-call register-user-fn!)
|
||||
(declare check-invoke check-user-call register-user-fn! not-callable? type-name)
|
||||
|
||||
(defn- var-key [fnode] (str (get fnode :ns) "/" (get fnode :name)))
|
||||
|
||||
|
|
@ -1044,7 +1044,12 @@
|
|||
;; everything else: type args, collect the call (var callee), use the
|
||||
;; declared/estimated return type. range produces a numeric vector.
|
||||
:else
|
||||
(let [fnode' (if iscall-var fnode (nth (infer fnode tenv) 1))
|
||||
(let [fr (when (not iscall-var) (infer fnode tenv))
|
||||
fnode' (if iscall-var fnode (nth fr 1))
|
||||
;; the callee's value type: a var's from vtype-box (a fn is
|
||||
;; :truthy, a def carries its inferred type), else the inferred
|
||||
;; type of the callee expression (jolt-wwy)
|
||||
callee-t (if iscall-var (get @vtype-box (var-key fnode)) (nth fr 0))
|
||||
ares (mapv (fn [a] (infer a tenv)) args)]
|
||||
(when iscall-var
|
||||
(swap! calls-box conj [(var-key fnode) (mapv (fn [r] (nth r 0)) ares)]))
|
||||
|
|
@ -1054,6 +1059,11 @@
|
|||
(when @checking?
|
||||
(let [ats (mapv (fn [r] (nth r 0)) ares) pos (get node :pos)]
|
||||
(when cn (check-invoke cn args ats pos))
|
||||
;; calling a provably non-function (jolt-wwy)
|
||||
(when (not-callable? callee-t)
|
||||
(swap! diag-box conj
|
||||
{:op :call :type (type-name callee-t) :pos pos
|
||||
:msg (str "cannot call " (type-name callee-t) " as a function")}))
|
||||
(when (and @strict-box iscall-var)
|
||||
(let [k (var-key fnode) usig (get @user-sig-box k)]
|
||||
(when usig (check-user-call k usig ats pos))))))
|
||||
|
|
@ -1128,6 +1138,15 @@
|
|||
(every? not-seqable? (umembers t))
|
||||
(or (= t :num) (= t :kw))))
|
||||
|
||||
;; concrete non-callable values (jolt-wwy): calling them throws "Cannot call X
|
||||
;; as a function". Only :num and :str — keywords/maps/vectors/sets are IFn,
|
||||
;; :truthy/:any/:nil are ambiguous (accepted). A union is non-callable only when
|
||||
;; every member is.
|
||||
(defn- not-callable? [t]
|
||||
(if (union-type? t)
|
||||
(every? not-callable? (umembers t))
|
||||
(or (= t :num) (= t :str))))
|
||||
|
||||
;; arithmetic / numeric ops: EVERY argument must be a number.
|
||||
(def ^:private num-ops
|
||||
#{"+" "-" "*" "/" "inc" "dec" "mod" "rem" "quot" "min" "max" "abs"
|
||||
|
|
@ -1213,11 +1232,13 @@
|
|||
:params (get ar :params) :body (get ar :body)}))))))))
|
||||
|
||||
(defn- check-user-call
|
||||
"Strict mode: report a call whose concrete argument type provably makes the
|
||||
callee body throw. For each concrete arg, re-check the body with ONLY that
|
||||
parameter bound to its arg type (others :any); a diagnostic the all-:any body
|
||||
did not already have means the argument alone is provably wrong. Monotonic —
|
||||
binding a concrete type can only ADD error-domain hits — so no false positive.
|
||||
"Strict mode: report a call to a registered user fn that provably throws —
|
||||
either a WRONG ARITY (the registered fn has one fixed arity, so a different
|
||||
arg count always throws, jolt-wwy) or an argument whose concrete type the body
|
||||
rejects. For the latter, re-check the body with ONLY that parameter bound to
|
||||
its arg type (others :any); a diagnostic the all-:any body did not already
|
||||
have means the argument alone is provably wrong. Monotonic — binding a
|
||||
concrete type can only ADD error-domain hits — so no false positive.
|
||||
Cycle-guarded so mutually recursive fns terminate."
|
||||
[key sig arg-types pos]
|
||||
(when (not (contains? @checking-box key))
|
||||
|
|
@ -1225,23 +1246,30 @@
|
|||
(reset! checking-box (conj prev key))
|
||||
(let [params (:params sig)
|
||||
body (:body sig)
|
||||
base (isolated-diag-count body (all-any-env params))
|
||||
npar (count params)
|
||||
nargs (count arg-types)
|
||||
np (if (< npar nargs) npar nargs)]
|
||||
(reduce
|
||||
(fn [_ i]
|
||||
(let [at (nth arg-types i)]
|
||||
(when (and (not= at :any) (not= at :truthy))
|
||||
(let [pe (assoc (all-any-env params) (nth params i) at)]
|
||||
(when (> (isolated-diag-count body pe) base)
|
||||
(swap! diag-box conj
|
||||
{:op :user-call :argpos i :type (type-name at) :pos pos
|
||||
:msg (str "argument " (inc i) " to `" (:name sig)
|
||||
"` is " (type-name at)
|
||||
", which its body provably rejects")})))))
|
||||
nil)
|
||||
nil (range np)))
|
||||
nargs (count arg-types)]
|
||||
(if (not= npar nargs)
|
||||
;; arity is provably wrong regardless of types — report and stop (the
|
||||
;; per-arg type re-check would bind params positionally, meaningless
|
||||
;; under a mismatch)
|
||||
(swap! diag-box conj
|
||||
{:op :user-call :type :arity :pos pos
|
||||
:msg (str "wrong number of args (" nargs ") passed to `"
|
||||
(:name sig) "` (expected " npar ")")})
|
||||
(let [base (isolated-diag-count body (all-any-env params))]
|
||||
(reduce
|
||||
(fn [_ i]
|
||||
(let [at (nth arg-types i)]
|
||||
(when (and (not= at :any) (not= at :truthy))
|
||||
(let [pe (assoc (all-any-env params) (nth params i) at)]
|
||||
(when (> (isolated-diag-count body pe) base)
|
||||
(swap! diag-box conj
|
||||
{:op :user-call :argpos i :type (type-name at) :pos pos
|
||||
:msg (str "argument " (inc i) " to `" (:name sig)
|
||||
"` is " (type-name at)
|
||||
", which its body provably rejects")})))))
|
||||
nil)
|
||||
nil (range npar)))))
|
||||
(reset! checking-box prev))))
|
||||
|
||||
;; --- Inter-procedural driver API (jolt-767) consumed by the back end --------
|
||||
|
|
|
|||
|
|
@ -142,6 +142,26 @@
|
|||
(os/setenv "JOLT_TYPE_CHECK" nil)
|
||||
r)
|
||||
(fn [s] (nil? (string/find "type error" s))))
|
||||
# negative/never types (jolt-wwy): calling a non-function is reported by default
|
||||
# in direct-link; wrong-arity to a user fn under the JOLT_TYPE_CHECK_USER opt-in
|
||||
(def tcn (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-tcneg-" (os/time) ".clj"))
|
||||
(spit tcn "(ns tcn)\n\n(defn nope []\n (let [n 5] (n 1)))\n")
|
||||
(check "direct-link reports calling a number as a function"
|
||||
(do (os/setenv "JOLT_DIRECT_LINK" "1")
|
||||
(def r (run-err tcn))
|
||||
(os/setenv "JOLT_DIRECT_LINK" nil)
|
||||
r)
|
||||
(has "cannot call a number as a function"))
|
||||
(def tca (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-tcarity-" (os/time) ".clj"))
|
||||
(spit tca "(ns tca)\n\n(defn f [x y] (+ x y))\n(defn g [] (f 1))\n")
|
||||
(check "JOLT_TYPE_CHECK_USER reports wrong arity to a user fn"
|
||||
(do (os/setenv "JOLT_DIRECT_LINK" "1")
|
||||
(os/setenv "JOLT_TYPE_CHECK_USER" "1")
|
||||
(def r (run-err tca))
|
||||
(os/setenv "JOLT_DIRECT_LINK" nil)
|
||||
(os/setenv "JOLT_TYPE_CHECK_USER" nil)
|
||||
r)
|
||||
(has "wrong number of args (1) passed to `f` (expected 2)"))
|
||||
|
||||
(if (> fails 0)
|
||||
(error (string "cli-test: " fails " failing check(s)"))
|
||||
|
|
|
|||
|
|
@ -45,6 +45,23 @@
|
|||
(assert (= 0 (nd "(inc (count [1 2 3]))")) "count of vector + inc of :num both fine")
|
||||
(assert (= 0 (nd "(inc (first [1 2 3]))")) "first of vector -> :num, inc fine")
|
||||
|
||||
# --- calling a non-function (jolt-wwy): :num and :str are not callable --------
|
||||
(assert (= 1 (nd "(5 1)")) "calling a number is reported")
|
||||
(assert (= 1 (nd "(\"hi\" 0)")) "calling a string is reported")
|
||||
(assert (= 1 (nd "((+ 1 2) :k)")) "calling an arithmetic result (a :num) is reported")
|
||||
(assert (= 1 (nd "(let [n 5] (n 1))")) "calling a let-bound number is reported")
|
||||
(assert (= 1 (nd "(let [s \"x\"] (s 0))")) "calling a let-bound string is reported")
|
||||
# (a var holding a number, e.g. (def nn 5) (nn 1), is caught in direct-link
|
||||
# mode via vtype-box; the standalone checker has no var value types)
|
||||
# callable values: keyword/map/vector/set as IFn — NOT reported
|
||||
(assert (= 0 (nd "(:k {:k 1})")) "keyword call is fine")
|
||||
(assert (= 0 (nd "({:a 1} :a)")) "map call is fine")
|
||||
(assert (= 0 (nd "([10 20] 1)")) "vector call is fine")
|
||||
(assert (= 0 (nd "(#{1 2} 1)")) "set call is fine")
|
||||
(assert (= 0 (nd "(fn [c] ((if c 1 :k) 0))")) "union {:num | :kw} callee accepted (:kw is callable)")
|
||||
(assert (= 0 (nd "(fn [f] (f 1))")) "calling an unknown (:any) param accepted")
|
||||
(assert (= 1 (nd "(fn [c] ((if c 1 \"x\") 0))")) "union {:num | :str} callee — both non-callable — reported")
|
||||
|
||||
# --- bounded unions (jolt-pz5): report only when EVERY member is in the error
|
||||
# domain; accept when any member is valid. Differing branches used to collapse
|
||||
# to :any (accepted); now they form {:union #{...}} and are checked per-member.
|
||||
|
|
@ -85,6 +102,18 @@
|
|||
"strict: a ^:redef fn is not a stable requirement, not reported")
|
||||
(assert (= 1 (nds "(do (defn ufrec [x] (ufrec (+ x 1))) (ufrec \"s\"))"))
|
||||
"strict: self-recursion terminates (cycle guard) and the (+ x 1) on a string is reported once")
|
||||
# wrong arity to a user fn (jolt-wwy), strict mode: the registered fixed arity
|
||||
# makes a mismatched call provably throw, regardless of argument types
|
||||
(assert (= 1 (nds "(do (defn uar [x y] (+ x y)) (uar 1))"))
|
||||
"strict: 2-arg fn called with 1 arg is reported")
|
||||
(assert (= 1 (nds "(do (defn uar2 [x] x) (uar2 1 2 3))"))
|
||||
"strict: 1-arg fn called with 3 args is reported")
|
||||
(assert (= 0 (nds "(do (defn uar3 [x y] (+ x y)) (uar3 1 2))"))
|
||||
"strict: correct arity accepted")
|
||||
(assert (= 0 (nd "(do (defn uar4 [x y] (+ x y)) (uar4 1))"))
|
||||
"default level does NOT report user-fn arity (closed-world, opt-in)")
|
||||
(assert (= 0 (nds "(do (defn ^:redef uar5 [x y] (+ x y)) (uar5 1))"))
|
||||
"strict: ^:redef fn arity not checked (could be redefined)")
|
||||
|
||||
# --- the diagnostic carries op + type + a message ----------------------------
|
||||
(def one (in (diags "(inc \"x\")") 0))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue