Infix operators parsing oddity?

When parsing the below function:

putStrLn $ 1 `foo` 2

-ddump-parsed-ast shows the following AST (after cleaning it up), tested with both GHC 9.6.6 and 9.10.1:

OpApp _
  ( OpApp _
      (HsVar "putStrLn")
      (HsVar "$")
      (HsIntegral 1)
  )
  (HsVar "foo")
  (HsIntegral 2)

I would think that the parse step takes fixity into account, is that not the case?

I’m writing a compiler plugin that basically does a rewrite of some expressions, e.g.

bar 10 `foo` baz
====>
foo (Expr "bar 10" (bar 10)) (Expr "baz" baz)

but the incorrect fixity is giving me weird results here:

putStrLn $ 1 `foo` 2
====>
foo (Expr "putStrLn $ 1" (putStrLn $ 1)) (Expr "2" 2)

I’m currently hooking into parsedResultAction; is that not the right place to do this? Do I need to use renamedResultAction? I think that would work too, as long as it’s before the typechecking phase

What fixity do you have declared for foo? (Remember " Fixity is a property of a particular entity (constructor or variable), just like its type; fixity is not a property of that entity’s name ." – Report 4.4.2 – which has some counter-intuitive examples.)

No juggling the binding happens after parsing, precisely because fixity depends on the entity (dictionary look-up). So in your example, at first $ and foo are ‘on the same level’.

I haven’t declared any fixity for foo, so I should do that.

But even with a fixity defined for foo, your last paragraph sounds like it won’t fix the problem. It looks like -ddump-rn-ast shows the right fixity; so I should use renamedResultAction instead?

1 Like

Emmm beyond my pay-scale to comment on that, sorry. ‘Juggling the binding’ must happen before type-checking, otherwise foo would get a left arg putStrLn $ 1, which would (presumably) be ill-typed. And type-checking must happen after dictionary look-up to get the entity’s type. renamedResult sounds more like it: looking up the dictionary resolves to the name’s in-scope declaration.

I vaguely remember something like “operator applications are initially parsed left to right and only adjusted to match their fixities in a later pass”.

2 Likes

Yeah, the output of the GHC parser does not take fixities into account, things get reassociated during renaming (this happens in mkOppAppRn).

That’s why e.g. in Ormolu (and hence also in Fourmolu), we have ad-hoc heuristics to find out what the fixity of a name is (both via an embedded list based on operators defined on Hackage, as well as user input), and then reassociate operator trees appropriately.

Lots of other tools also have the same underlying problem (hlint, apply-refact, retrie); there are a few approaches how to make handling of operator fixities easier for external tools:

But for a compiler plugin, using the renamed output should solve this problem :+1:

5 Likes