Interp - an Interpreted Programming Language Pt.8 - If else
Hey there! Today in Pt. 8 we implement conditional code.
Parsing
EpxrType
Add ExprTypes for these operators.
src/parser/expr.rs:
pub enum ExpType {
...
Eq(Box<Expr>, Box<Expr>),
Neq(Box<Expr>, Box<Expr>),
And(Box<Expr>, Box<Expr>),
Or(Box<Expr>, Box<Expr>),
LessThan(Box<Expr>, Box<Expr>),
MoreThan(Box<Expr>, Box<Expr>),
LessThanOrEq(Box<Expr>, Box<Expr>),
MoreThanOrEq(Box<Expr>, Box<Expr>),
Not(Box<Expr>),
}
LineTypes
Add LineTypes for if, else if, else, and for the closing brace ‘}’.
src/parser/line.rs:
pub enum LineType {
...
If(Expr),
ElseIf(Expr),
Else,
EndScope
}
Grammar
Let’s add the tokens and grammar rules.
src/parser/grammar.lalrpop:
match {
...
"if",
"else",
"{",
"}",
"==",
"!=",
"and",
"or",
"<",
">",
"<=",
">=",
"!",
}
...
pub Line: Line = {
...
<l:@L> "if" <a:expr> "{" => Line { type_: LineType::If(a), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
<l:@L> "}" "else" "if" <a:expr> "{" => Line { type_: LineType::ElseIf(a), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
<l:@L> "}" "else" "{" => Line { type_: LineType::Else, loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
<l:@L> "}" => Line { type_: LineType::EndScope, loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
}
...
expr2: Expr = {
expr1,
<l:@L> "-" <a:expr1> => Expr { type_: ExprType::Neg(Box::new(a)), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
<l:@L> "!" <a:expr1> => Expr { type_: ExprType::Not(Box::new(a)), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
}
...
expr5: Expr = {
expr4,
<l:@L> <a:expr5> "==" <b:expr4> => Expr { type_: ExprType::Eq(Box::new(a), Box::new(b)), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
<l:@L> <a:expr5> "!=" <b:expr4> => Expr { type_: ExprType::Neq(Box::new(a), Box::new(b)), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
<l:@L> <a:expr5> "<" <b:expr4> => Expr { type_: ExprType::LessThan(Box::new(a), Box::new(b)), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
... // Implement MoreThan, LessThanOrEq, and MoreThanOrEq
}
expr6: Expr = {
expr5,
<l:@L> <a:expr6> "and" <b:expr5> => Expr { type_: ExprType::And(Box::new(a), Box::new(b)), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
<l:@L> <a:expr6> "or" <b:expr5> => Expr { type_: ExprType::Or(Box::new(a), Box::new(b)), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
}
expr: Expr = {
expr6
}
Clone Trait
Add the Clone and PartialEq trait to structs Value, Line, LineType, Expr, ExprType, and Loc.
src/parser/*.rs:
src/vm/value.rs:
#[derive(..., Clone, PartialEq)] // Add Clone and PartialEq
VM
Value
Add the Boolean Value.
src/vm/value.rs:
pub enum Value {
...
Boolean(bool),
}
Operator Evaluation
Let’s define our operators. First, the functions.
src/vm/arithmetic.rs:
pub fn eq(x: Value, y: Value) -> Value {
Value::Boolean(x == y)
}
... // Implement neq
pub fn less_than(x: Value, y: Value, x_loc: &Loc) -> Value {
match (x, y) {
(Value::Float(a), Value::Float(b)) => Value::Boolean(a < b),
(Value::Int(a), Value::Float(b)) => Value::Boolean((a as f64) < b),
(Value::Float(a), Value::Int(b)) => Value::Boolean(a < b as f64),
(Value::Int(a), Value::Int(b)) => Value::Boolean(a < b),
_ => {
err::throw_loc(err::Err::TokenExpected { expected: "numbers".to_owned() }, x_loc);
Value::Void
}
}
}
... // Implement more_than, less_than_or_eq, and more_than_or_eq
pub fn and(x: Value, y: Value, x_loc: &Loc) -> Value {
if let (Value::Boolean(x), Value::Boolean(y)) = (x, y) {
Value::Boolean(x && y)
} else {
err::throw_loc(err::Err::TokenExpected { expected: "conditions".to_owned() }, &x_loc);
Value::Void
}
}
... // Implement or
pub fn not(x: Value, x_loc: &Loc) -> Value {
if let Value::Boolean(x) = x {
Value::Boolean(!x)
} else {
err::throw_loc(err::Err::TokenExpected { expected: "condition".to_owned() }, &x_loc);
Value::Void
}
}
expr_to_value
Then add cases to expr_to_value.
src/vm/mod.rs:
fn expr_to_value(vm: &mut VM, expr: &Expr) -> value::Value {
match &expr.type_ {
...
ExprType::Variable(var) => {
match var.as_str() {
...
"true" => value::Value::Boolean(true),
"false" => value::Value::Boolean(false),
...
}
}
...
ExprType::Eq(x_expr, y_expr) => {
let x = expr_to_value(vm, x_expr);
let y = expr_to_value(vm, y_expr);
arithmetic::eq(x, y)
}
... // Implement Neq
ExprType::LessThan(x_expr, y_expr) => {
let x = expr_to_value(vm, x_expr);
let y = expr_to_value(vm, y_expr);
arithmetic::less_than(x, y, &x_expr.loc)
}
... // Implement MoreThan, LessThanOrEq, MoreThanOrEq, And, and Or
ExprType::Not(cond_expr) => {
let cond_value = expr_to_value(vm, cond_expr);
arithmetic::not(cond_value, &cond_expr.loc)
}
}
}
value_repr
Lastly, let’s define our value_reprs.
fn value_repr(val: value::Value) -> String {
match val {
...
value::Value::Boolean(true) => "true".to_owned(),
value::Value::Boolean(false) => "false".to_owned(),
}
}
VM State
src/vm/mod.rs:
pub struct VM {
...
pub scope_state: ScopeState, // Tracks if we are inside an if, while, or function
pub line_buffer: Vec<Line>, // Cache for lines inside a scope
pub scope: usize, // How many scopes in
}
pub fn init_vm() -> VM {
VM {
...
scope_state: ScopeState::Regular,
line_buffer: vec![],
scope: 0,
}
}
pub enum ScopeState {
Regular,
Exec, // Mark the block of code for execution
Condition, // "true" If branch not yet found
DontExec, // If branch has been executed
}
exec_line
When the VM encounters an if statement, it goes to a gathering-mode that pushes lines into the line buffer. A matching “}” then stops this gathering. If condition was true the lines from the buffer get executed. Else if and else also clear and initialize the buffer and possibly execute code if appropriate.
pub fn exec_line(vm: &mut VM, parsed_line: &Line) {
if vm.scope != 0 {
// Means gathering mode is on
if vm.scope == 1 {
match parsed_line.type_ {
LineType::If(_) => {
vm.scope = 2;
vm.line_buffer.push(parsed_line.clone());
return;
}
LineType::ElseIf(_) |
LineType::Else |
LineType::EndScope => { }
_ => {
vm.line_buffer.push(parsed_line.clone());
return;
}
}
} else {
match parsed_line.type_ {
LineType::If(_) => vm.scope += 1,
LineType::EndScope => vm.scope -= 1,
_ => { }
}
vm.line_buffer.push(parsed_line.clone());
return;
}
}
Here’s the exec_line cases.
match &parsed_line.type_ {
...
LineType::If(cond_expr) => {
let cond_value = expr_to_value(vm, cond_expr);
if let value::Value::Boolean(cond) = cond_value {
if cond {
vm.scope_state = ScopeState::Exec;
} else {
vm.scope_state = ScopeState::Condition;
}
} else {
err::throw_loc(err::Err::TokenExpected
{ expected: "condition".to_owned() }, &cond_expr.loc
);
}
vm.scope = 1; // Into gathering mode
}
LineType::ElseIf(cond_expr) => {
vm.scope = 0; // Out of gathering mode
let prev_line_buffer = std::mem::replace(&mut vm.line_buffer, vec![]);
let cond_value = expr_to_value(vm, cond_expr);
if let value::Value::Boolean(cond) = cond_value {
match vm.scope_state {
ScopeState::Exec => {
for line in prev_line_buffer {
exec_line(vm, &line);
}
vm.scope_state = ScopeState::DontExec;
}
ScopeState::Condition => {
if cond {
vm.scope_state = ScopeState::Exec;
}
}
ScopeState::DontExec => { }
_ => {
err::throw_loc(err::Err::TokenUnexpected
{ not_expected: "else if".to_owned() }, &parsed_line.loc);
}
}
} else {
err::throw_loc(err::Err::TokenExpected
{ expected: "condition".to_owned() }, &cond_expr.loc);
}
vm.scope = 1;
}
LineType::Else => {
vm.scope = 0; // Out of gathering mode
let prev_line_buffer = std::mem::replace(&mut vm.line_buffer, vec![]);
match vm.scope_state {
ScopeState::Exec => {
for line in prev_line_buffer {
exec_line(vm, &line);
}
vm.scope_state = ScopeState::DontExec;
}
ScopeState::Condition => {
vm.scope_state = ScopeState::Exec;
}
ScopeState::DontExec => { }
_ => {
err::throw_loc(err::Err::TokenUnexpected
{ not_expected: "else".to_owned() }, &parsed_line.loc);
}
}
vm.scope = 1;
}
LineType::EndScope => {
vm.scope = 0;
let prev_line_buffer = std::mem::replace(&mut vm.line_buffer, vec![]);
match std::mem::replace(&mut vm.scope_state, ScopeState::Regular) {
ScopeState::Exec => {
for line in prev_line_buffer {
exec_line(vm, &line);
}
}
ScopeState::Condition | ScopeState::DontExec => {}
_ => {
err::throw_loc(
err::Err::TokenUnexpected { not_expected: "\"}\"".to_owned() },
&parsed_line.loc,
);
}
}
}
...
is_var_illegal
Finally, blacklist the ‘true’ and ‘false’ identifiers in the is_var_illegal function.
fn is_var_illegal(var: &str) -> bool {
if let "void" | "null" | "Print" | "true" | "false" = var {
true
} else { false }
}
TokenUnexpected Error
This error is like TokenExpected but reverse.
src/err.rs:
pub enum Err {
...
TokenUnexpected { not_expected: String },
}
pub fn throw(err: Err) {
match err {
...
Err::TokenUnexpected { not_expected } => eprintln!("did not expect {}", not_expected),
}
}
Wrapping Up
That’s it! Let’s try out some code.
test.it:
if 1 == 2 {
Print("does not execute")
} else if 3 < 4 and !false {
Print("does execute")
} else {
Print("does not")
}
Source Code
Source code for this part can be found here.