Content-Length: 782733 | pFad | https://github.com/purescript/purescript/pull/4411

8C Mention which row label the type error occurs on by FredTheDino · Pull Request #4411 · purescript/purescript · GitHub
Skip to content

Mention which row label the type error occurs on #4411

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

Merged

Conversation

FredTheDino
Copy link
Contributor

@FredTheDino FredTheDino commented Oct 27, 2022

Description of the change
Writing code is hard, we make errors. Better errors makes it easier to write code.
This PR makes it clear which row-label type errors occur on.

Example of the previous error message for this code:

a :: { f :: Int }
a = { f: 1 }

b :: { f :: String }
b = a -- Oh noes!
Error found:
...

  Could not match type

    Int

  with type

    String


while checking that type { f :: Int
                         , qa :: Int
                         , qb :: Int
                         , qc :: Int
                         , qd :: Int
                         }
...

Now it reads:

Error found:
...

  Could not match type

    Int

  with type

    String


while matching row label f
while checking that type { f :: Int
                         , qa :: Int
                         , qb :: Int
                         , qc :: Int
                         , qd :: Int
                         }

This is very helpful when you have a lot of labels in your records (150) which I sometimes have and the pretty printer for type errors cuts of the records halfway through or you have to manually differentiate these to find out what's wrong.

Needed guidance

  • Is the error formatting good?
  • Haskell formatting and style opinions are welcome

Related to #4005

Checklist:

  • Added a file to CHANGELOG.d for this PR (see CHANGELOG.d/README.md)
  • Added myself to CONTRIBUTORS.md (if this is my first contribution)
  • Linked any existing issues or proposals that this pull request should close
  • Updated or added relevant documentation
  • Added a test for the contribution (if applicable)

Copy link
Member

@purefunctor purefunctor left a comment

Choose a reason for hiding this comment

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

Just a quick round-up of the changes

@MonoidMusician
Copy link
Contributor

I think this is the next place you'll want to hit to add label information for subsumption:

subsumes' mode (TypeApp _ f1 r1) (TypeApp _ f2 r2) | eqType f1 tyRecord && eqType f2 tyRecord = do
let (common, ((ts1', r1'), (ts2', r2'))) = alignRowsWith (subsumes' SNoElaborate) r1 r2
-- For { ts1 | r1 } to subsume { ts2 | r2 } when r1 is empty (= we're working with a closed row),
-- every property in ts2 must appear in ts1. If not, then the candidate expression is missing a required property.
-- Conversely, when r2 is empty, every property in ts1 must appear in ts2, or else the expression has
-- an additional property which is not allowed.
when (isREmpty r1')
(for_ (firstMissingProp ts2' ts1') (throwError . errorMessage . PropertyIsMissing . rowListLabel))
when (isREmpty r2')
(for_ (firstMissingProp ts1' ts2') (throwError . errorMessage . AdditionalProperty . rowListLabel))
-- Check subsumption for common labels
sequence_ common

Probably need to add a alignRowsWithLabel helper next to alignRowsWith, or something:

-- | Align two rows of types, splitting them into three parts:
--
-- * Those types which appear in both rows
-- * Those which appear only on the left
-- * Those which appear only on the right
--
-- Note: importantly, we preserve the order of the types with a given label.
alignRowsWith
:: (Type a -> Type a -> r)
-> Type a
-> Type a
-> ([r], (([RowListItem a], Type a), ([RowListItem a], Type a)))

@FredTheDino
Copy link
Contributor Author

FredTheDino commented Nov 3, 2022

I think this is the next place you'll want to hit to add label information for subsumption:

subsumes' mode (TypeApp _ f1 r1) (TypeApp _ f2 r2) | eqType f1 tyRecord && eqType f2 tyRecord = do
let (common, ((ts1', r1'), (ts2', r2'))) = alignRowsWith (subsumes' SNoElaborate) r1 r2
-- For { ts1 | r1 } to subsume { ts2 | r2 } when r1 is empty (= we're working with a closed row),
-- every property in ts2 must appear in ts1. If not, then the candidate expression is missing a required property.
-- Conversely, when r2 is empty, every property in ts1 must appear in ts2, or else the expression has
-- an additional property which is not allowed.
when (isREmpty r1')
(for_ (firstMissingProp ts2' ts1') (throwError . errorMessage . PropertyIsMissing . rowListLabel))
when (isREmpty r2')
(for_ (firstMissingProp ts1' ts2') (throwError . errorMessage . AdditionalProperty . rowListLabel))
-- Check subsumption for common labels
sequence_ common

Probably need to add a alignRowsWithLabel helper next to alignRowsWith, or something:

-- | Align two rows of types, splitting them into three parts:
--
-- * Those types which appear in both rows
-- * Those which appear only on the left
-- * Those which appear only on the right
--
-- Note: importantly, we preserve the order of the types with a given label.
alignRowsWith
:: (Type a -> Type a -> r)
-> Type a
-> Type a
-> ([r], (([RowListItem a], Type a), ([RowListItem a], Type a)))

I think I've already changed that, and I think you're referencing into a branch. Currently it is:

subsumes' mode (TypeApp _ f1 r1) (TypeApp _ f2 r2) | eqType f1 tyRecord && eqType f2 tyRecord = do
let goWithLabel l t1 t2 = withErrorMessageHint (ErrorInRowLabel l) $ subsumes' SNoElaborate t1 t2
let (common, ((ts1', r1'), (ts2', r2'))) = alignRowsWith goWithLabel r1 r2
-- For { ts1 | r1 } to subsume { ts2 | r2 } when r1 is empty (= we're working with a closed row),
-- every property in ts2 must appear in ts1. If not, then the candidate expression is missing a required property.
-- Conversely, when r2 is empty, every property in ts1 must appear in ts2, or else the expression has
-- an additional property which is not allowed.
when (isREmpty r1')
(for_ (firstMissingProp ts2' ts1') (throwError . errorMessage . PropertyIsMissing . rowListLabel))
when (isREmpty r2')
(for_ (firstMissingProp ts1' ts2') (throwError . errorMessage . AdditionalProperty . rowListLabel))
-- Check subsumption for common labels

-- | Align two rows of types, splitting them into three parts:
--
-- * Those types which appear in both rows
-- * Those which appear only on the left
-- * Those which appear only on the right
--
-- Note: importantly, we preserve the order of the types with a given label.
alignRowsWith
:: (Label -> Type a -> Type a -> r)
-> Type a
-> Type a
-> ([r], (([RowListItem a], Type a), ([RowListItem a], Type a)))

But I changed the alignRowsWith function so the first given function also takes a label. I don't know which other places to poke in to get more of these row merge errors. But I can probably add some more error messages in other places. I feel like it's smart to start with the low hanging fruit.

What more is required for this to get merged?

@MonoidMusician
Copy link
Contributor

You're right, my bad! Since we're checking that a record literal ({ a: "a" }) has a known type ({ a :: Int }), we stay in checking mode, we are not doing subsumption yet. So the place that happens is actually here:

-- |
-- Check the type of a collection of named record fields
--
-- The @lax@ parameter controls whether or not every record member has to be provided. For object updates, this is not the case.
--
checkProperties
:: (MonadSupply m, MonadState CheckState m, MonadError MultipleErrors m, MonadWriter MultipleErrors m)
=> Expr
-> [(PSString, Expr)]
-> SourceType
-> Bool
-> m [(PSString, Expr)]

I think if we tag those errors there with your ErrorInRowLabel hint, your NestedRecordLabelOnTypeError test should have that label information now too.

Other than that I think this is looking good. I'd like to give it some testing locally of course.

@rhendric
Copy link
Member

rhendric commented Nov 3, 2022

I'm not sure we want the hint there. The error already points to the precise location in the source where the issue is, which is better than a hint.

@@ -419,6 +419,7 @@ unifyKindsWithFailure
-> m ()
unifyKindsWithFailure onFailure = go
where
goWithLabel l t1 t2 = withErrorMessageHint (ErrorInRowLabel l) $ go t1 t2
Copy link
Member

Choose a reason for hiding this comment

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

Instead of defining these local helpers in three different places, could you add a helper next to alignRowsWith called

alignRowsWithHinted
  :: (MonadState CheckState m, MonadError MultipleErrors m)
  => (Type a -> Type a -> m ())
  -> Type a -> Type a -> (m (), (([RowListItem a], Type a), ([RowListItem a], Type a)))

(note the absence of Label in this type), which simply delegates to your updated alignRowsWith and adds the hint? (I'd also factor the sequence_ call that co-occurs with all of these call sites into this helper, which is why m () and not [m ()].)

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've finally had enough time to look at this. But this is not really possible to do. We get a dependency cycle:

Module imports form a cycle:
        module ‘Language.PureScript.Types’ (src/Language/PureScript/Types.hs)
        imports module ‘Language.PureScript.Errors’ (src/Language/PureScript/Errors.hs)
  which imports module ‘Language.PureScript.Types’ (src/Language/PureScript/Types.hs)

I'm not sure I want to touch this much code and if this is the better change. It would have been great if it worked though. I'm all for exposing less and not moving complexity to the call sites. Curse your cyclic dependencies!

Copy link
Member

Choose a reason for hiding this comment

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

Oh, that's unfortunate. Fine to leave as-is, then.

@MonoidMusician
Copy link
Contributor

Perhaps, but you still need to manually keep track of how you got there since it doesn't tell you where the type is coming from. If the field is hidden behind a lot of type synonyms, I think it would be helpful to just see what label is failing at first glance.

I guess with verbose errors we see a little bit more context, but it's pretty rare that I actually enable that flag:

  Could not match type
          
    String
          
  (defined in module Prim)
  with type
       
    Int
       
  (defined in module Prim)

while trying to match type String
  with type Int
while checking that type String
  is at least as general as type Int
while checking that expression "a"
  has type Int
while checking that expression { a: "a"
                               }       
  has type { a :: Int
           }         
in value declaration record

@rhendric
Copy link
Member

rhendric commented Nov 3, 2022

Not sure I understand you. If we're checking a record literal, the thing that gets underlined in an IDE will be the field in the record literal; the label is right there in the expression, so it doesn't matter if the label in the type is hidden behind a synonym. If we're checking a non-literal term, the compiler will attempt to unify the type of that term with the expected type and the code path that already has hints will be taken.

@MonoidMusician
Copy link
Contributor

Well I think the root of my dissatisfaction is that the non-verbose hints jump straight to “checking expression "a"”:

while checking that expression "a"
  has type Int
in value declaration record

without an indication of how the typechecker got there from checking record = …. I think there should be some form of breadcrumbs there even in non-verbose mode. Otherwise it looks like it just picked the type Int out of thin air. I know you can reconstruct it from the source, but still, adding a hint “while matching label a” looks awfully convenient to me …

@FredTheDino
Copy link
Contributor Author

FredTheDino commented Nov 4, 2022

I'm not sure I understand the reasoning from @rhendric.
Is your reasoning that:

  • We point to the source location of the error
  • From this information we can deduce the label by looking at the code and the error
  • It's better to not touch code
    If these are your arguments I wonder why you don't think these warnings are useful information?

On one level I feel like this adds a bit of noise to the output of the error messages. But I also think it adds helpful information for understanding the type errors quickly and easily.

Adding a bit of extra information can probably help them find the errors quicker and easier even if they're using an IDE.

TL;DR:
I really agree with @MonoidMusician that informing users of how we got to the error is convenient and helpful. It aids understanding, troubleshooting and makes purescript a friendlier language.

@rhendric
Copy link
Member

rhendric commented Nov 4, 2022

I'm not making any sort of argument about touching code. Certainly the amount of code that would be involved here would be justified by improving the error message. I only disagree that, in the narrow case currently under discussion, a label hint is an improvement.

Your code is going to look like:

blah blah blah {
  label1: blah blah,
  label2: blah blah,
  label3: this is a problem,
  label4: blah blah
}

The error message is going to tell you that this is a problem is not the correct type. It'll point you to line and column, which one can visit manually or see highlighted in an IDE.

What value is added by adding a line to the error message that the problem was detected while checking label3? Of course we're checking label3! You'd hardly expect to see an error about this is a problem while checking label1!

The cost is that the more noise we add to our hints (which, IMO, are already on the noisy side), the less useful they are in aggregate for pointing users at what they need to know.

When we aren't talking about record literals, the hints you're adding in this PR certainly add value, because the error message no longer points to a specific label. I have no issue whatsoever with adding hints in those cases; the cost is justified.

I am all for making our error messages more convenient and more helpful. This PR does that, and I approve. More information is not always more helpful, though; one has to consider whether the information is necessary or redundant.

Copy link
Member

@rhendric rhendric left a comment

Choose a reason for hiding this comment

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

I am unimpressed with the latest two commits. I think the functionality added there is unnecessary, as I've previously argued, but I'm willing to accept being outvoted on that. However, the side effect is that actively misleading hints have been added to other scenarios, as demonstrated in the other outputs that needed to change, and this I'm going to hold my ground on as a bad thing. Please at the very least make sure that these cases aren't changed by what you want to do.

ps'' <- go ps' (delete (Label p, ty) ts) r
return $ (p, v') : ps''
go ((p,v):ps') ts r =
withErrorMessageHint (ErrorInRowLabel (Label p))
Copy link
Contributor

Choose a reason for hiding this comment

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

This is currently applying recursively for each call of go, resulting in extra labels showing up. It just needs to apply to infer/check below.

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 made these changes, it didn't fix all of the comments rhendic mentioned earlier. If you want to look for yourself you can see them here: e136c2a

I think the right decision is to not add the information there. I'd rather see this merged and then move to cleaning up the error messages in general. :D

Thanks for the input! :)

@MonoidMusician
Copy link
Contributor

I looked into better errors for the instances. The problem is that by the time they're typechecked, they're just tagged as value declarations not as instance declarations, so we aren't getting good hints for them in the first place. If we want to do anything better wrt to this patch (e.g. hiding these new hints for instance declarations, or changing them to say “while checking member compare”) we would need to fix that first. Personally I'm less concerned that the internal desugaring details are leaking through given that this was already happening (#dict Su and Su$Dict), it's out of scope for this PR to fix all of that.

@rhendric
Copy link
Member

rhendric commented Nov 9, 2022

In that case I would prefer to merge this without the most recent commits. The goal is to make the error messages more helpful, which this PR achieves even without adding hints on the case where hints are least necessary. If the PR adds hints that are sometimes helpful and sometimes misleading, I think that's a worse outcome. (It trains users to ignore the hint, and we have too many hints that, speaking only for myself, I already reflexively ignore.)

@FredTheDino
Copy link
Contributor Author

In that case I would prefer to merge this without the most recent commits. The goal is to make the error messages more helpful, which this PR achieves even without adding hints on the case where hints are least necessary. If the PR adds hints that are sometimes helpful and sometimes misleading, I think that's a worse outcome. (It trains users to ignore the hint, and we have too many hints that, speaking only for myself, I already reflexively ignore.)

I agree. Some of these hints are confusing. Errors overall can use some cleaning. I've reverted the last two commits. It's better we add something that is a clear improvement - if we want to add them we add them later.
Thanks for all the feedback @rhendric and @MonoidMusician!

Is there anything more that anyone wants to see regarding this PR. Otherwise I'd love to see this get merged. :D

@FredTheDino FredTheDino requested review from rhendric and MonoidMusician and removed request for rhendric and MonoidMusician November 12, 2022 15:10
Copy link
Member

@rhendric rhendric left a comment

Choose a reason for hiding this comment

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

You have my approval conditional on addressing the minor feedback on the last test ☑.

@FredTheDino
Copy link
Contributor Author

I'd love to see this merged. What more do you want me to do?

@rhendric
Copy link
Member

rhendric commented Jan 9, 2023

Appreciate your engagement! Currently, what this PR needs is a second maintainer to review it.

@JordanMartinez
Copy link
Contributor

I haven't forgotten about this PR. I anticipate re-reviewing this sometime in the next two weeks. Sorry for the delay. I've been focusing my free time on porting a JS library to PureScript.

Copy link
Contributor

@JordanMartinez JordanMartinez left a comment

Choose a reason for hiding this comment

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

LGTM. I'm grateful for all the others who reviewed this as it feels like I'm more rubber-stamping this than anything else.

@JordanMartinez
Copy link
Contributor

I don't have enough time to write up a cleaner commit message that summarizes the work down here and reasons for why other work was NOT done here, so I'm not going to merge this yet. Perhaps another core team member can do that if I don't get to it sooner.

Copy link
Contributor

@MonoidMusician MonoidMusician left a comment

Choose a reason for hiding this comment

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

Hey @FredTheDino, sorry I haven't had a chance to get back to this earlier! Been busy with things especially work on the registry.

I gave it a test run and it's working well so I'm happy to merge it. There is a small issue with the non-verbose hints still being a bit too verbose that I will look at addressing in a later PR (we could trim out some while matching type X with type Y in between row labels, it only shows up in some circumstances). I will start looking at your other PR again soon.

Thanks for your efforts!

@MonoidMusician MonoidMusician merged commit 7a5b2b8 into purescript:master Feb 1, 2023
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.

5 participants








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: https://github.com/purescript/purescript/pull/4411

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy