Block and Scope
Every variable we have defined so far live in one flat, global
namespace — the variables map in Interpreter.
In this chapter we introduce the concept block, which
defines the accessibility of the variables.
Block syntax
A block is a sequence of declarations wrapped in curly braces:
statement → exprStmt
| printStmt
| block ;
block → "{" declaration* "}" ;
The syntax tree gets a BlockStmt holding a list of statements:
struct BlockStmt : Stmt {
explicit BlockStmt(std::vector<std::unique_ptr<Stmt>> statements)
: statements(std::move(statements)) {}
void accept(StmtVisitor &v) const override { v.visit(*this); }
std::vector<std::unique_ptr<Stmt>> statements;
};
Parser::statement() is extended to dispatch to a new block_stmt()
method, which keeps parsing declarations until the closing brace:
std::unique_ptr<Stmt> Parser::block_stmt() {
std::vector<std::unique_ptr<Stmt>> statements;
while (!check(TOKEN_RIGHT_BRACE) && !ends()) {
statements.push_back(declaration());
}
consume(TOKEN_RIGHT_BRACE, "Expect '}' after block.");
return std::make_unique<BlockStmt>(std::move(statements));
}
Scopes
A block defines a scope for the variables. A variable defined
inside the scope is no longer accessible once the code execution
leaves the scope. The inner variable can hide the outer one inside
the scope, aka variable shadowing. We will replace the flat variables with
scopes:
std::vector<std::map<std::string, Value *>> scopes;
When visiting a block, the statements are evaluated against a fresh scope
appended to the scopes, then popped to restore the previous one.
void Interpreter::visit(const BlockStmt &stmt) {
// Push a fresh scope for the block, then pop it when the block ends.
scopes.emplace_back();
for (const auto &statement : stmt.statements) {
statement->accept(*this);
}
scopes.pop_back();
}
Variable declaration always writes to the current scope, while assignment and reference first look in the current scope, then walk upwards through the scopes.
// Resolve from the innermost scope outward.
for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) {
auto found = it->find(name);
if (found != it->end()) {
auto rval = assign.rvalue->accept(*this);
found->second = rval;
return rval;
}
}
return log_error_v("name is not declared");
See PR #15 for details.