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

replace unknown names with holes in name desugaring #3619

Open
wants to merge 25 commits into
base: master
Choose a base branch
from

Conversation

colinwahl
Copy link
Contributor

@colinwahl colinwahl commented May 2, 2019

Replaces unknown names with holes in name desugaring

This intends to close #3619

  • Modifies UnknownName error to support reporting inferred type and context
  • Introduces new UnknownValue case to Expr
  • modifies name desugaring (in the updateValue phase) to replace unknown names with UnknownValue
  • modifies typechecking to turn UnknownValue into UnknownName warnings
  • modifies typeCheckModule to escalate UnknownName warnings produced during typechecking into errors
  • modifies typechecking to error on UnknownName errors before reporting errors which may have been caused by tried to infer the type for the UnknownValue node in the AST

Here is an example:

module Main where

import Prelude

import Effect (Effect)

bar :: Effect Unit
bar = do
  boo <- a 10
  let x = foo "hello"
  pure boo

The UnknownName error now reports the inferred type of the unknown value, as well as the local context at the unknown value.

[1/2 UnknownName] src/Main.purs:9:10

  9    boo <- a 10
              ^

  Unknown value a has the inferred type

    Int -> Effect Unit

  in value declaration bar

[2/2 UnknownName] src/Main.purs:10:11

  10    let x = foo "hello"
                ^^^

  Unknown value foo has the inferred type

    String -> t0

  in the following context:

    boo :: Unit

  in value declaration bar

  where t0 is an unknown type

@hdgarrood
Copy link
Contributor

I'm not sure about the "you could subsitute with one of these values" section; usually, when I encounter an UnknownName error, it's because I:

  • forgot to import something,
  • used a function I was intending to implement but haven't done so yet, or
  • misspelled a function name.

In each case I know what I want to put there, so having a list of other things I could put there is unlikely to be helpful, especially because we already have holes for asking the compiler "what can I put here". I would prefer either having a "did you mean: something" section based mostly on edit distance for misspellings, or not having it at all.

@megamaddu
Copy link

  • used a function I was intending to implement but haven't done so yet, or

The "has the inferred type" section above seems to cover this case

  • forgot to import something,

Perhaps a "did you mean to import [something] from one of these modules?" message is also displayed when the name is an exact match for an existing unimported function

  • misspelled a function name.

If there are functions within a small edit distance list these as "did you mean...", possibly prioritizing functions which also appear in the list of matches for the type hole

@hdgarrood
Copy link
Contributor

Just to clarify: yes, I think suggesting values of the unknown name which haven't been imported or suggesting values a small edit distance away from that name would both be fantastic, but both of these are probably best tackled separately; for now, just adding the inferred type would probably be best.

@colinwahl
Copy link
Contributor Author

@hdgarrood I agree that the plain type search results without some sort of edit distance comparison isn't the most helpful. I've taken it out, and the error message is now a lot more readable.

@spicydonuts I think there is a lot we can do with edit distance comparison, and I really like the idea of suggesting possibly forgotten imports. I agree with Harry that this could be done in a follow up PR.

For now, I have updated this PR to:

  • remove type search results from UnknownValueHint error
  • move reporting of UnknownValueHint to the module level so that multiple UnknownValueHint errors can be reported for a single module

Here is an updated example:

module Main where

import Prelude

import Effect (Effect)

bar :: Effect Unit
bar = do
  boo <- a 10
  let x = foo "hello"
  pure boo

Previously, this would fail with an UnknownName error, but now it fails with a UnknownValueHint error, stating

[1/2 UnknownValueHint] src/Main.purs:9:10

  9    boo <- a 10
              ^

  Unknown name 'a' has the inferred type

    Int -> Effect Unit

  in value declaration bar

[2/2 UnknownValueHint] src/Main.purs:10:11

  10    let x = foo "hello"
                ^^^

  Unknown name 'foo' has the inferred type

    String -> t0

  in the following context:

    boo :: Unit

  in value declaration bar

  where t0 is an unknown type

src/Language/PureScript/AST/Declarations.hs Outdated Show resolved Hide resolved
src/Language/PureScript/AST/Declarations.hs Outdated Show resolved Hide resolved
src/Language/PureScript/Sugar/Names.hs Outdated Show resolved Hide resolved
src/Language/PureScript/Sugar/Names.hs Outdated Show resolved Hide resolved
@colinwahl
Copy link
Contributor Author

I just realized a problem with the current implementation - the holes created by an unknown value (or just a plain Hole) can spuriously cause AmbiguousTypeVariables or NoInstanceFound caused by unknown types. Here's an example:

module Main where

import Prelude

import Effect (Effect)

bar :: Effect Unit
bar = ?a \_ -> do
  pure 10

And

module Main where

import Prelude

import Effect (Effect)

bar :: Effect Unit
bar = foo \_ -> do
  pure 10

Both fail with

[1/1 NoInstanceFound] src/Main.purs:9:3

  9    pure 10
       ^^^^^^^

  No type class instance was found for

    Control.Applicative.Applicative t0

  The instance head contains unknown type variables. Consider adding a type annotation.

  while applying a function pure
    of type Applicative t0 => t1 -> t0 t1
    to argument 10
  while inferring the type of pure 10
  in value declaration bar

  where t0 is an unknown type
        t1 is an unknown type

While the real error in both of them is that there is a Hole or an unknown value. This is because the UnknownName/Hole errors are first collected as warnings and then escalated as errors later, whereas other errors are thrown immediately.

I'm not sure how big of a deal this is with Holes (or if it can even be helped), but for UnknownName errors, I think that this is incorrect behavior. It should report the unknown name before reporting errors down the line.

I'm thinking of ways to remedy this. One idea that I have is to modify some of the throwErrors to check for UnknownName warnings that have accumulated, and report those first before any other error.

@paf31
Copy link
Contributor

paf31 commented May 10, 2019

You could wrap the original UnknownName error you would have thrown inside the error constructor, and modify the error rendering function to handle that case specially, perhaps.

…or caused by trying to infer the type of a hole
@colinwahl

This comment has been minimized.

Colin Wahl added 2 commits May 21, 2019 14:06
…or rendering function to handle UnknownName and HoleInferredType errors specially
@colinwahl

This comment has been minimized.

@colinwahl
Copy link
Contributor Author

colinwahl commented Jun 6, 2019

I wasn't satisfied with the stateful approach I originally took, so I have created an explicit AST traversal that takes an Expr and returns all of the holes and unknown values within that Expr. This allows throwing all unknown name errors first in the case of a NoInstanceFound or AmbiguousTypeVariables error (which could have been caused by an unknown name hole in the first place). I have also added an extra hint message when the error is possibly caused by a hole, as I illustrated above.

Other than the problems with unknown name holes causing NoInstanceFound or AmbiguousTypeVariables errors (which should be fixed now), this seems to be working really well. I've been using it at work for about a week and I haven't run into any issues.

Let me know of any feedback, and I'd be happy to address it and try to get this over the line! I'm going to address the duplicated error code tomorrow when I get a chance.

@colinwahl
Copy link
Contributor Author

I have made a few changes that hopefully make this PR more focused/easier to review. At first I started to make possible improvements to errors messages for errors that are possibly caused by the existence of typed holes. However, in favor of having this PR be smaller and easier to review, I have reverted those changes and only included changes pertaining to the original issue. I can do follow up work on the other issues in subsequent PRs, if wanted.

The main change since @hdgarrood's last review adds a mechanism to avoid unknown name holes from causing spurious AmbiguousTypeVariable and NoInstanceFound errors. To do this, I added a function which throws UnknownName errors contained in an expression which has an AmbiguousTypeVariable or NoInstanceFound error before throwing said error. Note that this bypasses the logic which raises all unknown value holes into errors at the module level, meaning that if there are unknown names in some other binding group in the module, they will not be reported as errors. However, I think that this case is infrequent enough that this is acceptable.

@colinwahl
Copy link
Contributor Author

Has anyone had a chance to look over this? I'm happy to change my approach with a little guidance, if needed!

@natefaubion
Copy link
Contributor

Note that this bypasses the logic which raises all unknown value holes into errors at the module level, meaning that if there are unknown names in some other binding group in the module, they will not be reported as errors. However, I think that this case is infrequent enough that this is acceptable.

Can you give me an example that triggers this case?

renderSimpleErrorMessage (UnknownName name (Just (ty, ctx))) =
paras $ [ line $ "Unknown " <> printName name <> " has the inferred type "
, markCodeBox (indent (typeAsBox maxBound ty))
] ++ renderContext ctx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something about this error message isn't right to me. I know it was just "Unknown foo" before which is also terrible, but adding "has the inferred type" makes it seem less like an error.

Maybe something like for unqualified

The name foo is not in scope.

The name foo is not in scope. It has the inferred type ...

And for qualified:

The name foo is not exported by module Foo.

The name foo is not exported by module Foo. It has the inferred type ...

@hdgarrood @colinwahl What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of having different errors depending on whether or not the name is qualified! This particular error did seem very terse - I think this is a improvement

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@natefaubion Actually, now that I think about it, this may be missing a couple of cases. You could have some declaration

baz :: Effect Int
baz = do
  Bar.bar

Where the module Bar does export a value bar, but there just isn't an import for it. In this case, I don't think that the error The name bar is not exported by module Foo. Is an appropriate error.

Maybe something like The name bar is not imported from module Bar. ... Would be more accurate. What do you think?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one has actually bitten me one more than one occasion - not very serious - but it can just nudge you into haring off looking for the export in the imported file instead of fixing the import. If you have any kind of doubts about your build system integrity (as seems to be too often the case) it can pitch you into a worse yak-shaving-wild-goose-chase digression there too.

@colinwahl
Copy link
Contributor Author

Note that this bypasses the logic which raises all unknown value holes into errors at the module level, meaning that if there are unknown names in some other binding group in the module, they will not be reported as errors. However, I think that this case is infrequent enough that this is acceptable.

Can you give me an example that triggers this case?

Sure, something like this:

module Test where

import Prelude
import Effect (Effect)

foo :: Effect Unit
foo = b \_ ->
  pure 10

bar = f 10

Will fail with one UnknownName error (for b), instead of two.
Now that I look at it again I think that the error has the wrong source span - I'm looking into that now

@colinwahl
Copy link
Contributor Author

@natefaubion I pushed a proposed update to the typechecking logic for the cases where there is an unknown name in a expression. It alleviates the problem of throwing a spurious (NoInstanceFound/AmbiguousTypeVariables) The two cases that cause problems (NoInstanceFound and AmbiguousTypeVariables) can only be caused when the typechecker is solving unsolved types. To remedy this, one thing we could do is not do the processing of unknown types when an expression contains an unknown name. This will sometimes cause errors like

[3/4 UnknownName] src/Test.purs:10:7

  10  bar = f 10
            ^

  Unknown value f has the inferred type

    Int -> t0

  in value declaration bar

But at least we have given some indication of what the type should be, and we have also prevented the spurious errors that were previously caused. This has the added benefit of not reporting the wrong source spans, like my previous attempt did. What do you think? Is it OK to report unknown types for unknown name errors?

@colinwahl
Copy link
Contributor Author

@natefaubion bumping this PR. I still am unsure of the best way to resolve the AmbiguousTypeVariables/NoInstanceFound errors that can be caused by trying to infer the types of unknown names. I have a proposed solution above but it's definitely not the best.

Thanks!

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

Successfully merging this pull request may close these issues.

None yet

7 participants