Skip to main content

formualizer_eval/
interpreter.rs

1use crate::{
2    CellRef,
3    broadcast::{broadcast_shape, project_index},
4    coercion,
5    traits::{ArgumentHandle, DefaultFunctionContext, EvaluationContext},
6};
7use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
8use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
9
10use crate::engine::arena::{AstNodeData, AstNodeId, DataStore};
11use crate::engine::sheet_registry::SheetRegistry;
12
13// no Arc needed here after cache removal
14
15pub struct Interpreter<'a> {
16    pub context: &'a dyn EvaluationContext,
17    current_sheet: &'a str,
18    current_cell: Option<crate::CellRef>,
19}
20
21impl<'a> Interpreter<'a> {
22    pub fn new(context: &'a dyn EvaluationContext, current_sheet: &'a str) -> Self {
23        Self {
24            context,
25            current_sheet,
26            current_cell: None,
27        }
28    }
29
30    pub fn new_with_cell(
31        context: &'a dyn EvaluationContext,
32        current_sheet: &'a str,
33        cell: crate::CellRef,
34    ) -> Self {
35        Self {
36            context,
37            current_sheet,
38            current_cell: Some(cell),
39        }
40    }
41
42    pub fn current_sheet(&self) -> &'a str {
43        self.current_sheet
44    }
45
46    pub fn resolve_range_view<'c>(
47        &'c self,
48        reference: &ReferenceType,
49        current_sheet: &str,
50    ) -> Result<crate::engine::range_view::RangeView<'c>, ExcelError> {
51        self.context.resolve_range_view(reference, current_sheet)
52    }
53
54    /// Evaluate an AST node in a reference context and return a ReferenceType.
55    /// This is used for range combinators (e.g., ":"), by-ref argument flows,
56    /// and spill planning. Functions that can return references must set
57    /// `FnCaps::RETURNS_REFERENCE` and override `eval_reference`.
58    pub fn evaluate_ast_as_reference(&self, node: &ASTNode) -> Result<ReferenceType, ExcelError> {
59        match &node.node_type {
60            ASTNodeType::Reference { reference, .. } => Ok(reference.clone()),
61            ASTNodeType::Function { name, args } => {
62                if let Some(fun) = self.context.get_function("", name) {
63                    // Build handles; allow function to decide reference semantics
64                    let handles: Vec<ArgumentHandle> =
65                        args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
66                    let fctx = DefaultFunctionContext::new_with_sheet(
67                        self.context,
68                        None,
69                        self.current_sheet,
70                    );
71                    if let Some(res) = fun.eval_reference(&handles, &fctx) {
72                        res
73                    } else {
74                        Err(ExcelError::new(ExcelErrorKind::Ref)
75                            .with_message("Function does not return a reference"))
76                    }
77                } else {
78                    Err(ExcelError::new(ExcelErrorKind::Name)
79                        .with_message(format!("Unknown function: {name}")))
80                }
81            }
82            ASTNodeType::BinaryOp { op, left, right } if op == ":" => {
83                let lref = self.evaluate_ast_as_reference(left)?;
84                let rref = self.evaluate_ast_as_reference(right)?;
85                crate::reference::combine_references(&lref, &rref)
86            }
87            ASTNodeType::Array(_)
88            | ASTNodeType::UnaryOp { .. }
89            | ASTNodeType::BinaryOp { .. }
90            | ASTNodeType::Literal(_) => Err(ExcelError::new(ExcelErrorKind::Ref)
91                .with_message("Expression cannot be used as a reference")),
92        }
93    }
94
95    pub(crate) fn evaluate_arena_ast_as_reference(
96        &self,
97        node_id: AstNodeId,
98        data_store: &DataStore,
99        sheet_registry: &SheetRegistry,
100    ) -> Result<ReferenceType, ExcelError> {
101        let node = data_store.get_node(node_id).ok_or_else(|| {
102            ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
103        })?;
104
105        match node {
106            AstNodeData::Reference { ref_type, .. } => {
107                Ok(data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry))
108            }
109            AstNodeData::Function { name_id, .. } => {
110                let name = data_store.resolve_ast_string(*name_id);
111                let fun = self.context.get_function("", name).ok_or_else(|| {
112                    ExcelError::new(ExcelErrorKind::Name)
113                        .with_message(format!("Unknown function: {name}"))
114                })?;
115
116                let args = data_store.get_args(node_id).ok_or_else(|| {
117                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
118                })?;
119
120                let handles: Vec<ArgumentHandle> = args
121                    .iter()
122                    .copied()
123                    .map(|arg_id| {
124                        ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry)
125                    })
126                    .collect();
127
128                let fctx =
129                    DefaultFunctionContext::new_with_sheet(self.context, None, self.current_sheet);
130
131                fun.eval_reference(&handles, &fctx).ok_or_else(|| {
132                    ExcelError::new(ExcelErrorKind::Ref)
133                        .with_message("Function does not return a reference")
134                })?
135            }
136            AstNodeData::BinaryOp {
137                op_id,
138                left_id,
139                right_id,
140            } => {
141                let op = data_store.resolve_ast_string(*op_id);
142                if op != ":" {
143                    return Err(ExcelError::new(ExcelErrorKind::Ref)
144                        .with_message("Expression cannot be used as a reference"));
145                }
146                let lref =
147                    self.evaluate_arena_ast_as_reference(*left_id, data_store, sheet_registry)?;
148                let rref =
149                    self.evaluate_arena_ast_as_reference(*right_id, data_store, sheet_registry)?;
150                crate::reference::combine_references(&lref, &rref)
151            }
152            _ => Err(ExcelError::new(ExcelErrorKind::Ref)
153                .with_message("Expression cannot be used as a reference")),
154        }
155    }
156
157    /* ===================  public  =================== */
158    pub fn evaluate_ast(&self, node: &ASTNode) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
159        self.evaluate_ast_uncached(node)
160    }
161
162    pub(crate) fn evaluate_arena_ast(
163        &self,
164        node_id: AstNodeId,
165        data_store: &DataStore,
166        sheet_registry: &SheetRegistry,
167    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
168        let node = data_store.get_node(node_id).ok_or_else(|| {
169            ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
170        })?;
171
172        match node {
173            AstNodeData::Literal(vref) => Ok(crate::traits::CalcValue::Scalar(
174                data_store.retrieve_value(*vref),
175            )),
176            AstNodeData::Reference { ref_type, .. } => {
177                let reference =
178                    data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry);
179                self.eval_reference_to_calc(&reference)
180            }
181            AstNodeData::UnaryOp { op_id, expr_id } => {
182                let expr = self.evaluate_arena_ast(*expr_id, data_store, sheet_registry)?;
183
184                let op = data_store.resolve_ast_string(*op_id);
185                // For now, materialize for operators. Future: virtual range ops.
186                let v = expr.into_literal();
187                match v {
188                    LiteralValue::Array(arr) => self
189                        .map_array(arr, |cell| self.eval_unary_scalar(op, cell))
190                        .map(crate::traits::CalcValue::Scalar),
191                    other => self
192                        .eval_unary_scalar(op, other)
193                        .map(crate::traits::CalcValue::Scalar),
194                }
195            }
196            AstNodeData::BinaryOp {
197                op_id,
198                left_id,
199                right_id,
200            } => {
201                let op = data_store.resolve_ast_string(*op_id);
202                if op == ":" {
203                    let lref =
204                        self.evaluate_arena_ast_as_reference(*left_id, data_store, sheet_registry)?;
205                    let rref = self.evaluate_arena_ast_as_reference(
206                        *right_id,
207                        data_store,
208                        sheet_registry,
209                    )?;
210                    return match crate::reference::combine_references(&lref, &rref) {
211                        Ok(_r) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
212                            ExcelError::new(ExcelErrorKind::Ref).with_message(
213                                "Reference produced by ':' cannot be used directly as a value",
214                            ),
215                        ))),
216                        Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
217                    };
218                }
219
220                let left = self
221                    .evaluate_arena_ast(*left_id, data_store, sheet_registry)?
222                    .into_literal();
223                let right = self
224                    .evaluate_arena_ast(*right_id, data_store, sheet_registry)?
225                    .into_literal();
226
227                if matches!(op, "=" | "<>" | ">" | "<" | ">=" | "<=") {
228                    return self
229                        .compare(op, left, right)
230                        .map(crate::traits::CalcValue::Scalar);
231                }
232
233                match op {
234                    "+" => self
235                        .numeric_binary(left, right, |a, b| a + b)
236                        .map(crate::traits::CalcValue::Scalar),
237                    "-" => self
238                        .numeric_binary(left, right, |a, b| a - b)
239                        .map(crate::traits::CalcValue::Scalar),
240                    "*" => self
241                        .numeric_binary(left, right, |a, b| a * b)
242                        .map(crate::traits::CalcValue::Scalar),
243                    "/" => self
244                        .divide(left, right)
245                        .map(crate::traits::CalcValue::Scalar),
246                    "^" => self
247                        .power(left, right)
248                        .map(crate::traits::CalcValue::Scalar),
249                    "&" => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
250                        format!(
251                            "{}{}",
252                            crate::coercion::to_text_invariant(&left),
253                            crate::coercion::to_text_invariant(&right)
254                        ),
255                    ))),
256                    _ => Err(ExcelError::new(ExcelErrorKind::NImpl)
257                        .with_message(format!("Binary op '{op}'"))),
258                }
259            }
260            AstNodeData::Array { .. } => {
261                let (rows, cols, elements) =
262                    data_store.get_array_elems(node_id).ok_or_else(|| {
263                        ExcelError::new(ExcelErrorKind::Value).with_message("Invalid array")
264                    })?;
265
266                let rows_usize = rows as usize;
267                let cols_usize = cols as usize;
268                let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows_usize);
269                for r in 0..rows_usize {
270                    let mut row = Vec::with_capacity(cols_usize);
271                    for c in 0..cols_usize {
272                        let idx = r * cols_usize + c;
273                        if let Some(&elem_id) = elements.get(idx) {
274                            row.push(
275                                self.evaluate_arena_ast(elem_id, data_store, sheet_registry)?
276                                    .into_literal(),
277                            );
278                        }
279                    }
280                    out.push(row);
281                }
282
283                Ok(crate::traits::CalcValue::Range(
284                    crate::engine::range_view::RangeView::from_owned_rows(
285                        out,
286                        self.context.date_system(),
287                    ),
288                ))
289            }
290            AstNodeData::Function { name_id, .. } => {
291                let name = data_store.resolve_ast_string(*name_id);
292                let fun = self.context.get_function("", name).ok_or_else(|| {
293                    ExcelError::new(ExcelErrorKind::Name)
294                        .with_message(format!("Unknown function: {name}"))
295                })?;
296
297                let args = data_store.get_args(node_id).ok_or_else(|| {
298                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
299                })?;
300
301                let handles: Vec<ArgumentHandle> = args
302                    .iter()
303                    .copied()
304                    .map(|arg_id| {
305                        ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry)
306                    })
307                    .collect();
308
309                let fctx = DefaultFunctionContext::new_with_sheet(
310                    self.context,
311                    self.current_cell,
312                    self.current_sheet,
313                );
314
315                fun.dispatch(&handles, &fctx)
316            }
317        }
318    }
319
320    fn evaluate_ast_uncached(
321        &self,
322        node: &ASTNode,
323    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
324        // Plan-aware evaluation: build a plan for this node and execute accordingly.
325        // Provide the planner with a lightweight range-dimension probe and function lookup
326        // so it can select chunked reduction and arg-parallel strategies where appropriate.
327        let current_sheet = self.current_sheet.to_string();
328        let range_probe = |reference: &ReferenceType| -> Option<(u32, u32)> {
329            // Mirror Engine::resolve_range_storage bound normalization without materialising
330            use formualizer_parse::parser::ReferenceType as RT;
331            match reference {
332                RT::Range {
333                    sheet,
334                    start_row,
335                    start_col,
336                    end_row,
337                    end_col,
338                    ..
339                } => {
340                    let sheet_name = sheet.as_deref().unwrap_or(&current_sheet);
341                    // Start with provided values, fill None from used-region or sheet bounds.
342                    let mut sr = *start_row;
343                    let mut sc = *start_col;
344                    let mut er = *end_row;
345                    let mut ec = *end_col;
346
347                    // Column-only: rows are None on both ends
348                    if sr.is_none() && er.is_none() {
349                        // Full-column reference: anchor at row 1 for alignment across columns
350                        let scv = sc.unwrap_or(1);
351                        let ecv = ec.unwrap_or(scv);
352                        sr = Some(1);
353                        if let Some((_, max_r)) =
354                            self.context.used_rows_for_columns(sheet_name, scv, ecv)
355                        {
356                            er = Some(max_r);
357                        } else if let Some((max_rows, _)) = self.context.sheet_bounds(sheet_name) {
358                            er = Some(max_rows);
359                        }
360                    }
361
362                    // Row-only: cols are None on both ends
363                    if sc.is_none() && ec.is_none() {
364                        // Full-row reference: anchor at column 1 for alignment across rows
365                        let srv = sr.unwrap_or(1);
366                        let erv = er.unwrap_or(srv);
367                        sc = Some(1);
368                        if let Some((_, max_c)) =
369                            self.context.used_cols_for_rows(sheet_name, srv, erv)
370                        {
371                            ec = Some(max_c);
372                        } else if let Some((_, max_cols)) = self.context.sheet_bounds(sheet_name) {
373                            ec = Some(max_cols);
374                        }
375                    }
376
377                    // Partially bounded (e.g., A1:A or A:A10)
378                    if sr.is_some() && er.is_none() {
379                        let scv = sc.unwrap_or(1);
380                        let ecv = ec.unwrap_or(scv);
381                        if let Some((_, max_r)) =
382                            self.context.used_rows_for_columns(sheet_name, scv, ecv)
383                        {
384                            er = Some(max_r);
385                        } else if let Some((max_rows, _)) = self.context.sheet_bounds(sheet_name) {
386                            er = Some(max_rows);
387                        }
388                    }
389                    if er.is_some() && sr.is_none() {
390                        // Open start: anchor at row 1
391                        sr = Some(1);
392                    }
393                    if sc.is_some() && ec.is_none() {
394                        let srv = sr.unwrap_or(1);
395                        let erv = er.unwrap_or(srv);
396                        if let Some((_, max_c)) =
397                            self.context.used_cols_for_rows(sheet_name, srv, erv)
398                        {
399                            ec = Some(max_c);
400                        } else if let Some((_, max_cols)) = self.context.sheet_bounds(sheet_name) {
401                            ec = Some(max_cols);
402                        }
403                    }
404                    if ec.is_some() && sc.is_none() {
405                        // Open start: anchor at column 1
406                        sc = Some(1);
407                    }
408
409                    let sr = sr.unwrap_or(1);
410                    let sc = sc.unwrap_or(1);
411                    let er = er.unwrap_or(sr.saturating_sub(1));
412                    let ec = ec.unwrap_or(sc.saturating_sub(1));
413                    if er < sr || ec < sc {
414                        return Some((0, 0));
415                    }
416                    Some((er.saturating_sub(sr) + 1, ec.saturating_sub(sc) + 1))
417                }
418                RT::Cell { .. } => Some((1, 1)),
419                _ => None,
420            }
421        };
422        let fn_lookup = |ns: &str, name: &str| self.context.get_function(ns, name);
423
424        let mut planner = crate::planner::Planner::new(crate::planner::PlanConfig::default())
425            .with_range_probe(&range_probe)
426            .with_function_lookup(&fn_lookup);
427        let plan = planner.plan(node);
428        self.eval_with_plan(node, &plan.root)
429    }
430
431    fn eval_with_plan(
432        &self,
433        node: &ASTNode,
434        plan_node: &crate::planner::PlanNode,
435    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
436        match &node.node_type {
437            ASTNodeType::Literal(v) => Ok(crate::traits::CalcValue::Scalar(v.clone())),
438            ASTNodeType::Reference { reference, .. } => self.eval_reference_to_calc(reference),
439            ASTNodeType::UnaryOp { op, expr } => {
440                // For now, reuse existing unary implementation (which recurses).
441                // In a later phase, we can map plan_node.children[0].
442                self.eval_unary(op, expr)
443                    .map(crate::traits::CalcValue::Scalar)
444            }
445            ASTNodeType::BinaryOp { op, left, right } => self
446                .eval_binary(op, left, right)
447                .map(crate::traits::CalcValue::Scalar),
448            ASTNodeType::Function { name, args } => {
449                let strategy = plan_node.strategy;
450                if let Some(fun) = self.context.get_function("", name) {
451                    use crate::function::FnCaps;
452                    use crate::planner::ExecStrategy;
453                    let caps = fun.caps();
454
455                    // Short-circuit or volatile: always sequential
456                    if caps.contains(FnCaps::SHORT_CIRCUIT) || caps.contains(FnCaps::VOLATILE) {
457                        return self.eval_function_to_calc(name, args);
458                    }
459
460                    // Windowed/chunked strategies are handled by the unified `eval()` path.
461
462                    // Arg-parallel: prewarm subexpressions and then dispatch
463                    if matches!(strategy, ExecStrategy::ArgParallel)
464                        && caps.contains(FnCaps::PARALLEL_ARGS)
465                    {
466                        // Sequential prewarm of subexpressions (safe without Sync bounds)
467                        for arg in args {
468                            match &arg.node_type {
469                                ASTNodeType::Reference { reference, .. } => {
470                                    let _ = self
471                                        .context
472                                        .resolve_range_view(reference, self.current_sheet);
473                                }
474                                _ => {
475                                    let _ = self.evaluate_ast(arg);
476                                }
477                            }
478                        }
479                        return self.eval_function_to_calc(name, args);
480                    }
481
482                    // Default path
483                    return self.eval_function_to_calc(name, args);
484                }
485                self.eval_function_to_calc(name, args)
486            }
487            ASTNodeType::Array(rows) => self.eval_array_literal_to_calc(rows),
488        }
489    }
490
491    /* ===================  reference  =================== */
492    fn eval_reference_to_calc(
493        &self,
494        reference: &ReferenceType,
495    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
496        let view = self
497            .context
498            .resolve_range_view(reference, self.current_sheet)?
499            .with_cancel_token(self.context.cancellation_token());
500
501        match reference {
502            ReferenceType::Cell { .. } => {
503                // For a single cell reference, just return the value.
504                Ok(crate::traits::CalcValue::Scalar(
505                    view.as_1x1().unwrap_or(LiteralValue::Empty),
506                ))
507            }
508            _ => Ok(crate::traits::CalcValue::Range(view)),
509        }
510    }
511
512    fn eval_reference(&self, reference: &ReferenceType) -> Result<LiteralValue, ExcelError> {
513        self.eval_reference_to_calc(reference)
514            .map(|cv| cv.into_literal())
515    }
516
517    /* ===================  unary ops  =================== */
518    fn eval_unary(&self, op: &str, expr: &ASTNode) -> Result<LiteralValue, ExcelError> {
519        let v = self.evaluate_ast(expr)?.into_literal();
520        match v {
521            LiteralValue::Array(arr) => {
522                self.map_array(arr, |cell| self.eval_unary_scalar(op, cell))
523            }
524            other => self.eval_unary_scalar(op, other),
525        }
526    }
527
528    fn eval_unary_scalar(&self, op: &str, v: LiteralValue) -> Result<LiteralValue, ExcelError> {
529        match op {
530            "+" => self.apply_number_unary(v, |n| n),
531            "-" => self.apply_number_unary(v, |n| -n),
532            "%" => self.apply_number_unary(v, |n| n / 100.0),
533            _ => {
534                Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!("Unary op '{op}'")))
535            }
536        }
537    }
538
539    fn apply_number_unary<F>(&self, v: LiteralValue, f: F) -> Result<LiteralValue, ExcelError>
540    where
541        F: Fn(f64) -> f64,
542    {
543        match crate::coercion::to_number_lenient_with_locale(&v, &self.context.locale()) {
544            Ok(n) => match crate::coercion::sanitize_numeric(f(n)) {
545                Ok(n2) => Ok(LiteralValue::Number(n2)),
546                Err(e) => Ok(LiteralValue::Error(e)),
547            },
548            Err(e) => Ok(LiteralValue::Error(e)),
549        }
550    }
551
552    /* ===================  binary ops  =================== */
553    fn eval_binary(
554        &self,
555        op: &str,
556        left: &ASTNode,
557        right: &ASTNode,
558    ) -> Result<LiteralValue, ExcelError> {
559        // Comparisons use dedicated path.
560        if matches!(op, "=" | "<>" | ">" | "<" | ">=" | "<=") {
561            let l = self.evaluate_ast(left)?.into_literal();
562            let r = self.evaluate_ast(right)?.into_literal();
563            return self.compare(op, l, r);
564        }
565
566        let l_val = self.evaluate_ast(left)?.into_literal();
567        let r_val = self.evaluate_ast(right)?.into_literal();
568
569        match op {
570            "+" => self.numeric_binary(l_val, r_val, |a, b| a + b),
571            "-" => self.numeric_binary(l_val, r_val, |a, b| a - b),
572            "*" => self.numeric_binary(l_val, r_val, |a, b| a * b),
573            "/" => self.divide(l_val, r_val),
574            "^" => self.power(l_val, r_val),
575            "&" => Ok(LiteralValue::Text(format!(
576                "{}{}",
577                crate::coercion::to_text_invariant(&l_val),
578                crate::coercion::to_text_invariant(&r_val)
579            ))),
580            ":" => {
581                // Compute a combined reference; in value context return #REF! for now.
582                let lref = self.evaluate_ast_as_reference(left)?;
583                let rref = self.evaluate_ast_as_reference(right)?;
584                match crate::reference::combine_references(&lref, &rref) {
585                    Ok(_r) => Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
586                        "Reference produced by ':' cannot be used directly as a value",
587                    )),
588                    Err(e) => Ok(LiteralValue::Error(e)),
589                }
590            }
591            _ => {
592                Err(ExcelError::new(ExcelErrorKind::NImpl)
593                    .with_message(format!("Binary op '{op}'")))
594            }
595        }
596    }
597
598    /* ===================  function calls  =================== */
599    fn eval_function_to_calc(
600        &self,
601        name: &str,
602        args: &[ASTNode],
603    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
604        if let Some(fun) = self.context.get_function("", name) {
605            let handles: Vec<ArgumentHandle> =
606                args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
607            // Use the function's built-in dispatch method with a narrow FunctionContext
608            let fctx = DefaultFunctionContext::new_with_sheet(
609                self.context,
610                self.current_cell,
611                self.current_sheet,
612            );
613            fun.dispatch(&handles, &fctx)
614        } else {
615            // Include the function name in the error message for better debugging
616            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
617                ExcelError::new(ExcelErrorKind::Name)
618                    .with_message(format!("Unknown function: {name}")),
619            )))
620        }
621    }
622
623    fn eval_function(&self, name: &str, args: &[ASTNode]) -> Result<LiteralValue, ExcelError> {
624        self.eval_function_to_calc(name, args)
625            .map(|cv| cv.into_literal())
626    }
627
628    pub fn function_context(&self, cell_ref: Option<&CellRef>) -> DefaultFunctionContext<'_> {
629        DefaultFunctionContext::new_with_sheet(self.context, cell_ref.cloned(), self.current_sheet)
630    }
631
632    /* ===================  array literal  =================== */
633    fn eval_array_literal_to_calc(
634        &self,
635        rows: &[Vec<ASTNode>],
636    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
637        let mut out = Vec::with_capacity(rows.len());
638        for row in rows {
639            let mut r = Vec::with_capacity(row.len());
640            for cell in row {
641                r.push(self.evaluate_ast(cell)?.into_literal());
642            }
643            out.push(r);
644        }
645        Ok(crate::traits::CalcValue::Range(
646            crate::engine::range_view::RangeView::from_owned_rows(out, self.context.date_system()),
647        ))
648    }
649
650    fn eval_array_literal(&self, rows: &[Vec<ASTNode>]) -> Result<LiteralValue, ExcelError> {
651        self.eval_array_literal_to_calc(rows)
652            .map(|cv| cv.into_literal())
653    }
654
655    /* ===================  helpers  =================== */
656    fn numeric_binary<F>(
657        &self,
658        left: LiteralValue,
659        right: LiteralValue,
660        f: F,
661    ) -> Result<LiteralValue, ExcelError>
662    where
663        F: Fn(f64, f64) -> f64 + Copy,
664    {
665        self.broadcast_apply(left, right, |l, r| {
666            let a = crate::coercion::to_number_lenient_with_locale(&l, &self.context.locale());
667            let b = crate::coercion::to_number_lenient_with_locale(&r, &self.context.locale());
668            match (a, b) {
669                (Ok(a), Ok(b)) => match crate::coercion::sanitize_numeric(f(a, b)) {
670                    Ok(n2) => Ok(LiteralValue::Number(n2)),
671                    Err(e) => Ok(LiteralValue::Error(e)),
672                },
673                (Err(e), _) | (_, Err(e)) => Ok(LiteralValue::Error(e)),
674            }
675        })
676    }
677
678    fn divide(&self, left: LiteralValue, right: LiteralValue) -> Result<LiteralValue, ExcelError> {
679        self.broadcast_apply(left, right, |l, r| {
680            let ln = crate::coercion::to_number_lenient_with_locale(&l, &self.context.locale());
681            let rn = crate::coercion::to_number_lenient_with_locale(&r, &self.context.locale());
682            let (a, b) = match (ln, rn) {
683                (Ok(a), Ok(b)) => (a, b),
684                (Err(e), _) | (_, Err(e)) => return Ok(LiteralValue::Error(e)),
685            };
686            if b == 0.0 {
687                return Ok(LiteralValue::Error(ExcelError::from_error_string(
688                    "#DIV/0!",
689                )));
690            }
691            match crate::coercion::sanitize_numeric(a / b) {
692                Ok(n) => Ok(LiteralValue::Number(n)),
693                Err(e) => Ok(LiteralValue::Error(e)),
694            }
695        })
696    }
697
698    fn power(&self, left: LiteralValue, right: LiteralValue) -> Result<LiteralValue, ExcelError> {
699        self.broadcast_apply(left, right, |l, r| {
700            let ln = crate::coercion::to_number_lenient_with_locale(&l, &self.context.locale());
701            let rn = crate::coercion::to_number_lenient_with_locale(&r, &self.context.locale());
702            let (a, b) = match (ln, rn) {
703                (Ok(a), Ok(b)) => (a, b),
704                (Err(e), _) | (_, Err(e)) => return Ok(LiteralValue::Error(e)),
705            };
706            // Excel domain: negative base with non-integer exponent -> #NUM!
707            if a < 0.0 && b.fract() != 0.0 {
708                return Ok(LiteralValue::Error(ExcelError::new_num()));
709            }
710            match crate::coercion::sanitize_numeric(a.powf(b)) {
711                Ok(n) => Ok(LiteralValue::Number(n)),
712                Err(e) => Ok(LiteralValue::Error(e)),
713            }
714        })
715    }
716
717    fn map_array<F>(&self, arr: Vec<Vec<LiteralValue>>, f: F) -> Result<LiteralValue, ExcelError>
718    where
719        F: Fn(LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
720    {
721        let mut out = Vec::with_capacity(arr.len());
722        for row in arr {
723            let mut new_row = Vec::with_capacity(row.len());
724            for cell in row {
725                new_row.push(match f(cell) {
726                    Ok(v) => v,
727                    Err(e) => LiteralValue::Error(e),
728                });
729            }
730            out.push(new_row);
731        }
732        Ok(LiteralValue::Array(out))
733    }
734
735    fn combine_arrays<F>(
736        &self,
737        l: Vec<Vec<LiteralValue>>,
738        r: Vec<Vec<LiteralValue>>,
739        f: F,
740    ) -> Result<LiteralValue, ExcelError>
741    where
742        F: Fn(LiteralValue, LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
743    {
744        // Use strict broadcasting across dimensions
745        let l_shape = (l.len(), l.first().map(|r| r.len()).unwrap_or(0));
746        let r_shape = (r.len(), r.first().map(|r| r.len()).unwrap_or(0));
747        let target = match broadcast_shape(&[l_shape, r_shape]) {
748            Ok(s) => s,
749            Err(e) => return Ok(LiteralValue::Error(e)),
750        };
751
752        let mut out = Vec::with_capacity(target.0);
753        for i in 0..target.0 {
754            let mut row = Vec::with_capacity(target.1);
755            for j in 0..target.1 {
756                let (li, lj) = project_index((i, j), l_shape);
757                let (ri, rj) = project_index((i, j), r_shape);
758                let lv = l
759                    .get(li)
760                    .and_then(|r| r.get(lj))
761                    .cloned()
762                    .unwrap_or(LiteralValue::Empty);
763                let rv = r
764                    .get(ri)
765                    .and_then(|r| r.get(rj))
766                    .cloned()
767                    .unwrap_or(LiteralValue::Empty);
768                row.push(match f(lv, rv) {
769                    Ok(v) => v,
770                    Err(e) => LiteralValue::Error(e),
771                });
772            }
773            out.push(row);
774        }
775        Ok(LiteralValue::Array(out))
776    }
777
778    fn broadcast_apply<F>(
779        &self,
780        left: LiteralValue,
781        right: LiteralValue,
782        f: F,
783    ) -> Result<LiteralValue, ExcelError>
784    where
785        F: Fn(LiteralValue, LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
786    {
787        use LiteralValue::*;
788        match (left, right) {
789            (Array(l), Array(r)) => self.combine_arrays(l, r, f),
790            (Array(arr), v) => {
791                let shape_l = (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0));
792                let shape_r = (1usize, 1usize);
793                let target = match broadcast_shape(&[shape_l, shape_r]) {
794                    Ok(s) => s,
795                    Err(e) => return Ok(LiteralValue::Error(e)),
796                };
797                let mut out = Vec::with_capacity(target.0);
798                for i in 0..target.0 {
799                    let mut row = Vec::with_capacity(target.1);
800                    for j in 0..target.1 {
801                        let (li, lj) = project_index((i, j), shape_l);
802                        let lv = arr
803                            .get(li)
804                            .and_then(|r| r.get(lj))
805                            .cloned()
806                            .unwrap_or(LiteralValue::Empty);
807                        row.push(match f(lv, v.clone()) {
808                            Ok(vv) => vv,
809                            Err(e) => LiteralValue::Error(e),
810                        });
811                    }
812                    out.push(row);
813                }
814                Ok(LiteralValue::Array(out))
815            }
816            (v, Array(arr)) => {
817                let shape_l = (1usize, 1usize);
818                let shape_r = (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0));
819                let target = match broadcast_shape(&[shape_l, shape_r]) {
820                    Ok(s) => s,
821                    Err(e) => return Ok(LiteralValue::Error(e)),
822                };
823                let mut out = Vec::with_capacity(target.0);
824                for i in 0..target.0 {
825                    let mut row = Vec::with_capacity(target.1);
826                    for j in 0..target.1 {
827                        let (ri, rj) = project_index((i, j), shape_r);
828                        let rv = arr
829                            .get(ri)
830                            .and_then(|r| r.get(rj))
831                            .cloned()
832                            .unwrap_or(LiteralValue::Empty);
833                        row.push(match f(v.clone(), rv) {
834                            Ok(vv) => vv,
835                            Err(e) => LiteralValue::Error(e),
836                        });
837                    }
838                    out.push(row);
839                }
840                Ok(LiteralValue::Array(out))
841            }
842            (l, r) => f(l, r),
843        }
844    }
845
846    /* ---------- coercion helpers ---------- */
847    fn coerce_number(&self, v: &LiteralValue) -> Result<f64, ExcelError> {
848        coercion::to_number_lenient(v)
849    }
850
851    fn coerce_text(&self, v: &LiteralValue) -> String {
852        coercion::to_text_invariant(v)
853    }
854
855    /* ---------- comparison ---------- */
856    fn compare(
857        &self,
858        op: &str,
859        left: LiteralValue,
860        right: LiteralValue,
861    ) -> Result<LiteralValue, ExcelError> {
862        use LiteralValue::*;
863        if matches!(left, Error(_)) {
864            return Ok(left);
865        }
866        if matches!(right, Error(_)) {
867            return Ok(right);
868        }
869
870        // arrays: element‑wise with broadcasting
871        match (left, right) {
872            (Array(l), Array(r)) => self.combine_arrays(l, r, |a, b| self.compare(op, a, b)),
873            (Array(arr), v) => self.broadcast_apply(Array(arr), v, |a, b| self.compare(op, a, b)),
874            (v, Array(arr)) => self.broadcast_apply(v, Array(arr), |a, b| self.compare(op, a, b)),
875            (l, r) => {
876                let res = match (l, r) {
877                    (Number(a), Number(b)) => self.cmp_f64(a, b, op),
878                    (Int(a), Number(b)) => self.cmp_f64(a as f64, b, op),
879                    (Number(a), Int(b)) => self.cmp_f64(a, b as f64, op),
880                    (Boolean(a), Boolean(b)) => {
881                        self.cmp_f64(if a { 1.0 } else { 0.0 }, if b { 1.0 } else { 0.0 }, op)
882                    }
883                    (Text(a), Text(b)) => self.cmp_text(&a, &b, op),
884                    (a, b) => {
885                        // fallback to numeric coercion or text compare
886                        let an = crate::coercion::to_number_lenient_with_locale(
887                            &a,
888                            &self.context.locale(),
889                        )
890                        .ok();
891                        let bn = crate::coercion::to_number_lenient_with_locale(
892                            &b,
893                            &self.context.locale(),
894                        )
895                        .ok();
896                        if let (Some(a), Some(b)) = (an, bn) {
897                            self.cmp_f64(a, b, op)
898                        } else {
899                            self.cmp_text(
900                                &crate::coercion::to_text_invariant(&a),
901                                &crate::coercion::to_text_invariant(&b),
902                                op,
903                            )
904                        }
905                    }
906                };
907                Ok(LiteralValue::Boolean(res))
908            }
909        }
910    }
911
912    fn cmp_f64(&self, a: f64, b: f64, op: &str) -> bool {
913        match op {
914            "=" => a == b,
915            "<>" => a != b,
916            ">" => a > b,
917            "<" => a < b,
918            ">=" => a >= b,
919            "<=" => a <= b,
920            _ => unreachable!(),
921        }
922    }
923    fn cmp_text(&self, a: &str, b: &str, op: &str) -> bool {
924        let loc = self.context.locale();
925        let (a, b) = (loc.fold_case_invariant(a), loc.fold_case_invariant(b));
926        self.cmp_f64(
927            a.cmp(&b) as i32 as f64,
928            0.0,
929            match op {
930                "=" => "=",
931                "<>" => "<>",
932                ">" => ">",
933                "<" => "<",
934                ">=" => ">=",
935                "<=" => "<=",
936                _ => unreachable!(),
937            },
938        )
939    }
940}