diff --git a/core/src/doc.rs b/core/src/doc.rs index c4b782f..81366e3 100644 --- a/core/src/doc.rs +++ b/core/src/doc.rs @@ -305,4 +305,35 @@ mod tests { let c = classify_line(0, "let flag: bool = 1"); assert_eq!(c.kind, LineKind::Cordial); } + + #[test] + fn if_line() { + let c = classify_line(0, "if (x > 5) {"); + assert_eq!(c.kind, LineKind::Cordial); + } + + #[test] + fn else_line() { + let c = classify_line(0, "} else {"); + assert_eq!(c.kind, LineKind::Cordial); + } + + #[test] + fn for_line() { + let c = classify_line(0, "for i in arr {"); + assert_eq!(c.kind, LineKind::Cordial); + } + + #[test] + fn return_line() { + let c = classify_line(0, "return x"); + assert_eq!(c.kind, LineKind::Cordial); + } + + #[test] + fn if_block_body_classified() { + let doc = "if (x > 5) {\n x = 1\n} else {\n x = 0\n}"; + let lines = classify_document(doc); + assert!(lines.iter().all(|l| l.kind == LineKind::Cordial)); + } } diff --git a/core/src/eval.rs b/core/src/eval.rs index 3a4d0a9..75acb81 100644 --- a/core/src/eval.rs +++ b/core/src/eval.rs @@ -219,4 +219,36 @@ mod tests { assert_eq!(result.results.len(), 1); assert_eq!(result.results[0].result, "0"); } + + #[test] + fn eval_if_else() { + let doc = "let x = 10\nif (x > 5) {\n x = 1\n} else {\n x = 0\n}\n/= x"; + let result = evaluate_document(doc); + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0].result, "1"); + } + + #[test] + fn eval_for_loop() { + let doc = "let sum = 0\nfor i in [1, 2, 3] {\n sum = sum + i\n}\n/= sum"; + let result = evaluate_document(doc); + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0].result, "6"); + } + + #[test] + fn eval_array_index() { + let doc = "let arr = [10, 20, 30]\n/= arr[1]"; + let result = evaluate_document(doc); + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0].result, "20"); + } + + #[test] + fn eval_fn_return() { + let doc = "fn max(a, b) {\n if (a > b) {\n return a\n }\n return b\n}\n/= max(3, 7)"; + let result = evaluate_document(doc); + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0].result, "7"); + } } diff --git a/core/src/interp.rs b/core/src/interp.rs index ab2aef5..177de45 100644 --- a/core/src/interp.rs +++ b/core/src/interp.rs @@ -89,9 +89,15 @@ enum Token { Or, Bang, Colon, + DotDot, Let, While, Fn, + If, + Else, + For, + In, + Return, Newline, Eof, } @@ -126,7 +132,7 @@ fn tokenize(input: &str) -> Result, String> { if can_be_neg { let start = i; i += 1; - while i < len && (chars[i].is_ascii_digit() || chars[i] == '.') { + while i < len && (chars[i].is_ascii_digit() || (chars[i] == '.' && !(i + 1 < len && chars[i + 1] == '.'))) { i += 1; } let s: String = chars[start..i].iter().collect(); @@ -160,6 +166,9 @@ fn tokenize(input: &str) -> Result, String> { ']' => { 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::DotDot); i += 2; + } '!' => { if i + 1 < len && chars[i + 1] == '=' { tokens.push(Token::BangEq); i += 2; @@ -226,9 +235,9 @@ fn tokenize(input: &str) -> Result, String> { i += 1; // closing quote tokens.push(Token::Str(s)); } - _ if c.is_ascii_digit() || c == '.' => { + _ if c.is_ascii_digit() || (c == '.' && i + 1 < len && chars[i + 1].is_ascii_digit()) => { let start = i; - while i < len && (chars[i].is_ascii_digit() || chars[i] == '.') { + while i < len && (chars[i].is_ascii_digit() || (chars[i] == '.' && !(i + 1 < len && chars[i + 1] == '.'))) { i += 1; } let s: String = chars[start..i].iter().collect(); @@ -245,6 +254,11 @@ fn tokenize(input: &str) -> Result, String> { "let" => tokens.push(Token::Let), "while" => tokens.push(Token::While), "fn" => tokens.push(Token::Fn), + "if" => tokens.push(Token::If), + "else" => tokens.push(Token::Else), + "for" => tokens.push(Token::For), + "in" => tokens.push(Token::In), + "return" => tokens.push(Token::Return), "true" => tokens.push(Token::Bool(true)), "false" => tokens.push(Token::Bool(false)), _ => tokens.push(Token::Ident(word)), @@ -273,7 +287,10 @@ enum Stmt { Let(String, Option, Expr), Assign(String, Expr), While(Expr, Vec), + IfElse(Expr, Vec, Option>), + ForLoop(String, Expr, Vec), FnDef(String, Vec, Vec), + Return(Expr), ExprStmt(Expr), } @@ -287,6 +304,8 @@ enum Expr { UnaryOp(Op, Box), Call(String, Vec), Array(Vec), + Index(Box, Box), + Range(Box, Box), } // --- Parser --- @@ -353,6 +372,9 @@ impl Parser { match self.peek().clone() { Token::Let => self.parse_let(), Token::While => self.parse_while(), + Token::If => self.parse_if(), + Token::For => self.parse_for(), + Token::Return => self.parse_return(), Token::Fn => self.parse_fn_def(), Token::Ident(_) => { let saved = self.pos; @@ -450,6 +472,58 @@ impl Parser { Ok(Stmt::While(cond, body)) } + fn parse_if(&mut self) -> Result { + self.expect(&Token::If)?; + 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 then_body = self.parse_block()?; + self.skip_newlines(); + let else_body = if self.peek() == &Token::Else { + self.advance(); + self.skip_newlines(); + if self.peek() == &Token::If { + Some(vec![self.parse_if()?]) + } else { + Some(self.parse_block()?) + } + } else { + None + }; + Ok(Stmt::IfElse(cond, then_body, else_body)) + } + + fn parse_for(&mut self) -> Result { + self.expect(&Token::For)?; + let var = match self.advance() { + Token::Ident(n) => n, + t => return Err(format!("expected loop variable, got {:?}", t)), + }; + self.expect(&Token::In)?; + let iter = self.parse_expr()?; + self.skip_newlines(); + let body = self.parse_block()?; + Ok(Stmt::ForLoop(var, iter, body)) + } + + fn parse_return(&mut self) -> Result { + self.expect(&Token::Return)?; + if matches!(self.peek(), Token::Newline | Token::Eof | Token::RBrace) { + return Ok(Stmt::Return(Expr::Bool(false))); + } + let expr = self.parse_expr()?; + self.skip_newlines(); + Ok(Stmt::Return(expr)) + } + fn parse_fn_def(&mut self) -> Result { self.expect(&Token::Fn)?; let name = match self.advance() { @@ -477,9 +551,14 @@ impl Parser { 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() + let left = self.parse_or()?; + if self.peek() == &Token::DotDot { + self.advance(); + let right = self.parse_or()?; + return Ok(Expr::Range(Box::new(left), Box::new(right))); + } + Ok(left) } fn parse_or(&mut self) -> Result { @@ -581,7 +660,7 @@ impl Parser { } fn parse_call(&mut self) -> Result { - let expr = self.parse_atom()?; + let mut expr = self.parse_atom()?; if let Expr::Ident(ref name) = expr { if self.peek() == &Token::LParen { self.advance(); @@ -594,9 +673,15 @@ impl Parser { } } self.expect(&Token::RParen)?; - return Ok(Expr::Call(name.clone(), args)); + expr = Expr::Call(name.clone(), args); } } + while self.peek() == &Token::LBracket { + self.advance(); + let index = self.parse_expr()?; + self.expect(&Token::RBracket)?; + expr = Expr::Index(Box::new(expr), Box::new(index)); + } Ok(expr) } @@ -713,6 +798,37 @@ impl Interpreter { } Ok(Value::Void) } + Stmt::IfElse(cond, then_body, else_body) => { + let cv = self.eval_expr(cond, depth)?; + let body = if cv.truthy() { then_body } else { + match else_body { Some(b) => b, None => return Ok(Value::Void) } + }; + let mut last = Value::Void; + for s in body { + last = self.exec_stmt(s, depth)?; + } + Ok(last) + } + Stmt::ForLoop(var, iter_expr, body) => { + let iterable = self.eval_expr(iter_expr, depth)?; + let items = match iterable { + Value::Array(a) => a, + _ => return Err("for loop requires an array or range".into()), + }; + let mut iterations = 0; + let mut last = Value::Void; + for item in &items { + iterations += 1; + if iterations > MAX_ITERATIONS { + return Err(format!("loop exceeded {} iterations", MAX_ITERATIONS)); + } + self.vars.insert(var.clone(), item.clone()); + for s in body { + last = self.exec_stmt(s, depth)?; + } + } + Ok(last) + } Stmt::FnDef(name, params, body) => { self.fns.insert(name.clone(), FnDef { params: params.clone(), @@ -720,6 +836,10 @@ impl Interpreter { }); Ok(Value::Void) } + Stmt::Return(expr) => { + let val = self.eval_expr(expr, depth)?; + Err(format!("\x00return:{}", encode_return(&val))) + } Stmt::ExprStmt(expr) => { self.eval_expr(expr, depth) } @@ -756,6 +876,41 @@ impl Interpreter { 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), + Expr::Index(target, index) => { + let target_val = self.eval_expr(target, depth)?; + let index_val = self.eval_expr(index, depth)?; + match (&target_val, &index_val) { + (Value::Array(arr), Value::Number(n)) => { + let i = *n as i64; + let idx = if i < 0 { (arr.len() as i64 + i) as usize } else { i as usize }; + arr.get(idx).cloned().ok_or_else(|| format!("index {} out of bounds (len {})", i, arr.len())) + } + (Value::Str(s), Value::Number(n)) => { + let i = *n as i64; + let chars: Vec = s.chars().collect(); + let idx = if i < 0 { (chars.len() as i64 + i) as usize } else { i as usize }; + chars.get(idx).map(|c| Value::Str(c.to_string())) + .ok_or_else(|| format!("index {} out of bounds (len {})", i, chars.len())) + } + _ => Err(format!("cannot index {} with {}", type_name(&target_val), type_name(&index_val))), + } + } + Expr::Range(start, end) => { + let sv = self.eval_expr(start, depth)?; + let ev = self.eval_expr(end, depth)?; + match (&sv, &ev) { + (Value::Number(a), Value::Number(b)) => { + let a = *a as i64; + let b = *b as i64; + let items: Vec = (a..b).map(|n| Value::Number(n as f64)).collect(); + if items.len() > MAX_ITERATIONS { + return Err(format!("range too large ({} elements)", items.len())); + } + Ok(Value::Array(items)) + } + _ => Err("range requires two numbers".into()), + } + } } } @@ -861,6 +1016,35 @@ impl Interpreter { _ => Err("len() expects a string or array".into()), }; } + "range" => { + if args.len() != 2 { + return Err("range() expects 2 arguments".into()); + } + let start = match self.eval_expr(&args[0], depth)? { + Value::Number(n) => n as i64, + _ => return Err("range() expects numbers".into()), + }; + let end = match self.eval_expr(&args[1], depth)? { + Value::Number(n) => n as i64, + _ => return Err("range() expects numbers".into()), + }; + let items: Vec = (start..end).map(|n| Value::Number(n as f64)).collect(); + if items.len() > MAX_ITERATIONS { + return Err(format!("range too large ({} elements)", items.len())); + } + return Ok(Value::Array(items)); + } + "push" => { + if args.len() != 2 { + return Err("push() expects 2 arguments (array, value)".into()); + } + let arr = self.eval_expr(&args[0], depth)?; + let val = self.eval_expr(&args[1], depth)?; + return match arr { + Value::Array(mut a) => { a.push(val); Ok(Value::Array(a)) } + _ => Err("push() expects an array as first argument".into()), + }; + } _ => {} } @@ -884,7 +1068,17 @@ impl Interpreter { let mut result = Value::Void; for stmt in &fdef.body { - result = self.exec_stmt(stmt, depth + 1)?; + match self.exec_stmt(stmt, depth + 1) { + Ok(v) => result = v, + Err(e) if e.starts_with('\x00') => { + self.vars = saved_vars; + return Ok(decode_return(&e)); + } + Err(e) => { + self.vars = saved_vars; + return Err(e); + } + } } self.vars = saved_vars; @@ -892,6 +1086,31 @@ impl Interpreter { } } +const RETURN_PREFIX: &str = "\x00return:"; + +fn encode_return(val: &Value) -> String { + match val { + Value::Number(n) => format!("n:{}", n), + Value::Bool(b) => format!("b:{}", b), + Value::Str(s) => format!("s:{}", s), + Value::Void => "v:".into(), + _ => format!("s:{}", val.display()), + } +} + +fn decode_return(encoded: &str) -> Value { + let payload = &encoded[RETURN_PREFIX.len()..]; + if let Some(rest) = payload.strip_prefix("n:") { + rest.parse::().map(Value::Number).unwrap_or(Value::Void) + } else if let Some(rest) = payload.strip_prefix("b:") { + Value::Bool(rest == "true") + } else if let Some(rest) = payload.strip_prefix("s:") { + Value::Str(rest.to_string()) + } else { + Value::Void + } +} + fn type_name(v: &Value) -> &'static str { match v { Value::Number(_) => "number", @@ -1268,4 +1487,131 @@ fn fib(n) { /= fib(10)"; assert_eq!(eval(input), "55"); } + + #[test] + fn if_true() { + let input = "let x = 10\nif (x > 5) {\n x = 100\n}\n/= x"; + assert_eq!(eval(input), "100"); + } + + #[test] + fn if_false() { + let input = "let x = 3\nif (x > 5) {\n x = 100\n}\n/= x"; + assert_eq!(eval(input), "3"); + } + + #[test] + fn if_else() { + let input = "let x = 3\nif (x > 5) {\n x = 100\n} else {\n x = 0\n}\n/= x"; + assert_eq!(eval(input), "0"); + } + + #[test] + fn if_else_chain() { + let input = "\ +let x = 5 +let r = 0 +if (x > 10) { + r = 3 +} else if (x > 3) { + r = 2 +} else { + r = 1 +} +/= r"; + assert_eq!(eval(input), "2"); + } + + #[test] + fn if_without_parens() { + let input = "let x = 10\nif x > 5 {\n x = 100\n}\n/= x"; + assert_eq!(eval(input), "100"); + } + + #[test] + fn for_loop_array() { + let input = "let sum = 0\nfor x in [1, 2, 3, 4, 5] {\n sum = sum + x\n}\n/= sum"; + assert_eq!(eval(input), "15"); + } + + #[test] + fn for_loop_range() { + let input = "let sum = 0\nfor i in 0..5 {\n sum = sum + i\n}\n/= sum"; + assert_eq!(eval(input), "10"); + } + + #[test] + fn for_loop_range_fn() { + let input = "let sum = 0\nfor i in range(1, 6) {\n sum = sum + i\n}\n/= sum"; + assert_eq!(eval(input), "15"); + } + + #[test] + fn array_index() { + assert_eq!(eval_one("[10, 20, 30][1]"), "20"); + } + + #[test] + fn array_index_variable() { + let input = "let arr = [10, 20, 30]\n/= arr[2]"; + assert_eq!(eval(input), "30"); + } + + #[test] + fn array_negative_index() { + assert_eq!(eval_one("[10, 20, 30][-1]"), "30"); + } + + #[test] + fn string_index() { + assert_eq!(eval_one("\"hello\"[0]"), "h"); + } + + #[test] + fn array_index_out_of_bounds() { + let result = eval_one("[1, 2][5]"); + assert!(result.contains("error"), "should error: {}", result); + } + + #[test] + fn return_from_function() { + let input = "\ +fn first_positive(a, b) { + if (a > 0) { + return a + } + if (b > 0) { + return b + } + return 0 +} +/= first_positive(-1, 5)"; + assert_eq!(eval(input), "5"); + } + + #[test] + fn return_early_from_loop() { + let input = "\ +fn find(arr, target) { + for x in arr { + if (x == target) { + return x + } + } + return -1 +} +/= find([1, 2, 3, 4], 3)"; + assert_eq!(eval(input), "3"); + } + + #[test] + fn push_builtin() { + let input = "let arr = [1, 2]\nlet arr = push(arr, 3)\n/= arr"; + assert_eq!(eval(input), "[1, 2, 3]"); + } + + #[test] + fn range_expression() { + assert_eq!(eval_one("0..5"), "[0, 1, 2, 3, 4]"); + } }