Hi, I think I'm facing a bug when using unchecked mode. For example, when I write code like below, it should fail on type check using (t/check-ns-clj 'typed-example.unchecked-flow {:check-config {:unannotated-var :unchecked}}) but it says :ok.
(ns ^:typed.clojure typed-example.unchecked-flow
(:require [malli.core :as m]))
;; Typed function that expects an int
(m/=> requires-int [:-> :int :string])
(defn requires-int [x] (str "Got: " x))
;; Unannotated function (gets Unchecked type)
(defn unannotated-fn [x] x)
;; BUG: This should FAIL type checking but PASSES with :unchecked
(defn demonstrate-bug []
(let [unchecked-val (unannotated-fn 42)]
(cond
(string? unchecked-val) "it's a string"
;; This :else branch should be type-checked but isn't!
:else (requires-int "NOT-AN-INT"))))
I suspect that if there's a unchecked var inside cond branch, typed clojure assumes that :else branch is always unreachable?
So I changed typed.cljc.checker.check.if a bit to fix this.
(defn check-if [...]
(let [unchecked-mode? (= :unchecked (get-in opts [::vs/check-config :unannotated-var]))
chk-thn #(let [[env-thn reachable+] (update-lex+reachable lex-env fs+ opts)
forced-reachable+ (or reachable+ unchecked-mode?)]
{:env-thn env-thn
:cthen (check-if-reachable then env-thn forced-reachable+ expected opts)})
chk-els #(let [[env-els reachable-] (update-lex+reachable lex-env fs- opts)
forced-reachable- (or reachable- unchecked-mode?)]
{:env-els env-els
:celse (check-if-reachable else env-els forced-reachable- expected opts)})]))
This way the problem solved.
Or, maybe we can fix reachability on env+ and remove* function to properly address unchecked type.
Can I write a PR for this? Or am I doing wrong about this?Good idea.
Maybe it's really a set? :unannotated-var #{:any :from-arglists} Could also have :from-runtime, e.g., just type the var as (Var (Val @var)) if it's a number.
or perhaps a sequential for reproducibility?
:unannotated-var [:from-runtime :from-arglists :any] tries to find a decent type from left-to-right.
then each one could be configured with overrides. :unannotated-var [{:strategy :from-runtime, :exclude [#'my-var]} ...]
> Or, maybe we can fix reachability on env+ and remove* function to properly address unchecked type.
Yes it needs to be here. Please file an issue, since this is performance-sensitive code. I really don't like Unchecked, perhaps there is a better solution. For example, inferring a generous function type like [Any -> Any] instead of Uncheckedfor annotated-fn.
This is trivial from the arglists.
and we can add another :unchecked-var option like :from-arglists.
Thanks for replying. What do you think about improving :unannotated-var :any to properly address a function?
Seems good. What about starting with some simple improvements like, inferring to [Any * -> Any]?
;; In typed.clj.checker.check/check-var
;; ...
(impl/impl-case opts
:clojure (= :any (:unannotated-var check-config))
:cljs nil)
(do
(let [var-value (try @var (catch Exception _ nil))
inferred-type (if (fn? var-value)
;; If it's a function, assign [Any * -> Any] type
(do
(println (str "Inferring " vsym " dereference as [Any * -> Any] (function)"))
(r/make-Function [] r/-any :rest r/-any))
;; Otherwise, assign Any type
(do
(println (str "Inferring " vsym " dereference as Any"))
r/-any))]
(assoc expr
u/expr-type (below/maybe-check-below
(r/ret inferred-type)
expected
opts))))Yep that's good. I'm envisioning a cond that starts with :arglists meta then move to stuff like this (this is also fine to start with).