Skip to content
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

Closed
wants to merge 10 commits into from

Conversation

ebresafegaga
Copy link
Contributor

@ebresafegaga ebresafegaga commented Sep 26, 2022

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 type
a -> b, occurs checks prevents a = 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:

let f :  _ = fun x => x x in f 
let f :  _ =  fun esv es e => e esv e (es e) in f
{g = g 0}.g : Num

Actual output:

thread 'main' has overflowed its stack
fatal runtime error: stack overflow

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.

nickel> let f :  _ = fun x => x x 

The expression x has the inferred type a -> b. 
x cannot be the argument of x, because this will lead to the construction of the infinite type a = a -> b.
Maybe try using concrete types to guide your implementation instead of wildcards?

The error message generation is not yet like as I've described above.

Closes #277

@ebresafegaga ebresafegaga self-assigned this Sep 26, 2022
@github-actions github-actions bot temporarily deployed to pull request September 26, 2022 15:21 Inactive
@github-actions github-actions bot temporarily deployed to pull request September 26, 2022 23:03 Inactive
Copy link
Contributor

@matthew-healy matthew-healy left a 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?

src/typecheck/mod.rs Outdated Show resolved Hide resolved
src/typecheck/mod.rs Outdated Show resolved Hide resolved
Copy link
Member

@yannham yannham left a 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.

src/typecheck/mod.rs Outdated Show resolved Hide resolved
src/typecheck/mod.rs Outdated Show resolved Hide resolved
src/typecheck/mod.rs Outdated Show resolved Hide resolved
src/typecheck/mod.rs Outdated Show resolved Hide resolved
@github-actions github-actions bot temporarily deployed to pull request September 30, 2022 11:36 Inactive
@github-actions github-actions bot temporarily deployed to pull request October 3, 2022 14:47 Inactive
@github-actions github-actions bot temporarily deployed to pull request October 3, 2022 14:55 Inactive
@github-actions github-actions bot temporarily deployed to pull request October 4, 2022 10:08 Inactive
@github-actions github-actions bot temporarily deployed to pull request October 4, 2022 14:40 Inactive
@yannham
Copy link
Member

yannham commented Oct 6, 2022

@ebresafegaga Could you please report the result of the benchmarks here, to document the decision with respect to this PR? Thanks!

@ebresafegaga
Copy link
Contributor Author

On average, we're seeing a 68% regression in performance while type-checking typecheck-nixpkgs-lib.

Here's an example of such benchmark:

nixpkgs lists           time:   [2.8098 ms 2.8272 ms 2.8450 ms]
                        change: [+79.283% +81.642% +83.967%] (p = 0.00 < 0.05)
                        Performance has regressed.

@yannham
Copy link
Member

yannham commented Oct 10, 2022

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

@ebresafegaga
Copy link
Contributor Author

We'll just keep this branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Infinite loop during typechecking (recursive types)
3 participants