4. pyxc: Completing Basic Arithmetic

Next: make these expressions parse:

-5
x * -2
17 % 5

Chapter 3 recognizes binary -, but a leading - appears where the parser expects an operand. Add a new grammar layer for prefix operators, then add remainder beside multiplication and division.

Work in:

cd code/chapter-04

4.1 Replace the Term Grammar

Replace:

term = primary { ("*" | "/") primary } ;

with:

term   = factor { ("*" | "/" | "%") factor } ;
factor = "-" factor | primary ;

This puts unary minus between primary and multiplication:

primary -> factor -> term -> sum

Calling factor recursively allows repeated negation:

---x

4.2 Add the Remainder Token

Add:

tok_percent,

Add its readable token name:

{tok_percent, "'%'"},

Then recognize the character in getToken():

case '%':
  return tok_percent;

No new token is needed for unary minus. The lexer already returns tok_minus; the parser decides whether its position is unary or binary.

4.3 Add a Unary AST Node

Add:

class UnaryExpressionNode : public ExpressionNode {
  int Operator;
  unique_ptr<ExpressionNode> Operand;

public:
  UnaryExpressionNode(int Operator,
                      unique_ptr<ExpressionNode> Operand)
      : Operator(Operator), Operand(std::move(Operand)) {}
};

A binary node owns two operands. A unary node owns one.

For:

-x

the tree is:

UnaryExpression '-'
└── NameExpression x

4.4 Parse Unary Minus

Forward-declare ParseFactor() because unary parsing calls back into it:

static unique_ptr<ExpressionNode> ParseFactor();

Then add:

static unique_ptr<ExpressionNode> ParseUnaryMinus() {
  getNextToken(); // eat '-'

  auto Operand = ParseFactor();
  if (!Operand)
    return nullptr;

  return make_unique<UnaryExpressionNode>(
      tok_minus, std::move(Operand));
}

Implement the grammar choice:

static unique_ptr<ExpressionNode> ParseFactor() {
  if (CurrentToken == tok_minus)
    return ParseUnaryMinus();

  return ParsePrimary();
}

The recursive ParseFactor() call makes --x become -(-x). Calling ParsePrimary() after the recursion stops gives unary minus tighter precedence than *, /, +, and -.

4.5 Make Terms Consume Factors

In ParseTerm(), replace both calls to ParsePrimary() with ParseFactor().

Then extend the operator condition:

while (CurrentToken == tok_star || CurrentToken == tok_slash ||
       CurrentToken == tok_percent) {

The complete shape is:

static unique_ptr<ExpressionNode> ParseTerm() {
  auto Left = ParseFactor();
  if (!Left)
    return nullptr;

  while (CurrentToken == tok_star || CurrentToken == tok_slash ||
         CurrentToken == tok_percent) {
    int Operator = CurrentToken;
    getNextToken();

    auto Right = ParseFactor();
    if (!Right)
      return nullptr;

    Left = make_unique<BinaryExpressionNode>(
        Operator, std::move(Left), std::move(Right));
  }

  return Left;
}

4.6 Check the Resulting Grouping

These expressions now group as:

-2 * 3      -> (-2) * 3
x * -2      -> x * (-2)
-x % 4 + 1  -> ((-x) % 4) + 1
--x          -> -(-x)

Unary minus binds tighter because ParseTerm() asks ParseFactor() for each operand. Remainder shares the term tier with multiplication and division.

4.7 Build and Run

cmake -S . -B build
cmake --build build
./build/pyxc

Try:

ready> -5
ready> 17 % 5
ready> x * -2
ready> --3

Expected after each valid input:

Parsed a top-level expression.

The frontend still does not evaluate. These experiments verify that the new token positions produce the intended AST rather than an unexpected-token error.

Run all tests:

llvm-lit -v test/

What you built is one reusable prefix boundary:

leading '-' -> unary node over another factor
infix '-'   -> binary node in the sum tier

Next: Chapter 5 turns crude parser failures into source-located diagnostics with carets and recovery.

4.8 Need Help?

Build issues? Questions?

Include:

  • Your operating system and version
  • The chapter number
  • The exact command you ran
  • The complete error message
  • The output of c++ --version and cmake --version
  • The output of llvm-config --version for Chapter 6 and later