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
program = [ end-of-lines ]
[ top-level-item
{ end-of-lines top-level-item } ]
[ end-of-lines ] ;
end-of-lines = end-of-line { end-of-line } ;
top-level-item = function-definition
| external
| top-level-expression ;
function-definition = "def" function-signature ":"
( simple-statement
| end-of-lines block ) ;
external = "extern" "def" function-signature ;
top-level-expression = expression ;
function-signature = name "(" [ parameters ] ")" ;
parameters = parameter { "," parameter } ;
parameter = name ;
if-statement = "if" expression ":" suite
+ { [ end-of-lines ] "elif" expression ":" suite }
[ [ end-of-lines ] "else" ":" suite ] ;
for-statement = "for" [ "var" ] name "=" expression ","
expression "," expression ":" suite ;
variable-statement = "var" variable-binding
{ "," variable-binding } ;
assignment-statement = lvalue "=" expression ;
simple-statement = return-statement
| variable-statement
| assignment-statement
| expression ;
compound-statement = if-statement | for-statement ;
statement = simple-statement | compound-statement ;
suite = simple-statement
| compound-statement
| end-of-lines block ;
return-statement = "return" expression ;
statement-separator = end-of-lines | BLOCK_END ;
block = indent statement
{ statement-separator statement } dedent ;
expression = comparison ;
comparison = sum { comparison-operator sum } ;
comparison-operator = "==" | "!=" | "<=" | ">=" | "<" | ">" ;
sum = term { ("+" | "-") term } ;
term = factor { ("*" | "/" | "%") factor } ;
lvalue = name ;
variable-binding = name [ "=" expression ] ;
factor = "-" factor | primary ;
primary = name-expression
| number-expression
| parenthesized-expression ;
name-expression = name | call-expression ;
call-expression = name "(" [ arguments ] ")" ;
arguments = expression { "," expression } ;
number-expression = number ;
parenthesized-expression = "(" expression ")" ;
indent = INDENT ;
dedent = DEDENT ;
name = (letter | "_")
{ letter | digit | "_" } ;
number = digit { digit } [ "." { digit } ]
| "." digit { digit } ;
letter = "A".."Z" | "a".."z" ;
digit = "0".."9" ;
end-of-line = "\r\n" | "\r" | "\n" ;
comment = "#" { comment-character } ;
comment-character = ? any character except "\r" and "\n" ? ;
whitespace = " " | "\t" | "\v" | "\f" ;
INDENT = ? synthetic token emitted by lexer when indentation increases ? ;
DEDENT = ? synthetic token emitted by lexer when indentation decreases ? ;
BLOCK_END = ? synthetic token injected into the stream by ParseBlock
immediately after it consumes DEDENT ? ;
New Token and Keyword
One new token:
tok_elif = -22,
Added to the keyword table, right alongside if and else:
{"if", tok_if}, {"elif", tok_elif}, {"else", tok_else},
And to the token-name map used in error messages:
{tok_if, "'if'"}, {tok_elif, "'elif'"},
{tok_else, "'else'"},
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
llvm-lit -v test/
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.