Binary Operators

We extend the lexer with two new keywords for “binary” and “unary” toplevel definitions.

  1. lexer :: Tok.TokenParser ()
  2. lexer = Tok.makeTokenParser style
  3. where
  4. ops = ["+","*","-","/",";","=",",","<",">","|",":"]
  5. names = ["def","extern","if","then","else","in","for"
  6. ,"binary", "unary"]
  7. style = emptyDef {
  8. Tok.commentLine = "#"
  9. , Tok.reservedOpNames = ops
  10. , Tok.reservedNames = names
  11. }

Parsec has no default function to parse “any symbolic” string, but it can be added simply by defining an operator new token.

  1. operator :: Parser String
  2. operator = do
  3. c <- Tok.opStart emptyDef
  4. cs <- many $ Tok.opLetter emptyDef
  5. return (c:cs)

Using this we can then parse any binary expression. By default all our operators will be left-associative and have equal precedence, except for the bulletins we provide. A more general system would allow the parser to have internal state about the known precedences of operators before parsing. Without predefined precedence values we’ll need to disambiguate expressions with parentheses.

  1. binop = Ex.Infix (BinaryOp <$> op) Ex.AssocLeft

Using the expression parser we can extend our table of operators with the “binop” class of custom operators. Note that this will match any and all operators even at parse-time, even if there is no corresponding definition.

  1. binops = [[binary "*" Ex.AssocLeft,
  2. binary "/" Ex.AssocLeft]
  3. ,[binary "+" Ex.AssocLeft,
  4. binary "-" Ex.AssocLeft]
  5. ,[binary "<" Ex.AssocLeft]]
  6. expr :: Parser Expr
  7. expr = Ex.buildExpressionParser (binops ++ [[binop]]) factor

The extensions to the AST consist of adding new toplevel declarations for the operator definitions.

  1. data Expr =
  2. ...
  3. | BinaryOp Name Expr Expr
  4. | UnaryOp Name Expr
  5. | BinaryDef Name [Name] Expr
  6. | UnaryDef Name [Name] Expr

The parser extension is straightforward and essentially a function definition with a few slight changes. Note that we capture the string value of the operator as given to us by the parser.

  1. binarydef :: Parser Expr
  2. binarydef = do
  3. reserved "def"
  4. reserved "binary"
  5. o <- op
  6. prec <- int
  7. args <- parens $ many identifier
  8. body <- expr
  9. return $ BinaryDef o args body

To generate code we’ll implement two extensions to our existing code generator. At the toplevel we’ll emit the BinaryDef declarations as simply create a normal function with the name “binary” suffixed with the operator.

  1. codegenTop (S.BinaryDef name args body) =
  2. codegenTop $ S.Function ("binary" ++ name) args body

Now for our binary operator, instead of failing with the presence of a binary operator not declared in our binops list, we instead create a call to a named “binary” function with the operator name.

  1. cgen (S.BinaryOp op a b) = do
  2. case Map.lookup op binops of
  3. Just f -> do
  4. ca <- cgen a
  5. cb <- cgen b
  6. f ca cb
  7. Nothing -> cgen (S.Call ("binary" ++ op) [a,b])