clojure-dev

Issues: https://clojure.atlassian.net/browse/CLJ | Guide: https://insideclojure.org/2015/05/01/contributing-clojure/
denik 2021-02-26T20:18:48.015Z

is there way to find all non-local bindings of a clojure form? For example:

(non-local-bindings
  '(fn ([{:keys [items ks]}]
        (let [foo :bar]
          (conj [foo] (not.locally/bound))))))
;; should return:
;; => #{clojure.core/fn clojure.core/conj not.locally/bound}

2021-02-26T20:19:44.016100Z

@denik my first hunch is to use (keys &env) and filter out everything in that

2021-02-26T20:19:54.016300Z

(what remains is not local)

denik 2021-02-26T20:20:08.016700Z

@noisesmith since the form is quoted that wouldn’t work or would it?

2021-02-26T20:21:02.017600Z

it would require jumping through some hoops if it's possible, I don't know anything directly in clojure that answers your question - maybe some feature of tools.namspace?

2021-02-26T20:21:27.017800Z

Nope

2021-02-26T20:22:01.018600Z

sorry I was thinking of tools.analyzer and mis-remembered the name (answer might still be no)

1👌
2021-02-26T20:22:24.019200Z

Tools analyzer would be the thing, the general term for this is closed/free

2021-02-26T20:22:57.020300Z

Well I guess free, you are looking for the free variables in an expression

2021-02-26T20:23:52.021500Z

you could make something with tree-seq I bet, but you'd need to hard code every binding form to make it work

2021-02-26T20:24:04.021900Z

Which toops.analyzer will do, and a ton more

2021-02-26T20:24:21.022400Z

Nah, tree-seq won't work

borkdude 2021-02-26T20:24:36.023400Z

@denik you could also use clj-kondo which will report non-locals as unresolved symbols

2021-02-26T20:25:01.024400Z

You need to analyze in the lexical context, which tree-seq will peel away

2021-02-26T20:25:08.024600Z

my thought was the "children" function could return an empty list for non-binding forms, and a list with locals filtered otherwise

2021-02-26T20:25:12.024800Z

but that might be flawed

2021-02-26T20:26:11.025700Z

In general it is not an easy analysis to do, if you can avoid it

2021-02-26T20:27:50.027700Z

It is really impossible before macroexpansion and after macroexpansion things like fn are gone, replaced with fn*, which as a special form is usually not considered free

borkdude 2021-02-26T20:30:13.028Z

$ cat /tmp/example.clj
(fn ([{:keys [items ks]}]
     (let [foo :bar]
       (conj [foo] (not.locally/bound)))))
$ clj-kondo --lint /tmp/example.clj
/tmp/example.clj:1:15: warning: unused binding items
/tmp/example.clj:1:21: warning: unused binding ks
/tmp/example.clj:3:21: warning: Unresolved namespace not.locally. Are you missing a require?

2021-02-26T20:39:59.032300Z

The clojure compiler sort of analyzes names to determine which of three states it is in: free (a free name that doesn't resolve to a var is an error) closed over (compiles to an instance field lookup) or local (gets a slot in the methods stackframe)

denik 2021-02-26T20:46:03.033500Z

I’m already using sci 😛 this will a borkdude-heavy project 😄

2021-02-26T20:46:15.033900Z

I have some code from a sort of replacement for core.async's go macro I was writing that uses tools.analyzers postwalk-transforms feature to collect free variables https://gist.github.com/hiredman/5644dd40f2621b0a783a3231ea29ff1a#file-yield-clj-L655-L679

1👀
denik 2021-02-26T20:53:51.034900Z

@borkdude is there a way to use clj-kondo on forms directly

borkdude 2021-02-26T20:55:13.035300Z

@denik yes, let me create an example

1🙏
denik 2021-02-26T20:56:16.036100Z

also using sci, so if they share a ctx I could plug that in

borkdude 2021-02-26T20:57:16.036400Z

@denik

(require '[clj-kondo.core :as clj-kondo])

(def example '(fn ([{:keys [items ks]}]
                   (let [foo :bar]
                     (conj [foo] (not.locally/bound))))))

(def findings
  (:findings (with-in-str
               (pr-str example)
               (clj-kondo/run! {:lint ["-"]}))))

(require '[clojure.pprint :as pp])

(pp/pprint findings)
[{:type :unused-binding,
  :filename "<stdin>",
  :message "unused binding items",
 ...

borkdude 2021-02-26T21:01:15.036600Z

@denik Feel free to drop by in #sci if you have questions

1🙏
denik 2021-02-26T21:03:08.037300Z

thank you. looks like the symbols pat of a string. I’ll try to find lower-level fns that return the symbols

borkdude 2021-02-26T21:03:59.037500Z

@denik we could add that to the data. I think that could be useful too.

1💯
borkdude 2021-02-26T21:05:15.037700Z

what are you trying to accomplish with the symbol once you find it?

denik 2021-02-26T21:27:41.038Z

I’m storing function bodies in datalevin, example here: https://clojureverse.org/t/datalevin-powering-environment-and-runtime/7243

denik 2021-02-26T21:29:15.038300Z

and want to resolve symbols from the db and replace them with something that is invocable

denik 2021-02-26T21:31:57.038500Z

also wondering if ctx can be shared between the analyzer (kondo) and sci (evaluation)

borkdude 2021-02-26T21:52:12.038700Z

@denik What you can do maybe is: use sci/parser + sci/parse-next which will get you the function s-expression. Then you can do some processing on that (postwalking, replacing) and then you can feed that to sci/eval-form

borkdude 2021-02-26T21:53:32.038900Z

This is also how you can implement a REPL in sci: https://github.com/borkdude/sci#repl But you can do the processing step in between the parsing and evaluation.

denik 2021-02-26T22:00:16.039300Z

that works for parsing into edn form but how would it solve the local-bindings problem?

(sci.impl.parser/parse-next
  sci-ctx
  (sci.impl.parser/reader (str '(fn ([{:keys [items ks]}]
                                     (let [foo :bar]
                                       (conj [foo] (not.locally/bound))))))))
=> (fn ([{:keys [items ks]}] (let [foo :bar] (conj [foo] (not.locally/bound)))))

borkdude 2021-02-26T22:01:05.039700Z

It doesn't solve that part, but that's the place to potentially fix it. Why would you store s-expressions with unresolved vars/locals?

denik 2021-02-26T22:02:51.040Z

exactly, I wouldn’t! But since I store forms in a database (not namespaces) I need to find unresolved symbols in the db and replace them with their value and throw and error otherwise

borkdude 2021-02-26T22:06:39.040300Z

is an unresolved symbol a local or a var in your problem? and is it namespaced? and what would you replace it with?

borkdude 2021-02-26T22:06:56.040500Z

I'm trying to understand the problem, not entirely clear to me yet

denik 2021-02-26T22:09:01.040700Z

I’ll do my best to explain! If I can use sci’s ctx it is a symbol (namespaced or not) that is not available in ctx’s :namespaces

borkdude 2021-02-26T22:14:04.040900Z

And how do you know what to replace it with?

denik 2021-02-26T22:15:24.041100Z

yes I have a function that inlines the form

borkdude 2021-02-26T22:17:46.041300Z

but how do you know what to replace with what? can you give an example?

denik 2021-02-26T22:20:20.041500Z

sure,

(snipf all-tweets
       []
       (->> (datoms :aev :com.twitter/tweet-text)
            (mapv (comp entity :e))))

; =>
{:var cells.lab.code-db/all-tweets,
 :id "1MCsHLjz56",
 :created-at #inst"2021-02-26T22:19:12.408-00:00",
 :updated-at #inst"2021-02-26T22:19:12.408-00:00",
 :form (clojure.core/fn [] (->> (datoms :aev :com.twitter/tweet-text) (mapv (comp entity :e)))),
 :db/id 38}

(snipf all-tweets-ui
       []
       (into [:div]
             (map (fn [{:com.twitter/keys [tweet-text tweet-author]}]
                    [:div (str tweet-text " by " tweet-author)]
                    ))
             (cells.lab.code-db/all-tweets)))

; =>
{:var cells.lab.code-db/all-tweets-ui,
 :id "1YiWEQrLlG",
 :created-at #inst"2021-02-26T22:19:28.533-00:00",
 :updated-at #inst"2021-02-26T22:19:28.533-00:00",
 :form (clojure.core/fn
         []
         (into
           [:div]
           (map (fn [#:com.twitter{:keys [tweet-text tweet-author]}] [:div (str tweet-text " by " tweet-author)]))
           ((clojure.core/fn [] (->> (datoms :aev :com.twitter/tweet-text) (mapv (comp entity :e))))))),

denik 2021-02-26T22:22:09.041700Z

all-tweets-ui uses all-tweets which has been added to the db through the snipf macro. we can see that the form of all-tweets-ui inlined the form of all-tweets

borkdude 2021-02-26T22:23:23.041900Z

so it's more or less a dependency problem?

denik 2021-02-26T22:24:54.042100Z

dependency-resolution, yes. anything that is not provided in sci’s context will have to get inlined from the db. if it doesn’t exist, it should throw

borkdude 2021-02-26T22:25:56.042300Z

I think you should solve this differently and store some information on which other vars the var depends and load those first

borkdude 2021-02-26T22:27:04.042500Z

you can possibly store "require" expressions along with the fn expressions, to ensure the namespace gets loaded first

denik 2021-02-26T22:28:39.042700Z

hmm, it’s less about loading and more about existence of a form for a given var in the database. it would be great if this could be figured out during analysis

borkdude 2021-02-26T22:29:38.042900Z

in sci you could try to eval the form, catch the exception and try to load the other form with data from the exception, maybe

denik 2021-02-26T22:29:55.043100Z

none of the forms should need to be evaluated to know whether some of their contained symbols need to be replaced

borkdude 2021-02-26T22:31:01.043400Z

evaluating a fn expr is more or less the same as analyzing the fn body

denik 2021-02-26T22:31:08.043600Z

am I wrong in thinking that the sci-ctx and a form-walker that ignores local bindings should be enough to do this?

borkdude 2021-02-26T22:32:42.043900Z

sci does have a separate analysis step but this is not exposed. you will still get evaluations for macros that are expanded for example, so it isn't guaranteed to be side-effect free

denik 2021-02-26T22:33:59.044100Z

hmm ok time to ponder this for a bit. thanks so much for being helpful!

borkdude 2021-02-26T22:34:01.044300Z

> am I wrong in thinking that the sci-ctx and a form-walker that ignores local bindings should be enough to do this? this is more or less what happens during analysis

denik 2021-02-26T22:35:07.044500Z

I looked at it earlier in sci and unfortunately the analyzer closes over function arities so that I cannot inspect them

borkdude 2021-02-26T22:36:34.044700Z

what you can inspect is the parsed form

denik 2021-02-26T22:37:09.044900Z

can I?

denik 2021-02-26T22:37:16.045300Z

(sci.impl.analyzer/analyze sci-ctx
                             '(fn ([{:keys [items ks]}] items))
                             )
#:sci.impl{:fn-bodies [#:sci.impl{:body [#object[clojure.lang.AFunction$1 0xfbaf833 "clojure.lang.AFunction$1@fbaf833"]],
                                  :params [p__46761],
                                  :fixed-arity 1,
                                  :var-arg-name nil,
                                  :fn-name nil,
                                  :arglist [{:keys [items ks]}]}],
           :fn-name nil,
           :arglists [[{:keys [items ks]}]],
           :fn true,
           :fn-meta nil}

denik 2021-02-26T22:38:34.045500Z

this is the hack that currently works:

(defn- local-syms [params]
  (set
    (sp/select
      (sp/walker symbol?)
      params)))

(defn- resolve-arity-tail [arity-tail]
  (let [params     (->> arity-tail :params :params (mapv second))
        local-syms (local-syms params)
        body       (-> arity-tail :body second)]
    (apply list params
           (clojure.walk/postwalk
             (fn [x]
               ;; FIXME need something that ignores all local bindings
               (if (symbol? x)
                 ; fixme should resolve from sci' ctx
                 (if (or (local-syms x) (resolve x))
                   x                                        ; provided
                   (if-let [{:keys [form]} (get-db-var (namespace-sym x))]
                     form
                     ;; FIXME
                     (do (println (str "Couldn't resolve " x ", assume locally bound"))
                         x)
                     #_(throw (ex-info (str "No :form found for " x) {}))))
                 x))
             body))))

(defmacro snipf [name & f-bodies]
  (let [[arity# tail-or-tails#] (s/conform ::clj-specs/fn-tail f-bodies)
        ari# (case arity#
               :arity-1 (resolve-arity-tail tail-or-tails#)
               :arity-n (map resolve-arity-tail tail-or-tails#))
        f#   (conj ari# `fn)]
    `(db/transact-entity!
       code-conn
       {:var  (~namespace-sym '~name)
        :form '~f#})))

denik 2021-02-26T22:39:27.045700Z

but it would not catch vars that don’t exist in either the sci ctx or the db at def-time

borkdude 2021-02-26T22:40:17.045900Z

with parsed, I meant what you get back from parse-next. the analyzer is an impl detail, the output from that isn't meant for third party consumption

1👌
borkdude 2021-02-26T22:45:04.046200Z

So what is the use case of saving arbitrary snippets of code out of context in the db? Can you add the requirement that people can declare some kind of dependency so the function snippets become more standalone and easier to execute?

borkdude 2021-02-26T22:46:31.046400Z

I'm afk now

denik 2021-02-26T22:47:00.046600Z

it’s a little cumbersome but that could be done. I thought the namespaced symbol itself could be that requirement

denik 2021-02-26T23:15:52.046900Z

this would work if it was possible to pass a resolve-symbol function https://clojurians.slack.com/archives/C06E3HYPR/p1614378661043400?thread_ts=1614373036.036400&cid=C06E3HYPR