diff --git a/core/src/doc.rs b/core/src/doc.rs index f1ef005..cecc4e6 100644 --- a/core/src/doc.rs +++ b/core/src/doc.rs @@ -38,15 +38,42 @@ pub fn classify_line(index: usize, raw: &str) -> ClassifiedLine { fn is_cordial(line: &str) -> bool { if line.starts_with("let ") { let rest = &line[4..]; + if let Some(colon_pos) = rest.find(':') { + let before_colon = rest[..colon_pos].trim(); + if is_ident(before_colon) { + let after_colon = &rest[colon_pos + 1..]; + if after_colon.contains('=') { + return true; + } + } + } if let Some(eq_pos) = rest.find('=') { - let name = rest[..eq_pos].trim(); - if is_ident(name) { - return true; + let after_eq = rest.as_bytes().get(eq_pos + 1); + if after_eq != Some(&b'=') { + let name = rest[..eq_pos].trim(); + if is_ident(name) { + return true; + } } } return false; } + // while (...) { + if line.starts_with("while ") || line.starts_with("while(") { + return true; + } + + // fn name(...) { + if line.starts_with("fn ") { + return true; + } + + // lone closing brace + if line == "}" || line.starts_with("} ") { + return true; + } + if let Some(eq_pos) = line.find('=') { if eq_pos > 0 { let before = &line[..eq_pos]; @@ -88,16 +115,32 @@ fn is_ident(s: &str) -> bool { pub fn classify_document(text: &str) -> Vec { let mut result = Vec::new(); - let mut depth: usize = 0; + let mut comment_depth: usize = 0; + let mut brace_depth: i32 = 0; for (i, line) in text.lines().enumerate() { - let was_in_comment = depth > 0; - depth = scan_comment_depth(line, depth); + let was_in_comment = comment_depth > 0; + comment_depth = scan_comment_depth(line, comment_depth); if was_in_comment || line.trim().starts_with("/*") { result.push(ClassifiedLine { index: i, kind: LineKind::Comment, content: line.to_string() }); + } else if brace_depth > 0 { + let trimmed = line.trim(); + let opens = trimmed.matches('{').count() as i32; + let closes = trimmed.matches('}').count() as i32; + brace_depth += opens - closes; + if brace_depth < 0 { brace_depth = 0; } + result.push(ClassifiedLine { index: i, kind: LineKind::Cordial, content: line.to_string() }); } else { - result.push(classify_line(i, line)); + let cl = classify_line(i, line); + if cl.kind == LineKind::Cordial { + let trimmed = line.trim(); + let opens = trimmed.matches('{').count() as i32; + let closes = trimmed.matches('}').count() as i32; + brace_depth += opens - closes; + if brace_depth < 0 { brace_depth = 0; } + } + result.push(cl); } } @@ -221,4 +264,52 @@ mod tests { assert_eq!(lines[3].kind, LineKind::Comment); assert_eq!(lines[4].kind, LineKind::Cordial); } + + #[test] + fn while_line() { + let c = classify_line(0, "while (i < 10) {"); + assert_eq!(c.kind, LineKind::Cordial); + } + + #[test] + fn fn_line() { + let c = classify_line(0, "fn add(a, b) {"); + assert_eq!(c.kind, LineKind::Cordial); + } + + #[test] + fn closing_brace() { + let c = classify_line(0, "}"); + assert_eq!(c.kind, LineKind::Cordial); + } + + #[test] + fn while_block_body_classified() { + let doc = "while (x > 0) {\n x = x - 1\n}"; + let lines = classify_document(doc); + assert_eq!(lines[0].kind, LineKind::Cordial); + assert_eq!(lines[1].kind, LineKind::Cordial); + assert_eq!(lines[2].kind, LineKind::Cordial); + } + + #[test] + fn fn_block_body_classified() { + let doc = "fn add(a, b) {\n a + b\n}"; + let lines = classify_document(doc); + assert_eq!(lines[0].kind, LineKind::Cordial); + assert_eq!(lines[1].kind, LineKind::Cordial); + assert_eq!(lines[2].kind, LineKind::Cordial); + } + + #[test] + fn let_with_type_annotation() { + let c = classify_line(0, "let x: int = 5"); + assert_eq!(c.kind, LineKind::Cordial); + } + + #[test] + fn let_with_bool_type() { + let c = classify_line(0, "let flag: bool = 1"); + assert_eq!(c.kind, LineKind::Cordial); + } } diff --git a/core/src/eval.rs b/core/src/eval.rs index c283617..3a4d0a9 100644 --- a/core/src/eval.rs +++ b/core/src/eval.rs @@ -1,5 +1,6 @@ use serde::Serialize; use crate::doc::{classify_document, LineKind}; +use crate::interp; #[derive(Debug, Clone, Serialize)] pub struct EvalResult { @@ -21,56 +22,48 @@ pub struct DocumentResult { pub fn evaluate_document(text: &str) -> DocumentResult { let classified = classify_document(text); - let mut cordial_lines: Vec = Vec::new(); let mut results = Vec::new(); let mut errors = Vec::new(); + let mut lines: Vec<(usize, &str, bool)> = Vec::new(); for cl in &classified { match cl.kind { - LineKind::Cordial => { - cordial_lines.push(cl.content.clone()); - } - LineKind::Eval => { - let expr = cl.content.trim().strip_prefix("/=").unwrap_or("").trim(); - if expr.is_empty() { - errors.push(EvalError { - line: cl.index, - error: "empty expression".into(), - }); - continue; - } - - let mut eval_program = cordial_lines.clone(); - eval_program.push(expr.to_string()); - let program = eval_program.join("\n"); - - match cord_expr::parse_expr(&program) { - Ok(graph) => { - let val = cord_trig::eval::evaluate(&graph, 0.0, 0.0, 0.0); - results.push(EvalResult { - line: cl.index, - result: format_value(val), - }); - } - Err(e) => { - errors.push(EvalError { - line: cl.index, - error: e, - }); - } - } - } + LineKind::Cordial => lines.push((cl.index, &cl.content, false)), + LineKind::Eval => lines.push((cl.index, &cl.content, true)), LineKind::Comment | LineKind::Markdown => {} } } + let interp_results = interp::interpret_document(&lines); + for ir in interp_results { + match ir.value { + Some(interp::Value::Error(e)) => { + errors.push(EvalError { line: ir.line, error: e }); + } + Some(v) => { + let s = v.display(); + if !s.is_empty() { + results.push(EvalResult { line: ir.line, result: s }); + } + } + None => {} + } + } + DocumentResult { results, errors } } pub fn evaluate_line(text: &str) -> Result { - let graph = cord_expr::parse_expr(text)?; - let val = cord_trig::eval::evaluate(&graph, 0.0, 0.0, 0.0); - Ok(format_value(val)) + let mut interp = interp::Interpreter::new(); + match interp.eval_expr_str(text) { + Ok(v) => Ok(v.display()), + Err(_) => { + // fall back to cord-expr/cord-trig for trig and CORDIC expressions + let graph = cord_expr::parse_expr(text)?; + let val = cord_trig::eval::evaluate(&graph, 0.0, 0.0, 0.0); + Ok(format_value(val)) + } + } } fn format_value(val: f64) -> String { @@ -152,4 +145,78 @@ mod tests { assert_eq!(result.results.len(), 1); assert_eq!(result.results[0].result, "15"); } + + #[test] + fn eval_string_concat() { + let doc = "let x = \"hello\"\nlet y = \"world\"\n/= x + \" \" + y"; + let result = evaluate_document(doc); + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0].result, "hello world"); + } + + #[test] + fn eval_booleans() { + let doc = "let x = true\n/= x\n/= 1 > 0"; + let result = evaluate_document(doc); + assert_eq!(result.results.len(), 2); + assert_eq!(result.results[0].result, "true"); + assert_eq!(result.results[1].result, "true"); + } + + #[test] + fn eval_while_loop() { + let doc = "let i = 0\nlet sum = 0\nwhile (i < 10) {\n sum = sum + i\n i = i + 1\n}\n/= sum"; + let result = evaluate_document(doc); + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0].result, "45"); + } + + #[test] + fn eval_fn_block() { + let doc = "fn add(a, b) {\n a + b\n}\n/= add(3, 4)"; + let result = evaluate_document(doc); + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0].result, "7"); + } + + #[test] + fn eval_type_annotation() { + let doc = "let x: int = 3.7\n/= x"; + let result = evaluate_document(doc); + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0].result, "3"); + } + + #[test] + fn eval_type_annotation_bool_error() { + let doc = "let x: bool = 2\n/= x"; + let result = evaluate_document(doc); + assert!(result.errors.len() >= 1); + assert!(result.errors[0].error.contains("not a valid bool")); + } + + #[test] + fn eval_array() { + let doc = "let arr = [1, \"two\", true]\n/= arr"; + let result = evaluate_document(doc); + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0].result, "[1, \"two\", true]"); + } + + #[test] + fn eval_error_recovery() { + let doc = "let x = undefined_var\nlet y = 5\n/= y"; + let result = evaluate_document(doc); + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0].result, "5"); + assert!(result.errors.len() >= 1); + } + + #[test] + fn eval_mixed_markdown_and_code() { + let doc = "# Notes\nlet x = 10\nSome text here\nwhile (x > 0) {\n x = x - 1\n}\n/= x"; + let result = evaluate_document(doc); + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0].result, "0"); + } } diff --git a/core/src/interp.rs b/core/src/interp.rs new file mode 100644 index 0000000..ab2aef5 --- /dev/null +++ b/core/src/interp.rs @@ -0,0 +1,1271 @@ +use std::collections::HashMap; + +// --- Values --- + +#[derive(Clone, Debug)] +pub enum Value { + Number(f64), + Bool(bool), + Str(String), + Array(Vec), + Void, + Error(String), +} + +impl Value { + pub fn display(&self) -> String { + match self { + Value::Number(n) => format_number(*n), + Value::Bool(b) => b.to_string(), + Value::Str(s) => s.clone(), + Value::Array(items) => { + let inner: Vec = items.iter().map(|v| match v { + Value::Str(s) => format!("\"{}\"", s), + other => other.display(), + }).collect(); + format!("[{}]", inner.join(", ")) + } + Value::Void => String::new(), + Value::Error(e) => format!("error: {}", e), + } + } + + pub fn is_error(&self) -> bool { + matches!(self, Value::Error(_)) + } + + fn truthy(&self) -> bool { + match self { + Value::Bool(b) => *b, + Value::Number(n) => *n != 0.0, + Value::Str(s) => !s.is_empty(), + Value::Array(a) => !a.is_empty(), + Value::Void => false, + Value::Error(_) => false, + } + } +} + +fn format_number(n: f64) -> String { + if n == n.trunc() && n.abs() < 1e15 { + format!("{}", n as i64) + } else { + let s = format!("{:.10}", n); + let s = s.trim_end_matches('0'); + let s = s.trim_end_matches('.'); + s.to_string() + } +} + +// --- Tokens --- + +#[derive(Debug, Clone, PartialEq)] +enum Token { + Number(f64), + Str(String), + Bool(bool), + Ident(String), + Plus, + Minus, + Star, + Slash, + Percent, + Caret, + LParen, + RParen, + LBrace, + RBrace, + LBracket, + RBracket, + Comma, + Eq, + EqEq, + BangEq, + Lt, + Gt, + LtEq, + GtEq, + And, + Or, + Bang, + Colon, + Let, + While, + Fn, + Newline, + Eof, +} + +fn tokenize(input: &str) -> Result, String> { + let mut tokens = Vec::new(); + let chars: Vec = input.chars().collect(); + let len = chars.len(); + let mut i = 0; + + while i < len { + let c = chars[i]; + match c { + ' ' | '\t' | '\r' => { i += 1; } + '\n' => { tokens.push(Token::Newline); i += 1; } + '+' => { tokens.push(Token::Plus); i += 1; } + '-' => { + // negative number literal: only if preceded by operator/open/start/newline + if i + 1 < len && (chars[i + 1].is_ascii_digit() || chars[i + 1] == '.') { + let can_be_neg = if tokens.is_empty() { + true + } else { + matches!(tokens.last(), Some( + Token::Plus | Token::Minus | Token::Star | Token::Slash | + Token::Percent | Token::Caret | Token::LParen | Token::LBracket | + Token::Comma | Token::Eq | Token::EqEq | Token::BangEq | + Token::Lt | Token::Gt | Token::LtEq | Token::GtEq | + Token::And | Token::Or | Token::Bang | Token::Newline | + Token::Colon + )) + }; + if can_be_neg { + let start = i; + i += 1; + while i < len && (chars[i].is_ascii_digit() || chars[i] == '.') { + i += 1; + } + let s: String = chars[start..i].iter().collect(); + let n: f64 = s.parse().map_err(|_| format!("invalid number: {}", s))?; + tokens.push(Token::Number(n)); + } else { + tokens.push(Token::Minus); + i += 1; + } + } else { + tokens.push(Token::Minus); + i += 1; + } + } + '*' => { tokens.push(Token::Star); i += 1; } + '/' => { + if i + 1 < len && chars[i + 1] == '/' { + while i < len && chars[i] != '\n' { i += 1; } + } else { + tokens.push(Token::Slash); + i += 1; + } + } + '%' => { tokens.push(Token::Percent); i += 1; } + '^' => { tokens.push(Token::Caret); i += 1; } + '(' => { tokens.push(Token::LParen); i += 1; } + ')' => { tokens.push(Token::RParen); i += 1; } + '{' => { tokens.push(Token::LBrace); i += 1; } + '}' => { tokens.push(Token::RBrace); i += 1; } + '[' => { tokens.push(Token::LBracket); i += 1; } + ']' => { tokens.push(Token::RBracket); i += 1; } + ',' => { tokens.push(Token::Comma); i += 1; } + ':' => { tokens.push(Token::Colon); i += 1; } + '!' => { + if i + 1 < len && chars[i + 1] == '=' { + tokens.push(Token::BangEq); i += 2; + } else { + tokens.push(Token::Bang); i += 1; + } + } + '=' => { + if i + 1 < len && chars[i + 1] == '=' { + tokens.push(Token::EqEq); i += 2; + } else { + tokens.push(Token::Eq); i += 1; + } + } + '<' => { + if i + 1 < len && chars[i + 1] == '=' { + tokens.push(Token::LtEq); i += 2; + } else { + tokens.push(Token::Lt); i += 1; + } + } + '>' => { + if i + 1 < len && chars[i + 1] == '=' { + tokens.push(Token::GtEq); i += 2; + } else { + tokens.push(Token::Gt); i += 1; + } + } + '&' => { + if i + 1 < len && chars[i + 1] == '&' { + tokens.push(Token::And); i += 2; + } else { + return Err("unexpected '&', did you mean '&&'?".into()); + } + } + '|' => { + if i + 1 < len && chars[i + 1] == '|' { + tokens.push(Token::Or); i += 2; + } else { + return Err("unexpected '|', did you mean '||'?".into()); + } + } + '"' => { + i += 1; + let mut s = String::new(); + while i < len && chars[i] != '"' { + if chars[i] == '\\' && i + 1 < len { + i += 1; + match chars[i] { + 'n' => s.push('\n'), + 't' => s.push('\t'), + '\\' => s.push('\\'), + '"' => s.push('"'), + other => { s.push('\\'); s.push(other); } + } + } else { + s.push(chars[i]); + } + i += 1; + } + if i >= len { + return Err("unterminated string".into()); + } + i += 1; // closing quote + tokens.push(Token::Str(s)); + } + _ if c.is_ascii_digit() || c == '.' => { + let start = i; + while i < len && (chars[i].is_ascii_digit() || chars[i] == '.') { + i += 1; + } + let s: String = chars[start..i].iter().collect(); + let n: f64 = s.parse().map_err(|_| format!("invalid number: {}", s))?; + tokens.push(Token::Number(n)); + } + _ if c.is_alphabetic() || c == '_' => { + let start = i; + while i < len && (chars[i].is_alphanumeric() || chars[i] == '_') { + i += 1; + } + let word: String = chars[start..i].iter().collect(); + match word.as_str() { + "let" => tokens.push(Token::Let), + "while" => tokens.push(Token::While), + "fn" => tokens.push(Token::Fn), + "true" => tokens.push(Token::Bool(true)), + "false" => tokens.push(Token::Bool(false)), + _ => tokens.push(Token::Ident(word)), + } + } + _ => { + return Err(format!("unexpected character: '{}'", c)); + } + } + } + tokens.push(Token::Eof); + Ok(tokens) +} + +// --- AST --- + +#[derive(Debug, Clone)] +enum Op { + Add, Sub, Mul, Div, Mod, Pow, + Eq, Neq, Lt, Gt, Lte, Gte, + And, Or, Not, Neg, +} + +#[derive(Debug, Clone)] +enum Stmt { + Let(String, Option, Expr), + Assign(String, Expr), + While(Expr, Vec), + FnDef(String, Vec, Vec), + ExprStmt(Expr), +} + +#[derive(Debug, Clone)] +enum Expr { + Num(f64), + Str(String), + Bool(bool), + Ident(String), + BinOp(Op, Box, Box), + UnaryOp(Op, Box), + Call(String, Vec), + Array(Vec), +} + +// --- Parser --- + +struct Parser { + tokens: Vec, + pos: usize, +} + +impl Parser { + fn new(tokens: Vec) -> Self { + Parser { tokens, pos: 0 } + } + + fn peek(&self) -> &Token { + self.tokens.get(self.pos).unwrap_or(&Token::Eof) + } + + fn advance(&mut self) -> Token { + let tok = self.tokens.get(self.pos).cloned().unwrap_or(Token::Eof); + self.pos += 1; + tok + } + + fn expect(&mut self, expected: &Token) -> Result<(), String> { + let tok = self.advance(); + if &tok == expected { + Ok(()) + } else { + Err(format!("expected {:?}, got {:?}", expected, tok)) + } + } + + fn skip_newlines(&mut self) { + while self.peek() == &Token::Newline { + self.advance(); + } + } + + fn parse_program(&mut self) -> Result, String> { + let mut stmts = Vec::new(); + self.skip_newlines(); + while self.peek() != &Token::Eof { + stmts.push(self.parse_stmt()?); + self.skip_newlines(); + } + Ok(stmts) + } + + fn parse_block(&mut self) -> Result, String> { + self.expect(&Token::LBrace)?; + self.skip_newlines(); + let mut stmts = Vec::new(); + while self.peek() != &Token::RBrace && self.peek() != &Token::Eof { + stmts.push(self.parse_stmt()?); + self.skip_newlines(); + } + self.expect(&Token::RBrace)?; + Ok(stmts) + } + + fn parse_stmt(&mut self) -> Result { + self.skip_newlines(); + match self.peek().clone() { + Token::Let => self.parse_let(), + Token::While => self.parse_while(), + Token::Fn => self.parse_fn_def(), + Token::Ident(_) => { + let saved = self.pos; + if let Token::Ident(name) = self.advance() { + // name(params) = expr (legacy cord-expr function syntax) + if self.peek() == &Token::LParen { + let paren_saved = self.pos; + self.advance(); + let mut params = Vec::new(); + let mut valid = true; + if self.peek() != &Token::RParen { + match self.peek() { + Token::Ident(_) => { + if let Token::Ident(p) = self.advance() { params.push(p); } + while self.peek() == &Token::Comma { + self.advance(); + if let Token::Ident(p) = self.advance() { + params.push(p); + } else { + valid = false; + break; + } + } + } + _ => { valid = false; } + } + } + if valid && self.peek() == &Token::RParen { + self.advance(); + if self.peek() == &Token::Eq { + self.advance(); + let body_expr = self.parse_expr()?; + self.skip_newlines(); + return Ok(Stmt::FnDef(name, params, vec![Stmt::ExprStmt(body_expr)])); + } + } + self.pos = paren_saved; + // fall through: not a function def, might be assignment + } + if self.peek() == &Token::Eq { + self.advance(); + let expr = self.parse_expr()?; + self.skip_newlines(); + return Ok(Stmt::Assign(name, expr)); + } + } + self.pos = saved; + let expr = self.parse_expr()?; + self.skip_newlines(); + Ok(Stmt::ExprStmt(expr)) + } + _ => { + let expr = self.parse_expr()?; + self.skip_newlines(); + Ok(Stmt::ExprStmt(expr)) + } + } + } + + fn parse_let(&mut self) -> Result { + self.expect(&Token::Let)?; + let name = match self.advance() { + Token::Ident(n) => n, + t => return Err(format!("expected identifier after 'let', got {:?}", t)), + }; + let type_ann = if self.peek() == &Token::Colon { + self.advance(); + match self.advance() { + Token::Ident(t) => Some(t), + t => return Err(format!("expected type name after ':', got {:?}", t)), + } + } else { + None + }; + self.expect(&Token::Eq)?; + let expr = self.parse_expr()?; + self.skip_newlines(); + Ok(Stmt::Let(name, type_ann, expr)) + } + + fn parse_while(&mut self) -> Result { + self.expect(&Token::While)?; + let has_paren = if self.peek() == &Token::LParen { + self.advance(); + true + } else { + false + }; + let cond = self.parse_expr()?; + if has_paren { + self.expect(&Token::RParen)?; + } + self.skip_newlines(); + let body = self.parse_block()?; + Ok(Stmt::While(cond, body)) + } + + fn parse_fn_def(&mut self) -> Result { + self.expect(&Token::Fn)?; + let name = match self.advance() { + Token::Ident(n) => n, + t => return Err(format!("expected function name, got {:?}", t)), + }; + self.expect(&Token::LParen)?; + let mut params = Vec::new(); + if self.peek() != &Token::RParen { + match self.advance() { + Token::Ident(p) => params.push(p), + t => return Err(format!("expected parameter name, got {:?}", t)), + } + while self.peek() == &Token::Comma { + self.advance(); + match self.advance() { + Token::Ident(p) => params.push(p), + t => return Err(format!("expected parameter name, got {:?}", t)), + } + } + } + self.expect(&Token::RParen)?; + self.skip_newlines(); + let body = self.parse_block()?; + Ok(Stmt::FnDef(name, params, body)) + } + + // Expression precedence: or < and < comparison < add/sub < mul/div/mod < pow < unary < call/atom + fn parse_expr(&mut self) -> Result { + self.parse_or() + } + + fn parse_or(&mut self) -> Result { + let mut left = self.parse_and()?; + while self.peek() == &Token::Or { + self.advance(); + let right = self.parse_and()?; + left = Expr::BinOp(Op::Or, Box::new(left), Box::new(right)); + } + Ok(left) + } + + fn parse_and(&mut self) -> Result { + let mut left = self.parse_comparison()?; + while self.peek() == &Token::And { + self.advance(); + let right = self.parse_comparison()?; + left = Expr::BinOp(Op::And, Box::new(left), Box::new(right)); + } + Ok(left) + } + + fn parse_comparison(&mut self) -> Result { + let mut left = self.parse_additive()?; + loop { + let op = match self.peek() { + Token::EqEq => Op::Eq, + Token::BangEq => Op::Neq, + Token::Lt => Op::Lt, + Token::Gt => Op::Gt, + Token::LtEq => Op::Lte, + Token::GtEq => Op::Gte, + _ => break, + }; + self.advance(); + let right = self.parse_additive()?; + left = Expr::BinOp(op, Box::new(left), Box::new(right)); + } + Ok(left) + } + + fn parse_additive(&mut self) -> Result { + let mut left = self.parse_multiplicative()?; + loop { + let op = match self.peek() { + Token::Plus => Op::Add, + Token::Minus => Op::Sub, + _ => break, + }; + self.advance(); + let right = self.parse_multiplicative()?; + left = Expr::BinOp(op, Box::new(left), Box::new(right)); + } + Ok(left) + } + + fn parse_multiplicative(&mut self) -> Result { + let mut left = self.parse_power()?; + loop { + let op = match self.peek() { + Token::Star => Op::Mul, + Token::Slash => Op::Div, + Token::Percent => Op::Mod, + _ => break, + }; + self.advance(); + let right = self.parse_power()?; + left = Expr::BinOp(op, Box::new(left), Box::new(right)); + } + Ok(left) + } + + fn parse_power(&mut self) -> Result { + let base = self.parse_unary()?; + if self.peek() == &Token::Caret { + self.advance(); + let exp = self.parse_power()?; // right-associative + Ok(Expr::BinOp(Op::Pow, Box::new(base), Box::new(exp))) + } else { + Ok(base) + } + } + + fn parse_unary(&mut self) -> Result { + match self.peek() { + Token::Bang => { + self.advance(); + let expr = self.parse_unary()?; + Ok(Expr::UnaryOp(Op::Not, Box::new(expr))) + } + Token::Minus => { + // Only treat as unary neg if the minus wasn't already consumed as negative number + self.advance(); + let expr = self.parse_unary()?; + Ok(Expr::UnaryOp(Op::Neg, Box::new(expr))) + } + _ => self.parse_call(), + } + } + + fn parse_call(&mut self) -> Result { + let expr = self.parse_atom()?; + if let Expr::Ident(ref name) = expr { + if self.peek() == &Token::LParen { + self.advance(); + let mut args = Vec::new(); + if self.peek() != &Token::RParen { + args.push(self.parse_expr()?); + while self.peek() == &Token::Comma { + self.advance(); + args.push(self.parse_expr()?); + } + } + self.expect(&Token::RParen)?; + return Ok(Expr::Call(name.clone(), args)); + } + } + Ok(expr) + } + + fn parse_atom(&mut self) -> Result { + match self.peek().clone() { + Token::Number(n) => { self.advance(); Ok(Expr::Num(n)) } + Token::Str(s) => { self.advance(); Ok(Expr::Str(s)) } + Token::Bool(b) => { self.advance(); Ok(Expr::Bool(b)) } + Token::Ident(name) => { self.advance(); Ok(Expr::Ident(name)) } + Token::LParen => { + self.advance(); + let expr = self.parse_expr()?; + self.expect(&Token::RParen)?; + Ok(expr) + } + Token::LBracket => { + self.advance(); + let mut items = Vec::new(); + if self.peek() != &Token::RBracket { + items.push(self.parse_expr()?); + while self.peek() == &Token::Comma { + self.advance(); + items.push(self.parse_expr()?); + } + } + self.expect(&Token::RBracket)?; + Ok(Expr::Array(items)) + } + t => Err(format!("unexpected token: {:?}", t)), + } + } +} + +// --- Interpreter --- + +#[derive(Clone, Debug)] +struct FnDef { + params: Vec, + body: Vec, +} + +pub struct Interpreter { + vars: HashMap, + fns: HashMap, +} + +const MAX_ITERATIONS: usize = 10_000; +const MAX_CALL_DEPTH: u32 = 256; + +impl Interpreter { + pub fn new() -> Self { + Interpreter { + vars: HashMap::new(), + fns: HashMap::new(), + } + } + + pub fn exec_line(&mut self, line: &str) -> Result, String> { + let trimmed = line.trim(); + if trimmed.is_empty() { + return Ok(None); + } + let tokens = tokenize(trimmed)?; + let mut parser = Parser::new(tokens); + let stmts = parser.parse_program()?; + let mut last = Value::Void; + for stmt in stmts { + last = self.exec_stmt(&stmt, 0)?; + } + match last { + Value::Void => Ok(None), + v => Ok(Some(v)), + } + } + + pub fn eval_expr_str(&mut self, expr: &str) -> Result { + let trimmed = expr.trim(); + if trimmed.is_empty() { + return Err("empty expression".into()); + } + let tokens = tokenize(trimmed)?; + let mut parser = Parser::new(tokens); + let e = parser.parse_expr()?; + self.eval_expr(&e, 0) + } + + fn exec_stmt(&mut self, stmt: &Stmt, depth: u32) -> Result { + match stmt { + Stmt::Let(name, type_ann, expr) => { + let val = self.eval_expr(expr, depth)?; + let val = apply_type_annotation(&val, type_ann.as_deref())?; + self.vars.insert(name.clone(), val); + Ok(Value::Void) + } + Stmt::Assign(name, expr) => { + let val = self.eval_expr(expr, depth)?; + self.vars.insert(name.clone(), val); + Ok(Value::Void) + } + Stmt::While(cond, body) => { + let mut iterations = 0; + loop { + let cv = self.eval_expr(cond, depth)?; + if !cv.truthy() { break; } + iterations += 1; + if iterations > MAX_ITERATIONS { + return Err(format!("loop exceeded {} iterations", MAX_ITERATIONS)); + } + let mut last = Value::Void; + for s in body { + last = self.exec_stmt(s, depth)?; + } + drop(last); + } + Ok(Value::Void) + } + Stmt::FnDef(name, params, body) => { + self.fns.insert(name.clone(), FnDef { + params: params.clone(), + body: body.clone(), + }); + Ok(Value::Void) + } + Stmt::ExprStmt(expr) => { + self.eval_expr(expr, depth) + } + } + } + + fn eval_expr(&mut self, expr: &Expr, depth: u32) -> Result { + match expr { + Expr::Num(n) => Ok(Value::Number(*n)), + Expr::Str(s) => Ok(Value::Str(s.clone())), + Expr::Bool(b) => Ok(Value::Bool(*b)), + Expr::Ident(name) => { + self.vars.get(name).cloned() + .ok_or_else(|| format!("undefined variable '{}'", name)) + } + Expr::Array(items) => { + let mut vals = Vec::new(); + for item in items { + vals.push(self.eval_expr(item, depth)?); + } + Ok(Value::Array(vals)) + } + Expr::UnaryOp(Op::Not, inner) => { + let v = self.eval_expr(inner, depth)?; + Ok(Value::Bool(!v.truthy())) + } + Expr::UnaryOp(Op::Neg, inner) => { + let v = self.eval_expr(inner, depth)?; + match v { + Value::Number(n) => Ok(Value::Number(-n)), + _ => Err("cannot negate non-number".into()), + } + } + Expr::UnaryOp(op, _) => Err(format!("invalid unary op: {:?}", op)), + Expr::BinOp(op, lhs, rhs) => self.eval_binop(op, lhs, rhs, depth), + Expr::Call(name, args) => self.eval_call(name, args, depth), + } + } + + fn eval_binop(&mut self, op: &Op, lhs: &Expr, rhs: &Expr, depth: u32) -> Result { + // short-circuit for logical ops + if matches!(op, Op::And) { + let l = self.eval_expr(lhs, depth)?; + if !l.truthy() { return Ok(Value::Bool(false)); } + let r = self.eval_expr(rhs, depth)?; + return Ok(Value::Bool(r.truthy())); + } + if matches!(op, Op::Or) { + let l = self.eval_expr(lhs, depth)?; + if l.truthy() { return Ok(Value::Bool(true)); } + let r = self.eval_expr(rhs, depth)?; + return Ok(Value::Bool(r.truthy())); + } + + let l = self.eval_expr(lhs, depth)?; + let r = self.eval_expr(rhs, depth)?; + + match (op, &l, &r) { + // number arithmetic + (Op::Add, Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)), + (Op::Sub, Value::Number(a), Value::Number(b)) => Ok(Value::Number(a - b)), + (Op::Mul, Value::Number(a), Value::Number(b)) => Ok(Value::Number(a * b)), + (Op::Div, Value::Number(_, ), Value::Number(b)) if *b == 0.0 => Err("division by zero".into()), + (Op::Div, Value::Number(a), Value::Number(b)) => Ok(Value::Number(a / b)), + (Op::Mod, Value::Number(a), Value::Number(b)) => Ok(Value::Number(a % b)), + (Op::Pow, Value::Number(a), Value::Number(b)) => Ok(Value::Number(a.powf(*b))), + + // string concatenation + (Op::Add, Value::Str(a), Value::Str(b)) => Ok(Value::Str(format!("{}{}", a, b))), + (Op::Add, Value::Str(a), Value::Number(b)) => Ok(Value::Str(format!("{}{}", a, format_number(*b)))), + (Op::Add, Value::Number(a), Value::Str(b)) => Ok(Value::Str(format!("{}{}", format_number(*a), b))), + (Op::Add, Value::Str(a), Value::Bool(b)) => Ok(Value::Str(format!("{}{}", a, b))), + (Op::Add, Value::Bool(a), Value::Str(b)) => Ok(Value::Str(format!("{}{}", a, b))), + + // number comparisons + (Op::Lt, Value::Number(a), Value::Number(b)) => Ok(Value::Bool(a < b)), + (Op::Gt, Value::Number(a), Value::Number(b)) => Ok(Value::Bool(a > b)), + (Op::Lte, Value::Number(a), Value::Number(b)) => Ok(Value::Bool(a <= b)), + (Op::Gte, Value::Number(a), Value::Number(b)) => Ok(Value::Bool(a >= b)), + + // equality + (Op::Eq, Value::Number(a), Value::Number(b)) => Ok(Value::Bool(a == b)), + (Op::Eq, Value::Str(a), Value::Str(b)) => Ok(Value::Bool(a == b)), + (Op::Eq, Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a == b)), + (Op::Eq, _, _) => Ok(Value::Bool(false)), + + (Op::Neq, Value::Number(a), Value::Number(b)) => Ok(Value::Bool(a != b)), + (Op::Neq, Value::Str(a), Value::Str(b)) => Ok(Value::Bool(a != b)), + (Op::Neq, Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a != b)), + (Op::Neq, _, _) => Ok(Value::Bool(true)), + + _ => Err(format!("type error: cannot apply {:?} to {:?} and {:?}", op, type_name(&l), type_name(&r))), + } + } + + fn eval_call(&mut self, name: &str, args: &[Expr], depth: u32) -> Result { + if depth >= MAX_CALL_DEPTH { + return Err("maximum call depth exceeded".into()); + } + + // built-in math functions + match name { + "sin" | "cos" | "tan" | "asin" | "acos" | "atan" | + "sqrt" | "abs" | "floor" | "ceil" | "round" | "ln" | "log" => { + if args.len() != 1 { + return Err(format!("{}() expects 1 argument", name)); + } + let v = self.eval_expr(&args[0], depth)?; + let n = match v { + Value::Number(n) => n, + _ => return Err(format!("{}() expects a number", name)), + }; + let result = match name { + "sin" => n.sin(), + "cos" => n.cos(), + "tan" => n.tan(), + "asin" => n.asin(), + "acos" => n.acos(), + "atan" => n.atan(), + "sqrt" => n.sqrt(), + "abs" => n.abs(), + "floor" => n.floor(), + "ceil" => n.ceil(), + "round" => n.round(), + "ln" => n.ln(), + "log" => n.log10(), + _ => unreachable!(), + }; + return Ok(Value::Number(result)); + } + "len" => { + if args.len() != 1 { + return Err("len() expects 1 argument".into()); + } + let v = self.eval_expr(&args[0], depth)?; + return match v { + Value::Str(s) => Ok(Value::Number(s.len() as f64)), + Value::Array(a) => Ok(Value::Number(a.len() as f64)), + _ => Err("len() expects a string or array".into()), + }; + } + _ => {} + } + + let fdef = self.fns.get(name).cloned() + .ok_or_else(|| format!("undefined function '{}'", name))?; + + if args.len() != fdef.params.len() { + return Err(format!("{}() expects {} arguments, got {}", name, fdef.params.len(), args.len())); + } + + let mut arg_vals = Vec::new(); + for arg in args { + arg_vals.push(self.eval_expr(arg, depth)?); + } + + // save current vars, set up function scope + let saved_vars = self.vars.clone(); + for (param, val) in fdef.params.iter().zip(arg_vals) { + self.vars.insert(param.clone(), val); + } + + let mut result = Value::Void; + for stmt in &fdef.body { + result = self.exec_stmt(stmt, depth + 1)?; + } + + self.vars = saved_vars; + Ok(result) + } +} + +fn type_name(v: &Value) -> &'static str { + match v { + Value::Number(_) => "number", + Value::Bool(_) => "bool", + Value::Str(_) => "str", + Value::Array(_) => "array", + Value::Void => "void", + Value::Error(_) => "error", + } +} + +fn apply_type_annotation(val: &Value, ann: Option<&str>) -> Result { + let ann = match ann { + Some(a) => a, + None => return Ok(val.clone()), + }; + match ann { + "int" => match val { + Value::Number(n) => Ok(Value::Number(n.trunc())), + _ => Err(format!("type error: expected int, got {}", type_name(val))), + }, + "float" => match val { + Value::Number(_) => Ok(val.clone()), + _ => Err(format!("type error: expected float, got {}", type_name(val))), + }, + "bool" => match val { + Value::Bool(_) => Ok(val.clone()), + Value::Number(n) => { + if *n == 0.0 { Ok(Value::Bool(false)) } + else if *n == 1.0 { Ok(Value::Bool(true)) } + else { Err(format!("{} is not a valid bool", format_number(*n))) } + } + _ => Err(format!("type error: expected bool, got {}", type_name(val))), + }, + "str" => match val { + Value::Str(_) => Ok(val.clone()), + _ => Err(format!("type error: expected str, got {}", type_name(val))), + }, + other => Err(format!("unknown type annotation: {}", other)), + } +} + +// --- Public API for eval.rs integration --- + +pub struct InterpResult { + pub line: usize, + pub value: Option, +} + +pub fn interpret_document(lines: &[(usize, &str, bool)]) -> Vec { + // lines: (line_index, content, is_eval) + // Collect all cordial lines and eval lines in order. + // For each eval line, evaluate the expression and record the result. + let mut interp = Interpreter::new(); + let mut results = Vec::new(); + + // First pass: collect the entire program from cordial lines + // and evaluate line by line, recording results for eval lines + let mut brace_depth: i32 = 0; + let mut block_acc: Vec = Vec::new(); + + for &(idx, content, is_eval) in lines { + if is_eval { + // flush any accumulated block first + if !block_acc.is_empty() { + let block_text = block_acc.join("\n"); + block_acc.clear(); + brace_depth = 0; + match interp.exec_line(&block_text) { + Ok(_) => {} + Err(e) => { + results.push(InterpResult { line: idx, value: Some(Value::Error(e)) }); + continue; + } + } + } + + let expr = content.trim().strip_prefix("/=").unwrap_or("").trim(); + if expr.is_empty() { + results.push(InterpResult { line: idx, value: Some(Value::Error("empty expression".into())) }); + continue; + } + match interp.eval_expr_str(expr) { + Ok(val) => results.push(InterpResult { line: idx, value: Some(val) }), + Err(e) => results.push(InterpResult { line: idx, value: Some(Value::Error(e)) }), + } + } else { + let trimmed = content.trim(); + // track brace depth for multi-line blocks + let opens = trimmed.matches('{').count() as i32; + let closes = trimmed.matches('}').count() as i32; + + if brace_depth > 0 || !block_acc.is_empty() { + block_acc.push(trimmed.to_string()); + brace_depth += opens - closes; + if brace_depth <= 0 { + let block_text = block_acc.join("\n"); + block_acc.clear(); + brace_depth = 0; + if let Err(e) = interp.exec_line(&block_text) { + results.push(InterpResult { line: idx, value: Some(Value::Error(e)) }); + } + } + } else if opens > closes { + // starting a block + block_acc.push(trimmed.to_string()); + brace_depth = opens - closes; + } else { + // single-line cordial statement + if let Err(e) = interp.exec_line(trimmed) { + results.push(InterpResult { line: idx, value: Some(Value::Error(e)) }); + } + } + } + } + + results +} + +// --- Display helpers for type-annotated int --- + +pub fn display_value_with_type(val: &Value, type_ann: Option<&str>) -> String { + match (val, type_ann) { + (Value::Number(n), Some("int")) => format!("{}", *n as i64), + _ => val.display(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn eval(input: &str) -> String { + let lines: Vec<&str> = input.lines().collect(); + let mut tagged: Vec<(usize, &str, bool)> = Vec::new(); + for (i, line) in lines.iter().enumerate() { + let is_eval = line.trim().starts_with("/="); + let is_comment = line.trim().starts_with("//"); + if !is_eval && !is_comment && !line.trim().is_empty() && !line.trim().starts_with('#') { + tagged.push((i, line, false)); + } else if is_eval { + tagged.push((i, line, true)); + } + } + let results = interpret_document(&tagged); + results.iter() + .filter_map(|r| r.value.as_ref().map(|v| v.display())) + .collect::>() + .join(", ") + } + + fn eval_one(input: &str) -> String { + let mut interp = Interpreter::new(); + match interp.eval_expr_str(input) { + Ok(v) => v.display(), + Err(e) => format!("error: {}", e), + } + } + + #[test] + fn basic_arithmetic() { + assert_eq!(eval_one("2 + 3"), "5"); + assert_eq!(eval_one("10 - 4"), "6"); + assert_eq!(eval_one("3 * 7"), "21"); + assert_eq!(eval_one("15 / 3"), "5"); + assert_eq!(eval_one("2 ^ 10"), "1024"); + assert_eq!(eval_one("10 % 3"), "1"); + } + + #[test] + fn string_literals() { + assert_eq!(eval_one("\"hello\""), "hello"); + assert_eq!(eval_one("\"hello\" + \" \" + \"world\""), "hello world"); + } + + #[test] + fn string_concatenation_mixed() { + assert_eq!(eval_one("\"val: \" + 42"), "val: 42"); + assert_eq!(eval_one("100 + \" items\""), "100 items"); + } + + #[test] + fn boolean_literals() { + assert_eq!(eval_one("true"), "true"); + assert_eq!(eval_one("false"), "false"); + } + + #[test] + fn comparison_operators() { + assert_eq!(eval_one("1 < 2"), "true"); + assert_eq!(eval_one("2 > 3"), "false"); + assert_eq!(eval_one("5 == 5"), "true"); + assert_eq!(eval_one("5 != 3"), "true"); + assert_eq!(eval_one("3 <= 3"), "true"); + assert_eq!(eval_one("4 >= 5"), "false"); + } + + #[test] + fn logical_operators() { + assert_eq!(eval_one("true && false"), "false"); + assert_eq!(eval_one("true || false"), "true"); + assert_eq!(eval_one("!true"), "false"); + assert_eq!(eval_one("!false"), "true"); + } + + #[test] + fn arrays() { + assert_eq!(eval_one("[1, 2, 3]"), "[1, 2, 3]"); + assert_eq!(eval_one("[1, \"two\", true]"), "[1, \"two\", true]"); + assert_eq!(eval_one("[]"), "[]"); + } + + #[test] + fn variable_binding() { + let input = "let x = 5\n/= x + 10"; + assert_eq!(eval(input), "15"); + } + + #[test] + fn variable_reassignment() { + let input = "let x = 5\nx = 10\n/= x"; + assert_eq!(eval(input), "10"); + } + + #[test] + fn while_loop() { + let input = "let i = 0\nlet sum = 0\nwhile (i < 10) {\n sum = sum + i\n i = i + 1\n}\n/= sum"; + assert_eq!(eval(input), "45"); + } + + #[test] + fn while_loop_guard() { + let input = "let i = 0\nwhile (true) {\n i = i + 1\n}\n/= i"; + let result = eval(input); + assert!(result.contains("error"), "should error on infinite loop: {}", result); + } + + #[test] + fn function_def_and_call() { + let input = "fn add(a, b) {\n a + b\n}\n/= add(3, 4)"; + assert_eq!(eval(input), "7"); + } + + #[test] + fn function_calling_function() { + let input = "fn double(x) {\n x * 2\n}\nfn quad(x) {\n double(double(x))\n}\n/= quad(5)"; + assert_eq!(eval(input), "20"); + } + + #[test] + fn type_annotation_int() { + let input = "let x: int = 3.7\n/= x"; + assert_eq!(eval(input), "3"); + } + + #[test] + fn type_annotation_bool_valid() { + let input = "let x: bool = 1\n/= x"; + assert_eq!(eval(input), "true"); + } + + #[test] + fn type_annotation_bool_zero() { + let input = "let x: bool = 0\n/= x"; + assert_eq!(eval(input), "false"); + } + + #[test] + fn type_annotation_bool_invalid() { + let input = "let x: bool = 2\n/= x"; + let result = eval(input); + assert!(result.contains("error"), "should error: {}", result); + } + + #[test] + fn type_annotation_str() { + let input = "let x: str = \"hello\"\n/= x"; + assert_eq!(eval(input), "hello"); + } + + #[test] + fn type_annotation_str_mismatch() { + let input = "let x: str = 42\n/= x"; + let result = eval(input); + assert!(result.contains("error"), "should error: {}", result); + } + + #[test] + fn error_undefined_variable() { + let result = eval("/= undefined_var"); + assert!(result.contains("error"), "should error: {}", result); + assert!(result.contains("undefined variable"), "{}", result); + } + + #[test] + fn error_recovery() { + let input = "let x = bad_var\nlet y = 5\n/= y"; + // x assignment fails, but y should still work + let result = eval(input); + assert!(result.contains("5"), "should recover and eval y: {}", result); + } + + #[test] + fn error_undefined_function() { + let result = eval("/= nope(1, 2)"); + assert!(result.contains("error"), "should error: {}", result); + } + + #[test] + fn multiple_evals() { + let input = "let a = 3\n/= a\nlet b = 7\n/= a + b"; + assert_eq!(eval(input), "3, 10"); + } + + #[test] + fn builtin_math_functions() { + assert_eq!(eval_one("abs(-5)"), "5"); + assert_eq!(eval_one("floor(3.7)"), "3"); + assert_eq!(eval_one("ceil(3.2)"), "4"); + assert_eq!(eval_one("sqrt(16)"), "4"); + } + + #[test] + fn nested_expressions() { + assert_eq!(eval_one("(2 + 3) * (4 - 1)"), "15"); + assert_eq!(eval_one("2 * (3 + 4 * 5)"), "46"); + } + + #[test] + fn string_variable() { + let input = "let x = \"hello\"\nlet y = \"world\"\n/= x + \" \" + y"; + assert_eq!(eval(input), "hello world"); + } + + #[test] + fn division_by_zero() { + let result = eval_one("1 / 0"); + assert!(result.contains("error"), "should error on div by zero: {}", result); + } + + #[test] + fn len_function() { + assert_eq!(eval_one("len(\"hello\")"), "5"); + assert_eq!(eval_one("len([1, 2, 3])"), "3"); + } + + #[test] + fn negative_numbers() { + assert_eq!(eval_one("-5"), "-5"); + assert_eq!(eval_one("-3 + 7"), "4"); + assert_eq!(eval_one("10 + -3"), "7"); + } + + #[test] + fn empty_array() { + assert_eq!(eval_one("len([])"), "0"); + } + + #[test] + fn complex_while_with_function() { + let input = "\ +fn fib(n) { + let a = 0 + let b = 1 + let i = 0 + while (i < n) { + let tmp = b + b = a + b + a = tmp + i = i + 1 + } + a +} +/= fib(10)"; + assert_eq!(eval(input), "55"); + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index e76e8dc..9d7f74c 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,5 +1,6 @@ pub mod doc; pub mod document; pub mod eval; +pub mod interp; pub mod persist; pub mod ffi;