State and Variable
The llox is no more than a calculator, let’s level it up
to support variables. We can start with the minimum,
all variables are defined in a global namespace.
First We introduces a new syntax for variable declaration:
program → declaration* EOF ;
declaration → varDecl
| statement ;
varDecl → "var" IDENTIFIER ( "=" expression )? ";" ;
and use the visitor pattern for Stmt and the derived classes.
More concretely, the Parser::declaration peeks the token
stream, and resolve to VarStmt. The visitor of VarStmt
put the variable to the dictionary, variables with the
value resolved from the initializer:
void Interpreter::visit(const VarStmt &stmt) {
Value *val = nullptr;
if (stmt.initializer != nullptr) {
val = stmt.initializer->accept(*this);
}
// Resolve to assign expr.
variables.insert({stmt.name.lexeme, val});
}
Then when the interpreter visits the Literal,
we would look up the variables dictionary:
if (auto pval = std::get_if<std::string>(&literal.value)) {
Value *val = variables.at(*pval);
return val;
}
With this, our calculator is slightly more sophisticated:
> var a = 3; print a + 5;
8
Check out the PR #11 for more details.
Assignment statement
Next, we would support the assignment statement, such as:
> var a = 3; var b; b = a = a + 1; print a + b;
8
This would introduce new syntax:
expression → assignment ;
assignment → IDENTIFIER "=" assignment
| equality ;
and new Expr types, Assign and Variable.
struct Assign : Expr {
explicit Assign(std::unique_ptr<Expr> lvalue, std::unique_ptr<Expr> rvalue)
: lvalue(std::move(lvalue)), rvalue(std::move(rvalue)) {}
void accept(ExprVisitor &v) const override { v.visit(*this); }
Value *accept(CodegenVisitor &v) const override { return v.visit(*this); }
std::unique_ptr<Expr> lvalue;
std::unique_ptr<Expr> rvalue;
};
Notice the lvalue and rvalue above. The lvalue represents a persistent
memory location, such as breakfast.bagel; but for now it MUST be resolved
to a variable.
Value *Interpreter::visit(const Assign &assign) {
// Make sure the variable is declared.
auto lval = dynamic_cast<Variable *>(assign.lvalue.get());
if (lval != nullptr) {
auto name = lval->name.lexeme;
auto it = variables.find(name);
if (it == variables.end()) {
return log_error_v("name is not declared");
}
auto rval = assign.rvalue->accept(*this);
it->second = rval;
return rval;
}
throw std::runtime_error("Cannot assign to lvalue");
}
The Variable would lookup the variables for the Value*:
Value *Interpreter::visit(const Variable &variable) {
Value *val = variables.at(variable.name.lexeme);
return val;
}