Interp - an Interpreted Programming Language Pt.8 - If else

6 minute read

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.

Next Up

Click for Pt. 9 or click here for all parts.

Updated: