12. pyxc: Statement Blocks
Next: replace one-expression function bodies with real statement blocks.
Chapter 11 needs sequencing tricks:
def sum_to(n): var acc = 0: (for var i = 1, i <= n, i = i + 1: acc = acc + i) + acc
After this chapter, write the same function directly:
def sum_to(n):
var acc = 0
for var i = 1, i <= n, i = i + 1:
acc = acc + i
return acc
The new compiler boundary is:
newline + indentation -> statement block -> ordered codegen
Work in:
cd code/chapter-12
12.1 Separate Statements from Expressions
Replace the Chapter 11 expression forms with:
function-definition = "def" function-signature ":" suite ;
statement = simple-statement | compound-statement ;
simple-statement = return-statement
| variable-statement
| assignment-statement
| expression ;
compound-statement = if-statement | for-statement ;
suite = simple-statement | end-of-lines block ;
block = INDENT statement
{ (end-of-lines | BLOCK_END) statement }
DEDENT ;
return-statement = "return" expression ;
variable-statement = "var" variable-binding
{ "," variable-binding } ;
assignment-statement = lvalue "=" expression ;
if-statement = "if" expression ":" suite
[ [ end-of-lines ] "else" ":" suite ] ;
for-statement = "for" [ "var" ] name "=" expression ","
expression "," for-update ":" suite ;
for-update = assignment-statement | expression ;
expression = comparison ;
lvalue = name ;
The essential split is:
value-producing: number, name, call, unary, binary, comparison
statement-only: var, assignment, return, if, for, block
An inline suite remains valid:
if x: return 1
An indented suite can contain multiple statements:
if x:
var y = 1
return y
12.2 Add Statement and Layout Tokens
Add:
tok_return = -14,
tok_indent = -17,
tok_dedent = -18,
tok_block_end = -19,
Add {"return", tok_return} to the keyword table and readable token names for all four.
INDENT and DEDENT come from the lexer. BLOCK_END is injected by the parser after it consumes a DEDENT; it tells an enclosing block that a nested compound statement ended without requiring another physical newline.
12.3 Track Indentation State
Add:
static vector<int> IndentStack = {0};
static deque<int> PendingTokens;
static bool AtLineStart = true;
static constexpr int IndentTabWidth = 8;
The stack stores active indentation columns:
[0] -> top level
[0, 4] -> one block
[0, 4, 8] -> nested block
The token queue handles a line that closes several blocks. One source position may need to emit several DEDENT tokens before its first real token.
At the start of getToken(), drain queued layout tokens first:
if (!PendingTokens.empty()) {
int Tok = PendingTokens.front();
PendingTokens.pop_front();
return Tok;
}
12.4 Count Indentation at Line Start
When AtLineStart is true, consume spaces and tabs before normal tokenization:
int CurrentIndentRead = 0;
while (LastChar == ' ' || LastChar == '\t') {
CurrentIndentRead +=
LastChar == ' '
? 1
: IndentTabWidth - CurrentIndentRead % IndentTabWidth;
LastChar = advance();
}
Tabs advance to the next multiple of eight. Blank and comment-only lines do not change indentation in file mode.
In REPL mode, a blank line while inside a block closes one indentation level. This gives the user a way to finish a multi-line definition.
12.5 Emit INDENT and DEDENT
For an increase:
int CurrentIndentOnStack = IndentStack.back();
if (CurrentIndentRead > CurrentIndentOnStack) {
IndentStack.push_back(CurrentIndentRead);
AtLineStart = false;
return tok_indent;
}
For a decrease, pop and queue every closed level:
if (CurrentIndentRead < CurrentIndentOnStack) {
while (IndentStack.size() > 1 &&
CurrentIndentRead < IndentStack.back()) {
IndentStack.pop_back();
PendingTokens.push_back(tok_dedent);
}
if (CurrentIndentRead != IndentStack.back()) {
LogErrorAtLocation("inconsistent indentation",
CurrentTokenLocation);
PendingTokens.clear();
AtLineStart = false;
return tok_error;
}
AtLineStart = false;
int Tok = PendingTokens.front();
PendingTokens.pop_front();
return Tok;
}
Reject a dedent that matches no earlier level. At EOF, emit remaining DEDENT tokens one at a time before tok_eof. Whenever a newline is emitted, set AtLineStart = true.
12.6 Add Statement AST Nodes
Add a return node:
class ReturnStatementNode : public ExpressionNode {
unique_ptr<ExpressionNode> Expression;
public:
ReturnStatementNode(unique_ptr<ExpressionNode> Expression)
: Expression(std::move(Expression)) {}
Value *codegen() override;
};
Add a block node:
class BlockStatementNode : public ExpressionNode {
vector<unique_ptr<ExpressionNode>> Statements;
public:
BlockStatementNode(vector<unique_ptr<ExpressionNode>> Statements)
: Statements(std::move(Statements)) {}
Value *codegen() override;
};
Rename the Chapter 11 nodes:
AssignmentExpressionNode -> AssignmentStatementNode
VariableExpressionNode -> VariableStatementNode
IfExpressionNode -> IfStatementNode
ForExpressionNode -> ForStatementNode
var no longer owns a body. Its bindings remain active for the current block. if no longer needs both arms to produce a value, so else becomes optional.
12.7 Track Variables While Parsing
Add a stack of parse-time scopes:
static vector<set<string>> LocalVariableScopes;
Add helpers to:
begin/end a function scope
begin/end a nested block scope
begin/end a loop-variable scope
declare a name in the current scope
check the current scope for redeclaration
search all active scopes for assignment
Parameters enter the function scope. var bindings enter the current block. A for var name enters a temporary loop scope.
Use RAII guards so early parser returns still pop scopes:
struct BlockScopeGuard {
BlockScopeGuard() { BeginBlockScope(); }
~BlockScopeGuard() { EndBlockScope(); }
};
Use equivalent guards for function and loop scopes. This gives early errors for assignment to an undeclared name and redeclaration in the same scope.
12.8 Parse return and var
Add:
static unique_ptr<ExpressionNode> ParseReturnStatement() {
getNextToken(); // eat 'return'
auto Expr = ParseExpression();
if (!Expr)
return nullptr;
return make_unique<ReturnStatementNode>(std::move(Expr));
}
Reuse Chapter 11's binding loop for ParseVariableStatement(), but remove the colon and body. For each binding:
- Require a name.
- Reject a duplicate in the current scope.
- Parse an initializer or use
0.0. - Append the binding.
- Declare the name for later statements.
Return a VariableStatementNode containing only the bindings.
12.9 Parse Assignment at Statement Level
Make ParseExpression() return only ParseComparison() again.
Add:
static unique_ptr<ExpressionNode>
ParseAssignmentOrExpressionStatement() {
auto Expr = ParseExpression();
if (!Expr)
return nullptr;
if (CurrentToken != tok_assign)
return Expr;
const string *AssignedName = Expr->getLValueName();
if (!AssignedName)
return LogErrorExpression(
"Destination of '=' must be a variable");
if (!IsVariableDeclared(*AssignedName))
return LogErrorExpression(
"Assignment to undeclared variable");
getNextToken(); // eat '='
auto Right = ParseExpression();
if (!Right)
return nullptr;
return make_unique<AssignmentStatementNode>(
*AssignedName, std::move(Right));
}
Use this for ordinary statements and the for update so i = i + 1 remains valid in the header.
12.10 Parse Statements and Suites
Add the dispatch layers:
static unique_ptr<ExpressionNode> ParseSimpleStatement() {
if (CurrentToken == tok_return)
return ParseReturnStatement();
if (CurrentToken == tok_var)
return ParseVariableStatement();
return ParseAssignmentOrExpressionStatement();
}
static unique_ptr<ExpressionNode> ParseStatement() {
if (CurrentToken == tok_if)
return ParseIfStatement();
if (CurrentToken == tok_for)
return ParseForStatement();
return ParseSimpleStatement();
}
Then parse what follows a colon:
static unique_ptr<ExpressionNode> ParseSuite() {
if (CurrentToken == tok_eol) {
consumeNewlines();
if (CurrentToken != tok_indent)
return LogErrorExpression("Expected an indented block");
return ParseBlock();
}
return ParseSimpleStatement();
}
12.11 Parse Blocks and Inject BLOCK_END
ParseBlock() should:
- Consume
INDENT. - Begin a block scope.
- Require at least one statement.
- Parse statements until
DEDENT. - Accept newline or
BLOCK_ENDbetween statements. - Consume
DEDENTand expose a syntheticBLOCK_ENDto the caller.
The core loop is:
while (CurrentToken != tok_dedent) {
auto Statement = ParseStatement();
if (!Statement)
return nullptr;
Statements.push_back(std::move(Statement));
if (CurrentToken == tok_eol) {
consumeNewlines();
continue;
}
if (CurrentToken == tok_block_end) {
getNextToken();
continue;
}
if (CurrentToken != tok_dedent)
return LogErrorExpression(
"Expected newline or end of block");
}
Before returning:
PendingTokens.push_front(tok_block_end);
getNextToken(); // consume DEDENT, expose BLOCK_END
The marker solves nested-block bookkeeping. A compound statement consumes the newline and dedent that ended its suite; the enclosing block still needs a separator before its next statement.
12.12 Convert if and for to Statements
For if, parse the condition and colon, then call ParseSuite() for the then arm. Make else optional. If the then suite ended with BLOCK_END, temporarily consume it while probing for else; restore it if no else follows.
For for, retain the Chapter 11 header fields:
optional var, name, start, condition, update
Parse the update with ParseAssignmentOrExpressionStatement() and the body with ParseSuite(). If var is absent, require the loop variable to be already declared.
The loop itself no longer participates in (for ...) + result. The enclosing block sequences it naturally.
12.13 Generate return
Implement:
Value *ReturnStatementNode::codegen() {
Value *RetVal = Expression->codegen();
if (!RetVal)
return nullptr;
TheBuilder->CreateRet(RetVal);
return RetVal;
}
ret terminates the current basic block. No later instruction may be appended there.
12.14 Generate Blocks in Order
Implement:
Value *BlockStatementNode::codegen() {
auto SavedBindings = NamedValues;
Value *Last = nullptr;
for (auto &Statement : Statements) {
if (TheBuilder->GetInsertBlock()->getTerminator())
break;
Last = Statement->codegen();
if (!Last) {
NamedValues = SavedBindings;
return nullptr;
}
}
NamedValues = SavedBindings;
if (!Last)
return LogErrorValue("Empty block");
return ConstantFP::get(*TheContext, APFloat(0.0));
}
Stop after a terminator such as return. Restore NamedValues when leaving the block so inner declarations do not escape.
12.15 Generate Statement-Style Control Flow
For VariableStatementNode, allocate and initialize every binding, leave it installed until the enclosing block restores its table, and return 0.0.
For IfStatementNode:
- Convert the condition to
i1. - Branch to then or else blocks.
- Generate each present suite.
- Branch to the merge only if that arm has no terminator.
- Return
0.0from the merge.
There is no PHI because an if statement does not produce a selected value.
For ForStatementNode, retain the mutable-slot CFG from Chapter 11. Generate the suite as the body, then the complete update, then branch back to the condition. Return 0.0 after the loop.
12.16 Add Implicit 0.0 Return
After generating a function body, add a return only if the current block has no terminator:
if (!TheBuilder->GetInsertBlock()->getTerminator())
TheBuilder->CreateRet(
ConstantFP::get(*TheContext, APFloat(0.0)));
An explicit return wins. Reaching the end returns 0.0. The last expression in a block is no longer an implicit return.
12.17 Keep Top-Level Mutable State Out for Now
Reject top-level var and assignment. Each top-level expression uses a temporary module; a local alloca cannot persist into the next REPL input.
ready> var count = 0
Expected:
Error (Line 1, Column 1): Unexpected 'var'
Mutable storage works inside functions, where its lifetime is one call. Globals arrive later.
12.18 Build and Run
cmake -S . -B build \
-DLLVM_DIR="$(llvm-config --cmakedir)"
cmake --build build
./build/pyxc
Enter the accumulator, ending the REPL block with a blank line:
ready> def sum_to(n):
var acc = 0
for var i = 1, i <= n, i = i + 1:
acc = acc + i
return acc
ready> sum_to(5)
Expected:
Parsed a function definition.
Parsed a top-level expression.
Evaluated to 15.000000
Run the suite:
llvm-lit -v test/
Pay particular attention to indentation, multi-level dedent, nested blocks, inline suites, optional else, returns, block scoping, undeclared assignment, implicit return, and EOF with an open block.
What you built is the statement pipeline:
indentation -> INDENT/DEDENT
suite -> one statement or block
block -> ordered statements
return -> terminator
fallthrough -> implicit 0.0
Next: Chapter 13 adds more loop control on top of statement blocks.
12.19 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++ --versionandcmake --version - The output of
llvm-config --versionfor Chapter 6 and later