From 7485394c27aa0618fe5202c5e4201b73cf9a8e67 Mon Sep 17 00:00:00 2001 From: Rob Hand <146272+sinon@users.noreply.github.com> Date: Sat, 19 Jul 2025 16:15:07 +0100 Subject: [PATCH 01/11] clock builtin/native fn --- src/eval.rs | 8 +++ src/main.rs | 2 +- src/parser.rs | 52 +++++++++++++++- src/run.rs | 169 ++++++++++++++++++++++++++++++++++++++------------ test.lox | 8 +++ 5 files changed, 197 insertions(+), 42 deletions(-) diff --git a/src/eval.rs b/src/eval.rs index 6065b84..f1f8c0f 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -8,6 +8,7 @@ use std::fmt::Display; use crate::{ eval_parser::{Expr, LiteralAtom, Parser}, lexer::TokenType, + run::NativeFunction, }; /// The value that an expression has evaluated too, this can be a literal. @@ -21,6 +22,8 @@ pub enum EvaluatedValue { Nil, /// Boolean value `true`/`false` Bool(bool), + /// fn + NativeFunction(NativeFunction), } impl EvaluatedValue { @@ -29,6 +32,7 @@ impl EvaluatedValue { Self::String(_) | Self::Number(_) => true, Self::Nil => false, Self::Bool(b) => *b, + Self::NativeFunction(f) => true, } } } @@ -39,6 +43,7 @@ impl From for bool { EvaluatedValue::String(_) | EvaluatedValue::Number(_) => true, EvaluatedValue::Nil => false, EvaluatedValue::Bool(b) => b, + EvaluatedValue::NativeFunction(f) => true, } } } @@ -50,6 +55,7 @@ impl Display for EvaluatedValue { Self::Number(n) => write!(f, "{n}"), Self::Nil => write!(f, "nil"), Self::Bool(b) => write!(f, "{b:}"), + Self::NativeFunction(native_fn) => write!(f, "nat fn # TODO"), } } } @@ -177,6 +183,7 @@ fn evaluate_expression(expr: Expr) -> Result { true => Ok(EvaluatedValue::Bool(false)), false => Ok(EvaluatedValue::Bool(true)), }, + EvaluatedValue::NativeFunction(f) => todo!(), }, ), TokenType::Minus => r.as_ref().map_or_else( @@ -186,6 +193,7 @@ fn evaluate_expression(expr: Expr) -> Result { EvaluatedValue::Number(n) => Ok(EvaluatedValue::Number(-n)), EvaluatedValue::Nil => todo!(), EvaluatedValue::Bool(_) => todo!(), + EvaluatedValue::NativeFunction(_) => todo!(), }, ), // TODO: Make unrepresentable by narrowing `operator` to `UnaryOperator:Not|Negate` diff --git a/src/main.rs b/src/main.rs index 5e4cd0f..d959d03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,7 +30,7 @@ fn main() -> Result { Commands::Tokenize { filename } => { let input = fs::read_to_string(filename) .into_diagnostic() - .wrap_err_with(|| "reading file".to_string())?; + .wrap_err_with(|| format!("reading file"))?; Ok(Lexer::new(&input).tokenize_lex()) } Commands::Parse { filename } => { diff --git a/src/parser.rs b/src/parser.rs index 824ff24..915872e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -63,6 +63,15 @@ pub enum Expr<'de> { /// The right expression of a Logical expression right: Box>, }, + /// Function `Call` + Call { + /// function to be called + callee: Box>, + /// paren token + paren: Token<'de>, + /// arguments to be passed to function call + arguments: Vec>, + }, } #[derive(Debug)] @@ -196,7 +205,7 @@ impl<'de> Parser<'de> { }; if let Some(init) = initializer { body = Stmt::Block(vec![init, body]); - } + }; Ok(body) } @@ -382,7 +391,46 @@ impl<'de> Parser<'de> { }); } - self.primary() + self.call() + } + + fn call(&mut self) -> Result, String> { + let mut expr = self.primary()?; + + loop { + if self.match_tokens(&[TokenType::LeftParen]) { + expr = self.finish_call(expr)?; + } else { + break; + } + } + Ok(expr) + } + + fn finish_call(&mut self, callee: Expr<'de>) -> Result, String> { + let mut arguments: Vec> = vec![]; + + if !self.check(&TokenType::RightParen) { + loop { + arguments.push(self.expression()?); + // https://users.rust-lang.org/t/how-many-arguments-can-i-pass-to-a-function/84250 + if arguments.len() >= 65535 { + return Err("Can't have more than 65535 arguments.".to_string()); + } + if !self.match_tokens(&[TokenType::Comma]) { + break; + } + } + } + let paren = self + .consume(&TokenType::RightParen, "Expect ')' after arguments.")? + .clone(); + + Ok(Expr::Call { + callee: Box::new(callee), + paren, + arguments, + }) } fn primary(&mut self) -> Result, String> { diff --git a/src/run.rs b/src/run.rs index 7f65368..f79111d 100644 --- a/src/run.rs +++ b/src/run.rs @@ -3,7 +3,13 @@ //! Responsible for running the AST and returning the computed values //! -use std::{cell::RefCell, collections::HashMap, rc::Rc}; +use std::{ + cell::RefCell, + collections::HashMap, + fmt, + rc::Rc, + time::{SystemTime, UNIX_EPOCH}, +}; use crate::{ eval::EvaluatedValue, @@ -11,11 +17,47 @@ use crate::{ parser::{Expr, LiteralAtom, Parser, Stmt}, }; +trait Callable { + fn arity(&self, interpreter: &Run) -> u8; + fn call( + &self, + interpreter: &mut Run, + args: &[EvaluatedValue], + ) -> Result; +} + +#[derive(Clone)] +pub struct NativeFunction { + pub name: String, + pub arity: u8, + pub callable: fn(&mut Run, &[EvaluatedValue]) -> Result, +} + +impl fmt::Debug for NativeFunction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "NativeFunction({})", self.name) + } +} + +impl Callable for NativeFunction { + fn arity(&self, _interpreter: &Run) -> u8 { + self.arity + } + fn call( + &self, + interpreter: &mut Run, + args: &[EvaluatedValue], + ) -> Result { + (self.callable)(interpreter, args) + } +} + /// `Run` /// an iterator that consumes expressions from the parser and tries to evaluate them. pub struct Run<'de> { parser: Parser<'de>, environment: Environment<'de>, + globals: Environment<'de>, } #[derive(Debug, Clone)] @@ -76,12 +118,31 @@ impl<'de> Environment<'de> { } impl<'de> Run<'de> { - /// Create a new `Run` to process a given input source code + /// Create a new `Interpreter` to process a given input source code #[must_use] pub fn new(input: &'de str) -> Self { - Run { + let mut global_data = HashMap::new(); + global_data.insert( + "clock", + EvaluatedValue::NativeFunction(NativeFunction { + name: "clock".to_string(), + arity: 0, + callable: |_, _| { + let start = SystemTime::now(); + let since_the_epoch = start.duration_since(UNIX_EPOCH).unwrap(); + + Ok(EvaluatedValue::Number(since_the_epoch.as_millis() as f64)) + }, + }), + ); + let globals = Environment { + data: global_data, + enclosing: None, + }; + Self { parser: Parser::new(input), environment: Environment::new(), + globals, } } } @@ -93,7 +154,7 @@ impl Iterator for Run<'_> { let stmt = self.parser.next()?; match stmt { Ok(s) => { - let eval_stmt = evaluate_statement(&s, &mut self.environment); + let eval_stmt = evaluate_statement(&s, self); match eval_stmt { Ok(()) => Some(Ok(())), Err(_) => Some(Err(70)), @@ -104,51 +165,54 @@ impl Iterator for Run<'_> { } } -fn evaluate_statement<'de>( - stmt: &Stmt<'de>, - environment: &mut Environment<'de>, -) -> Result<(), String> { +fn evaluate_statement<'de>(stmt: &Stmt<'de>, interpreter: &mut Run<'de>) -> Result<(), String> { match stmt { Stmt::Print(expr) => { - let val = evaluate_expression(expr, environment)?; + let val = evaluate_expression(expr, interpreter)?; println!("{val}"); } Stmt::If(cond, then_branch, else_branch) => { - let eval_cond = evaluate_expression(cond, environment)?; + let eval_cond = evaluate_expression(cond, interpreter)?; if eval_cond.into() { - evaluate_statement(then_branch, environment)?; + evaluate_statement(then_branch, interpreter)?; } else if let Some(e) = else_branch { - evaluate_statement(e, environment)?; + evaluate_statement(e, interpreter)?; } } Stmt::ExpressionStatement(expr) => { - evaluate_expression(expr, environment)?; + evaluate_expression(expr, interpreter)?; } Stmt::Var(name, expr) => match expr { Some(v) => { - let evalutated_val = evaluate_expression(v, environment)?; - environment.var_assign(name, &evalutated_val); + let evalutated_val = evaluate_expression(v, interpreter)?; + interpreter.environment.var_assign(name, &evalutated_val); } None => { - environment.var_assign(name, &EvaluatedValue::Nil); + interpreter + .environment + .var_assign(name, &EvaluatedValue::Nil); } }, Stmt::Block(stmts) => { - let mut new_env = Environment::from_parent(environment.clone()); + let new_env = Environment::from_parent(interpreter.environment.clone()); + interpreter.environment = new_env; for stmt in stmts { - evaluate_statement(stmt, &mut new_env)?; + evaluate_statement(stmt, interpreter)?; } - *environment = new_env + // # TODO: Remove this clone + interpreter.environment = interpreter + .environment + .clone() .enclosing .expect("`new_env` declared above will always have `enclosing` set") .borrow() .clone(); } Stmt::While { condition, body } => loop { - if !(evaluate_expression(condition, environment)?.is_truthy()) { + if !(evaluate_expression(condition, interpreter)?.is_truthy()) { break; } - evaluate_statement(body, environment)?; + evaluate_statement(body, interpreter)?; }, } Ok(()) @@ -156,7 +220,9 @@ fn evaluate_statement<'de>( fn evaluate_expression<'de>( expr: &Expr<'de>, - environment: &mut Environment<'de>, + interpreter: &mut Run<'de>, + // environment: &mut Environment<'de>, + // globals: &mut Environment<'de>, ) -> Result { match expr { Expr::Binary { @@ -164,8 +230,8 @@ fn evaluate_expression<'de>( operator, right, } => { - let l_expr = evaluate_expression(left, environment)?; - let r_expr = evaluate_expression(right, environment)?; + let l_expr = evaluate_expression(left, interpreter)?; + let r_expr = evaluate_expression(right, interpreter)?; match operator.token_type { TokenType::Minus | TokenType::Star @@ -242,7 +308,7 @@ fn evaluate_expression<'de>( } } Expr::Unary { operator, right } => { - let r = evaluate_expression(right, environment); + let r = evaluate_expression(right, interpreter); if let (TokenType::Minus, Ok(e)) = (operator.token_type, &r) { if let EvaluatedValue::Number(_) = e { } else { @@ -263,6 +329,7 @@ fn evaluate_expression<'de>( true => Ok(EvaluatedValue::Bool(false)), false => Ok(EvaluatedValue::Bool(true)), }, + EvaluatedValue::NativeFunction(f) => todo!(), }, ), TokenType::Minus => r.as_ref().map_or_else( @@ -272,6 +339,7 @@ fn evaluate_expression<'de>( EvaluatedValue::Number(n) => Ok(EvaluatedValue::Number(-n)), EvaluatedValue::Nil => todo!(), EvaluatedValue::Bool(_) => todo!(), + EvaluatedValue::NativeFunction(f) => todo!(), }, ), // TODO: Make unrepresentable by narrowing `operator` to `UnaryOperator:Not|Negate` @@ -286,19 +354,22 @@ fn evaluate_expression<'de>( LiteralAtom::Nil => Ok(EvaluatedValue::Nil), LiteralAtom::Bool(b) => Ok(EvaluatedValue::Bool(*b)), }, - Expr::Grouping(expr) => evaluate_expression(expr, environment), - Expr::Variable(token) => environment.get(token.origin).map_or_else( - || { - eprintln!("Undefined variable '{}'.", token.origin); - eprintln!("[line {}]", token.line); - Err("Undefined var".to_string()) - }, - Ok, - ), + Expr::Grouping(expr) => evaluate_expression(expr, interpreter), + Expr::Variable(token) => match interpreter.environment.get(token.origin) { + Some(v) => Ok(v), + None => interpreter.globals.get(token.origin).map_or_else( + || { + eprintln!("Undefined variable '{}'.", token.origin); + eprintln!("[line {}]", token.line); + Err("Undefined var".to_string()) + }, + Ok, + ), + }, Expr::Assign(name, expr) => { - if environment.get(name).is_some() { - let eval_expr = evaluate_expression(expr, environment)?; - environment.assign(name, &eval_expr)?; + if interpreter.environment.get(name).is_some() { + let eval_expr = evaluate_expression(expr, interpreter)?; + interpreter.environment.assign(name, &eval_expr)?; Ok(eval_expr) } else { eprintln!("Undefined variable '{name}'."); @@ -310,7 +381,7 @@ fn evaluate_expression<'de>( operator, right, } => { - let left_val = evaluate_expression(left, environment)?; + let left_val = evaluate_expression(left, interpreter)?; let left_truth: bool = left_val.is_truthy(); if operator.token_type == TokenType::Or { if left_truth { @@ -319,7 +390,27 @@ fn evaluate_expression<'de>( } else if !left_truth { return Ok(left_val); } - Ok(evaluate_expression(right, environment)?) + Ok(evaluate_expression(right, interpreter)?) + } + Expr::Call { + callee, + paren, + arguments, + } => { + let callee_fn = evaluate_expression(callee, interpreter)?; + let mut args: Vec = Vec::new(); + for arg in arguments { + args.push(evaluate_expression(arg, interpreter)?); + } + // TODO: Need to check function arity on `EvaluatedValue` for Fn + + match callee_fn { + EvaluatedValue::NativeFunction(native_function) => { + let fun = native_function.call(interpreter, &args); + return fun; + } + _ => todo!(), + } } } } diff --git a/test.lox b/test.lox index d88ec81..2385ff4 100644 --- a/test.lox +++ b/test.lox @@ -1 +1,9 @@ if (true) print "bar"; // "bar" + +print clock(); + +// fun sayHi(first, last) { +// print "Hi, " + first + " " + last + "!"; +// } + +// sayHi("Dear", "Reader"); \ No newline at end of file From a7b8df4a187f4d0dc929ca077c5fa8b03829b418 Mon Sep 17 00:00:00 2001 From: Rob Hand <146272+sinon@users.noreply.github.com> Date: Sat, 19 Jul 2025 16:53:19 +0100 Subject: [PATCH 02/11] lints --- justfile | 4 +++ src/eval.rs | 8 ++--- src/main.rs | 6 ++-- src/parser.rs | 2 +- src/run.rs | 73 ++++++++++++++++++++++++--------------- tests/eval_parser_test.rs | 2 +- 6 files changed, 58 insertions(+), 37 deletions(-) diff --git a/justfile b/justfile index eb73ac2..dc70acf 100644 --- a/justfile +++ b/justfile @@ -8,6 +8,10 @@ lint: @cargo clippy --version cargo clippy -- -D warnings -W clippy::pedantic -W clippy::nursery cargo doc +lint_fix: + @cargo clippy --version + cargo clippy --fix -- -D warnings -W clippy::pedantic -W clippy::nursery + test: cargo nextest run --all-targets --no-fail-fast diff --git a/src/eval.rs b/src/eval.rs index f1f8c0f..cd23a72 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -32,7 +32,7 @@ impl EvaluatedValue { Self::String(_) | Self::Number(_) => true, Self::Nil => false, Self::Bool(b) => *b, - Self::NativeFunction(f) => true, + Self::NativeFunction(_f) => true, } } } @@ -43,7 +43,7 @@ impl From for bool { EvaluatedValue::String(_) | EvaluatedValue::Number(_) => true, EvaluatedValue::Nil => false, EvaluatedValue::Bool(b) => b, - EvaluatedValue::NativeFunction(f) => true, + EvaluatedValue::NativeFunction(_f) => true, } } } @@ -55,7 +55,7 @@ impl Display for EvaluatedValue { Self::Number(n) => write!(f, "{n}"), Self::Nil => write!(f, "nil"), Self::Bool(b) => write!(f, "{b:}"), - Self::NativeFunction(native_fn) => write!(f, "nat fn # TODO"), + Self::NativeFunction(_native_fn) => write!(f, "nat fn # TODO"), } } } @@ -183,7 +183,7 @@ fn evaluate_expression(expr: Expr) -> Result { true => Ok(EvaluatedValue::Bool(false)), false => Ok(EvaluatedValue::Bool(true)), }, - EvaluatedValue::NativeFunction(f) => todo!(), + EvaluatedValue::NativeFunction(_f) => todo!(), }, ), TokenType::Minus => r.as_ref().map_or_else( diff --git a/src/main.rs b/src/main.rs index d959d03..f8e281e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use clap::Subcommand; use loxide::eval::Eval; use loxide::eval_parser::Parser; use loxide::lexer::Lexer; -use loxide::run::Run; +use loxide::run::Interpreter; use miette::{IntoDiagnostic, Result, WrapErr}; use std::fs; use std::path::PathBuf; @@ -30,7 +30,7 @@ fn main() -> Result { Commands::Tokenize { filename } => { let input = fs::read_to_string(filename) .into_diagnostic() - .wrap_err_with(|| format!("reading file"))?; + .wrap_err_with(|| "reading file".to_string())?; Ok(Lexer::new(&input).tokenize_lex()) } Commands::Parse { filename } => { @@ -57,7 +57,7 @@ fn main() -> Result { .into_diagnostic() .wrap_err_with(|| "reading file".to_string())?; let mut exit_code = 0; - for res in Run::new(&input) { + for res in Interpreter::new(&input) { match res { Ok(()) => {} Err(err_code) => { diff --git a/src/parser.rs b/src/parser.rs index 915872e..7d5a33a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -205,7 +205,7 @@ impl<'de> Parser<'de> { }; if let Some(init) = initializer { body = Stmt::Block(vec![init, body]); - }; + } Ok(body) } diff --git a/src/run.rs b/src/run.rs index f79111d..0006746 100644 --- a/src/run.rs +++ b/src/run.rs @@ -18,19 +18,23 @@ use crate::{ }; trait Callable { - fn arity(&self, interpreter: &Run) -> u8; + fn arity(&self, interpreter: &Interpreter) -> u8; fn call( &self, - interpreter: &mut Run, + interpreter: &mut Interpreter, args: &[EvaluatedValue], ) -> Result; } +/// `NativeFunction` is used to represent builtin native functions #[derive(Clone)] pub struct NativeFunction { + /// `name` of the native function pub name: String, + /// Numbers of arguments that should be passed to `callable` pub arity: u8, - pub callable: fn(&mut Run, &[EvaluatedValue]) -> Result, + /// A function to be run + pub callable: fn(&mut Interpreter, &[EvaluatedValue]) -> Result, } impl fmt::Debug for NativeFunction { @@ -40,21 +44,22 @@ impl fmt::Debug for NativeFunction { } impl Callable for NativeFunction { - fn arity(&self, _interpreter: &Run) -> u8 { + fn arity(&self, _interpreter: &Interpreter) -> u8 { self.arity } fn call( &self, - interpreter: &mut Run, + interpreter: &mut Interpreter, args: &[EvaluatedValue], ) -> Result { (self.callable)(interpreter, args) } } -/// `Run` -/// an iterator that consumes expressions from the parser and tries to evaluate them. -pub struct Run<'de> { +/// `Interpreter` +/// responsible for iterating over the rusults of parser +/// and evaluating the statements and expressions encountered +pub struct Interpreter<'de> { parser: Parser<'de>, environment: Environment<'de>, globals: Environment<'de>, @@ -117,8 +122,10 @@ impl<'de> Environment<'de> { } } -impl<'de> Run<'de> { +impl<'de> Interpreter<'de> { /// Create a new `Interpreter` to process a given input source code + /// # Panics + /// Will panic if `SystemTime::now()` returns value before `UNIX_EPOCH` #[must_use] pub fn new(input: &'de str) -> Self { let mut global_data = HashMap::new(); @@ -129,8 +136,10 @@ impl<'de> Run<'de> { arity: 0, callable: |_, _| { let start = SystemTime::now(); - let since_the_epoch = start.duration_since(UNIX_EPOCH).unwrap(); - + let since_the_epoch = start.duration_since(UNIX_EPOCH).expect( + "We should always be able to calculate this if system clock is set", + ); + #[allow(clippy::cast_precision_loss)] Ok(EvaluatedValue::Number(since_the_epoch.as_millis() as f64)) }, }), @@ -147,25 +156,25 @@ impl<'de> Run<'de> { } } -impl Iterator for Run<'_> { +impl Iterator for Interpreter<'_> { type Item = Result<(), u8>; fn next(&mut self) -> Option { let stmt = self.parser.next()?; - match stmt { - Ok(s) => { - let eval_stmt = evaluate_statement(&s, self); - match eval_stmt { - Ok(()) => Some(Ok(())), - Err(_) => Some(Err(70)), - } + stmt.map_or(Some(Err(65)), |s| { + let eval_stmt = evaluate_statement(&s, self); + match eval_stmt { + Ok(()) => Some(Ok(())), + Err(_) => Some(Err(70)), } - Err(_) => Some(Err(65)), - } + }) } } -fn evaluate_statement<'de>(stmt: &Stmt<'de>, interpreter: &mut Run<'de>) -> Result<(), String> { +fn evaluate_statement<'de>( + stmt: &Stmt<'de>, + interpreter: &mut Interpreter<'de>, +) -> Result<(), String> { match stmt { Stmt::Print(expr) => { let val = evaluate_expression(expr, interpreter)?; @@ -220,7 +229,7 @@ fn evaluate_statement<'de>(stmt: &Stmt<'de>, interpreter: &mut Run<'de>) -> Resu fn evaluate_expression<'de>( expr: &Expr<'de>, - interpreter: &mut Run<'de>, + interpreter: &mut Interpreter<'de>, // environment: &mut Environment<'de>, // globals: &mut Environment<'de>, ) -> Result { @@ -329,7 +338,7 @@ fn evaluate_expression<'de>( true => Ok(EvaluatedValue::Bool(false)), false => Ok(EvaluatedValue::Bool(true)), }, - EvaluatedValue::NativeFunction(f) => todo!(), + EvaluatedValue::NativeFunction(_f) => todo!(), }, ), TokenType::Minus => r.as_ref().map_or_else( @@ -339,7 +348,7 @@ fn evaluate_expression<'de>( EvaluatedValue::Number(n) => Ok(EvaluatedValue::Number(-n)), EvaluatedValue::Nil => todo!(), EvaluatedValue::Bool(_) => todo!(), - EvaluatedValue::NativeFunction(f) => todo!(), + EvaluatedValue::NativeFunction(_f) => todo!(), }, ), // TODO: Make unrepresentable by narrowing `operator` to `UnaryOperator:Not|Negate` @@ -394,7 +403,7 @@ fn evaluate_expression<'de>( } Expr::Call { callee, - paren, + paren: _, arguments, } => { let callee_fn = evaluate_expression(callee, interpreter)?; @@ -406,8 +415,16 @@ fn evaluate_expression<'de>( match callee_fn { EvaluatedValue::NativeFunction(native_function) => { - let fun = native_function.call(interpreter, &args); - return fun; + if native_function.arity(interpreter) as usize != args.len() { + eprintln!( + "Incorrect number of arguments passed to {}. Expected {} Found {}", + native_function.name, + native_function.arity, + args.len() + ); + return Err("Incorrect arity".to_string()); + } + native_function.call(interpreter, &args) } _ => todo!(), } diff --git a/tests/eval_parser_test.rs b/tests/eval_parser_test.rs index e9dd5e3..02b9c6f 100644 --- a/tests/eval_parser_test.rs +++ b/tests/eval_parser_test.rs @@ -22,7 +22,7 @@ fn test_parser_literals(#[case] input: &str) { .take_while(std::result::Result::is_ok) .map(|e| match e { Ok(exp) => format!("{exp}"), - Err(_) => format!(""), + Err(_) => String::new(), }) .collect(); assert_snapshot!(exprs.join("\n")); From a0d7c31bc9432d3248a1218ff2a50711c344f0c8 Mon Sep 17 00:00:00 2001 From: Rob Hand <146272+sinon@users.noreply.github.com> Date: Sat, 19 Jul 2025 17:07:57 +0100 Subject: [PATCH 03/11] remove commented out code --- src/run.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/run.rs b/src/run.rs index 0006746..3ec75bc 100644 --- a/src/run.rs +++ b/src/run.rs @@ -230,8 +230,6 @@ fn evaluate_statement<'de>( fn evaluate_expression<'de>( expr: &Expr<'de>, interpreter: &mut Interpreter<'de>, - // environment: &mut Environment<'de>, - // globals: &mut Environment<'de>, ) -> Result { match expr { Expr::Binary { From dc86cbd50b4f24087308b4434b826ff98072c442 Mon Sep 17 00:00:00 2001 From: Rob Hand <146272+sinon@users.noreply.github.com> Date: Sat, 19 Jul 2025 18:03:29 +0100 Subject: [PATCH 04/11] builtin mod, add clock() test --- src/builtins.rs | 16 ++++++++++++++++ src/lib.rs | 1 + src/run.rs | 18 +++--------------- tests/fixtures/run.lox | 3 ++- 4 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 src/builtins.rs diff --git a/src/builtins.rs b/src/builtins.rs new file mode 100644 index 0000000..6893e93 --- /dev/null +++ b/src/builtins.rs @@ -0,0 +1,16 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::eval::EvaluatedValue; +use crate::run; + +pub fn clock( + _interpreter: &mut run::Interpreter, + _args: &[EvaluatedValue], +) -> Result { + let start = SystemTime::now(); + #[allow(clippy::cast_precision_loss)] + start.duration_since(UNIX_EPOCH).map_or_else( + |_| Err("Unable to calculate time since UNIX_EPOC".to_string()), + |since_the_epoch| Ok(EvaluatedValue::Number(since_the_epoch.as_millis() as f64)), + ) +} diff --git a/src/lib.rs b/src/lib.rs index d7bbd84..529ec82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ #![allow(clippy::float_cmp)] #![allow(clippy::too_many_lines)] #![warn(missing_docs)] +mod builtins; pub mod eval; pub mod eval_parser; pub mod lexer; diff --git a/src/run.rs b/src/run.rs index 3ec75bc..760b865 100644 --- a/src/run.rs +++ b/src/run.rs @@ -3,15 +3,10 @@ //! Responsible for running the AST and returning the computed values //! -use std::{ - cell::RefCell, - collections::HashMap, - fmt, - rc::Rc, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::{cell::RefCell, collections::HashMap, fmt, rc::Rc}; use crate::{ + builtins, eval::EvaluatedValue, lexer::TokenType, parser::{Expr, LiteralAtom, Parser, Stmt}, @@ -134,14 +129,7 @@ impl<'de> Interpreter<'de> { EvaluatedValue::NativeFunction(NativeFunction { name: "clock".to_string(), arity: 0, - callable: |_, _| { - let start = SystemTime::now(); - let since_the_epoch = start.duration_since(UNIX_EPOCH).expect( - "We should always be able to calculate this if system clock is set", - ); - #[allow(clippy::cast_precision_loss)] - Ok(EvaluatedValue::Number(since_the_epoch.as_millis() as f64)) - }, + callable: builtins::clock, }), ); let globals = Environment { diff --git a/tests/fixtures/run.lox b/tests/fixtures/run.lox index 189d6f3..d232700 100644 --- a/tests/fixtures/run.lox +++ b/tests/fixtures/run.lox @@ -61,4 +61,5 @@ var quz = "global quz"; } print world; // "global world" print baz; // "global baz" -print quz; // "global_quz" \ No newline at end of file +print quz; // "global_quz" +clock(); \ No newline at end of file From 0a9688c245d479294f175bd91336eef52cf32ec4 Mon Sep 17 00:00:00 2001 From: Rob Hand <146272+sinon@users.noreply.github.com> Date: Sat, 19 Jul 2025 18:15:02 +0100 Subject: [PATCH 05/11] add pre-commit --- .github/workflows/check.yml | 1 - .gitignore | 2 +- justfile | 2 +- src/lexer.rs | 2 +- test.lox | 2 +- tests/fixtures/evaluate.lox | 2 +- tests/fixtures/evaluate_error.lox | 2 +- tests/fixtures/lexer.lox | 2 +- tests/fixtures/lexer_error.lox | 2 +- tests/fixtures/parser.lox | 2 +- tests/fixtures/parser_error.lox | 2 +- tests/fixtures/run.lox | 2 +- tests/fixtures/run_error.lox | 2 +- tests/fixtures/run_error_must_be_a_number.lox | 2 +- tests/fixtures/run_error_undefined_var.lox | 2 +- tests/fixtures/run_error_var.lox | 2 +- tests/fixtures/run_error_var_assign.lox | 2 +- tests/fixtures/run_for.lox | 2 +- tests/fixtures/run_if.lox | 4 ++-- tests/fixtures/run_logical.lox | 2 +- 20 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0c544ba..bca4103 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -57,4 +57,3 @@ jobs: with: reporter: 'github-pr-check' github_token: ${{ secrets.GITHUB_TOKEN }} - \ No newline at end of file diff --git a/.gitignore b/.gitignore index f81d7f6..d6686bf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ target/ *.pdb -reports/ \ No newline at end of file +reports/ diff --git a/justfile b/justfile index dc70acf..d891611 100644 --- a/justfile +++ b/justfile @@ -18,4 +18,4 @@ test: t:test lox_run:build - cargo run run test.lox \ No newline at end of file + cargo run run test.lox diff --git a/src/lexer.rs b/src/lexer.rs index 8df6ca2..66089e6 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -236,7 +236,7 @@ impl<'de> Iterator for Lexer<'de> { }, _ => { return Some(Err( - miette! {labels = vec![LabeledSpan::at(self.byte-c.len_utf8()..self.byte, "this character")], + miette! {labels = vec![LabeledSpan::at(self.byte-c.len_utf8()..self.byte, "this character")], "[line {}] Error: Unexpected character: {c}", self.line_num} .with_source_code(self.whole.to_string()), )) diff --git a/test.lox b/test.lox index 2385ff4..33f719f 100644 --- a/test.lox +++ b/test.lox @@ -6,4 +6,4 @@ print clock(); // print "Hi, " + first + " " + last + "!"; // } -// sayHi("Dear", "Reader"); \ No newline at end of file +// sayHi("Dear", "Reader"); diff --git a/tests/fixtures/evaluate.lox b/tests/fixtures/evaluate.lox index 44ea417..406583b 100644 --- a/tests/fixtures/evaluate.lox +++ b/tests/fixtures/evaluate.lox @@ -1,3 +1,3 @@ false true -nil \ No newline at end of file +nil diff --git a/tests/fixtures/evaluate_error.lox b/tests/fixtures/evaluate_error.lox index a1fa585..3216e5a 100644 --- a/tests/fixtures/evaluate_error.lox +++ b/tests/fixtures/evaluate_error.lox @@ -1,2 +1,2 @@ -"hello" --true \ No newline at end of file +-true diff --git a/tests/fixtures/lexer.lox b/tests/fixtures/lexer.lox index 8ecf4bf..2cea7df 100644 --- a/tests/fixtures/lexer.lox +++ b/tests/fixtures/lexer.lox @@ -1 +1 @@ -"hello" 123 false nil \ No newline at end of file +"hello" 123 false nil diff --git a/tests/fixtures/lexer_error.lox b/tests/fixtures/lexer_error.lox index f1ee89c..bf82c85 100644 --- a/tests/fixtures/lexer_error.lox +++ b/tests/fixtures/lexer_error.lox @@ -1 +1 @@ -#"// \ No newline at end of file +#"// diff --git a/tests/fixtures/parser.lox b/tests/fixtures/parser.lox index 75dc39d..93c6c7d 100644 --- a/tests/fixtures/parser.lox +++ b/tests/fixtures/parser.lox @@ -1,2 +1,2 @@ 52 + 80 - 94 -83 < 99 > 115 \ No newline at end of file +83 < 99 > 115 diff --git a/tests/fixtures/parser_error.lox b/tests/fixtures/parser_error.lox index f41d69e..02d56b5 100644 --- a/tests/fixtures/parser_error.lox +++ b/tests/fixtures/parser_error.lox @@ -1,2 +1,2 @@ (72 +) -"foo \ No newline at end of file +"foo diff --git a/tests/fixtures/run.lox b/tests/fixtures/run.lox index d232700..5e39b9b 100644 --- a/tests/fixtures/run.lox +++ b/tests/fixtures/run.lox @@ -62,4 +62,4 @@ var quz = "global quz"; print world; // "global world" print baz; // "global baz" print quz; // "global_quz" -clock(); \ No newline at end of file +clock(); diff --git a/tests/fixtures/run_error.lox b/tests/fixtures/run_error.lox index 1d23839..beb534a 100644 --- a/tests/fixtures/run_error.lox +++ b/tests/fixtures/run_error.lox @@ -1,4 +1,4 @@ 49 + "baz"; print "this should not be printed"; print "79" + "baz"; -print false * (18 + 84); \ No newline at end of file +print false * (18 + 84); diff --git a/tests/fixtures/run_error_must_be_a_number.lox b/tests/fixtures/run_error_must_be_a_number.lox index 2c3f14a..77b413e 100644 --- a/tests/fixtures/run_error_must_be_a_number.lox +++ b/tests/fixtures/run_error_must_be_a_number.lox @@ -1,3 +1,3 @@ if ("x" > 1) { print "panic!"; -} \ No newline at end of file +} diff --git a/tests/fixtures/run_error_undefined_var.lox b/tests/fixtures/run_error_undefined_var.lox index 92c08d2..dfc4801 100644 --- a/tests/fixtures/run_error_undefined_var.lox +++ b/tests/fixtures/run_error_undefined_var.lox @@ -14,4 +14,4 @@ print foo; // "modified foo" print quz; // "outer quz" } -print quz; \ No newline at end of file +print quz; diff --git a/tests/fixtures/run_error_var.lox b/tests/fixtures/run_error_var.lox index d145095..96bb531 100644 --- a/tests/fixtures/run_error_var.lox +++ b/tests/fixtures/run_error_var.lox @@ -1,2 +1,2 @@ x = 10; -print x; \ No newline at end of file +print x; diff --git a/tests/fixtures/run_error_var_assign.lox b/tests/fixtures/run_error_var_assign.lox index 806d369..97cdd5e 100644 --- a/tests/fixtures/run_error_var_assign.lox +++ b/tests/fixtures/run_error_var_assign.lox @@ -1 +1 @@ -var x = ; \ No newline at end of file +var x = ; diff --git a/tests/fixtures/run_for.lox b/tests/fixtures/run_for.lox index 7487b92..3db3a10 100644 --- a/tests/fixtures/run_for.lox +++ b/tests/fixtures/run_for.lox @@ -32,4 +32,4 @@ var quz = "after"; for (quz = 0; quz < 1; quz = quz + 1) { print quz; } -} \ No newline at end of file +} diff --git a/tests/fixtures/run_if.lox b/tests/fixtures/run_if.lox index 0057f25..5b77064 100644 --- a/tests/fixtures/run_if.lox +++ b/tests/fixtures/run_if.lox @@ -12,7 +12,7 @@ if (a = true) { var stage = "unknown"; var age = 50; if (age < 18) { stage = "child"; } -if (age >= 18) { stage = "adult"; } +if (age >= 18) { stage = "adult"; } print stage; // "adult" var isAdult = age >= 18; @@ -23,4 +23,4 @@ if (1 > 2 and 1 != 2 or 1 < 3) { } else { print "else"; } -if (1>2) {print"if";} else {print"else";} \ No newline at end of file +if (1>2) {print"if";} else {print"else";} diff --git a/tests/fixtures/run_logical.lox b/tests/fixtures/run_logical.lox index a37de9f..e8437b8 100644 --- a/tests/fixtures/run_logical.lox +++ b/tests/fixtures/run_logical.lox @@ -19,4 +19,4 @@ var a = "hello"; var b = "hello"; (a = false) or (b = true) or (a = "hello"); print a; -print b; \ No newline at end of file +print b; From 532ebccfbc3401a32941a4ec41c51cb3cd703869 Mon Sep 17 00:00:00 2001 From: Rob Hand <146272+sinon@users.noreply.github.com> Date: Sat, 19 Jul 2025 18:26:21 +0100 Subject: [PATCH 06/11] add pre-commit --- .pre-commit-config.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2ba4d01 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: local + hooks: + - id: cargo-fmt + name: cargo fmt + entry: cargo fmt -- --check + language: system + files: \.rs$ + args: [] + - id: lint + name: lint + entry: just lint + language: system + pass_filenames: false + files: \.rs$ + args: [] + - id: test + name: test + entry: just test + language: system + pass_filenames: false + args: [] From 4542fcacd466ede3b68030db560a2f497ed9fb3b Mon Sep 17 00:00:00 2001 From: Rob Hand <146272+sinon@users.noreply.github.com> Date: Sat, 19 Jul 2025 18:35:44 +0100 Subject: [PATCH 07/11] illegal call test --- .pre-commit-config.yaml | 1 + src/run.rs | 28 +++++++++++++--------------- test.lox | 9 +++++---- tests/fixtures/run_illegal_call.lox | 4 ++++ tests/main_test.rs | 1 + 5 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 tests/fixtures/run_illegal_call.lox diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ba4d01..e1978c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,4 +25,5 @@ repos: entry: just test language: system pass_filenames: false + files: \.rs$ args: [] diff --git a/src/run.rs b/src/run.rs index 760b865..7d56437 100644 --- a/src/run.rs +++ b/src/run.rs @@ -397,22 +397,20 @@ fn evaluate_expression<'de>( for arg in arguments { args.push(evaluate_expression(arg, interpreter)?); } - // TODO: Need to check function arity on `EvaluatedValue` for Fn - - match callee_fn { - EvaluatedValue::NativeFunction(native_function) => { - if native_function.arity(interpreter) as usize != args.len() { - eprintln!( - "Incorrect number of arguments passed to {}. Expected {} Found {}", - native_function.name, - native_function.arity, - args.len() - ); - return Err("Incorrect arity".to_string()); - } - native_function.call(interpreter, &args) + if let EvaluatedValue::NativeFunction(native_function) = callee_fn { + if native_function.arity(interpreter) as usize != args.len() { + eprintln!( + "Incorrect number of arguments passed to {}. Expected {} Found {}", + native_function.name, + native_function.arity, + args.len() + ); + return Err("Incorrect arity".to_string()); } - _ => todo!(), + native_function.call(interpreter, &args) + } else { + eprintln!("Can only call function and classes."); + Err("Can only call functions and classes.".to_string()) } } } diff --git a/test.lox b/test.lox index 33f719f..59d1ccd 100644 --- a/test.lox +++ b/test.lox @@ -1,9 +1,10 @@ if (true) print "bar"; // "bar" print clock(); +print true(); -// fun sayHi(first, last) { -// print "Hi, " + first + " " + last + "!"; -// } +fun sayHi(first, last) { + print "Hi, " + first + " " + last + "!"; +} -// sayHi("Dear", "Reader"); +sayHi("Dear", "Reader"); diff --git a/tests/fixtures/run_illegal_call.lox b/tests/fixtures/run_illegal_call.lox new file mode 100644 index 0000000..f5e13cc --- /dev/null +++ b/tests/fixtures/run_illegal_call.lox @@ -0,0 +1,4 @@ +true(); +false(); +1(); +"string"(); diff --git a/tests/main_test.rs b/tests/main_test.rs index 30b12a0..eb568c2 100644 --- a/tests/main_test.rs +++ b/tests/main_test.rs @@ -193,6 +193,7 @@ fn test_run_with_file(#[case] file_name: &str, #[case] expected_stdout: &str) { "", 70 )] +#[case("run_illegal_call", "Can only call function and classes.\n", "", 70)] fn test_run_with_file_error( #[case] file_name: &str, #[case] expected_err: &str, From e228027103af1219268d29c8eb08c9c77b5a0911 Mon Sep 17 00:00:00 2001 From: Rob Hand <146272+sinon@users.noreply.github.com> Date: Sat, 19 Jul 2025 18:40:28 +0100 Subject: [PATCH 08/11] add test for incorrect arity, align err msg with ci book --- src/run.rs | 3 +-- test.lox | 10 +++++----- ..._illegal_call.lox => run_error_illegal_call.lox} | 0 tests/fixtures/run_error_incorrect_arity.lox | 1 + tests/main_test.rs | 13 ++++++++++++- 5 files changed, 19 insertions(+), 8 deletions(-) rename tests/fixtures/{run_illegal_call.lox => run_error_illegal_call.lox} (100%) create mode 100644 tests/fixtures/run_error_incorrect_arity.lox diff --git a/src/run.rs b/src/run.rs index 7d56437..c7034a9 100644 --- a/src/run.rs +++ b/src/run.rs @@ -400,8 +400,7 @@ fn evaluate_expression<'de>( if let EvaluatedValue::NativeFunction(native_function) = callee_fn { if native_function.arity(interpreter) as usize != args.len() { eprintln!( - "Incorrect number of arguments passed to {}. Expected {} Found {}", - native_function.name, + "Expected {} arguments but got {}.", native_function.arity, args.len() ); diff --git a/test.lox b/test.lox index 59d1ccd..9fefa2a 100644 --- a/test.lox +++ b/test.lox @@ -1,10 +1,10 @@ if (true) print "bar"; // "bar" print clock(); -print true(); +clock(1, 2, 3); -fun sayHi(first, last) { - print "Hi, " + first + " " + last + "!"; -} +// fun sayHi(first, last) { +// print "Hi, " + first + " " + last + "!"; +// } -sayHi("Dear", "Reader"); +// sayHi("Dear", "Reader"); diff --git a/tests/fixtures/run_illegal_call.lox b/tests/fixtures/run_error_illegal_call.lox similarity index 100% rename from tests/fixtures/run_illegal_call.lox rename to tests/fixtures/run_error_illegal_call.lox diff --git a/tests/fixtures/run_error_incorrect_arity.lox b/tests/fixtures/run_error_incorrect_arity.lox new file mode 100644 index 0000000..cd27a8c --- /dev/null +++ b/tests/fixtures/run_error_incorrect_arity.lox @@ -0,0 +1 @@ +clock(1, 2, 3); diff --git a/tests/main_test.rs b/tests/main_test.rs index eb568c2..69ade05 100644 --- a/tests/main_test.rs +++ b/tests/main_test.rs @@ -193,7 +193,18 @@ fn test_run_with_file(#[case] file_name: &str, #[case] expected_stdout: &str) { "", 70 )] -#[case("run_illegal_call", "Can only call function and classes.\n", "", 70)] +#[case( + "run_error_illegal_call", + "Can only call function and classes.\n", + "", + 70 +)] +#[case( + "run_error_incorrect_arity", + "Expected 0 arguments but got 3.\n", + "", + 70 +)] fn test_run_with_file_error( #[case] file_name: &str, #[case] expected_err: &str, From 316fa6b9a7394ed184f3d0ae4aa95336c6fc2b71 Mon Sep 17 00:00:00 2001 From: Rob Hand <146272+sinon@users.noreply.github.com> Date: Sat, 19 Jul 2025 18:42:02 +0100 Subject: [PATCH 09/11] run tests on pre-push instead of pre-commit --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1978c5..9a1e08b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,3 +27,4 @@ repos: pass_filenames: false files: \.rs$ args: [] + stages: [pre-push] From 2eb88f8b043a92c2bf23cfd1cb9dfa0d5384a81d Mon Sep 17 00:00:00 2001 From: Rob Hand <146272+sinon@users.noreply.github.com> Date: Sat, 19 Jul 2025 18:52:40 +0100 Subject: [PATCH 10/11] rename --- src/builtins.rs | 4 ++-- src/eval.rs | 2 +- src/{run.rs => interpreter.rs} | 0 src/lib.rs | 2 +- src/main.rs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/{run.rs => interpreter.rs} (100%) diff --git a/src/builtins.rs b/src/builtins.rs index 6893e93..eb74919 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -1,10 +1,10 @@ use std::time::{SystemTime, UNIX_EPOCH}; use crate::eval::EvaluatedValue; -use crate::run; +use crate::interpreter; pub fn clock( - _interpreter: &mut run::Interpreter, + _interpreter: &mut interpreter::Interpreter, _args: &[EvaluatedValue], ) -> Result { let start = SystemTime::now(); diff --git a/src/eval.rs b/src/eval.rs index cd23a72..c9470dd 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -7,8 +7,8 @@ use std::fmt::Display; use crate::{ eval_parser::{Expr, LiteralAtom, Parser}, + interpreter::NativeFunction, lexer::TokenType, - run::NativeFunction, }; /// The value that an expression has evaluated too, this can be a literal. diff --git a/src/run.rs b/src/interpreter.rs similarity index 100% rename from src/run.rs rename to src/interpreter.rs diff --git a/src/lib.rs b/src/lib.rs index 529ec82..9990d35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,6 @@ mod builtins; pub mod eval; pub mod eval_parser; +pub mod interpreter; pub mod lexer; pub mod parser; -pub mod run; diff --git a/src/main.rs b/src/main.rs index f8e281e..03e9637 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ use clap::Subcommand; use loxide::eval::Eval; use loxide::eval_parser::Parser; +use loxide::interpreter::Interpreter; use loxide::lexer::Lexer; -use loxide::run::Interpreter; use miette::{IntoDiagnostic, Result, WrapErr}; use std::fs; use std::path::PathBuf; From 72f01838f1544f3b041c7a1cc79edc11dcbaf6cd Mon Sep 17 00:00:00 2001 From: Rob Hand <146272+sinon@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:10:19 +0100 Subject: [PATCH 11/11] wip: implementing func calling --- NOTES.md | 18 +++++++++++++ src/builtins.rs | 2 +- src/eval.rs | 64 ++++------------------------------------------ src/interpreter.rs | 60 +++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 3 ++- src/parser.rs | 61 ++++++++++++++++++++++++++++++++++++------- src/value.rs | 60 +++++++++++++++++++++++++++++++++++++++++++ test.lox | 14 +++------- 8 files changed, 200 insertions(+), 82 deletions(-) create mode 100644 NOTES.md create mode 100644 src/value.rs diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..449d964 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,18 @@ +https://craftinginterpreters.com/functions.html#function-objects + +Implementing custom functions. + +i.e +```lox +fun sayHi(first, last) { + print "Hi, " + first + " " + last + "!"; +} +sayHi("Dear", "Reader"); +``` + +The parsing for the func definition and the func call are implemented. + +Work to do: +- Store the `IDENTIFIER:sayHi -> code to run` in interpreter +- Lookup this code +- Run this code diff --git a/src/builtins.rs b/src/builtins.rs index eb74919..147a4ec 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -1,7 +1,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; -use crate::eval::EvaluatedValue; use crate::interpreter; +use crate::value::EvaluatedValue; pub fn clock( _interpreter: &mut interpreter::Interpreter, diff --git a/src/eval.rs b/src/eval.rs index c9470dd..e845da5 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,65 +1,14 @@ //! Eval module //! -//! Responsible for evlauting the AST and returning the computed values -//! - -use std::fmt::Display; +//! Responsible for evalulating the AST and returning the computed values +//! Only supports simple expressions use crate::{ eval_parser::{Expr, LiteralAtom, Parser}, - interpreter::NativeFunction, lexer::TokenType, + value::EvaluatedValue, }; -/// The value that an expression has evaluated too, this can be a literal. -#[derive(Clone, Debug)] -pub enum EvaluatedValue { - /// String value `"hello"` - String(String), - /// Number value. Note Lox only supports double precision floating point - Number(f64), - /// nil, the unset/null value - Nil, - /// Boolean value `true`/`false` - Bool(bool), - /// fn - NativeFunction(NativeFunction), -} - -impl EvaluatedValue { - pub(crate) const fn is_truthy(&self) -> bool { - match self { - Self::String(_) | Self::Number(_) => true, - Self::Nil => false, - Self::Bool(b) => *b, - Self::NativeFunction(_f) => true, - } - } -} - -impl From for bool { - fn from(val: EvaluatedValue) -> Self { - match val { - EvaluatedValue::String(_) | EvaluatedValue::Number(_) => true, - EvaluatedValue::Nil => false, - EvaluatedValue::Bool(b) => b, - EvaluatedValue::NativeFunction(_f) => true, - } - } -} - -impl Display for EvaluatedValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::String(s) => write!(f, "{s}"), - Self::Number(n) => write!(f, "{n}"), - Self::Nil => write!(f, "nil"), - Self::Bool(b) => write!(f, "{b:}"), - Self::NativeFunction(_native_fn) => write!(f, "nat fn # TODO"), - } - } -} - /// `Eval` /// an iterator that consumes expressions from the parser and tries to evaluate them. pub struct Eval<'de> { @@ -183,17 +132,14 @@ fn evaluate_expression(expr: Expr) -> Result { true => Ok(EvaluatedValue::Bool(false)), false => Ok(EvaluatedValue::Bool(true)), }, - EvaluatedValue::NativeFunction(_f) => todo!(), + _ => todo!(), }, ), TokenType::Minus => r.as_ref().map_or_else( |_| todo!(), |v| match v { - EvaluatedValue::String(_) => todo!(), EvaluatedValue::Number(n) => Ok(EvaluatedValue::Number(-n)), - EvaluatedValue::Nil => todo!(), - EvaluatedValue::Bool(_) => todo!(), - EvaluatedValue::NativeFunction(_) => todo!(), + _ => todo!(), }, ), // TODO: Make unrepresentable by narrowing `operator` to `UnaryOperator:Not|Negate` diff --git a/src/interpreter.rs b/src/interpreter.rs index c7034a9..c25d853 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -7,9 +7,9 @@ use std::{cell::RefCell, collections::HashMap, fmt, rc::Rc}; use crate::{ builtins, - eval::EvaluatedValue, - lexer::TokenType, + lexer::{Token, TokenType}, parser::{Expr, LiteralAtom, Parser, Stmt}, + value::EvaluatedValue, }; trait Callable { @@ -51,6 +51,27 @@ impl Callable for NativeFunction { } } +#[derive(Debug)] +pub struct LoxFunction<'de> { + pub name: Token<'de>, + pub parameters: Vec>, + pub body: Vec>, +} + +impl Callable for LoxFunction<'_> { + fn arity(&self, _interpreter: &Interpreter) -> u8 { + self.parameters.len() as u8 + } + + fn call( + &self, + interpreter: &mut Interpreter, + args: &[EvaluatedValue], + ) -> Result { + todo!() + } +} + /// `Interpreter` /// responsible for iterating over the rusults of parser /// and evaluating the statements and expressions encountered @@ -58,6 +79,8 @@ pub struct Interpreter<'de> { parser: Parser<'de>, environment: Environment<'de>, globals: Environment<'de>, + lox_functions: HashMap>, + counter: u64, } #[derive(Debug, Clone)] @@ -140,6 +163,8 @@ impl<'de> Interpreter<'de> { parser: Parser::new(input), environment: Environment::new(), globals, + lox_functions: Default::default(), + counter: 0, } } } @@ -159,6 +184,13 @@ impl Iterator for Interpreter<'_> { } } +impl Interpreter<'_> { + fn alloc_id(&mut self) -> u64 { + self.counter += 1; + self.counter + } +} + fn evaluate_statement<'de>( stmt: &Stmt<'de>, interpreter: &mut Interpreter<'de>, @@ -211,6 +243,28 @@ fn evaluate_statement<'de>( } evaluate_statement(body, interpreter)?; }, + Stmt::Function { + name, + parameters, + body, + } => { + let func_id = interpreter.alloc_id(); + // interpreter.environment.assign( + // &'de key, + // &EvaluatedValue::LoxFunction { + // name: name.to_string(), + // binding: None, + // }, + // ); + let lox_fun = LoxFunction { + name: name.clone(), + parameters: parameters.clone(), + body: body.clone(), + }; + interpreter.lox_functions.insert(func_id, lox_fun); + interpreter.globals.insert(name, lox_fun); + () + } } Ok(()) } @@ -325,6 +379,7 @@ fn evaluate_expression<'de>( false => Ok(EvaluatedValue::Bool(true)), }, EvaluatedValue::NativeFunction(_f) => todo!(), + EvaluatedValue::LoxFunction { name, binding } => todo!(), }, ), TokenType::Minus => r.as_ref().map_or_else( @@ -335,6 +390,7 @@ fn evaluate_expression<'de>( EvaluatedValue::Nil => todo!(), EvaluatedValue::Bool(_) => todo!(), EvaluatedValue::NativeFunction(_f) => todo!(), + EvaluatedValue::LoxFunction { name, binding } => todo!(), }, ), // TODO: Make unrepresentable by narrowing `operator` to `UnaryOperator:Not|Negate` diff --git a/src/lib.rs b/src/lib.rs index 9990d35..d57be5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,4 +12,5 @@ pub mod eval; pub mod eval_parser; pub mod interpreter; pub mod lexer; -pub mod parser; +mod parser; +mod value; diff --git a/src/parser.rs b/src/parser.rs index 7d5a33a..184fb60 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4,7 +4,10 @@ //! //! Uses a recursive desecent parser. To transform the token stream into //! `Expr` -use crate::lexer::{Lexer, Token, TokenType}; +use crate::{ + interpreter, + lexer::{Lexer, Token, TokenType}, +}; /// `Parser` is responsible for iterating over the token stream from `Lexer` /// and converting the lexed `Token` into `Expr` which represent an Abstract Syntax Tree (AST) @@ -27,7 +30,7 @@ pub enum LiteralAtom<'de> { Bool(bool), } -#[derive(Debug)] +#[derive(Debug, Clone)] /// `Expr` represents a unit of an AST pub enum Expr<'de> { /// `Binary` is a binary expression such as `1 * 2` @@ -74,7 +77,7 @@ pub enum Expr<'de> { }, } -#[derive(Debug)] +#[derive(Debug, Clone)] /// `Stmt` represents the possible statements supported pub enum Stmt<'de> { /// A print statement @@ -95,6 +98,12 @@ pub enum Stmt<'de> { /// The statements that will be executed repreatedly if `condition` body: Box>, }, + /// Func statement + Function { + name: Token<'de>, + parameters: Vec>, + body: Vec>, + }, } impl<'de> Iterator for Parser<'de> { @@ -130,12 +139,49 @@ impl<'de> Parser<'de> { } fn declaration(&mut self) -> Result, String> { + if self.match_tokens(&[TokenType::Fun]) { + return self.function("function"); + } if self.match_tokens(&[TokenType::Var]) { return self.var_declaration(); } self.statement() } + fn function(&mut self, kind: &str) -> Result, String> { + let name = self + .consume(&TokenType::Identifier, &format!("Expect {} name.", kind))? + .clone(); + self.consume( + &TokenType::LeftParen, + &format!("Expect '(' after {} name.", kind), + )?; + let mut parameters = Vec::new(); + if !self.check(&TokenType::RightParen) { + loop { + if parameters.len() > 255 { + return Err("Can't have more than 255 parameters.".to_string()); + } + // TODO: Figure out right way to avoid this clone + let param = self + .consume(&TokenType::Identifier, "Expect parameter name")? + .clone(); + parameters.push(param); + if !self.match_tokens(&[TokenType::Comma]) { + break; + } + } + } + self.consume(&TokenType::RightParen, "Expect ')' after parameters")?; + self.consume(&TokenType::LeftBrace, "Expect '{' after parameters")?; + let body = self.block()?; + Ok(Stmt::Function { + name, + parameters, + body, + }) + } + fn var_declaration(&mut self) -> Result, String> { let name = self .consume(&TokenType::Identifier, "Expect variable name.")? @@ -238,20 +284,19 @@ impl<'de> Parser<'de> { while !self.check(&TokenType::RightBrace) && !self.is_at_end() { stmts.push(self.declaration()?); } - - self.consume(&TokenType::RightBrace, "Expect '}' after block.")?; + self.consume(&TokenType::RightBrace, "Expect '}' after block")?; Ok(stmts) } fn expression_statement(&mut self) -> Result, String> { let expr = self.expression()?; - self.consume(&TokenType::Semicolon, "Expect ';' after value.")?; + self.consume(&TokenType::Semicolon, "Expect ';' after value")?; Ok(Stmt::ExpressionStatement(expr)) } fn print_statement(&mut self) -> Result, String> { let expr = self.expression()?; - self.consume(&TokenType::Semicolon, "Expect ';' after value.")?; + self.consume(&TokenType::Semicolon, "Expect ';' after value")?; Ok(Stmt::Print(expr)) } @@ -396,7 +441,6 @@ impl<'de> Parser<'de> { fn call(&mut self) -> Result, String> { let mut expr = self.primary()?; - loop { if self.match_tokens(&[TokenType::LeftParen]) { expr = self.finish_call(expr)?; @@ -425,7 +469,6 @@ impl<'de> Parser<'de> { let paren = self .consume(&TokenType::RightParen, "Expect ')' after arguments.")? .clone(); - Ok(Expr::Call { callee: Box::new(callee), paren, diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..00774d1 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,60 @@ +use std::fmt::{Display, write}; + +use crate::interpreter::NativeFunction; + +/// The value that an expression has evaluated too, this can be a literal. +#[derive(Clone, Debug)] +pub enum EvaluatedValue { + /// String value `"hello"` + String(String), + /// Number value. Note Lox only supports double precision floating point + Number(f64), + /// nil, the unset/null value + Nil, + /// Boolean value `true`/`false` + Bool(bool), + /// builtin fn + NativeFunction(NativeFunction), + /// fn + LoxFunction { + name: String, + binding: Option>, + }, +} + +impl EvaluatedValue { + pub(crate) const fn is_truthy(&self) -> bool { + match self { + Self::String(_) | Self::Number(_) => true, + Self::Nil => false, + Self::Bool(b) => *b, + Self::NativeFunction(_f) => true, + Self::LoxFunction { name, binding } => true, + } + } +} + +impl From for bool { + fn from(val: EvaluatedValue) -> Self { + match val { + EvaluatedValue::String(_) | EvaluatedValue::Number(_) => true, + EvaluatedValue::Nil => false, + EvaluatedValue::Bool(b) => b, + EvaluatedValue::NativeFunction(_f) => true, + EvaluatedValue::LoxFunction { name, binding } => true, + } + } +} + +impl Display for EvaluatedValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::String(s) => write!(f, "{s}"), + Self::Number(n) => write!(f, "{n}"), + Self::Nil => write!(f, "nil"), + Self::Bool(b) => write!(f, "{b:}"), + Self::NativeFunction(native_fn) => write!(f, "{native_fn:?}"), + Self::LoxFunction { name, binding } => write!(f, "{name:?}"), + } + } +} diff --git a/test.lox b/test.lox index 9fefa2a..12fff54 100644 --- a/test.lox +++ b/test.lox @@ -1,10 +1,4 @@ -if (true) print "bar"; // "bar" - -print clock(); -clock(1, 2, 3); - -// fun sayHi(first, last) { -// print "Hi, " + first + " " + last + "!"; -// } - -// sayHi("Dear", "Reader"); +fun sayHi(first, last) { + print "Hi, " + first + " " + last + "!"; +} +sayHi("Dear", "Reader");