Creating Custom Operators

Frost allows developers to extend the language with custom operators, providing flexibility and expressiveness for domain-specific needs. This guide will walk you through the process of implementing new operators in the Frost compiler.

Overview

Custom operators in Frost are implemented by modifying the parser and adding new entries to the operationTable. This table defines the precedence and associativity of operators.

Steps to Add a Custom Operator

  1. Define the Operator in AST Types

First, add your new operator to the Operation data type in Ast/Types.hs:

data Operation
  = -- ... existing operators ...
  | YourNewOperator
  deriving (Show, Eq, Ord)
  1. Update the Parser

In Ast/Parser/Expr.hs, add your new operator to the operationTable:

operationTable :: [[CE.Operator PU.Parser AT.Expr]]
operationTable =
  [ -- ... existing operator groups ...
    [ PU.binary "your_operator_symbol" (`AT.Op` AT.YourNewOperator)
    ]
    -- ... other operator groups ...
  ]
  1. Implement Code Generation

Update the code generation logic in Codegen/ExprGen/Operator.hs to handle your new operator:

generateBinaryOp :: AT.Operation -> AST.Operand -> AST.Operand -> Codegen AST.Operand
generateBinaryOp op left right = case op of
  -- ... existing cases ...
  AT.YourNewOperator -> -- Implement your operator logic here
  1. Update Error Handling

Ensure that Codegen/Errors.hs can handle potential errors related to your new operator:

data CodegenErrorType
  = -- ... existing error types ...
  | InvalidYourNewOperatorUsage
  deriving (Show)
  1. Add Tests

Create unit tests for your new operator in the test/ directory:

testYourNewOperator :: Test
testYourNewOperator = TestCase $ do
  let code = "a your_operator_symbol b"
  result <- runParser parseExpr code
  assertEqual "Your new operator test" expectedAST result

Best Practices

  • Ensure your operator has a clear and intuitive meaning

  • Document the operator's behavior thoroughly

  • Consider precedence carefully when adding to the operationTable

  • Implement comprehensive error checking and reporting

Example: Adding a Power Operator

Let's add a power operator ** as an example:

  1. In Ast/Types.hs:

data Operation
  = -- ... existing operators ...
  | Power
  deriving (Show, Eq, Ord)
  1. In Ast/Parser/Expr.hs:

operationTable =
  [ -- ...
    [ PU.binary "**" (`AT.Op` AT.Power)
    ]
    -- ...
  ]
  1. In Codegen/ExprGen/Operator.hs:

This implementation uses the llvm.pow.f64 function to calculate the power of two floating-point numbers. If you want to implement other types, you will need to extend this function accordingly.

generateBinaryOp AT.Power left right = do
  powFunc <- externf (AST.FloatingPointType 64) "llvm.pow.f64" [(AST.FloatingPointType 64, []), (AST.FloatingPointType 64, [])]
  call powFunc [(left, []), (right, [])]

By following these steps and best practices, you can extend Frost with powerful custom operators that enhance its capabilities for specific domains or programming paradigms.

Last updated