Building a language interpreter: Evaluation II
So far, we have explored evaluating Expressions
, now let's dive into how our interpreter deals with executing Statements
!
Visiting Statements
Our interpreter’s implementation of the StmtVisitor
trait, reproduced below, defines each statement type’s behaviour.
pub trait StmtVisitor {
type T;
fn visit_expression_stmt(&mut self, stmt: &Stmt) -> Self::T;
fn visit_print_stmt(&mut self, stmt: &Stmt) -> Self::T;
fn visit_decl_stmt(&mut self, stmt: &Stmt) -> Self::T;
fn visit_block_stmt(&mut self, stmt: &Stmt) -> Self::T;
fn visit_if_stmt(&mut self, stmt: &Stmt) -> Self::T;
fn visit_while_stmt(&mut self, stmt: &Stmt) -> Self::T;
fn visit_function_stmt(&mut self, stmt: &Stmt) -> Self::T;
fn visit_return_stmt(&mut self, stmt: &Stmt) -> Self::T;
fn visit_class_stmt(&mut self, stmt: &Stmt) -> Self::T;
}
The only return values we expect from statements in Sox are errors so the associated type T
for our trait implementation is defined as Result<(), Exception>
. The interpreter’s entry point for executing statements is the execute
method shown below. This, just like the entry point for evaluating expressions calls the accept method on a statement with the interpreter itself as the argument. The accept
method then calls the respective method for each statement type. Our trait method implementations follow the same pattern where the method checks the type of the statement prior to executing some code for that type.
fn execute(&mut self, stmt: &Stmt) -> Result<(), Exception> {
stmt.accept(self)
}
The first method we look at is visit_expression_stmt
method that handles execution of expressions. This method simply calls the interpreter’s evaluate
method with the expression as argument and returns an error that may have occurred.
fn visit_expression_stmt(&mut self, stmt: &Stmt) -> Self::T {
let mut return_value = Ok(());
if let Stmt::Expression(expr) = stmt {
let value = self.evaluate(expr);
return_value = match value {
Ok(_) => {
Ok(())
}
Err(v) => Err(v.into()),
};
}
return_value
}
Next is the visit_print_stmt
, our method that handles printing values to stdin. The argument to a print statement is an expression so first, we evaluate that expression and then call the Rust println macro to display the value of the expression.
fn visit_print_stmt(&mut self, stmt: &Stmt) -> Self::T {
let return_value = if let Stmt::Print(expr) = stmt {
let value = self.evaluate(expr);
match value {
Ok(v) => {
println!(">> {:?}", v);
Ok(())
}
Err(v) => Err(v.into()),
}
} else {
Err(RuntimeError {
msg: "Evaluation failed - visited non print statement with visit_print_stmt.".to_string(),
}
.into())
};
return_value
}
Then, we have the visit_decl_stmt
that handles variable declaration. A declaration can be a standalone such as let x
; or it can be a declaration coupled with an initializer value such as let x = 1
; where x
is initialized to 1
.
fn visit_decl_stmt(&mut self, stmt: &Stmt) -> Self::T {
let mut value = SoxObject::None;
if let Stmt::Var { name, initializer } = stmt {
if initializer.is_some() {
let v = initializer.clone().unwrap();
value = self.evaluate(&v)?;
}
let active_env = self.active_env();
let name_ident = name.lexeme.to_string();
active_env.define(name_ident, value)
} else {
return Err(RuntimeError {
msg: "Evaluation failed - visiting a non declaration statement with visit_decl_stmt".to_string(),
}
.into());
}
Ok(())
}
The next method is the visit_block_stmt
that as the name suggests handles block statements. A block is a collection of statements delimited by braces - {}
. To execute a block, the visit_block_stmt
calls the interpreter’s execute_block
method with the collection of statements in the block as argument.
fn visit_block_stmt(&mut self, stmt: &Stmt) -> Self::T {
if let Stmt::Block(statements) = stmt {
let stmts = statements.iter().map(|v| v).collect::<Vec<&Stmt>>();
debug!("statements are {:?}", stmts);
self.execute_block(stmts)?;
return Ok(());
} else {
return Err(RuntimeError {
msg: "Evaluation failed - visited non block statement with visit_block_stmt".to_string(),
}
.into());
}
}
The execute_block
function is shown below. The block is a very important part of our language as every block creates a new namespace in which the block’s code executes.
This method does the following -
First, it creates a new namespace for the block’s statements.
It then executes each of statements in the block within that context.
Once execution is complete either due to the successful execution of all statements or an error during execution of a statement, the namespace that was created for that block is removed from the environment.
In the case of an error, this is returned to the calling method.
pub fn execute_block(&mut self, statements: Vec<&Stmt>) -> Result<(), Exception> {
let active_env = self.active_env();
active_env.new_namespace()?;
for statement in statements {
let res = self.execute(statement);
if let Err(v) = res {
let active_env = self.active_env();
active_env.pop()?;
return Err(v);
}
}
let active_env = self.active_env();
active_env.pop()?;
Ok(())
}
The other methods that we implement are visitors for conditional statements. These statements are implemented using their analogous statements in Rust. The visit_if_stmt
shown below handles the Sox if
statements. The condition of an if
statement is an expression so this is evaluated first and if it is true, this method executes the then_branch
otherwise it executes the else_branch
.
fn visit_if_stmt(&mut self, stmt: &Stmt) -> Self::T {
if let Stmt::If {
condition,
then_branch,
else_branch,
} = stmt
{
let cond_val = self.evaluate(condition)?;
if self.is_truthy(&cond_val) {
self.execute(then_branch)?;
} else if let Some(else_branch_stmt) = else_branch.as_ref() {
self.execute(else_branch_stmt)?;
}
} else {
return Err(RuntimeError {
msg: "Evaluation failed - visited non if statement with visit_if_stmt".into(),
}
.into());
}
Ok(())
}
The visit_while_stmt
method is the other visitor for conditional statement - the while statement. This is implemented in a Rust while loop. Here, our while
loop executes the body of our while
statement as long as the condition expression of the while statement evaluates to true.
fn visit_while_stmt(&mut self, stmt: &Stmt) -> Self::T {
if let Stmt::While { condition, body } = stmt {
let mut cond = self.evaluate(condition)?;
while self.is_truthy(&cond) {
self.execute(body)?;
cond = self.evaluate(&condition)?;
}
Ok(())
} else {
Err(RuntimeError {
msg: "Evaluation failed - visited non while statement with visit_while_stmt."
.into(),
}
.into())
}
}
We also have the visit_return_stmt, visit_return_stmt
, and visit_class_stmt
statements pending and we will return to these once we add classes and functions to our language. However, we now have a tree walker interpreter that can execute a good part of our language grammar. For example, our interpreter can execute the following statements.
for (let i=0; i < 5; i=i+1){
print i * 2;
}
The git tag, sox-evaluation, contains all the code up to this point. At this point, you should take a pause and play around with the contents of the resources/main.sox module. Remember you can use the cargo run_sox command to execute this module.