use crate::context::EvalContext; use crate::interpreter::evaluate; use crate::lexer::tokenize; use crate::parser::parse; use crate::span::Span; use crate::types::CalcResult; use crate::variables::aggregators; /// Evaluate a single line of input and return the result. pub fn eval_line(input: &str, ctx: &mut EvalContext) -> CalcResult { let trimmed = input.trim(); if trimmed.is_empty() { return CalcResult::empty(Span::new(0, 0)); } // Detect headings (# Title) and aggregator keywords (sum, total, etc.) // before tokenizing — the lexer misinterprets `#` as a line reference prefix. if aggregators::is_heading(trimmed) || aggregators::detect_aggregator(trimmed).is_some() { return CalcResult::non_calculable(Span::new(0, trimmed.len())); } let tokens = tokenize(trimmed); // Check for text-only or comment-only lines let has_expr = tokens.iter().any(|t| { !matches!( t.kind, crate::token::TokenKind::Text(_) | crate::token::TokenKind::Comment(_) | crate::token::TokenKind::Eof ) }); if !has_expr { return CalcResult::non_calculable(Span::new(0, trimmed.len())); } match parse(tokens) { Ok(expr) => evaluate(&expr, ctx), Err(e) => CalcResult::error(&e.message, e.span), } } /// Evaluate multiple lines of input, sharing context across lines. /// Variable assignments on earlier lines are visible to later lines. pub fn eval_sheet(lines: &[&str], ctx: &mut EvalContext) -> Vec { lines.iter().map(|line| eval_line(line, ctx)).collect() }