Skip to content

Fix error on missing type class name with non-exported superclass (#3132)#3173

Merged
hdgarrood merged 24 commits into
purescript:masterfrom
parsonsmatt:matt/3132
Jan 20, 2018
Merged

Fix error on missing type class name with non-exported superclass (#3132)#3173
hdgarrood merged 24 commits into
purescript:masterfrom
parsonsmatt:matt/3132

Conversation

@parsonsmatt

@parsonsmatt parsonsmatt commented Dec 19, 2017

Copy link
Copy Markdown
Contributor

This PR adds some test cases for #3132 and might even have a fix 😄

Any call to internalError will trigger an MVar bug. That should be fixed too. But the real underlying bug is that superclasses must be exported.

  1) Passing examples '3132.purs' should compile and run without error
       Error found:
       in module Main
       at /home/matt/Projects/purescript/examples/passing/3132.purs line 19, column 1 - line 1
9, column 34

         No type class instance was found for class

           ClassDefinitions.Zero

         because the class was not in scope. Perhaps it was not exported.

       while solving type class constraint

         ClassDefinitions.Zero

       while checking that expression #dict Zero
         has type Zero
       in value declaration inst3

       See https://github.com/purescript/documentation/blob/master/errors/UnknownClass.md for
more information,
       or to contribute content related to this error.
When you provide a concrete type that satisfies the instance, all is
well. So the bug only triggers when you ask for the instance in the
instance context.

@parsonsmatt parsonsmatt left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The PR is ready for review now 😄

Comment thread tests/TestCompiler.hs
else Just $ "Expected these errors: " ++ show expected ++ ", but got these: " ++ show actual
else Just $ "Expected these errors: " ++ show expected ++ ", but got these: "
++ show actual ++ ", full error messages: "
++ unlines (map (P.renderBox . P.prettyPrintSingleError P.defaultPPEOptions) (P.runMultipleErrors errs))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This modification to the reporting made it much easier to debug exactly what was going on with a mistaken error code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd be in favour of merging this in a separate PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Cool, I'll factor it out then.

maybe
(throwError . errorMessage . UnknownClass $ className)
pure
$ M.lookup className tcs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If there's a more suitable idiom, I'd be happy to give it a shot 🙂

Comment thread examples/failing/3132.purs Outdated
data B

instance inst2 :: C2 A
instance inst3 :: (C2 a) => C3 a B

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It seems like the effect of having a "hidden" nullary class/instance is to prevent polymorphic instances like this. Note that C2 A is totally fine, despite C1 not being in scope, and C3 A B is also totally fine. But requiring C2 a => C3 a B causes the missing name error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hm, I'm not sure. It doesn't seem right to me that this code would cause an unknown class error from C1 not being in scope, and it also doesn't seem right to me that hidden nullary classes can prevent you from writing instances like this.

My suggestion is that we say this code should actually compile successfully, and in fact we need to modify the compiler to make sure that hidden classes like C1 are made available in the relevant places in the compiler to make sure that internal error doesn't get hit.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the real problem with all this, is that the ClassDefinitions module should demand that you export C1 because it's required by C2. That would be the rule we use for all the other cases for this kind of thing.

@parsonsmatt parsonsmatt changed the title Fix "Operation blocked on MVar" problem with explicit export lists (#3132) Fix error on missing type class name with non-exported superclass (#3132) Dec 20, 2017
@parsonsmatt

Copy link
Copy Markdown
Contributor Author

So this PR fixes the MVar bug and properly reports the missing class constraint. It appears this has also uncovered some behavior that we want to modify, with two suggestions:

  1. My suggestion is that we say this code should actually compile successfully, and in fact we need to modify the compiler to make sure that hidden classes like C1 are made available in the relevant places in the compiler to make sure that internal error doesn't get hit.

  2. I think the real problem with all this, is that the ClassDefinitions module should demand that you export C1 because it's required by C2. That would be the rule we use for all the other cases for this kind of thing.

@hdgarrood

Copy link
Copy Markdown
Contributor

I think suggestion 2 (@garyb's) makes the most sense.

@parsonsmatt

Copy link
Copy Markdown
Contributor Author

Do we want to merge this as-is, and create another issue/PR for that, or work that change into this PR? I think that's going to require a good bit more work

@hdgarrood

Copy link
Copy Markdown
Contributor

I think I'd say that the MVar bug is actually within Make, in that any use of error in one of the threads in there manifests as an error whose message implies that there's a bug in the threading code when there isn't. That is, I don't think I'd say that this PR fixes the MVar bug.

As far as I can see, I think it's correct that we use internalError in TypeChecker.Entailment there, because if we go down that code path, it means there's a bug somewhere else in the compiler. Therefore, I think we want this to manifest as an internal compiler error (as it does now) rather than a regular compiler error.

Given this, I think the best course of action is to not merge this, and instead to expand the the TransitiveExportError check to include superclass constraints.

@parsonsmatt

Copy link
Copy Markdown
Contributor Author

Cool, I think I can get that implemented 😄 Thanks for the feedback!

@hdgarrood

Copy link
Copy Markdown
Contributor

Of course, thank you for all your work on this!

@hdgarrood hdgarrood closed this Dec 21, 2017
@parsonsmatt

Copy link
Copy Markdown
Contributor Author

I'm actually planning on keeping the work on this branch; mind re-opening?

@hdgarrood hdgarrood reopened this Dec 21, 2017
@@ -0,0 +1,18 @@
-- @shouldFailWith TransitiveExportError

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the reason this should fail.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you paste the full error you get when you attempt to compile this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Failing examples
  '3132.purs' should fail with 'UnknownName' FAILED [1]

Failures:

  tests/TestCompiler.hs:218:
  1) Failing examples '3132.purs' should fail with 'UnknownName'
       Expected these errors: ["UnknownName"], but got these: ["TransitiveExportError"], full error messages:
       in module Main
       at /home/matt/Projects/purescript/examples/failing/3132.purs line 1, column 1 - line 18, column 7

         An export for class C3 requires the following to also be exported:

           class C1
           class C2



       See https://github.com/purescript/documentation/blob/master/errors/TransitiveExportError.md for more information,
       or to contribute content related to this error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great, thank you. I was just wondering. :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

"Does this fail with the right type of error, but also the right reason that I am expecting?" is definitely a good thing to check 😄

| AnonymousArgument
-- |
-- A typed hole that will be turned into a hint/error duing typechecking
-- A typed hole that will be turned into a hint/error during typechecking

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Minor typo fix

-- ^ Available type class dictionaries
-- ^ Available type class dictionaries. When looking up 'Nothing' in the
-- outer map, this returns the map of type class dictionaries in local
-- scope (ie dictionaries brought in by a constrained type).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updating this based on comments in Slack

Comment thread src/Language/PureScript/TypeChecker.hs Outdated
S.intersection moduleClassDeclarationSet
$ untilSame
(\s -> s <> foldMap (\n -> fromMaybe S.empty (M.lookup n classesToSuperClasses)) s)
(fromMaybe S.empty (M.lookup qname classesToSuperClasses))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is going to do a lot of recomputation to find the fixed point of that set.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suppose you could argue that it's not necessary to obtain transitive superclasses, only immediate ones, as I think this check will fail in the transitive case if and only if it fails in the immediate case. Obtaining transitive dependencies just means we get a more useful error if it does fail, I think?

E.g. If we're exporting C3 here, we also need to export C2 and C1:

module X (class C3) where
class C1 a
class (C1 a) <= C2 a
class (C2 a) <= C3 a

And whether we obtain transitive dependencies here or not, we're going to fail with a transitive export error; obtaining transitive superclasses presumably just means that we have a more useful error, which lists both C2 and C1 as opposed to only C2.

Perhaps as a middle ground we could only obtain immediate superclasses in the first pass, and then if there are any errors, obtain transitive superclasses for the offending classes for the sake of a more detailed error? Does that sound feasible?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That is a really good point -- we're already checking every exported item in the list, so we only need to verify that the immediate superclasses (also defined in the module) are exported. The transitive superclasses defined in module would be caught on when they're being inspected 😄

You're correct that this method does provide a nicer error message, in that you'll get one error message telling you to add all the things to the exports.

We can get "best of both worlds" if we can catch the error and add the lookup for transitive superclasses there before rethrowing.

for_ dctors $ \dctors' ->
for_ dctors' $ \dctor ->
for_ (M.lookup (qualify' dctor) (dataConstructors env)) $ \(_, _, ty, _) ->
checkExport dr extract ty

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The inconsistent use of for_ and case made this hard to parse, so I simplified. I can revert if wanted.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This definitely seems like an improvement to me 👍

Comment thread src/Language/PureScript/Types.hs Outdated
go t@(TypeApp t1 t2) = f t <+> go t1 <+> go t2
go t@(ForAll _ ty _) = f t <+> go ty
go t@(ConstrainedType c ty) = foldl (<+>) (f t) (map go (constraintArgs c)) <+> go ty
go t@(ConstrainedType c ty) = foldl' (<+>) (f t) (map go (constraintArgs c)) <+> go ty

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maybe unnecessary, but it can't hurt to be sure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps it would be better to do this in a separate PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure thing 🙂

@kritzcreek kritzcreek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'll take a more thorough look later, this already looks like a very good start though!

Comment thread src/Language/PureScript/TypeChecker.hs Outdated
Nothing)
$ decls
moduleClassDeclarationSet :: S.Set (Qualified (ProperName 'ClassName))
moduleClassDeclarationSet =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could just use Data.Map.keysSet here

Comment thread src/Language/PureScript/Types.hs Outdated
go t@(TypeApp t1 t2) = f t <+> go t1 <+> go t2
go t@(ForAll _ ty _) = f t <+> go ty
go t@(ConstrainedType c ty) = foldl (<+>) (f t) (map go (constraintArgs c)) <+> go ty
go t@(ConstrainedType c ty) = foldl' (<+>) (f t) (map go (constraintArgs c)) <+> go ty

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps it would be better to do this in a separate PR?

-- for all implied superclass instances.
newDictionaries
:: MonadState CheckState m
:: (MonadState CheckState m, MonadError MultipleErrors m)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can this change be reverted now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes!

for_ dctors $ \dctors' ->
for_ dctors' $ \dctor ->
for_ (M.lookup (qualify' dctor) (dataConstructors env)) $ \(_, _, ty, _) ->
checkExport dr extract ty

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This definitely seems like an improvement to me 👍

Comment thread src/Language/PureScript/TypeChecker.hs Outdated
S.intersection moduleClassDeclarationSet
$ untilSame
(\s -> s <> foldMap (\n -> fromMaybe S.empty (M.lookup n classesToSuperClasses)) s)
(fromMaybe S.empty (M.lookup qname classesToSuperClasses))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suppose you could argue that it's not necessary to obtain transitive superclasses, only immediate ones, as I think this check will fail in the transitive case if and only if it fails in the immediate case. Obtaining transitive dependencies just means we get a more useful error if it does fail, I think?

E.g. If we're exporting C3 here, we also need to export C2 and C1:

module X (class C3) where
class C1 a
class (C1 a) <= C2 a
class (C2 a) <= C3 a

And whether we obtain transitive dependencies here or not, we're going to fail with a transitive export error; obtaining transitive superclasses presumably just means that we have a more useful error, which lists both C2 and C1 as opposed to only C2.

Perhaps as a middle ground we could only obtain immediate superclasses in the first pass, and then if there are any errors, obtain transitive superclasses for the offending classes for the sake of a more detailed error? Does that sound feasible?

Comment thread src/Language/PureScript/TypeChecker.hs Outdated
findClasses = everythingOnTypes (++) go
where
go (ConstrainedType c _) = (fmap (TypeClassRef (declRefSourceSpan ref)) . extractCurrentModuleClass . constraintClass) c
go (ConstrainedType c _) = (fmap (TypeClassRef (declRefSourceSpan ref)) . extractCurrentModuleClass . constraintClass) c

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Trailing space here

@@ -0,0 +1,18 @@
-- @shouldFailWith TransitiveExportError

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you paste the full error you get when you attempt to compile this?

@parsonsmatt

Copy link
Copy Markdown
Contributor Author

Only grabbing transitive dependencies on rethrowing the error:

  1) Failing examples '3132.purs' should fail with 'UnknownName'
       Expected these errors: ["UnknownName"], but got these: ["TransitiveExportError"], full error messages:
       in module Main
       at /home/matt/Projects/purescript/examples/failing/3132.purs line 1, column 1 - line 18, column 7

         An export for class C3 requires the following to also be exported:

           class C1
           class C2



       See https://github.com/purescript/documentation/blob/master/errors/TransitiveExportError.md for more information,
       or to contribute content related to this error.

Comment thread src/Language/PureScript/TypeChecker.hs Outdated
qualify' :: a -> Qualified a
qualify' = Qualified (Just mn)

annotateSuperClasses

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you please add a comment summarising why we only obtain transitive superclasses after the immediate check has failed?

Comment thread src/Language/PureScript/TypeChecker.hs Outdated
-- been exported from the module
checkClassesAreExported :: DeclarationRef -> m ()
checkClassesAreExported ref = checkMemberExport findClasses ref
checkClassesAreExported ref = checkMemberExport findClasses ref

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry to nitpick but would you mind reverting this? I'm just trying to keep git blame useful where we can.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not a problem at all, I should have caught it :)

Comment thread src/Language/PureScript/TypeChecker.hs Outdated
unless (null unexportedClasses)
. throwError . errorMessage' (declRefSourceSpan dr)
. TransitiveExportError dr
$ mapMaybe (\n -> M.lookup n classMap) (toList unexportedClasses)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems to me that there's a bit of a code smell here as we chuck away the second argument of TransitiveExportError here almost immediately, i.e. once we reach annotateSuperClasses. Have you considered consolidating annotateSuperClasses and this into a single function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great catch. Merged.

classesToSuperClasses <- gets
( M.map
( S.fromList
. fmap constraintClass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rather than performing the intersections below to make sure we only keep local superclasses*, can we do a filter here which inspects the Constraint values and only keeps the superclasses which have been defined in the current module? That way, if I understand correctly, we won't need to construct the moduleClassDeclarations map (which saves us from iterating over all of the declarations in the module).

*that is the reason for the intersections below, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

okay, that's a much better idea 😂

@hdgarrood

Copy link
Copy Markdown
Contributor

I also just noticed that the source span for the error is a bit dodgy - it seems to cover almost the entire file! I guess ideally it would only point to the part of the export list which is exporting the offending class.

@hdgarrood

Copy link
Copy Markdown
Contributor

Oh, maybe that's a problem in the part of the compiler that elaborates exports though, so perhaps best not to worry about that here.

@hdgarrood

Copy link
Copy Markdown
Contributor

Yeah, the same thing happens with the other types of TransitiveExportError currently, so let's address that separately.

Comment thread src/Language/PureScript/TypeChecker.hs Outdated
unless (null unexported)
. throwError . errorMessage' (declRefSourceSpan dr)
. TransitiveExportError dr
$ mapMaybe (\n -> M.lookup n classMap) (toList transitiveSuperClasses)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can get rid of the classMap argument here; the DeclarationRef values in the second argument to TransitiveExportError aren't intended to have real source spans (since they are things which are supposed to be in the exports list but aren't), so I think we can just reuse the one from dr. That is what the other TransitiveExportError checks seem to be doing, too.

Do you think it would work to use map (TypeClassRef (declRefSourceSpan dr)) on this line instead of mapMaybe (\n -> M.lookup n classMap)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great call :) That cut out a lot of code.

@hdgarrood

Copy link
Copy Markdown
Contributor

Great! Sorry to keep making suggestions, but now that you've been able to remove all that code I have just one last one: I quite like how typeCheckModule reads for the bit until you get to the first where, and I think it would be nice to preserve that. Could you perhaps extract all the code you've added there into a separate function, so that we end up with something like this?

   warnAndRethrow (addHint (ErrorInModule mn)) $ do
     modify (\s -> s { checkCurrentModule = Just mn })
     decls' <- typeCheckAll mn exps decls
     checkSuperClassesAreExported <- getSuperClassExportCheck
     for_ exps $ \e -> do
       checkTypesAreExported e
       checkClassMembersAreExported e
       checkClassesAreExported e
       checkSuperClassesAreExported e
     return $ Module ss coms mn decls' (Just exps)

@parsonsmatt

Copy link
Copy Markdown
Contributor Author

I really appreciate the feedback and suggestions 😄

@hdgarrood hdgarrood left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks great, thanks very much!

@kritzcreek kritzcreek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Works for me, took me a while to follow the code, but I'd say that's just inherent to the problem being tackled here, lots of indirections.

classesToSuperClasses <- gets
( M.map
( S.fromList
. filter (\(Qualified mn' _) -> mn' == Just mn)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We have a helper for this: filter (isQualifiedWith mn)

@hdgarrood

Copy link
Copy Markdown
Contributor

Thanks @kritzcreek! I think I'll just merge this now to make sure it doesn't rot. Maybe I (or anyone who feels like it) could look through the compiler to find places we can use isQualifiedWith; I imagine this won't be the only one.

@hdgarrood hdgarrood merged commit 950f184 into purescript:master Jan 20, 2018
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.

4 participants