Fix error on missing type class name with non-exported superclass (#3132)#3173
Conversation
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
left a comment
There was a problem hiding this comment.
The PR is ready for review now 😄
| 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)) |
There was a problem hiding this comment.
This modification to the reporting made it much easier to debug exactly what was going on with a mistaken error code.
There was a problem hiding this comment.
I'd be in favour of merging this in a separate PR.
There was a problem hiding this comment.
Cool, I'll factor it out then.
| maybe | ||
| (throwError . errorMessage . UnknownClass $ className) | ||
| pure | ||
| $ M.lookup className tcs |
There was a problem hiding this comment.
If there's a more suitable idiom, I'd be happy to give it a shot 🙂
| data B | ||
|
|
||
| instance inst2 :: C2 A | ||
| instance inst3 :: (C2 a) => C3 a B |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
So this PR fixes the
|
|
I think suggestion 2 (@garyb's) makes the most sense. |
|
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 |
|
I think I'd say that the MVar bug is actually within Make, in that any use of As far as I can see, I think it's correct that we use 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. |
|
Cool, I think I can get that implemented 😄 Thanks for the feedback! |
|
Of course, thank you for all your work on this! |
|
I'm actually planning on keeping the work on this branch; mind re-opening? |
| @@ -0,0 +1,18 @@ | |||
| -- @shouldFailWith TransitiveExportError | |||
There was a problem hiding this comment.
Updated the reason this should fail.
There was a problem hiding this comment.
Can you paste the full error you get when you attempt to compile this?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Great, thank you. I was just wondering. :)
There was a problem hiding this comment.
"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 |
| -- ^ 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). |
There was a problem hiding this comment.
Updating this based on comments in Slack
| S.intersection moduleClassDeclarationSet | ||
| $ untilSame | ||
| (\s -> s <> foldMap (\n -> fromMaybe S.empty (M.lookup n classesToSuperClasses)) s) | ||
| (fromMaybe S.empty (M.lookup qname classesToSuperClasses)) |
There was a problem hiding this comment.
This is going to do a lot of recomputation to find the fixed point of that set.
There was a problem hiding this comment.
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 aAnd 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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
The inconsistent use of for_ and case made this hard to parse, so I simplified. I can revert if wanted.
There was a problem hiding this comment.
This definitely seems like an improvement to me 👍
| 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 |
There was a problem hiding this comment.
Maybe unnecessary, but it can't hurt to be sure.
There was a problem hiding this comment.
Perhaps it would be better to do this in a separate PR?
kritzcreek
left a comment
There was a problem hiding this comment.
I'll take a more thorough look later, this already looks like a very good start though!
| Nothing) | ||
| $ decls | ||
| moduleClassDeclarationSet :: S.Set (Qualified (ProperName 'ClassName)) | ||
| moduleClassDeclarationSet = |
There was a problem hiding this comment.
Could just use Data.Map.keysSet here
| 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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Can this change be reverted now?
| for_ dctors $ \dctors' -> | ||
| for_ dctors' $ \dctor -> | ||
| for_ (M.lookup (qualify' dctor) (dataConstructors env)) $ \(_, _, ty, _) -> | ||
| checkExport dr extract ty |
There was a problem hiding this comment.
This definitely seems like an improvement to me 👍
| S.intersection moduleClassDeclarationSet | ||
| $ untilSame | ||
| (\s -> s <> foldMap (\n -> fromMaybe S.empty (M.lookup n classesToSuperClasses)) s) | ||
| (fromMaybe S.empty (M.lookup qname classesToSuperClasses)) |
There was a problem hiding this comment.
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 aAnd 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?
| findClasses = everythingOnTypes (++) go | ||
| where | ||
| go (ConstrainedType c _) = (fmap (TypeClassRef (declRefSourceSpan ref)) . extractCurrentModuleClass . constraintClass) c | ||
| go (ConstrainedType c _) = (fmap (TypeClassRef (declRefSourceSpan ref)) . extractCurrentModuleClass . constraintClass) c |
| @@ -0,0 +1,18 @@ | |||
| -- @shouldFailWith TransitiveExportError | |||
There was a problem hiding this comment.
Can you paste the full error you get when you attempt to compile this?
|
Only grabbing transitive dependencies on rethrowing the error: |
| qualify' :: a -> Qualified a | ||
| qualify' = Qualified (Just mn) | ||
|
|
||
| annotateSuperClasses |
There was a problem hiding this comment.
Could you please add a comment summarising why we only obtain transitive superclasses after the immediate check has failed?
| -- been exported from the module | ||
| checkClassesAreExported :: DeclarationRef -> m () | ||
| checkClassesAreExported ref = checkMemberExport findClasses ref | ||
| checkClassesAreExported ref = checkMemberExport findClasses ref |
There was a problem hiding this comment.
Sorry to nitpick but would you mind reverting this? I'm just trying to keep git blame useful where we can.
There was a problem hiding this comment.
Not a problem at all, I should have caught it :)
| unless (null unexportedClasses) | ||
| . throwError . errorMessage' (declRefSourceSpan dr) | ||
| . TransitiveExportError dr | ||
| $ mapMaybe (\n -> M.lookup n classMap) (toList unexportedClasses) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Great catch. Merged.
| classesToSuperClasses <- gets | ||
| ( M.map | ||
| ( S.fromList | ||
| . fmap constraintClass |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
okay, that's a much better idea 😂
|
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. |
|
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. |
|
Yeah, the same thing happens with the other types of |
| unless (null unexported) | ||
| . throwError . errorMessage' (declRefSourceSpan dr) | ||
| . TransitiveExportError dr | ||
| $ mapMaybe (\n -> M.lookup n classMap) (toList transitiveSuperClasses) |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
Great call :) That cut out a lot of code.
|
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 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) |
|
I really appreciate the feedback and suggestions 😄 |
hdgarrood
left a comment
There was a problem hiding this comment.
Looks great, thanks very much!
kritzcreek
left a comment
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
We have a helper for this: filter (isQualifiedWith mn)
|
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 |
This PR adds some test cases for #3132 and might even have a fix 😄
Any call to
internalErrorwill trigger anMVarbug. That should be fixed too. But the real underlying bug is that superclasses must be exported.