Advanced Projects

Mini Programming Language Interpreter

A tree-walking interpreter executes a small imperative language by transforming source code into tokens (lexer), building an Abstract Syntax Tree of statements and expressions (parser), and evaluating nodes against a lexically scoped environment (evaluator). Supporting let bindings, assignment, if/else conditionals, while loops, and print statements exercises recursive AST modeling (Box<Expr>, Vec<Stmt>) and runtime value representation (Value).

Interpreter Pipeline

Source String
   |
   v  Lexer::tokenize()
Vec<Token>  ([Let, Ident("x"), Assign, Int(1), Semi, ...])
   |
   v  Parser::parse_program()
Vec<Stmt>   ([Stmt::Let { name: "x", expr }, Stmt::While { cond, body }, ...])
   |
   v  Interpreter::exec_program(&mut Environment)
Runtime Output + Final Environment State
Stage Primary Types Responsibility
Lexer Token Scans keywords (let, if, else, while, print), identifiers, integer literals, and operators
AST Stmt, Expr, BinOp Represents control flow blocks and recursive arithmetic/comparison expressions
Runtime Value, Environment Stores variable bindings across nested block scopes and evaluates expressions

Complete Implementation

use std::collections::HashMap;
use std::fmt;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Token {
    Let,
    If,
    Else,
    While,
    Print,
    Ident(String),
    Int(i64),
    Plus,
    Minus,
    Star,
    Slash,
    Assign,
    EqEq,
    Lt,
    Gt,
    LParen,
    RParen,
    LBrace,
    RBrace,
    Semi,
    Eof,
}

pub fn tokenize(src: &str) -> Result<Vec<Token>, String> {
    let mut chars = src.chars().peekable();
    let mut tokens = Vec::new();

    while let Some(&ch) = chars.peek() {
        if ch.is_whitespace() {
            chars.next();
        } else if ch.is_ascii_digit() {
            let mut num = 0i64;
            while let Some(&d) = chars.peek() {
                if d.is_ascii_digit() {
                    num = num * 10 + (d as i64 - '0' as i64);
                    chars.next();
                } else {
                    break;
                }
            }
            tokens.push(Token::Int(num));
        } else if ch.is_ascii_alphabetic() || ch == '_' {
            let mut ident = String::new();
            while let Some(&c) = chars.peek() {
                if c.is_ascii_alphanumeric() || c == '_' {
                    ident.push(c);
                    chars.next();
                } else {
                    break;
                }
            }
            let tok = match ident.as_str() {
                "let" => Token::Let,
                "if" => Token::If,
                "else" => Token::Else,
                "while" => Token::While,
                "print" => Token::Print,
                _ => Token::Ident(ident),
            };
            tokens.push(tok);
        } else {
            chars.next();
            match ch {
                '+' => tokens.push(Token::Plus),
                '-' => tokens.push(Token::Minus),
                '*' => tokens.push(Token::Star),
                '/' => tokens.push(Token::Slash),
                '<' => tokens.push(Token::Lt),
                '>' => tokens.push(Token::Gt),
                '(' => tokens.push(Token::LParen),
                ')' => tokens.push(Token::RParen),
                '{' => tokens.push(Token::LBrace),
                '}' => tokens.push(Token::RBrace),
                ';' => tokens.push(Token::Semi),
                '=' => {
                    if chars.peek() == Some(&'=') {
                        chars.next();
                        tokens.push(Token::EqEq);
                    } else {
                        tokens.push(Token::Assign);
                    }
                }
                other => return Err(format!("unexpected character: '{other}'")),
            }
        }
    }
    tokens.push(Token::Eof);
    Ok(tokens)
}

#[derive(Debug, Clone, Copy)]
pub enum BinOp {
    Add,
    Sub,
    Mul,
    Div,
    Eq,
    Lt,
    Gt,
}

#[derive(Debug, Clone)]
pub enum Expr {
    Int(i64),
    Var(String),
    Binary {
        op: BinOp,
        lhs: Box<Expr>,
        rhs: Box<Expr>,
    },
}

#[derive(Debug, Clone)]
pub enum Stmt {
    Let {
        name: String,
        expr: Expr,
    },
    Assign {
        name: String,
        expr: Expr,
    },
    Print(Expr),
    If {
        cond: Expr,
        then_branch: Vec<Stmt>,
        else_branch: Vec<Stmt>,
    },
    While {
        cond: Expr,
        body: Vec<Stmt>,
    },
}

pub struct Parser {
    tokens: Vec<Token>,
    pos: usize,
}

impl Parser {
    pub fn new(tokens: Vec<Token>) -> Self {
        Self { tokens, pos: 0 }
    }

    fn peek(&self) -> &Token {
        &self.tokens[self.pos]
    }

    fn bump(&mut self) -> Token {
        let tok = self.tokens[self.pos].clone();
        if self.pos + 1 < self.tokens.len() {
            self.pos += 1;
        }
        tok
    }

    fn expect(&mut self, expected: Token) -> Result<(), String> {
        let actual = self.bump();
        if actual == expected {
            Ok(())
        } else {
            Err(format!("expected {expected:?}, found {actual:?}"))
        }
    }

    pub fn parse_program(&mut self) -> Result<Vec<Stmt>, String> {
        let mut stmts = Vec::new();
        while *self.peek() != Token::Eof {
            stmts.push(self.parse_stmt()?);
        }
        Ok(stmts)
    }

    fn parse_block(&mut self) -> Result<Vec<Stmt>, String> {
        self.expect(Token::LBrace)?;
        let mut stmts = Vec::new();
        while *self.peek() != Token::RBrace && *self.peek() != Token::Eof {
            stmts.push(self.parse_stmt()?);
        }
        self.expect(Token::RBrace)?;
        Ok(stmts)
    }

    fn parse_stmt(&mut self) -> Result<Stmt, String> {
        match self.peek().clone() {
            Token::Let => {
                self.bump();
                let Token::Ident(name) = self.bump() else {
                    return Err("expected identifier after 'let'".into());
                };
                self.expect(Token::Assign)?;
                let expr = self.parse_expr()?;
                self.expect(Token::Semi)?;
                Ok(Stmt::Let { name, expr })
            }
            Token::Print => {
                self.bump();
                let expr = self.parse_expr()?;
                self.expect(Token::Semi)?;
                Ok(Stmt::Print(expr))
            }
            Token::If => {
                self.bump();
                let cond = self.parse_expr()?;
                let then_branch = self.parse_block()?;
                let else_branch = if *self.peek() == Token::Else {
                    self.bump();
                    self.parse_block()?
                } else {
                    Vec::new()
                };
                Ok(Stmt::If {
                    cond,
                    then_branch,
                    else_branch,
                })
            }
            Token::While => {
                self.bump();
                let cond = self.parse_expr()?;
                let body = self.parse_block()?;
                Ok(Stmt::While { cond, body })
            }
            Token::Ident(name) => {
                self.bump();
                self.expect(Token::Assign)?;
                let expr = self.parse_expr()?;
                self.expect(Token::Semi)?;
                Ok(Stmt::Assign { name, expr })
            }
            other => Err(format!("unexpected token at statement start: {other:?}")),
        }
    }

    fn parse_expr(&mut self) -> Result<Expr, String> {
        let mut lhs = self.parse_additive()?;
        while let Token::EqEq | Token::Lt | Token::Gt = self.peek() {
            let op = match self.bump() {
                Token::EqEq => BinOp::Eq,
                Token::Lt => BinOp::Lt,
                Token::Gt => BinOp::Gt,
                _ => unreachable!(),
            };
            let rhs = self.parse_additive()?;
            lhs = Expr::Binary {
                op,
                lhs: Box::new(lhs),
                rhs: Box::new(rhs),
            };
        }
        Ok(lhs)
    }

    fn parse_additive(&mut self) -> Result<Expr, String> {
        let mut lhs = self.parse_multiplicative()?;
        while let Token::Plus | Token::Minus = self.peek() {
            let op = match self.bump() {
                Token::Plus => BinOp::Add,
                Token::Minus => BinOp::Sub,
                _ => unreachable!(),
            };
            let rhs = self.parse_multiplicative()?;
            lhs = Expr::Binary {
                op,
                lhs: Box::new(lhs),
                rhs: Box::new(rhs),
            };
        }
        Ok(lhs)
    }

    fn parse_multiplicative(&mut self) -> Result<Expr, String> {
        let mut lhs = self.parse_primary()?;
        while let Token::Star | Token::Slash = self.peek() {
            let op = match self.bump() {
                Token::Star => BinOp::Mul,
                Token::Slash => BinOp::Div,
                _ => unreachable!(),
            };
            let rhs = self.parse_primary()?;
            lhs = Expr::Binary {
                op,
                lhs: Box::new(lhs),
                rhs: Box::new(rhs),
            };
        }
        Ok(lhs)
    }

    fn parse_primary(&mut self) -> Result<Expr, String> {
        match self.bump() {
            Token::Int(n) => Ok(Expr::Int(n)),
            Token::Ident(name) => Ok(Expr::Var(name)),
            Token::LParen => {
                let expr = self.parse_expr()?;
                self.expect(Token::RParen)?;
                Ok(expr)
            }
            other => Err(format!("expected expression, found {other:?}")),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Value {
    Int(i64),
    Bool(bool),
}

impl fmt::Display for Value {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Value::Int(n) => write!(f, "{n}"),
            Value::Bool(b) => write!(f, "{b}"),
        }
    }
}

pub struct Interpreter {
    scopes: Vec<HashMap<String, Value>>,
    pub output: Vec<String>,
}

impl Interpreter {
    pub fn new() -> Self {
        Self {
            scopes: vec![HashMap::new()],
            output: Vec::new(),
        }
    }

    fn get_var(&self, name: &str) -> Result<Value, String> {
        for scope in self.scopes.iter().rev() {
            if let Some(&val) = scope.get(name) {
                return Ok(val);
            }
        }
        Err(format!("undefined variable '{name}'"))
    }

    fn assign_var(&mut self, name: &str, val: Value) -> Result<(), String> {
        for scope in self.scopes.iter_mut().rev() {
            if let Some(slot) = scope.get_mut(name) {
                *slot = val;
                return Ok(());
            }
        }
        Err(format!("cannot assign to undeclared variable '{name}'"))
    }

    pub fn eval_expr(&self, expr: &Expr) -> Result<Value, String> {
        match expr {
            Expr::Int(n) => Ok(Value::Int(*n)),
            Expr::Var(name) => self.get_var(name),
            Expr::Binary { op, lhs, rhs } => {
                let (Value::Int(a), Value::Int(b)) = (self.eval_expr(lhs)?, self.eval_expr(rhs)?)
                else {
                    return Err("binary operators require integer operands".into());
                };
                match op {
                    BinOp::Add => Ok(Value::Int(a + b)),
                    BinOp::Sub => Ok(Value::Int(a - b)),
                    BinOp::Mul => Ok(Value::Int(a * b)),
                    BinOp::Div => {
                        if b == 0 {
                            Err("division by zero".into())
                        } else {
                            Ok(Value::Int(a / b))
                        }
                    }
                    BinOp::Eq => Ok(Value::Bool(a == b)),
                    BinOp::Lt => Ok(Value::Bool(a < b)),
                    BinOp::Gt => Ok(Value::Bool(a > b)),
                }
            }
        }
    }

    pub fn exec_block(&mut self, stmts: &[Stmt]) -> Result<(), String> {
        self.scopes.push(HashMap::new());
        for stmt in stmts {
            self.exec_stmt(stmt)?;
        }
        self.scopes.pop();
        Ok(())
    }

    pub fn exec_stmt(&mut self, stmt: &Stmt) -> Result<(), String> {
        match stmt {
            Stmt::Let { name, expr } => {
                let val = self.eval_expr(expr)?;
                self.scopes.last_mut().unwrap().insert(name.clone(), val);
                Ok(())
            }
            Stmt::Assign { name, expr } => {
                let val = self.eval_expr(expr)?;
                self.assign_var(name, val)
            }
            Stmt::Print(expr) => {
                let val = self.eval_expr(expr)?;
                self.output.push(val.to_string());
                Ok(())
            }
            Stmt::If {
                cond,
                then_branch,
                else_branch,
            } => {
                if self.eval_expr(cond)? == Value::Bool(true) {
                    self.exec_block(then_branch)
                } else {
                    self.exec_block(else_branch)
                }
            }
            Stmt::While { cond, body } => {
                while self.eval_expr(cond)? == Value::Bool(true) {
                    self.exec_block(body)?;
                }
                Ok(())
            }
        }
    }
}

fn main() {
    let program_src = r#"
        let n = 6;
        let acc = 1;
        let step = 1;

        while step < n + 1 {
            acc = acc * step;
            if step == 3 {
                print acc;
            }
            step = step + 1;
        }
        print acc;
    "#;

    let tokens = tokenize(program_src).expect("lexing failed");
    let mut parser = Parser::new(tokens);
    let ast = parser.parse_program().expect("parsing failed");

    let mut interp = Interpreter::new();
    for stmt in &ast {
        interp.exec_stmt(stmt).expect("runtime error");
    }

    println!("Executed {} top-level statements.", ast.len());
    println!("Program stdout lines: {:?}", interp.output);
}
Executed 5 top-level statements.
Program stdout lines: ["6", "720"]