Interp - an Interpreted Programming Language Pt.7 - Variables

3 minute read

It’s Pt. 7 Interp is now closer to becoming an actual programming language. This time we implement variables.

Parsing

LineTypes

Add LineTypes for the assignment operators.

src/parser/line.rs:

pub enum LineType {

    ...

    Assign(Expr, Expr),
    AddAssign(Expr, Expr),
    SubAssign(Expr, Expr),
    MulAssign(Expr, Expr),
    DivAssign(Expr, Expr),
    ModAssign(Expr, Expr),
    PowAssign(Expr, Expr),
    ConcatAssign(Expr, Expr),
    Var(Expr, Expr),
}

Grammar

Next we add tokens and grammar rules for the operators.

src/parser/grammar.lalrpop:

match {

    ...
    
    "=",
    "+=",
    "-=",
    "*=",
    "/=",
    "%=",
    "^=",
    "<>=",
    "var",
}

...

pub Line: Line = {

    ...

    <l:@L> <a:expr> "=" <b:expr> => Line { type_: LineType::Assign(a, b), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
    <l:@L> <a:expr> "+=" <b:expr> => Line { type_: LineType::AddAssign(a, b), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
    
    ... // Implement the rest of the assign operators here
    
    <l:@L> "var" <a:variable> "=" <b:expr> => Line { type_: LineType::Var(a, b), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
}

VM

The Value struct must be clonable and equatable. Le’ts also add null while we’re at it.

Values

src/vm/value.rs:

#[derive(Debug, Clone)]
pub enum Value {
    Void,
    Null,

    ...
    
}

VM State

We finally add some state to our programs.

src/vm/mod.rs:


...

use crate::parser::line::{Line, LineType};
use crate::parser::expr::{Expr, ExprType};
use crate::err;
use std::collections::HashMap;

pub struct VM {
    
    // the stack stores our values
    pub stack: Vec<value::Value>,

    // symbols point variable names into stack addresses
    pub symbols: HashMap<String, usize>,
}

pub fn init_vm() -> VM {
    VM {
        stack: vec![],
        symbols: HashMap::new(),
    }
}

exec_line

Your job is to add the missing exec_line cases.

pub fn exec_line(vm: &mut VM, parsed_line: &Line) {
    match &parsed_line.type_ {
        
        ...

        LineType::Assign(left_expr, right_expr) => {
            let value = expr_to_value(vm, right_expr);
            assign(vm, left_expr, value);
        }
        LineType::AddAssign(left_expr, right_expr) => {
            let left_value = expr_to_value(vm, left_expr);
            let right_value = expr_to_value(vm, right_expr);
            let result = arithmetic::add(left_value, right_value, &left_expr.loc);
            assign(vm, left_expr, result);
        }

        ... // Define SubAssign, MulAssign, DivAssign, ModAssign, PowAssign, and ConcatAssign here
        
        LineType::Var(left_expr, right_expr) => {
            if let ExprType::Variable(var) = &left_expr.type_ {
                if is_var_illegal(var) {
                    err::throw_loc(err::Err::CannotAssignIllegalName, &left_expr.loc);
                } else {
                    let value = expr_to_value(vm, right_expr);
                    vm.symbols.insert(var.to_owned(), vm.stack.len());
                    vm.stack.push(value);
                }
            } else { panic!() }
        }
    }
}

expr_to_value

fn expr_to_value(vm: &mut VM, expr: &Expr) -> value::Value {
    match &expr.type_ {

        ...

        ExprType::Variable(var) => {
            match var.as_str() {

                // Add Void and Null here
                "void"  => value::Value::Void,
                "null"  => value::Value::Null,

                "Print" => value::Value::PrintBuiltin,
                _ => {
                    // Return Void if variable not found
                    if let Some(address) = var_address(vm, var) {
                        vm.stack[address].clone()
                    } else {
                        value::Value::Void
                    }
                }
            }
        }

Helper Functions

Some useful functions.

fn var_address(vm: &VM, var: &str) -> Option<usize> {
    if let Some(address) = vm.symbols.get(var) {
        Some(*address)
    } else {
        None
    }
}

// Assign Value to Expr
fn assign(vm: &mut VM, left_expr: &Expr, right_value: value::Value) {
    match &left_expr.type_ {
        ExprType::Variable(var) => {
           if let Some(address) = var_address(vm, var) {
               vm.stack[address] = right_value;
           } else {
               err::throw_loc(err::Err::CannotAssign, &left_expr.loc);
           }
        }
        _ => {
            err::throw_loc(err::Err::CannotAssign, &left_expr.loc);
        }
    }
}

fn is_var_illegal(var: &str) -> bool {
    if let "void" | "null" | "Print" = var {
        true
    } else { false }
}

value_repr

don’t forget to implement a case in the value_repr function.

fn value_repr(val: value::Value) -> String {
    match val {
    
        ...

        value::Value::Null => "<null>".to_owned(),

        ...

    }
}

CannotAssign Error

You must also define the CannotAssign error.

src/err.rs:

pub enum Err {

    ...

    CannotAssign,
    CannotAssignIllegalName,
}

pub fn throw(err: Err) {
    match err {

        ...

        Err::CannotAssign => eprintln!("can't assign"),
        Err::CannotAssignIllegalName => eprintln!("can't assign to a reserved variable"),
    }
}

Testing

Let’s test!

var my_number = 1
my_number += 2
Print(my_number ^ 3) # Output: 27

Source Code

Source code for this part can be found here.

Next Up

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

Updated: