add if/else, for loops, array indexing, ranges, and return to Cordial

This commit is contained in:
jess 2026-04-06 11:35:23 -07:00
parent 39d2658d67
commit 8b3e780817
3 changed files with 417 additions and 8 deletions

View File

@ -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));
}
}

View File

@ -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");
}
}

View File

@ -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<Vec<Token>, 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<Vec<Token>, 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<Vec<Token>, 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<Vec<Token>, 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<String>, Expr),
Assign(String, Expr),
While(Expr, Vec<Stmt>),
IfElse(Expr, Vec<Stmt>, Option<Vec<Stmt>>),
ForLoop(String, Expr, Vec<Stmt>),
FnDef(String, Vec<String>, Vec<Stmt>),
Return(Expr),
ExprStmt(Expr),
}
@ -287,6 +304,8 @@ enum Expr {
UnaryOp(Op, Box<Expr>),
Call(String, Vec<Expr>),
Array(Vec<Expr>),
Index(Box<Expr>, Box<Expr>),
Range(Box<Expr>, Box<Expr>),
}
// --- 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<Stmt, String> {
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<Stmt, String> {
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<Stmt, String> {
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<Stmt, String> {
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<Expr, String> {
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<Expr, String> {
@ -581,7 +660,7 @@ impl Parser {
}
fn parse_call(&mut self) -> Result<Expr, String> {
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<char> = 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<Value> = (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<Value> = (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::<f64>().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]");
}
}