13. pyxc: elif Chains
What I Am Building
Chapter 12 added if/else as a statement, but nothing between them. For more than two branches, I'm forced to nest:
def sign(x):
if x > 0:
return 1.0
else:
if x == 0:
return 0.0
else:
return -1.0
After this chapter, I can write the same logic flat:
ready> def sign(x): if x > 0: return 1.0 elif x == 0: return 0.0 else: return -1.0Parsed a function definition.
Source Code
git clone --depth 1 https://github.com/alankarmisra/pyxc-llvm-tutorial
cd pyxc-llvm-tutorial/code/chapter-13
Grammar
I add one alternative to if-statement: zero or more elif clauses between the if clause and the optional else:
code/chapter-13/pyxc.ebnf
*...
*parameter = name ;
*if-statement = "if" expression ":" suite
+ { [ end-of-lines ] "elif" expression ":" suite }
* [ [ end-of-lines ] "else" ":" suite ] ;
*for-statement = "for" [ "var" ] name "=" expression ","
*...
New Token and Keyword
One new token:
*enum Token {
* ...
* // control
* tok_if = -12,
* tok_else = -13,
* tok_return = -14,
+ tok_elif = -22,
*
* // loops
* ...
*};
Added to the keyword table, right alongside if and else:
*static map<string, Token> Keywords = {
- {"def", tok_def}, {"extern", tok_extern}, {"return", tok_return},
- {"if", tok_if}, {"else", tok_else}, {"for", tok_for},
- {"var", tok_var}};
+ {"def", tok_def}, {"extern", tok_extern}, {"return", tok_return},
+ {"if", tok_if}, {"elif", tok_elif}, {"else", tok_else},
+ {"for", tok_for},
+ {"var", tok_var}};
And to the token-name map used in error messages:
*static map<int, string> TokenNames = [] {
* // Unprintable character tokens, and multi-character tokens.
* static map<int, string> Names = {
* ...
- {tok_if, "'if'"}, {tok_else, "'else'"},
- {tok_for, "'for'"}, {tok_var, "'var'"},
+ {tok_if, "'if'"}, {tok_elif, "'elif'"},
+ {tok_else, "'else'"},
+ {tok_for, "'for'"}, {tok_var, "'var'"},
* {tok_indent, "indent"}, {tok_dedent, "dedent"},
* {tok_block_end, "block-end"}};
* ...
*};
Refactoring If/Elif Parsing to Collect Branches
Before this chapter, ParseIfStatement parsed exactly one condition and one body. Now I collect an arbitrary number of (condition, body) pairs in a loop before I even know whether an else follows:
static unique_ptr<ExpressionNode> ParseIfStatement() {
getNextToken(); // eat 'if'
vector<pair<unique_ptr<ExpressionNode>, unique_ptr<ExpressionNode>>> Branches;
bool LastBranchWasBlock = false;
bool LastBranchHadTrailingEol = false;
while (true) {
auto Cond = ParseExpression();
if (!Cond)
return nullptr;
if (CurrentToken != tok_colon)
return LogErrorExpression("Expected ':' after if/elif condition");
getNextToken(); // eat ':'
auto Body = ParseSuite();
if (!Body)
return nullptr;
LastBranchWasBlock = (CurrentToken == tok_block_end);
if (LastBranchWasBlock)
getNextToken();
LastBranchHadTrailingEol = (CurrentToken == tok_eol);
Branches.push_back({std::move(Cond), std::move(Body)});
consumeNewlines();
if (CurrentToken != tok_elif)
break;
getNextToken(); // eat 'elif'
}
// ...
}
Each pass through the loop parses one if or elif branch — I don't distinguish between them; the first iteration happens to follow if, and every iteration after that follows elif. After each body, I call consumeNewlines() and check whether elif comes next. If it does, I eat it and loop again. Anything else — else, a dedent, end of file — and I break out.
LastBranchWasBlock and LastBranchHadTrailingEol are the same bookkeeping Chapter 12 already needed for a bare if/else, just tracked per-branch now instead of once.
Missing colon after an elif condition:
ready> def bad(x): if x > 0: return 1 elif x == 0 return 0 else: return -1Error (Line 4, Column 16): Expected ':' after if/elif condition elif x == 0 ^~~~ Error (Line 5, Column 9): Unexpected indentation r ^~~~ Error (Line 6, Column 5): unknown token when expecting an expression else: ^~~~ Error (Line 7, Column 9): Unexpected indentation r ^~~~
The first line is the real error — the parser bails out of ParseIfStatement the moment the colon check fails. Everything after that is the parser trying to recover from a token stream that no longer makes sense, the same cascading-error behavior Chapter 12 already showed for bad indentation.
Lowering to a Nested If Tree
I don't introduce a new AST node for elif. Once the loop above exits, I check for a trailing else — this part is unchanged from Chapter 12, just renamed from Then/ThenWasBlock to LastBranchWasBlock since there can now be more than one branch before it:
unique_ptr<ExpressionNode> Else;
if (CurrentToken == tok_else) {
getNextToken(); // eat 'else'
if (CurrentToken != tok_colon)
return LogErrorExpression("Expected ':' after else");
getNextToken(); // eat ':'
Else = ParseSuite();
if (!Else)
return nullptr;
} else if (LastBranchWasBlock) {
PendingTokens.push_front(CurrentToken);
CurrentToken = tok_block_end;
} else if (LastBranchHadTrailingEol) {
PendingTokens.push_front(CurrentToken);
CurrentToken = tok_eol;
}
Then I lower the whole chain to a right-nested IfStatementNode tree: the (possibly null) else body becomes the initial innermost node, and I walk Branches in reverse, wrapping one more IfStatementNode around it per branch:
// I lower the chain to nested IfStatementNodes in the else branch.
unique_ptr<ExpressionNode> Tree = std::move(Else);
for (auto It = Branches.rbegin(); It != Branches.rend(); ++It) {
Tree = make_unique<IfStatementNode>(std::move(It->first),
std::move(It->second), std::move(Tree));
}
return Tree;
Given:
if a: body_a
elif b: body_b
elif c: body_c
else: body_d
I build:
IfStatementNode(a, body_a,
IfStatementNode(b, body_b,
IfStatementNode(c, body_c,
body_d)))
If there's no else at all, Else starts as nullptr, so the innermost IfStatementNode's Else is null too — exactly what a bare if without else already produces. IfStatementNode::codegen() doesn't change at all; it has no idea whether it came from a literal if/else or from one link in an elif chain.
My codegen sees exactly what it would see for hand-written nested if/else blocks, so conditions are evaluated top to bottom, one at a time, same as nested if/else would be. elif buys me flatter source, not a different runtime shape — switch, which dispatches on a value directly instead of testing a chain of conditions, comes in Chapter 23.
Build and Run
cd code/chapter-13
cmake -S . -B build && cmake --build build
./build/pyxc
Try It
ready> def sign(x): if x > 0: return 1.0 elif x == 0: return 0.0 else: return -1.0Parsed a function definition.ready> sign(5)Parsed a top-level expression. Evaluated to 1.000000ready> sign(0)Parsed a top-level expression. Evaluated to 0.000000ready> sign(-2)Parsed a top-level expression. Evaluated to -1.000000
More than one elif — the first matching branch wins, and later ones are never even evaluated:
ready> def mapv(x): if x == 1: return 10.0 elif x == 2: return 20.0 elif x == 3: return 30.0 elif x == 4: return 40.0 else: return 99.0Parsed a function definition.ready> mapv(3)Parsed a top-level expression. Evaluated to 30.000000ready> mapv(4)Parsed a top-level expression. Evaluated to 40.000000ready> mapv(7)Parsed a top-level expression. Evaluated to 99.000000
An elif chain with no else at all — same fall-through behavior a bare if has always had:
ready> def classify(x): var result = 0 if x == 1: result = 1 elif x == 2: result = 2 return resultParsed a function definition.ready> classify(1)Parsed a top-level expression. Evaluated to 1.000000ready> classify(2)Parsed a top-level expression. Evaluated to 2.000000ready> classify(99)Parsed a top-level expression. Evaluated to 0.000000
classify(99) matches neither x == 1 nor x == 2, so the if falls through without touching result — it stays 0.
What's Next
Chapter 14 completes looping: while, do/while, break, and continue.
Need Help?
Build issues? Questions?
- GitHub Issues: Report problems
- Discussions: Ask questions
Include:
- Your OS and version
- Full error message
- Output of
cmake --version,ninja --version, andllvm-config --version
I'll help you figure it out.