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

Fix pretty-printing of infix operators #8

Closed
byorgey opened this issue Sep 20, 2021 · 3 comments · Fixed by #118
Closed

Fix pretty-printing of infix operators #8

byorgey opened this issue Sep 20, 2021 · 3 comments · Fixed by #118
Labels
C-Moderate Effort Should take a moderate amount of time to address. L-Pretty-printing Pretty-printing ASTs or values into a string representation. S-Critical This is an issue that seriously affects playability or user experience.

Comments

@byorgey
Copy link
Member

byorgey commented Sep 20, 2021

Infix operators do not print correctly at the moment; they print as if they were prefix operations, like + 2 3.

Fixing this will be critical to being able to round-trip pretty-printing and parsing, which in turn is needed to be able to support #7 .

@byorgey byorgey added C-Low Hanging Fruit Ideal issue for new contributors. S-Critical This is an issue that seriously affects playability or user experience. L-Pretty-printing Pretty-printing ASTs or values into a string representation. labels Sep 20, 2021
@xsebek
Copy link
Member

xsebek commented Sep 26, 2021

Hi, I took a look at what this would look like and from what I found:

  • operators are made by mkOp as TApp of some TConst:
    -- | Make infix operation (e.g. @2 + 3@) a curried function
    --   application (@((+) 2) 3@).
    --
    -- >>> mkOp (Arith Add) (TInt 2) (TInt 3)
    -- TApp (TApp (TConst (Arith Add)) (TInt 2)) (TInt 3)
    mkOp :: Const -> Term -> Term -> Term
    mkOp c = TApp . TApp (TConst c)
  • correct printing requires adjusting PrettyPrec Term - something like:
    -- Special handling of infix operators - ((+) 2) 3 --> 2 + 3
    prettyPrec p (TApp (TApp (TConst c) l) r) = hsep
      [prettyPrec p l, ppr c, prettyPrec p r]
    prettyPrec p (TApp t1 t2)  = pparens (p > 10) $
      prettyPrec 10 t1 <+> prettyPrec 11 t2

Can any other TConst apart from Arith and Cmp be applied? And do I have to respect the precedence here?

@byorgey
Copy link
Member Author

byorgey commented Sep 26, 2021

Hi, thanks for taking a look at this! You're absolutely right that the pretty printer would need a special case for nested TApps of a TConst. But yes, any TConst that takes arguments (i.e. with arity > 0) will also be applied via TApp. So given a TApp (TApp (TConst c) l) r we also need to check if the constant c is infix. And yes, precedence will come into play as well.

I think I see three levels for a solution here, ranging from the simplest thing that will work and enable #7 , up to the one that will be the most work but will be the best long-term solution. It is totally up to you what approach you want to take here --- e.g. you could just implement the simplest fix for now and spin off other issue(s) to tackle later, or you could just go straight for the more principled approach that is more work.

Level 1

We can add a function isInfix : Const -> Bool to Swarm.Language.Syntax. Fortunately this function is very easy to write: Arith and Cmp things are infix, and nothing else. Then to avoid having to worry about precedence, we can simply pretty-print every infix constant application with parentheses around it. e.g. 1 + 2 * 3 would be pretty-printed as (1 + (2 * 3)). This is a bit ugly, of course, but it is easy and correct.

Level 2

To correctly handle precedence, we need to make another function prec : Const -> Int which returns the precedence level of each infix constant (it can just return anything, say 0, for the others). To know what the proper precedence levels are, take a look at parseExpr in the parser, or just use the same precedence levels that Haskell uses. Then you can use the mparens function to decide whether to print parentheses or not (see some examples of how it is used in the rest of the pretty-printer; I'm happy to provide more explicit guidance for anyone who is not sure how this works).

Level 3

A bigger problem is that now a lot of information about constants is fragmented around the codebase: the arity function, isInfix function, and isCmd function encode data about each constant; the precedence of infix constants is now encoded in two separate places (implicitly in the parser, and explicitly in the prec function); and also the information about what concrete textual symbol to use for each constant is also encoded in two separate places (the parser and pretty-printer). It would be too easy for these to get out of sync, and when adding a new constant it is difficult to know all the places that must be updated.

A much better way is to define a data type to represent all the information about a constant, and a single function to output this information for each constant, something like

data ConstInfo = ConstInfo
  { syntax :: Text
  , arity :: Int
  , isCmd :: Bool
  , isInfix :: Bool
  , precedence :: Int
  }

constInfo :: Const -> ConstInfo
constInfo = \case
  Wait -> ConstInfo "wait" 0 True False 0
  ...
  Arith Add -> ConstInfo "+" 2 False True 6
  ...

This will replace the arity, isInfix, isCmd, and prec functions. We should probably get rid of the separate ArithConst and CmpConst types and just inline all into the single Const type; that way, we can derive Enum and Bounded for Const and get a list of all constants by [minBound .. maxBound]. Then the parser and pretty-printer no longer need to give an explicit list of all the constants; we can simply derive the relevant parts of the parser and pretty-printer from the list of all constants and the constInfo function. The ultimate goal is that in order to add a new constant to the language, we only have to add a constructor to the Const type, and a case to the constInfo function, and that's it! The parser and pretty-printer don't need to be touched since they will automatcially pick up the new constant and use the relevant info to do their job.

@byorgey
Copy link
Member Author

byorgey commented Sep 29, 2021

Hi @xsebek , are you thinking of working on this (or are you already)? I'm kind of itching to implement the "Level 3" thing in my comment above, because I'm getting very annoyed having to edit like 7 or 8 files every time I add a single command. But if you're already working on any of this I don't want to step on your toes, and there are plenty of other things I can work on.

@byorgey byorgey added C-Moderate Effort Should take a moderate amount of time to address. and removed C-Low Hanging Fruit Ideal issue for new contributors. labels Sep 30, 2021
@mergify mergify bot closed this as completed in #118 Oct 1, 2021
mergify bot pushed a commit that referenced this issue Oct 1, 2021
- moves info about constants to `Syntax.hs`
  - adds `infoConst`&friends functions on `Const`
  - lot of noise - needs to be simplified
- changes the pretty-printing logic for `Term` application
- creates parsers for `Const` automatically
- ~~adds a heavy dependency to get [something like `Enum`](https://hackage.haskell.org/package/finitary)~~

Closes #8:
- #8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-Moderate Effort Should take a moderate amount of time to address. L-Pretty-printing Pretty-printing ASTs or values into a string representation. S-Critical This is an issue that seriously affects playability or user experience.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants