use crate::eval::{Expr, Op, expr_to_string};
#[derive(Debug)]
pub enum Stmt {
Assign(String, Expr),
Expr(Expr),
If {
cond: Expr,
body: Vec<(Stmt, bool)>,
elseif_branches: Vec<(Expr, Vec<(Stmt, bool)>)>,
else_body: Option<Vec<(Stmt, bool)>>,
},
For {
var: String,
range_expr: Expr,
body: Vec<(Stmt, bool)>,
},
While {
cond: Expr,
body: Vec<(Stmt, bool)>,
},
Break,
Continue,
#[allow(clippy::type_complexity)]
Switch {
expr: Expr,
cases: Vec<(Vec<Expr>, Vec<(Stmt, bool)>)>,
otherwise_body: Option<Vec<(Stmt, bool)>>,
},
DoUntil {
body: Vec<(Stmt, bool)>,
cond: Expr,
},
FunctionDef {
name: String,
outputs: Vec<String>,
params: Vec<String>,
body_source: String,
doc: Option<String>,
},
Return,
MultiAssign {
targets: Vec<String>,
expr: Expr,
},
TryCatch {
try_body: Vec<(Stmt, bool)>,
catch_var: Option<String>,
catch_body: Vec<(Stmt, bool)>,
},
CellSet(String, Expr, Expr),
FieldSet(String, Vec<String>, Expr),
StructArrayFieldSet(String, Expr, Vec<String>, Expr),
IndexSet {
name: String,
indices: Vec<Expr>,
value: Expr,
},
Global(Vec<String>),
Persistent(Vec<String>),
}
#[derive(Debug, Clone)]
enum Token {
Number(f64),
Ident(String),
Str(String), StringObj(String), Plus,
Minus,
Star,
Slash,
Caret,
DotStar,
DotSlash,
DotCaret,
Apostrophe,
LParen,
RParen,
Comma,
LBracket,
RBracket,
Semicolon,
Colon,
PlusEq, MinusEq, StarEq, SlashEq, PlusPlus, MinusMinus, EqEq, NotEq, Lt, Gt, LtEq, GtEq, AmpAmp, PipePipe, Amp, Pipe, Tilde, At, LBrace, RBrace, StarStar, DotApostrophe, Backslash, Dot, }
fn parse_integer_literal(
chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
radix: u32,
prefix: &str,
) -> Result<f64, String> {
let mut digit_str = String::new();
while let Some(&d) = chars.peek() {
let valid = match radix {
16 => d.is_ascii_hexdigit(),
2 => d == '0' || d == '1',
8 => ('0'..='7').contains(&d),
_ => false,
};
if valid {
digit_str.push(d);
chars.next();
} else {
break;
}
}
if digit_str.is_empty() {
return Err(format!("Expected digits after '{prefix}'"));
}
i64::from_str_radix(&digit_str, radix)
.map(|i| i as f64)
.map_err(|_| format!("Invalid {prefix} literal: '{prefix}{digit_str}'"))
}
fn try_consume_sci_exponent(
chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
num_str: &mut String,
) {
if !matches!(chars.peek(), Some('e') | Some('E')) {
return;
}
let mut lookahead = chars.clone();
let e_char = lookahead.next().unwrap();
match lookahead.peek().copied() {
Some('+') | Some('-') => {
let sign = lookahead.next().unwrap();
if lookahead.peek().is_some_and(|d| d.is_ascii_digit()) {
chars.next();
chars.next();
num_str.push(e_char);
num_str.push(sign);
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() {
num_str.push(d);
chars.next();
} else {
break;
}
}
}
}
Some(d) if d.is_ascii_digit() => {
chars.next();
num_str.push(e_char);
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() {
num_str.push(d);
chars.next();
} else {
break;
}
}
}
_ => {}
}
}
#[inline]
fn push_imag_suffix(chars: &mut std::iter::Peekable<std::str::Chars<'_>>, tokens: &mut Vec<Token>) {
if matches!(chars.peek(), Some('i') | Some('j')) {
let mut la = chars.clone();
la.next();
if !la.peek().is_some_and(|c| c.is_alphanumeric() || *c == '_') {
chars.next(); tokens.push(Token::Star);
tokens.push(Token::Ident("i".to_string()));
}
}
}
fn tokenize(input: &str) -> Result<Vec<Token>, String> {
let mut tokens = Vec::new();
let mut chars = input.chars().peekable();
while let Some(&c) = chars.peek() {
match c {
' ' | '\t' => {
chars.next();
}
'+' => {
chars.next();
match chars.peek() {
Some('=') => {
chars.next();
tokens.push(Token::PlusEq);
}
Some('+') => {
chars.next();
tokens.push(Token::PlusPlus);
}
_ => tokens.push(Token::Plus),
}
}
'-' => {
chars.next();
match chars.peek() {
Some('=') => {
chars.next();
tokens.push(Token::MinusEq);
}
Some('-') => {
chars.next();
tokens.push(Token::MinusMinus);
}
_ => tokens.push(Token::Minus),
}
}
'*' => {
chars.next();
match chars.peek() {
Some('=') => {
chars.next();
tokens.push(Token::StarEq);
}
Some('*') => {
chars.next();
tokens.push(Token::StarStar);
}
_ => tokens.push(Token::Star),
}
}
'/' => {
chars.next();
if chars.peek() == Some(&'=') {
chars.next();
tokens.push(Token::SlashEq);
} else {
tokens.push(Token::Slash);
}
}
'^' => {
tokens.push(Token::Caret);
chars.next();
}
'\'' => {
let is_transpose = matches!(
tokens.last(),
Some(
Token::Number(_)
| Token::Ident(_)
| Token::RParen
| Token::RBracket
| Token::Apostrophe
| Token::Str(_)
)
);
chars.next(); if is_transpose {
tokens.push(Token::Apostrophe);
} else {
let mut content = String::new();
loop {
match chars.next() {
None => return Err("Unterminated string literal".to_string()),
Some('\'') => {
if chars.peek().copied() == Some('\'') {
chars.next();
content.push('\'');
} else {
break;
}
}
Some(c) => content.push(c),
}
}
tokens.push(Token::Str(content));
}
}
'"' => {
chars.next(); let mut content = String::new();
loop {
match chars.next() {
None => return Err("Unterminated string literal".to_string()),
Some('"') => {
if chars.peek().copied() == Some('"') {
chars.next();
content.push('"');
} else {
break;
}
}
Some('\\') => match chars.next() {
Some('n') => content.push('\n'),
Some('t') => content.push('\t'),
Some('\\') => content.push('\\'),
Some('\'') => content.push('\''),
Some('"') => content.push('"'),
Some(other) => {
content.push('\\');
content.push(other);
}
None => return Err("Unterminated string literal".to_string()),
},
Some(c) => content.push(c),
}
}
tokens.push(Token::StringObj(content));
}
'.' => {
chars.next();
match chars.peek().copied() {
Some('.') => {
chars.next(); if chars.peek() == Some(&'.') {
chars.next(); while chars.next().is_some() {}
} else {
return Err("Unexpected '..'".to_string());
}
}
Some('\'') => {
chars.next();
tokens.push(Token::DotApostrophe);
}
Some('*') => {
chars.next();
tokens.push(Token::DotStar);
}
Some('/') => {
chars.next();
tokens.push(Token::DotSlash);
}
Some('^') => {
chars.next();
tokens.push(Token::DotCaret);
}
Some(d) if d.is_ascii_digit() => {
let mut num_str = String::from(".");
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() {
num_str.push(d);
chars.next();
} else {
break;
}
}
try_consume_sci_exponent(&mut chars, &mut num_str);
let n: f64 = num_str
.parse()
.map_err(|_| format!("Invalid number: '{num_str}'"))?;
tokens.push(Token::Number(n));
}
Some(c) if c.is_ascii_alphabetic() || c == '_' => {
tokens.push(Token::Dot);
}
_ => return Err("Unexpected '.'".to_string()),
}
}
'%' | '#' => {
break;
}
'!' => {
chars.next();
if chars.peek().copied() == Some('=') {
chars.next();
tokens.push(Token::NotEq);
} else {
tokens.push(Token::Tilde);
}
}
'(' => {
tokens.push(Token::LParen);
chars.next();
}
')' => {
tokens.push(Token::RParen);
chars.next();
}
',' => {
tokens.push(Token::Comma);
chars.next();
}
'[' => {
tokens.push(Token::LBracket);
chars.next();
}
']' => {
tokens.push(Token::RBracket);
chars.next();
}
'{' => {
tokens.push(Token::LBrace);
chars.next();
}
'}' => {
tokens.push(Token::RBrace);
chars.next();
}
';' => {
tokens.push(Token::Semicolon);
chars.next();
}
':' => {
tokens.push(Token::Colon);
chars.next();
}
'=' => {
chars.next();
if chars.peek().copied() == Some('=') {
chars.next();
tokens.push(Token::EqEq);
} else {
return Err("Unexpected '=': use '==' for comparison".to_string());
}
}
'~' => {
chars.next();
if chars.peek().copied() == Some('=') {
chars.next();
tokens.push(Token::NotEq);
} else {
tokens.push(Token::Tilde);
}
}
'<' => {
chars.next();
if chars.peek().copied() == Some('=') {
chars.next();
tokens.push(Token::LtEq);
} else {
tokens.push(Token::Lt);
}
}
'>' => {
chars.next();
if chars.peek().copied() == Some('=') {
chars.next();
tokens.push(Token::GtEq);
} else {
tokens.push(Token::Gt);
}
}
'&' => {
chars.next();
if chars.peek().copied() == Some('&') {
chars.next();
tokens.push(Token::AmpAmp);
} else {
tokens.push(Token::Amp);
}
}
'|' => {
chars.next();
if chars.peek().copied() == Some('|') {
chars.next();
tokens.push(Token::PipePipe);
} else {
tokens.push(Token::Pipe);
}
}
'0'..='9' => {
if c == '0' {
chars.next();
match chars.peek().copied() {
Some('x') | Some('X') => {
chars.next();
let n = parse_integer_literal(&mut chars, 16, "0x")?;
tokens.push(Token::Number(n));
}
Some('b') | Some('B') => {
chars.next();
let n = parse_integer_literal(&mut chars, 2, "0b")?;
tokens.push(Token::Number(n));
}
Some('o') | Some('O') => {
chars.next();
let n = parse_integer_literal(&mut chars, 8, "0o")?;
tokens.push(Token::Number(n));
}
_ => {
let mut num_str = String::from("0");
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() {
num_str.push(d);
chars.next();
} else if d == '.' {
let mut la = chars.clone();
la.next();
if matches!(la.peek(), Some('*') | Some('/') | Some('^')) {
break;
}
num_str.push('.');
chars.next();
} else {
break;
}
}
try_consume_sci_exponent(&mut chars, &mut num_str);
let n: f64 = num_str
.parse()
.map_err(|_| format!("Invalid number: '{num_str}'"))?;
tokens.push(Token::Number(n));
push_imag_suffix(&mut chars, &mut tokens);
}
}
} else {
let mut num_str = String::new();
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() {
num_str.push(d);
chars.next();
} else if d == '.' {
let mut la = chars.clone();
la.next();
if matches!(la.peek(), Some('*') | Some('/') | Some('^')) {
break;
}
num_str.push('.');
chars.next();
} else {
break;
}
}
try_consume_sci_exponent(&mut chars, &mut num_str);
let n: f64 = num_str
.parse()
.map_err(|_| format!("Invalid number: '{num_str}'"))?;
tokens.push(Token::Number(n));
push_imag_suffix(&mut chars, &mut tokens);
}
}
'@' => {
tokens.push(Token::At);
chars.next();
}
'\\' => {
tokens.push(Token::Backslash);
chars.next();
}
'a'..='z' | 'A'..='Z' | '_' => {
let mut ident = String::new();
while let Some(&c) = chars.peek() {
if c.is_alphanumeric() || c == '_' {
ident.push(c);
chars.next();
} else {
break;
}
}
tokens.push(Token::Ident(ident));
}
_ => return Err(format!("Unexpected character: '{c}'")),
}
}
Ok(tokens)
}
fn try_split_struct_array_field_assign(input: &str) -> Option<(String, &str, Vec<String>, &str)> {
let trimmed = input.trim();
let bytes = trimmed.as_bytes();
let mut i = 0;
if i >= bytes.len() || !(bytes[i].is_ascii_alphabetic() || bytes[i] == b'_') {
return None;
}
let base_start = i;
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
i += 1;
}
let base_var = trimmed[base_start..i].to_string();
if i >= bytes.len() || bytes[i] != b'(' {
return None;
}
i += 1;
let idx_start = i;
let mut depth = 1usize;
while i < bytes.len() && depth > 0 {
match bytes[i] {
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
_ => {}
}
i += 1;
}
if depth != 0 {
return None;
}
let idx_str = &trimmed[idx_start..i - 1];
if i >= bytes.len() || bytes[i] != b'.' {
return None;
}
let mut fields = Vec::new();
while i < bytes.len() && bytes[i] == b'.' {
i += 1;
if i >= bytes.len() || !(bytes[i].is_ascii_alphabetic() || bytes[i] == b'_') {
return None;
}
let field_start = i;
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
i += 1;
}
fields.push(trimmed[field_start..i].to_string());
}
if fields.is_empty() {
return None;
}
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if i >= bytes.len() || bytes[i] != b'=' {
return None;
}
i += 1;
if i < bytes.len() && bytes[i] == b'=' {
return None; }
let rhs = trimmed[i..].trim();
if rhs.is_empty() {
return None;
}
Some((base_var, idx_str, fields, rhs))
}
fn try_split_field_assign(input: &str) -> Option<(String, Vec<String>, &str)> {
let trimmed = input.trim();
let bytes = trimmed.as_bytes();
let mut i = 0;
if i >= bytes.len() || !(bytes[i].is_ascii_alphabetic() || bytes[i] == b'_') {
return None;
}
let base_start = i;
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
i += 1;
}
let base_var = trimmed[base_start..i].to_string();
let mut fields = Vec::new();
while i < bytes.len() && bytes[i] == b'.' {
i += 1;
if i >= bytes.len() || !(bytes[i].is_ascii_alphabetic() || bytes[i] == b'_') {
return None;
}
let field_start = i;
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
i += 1;
}
fields.push(trimmed[field_start..i].to_string());
}
if fields.is_empty() {
return None;
}
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if i >= bytes.len() || bytes[i] != b'=' {
return None;
}
i += 1;
if i < bytes.len() && bytes[i] == b'=' {
return None; }
let rhs = trimmed[i..].trim();
if rhs.is_empty() {
return None;
}
Some((base_var, fields, rhs))
}
fn parse_name_list(rest: &str) -> Result<Vec<String>, String> {
let names: Vec<String> = rest
.split(|c: char| c.is_whitespace() || c == ',')
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
if names.is_empty() {
return Err("Expected at least one variable name".to_string());
}
for name in &names {
if !name.starts_with(|c: char| c.is_alphabetic() || c == '_')
|| name.chars().any(|c| !c.is_alphanumeric() && c != '_')
{
return Err(format!("Invalid variable name: '{name}'"));
}
}
Ok(names)
}
pub fn parse(input: &str) -> Result<Stmt, String> {
let trimmed = input.trim();
if let Some(rest) = trimmed
.strip_prefix("global")
.filter(|r| r.is_empty() || r.starts_with(|c: char| c.is_whitespace() || c == ','))
{
return Ok(Stmt::Global(parse_name_list(rest)?));
}
if let Some(rest) = trimmed
.strip_prefix("persistent")
.filter(|r| r.is_empty() || r.starts_with(|c: char| c.is_whitespace() || c == ','))
{
return Ok(Stmt::Persistent(parse_name_list(rest)?));
}
if trimmed == "return" {
return Ok(Stmt::Return);
}
if let Some((base_var, idx_str, fields, rhs)) = try_split_struct_array_field_assign(trimmed) {
let idx_tokens = tokenize(idx_str)?;
if idx_tokens.is_empty() {
return Err("Expected index expression inside '()'".to_string());
}
let mut idx_pos = 0;
let idx_expr = parse_logical_or(&idx_tokens, &mut idx_pos)?;
if idx_pos != idx_tokens.len() {
return Err("Unexpected token in struct array index expression".to_string());
}
let rhs_tokens = tokenize(rhs)?;
if rhs_tokens.is_empty() {
return Err("Expected expression after '='".to_string());
}
let mut rhs_pos = 0;
let rhs_expr = parse_logical_or(&rhs_tokens, &mut rhs_pos)?;
if rhs_pos != rhs_tokens.len() {
return Err("Unexpected token after expression".to_string());
}
return Ok(Stmt::StructArrayFieldSet(
base_var, idx_expr, fields, rhs_expr,
));
}
if let Some((name, idx_str, rhs)) = try_split_index_assign(trimmed) {
let idx_tokens = tokenize(idx_str)?;
let indices = parse_index_args(&idx_tokens)?;
if indices.len() > 2 {
return Err("Indexed assignment supports at most 2 indices".to_string());
}
let rhs_tokens = tokenize(rhs)?;
if rhs_tokens.is_empty() {
return Err("Expected expression after '='".to_string());
}
let mut rhs_pos = 0;
let value = parse_logical_or(&rhs_tokens, &mut rhs_pos)?;
if rhs_pos != rhs_tokens.len() {
return Err("Unexpected token after expression".to_string());
}
return Ok(Stmt::IndexSet {
name,
indices,
value,
});
}
if let Some((base_var, fields, rhs)) = try_split_field_assign(trimmed) {
let tokens = tokenize(rhs)?;
if tokens.is_empty() {
return Err("Expected expression after '='".to_string());
}
let mut pos = 0;
let rhs_expr = parse_logical_or(&tokens, &mut pos)?;
if pos != tokens.len() {
return Err("Unexpected token after expression".to_string());
}
return Ok(Stmt::FieldSet(base_var, fields, rhs_expr));
}
if let Some((name, idx_str, rhs)) = try_split_cell_assign(trimmed) {
let idx_tokens = tokenize(idx_str)?;
if idx_tokens.is_empty() {
return Err("Expected index expression inside '{}'".to_string());
}
let mut idx_pos = 0;
let idx_expr = parse_logical_or(&idx_tokens, &mut idx_pos)?;
if idx_pos != idx_tokens.len() {
return Err("Unexpected token in cell index expression".to_string());
}
let rhs_tokens = tokenize(rhs)?;
if rhs_tokens.is_empty() {
return Err("Expected expression after '='".to_string());
}
let mut rhs_pos = 0;
let rhs_expr = parse_logical_or(&rhs_tokens, &mut rhs_pos)?;
if rhs_pos != rhs_tokens.len() {
return Err("Unexpected token after expression".to_string());
}
return Ok(Stmt::CellSet(name.to_string(), idx_expr, rhs_expr));
}
if let Some((targets, rhs)) = try_split_multi_assign(trimmed) {
let tokens = tokenize(rhs)?;
if tokens.is_empty() {
return Err("Expected expression after '='".to_string());
}
let mut pos = 0;
let expr = parse_logical_or(&tokens, &mut pos)?;
if pos != tokens.len() {
return Err("Unexpected token after expression".to_string());
}
return Ok(Stmt::MultiAssign { targets, expr });
}
if let Some((name, rhs)) = try_split_assignment(trimmed) {
let tokens = tokenize(rhs)?;
if tokens.is_empty() {
return Err("Expected expression after '='".to_string());
}
let mut pos = 0;
let expr = parse_logical_or(&tokens, &mut pos)?;
if pos != tokens.len() {
return Err("Unexpected token after expression".to_string());
}
return Ok(Stmt::Assign(name.to_string(), expr));
}
let tokens = tokenize(trimmed)?;
if tokens.is_empty() {
return Err("Empty expression".to_string());
}
if let Some(stmt) = try_parse_compound(&tokens)? {
return Ok(stmt);
}
let mut pos = 0;
let expr = parse_logical_or(&tokens, &mut pos)?;
if pos != tokens.len() {
return Err("Unexpected token after expression".to_string());
}
Ok(Stmt::Expr(expr))
}
fn try_parse_compound(tokens: &[Token]) -> Result<Option<Stmt>, String> {
if tokens.len() == 2
&& let Token::Ident(name) = &tokens[1]
{
let op = match &tokens[0] {
Token::PlusPlus => Some(Op::Add),
Token::MinusMinus => Some(Op::Sub),
_ => None,
};
if let Some(op) = op {
let expr = Expr::BinOp(
Box::new(Expr::Var(name.clone())),
op,
Box::new(Expr::Number(1.0)),
);
return Ok(Some(Stmt::Assign(name.clone(), expr)));
}
}
let name = match tokens.first() {
Some(Token::Ident(n)) => n.clone(),
_ => return Ok(None),
};
if tokens.len() < 2 {
return Ok(None);
}
match &tokens[1] {
Token::PlusPlus | Token::MinusMinus if tokens.len() == 2 => {
let op = if matches!(&tokens[1], Token::PlusPlus) {
Op::Add
} else {
Op::Sub
};
let expr = Expr::BinOp(
Box::new(Expr::Var(name.clone())),
op,
Box::new(Expr::Number(1.0)),
);
Ok(Some(Stmt::Assign(name, expr)))
}
Token::PlusEq | Token::MinusEq | Token::StarEq | Token::SlashEq => {
let op = match &tokens[1] {
Token::PlusEq => Op::Add,
Token::MinusEq => Op::Sub,
Token::StarEq => Op::Mul,
Token::SlashEq => Op::Div,
_ => unreachable!(),
};
let rhs_tokens = &tokens[2..];
if rhs_tokens.is_empty() {
let op_str = match op {
Op::Add => "+=",
Op::Sub => "-=",
Op::Mul => "*=",
Op::Div => "/=",
_ => "op=",
};
return Err(format!("Expected expression after '{op_str}'"));
}
let mut pos = 0;
let rhs = parse_logical_or(rhs_tokens, &mut pos)?;
if pos != rhs_tokens.len() {
return Err("Unexpected token after expression".to_string());
}
let expr = Expr::BinOp(Box::new(Expr::Var(name.clone())), op, Box::new(rhs));
Ok(Some(Stmt::Assign(name, expr)))
}
_ => Ok(None),
}
}
pub fn is_partial(input: &str) -> bool {
let mut chars = input.trim_start().chars();
match chars.next() {
Some('+') => !matches!(chars.next(), Some('+')),
Some('-') => !matches!(chars.next(), Some('-')),
Some('*' | '/' | '^' | '<' | '>') => true,
Some('.') => matches!(chars.next(), Some('*' | '/' | '^')),
Some('=') => chars.next() == Some('='),
Some('~') => chars.next() == Some('='),
Some('&') => chars.next() == Some('&'),
Some('|') => chars.next() == Some('|'),
_ => false,
}
}
pub fn split_stmts(input: &str) -> Vec<(&str, bool)> {
let mut separators: Vec<(usize, bool)> = Vec::new();
let mut comment_at = input.len();
let mut in_sq = false;
let mut in_dq = false;
let mut paren_depth: i32 = 0;
let mut bracket_depth: i32 = 0;
let mut brace_depth: i32 = 0;
let chars: Vec<(usize, char)> = input.char_indices().collect();
let mut ci = 0;
while ci < chars.len() {
let (i, c) = chars[ci];
let at_depth0 =
!in_sq && !in_dq && paren_depth == 0 && bracket_depth == 0 && brace_depth == 0;
match c {
'\'' if !in_dq => {
if in_sq {
let next = chars.get(ci + 1).map(|&(_, c)| c);
if next == Some('\'') {
ci += 1; } else {
in_sq = false;
}
} else {
let before = input[..i].trim_end_matches([' ', '\t']);
let is_transpose = before.ends_with(|c: char| {
c.is_alphanumeric()
|| c == '_'
|| c == ')'
|| c == ']'
|| c == '\''
|| c == '.'
});
if !is_transpose {
in_sq = true;
}
}
}
'"' if !in_sq => in_dq = !in_dq,
'(' if !in_sq && !in_dq => paren_depth += 1,
')' if !in_sq && !in_dq && paren_depth > 0 => {
paren_depth -= 1;
}
'[' if !in_sq && !in_dq => bracket_depth += 1,
']' if !in_sq && !in_dq && bracket_depth > 0 => {
bracket_depth -= 1;
}
'{' if !in_sq && !in_dq => brace_depth += 1,
'}' if !in_sq && !in_dq && brace_depth > 0 => {
brace_depth -= 1;
}
'%' | '#' if at_depth0 => {
comment_at = i;
break;
}
';' if at_depth0 => separators.push((i, true)),
',' if at_depth0 => separators.push((i, false)),
_ => {}
}
ci += 1;
}
let content = input[..comment_at].trim_end();
if content.is_empty() {
return Vec::new();
}
let mut result = Vec::new();
let mut start = 0;
for &(sc, silent) in &separators {
if sc >= content.len() {
break;
}
let part = content[start..sc].trim();
if !part.is_empty() {
result.push((part, silent));
}
start = sc + 1;
}
if start <= content.len() {
let last = content[start..].trim();
if !last.is_empty() {
result.push((last, false));
}
}
result
}
pub fn block_depth_delta(line: &str) -> i32 {
let trimmed = line.trim();
if trimmed.starts_with("%{") || trimmed.starts_with("#{") {
let rest = &trimmed[2..];
return if rest.contains("%}") || rest.contains("#}") {
0
} else {
1
};
}
if trimmed.starts_with("%}") || trimmed.starts_with("#}") {
return -1;
}
let stripped = strip_line_comment(line).trim();
match leading_keyword(stripped) {
Some("if") | Some("for") | Some("while") | Some("switch") | Some("do")
| Some("function") | Some("try") => 1,
Some("end") | Some("until") => -1,
_ => 0,
}
}
pub fn is_single_line_block(line: &str) -> bool {
let stripped = strip_line_comment(line).trim();
if !matches!(
leading_keyword(stripped),
Some("if" | "for" | "while" | "switch" | "do")
) {
return false;
}
let parts = split_block_line(stripped);
matches!(
parts.last().map(|s| leading_keyword(s.trim())),
Some(Some("end" | "until"))
)
}
fn strip_block_comments(lines: &[&str]) -> Result<Vec<String>, String> {
let mut result = Vec::with_capacity(lines.len());
let mut in_block = false;
for &line in lines {
let trimmed = line.trim();
if !in_block {
if trimmed.starts_with("%{") || trimmed.starts_with("#{") {
let rest = &trimmed[2..];
if rest.contains("%}") || rest.contains("#}") {
result.push(String::new());
} else {
in_block = true;
result.push(String::new());
}
} else {
result.push(line.to_string());
}
} else {
if trimmed.starts_with("%}") || trimmed.starts_with("#}") {
in_block = false;
}
result.push(String::new());
}
}
if in_block {
Err("Unterminated block comment: missing closing '%}'".to_string())
} else {
Ok(result)
}
}
fn join_line_continuations(input: &str) -> String {
let mut result = String::new();
let mut pending = String::new();
for line in input.lines() {
let stripped = strip_line_comment(line);
let trimmed = stripped.trim_end();
if let Some(before_dots) = trimmed.strip_suffix("...") {
pending.push_str(before_dots);
pending.push(' ');
} else if pending.is_empty() {
result.push_str(line);
result.push('\n');
} else {
pending.push_str(line.trim_start());
result.push_str(&pending);
result.push('\n');
pending.clear();
}
}
if !pending.is_empty() {
result.push_str(pending.trim_end());
}
result
}
fn split_block_line(line: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut current = String::new();
let mut in_sq = false;
let mut in_dq = false;
let mut paren: i32 = 0;
let mut bracket: i32 = 0;
let mut brace: i32 = 0;
for c in line.chars() {
let at_depth0 = !in_sq && !in_dq && paren == 0 && bracket == 0 && brace == 0;
match c {
'\'' if !in_dq => {
in_sq = !in_sq;
current.push(c);
}
'"' if !in_sq => {
in_dq = !in_dq;
current.push(c);
}
'(' if !in_sq && !in_dq => {
paren += 1;
current.push(c);
}
')' if !in_sq && !in_dq => {
if paren > 0 {
paren -= 1;
}
current.push(c);
}
'[' if !in_sq && !in_dq => {
bracket += 1;
current.push(c);
}
']' if !in_sq && !in_dq => {
if bracket > 0 {
bracket -= 1;
}
current.push(c);
}
'{' if !in_sq && !in_dq => {
brace += 1;
current.push(c);
}
'}' if !in_sq && !in_dq => {
if brace > 0 {
brace -= 1;
}
current.push(c);
}
';' if at_depth0 => {
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
parts.push(trimmed);
}
current.clear();
}
_ => current.push(c),
}
}
let last = current.trim().to_string();
if !last.is_empty() {
parts.push(last);
}
parts
}
pub fn parse_stmts(input: &str) -> Result<Vec<(Stmt, bool)>, String> {
let raw_lines: Vec<&str> = input.lines().collect();
let stripped = strip_block_comments(&raw_lines)?;
let stripped_str = stripped.join("\n");
let joined = join_line_continuations(&stripped_str);
let lines: Vec<&str> = joined.lines().collect();
let mut pos = 0;
parse_stmts_from_lines(&lines, &mut pos, &[])
}
fn parse_stmts_from_lines(
lines: &[&str],
pos: &mut usize,
stop_at: &[&str],
) -> Result<Vec<(Stmt, bool)>, String> {
let mut stmts = Vec::new();
while *pos < lines.len() {
let raw = lines[*pos];
let line = strip_line_comment(raw).trim();
if line.is_empty() {
*pos += 1;
continue;
}
if let Some(kw) = leading_keyword(line)
&& stop_at.contains(&kw)
{
return Ok(stmts);
}
if matches!(
leading_keyword(line),
Some("if" | "for" | "while" | "switch" | "do")
) {
let virtual_parts = split_block_line(line);
let last_kw = virtual_parts
.last()
.map(|s| leading_keyword(s.trim()))
.unwrap_or(None);
if matches!(last_kw, Some("end") | Some("until")) {
let virtual_refs: Vec<&str> = virtual_parts.iter().map(|s| s.as_str()).collect();
let mut vpos = 0;
let inner = parse_stmts_from_lines(&virtual_refs, &mut vpos, stop_at)?;
stmts.extend(inner);
*pos += 1;
continue;
}
}
match leading_keyword(line) {
Some("if") => {
let cond_str = line["if".len()..].trim();
if cond_str.is_empty() {
return Err("Expected condition after 'if'".to_string());
}
let cond = parse_condition(cond_str)?;
*pos += 1;
let body = parse_stmts_from_lines(lines, pos, &["elseif", "else", "end"])?;
let mut elseif_branches = Vec::new();
loop {
if *pos >= lines.len() {
return Err(
"Unexpected end of input inside 'if': expected 'end'".to_string()
);
}
let kw_line = strip_line_comment(lines[*pos]).trim();
if leading_keyword(kw_line) == Some("elseif") {
let ei_str = kw_line["elseif".len()..].trim();
if ei_str.is_empty() {
return Err("Expected condition after 'elseif'".to_string());
}
let ei_cond = parse_condition(ei_str)?;
*pos += 1;
let ei_body =
parse_stmts_from_lines(lines, pos, &["elseif", "else", "end"])?;
elseif_branches.push((ei_cond, ei_body));
} else {
break;
}
}
let else_body = if *pos < lines.len()
&& leading_keyword(strip_line_comment(lines[*pos]).trim()) == Some("else")
{
*pos += 1; Some(parse_stmts_from_lines(lines, pos, &["end"])?)
} else {
None
};
expect_end(lines, pos, "if")?;
stmts.push((
Stmt::If {
cond,
body,
elseif_branches,
else_body,
},
false,
));
}
Some("for") => {
let rest = line["for".len()..].trim();
if rest.is_empty() {
return Err("Expected 'var = expr' after 'for'".to_string());
}
let (var, range_expr) = parse_for_header(rest)?;
*pos += 1;
let body = parse_stmts_from_lines(lines, pos, &["end"])?;
expect_end(lines, pos, "for")?;
stmts.push((
Stmt::For {
var,
range_expr,
body,
},
false,
));
}
Some("while") => {
let cond_str = line["while".len()..].trim();
if cond_str.is_empty() {
return Err("Expected condition after 'while'".to_string());
}
let cond = parse_condition(cond_str)?;
*pos += 1;
let body = parse_stmts_from_lines(lines, pos, &["end"])?;
expect_end(lines, pos, "while")?;
stmts.push((Stmt::While { cond, body }, false));
}
Some("break") => {
stmts.push((Stmt::Break, false));
*pos += 1;
}
Some("continue") => {
stmts.push((Stmt::Continue, false));
*pos += 1;
}
Some("switch") => {
let expr_str = line["switch".len()..].trim();
if expr_str.is_empty() {
return Err("Expected expression after 'switch'".to_string());
}
let expr = parse_condition(expr_str)?;
*pos += 1;
#[allow(clippy::type_complexity)]
let mut cases: Vec<(Vec<Expr>, Vec<(Stmt, bool)>)> = Vec::new();
let mut otherwise_body: Option<Vec<(Stmt, bool)>> = None;
loop {
if *pos >= lines.len() {
return Err(
"Unexpected end of input inside 'switch': expected 'end'".to_string()
);
}
let kw_line = strip_line_comment(lines[*pos]).trim();
match leading_keyword(kw_line) {
Some("case") => {
let case_str = kw_line["case".len()..].trim();
if case_str.is_empty() {
return Err("Expected value after 'case'".to_string());
}
let case_expr = parse_condition(case_str)?;
*pos += 1;
let case_body =
parse_stmts_from_lines(lines, pos, &["case", "otherwise", "end"])?;
cases.push((vec![case_expr], case_body));
}
Some("otherwise") => {
*pos += 1;
let ob = parse_stmts_from_lines(lines, pos, &["end"])?;
otherwise_body = Some(ob);
break;
}
Some("end") => break,
_ => {
return Err(format!(
"Expected 'case', 'otherwise', or 'end' in switch block, found: '{kw_line}'"
));
}
}
}
expect_end(lines, pos, "switch")?;
stmts.push((
Stmt::Switch {
expr,
cases,
otherwise_body,
},
false,
));
}
Some("do") => {
*pos += 1;
let body = parse_stmts_from_lines(lines, pos, &["until"])?;
if *pos >= lines.len() {
return Err("Unexpected end of input inside 'do': expected 'until'".to_string());
}
let until_line = strip_line_comment(lines[*pos]).trim();
if leading_keyword(until_line) != Some("until") {
return Err(format!("Expected 'until', found: '{until_line}'"));
}
let cond_str = until_line["until".len()..].trim();
if cond_str.is_empty() {
return Err("Expected condition after 'until'".to_string());
}
let cond = parse_condition(cond_str)?;
*pos += 1;
stmts.push((Stmt::DoUntil { body, cond }, false));
}
Some("function") => {
let header = line["function".len()..].trim();
if header.is_empty() {
return Err("Expected function header after 'function'".to_string());
}
let (name, outputs, params) = parse_function_header(header)?;
*pos += 1;
let body_start = *pos;
let doc = {
let mut doc_lines: Vec<String> = Vec::new();
let mut scan = body_start;
while scan < lines.len() {
let raw = lines[scan].trim();
if raw.starts_with('%') || raw.starts_with('#') {
let stripped = raw.trim_start_matches(['%', '#']);
let text = stripped
.strip_prefix(' ')
.unwrap_or(stripped)
.trim_end()
.to_string();
doc_lines.push(text);
scan += 1;
} else {
break;
}
}
if doc_lines.is_empty() {
None
} else {
Some(doc_lines.join("\n"))
}
};
let mut depth: i32 = 1;
while *pos < lines.len() && depth > 0 {
let l = strip_line_comment(lines[*pos]).trim();
let delta = if is_single_line_block(l) {
0
} else {
block_depth_delta(l)
};
depth += delta;
if depth == 0 {
break;
}
*pos += 1;
}
if depth != 0 {
return Err(format!(
"Unexpected end of input: expected 'end' to close 'function {name}'"
));
}
let body_source = lines[body_start..*pos].join("\n");
*pos += 1; stmts.push((
Stmt::FunctionDef {
name,
outputs,
params,
body_source,
doc,
},
false,
));
}
Some("return") => {
stmts.push((Stmt::Return, false));
*pos += 1;
}
Some("try") => {
*pos += 1;
let try_body = parse_stmts_from_lines(lines, pos, &["catch", "end"])?;
if *pos >= lines.len() {
return Err(
"Unexpected end of input inside 'try': expected 'catch' or 'end'"
.to_string(),
);
}
let kw_line = strip_line_comment(lines[*pos]).trim();
let (catch_var, catch_body) = if leading_keyword(kw_line) == Some("catch") {
let catch_rest = kw_line["catch".len()..].trim();
let catch_var = if catch_rest.is_empty() {
None
} else if is_valid_ident(catch_rest) {
Some(catch_rest.to_string())
} else {
return Err(format!(
"Expected identifier after 'catch', got '{catch_rest}'"
));
};
*pos += 1;
let catch_body = parse_stmts_from_lines(lines, pos, &["end"])?;
(catch_var, catch_body)
} else {
(None, vec![])
};
expect_end(lines, pos, "try")?;
stmts.push((
Stmt::TryCatch {
try_body,
catch_var,
catch_body,
},
false,
));
}
Some(kw @ ("end" | "else" | "elseif" | "case" | "otherwise" | "until" | "catch")) => {
return Err(format!("Unexpected '{kw}' without matching block opener"));
}
_ => {
if line == "clear" {
stmts.push((Stmt::Expr(Expr::Call("clear".to_string(), vec![])), false));
*pos += 1;
continue;
}
if let Some(rest) = line
.strip_prefix("clear")
.filter(|r| r.starts_with(|c: char| c.is_whitespace()))
{
let names: Vec<Expr> = rest
.split_whitespace()
.map(|n| Expr::StrLiteral(n.to_string()))
.collect();
stmts.push((Stmt::Expr(Expr::Call("clear".to_string(), names)), false));
*pos += 1;
continue;
}
if line == "format"
|| line
.strip_prefix("format")
.is_some_and(|r| r.starts_with(|c: char| c.is_whitespace()))
{
let arg = line
.strip_prefix("format")
.map(str::trim)
.unwrap_or("")
.to_string();
let args = if arg.is_empty() {
vec![]
} else {
vec![Expr::StrLiteral(arg)]
};
stmts.push((Stmt::Expr(Expr::Call("format".to_string(), args)), true));
*pos += 1;
continue;
}
for (stmt_str, silent) in split_stmts(raw) {
stmts.push((parse(stmt_str)?, silent));
}
*pos += 1;
}
}
}
Ok(stmts)
}
fn expect_end(lines: &[&str], pos: &mut usize, opener: &str) -> Result<(), String> {
if *pos >= lines.len() {
return Err(format!(
"Unexpected end of input: expected 'end' to close '{opener}'"
));
}
let kw_line = strip_line_comment(lines[*pos]).trim();
if leading_keyword(kw_line) != Some("end") {
return Err(format!(
"Expected 'end' to close '{opener}', found '{kw_line}'"
));
}
*pos += 1;
Ok(())
}
fn strip_line_comment(line: &str) -> &str {
let mut in_sq = false;
let mut in_dq = false;
for (i, c) in line.char_indices() {
match c {
'\'' if !in_dq => in_sq = !in_sq,
'"' if !in_sq => in_dq = !in_dq,
'%' | '#' if !in_sq && !in_dq => return &line[..i],
_ => {}
}
}
line
}
fn leading_keyword(line: &str) -> Option<&str> {
let end = line
.find(|c: char| !c.is_alphanumeric() && c != '_')
.unwrap_or(line.len());
let word = &line[..end];
match word {
"if" | "elseif" | "else" | "end" | "for" | "while" | "break" | "continue" | "switch"
| "case" | "otherwise" | "do" | "until" | "function" | "return" | "try" | "catch" => {
Some(word)
}
_ => None,
}
}
fn parse_function_header(header: &str) -> Result<(String, Vec<String>, Vec<String>), String> {
if let Some(eq_pos) = header.find('=')
&& !header[eq_pos + 1..].starts_with('=')
{
let lhs = header[..eq_pos].trim();
let rhs = header[eq_pos + 1..].trim();
let outputs = parse_output_list(lhs)?;
let (name, params) = parse_func_name_params(rhs)?;
return Ok((name, outputs, params));
}
let (name, params) = parse_func_name_params(header.trim())?;
Ok((name, vec![], params))
}
fn parse_output_list(lhs: &str) -> Result<Vec<String>, String> {
let lhs = lhs.trim();
if lhs.starts_with('[') && lhs.ends_with(']') {
let inner = &lhs[1..lhs.len() - 1];
inner
.split(',')
.map(|s| {
let s = s.trim();
if is_valid_ident(s) {
Ok(s.to_string())
} else {
Err(format!("Invalid output variable name: '{s}'"))
}
})
.collect()
} else if is_valid_ident(lhs) {
Ok(vec![lhs.to_string()])
} else {
Err(format!("Invalid function output list: '{lhs}'"))
}
}
fn parse_func_name_params(s: &str) -> Result<(String, Vec<String>), String> {
let s = s.trim();
if let Some(paren_pos) = s.find('(') {
let name = s[..paren_pos].trim();
if !is_valid_ident(name) {
return Err(format!("Invalid function name: '{name}'"));
}
let rest = s[paren_pos + 1..].trim();
if !rest.ends_with(')') {
return Err(format!("Expected ')' in function header: '{s}'"));
}
let params_str = rest[..rest.len() - 1].trim();
let params = if params_str.is_empty() {
vec![]
} else {
params_str
.split(',')
.map(|p| {
let p = p.trim();
if is_valid_ident(p) {
Ok(p.to_string())
} else {
Err(format!("Invalid parameter name: '{p}'"))
}
})
.collect::<Result<Vec<_>, _>>()?
};
Ok((name.to_string(), params))
} else {
if !is_valid_ident(s) {
return Err(format!("Invalid function name: '{s}'"));
}
Ok((s.to_string(), vec![]))
}
}
fn parse_condition(cond_str: &str) -> Result<Expr, String> {
match parse(cond_str)? {
Stmt::Expr(e) => Ok(e),
Stmt::Assign(_, _) => Err("Expected condition expression, found assignment".to_string()),
_ => Err("Expected condition expression".to_string()),
}
}
fn parse_for_header(rest: &str) -> Result<(String, Expr), String> {
match parse(rest)? {
Stmt::Assign(var, expr) => Ok((var, expr)),
_ => Err(format!(
"Expected 'variable = expression' in 'for' header, found: '{rest}'"
)),
}
}
fn try_split_multi_assign(input: &str) -> Option<(Vec<String>, &str)> {
let trimmed = input.trim();
if !trimmed.starts_with('[') {
return None;
}
let close = trimmed.find(']')?;
let rest = trimmed[close + 1..].trim();
if !rest.starts_with('=') || rest.starts_with("==") {
return None;
}
let rhs = rest[1..].trim();
let inner = trimmed[1..close].trim();
if inner.is_empty() {
return None;
}
let targets: Vec<String> = inner.split(',').map(|s| s.trim().to_string()).collect();
for t in &targets {
if t != "~" && !is_valid_ident(t) {
return None;
}
}
Some((targets, rhs))
}
fn try_split_cell_assign(input: &str) -> Option<(&str, &str, &str)> {
let trimmed = input.trim();
let brace_pos = trimmed.find('{')?;
let name = trimmed[..brace_pos].trim();
if !is_valid_ident(name) {
return None;
}
let after_open = &trimmed[brace_pos + 1..];
let close_pos = after_open.find('}')?;
let idx_str = after_open[..close_pos].trim();
let after_close = after_open[close_pos + 1..].trim();
if !after_close.starts_with('=') || after_close.starts_with("==") {
return None;
}
let rhs = after_close[1..].trim();
Some((name, idx_str, rhs))
}
fn try_split_index_assign(input: &str) -> Option<(String, &str, &str)> {
let trimmed = input.trim();
let bytes = trimmed.as_bytes();
let mut i = 0;
if i >= bytes.len() || !(bytes[i].is_ascii_alphabetic() || bytes[i] == b'_') {
return None;
}
let name_start = i;
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
i += 1;
}
let name = trimmed[name_start..i].to_string();
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if i >= bytes.len() || bytes[i] != b'(' {
return None;
}
i += 1;
let idx_start = i;
let mut depth = 1usize;
while i < bytes.len() && depth > 0 {
match bytes[i] {
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
_ => {}
}
i += 1;
}
if depth != 0 {
return None;
}
let idx_str = trimmed[idx_start..i - 1].trim();
let rest = trimmed[i..].trim_start();
if rest.starts_with('.') {
return None;
}
if !rest.starts_with('=') || rest.starts_with("==") {
return None;
}
let rhs = rest[1..].trim();
if rhs.is_empty() {
return None;
}
Some((name, idx_str, rhs))
}
fn parse_index_args(tokens: &[Token]) -> Result<Vec<Expr>, String> {
if tokens.is_empty() {
return Err("Expected index expression inside '()'".to_string());
}
let mut pos = 0;
let mut args = Vec::new();
loop {
args.push(parse_call_arg(tokens, &mut pos)?);
match tokens.get(pos) {
Some(Token::Comma) => {
pos += 1;
}
None => break,
Some(_) => return Err("Unexpected token in index expression".to_string()),
}
}
Ok(args)
}
fn try_split_assignment(input: &str) -> Option<(&str, &str)> {
let trimmed = input.trim();
let eq_pos = trimmed.find('=')?;
if trimmed[eq_pos + 1..].starts_with('=') {
return None;
}
let lhs = trimmed[..eq_pos].trim();
let rhs = trimmed[eq_pos + 1..].trim();
if is_valid_ident(lhs) {
Some((lhs, rhs))
} else {
None
}
}
fn is_valid_ident(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_alphabetic() || c == '_' => chars.all(|c| c.is_alphanumeric() || c == '_'),
_ => false,
}
}
fn parse_call_arg(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
if matches!(tokens.get(*pos), Some(Token::Colon)) {
*pos += 1;
return Ok(Expr::Colon);
}
parse_logical_or(tokens, pos)
}
fn parse_logical_or(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
let mut left = parse_logical_and(tokens, pos)?;
while matches!(tokens.get(*pos), Some(Token::PipePipe)) {
*pos += 1;
let right = parse_logical_and(tokens, pos)?;
left = Expr::BinOp(Box::new(left), Op::Or, Box::new(right));
}
Ok(left)
}
fn parse_logical_and(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
let mut left = parse_elem_or(tokens, pos)?;
while matches!(tokens.get(*pos), Some(Token::AmpAmp)) {
*pos += 1;
let right = parse_elem_or(tokens, pos)?;
left = Expr::BinOp(Box::new(left), Op::And, Box::new(right));
}
Ok(left)
}
fn parse_elem_or(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
let mut left = parse_elem_and(tokens, pos)?;
while matches!(tokens.get(*pos), Some(Token::Pipe)) {
*pos += 1;
let right = parse_elem_and(tokens, pos)?;
left = Expr::BinOp(Box::new(left), Op::ElemOr, Box::new(right));
}
Ok(left)
}
fn parse_elem_and(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
let mut left = parse_comparison(tokens, pos)?;
while matches!(tokens.get(*pos), Some(Token::Amp)) {
*pos += 1;
let right = parse_comparison(tokens, pos)?;
left = Expr::BinOp(Box::new(left), Op::ElemAnd, Box::new(right));
}
Ok(left)
}
fn parse_comparison(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
let left = parse_range(tokens, pos)?;
let op = match tokens.get(*pos) {
Some(Token::EqEq) => Op::Eq,
Some(Token::NotEq) => Op::NotEq,
Some(Token::Lt) => Op::Lt,
Some(Token::Gt) => Op::Gt,
Some(Token::LtEq) => Op::LtEq,
Some(Token::GtEq) => Op::GtEq,
_ => return Ok(left),
};
*pos += 1;
let right = parse_range(tokens, pos)?;
Ok(Expr::BinOp(Box::new(left), op, Box::new(right)))
}
fn parse_range(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
let start = parse_expr(tokens, pos)?;
if !matches!(tokens.get(*pos), Some(Token::Colon)) {
return Ok(start);
}
*pos += 1;
let second = parse_expr(tokens, pos)?;
if !matches!(tokens.get(*pos), Some(Token::Colon)) {
return Ok(Expr::Range(Box::new(start), None, Box::new(second)));
}
*pos += 1;
let third = parse_expr(tokens, pos)?;
Ok(Expr::Range(
Box::new(start),
Some(Box::new(second)),
Box::new(third),
))
}
fn parse_expr(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
let mut left = parse_term(tokens, pos)?;
while *pos < tokens.len() {
match &tokens[*pos] {
Token::Plus => {
*pos += 1;
let right = parse_term(tokens, pos)?;
left = Expr::BinOp(Box::new(left), Op::Add, Box::new(right));
}
Token::Minus => {
*pos += 1;
let right = parse_term(tokens, pos)?;
left = Expr::BinOp(Box::new(left), Op::Sub, Box::new(right));
}
_ => break,
}
}
Ok(left)
}
fn parse_term(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
let mut left = parse_power(tokens, pos)?;
while *pos < tokens.len() {
match &tokens[*pos] {
Token::Star => {
*pos += 1;
let right = parse_power(tokens, pos)?;
left = Expr::BinOp(Box::new(left), Op::Mul, Box::new(right));
}
Token::Slash => {
*pos += 1;
let right = parse_power(tokens, pos)?;
left = Expr::BinOp(Box::new(left), Op::Div, Box::new(right));
}
Token::DotStar => {
*pos += 1;
let right = parse_power(tokens, pos)?;
left = Expr::BinOp(Box::new(left), Op::ElemMul, Box::new(right));
}
Token::DotSlash => {
*pos += 1;
let right = parse_power(tokens, pos)?;
left = Expr::BinOp(Box::new(left), Op::ElemDiv, Box::new(right));
}
Token::Backslash => {
*pos += 1;
let right = parse_power(tokens, pos)?;
left = Expr::BinOp(Box::new(left), Op::LDiv, Box::new(right));
}
Token::LParen => {
let right = parse_power(tokens, pos)?;
left = Expr::BinOp(Box::new(left), Op::Mul, Box::new(right));
}
_ => break,
}
}
Ok(left)
}
fn parse_power(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
let base = parse_unary(tokens, pos)?;
if *pos < tokens.len() {
match &tokens[*pos] {
Token::Caret | Token::StarStar => {
*pos += 1;
let exp = parse_power(tokens, pos)?;
return Ok(Expr::BinOp(Box::new(base), Op::Pow, Box::new(exp)));
}
Token::DotCaret => {
*pos += 1;
let exp = parse_power(tokens, pos)?;
return Ok(Expr::BinOp(Box::new(base), Op::ElemPow, Box::new(exp)));
}
_ => {}
}
}
Ok(base)
}
fn parse_unary(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
if *pos < tokens.len() {
match &tokens[*pos] {
Token::Plus => {
*pos += 1;
return parse_unary(tokens, pos); }
Token::Minus => {
*pos += 1;
let expr = parse_unary(tokens, pos)?;
return Ok(Expr::UnaryMinus(Box::new(expr)));
}
Token::Tilde => {
*pos += 1;
let expr = parse_unary(tokens, pos)?;
return Ok(Expr::UnaryNot(Box::new(expr)));
}
_ => {}
}
}
parse_primary(tokens, pos)
}
fn parse_primary(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
if *pos >= tokens.len() {
return Err("Unexpected end of expression".to_string());
}
let mut expr = match &tokens[*pos] {
Token::Number(n) => {
let n = *n;
*pos += 1;
Expr::Number(n)
}
Token::LBrace => {
*pos += 1;
let mut elems = Vec::new();
loop {
match tokens.get(*pos) {
None => return Err("Expected '}'".to_string()),
Some(Token::RBrace) => {
*pos += 1;
break;
}
Some(Token::Comma) => {
*pos += 1;
}
_ => {
elems.push(parse_logical_or(tokens, pos)?);
}
}
}
Expr::CellLiteral(elems)
}
Token::Ident(name) => {
let name = name.clone();
*pos += 1;
if *pos < tokens.len()
&& let Token::LBrace = &tokens[*pos]
{
*pos += 1;
let idx = parse_logical_or(tokens, pos)?;
if *pos >= tokens.len() {
return Err("Expected '}'".to_string());
}
match &tokens[*pos] {
Token::RBrace => {
*pos += 1;
Expr::CellIndex(Box::new(Expr::Var(name)), Box::new(idx))
}
_ => return Err("Expected '}'".to_string()),
}
} else if *pos < tokens.len()
&& let Token::LParen = &tokens[*pos]
{
*pos += 1;
let args = if *pos < tokens.len() {
if let Token::RParen = &tokens[*pos] {
vec![]
} else {
let mut list = vec![parse_call_arg(tokens, pos)?];
while *pos < tokens.len() {
if let Token::Comma = &tokens[*pos] {
*pos += 1;
list.push(parse_call_arg(tokens, pos)?);
} else {
break;
}
}
list
}
} else {
return Err("Expected closing ')'".to_string());
};
if *pos >= tokens.len() {
return Err("Expected closing ')'".to_string());
}
match &tokens[*pos] {
Token::RParen => {
*pos += 1;
Expr::Call(name, args)
}
_ => return Err("Expected closing ')'".to_string()),
}
} else {
match name.as_str() {
"pi" => Expr::Number(std::f64::consts::PI),
"e" => Expr::Var("e".to_string()),
"nan" | "NaN" => Expr::Number(f64::NAN),
"inf" | "Inf" => Expr::Number(f64::INFINITY),
_ => Expr::Var(name),
}
}
}
Token::LParen => {
*pos += 1;
let inner = parse_logical_or(tokens, pos)?;
if *pos >= tokens.len() {
return Err("Expected closing ')'".to_string());
}
match &tokens[*pos] {
Token::RParen => {
*pos += 1;
inner
}
_ => return Err("Expected closing ')'".to_string()),
}
}
Token::LBracket => {
*pos += 1;
parse_matrix(tokens, pos)?
}
Token::Str(s) => {
let s = s.clone();
*pos += 1;
Expr::StrLiteral(s)
}
Token::StringObj(s) => {
let s = s.clone();
*pos += 1;
Expr::StringObjLiteral(s)
}
Token::At => {
*pos += 1;
if let Some(Token::Ident(name)) = tokens.get(*pos) {
let name = name.clone();
*pos += 1;
return Ok(Expr::FuncHandle(name));
}
if !matches!(tokens.get(*pos), Some(Token::LParen)) {
return Err("Expected '(' or identifier after '@'".to_string());
}
*pos += 1;
let mut params = Vec::new();
loop {
match tokens.get(*pos) {
Some(Token::RParen) => {
*pos += 1;
break;
}
Some(Token::Ident(name)) => {
params.push(name.clone());
*pos += 1;
if matches!(tokens.get(*pos), Some(Token::Comma)) {
*pos += 1;
}
}
None => return Err("Expected ')' in lambda parameter list".to_string()),
_ => return Err("Expected parameter name in lambda".to_string()),
}
}
let body = parse_logical_or(tokens, pos)?;
let source = format!("@({}) {}", params.join(", "), expr_to_string(&body));
Expr::Lambda {
params,
body: Box::new(body),
source,
}
}
_ => {
return Err(
"Expected number, function, variable, string, '-', '[', '@', or '('".to_string(),
);
}
};
loop {
match tokens.get(*pos) {
Some(Token::Dot) => {
*pos += 1;
match tokens.get(*pos) {
Some(Token::Ident(field)) => {
let field = field.clone();
*pos += 1;
expr = Expr::FieldGet(Box::new(expr), field);
}
_ => return Err("Expected field name after '.'".to_string()),
}
}
Some(Token::LParen) => {
if let Some(segs) = field_chain_segments(&expr)
&& segs.len() >= 2
{
*pos += 1;
let args = if matches!(tokens.get(*pos), Some(Token::RParen)) {
vec![]
} else {
let mut list = vec![parse_call_arg(tokens, pos)?];
while matches!(tokens.get(*pos), Some(Token::Comma)) {
*pos += 1;
list.push(parse_call_arg(tokens, pos)?);
}
list
};
if !matches!(tokens.get(*pos), Some(Token::RParen)) {
return Err("Expected closing ')'".to_string());
}
*pos += 1;
expr = Expr::DotCall(segs, args);
} else {
break;
}
}
Some(Token::Apostrophe) => {
*pos += 1;
expr = Expr::Transpose(Box::new(expr));
}
Some(Token::DotApostrophe) => {
*pos += 1;
expr = Expr::PlainTranspose(Box::new(expr));
}
_ => break,
}
}
Ok(expr)
}
fn field_chain_segments(e: &Expr) -> Option<Vec<String>> {
match e {
Expr::Var(name) => Some(vec![name.clone()]),
Expr::FieldGet(inner, field) => {
let mut segs = field_chain_segments(inner)?;
segs.push(field.clone());
Some(segs)
}
_ => None,
}
}
fn parse_matrix(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
if matches!(tokens.get(*pos), Some(Token::RBracket)) {
*pos += 1;
return Ok(Expr::Matrix(vec![]));
}
let mut rows: Vec<Vec<Expr>> = Vec::new();
let mut current_row: Vec<Expr> = Vec::new();
loop {
match tokens.get(*pos) {
None => return Err("Expected ']'".to_string()),
Some(Token::RBracket) => {
*pos += 1;
if !current_row.is_empty() {
rows.push(current_row);
}
break;
}
Some(Token::Semicolon) => {
*pos += 1;
if !current_row.is_empty() {
rows.push(std::mem::take(&mut current_row));
}
}
Some(Token::Comma) => {
*pos += 1;
}
_ => {
current_row.push(parse_logical_or(tokens, pos)?);
}
}
}
Ok(Expr::Matrix(rows))
}
#[cfg(test)]
#[path = "parser_tests.rs"]
mod tests;