36. pyxc: elif Chains

Where We Are

Chapter 35 added switch. Multi-way conditionals on non-integer values — booleans, comparisons, method results — are still written as nested if/else blocks, which stack up fast:

def classify(x: int) -> int:
  if x < 0:
    return -1
  else:
    if x == 0:
      return 0
    else:
      return 1

After this chapter, the same logic reads cleanly:

def classify(x: int) -> int:
  if x < 0:
    return -1
  elif x == 0:
    return 0
  else:
    return 1

Source Code

git clone --depth 1 https://github.com/alankarmisra/pyxc-llvm-tutorial
cd pyxc-llvm-tutorial/code/chapter-36

New Token and Keyword

One new token:

tok_elif = -63,

Added to the keyword table:

{"elif", tok_elif},

And to the token name map for error messages:

{tok_elif, "'elif'"},

ParseIfStmt Refactored to Collect Branches

Previously ParseIfStmt parsed a single condition and body. Now it collects an arbitrary number of (condition, body) pairs in a loop:

vector<pair<unique_ptr<ExprAST>, unique_ptr<ExprAST>>> Branches;
bool LastBranchWasBlock = false;

// eat 'if'
getNextToken();

while (true) {
  auto Cond = ParseExpression();
  if (!Cond)
    return nullptr;
  if (Cond->getType() != ValueType::Bool)
    return LogError("If condition must be bool");

  if (CurTok != ':')
    return LogError("Expected ':' after if/elif condition");
  getNextToken(); // eat ':'

  auto Body = ParseSuite();
  if (!Body)
    return nullptr;
  LastBranchWasBlock = (CurTok == tok_block_end);
  if (LastBranchWasBlock)
    getNextToken();
  Branches.push_back({std::move(Cond), std::move(Body)});

  consumeNewlines();
  if (CurTok != tok_elif)
    break;
  getNextToken(); // eat 'elif'
}

After each body, consumeNewlines() skips the line ending. If the next token is tok_elif, the loop continues — eating elif and parsing another condition and body. Any other token (including tok_else or anything else) exits the loop.

Lowering to a Nested IfStmtAST Tree

No new AST node is introduced. The elif chain is lowered directly to a right-nested IfStmtAST tree during parsing. The optional else body becomes the initial innermost node, and the Branches vector is walked in reverse:

// Lower if/elif chain to nested IfStmtAST in else branch.
unique_ptr<ExprAST> Tree = std::move(Else);
for (auto It = Branches.rbegin(); It != Branches.rend(); ++It) {
  Tree = make_unique<IfStmtAST>(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

The parser builds:

IfStmtAST(a, body_a,
  IfStmtAST(b, body_b,
    IfStmtAST(c, body_c,
      body_d)))

Codegen sees exactly what it would see for hand-written nested if/else blocks. The IR is identical.

Grammar

ifstmt = "if" expression ":" suite
       { eols "elif" expression ":" suite }   -- new
       [ eols "else" ":" suite ] ;            -- changed

Error Cases

Missing colon after elif condition:

if x > 0:
  return 1
elif x == 0
  return 0   # Error: Expected ':' after if/elif condition

Non-bool elif condition:

if x > 0:
  return 1
elif x + 1:   # Error: If condition must be bool
  return 0

Things Worth Knowing

elif without else is fine. If no branch matches and there is no else, execution continues after the chain. The same is true for a plain if without else.

elif uses the same condition type rule as if. Every condition must be bool. There is no implicit integer-to-bool coercion.

Use switch for integer dispatch on constants; use elif for everything else. switch is limited to compile-time integer literals and allows LLVM to emit a real branch table. elif works on any bool expression but generates a linear chain of comparisons.

What's Next

Chapter 37 adds character literals: 'a', '\n', '\t', and friends.

Need Help?

Build issues? Questions?

Include:

  • Your OS and version
  • Full error message
  • Output of cmake --version, ninja --version, and llvm-config --version

We'll figure it out.