rewrite-clj

2023-02-14T20:06:21.146609Z

I just wrote a linting engine using rewrite-clj (actually clj-kondo's rewrite-clj output), and it runs really fast compared to the hand-rolled stuff I was playing with beforehand. i'm reading through various defunct clojure projects that do hand-rolled parsing, and it's really surprising to me how simple they would be if they had relied on rewrite-clj (kibit and marginalia specifically). thanks to y'all for working so hard on this library.

lread 2023-02-14T20:19:28.961869Z

Thanks for the kind words @nbtheduke!

❤️ 1
lread 2023-02-14T20:53:05.232789Z

Is it kibit you are working on?

2023-02-14T20:53:12.985329Z

sort of, yeah

2023-02-14T20:54:43.753589Z

I originally was building an s-expression pattern matching library for use in clj-kondo, but i think it's not quite appropriate given how @borkdude likes to write his code, and then i saw how slow kibit is and in the process of taking over the project, i ended up finishing my own version, which lints much faster lol

2023-02-14T20:54:52.665429Z

gonna see if it's appropriate for usage within kibit itself

lread 2023-02-14T20:56:51.972279Z

cool! looking forward to seeing the fruits of your experiments!

2023-02-14T21:15:33.898619Z

initial report:

$ time lein kibit
...
real    41m41.018s
user    42m18.347s
sys     0m3.742s
vs
$ time clojure -M:run ../netrunner/
...
real    0m14.986s
user    1m47.699s
sys     0m1.404s

lread 2023-02-14T21:28:46.720969Z

Yoiks! That's more than a tad faster!

borkdude 2023-02-14T21:47:56.435929Z

> but i think it's not quite appropriate given how @borkdude likes to write his code Not sure what you mean by that :P

2023-02-14T21:50:03.444409Z

sorry, i don't mean to insult or denigrate. you've said that you prefer to perform each lint check exactly where it needs to happen as you process the files in an effort to keep speed as fast as possible, instead of pre-processing a given hunk of code and running over it a second time to find errors

borkdude 2023-02-14T21:50:54.071789Z

that's correct. this is not a matter of preference, but a matter of performance. clj-kondo has to be fast to be a viable option to execute on every keystroke

👍 1
borkdude 2023-02-14T21:55:34.641159Z

I'm still thinking about a way to let people have the whole file's rewrite-clj structure and do whatever they want, as a hook. But having kibit as a hook, is probably a little bit on the heavy side, right

borkdude 2023-02-14T21:56:16.201209Z

Also kibit's matching is a little bit course, since it doesn't really see if a symbol is a local or a var reference, for example, right?

2023-02-14T21:56:44.383099Z

yeah, it doesn't care about that, it's purely focused on shape. if i were to attempt to integrate this into clj-kondo, a bunch of stuff would need to change

2023-02-14T21:59:28.499549Z

it is pretty fast tho. linting the https://github.com/mtgred/netrunner/ with native clj-kondo takes 28 seconds, and linting the same repo with my library as an uberjar takes 10 seconds.

borkdude 2023-02-14T22:01:29.403429Z

nice. what makes your lib faster than kibit?

2023-02-14T22:04:24.262299Z

macros to generate functions that compare the shape: given (not (empty? coll)), it'll generate something like (and (= :list (:tag form)) (let [children (:children form)] (and (= 1 (count children)) (= 'not (:sym (nth children 0))) ...)

2023-02-14T22:04:38.165969Z

wraps it in a function, and then calling that function is pretty quick

borkdude 2023-02-14T22:05:19.477199Z

but you could do the same on raw s-expressions right?

borkdude 2023-02-14T22:05:45.013069Z

does it come down to: not using core.logic is faster than rolling your own matching logic? that's what I would have expected :)

2023-02-14T22:06:36.083659Z

Haha yes, it is due to that

borkdude 2023-02-14T22:07:36.036589Z

I'm not sure how rewrite-clj makes stuff simpler for you compared to working with s-expressions directly. in clj-kondo I only use rewrite-clj so I have more control over the locations (e.g. numbers can have location metadata) and invalid expressions still parse, etc.

borkdude 2023-02-14T22:09:06.834159Z

you are aware of grasp right? https://github.com/borkdude/grasp it works directly on s-expressions using clojure.spec expressions

👍 1
2023-02-14T22:19:11.593949Z

I relied on clj-kondo’s parser because i started this to satisfy my curiosity about alternate methods of writing clj-kondo linters lol

😄 1
borkdude 2023-02-14T22:23:57.703339Z

well, clj-kondo still has access to the rewrite-clj nodes so in theory it could give you access to every s-expression it lints to do your custom kibit like matching. Are you aware of the macroexpand hook? It turns a macro rewrite-clj node into an s-expression, which the user can then transform (or lint) and after that it's turned back into a rewrite-clj node.

borkdude 2023-02-14T22:27:10.434649Z

What you're doing currently is probably more performant though.

2023-02-15T22:07:19.958519Z

I just switched from using clj-kondo's rewrite-clj to the rewrite-clj library itself and running takes longer: 7.6 seconds to 19.2 seconds

2023-02-15T22:07:51.614769Z

i suspect because i had to add a small bit of logic to skip over comment and whitespace nodes

2023-02-15T22:08:39.875709Z

(from (let [~children-form (vec (:children ~new-form))] to (let [~children-form (vec (remove n/whitespace-or-comment? (n/children ~new-form)))]

borkdude 2023-02-15T22:10:26.050759Z

I've alluded to it before but why aren't you just matching against s-expressions instead of using rewrite-clj?

2023-02-15T22:11:34.012939Z

using edn/read-string?

borkdude 2023-02-15T22:12:01.713689Z

Oh god no, edn/read-string doesn't work for clojure code. That is very close to what grasp is doing (which I also mentioned before in this thread), if you want to take that approach, you can take a look there.

👍 1
😂 1
2023-02-15T22:12:45.100319Z

i don't want to fall down the well of having to update my code to handle new edge cases in clojure parsing, you know? better to rely on the existing libraries with nice apis

2023-02-15T22:13:06.351839Z

kibit does that, and is currently broken if you use :as-alias lol

borkdude 2023-02-15T22:13:13.401049Z

grasp uses edamame to parse clojure code: https://github.com/borkdude/edamame just look at its source, it's very simple.

2023-02-15T22:13:24.812859Z

ah excellent, i forgot about that library

borkdude 2023-02-15T22:13:54.795509Z

I mean source as in, look at how grasp is using edamame

2023-02-15T22:14:30.472539Z

i'll read it over

2023-02-15T22:14:44.035569Z

here's my library: https://github.com/NoahTheDuke/spat

2023-02-15T22:14:47.734759Z

warning, i wrote it for myself lol

borkdude 2023-02-15T22:17:52.189149Z

I'll take a look when I'm not close to going to bed :)

borkdude 2023-02-15T22:19:02.082969Z

your lib is also rewriting the original source?

borkdude 2023-02-15T22:22:01.434049Z

I didn't know that was something that kibit did, but I think you could take the same approach with this library: the matching can happen on regular s-expressions like kibit does (which will make it faster than working with rewrite node, I think and the matching logic becomes simpler) and the rewriting can be done using rewrite-clj

2023-02-15T23:10:31.773609Z

I’m only rewriting in the printing to out, not in the file itself

2023-02-15T23:31:21.603249Z

Interesting, I’ll try it on edamame output, see how that feels. The primary logic doesn’t change much thankfully, the output of the macro barely relies on the rewrite-clj form