-
Notifications
You must be signed in to change notification settings - Fork 87
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
Implement "occurs check" to prevent infinite loop during type-checking #846
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change looks good to me, though I've only been looking at the Nickel codebase for a few days and I'm mainly reviewing to get some more exposure to the code. 😁
Do you think it might make sense to add test cases using the examples from the PR description (e.g. in tests/typecheck_fail.rs
) to prevent future regressions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be interesting to run the benchmarks on typechecking the nix list lib to see if that affects performance. My occurs check knowledge is rusty but I remember that running it without any form of memoization like this can turn out to be expensive. For the record, although this is not the main subject of the article, efficient and insightful generalization also mentions how to do it in a "lazy" way (and not at each unification).
I remember why this issue has been open for quite some time, now. I think my original plan was to implement path compression first (i.e., when you call get_root
, you remember all the unification variable on the way, like if there is a link ?a -> ?b -> ?c -> ?d -> Num
and at the end makes the table look like ?a -> Num, ?b -> Num, ?c -> Num, ?d -> Num
). Doing so, we can avoid the occurs check at unification, and perform it only when calling to get_root
, potentially grouping several traversals into one. This probably requires to remember for each unification variable if the check has been done or not.
In order to implement path compression efficiently, you want TypeWrapper
to use Rc
, not bare Box
, otherwise you will deep-copy your types a lot. But this hasn't happened yet 😕
We can first see what happens on the benchmark: if the numbers are reasonable, this can be a first implementation, and then we can work toward shareable types (Rc) + path compression + occurs check.
@ebresafegaga Could you please report the result of the benchmarks here, to document the decision with respect to this PR? Thanks! |
On average, we're seeing a 68% regression in performance while type-checking Here's an example of such benchmark:
|
We are cheating by not doing the occurs check right now, so a hit is to be expected, but 80% doesn't sound acceptable. I propose to close this PR (but keep the branch somewhere, or maybe just commit the checking function somewhere) and try to do something a bit lazier/smarter |
We'll just keep this branch. |
Summary
Currently, the Nickel type-checker does not perform "occurs check" when unifying a type variable with another type . Occurs checks normally prevents us from creating "infinite" type substitutions. For example, if we want to unify a type variable
a
with another typea -> b
, occurs checks preventsa = a -> b
from being a type substitution equation.In Nickel, this means that programs which result in a type variable unifying with another type which includes that variable results in a stack overflow.
Example:
Actual output:
Desired output:
Nickel should give a type error, stating the exact problem in a friendly way; and maybe even "tips" on how to resolve it.
The error message generation is not yet like as I've described above.
Closes #277