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