Interp - an Interpreted Programming Language Pt.12 - Functions

5 minute read

Hello there! Let’s do functions. It’s Pt. 12 ;)

Parsing

ExprType

Add the Ref ExprType which is used to pass values to functions by reference.

src/parser/expr.rs:

pub enum ExprType {
    ...
    Ref(Box<Expr>),
}

LineType

Add the FunctionDef and Return LineType.

src/parser/line.rs:

pub enum LineType {
    ...
    FunctionDef(Expr, Vec<Expr>),
    Return(Expr),
}

Grammar

Add these tokens and grammar rules.

src/parser/grammar.lalrpop:

match {

    ...

    "fun",
    "ref",
    "ret",
}

...

pub Line: Line = {

    ...

    <l:@L> "fun" <a:variable> "(" <b:expr_list> ")" "{" => Line { type_: LineType::FunctionDef(a, b), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
    <l:@L> "ret" <a:expr> => Line { type_: LineType::Return(a), loc: Loc { filename: filename.to_owned(), line_nr, index: l } },
}

expr0: Expr = {

    ...

    ref_,
}

...

ref_: Expr = {
    <l:@L> "ref" <a:variable> => Expr { type_: ExprType::Ref(Box::new(a)), loc: Loc { filename: filename.to_owned(), line_nr, index: l } }
}

VM

Value

Add the following Value.

src/vm/value.rs:

use crate::parser::line::Line;

pub enum Value {
    
    ...

    Function { params: Vec<String>, lines: Vec<Line> },
}

VM State

Let’s add some state. Symbols will now be a vector. Each time a function is called, a new HashMap will be pushed into Symbols. The stack address of a variable will first be searched from the last HashMap to the first. Functions can “shadow” other variables. The ScopeState Function variant is active when gathering a functions body. The Return variant is active when a return statement is found. It will exit all loops and return a value.

src/vm/mod.rs:

pub struct VM {

    ...

    pub symbols: Vec<HashMap<String, usize>>,

    ...
}

pub fn init_vm() -> VM {
    VM {
        
        ...

        symbols: vec![HashMap::new()],

        ...
        
    }
}

#[derive(Debug)]
pub enum ScopeState {

    ...
    
    Function { name_expr: Expr, params: Vec<Expr> },
    Return { value: value::Value },
}

exec_line

Modify exec_line. Var case checks if the current function contains a variable of the specified name and overrides it if found. Otherwise a new variable is pushed into the symbol HashMap and the value is pushed onto the stack.

src/vm/mod.rs:

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

    match &parsed_line.type_ {

        ...
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);
            if let Some(address) = vm.symbols.last_mut().unwrap().get(var) {
                // Override
                vm.stack[*address] = value;
            } else {
                // Create
                vm.symbols.last_mut().unwrap().insert(var.clone(), vm.stack.len());
                vm.stack.push(value);
            }
        }
    } else { panic!() }
}

...

We must add Return cases to ElseIf, Else, and EndScope so that we can return inside if and else if blocks.

LineType::ElseIf(cond_expr) => {

            ...

        ScopeState::Exec => {
            for line in prev_line_buffer {
                exec_line(vm, &line);
            }
            if let ScopeState::Return { .. } = vm.scope_state {
                return;
            } else {
                vm.scope_state = ScopeState::DontExec;
            }
        }

            ...

LineType::ElseIf(cond_expr) => {

        ...
            
        ScopeState::Exec => {
            for line in prev_line_buffer {
                exec_line(vm, &line);
            }
            if let ScopeState::Return { .. } = vm.scope_state {
                return;
            } else {
                vm.scope_state = ScopeState::DontExec;
            }
        }

            ...

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 => {}
        ScopeState::While { cond_expr, name: while_name } => {
            'outer: loop {
                let cond_value = expr_to_value(vm, &cond_expr);
                if let value::Value::Boolean(cond) = cond_value {
                    if cond {
                        for line in &prev_line_buffer {
                            exec_line(vm, line);
                            if let ScopeState::Break { name } = &vm.scope_state {

                                ...

                            }
                            if let ScopeState::Continue { name } = &vm.scope_state {

                                ...

                            }
                            if let ScopeState::Return { .. } = vm.scope_state {
                                return;
                            }

Let’s modify EndScope. After the function body has been gathered a Function value is made and assigned to a variable similar to the Var case.

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::Function { name_expr, params } => {
    let func_val = value::Value::Function {
        lines: prev_line_buffer,
        params: params.into_iter().map(|a| {
            if let ExprType::Variable(var) = a.type_ {
                var
            } else {
                panic!()
            }
        }).collect()
    };
    if let ExprType::Variable(var) = &name_expr.type_ {
        if is_var_illegal(var) {
            err::throw_loc(err::Err::CannotAssign, &name_expr.loc);
        } else {
            if let Some(address) = vm.symbols.last_mut().unwrap().get(var) {
                vm.stack[*address] = func_val;
            } else {
                vm.symbols.last_mut().unwrap().insert(var.clone(), vm.stack.len());
                vm.stack.push(func_val);
            }
        }
    } else { panic!() }
}

...

Add the new LineType cases.

    }
}

// Add these
LineType::FunctionDef(name_expr, params) => {
    vm.scope_state = ScopeState::Function { name_expr: name_expr.clone(), params: params.clone() };
    vm.scope = 1;  // Into gathering mode
}
LineType::Return(expr) => {
    vm.scope_state = ScopeState::Return { value: expr_to_value(vm, expr) };
}

...

var_address

Modify var_address.

fn var_address(vm: &VM, var: &str) -> Option<usize> {
    for i in (0..vm.symbols.len()).rev() {
        if let Some(address) = vm.symbols[i].get(var) {
            return Some(*address);
        }
    }
    None
}

expr_to_value

src/vm/expr_to_value.rs:

use crate::vm::{..., exec_line, ScopeState};

...

use std::collections::HashMap;

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

        ...

Modify Call. We store the stack length before the function so we can discard local variables. Iterate the input argument expressions. If a Ref expression is encountered we add the address of the Refd variable to the parameter. otherwise we push the argument into the stack.

ExprType::Call(fn_expr, arg_exprs) => {
    let fn_value = expr_to_value(vm, &fn_expr);
    match fn_value {
value::Value::Function { params, lines } => {
    if arg_exprs.len() != params.len() {
        err::throw_loc(err::Err::WrongNumberOfArgs { n: params.len() }, &fn_expr.loc);
    }

    let stack_len = vm.stack.len();

    let mut symbols = HashMap::new();
    for i in 0..arg_exprs.len().min(params.len()) {
        if let ExprType::Ref(ref_expr) = &arg_exprs[i].type_ {
            if let ExprType::Variable(var) = &ref_expr.type_ {
                if let Some(address) = var_address(vm, var) {
                    symbols.insert(params[i].clone(), address);
                } else {
                    err::throw_loc(err::Err::CannotTakeReference, &ref_expr.loc);
                }
            }
        } else {
            symbols.insert(params[i].clone(), vm.stack.len());
            let value = expr_to_value(vm, &arg_exprs[i]);
            vm.stack.push(value);
        }
    }

    vm.symbols.push(symbols);
    for line in lines {
        exec_line(vm, &line);
        if let ScopeState::Return { value } = &vm.scope_state {
            let value = value.clone();
            vm.scope_state = ScopeState::Regular;
            vm.symbols.pop();
            vm.stack.truncate(stack_len);
            return value;
        }
    }
    vm.symbols.pop().unwrap();

    vm.stack.truncate(stack_len);
    value::Value::Void
}
value::Value::PrintBuiltin => {
    println!("{}", arg_exprs.into_iter().map(
        |a| value_repr(expr_to_value(vm, a)))
        .collect::<Vec<String>>().join(" "));
    value::Value::Void
}
_ => {
    err::throw_loc(err::Err::CannotCall, &fn_expr.loc);
    value::Value::Void
}  

...

Ref can’t be used outside a function call so let’s add a case for it.

        ExprType::Ref(_) => {
            err::throw_loc(err::Err::RefOutsideCall, &expr.loc);
            value::Value::Void
        }
    }
}

value_repr

Add the repr.

src/vm/value_repr.rs:

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

        ...

        value::Value::Function { params, .. } => 
            format!("f({})", params.join(", ")),
    }
}

Errors

Add the following Errs.

src/err.rs:

pub enum Err {

    ...

    RefOutsideCall,
    WrongNumberOfArgs { n: usize },
    CannotTakeReference,
    CannotCall,
}

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

        Err::RefOutsideCall => eprintln!("can't take reference, not calling a function"),
        Err::WrongNumberOfArgs { n } => eprintln!("expected {} arguments", n),
        Err::CannotTakeReference => eprintln!("can't take reference"),
        Err::CannotCall => eprintln!("can't call"),
    }
}

Source Code

Source code for this part can be found here.

Next Up

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

Updated: