4. pyxc: Completing Basic Arithmetic
What I Am Building
In Chapter 3, - only ever showed up as subtraction. A leading - doesn't parse at all:
ready> -5Error: Unexpected '-' (token: '-')
% doesn't exist either — there's no way to ask for a remainder. I close both gaps in this chapter.
Source Code
git clone --depth 1 https://github.com/alankarmisra/pyxc-llvm-tutorial
cd pyxc-llvm-tutorial/code/chapter-04
Grammar
I insert factor between term and primary. term now loops over factor instead of primary directly, and factor is where - and % both live:
code/chapter-04/pyxc.ebnf
*...
*comparison = sum { "<" sum } ;
*sum = term { ("+" | "-") term } ;
-term = primary { ("*" | "/") primary } ;
+term = factor { ("*" | "/" | "%") factor } ;
+factor = "-" factor
+ | primary ;
*primary = name-expression
* | number-expression
*...
I could have special-cased - inside ParsePrimary instead of giving it its own tier, but then --5 and -(x + 1) wouldn't fall out naturally — I'd have to handle chaining by hand at the call site. Giving factor its own self-recursive rule means - in front of anything that can start a factor, including another -, just works. % doesn't need any of that; it's a plain sibling of * and / at the same tier.
A New Token
I add one token for %:
*enum Token {
* ...
* tok_star,
* tok_slash,
+ tok_percent,
* tok_less,
*};
And give it a readable name for error messages, right alongside %'s siblings:
*static map<int, string> TokenNames = {
* ...
- {tok_star, "'*'"}, {tok_slash, "'/'"}, {tok_less, "'<'"},
+ {tok_star, "'*'"}, {tok_slash, "'/'"}, {tok_percent, "'%'"},
+ {tok_less, "'<'"},
*};
And return it from the lexer:
* switch (ThisChar) {
* ...
* case '/':
* return tok_slash;
+ case '%':
+ return tok_percent;
* case '<':
* return tok_less;
* default:
* return tok_error;
* }
A New AST Node
Unary minus needs a node shaped differently from BinaryExpressionNode — one operand, not two:
/// UnaryExpressionNode - Expression class for applying a unary operator.
class UnaryExpressionNode : public ExpressionNode {
int Operator;
unique_ptr<ExpressionNode> Operand;
public:
UnaryExpressionNode(int Operator, unique_ptr<ExpressionNode> Operand)
: Operator(Operator), Operand(std::move(Operand)) {}
};
I store Operator as an int, the same type CurrentToken already is, even though - is the only unary operator I have right now. That leaves room to add ! or ~ later without changing the node's shape.
Parsing factor
ParseTerm calls ParseFactor for each operand instead of ParsePrimary:
ParseFactor needs a forward declaration above ParseUnaryMinus because the two functions call each other: ParseUnaryMinus needs ParseFactor to parse its operand, and ParseFactor needs ParseUnaryMinus to handle the - case. Whichever one I define first has to declare the other ahead of its own body.
static unique_ptr<ExpressionNode> ParseFactor();
/// I parse the unary-minus branch of factor.
static unique_ptr<ExpressionNode> ParseUnaryMinus() {
getNextToken(); // I eat '-'.
auto Operand = ParseFactor();
if (!Operand)
return nullptr;
return make_unique<UnaryExpressionNode>(tok_minus, std::move(Operand));
}
/// factor
/// = "-" factor
/// | primary ;
static unique_ptr<ExpressionNode> ParseFactor() {
if (CurrentToken == tok_minus)
return ParseUnaryMinus();
return ParsePrimary();
}
ParseUnaryMinus calls ParseFactor for its own operand, not ParsePrimary — that's what lets it recurse into itself. --5 works because the first - calls ParseFactor, which sees the second - and calls ParseUnaryMinus again before either call has produced a value.
*/// term
-/// = primary { ("*" | "/") primary } ;
+/// = factor { ("*" | "/" | "%") factor } ;
*static unique_ptr<ExpressionNode> ParseTerm() {
- // I start the term by parsing one primary.
- auto Left = ParsePrimary();
+ // I start the term by parsing one factor.
+ auto Left = ParseFactor();
* if (!Left)
* return nullptr;
*
* // I consume only the operators that belong to this tier.
- while (CurrentToken == tok_star || CurrentToken == tok_slash) {
+ while (CurrentToken == tok_star || CurrentToken == tok_slash ||
+ CurrentToken == tok_percent) {
* int Operator = CurrentToken;
- getNextToken(); // I eat '*' or '/'.
- auto Right = ParsePrimary();
+ getNextToken(); // I eat '*', '/', or '%'.
+ auto Right = ParseFactor();
* if (!Right)
* return nullptr;
*
* // I fold each new operation into the tree on my left.
* Left = make_unique<BinaryExpressionNode>(Operator, std::move(Left),
* std::move(Right));
* }
*
* return Left;
*}
That single change — ParsePrimary() to ParseFactor(), twice, in ParseTerm — is what makes -2 * 3 parse as (-2) * 3 rather than -(2 * 3). ParseFactor grabs the -2 as a complete unit before ParseTerm's while loop ever sees the *. % needed no equivalent change anywhere else — it just joins * and / in the same while condition, since it belongs at exactly their precedence.
There's no new error path for a bad operand after -: ParseUnaryMinus just propagates the Unexpected ... diagnostic produced by ParsePrimary():
ready> -)Error: Unexpected ')' (token: ')')
Try It
ready> def wrap(x): -x % 10Parsed a function definition.
ready> -5Parsed a top-level expression.
ready> 7 % 3Parsed a top-level expression.
ready> -2 * 3Parsed a top-level expression.
ready> --5Parsed a top-level expression.
-2 * 3 is (-2) * 3, not -(2 * 3) written differently — unary minus binds tighter than *, same as it does in every C-family language. --5 is double negation, not decrement — pyxc has no -- token yet, so this is just - applied twice, and ParseFactor handles both - characters through the same recursive call. There's still no codegen at this stage, so every valid line just reports that it parsed; I don't see real arithmetic results until I connect the AST to LLVM IR.
Build and Run
cd code/chapter-04
cmake -S . -B build && cmake --build build
./build/pyxc
I run the chapter tests with:
llvm-lit -v test/
What's Next
Chapter 5 adds real source locations and caret-style error messages.
Need Help?
Build issues? Questions?
- GitHub Issues: Report problems
- Discussions: Ask questions
Include:
- Your OS and version
- Full error message
- Output of
cmake --version
I'll help you figure it out.