-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Review usage of assert #95
Comments
Issue comments from rewrite-cljc-playground: @borkdude: Also please don't replace with spec, unless the specs are in a different namespace and optional to load. @borkdude: The checks in asserts are mostly useful for developers. They should be elidable at compile time for performance. It's nice to get some extra checking during development, but you should be able to turn that off when compiling a final artifact. E.g. linters and formatters should get the ultimate performance possible, since they run on every keystroke. So I would really do nothing at this point, since people are relying on being able to turn asserts off. @lread: Question to self: if asserts are disabled is rewrite-clj able to parse invalid (or less valid) Clojure code? (Note that this is not necessarily a bad thing). @borkdude: Asserts aren't bad, they are designed exactly for this purpose. @lread: @sogaiu: |
This one is biting me as I'm trying to cut a new refactor-nrepl release. See e.g. clojure-emacs/refactor-nrepl#332 It'd be pretty bad to leave users with errors that can't quite be debugged.
It's fairly trivial to replace Probably for refactor-nrepl I'd be OK with the performance hit of checking preconditions. I prefer correct code that can be debugged to opaque NPEs or such. Also for refactor-nrepl the bottleneck is tools.analyzer, typically it will trump any other cost. You might want to define your own Cheers - V |
Thanks for adding to the issue @vemv! This is all a bit foggy so I shall recap and paraphrase:
Since |
Hey there!
The nuance is that So it's easy to couple things. End users may tweak I think that a minimalistic solution would look like this: (defn foo [x]
(when *assert*
(or (string? x)
(throw (ex-info "..." {:faulty-value x}))))) ...It's not merely a hand-rolled (If going this direction I'd recommend defining your own I can contribute a POC PR with this setup upgrading a few |
Note that moving checks from compile time to runtime isn't exactly better for performance, especially not when deref-ing dynamic vars, but perhaps in the grand scheme of things it doesn't matter that much. Perhaps just moving the checks to a rewrite-clj.specs namespace that you can optionally load / instrument if you want spec checking for these functions is the way to go in this day and age. |
Adding spec to the mix seems to go against Also spec tempts one to bring in Orchestra or such, most devs out there appreciate checking the return value etc. Which can further complicate things. tldr Spec is not exactly agnostic/minimalistic for this use case.
I was thinking of a 'double-checked' pattern i.e. first you check against |
Using more precise wording, if |
I don't really agree with this one: spec is an already available dependency and loading the specs can (and imo should) be made optional for users by providing them in a separate namespace. Instrumentation can be chosen at a fine-grained level (per function even). |
Well I'll leave this one to @lread however this thing you wrote here sums up well a common sentiment:
i.e. instrumentation is bit of a frustrating experience, that can be tracked in various places (for example the Expound issue tracker comes to mind; integrating it with Orchestra is not trivial). Used expertly, sure it should work. I'd simply be pessimistic about edge cases i.e. rewrite-clj can be used in unexpected ways (e.g. inlined via mranderson) and composability (namely: how different rewrite-clj consumers instrument rewrite-clj concurrently) Whereas a custom |
The I'm not necessarily a fan of everything spec does and looking forward to spec2, but meanwhile providing a Having said this, some custom code that doesn't incur any performance penalty when you won't want validation, could work. |
I'm aware of the pattern however I'd be hesitant about instrumentation. Isn't it essentially a global side-effect? Whereas a e.g. for the refactor-nrepl use case, we can bind e.g.
If the exact same thing can be said of instrumentation, so much the better, LMK |
For me personally these extra checks are only useful during development and even then, mostly in development of rewrite-clj itself. Why is it important to turn this on in production code of refactor-nrepl? What action can the user take when something turns out to be wrong in the way that refactor-nrepl is using rewrite-clj? Moreover, I think these checks exist mostly as a development aid of rewrite-clj itself, since node construction isn't usually done "manually"? |
As mentioned, for refactor-nrepl the bottleneck is tools.analyzer so running these checks is affordable. |
Doesn't this all mean that everything already works quite the way it should for refactor-nrepl, as it is now? And if people did disable |
Things aren't working nicely right now because:
This is misleading, I would have gotten a stacktrace either way. |
Yeah, I meant: the stack trace like you received it, with the assert failed message. |
Btw, I can do some perf tests with clj-kondo to see if lint time drastically reduces if I re-enable assert. Perhaps it doesn't make a huge difference. Perhaps it was overkill to disable it. Note: I've only disabled it in the native image build, not globally for library users, that would be a mistake. |
tbh I don't feel comfortable either if making a 'client' happy will make others less happy i.e. I'm aware clj-kondo is editor-oriented so it makes sense for it to squeeze perf. A POC would have to be 'non-breaking' in this regard |
@vemv I guess you can also demand/insist that users enable |
Agreed. It's only part of the equation though Summarizing my thinking, I'd propose a macro like this: (def ^:dynamic *check* true)
(defmacro check [[f & args :as call]]
{:pre [(symbol? f)
(ns-resolve *ns* &env f)
(not (-> (ns-resolve *ns* &env f) meta :macro)) ;; ensure `apply` will work
(list? call)]}
(when *assert*
`(when *check*
(let [as# ~(vec args)]
(when-not (apply ~f as#)
(throw (ex-info "Expectation failed"
{:call (apply list ~(list 'quote f) as#)})))))))
(defn foo [a]
(check (pos? a))
(+ a a))
(foo 1) ;; OK
(foo -1) ;; Fails and tells you that -1 was the value at fault
(binding [*check* false]
(foo -1)) ;; -2
|
I think given the last:
that doing nothing is perhaps still a valid option and leave the code as is. If people disable assert, it's on them to enable it for better error messages. As I said, I might do some perf checks in clj-kondo to verify if disabling these checks was really worth it, I might not have done it with good reasons. |
Also I don't think introducing dynamic var derefs at runtime is an improvement (in terms of performance) over what we have right now. Then again, this may not be so critical as when you're developing a library like malli or so. |
Data to back up my opinion:
The deref of the dynamic var itself is way more expensive than executing the actual predicate. |
Wow, I never expected this issue to generate such a full and interesting discussion! |
Worth emphasizing the compile-time Still, it's a good observation and perhaps we can use an alternative. InheritableThreadLocals come to mind. |
Remember that vanilla :pre doesn't inform of the value at fault, which can very plausibly improve time-to-fix. |
I don't want to incur any extra perf hits when I decide to re-enable Some data with assert enabled (
Practically no difference, so I'm probably just removing the assert false in my native-image! I probably just disabled it in the hope it would be faster, but it's not really noticeable and I did so without good reason. So, I propose either one of these:
Since clj-kondo won't be disabling |
I disagree with your reasoning, which can be summarized as:
It's kind of a circular reasoning. clj-kondo/clj-kondo@014aa3c introduces a design constraint almost on purpose. You are on time to undo that 🍻 |
I think most people use this library with the default which is If that situation would arise (hypothetically) then would perhaps be a good time to review what should be done, but that's not the case right now as far as I know. So I think we're discussing only a hypothetical problem and not a real pressing issue. |
clojure-lsp certainly doesn't disable |
Maybe time to recap and summarize what actual problem we are trying to solve for users of the rewrite-clj library? @vemv I think your original idea was to include more context so that when assertions were triggered they could be more easily diagnosed? An important note: The rewrite-clj assertions we are talking about were carried forward from rewrite-clj v0. I have done no review of them and not added in any more (so coverage may be quite inconsistent). |
Precisely in face of that type of considerations I've repeatedly hinted how it is important to decouple one's would-be Going back to this snippet #95 (comment) , we can change the default of It would be set to (the original |
I think I would rather have non-optional cheap asserts than adding a dynamic var deref around it which makes it 6x slower. |
I wrote:
Please let's not talk over each other. |
Achieving so should not affect performance by default or introduce breakage, complexities etc. |
Raised me in rewrite-cljc-playground, see lread/rewrite-cljc-playground#59
Much thanks to @sogaiu for bringing this up on Slack.
Rewrite-clj (and therefore rewrite-cljc) makes use of
assert
, mostly to validate node creation, and therefore also for general parsing. The most common assertion is that a node has the expected number of children.@sogaiu highlighted the following interesting points (which I am paraphrasing, hopefully correctly):
the naive use of
assert
can lead to unintented results as described in "Use of Assertions" by John Regehr from which @sogaiu quoted:@sogaiu also came across "Beware of Assertions" by Alexander Yakushev
@borkdude effectively disables these
assert
s for some of his projects, for example here's how he does so for clj-kondo, mostly for performance.If we take point 1 to heart, we can argue that rewrite-cljc really shouldn't be making use of
assert
. It should not be deciding that the situation of encountering invalid Clojure is dire for the calling library/application.Note that I did replace all explicit Exception throws with
ex-info
exceptions for rewrite-cljc, for cross-platform support between clj and cljs - so I do have a precedent for replacing other types of exceptions. But I am not sure what makes sense moving forward. Some initial ideas:The text was updated successfully, but these errors were encountered: