From 2da10ae7f19276ea8dd1cd825e01876775a4458c Mon Sep 17 00:00:00 2001 From: Alex van de Sandt Date: Tue, 28 May 2024 18:53:40 +0000 Subject: [PATCH] Implement binary expression interpreting --- src/interpreter.rs | 385 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 ++ src/span.rs | 4 ++++ src/value.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/runners/file.rs | 6 +++++- src/runners/repl.rs | 26 +++++++++++++++++++++----- 6 file(s) changed, 497 insertion(s)(+), 6 deletion(s)(-) diff --git a/src/interpreter.rs b/src/interpreter.rs new file mode 100644 --- /dev/null +++ b/src/interpreter.rs @@ -0,0 +1,385 @@ +use miette::SourceSpan; +use tracing::{info_span, span}; + +use crate::{ + ast::{Ast, BinaryOp, Expr, Literal, UnaryOp}, + span::{Span, Spanned}, + value::Value, +}; + +type Result = std::result::Result; + +pub fn interpret(ast: &Ast) -> Result { + Interpreter.interpret(ast) +} + +#[cfg(test)] +pub fn interpret_expr(expr: Spanned) -> Result { + Interpreter.interpret_expr(&expr) +} + +struct Interpreter; + +impl Interpreter { + fn interpret(&self, ast: &Ast) -> Result { + self.interpret_expr(&ast.0) + } + + #[tracing::instrument(name = "expr", skip_all)] + fn interpret_expr(&self, expr: &Spanned) -> Result { + match expr.as_ref() { + Expr::Binary { left, op, right } => { + let span = info_span!("binary"); + let _guard = span.enter(); + + tracing::trace!("evaluating lhs of binary expr"); + let left_val = self.interpret_expr(left)?; + + tracing::trace!("evaluating rhs of binary expr"); + let right_val = self.interpret_expr(right)?; + + match op.as_ref() { + BinaryOp::Add => match (&left_val, &right_val) { + (Value::String(left_str), Value::String(right_str)) => { + let mut s = left_str.clone(); + s.push_str(&right_str); + Ok(Value::String(s)) + } + (Value::String(_), _) => Err(RuntimeError::non_string_concat( + right_val, + right.span(), + op.span(), + )), + _ => { + let left_num = left_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic( + left_val, + left.span(), + op.span(), + ) + })?; + let right_num = right_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic( + right_val, + right.span(), + op.span(), + ) + })?; + Ok(Value::Number(left_num + right_num)) + } + }, + BinaryOp::Sub => { + let left_num = left_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(left_val, left.span(), op.span()) + })?; + let right_num = right_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(right_val, left.span(), op.span()) + })?; + Ok(Value::Number(left_num - right_num)) + } + BinaryOp::Mult => { + let left_num = left_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(left_val, left.span(), op.span()) + })?; + let right_num = right_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(right_val, right.span(), op.span()) + })?; + Ok(Value::Number(left_num * right_num)) + } + BinaryOp::Div => { + let left_num = left_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(left_val, left.span(), op.span()) + })?; + let right_num = right_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(right_val, right.span(), op.span()) + })?; + Ok(Value::Number(left_num / right_num)) + } + + BinaryOp::Greater => { + let left_num = left_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(left_val, left.span(), op.span()) + })?; + let right_num = right_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(right_val, right.span(), op.span()) + })?; + Ok(Value::Bool(left_num > right_num)) + } + BinaryOp::GreaterEq => { + let left_num = left_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(left_val, left.span(), op.span()) + })?; + let right_num = right_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(right_val, right.span(), op.span()) + })?; + Ok(Value::Bool(left_num >= right_num)) + } + BinaryOp::Less => { + let left_num = left_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(left_val, left.span(), op.span()) + })?; + let right_num = right_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(right_val, right.span(), op.span()) + })?; + Ok(Value::Bool(left_num < right_num)) + } + BinaryOp::LessEq => { + let left_num = left_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(left_val, left.span(), op.span()) + })?; + let right_num = right_val.as_number().ok_or_else(|| { + RuntimeError::non_number_arithmetic(right_val, right.span(), op.span()) + })?; + Ok(Value::Bool(left_num <= right_num)) + } + + BinaryOp::Eq => Ok(Value::Bool(left_val == right_val)), + BinaryOp::NotEq => Ok(Value::Bool(left_val != right_val)), + } + } + Expr::Grouping { inner } => { + let span = info_span!("grouping"); + let _guard = span.enter(); + tracing::trace!("descending to group"); + // TODO: investigate using more span features here + self.interpret_expr(inner.as_ref()) + } + Expr::Literal { lit } => { + tracing::trace!("resolving literal"); + Ok(Value::from(lit.as_ref())) + } + Expr::Unary { op, expr } => { + let span = info_span!("unary"); + let _guard = span.enter(); + tracing::trace!("descending into unary"); + let val = self.interpret_expr(expr)?; + match op.as_ref() { + UnaryOp::Negative => { + tracing::trace!("negating value"); + val.as_number().map(|n| Value::Number(-n)).ok_or_else(|| { + RuntimeError::NegatedNonNumber { + actual_type: val.type_str(), + span: expr.span().into(), + } + }) + } + UnaryOp::Invert => { + tracing::trace!("inverting value"); + Ok(Value::Bool(!val.is_truthy())) + } + } + } + } + } +} + +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +pub enum RuntimeError { + #[error("Only strings can be concatenated")] + NonStringConcat { + actual_type: &'static str, + #[label("this is a `{actual_type}`")] + value_span: SourceSpan, + #[label("concatenation only works on strings")] + op_span: SourceSpan, + }, + #[error("Artihmetic operators only accept number values")] + NonNumberArithmetic { + actual_type: &'static str, + #[label("this is a `{actual_type}`")] + value_span: SourceSpan, + #[label("this operator only accepts numbers")] + op_span: SourceSpan, + }, + #[error("Only number values can be negated")] + NegatedNonNumber { + actual_type: &'static str, + #[label("this is a `{actual_type}`")] + span: SourceSpan, + }, +} + +impl RuntimeError { + fn non_string_concat(value: Value, value_span: Span, op_span: Span) -> Self { + Self::NonStringConcat { + actual_type: value.type_str(), + value_span: value_span.into(), + op_span: op_span.into(), + } + } + + fn non_number_arithmetic(value: Value, value_span: Span, op_span: Span) -> Self { + Self::NonNumberArithmetic { + actual_type: value.type_str(), + value_span: value_span.into(), + op_span: op_span.into(), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{parser::parse, scanner::scan}; + use claims::assert_matches; + + fn interpret_expr_to_value(input: &str) -> Value { + let tokens = + scan(input).unwrap_or_else(|e| panic!("input `{input}` should scan. error: {e:?}")); + let ast = + parse(tokens).unwrap_or_else(|e| panic!("input `{input}` should parse. error: {e:?}")); + interpret_expr(ast.0) + .unwrap_or_else(|e| panic!("input `{input}` to be interpreted. error: {e:?}")) + } + + fn interpret_expr_to_err(input: &str) -> RuntimeError { + let tokens = + scan(input).unwrap_or_else(|e| panic!("input `{input}` should scan. error: {e:?}")); + let ast = + parse(tokens).unwrap_or_else(|e| panic!("input `{input}` should parse. error: {e:?}")); + interpret_expr(ast.0).unwrap_err() + } + + #[test] + fn interprets_arithmetic() { + for (input, expected_value) in [ + ("2 + 2", 4.0), + ("2 - 2", 0.0), + ("2 * 2", 4.0), + ("2 / 2", 1.0), + ] { + let value = interpret_expr_to_value(input); + assert_eq!(value, Value::Number(expected_value), "input: `{input}`"); + } + } + + #[test] + fn interprets_concatenation() { + let value = interpret_expr_to_value(r#""hello " + "world""#); + assert_eq!(value, Value::String("hello world".to_string())) + } + + #[test] + fn interprets_comparison() { + for (input, expected_value) in [ + ("2 > 2", false), + ("2 >= 2", true), + ("2 < 2", false), + ("2 >= 2", true), + ] { + assert_eq!( + interpret_expr_to_value(input), + Value::Bool(expected_value), + "input: {input}", + ); + } + } + + #[test] + fn interprets_equality() { + for (l, r, expected_eq) in [ + ("1", "1", true), + ("1", "2", false), + (r#""hello""#, r#""hello""#, true), + (r#""hello""#, r#""world""#, false), + ("true", "true", true), + ("true", "false", false), + ("nil", "nil", true), + ("nil", "1", false), + ] { + let input_eq = format!("{l} == {r}"); + assert_eq!(interpret_expr_to_value(&input_eq), Value::Bool(expected_eq)); + let input_neq = format!("{l} != {r}"); + assert_eq!( + interpret_expr_to_value(&input_neq), + Value::Bool(!expected_eq) + ); + } + } + + #[test] + fn interprets_grouping() { + let value = interpret_expr_to_value("(1)"); + assert_eq!(value, Value::Number(1.0)); + } + + #[test] + fn interprets_literal() { + for (input, expected_value) in [ + (r#""hello""#, Value::String("hello".to_string())), + ("1", Value::Number(1.0)), + ("true", Value::Bool(true)), + ("false", Value::Bool(false)), + ("nil", Value::Nil), + ] { + let value = interpret_expr_to_value(input); + assert_eq!(value, expected_value, "input: `{input}`"); + } + } + + #[test] + fn interprets_negation() { + let value = interpret_expr_to_value("-1"); + assert_eq!(value, Value::Number(-1.0)); + } + + #[test] + fn interprets_inversion() { + for (input, expected_value) in [ + ("!true", false), + ("!false", true), + (r#"!"hello""#, false), + (r#"!"""#, false), + ("!1", false), + ] { + let value = interpret_expr_to_value(input); + assert_eq!(value, Value::Bool(expected_value), "input: `{input}`"); + } + } + + #[test] + fn errs_on_illegal_arithmetic() { + for input in [ + "1 + true", + "1 - true", + "1 * true", + "1 / true", + "nil + 1", + "nil - 1", + "nil * 1", + "nil / 1", + "true + nil", + "true - nil", + "true * nil", + "true / nil", + ] { + assert_matches!( + interpret_expr_to_err(input), + RuntimeError::NonNumberArithmetic { .. }, + "input: {input}", + ) + } + } + + #[test] + fn errs_on_illegal_concatenation() { + for input in [r#""hello" + 1"#, r#""hello" + true"#, r#""hello" + nil"#] { + assert_matches!( + interpret_expr_to_err(input), + RuntimeError::NonStringConcat { .. }, + "input: {input}" + ) + } + } + + #[test] + fn errs_on_negated_non_number() { + for input in [r#""hello""#, "true", "false", "nil"] { + assert_matches!( + interpret_expr_to_err(&format!("-{input}")), + RuntimeError::NegatedNonNumber { .. }, + "input: {input}", + ); + } + } +} diff --git a/src/lib.rs b/src/lib.rs --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ mod ast; +mod interpreter; mod logging; mod match_token; mod parser; @@ -6,6 +7,7 @@ mod scanner; mod span; mod token; +mod value; pub use logging::start_tracing; pub use runners::{file::run_file, repl::run_repl}; diff --git a/src/span.rs b/src/span.rs --- a/src/span.rs +++ b/src/span.rs @@ -39,6 +39,10 @@ Self(inner, span) } + pub fn as_ref(&self) -> &T { + &self.0 + } + pub fn span(&self) -> Span { self.1 } diff --git a/src/value.rs b/src/value.rs new file mode 100644 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,80 @@ +use std::fmt; + +use crate::ast::Literal; + +#[derive(Clone, Debug)] +pub enum Value { + String(String), + Number(f64), + Bool(bool), + Nil, +} + +impl Value { + pub fn is_truthy(&self) -> bool { + match self { + Value::Bool(b) => *b, + Value::Nil => false, + _ => true, + } + } + + pub fn type_str(&self) -> &'static str { + match self { + Value::String(_) => "string", + Value::Number(_) => "number", + Value::Bool(_) => "bool", + Value::Nil => "nil", + } + } + + pub fn as_number(&self) -> Option { + match self { + Value::Number(n) => Some(*n), + _ => None, + } + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Number(l), Self::Number(r)) => { + // See footnote at end of section 7.2 + if l.is_nan() { + r.is_nan() + } else { + l.eq(r) + } + } + (Self::Bool(l), Self::Bool(r)) => l.eq(r), + (Self::String(l), Self::String(r)) => l.eq(r), + (Self::Nil, Self::Nil) => true, + _ => false, + } + } +} + +impl From<&Literal> for Value { + fn from(lit: &Literal) -> Self { + match lit { + Literal::String(s) => Self::String(s.clone()), + Literal::Number(n) => Self::Number(*n), + Literal::Bool(b) => Self::Bool(*b), + Literal::Nil => Self::Nil, + } + } +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Value::String(s) => write!(f, r#""{s}""#), + Value::Number(n) => n.fmt(f), + Value::Bool(b) => b.fmt(f), + Value::Nil => "nil".fmt(f), + } + } +} + +// TODO: tests for truthiness and equality diff --git a/src/runners/file.rs b/src/runners/file.rs --- a/src/runners/file.rs +++ b/src/runners/file.rs @@ -2,6 +2,7 @@ use miette::{IntoDiagnostic, Report, Result, WrapErr}; +use crate::interpreter::interpret; use crate::{parser::parse, scanner::scan}; #[tracing::instrument(skip_all, fields(path = %path.display()))] @@ -15,8 +16,11 @@ let tokens = scan(&contents).map_err(|e| Report::new(e).with_source_code(contents.clone()))?; dbg!(&tokens.len()); - let ast = parse(tokens).map_err(|e| Report::new(e).with_source_code(contents))?; + let ast = parse(tokens).map_err(|e| Report::new(e).with_source_code(contents.clone()))?; dbg!(&ast); + + let final_value = interpret(&ast).map_err(|e| Report::new(e).with_source_code(contents))?; + println!("{final_value}"); Ok(()) } diff --git a/src/runners/repl.rs b/src/runners/repl.rs --- a/src/runners/repl.rs +++ b/src/runners/repl.rs @@ -2,6 +2,7 @@ use miette::{IntoDiagnostic, Report, Result, WrapErr}; +use crate::interpreter::interpret; use crate::{parser::parse, scanner::scan}; #[tracing::instrument] @@ -42,8 +43,8 @@ } }; - if !tokens.is_empty() { - let ast = match parse(tokens) { + let ast = if !tokens.is_empty() { + match parse(tokens) { Ok(ast) => ast, Err(e) => { let report = Report::new(e).with_source_code(line.clone()); @@ -52,9 +53,24 @@ prompt()?; continue; } - }; - dbg!(&ast); - } + } + } else { + prompt()?; + continue; + }; + dbg!(&ast); + + let final_value = match interpret(&ast) { + Ok(value) => value, + Err(e) => { + let report = Report::new(e).with_source_code(line.clone()); + eprintln!("{report:?}"); + prompt()?; + continue; + } + }; + // dbg!(&final_value); + println!("{final_value}"); prompt()?; } -- tangled.sh