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};
9use rustc_hash::FxHashMap;
10use std::{borrow::Cow, sync::Arc};
11
12use crate::engine::arena::ast::SheetKey;
13use crate::engine::arena::{AstNodeData, AstNodeId, CompactRefType, DataStore};
14use crate::engine::sheet_registry::SheetRegistry;
15use crate::engine::used_extent::{
16    ExtentPolicy, OpenRangeBounds, resolve_used_extent_with_fallback,
17};
18use crate::formula_plane::template_canonical::LiteralSlotId;
19
20pub(crate) fn probe_range_dimensions<C: EvaluationContext + ?Sized>(
21    context: &C,
22    current_sheet: &str,
23    reference: &ReferenceType,
24) -> Option<(u32, u32)> {
25    match reference {
26        ReferenceType::Range {
27            sheet,
28            start_row,
29            start_col,
30            end_row,
31            end_col,
32            ..
33        } => {
34            let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
35            let extent = resolve_used_extent_with_fallback(
36                OpenRangeBounds {
37                    start_row: *start_row,
38                    start_column: *start_col,
39                    end_row: *end_row,
40                    end_column: *end_col,
41                },
42                ExtentPolicy::EvaluationCompat {
43                    fallback_row: None,
44                    fallback_column: None,
45                },
46                || context.sheet_bounds(sheet_name).map(|bounds| bounds.0),
47                || context.sheet_bounds(sheet_name).map(|bounds| bounds.1),
48                |first, last| context.used_rows_for_columns(sheet_name, first, last),
49                |first, last| context.used_cols_for_rows(sheet_name, first, last),
50            );
51            let Some(extent) = extent else {
52                return Some((0, 0));
53            };
54            Some((
55                extent.end_row - extent.start_row + 1,
56                extent.end_column - extent.start_column + 1,
57            ))
58        }
59        ReferenceType::Cell { .. } => Some((1, 1)),
60        _ => None,
61    }
62}
63
64#[derive(Clone)]
65pub enum LocalBinding {
66    Value(LiteralValue),
67    Callable(Arc<dyn crate::traits::CustomCallable>),
68}
69
70#[derive(Clone, Default)]
71pub struct LocalEnv {
72    head: Option<Arc<EnvFrame>>,
73}
74
75#[derive(Clone)]
76struct EnvFrame {
77    parent: Option<Arc<EnvFrame>>,
78    bindings: FxHashMap<String, LocalBinding>,
79}
80
81impl LocalEnv {
82    #[inline(always)]
83    pub fn is_empty(&self) -> bool {
84        self.head.is_none()
85    }
86
87    fn norm(name: &str) -> String {
88        name.to_ascii_uppercase()
89    }
90
91    pub fn lookup(&self, name: &str) -> Option<LocalBinding> {
92        self.head.as_ref()?;
93        let key = Self::norm(name);
94        let mut cur = self.head.as_ref().cloned();
95        while let Some(frame) = cur {
96            if let Some(v) = frame.bindings.get(&key) {
97                return Some(v.clone());
98            }
99            cur = frame.parent.clone();
100        }
101        None
102    }
103
104    pub fn with_binding(&self, name: &str, value: LocalBinding) -> Self {
105        let mut bindings = FxHashMap::default();
106        bindings.insert(Self::norm(name), value);
107        Self {
108            head: Some(Arc::new(EnvFrame {
109                parent: self.head.clone(),
110                bindings,
111            })),
112        }
113    }
114}
115
116#[derive(Clone, Copy)]
117pub(crate) struct InterpreterParameterBindings<'a> {
118    pub(crate) literal_slots_by_node: &'a FxHashMap<AstNodeId, LiteralSlotId>,
119    pub(crate) literal_values: &'a [LiteralValue],
120}
121
122pub struct Interpreter<'a> {
123    pub context: &'a dyn EvaluationContext,
124    current_sheet: &'a str,
125    current_cell: Option<crate::CellRef>,
126    local_env: LocalEnv,
127    reference_row_delta: i64,
128    reference_col_delta: i64,
129    disable_ast_planner: bool,
130    parameter_bindings: Option<InterpreterParameterBindings<'a>>,
131}
132
133impl<'a> Interpreter<'a> {
134    pub fn new(context: &'a dyn EvaluationContext, current_sheet: &'a str) -> Self {
135        Self {
136            context,
137            current_sheet,
138            current_cell: None,
139            local_env: LocalEnv::default(),
140            reference_row_delta: 0,
141            reference_col_delta: 0,
142            disable_ast_planner: false,
143            parameter_bindings: None,
144        }
145    }
146
147    pub fn new_with_cell(
148        context: &'a dyn EvaluationContext,
149        current_sheet: &'a str,
150        cell: crate::CellRef,
151    ) -> Self {
152        Self {
153            context,
154            current_sheet,
155            current_cell: Some(cell),
156            local_env: LocalEnv::default(),
157            reference_row_delta: 0,
158            reference_col_delta: 0,
159            disable_ast_planner: false,
160            parameter_bindings: None,
161        }
162    }
163
164    pub fn current_sheet(&self) -> &'a str {
165        self.current_sheet
166    }
167
168    pub fn local_env(&self) -> &LocalEnv {
169        &self.local_env
170    }
171
172    pub(crate) fn with_current_cell(&self, cell: crate::CellRef) -> Self {
173        Self {
174            context: self.context,
175            current_sheet: self.current_sheet,
176            current_cell: Some(cell),
177            local_env: self.local_env.clone(),
178            reference_row_delta: self.reference_row_delta,
179            reference_col_delta: self.reference_col_delta,
180            disable_ast_planner: self.disable_ast_planner,
181            parameter_bindings: self.parameter_bindings,
182        }
183    }
184
185    pub fn with_local_env(&self, env: LocalEnv) -> Self {
186        Self {
187            context: self.context,
188            current_sheet: self.current_sheet,
189            current_cell: self.current_cell,
190            local_env: env,
191            reference_row_delta: self.reference_row_delta,
192            reference_col_delta: self.reference_col_delta,
193            disable_ast_planner: self.disable_ast_planner,
194            parameter_bindings: self.parameter_bindings,
195        }
196    }
197
198    pub(crate) fn with_parameter_bindings(
199        &self,
200        bindings: InterpreterParameterBindings<'a>,
201    ) -> Self {
202        Self {
203            context: self.context,
204            current_sheet: self.current_sheet,
205            current_cell: self.current_cell,
206            local_env: self.local_env.clone(),
207            reference_row_delta: self.reference_row_delta,
208            reference_col_delta: self.reference_col_delta,
209            disable_ast_planner: self.disable_ast_planner,
210            parameter_bindings: Some(bindings),
211        }
212    }
213
214    fn effective_reference<'r>(
215        &self,
216        reference: &'r ReferenceType,
217    ) -> Result<Cow<'r, ReferenceType>, ExcelError> {
218        if self.reference_row_delta == 0 && self.reference_col_delta == 0 {
219            return Ok(Cow::Borrowed(reference));
220        }
221
222        Ok(Cow::Owned(relocate_reference_for_offset(
223            reference,
224            self.reference_row_delta,
225            self.reference_col_delta,
226        )?))
227    }
228
229    fn resolve_local_reference(
230        &self,
231        reference: &ReferenceType,
232    ) -> Option<crate::traits::CalcValue<'a>> {
233        if self.local_env.is_empty() {
234            return None;
235        }
236        let name = match reference {
237            ReferenceType::NamedRange(name) => name,
238            _ => return None,
239        };
240        match self.local_env.lookup(name)? {
241            LocalBinding::Value(v) => Some(crate::traits::CalcValue::Scalar(v)),
242            LocalBinding::Callable(c) => Some(crate::traits::CalcValue::Callable(c)),
243        }
244    }
245
246    fn resolve_local_callable(&self, name: &str) -> Option<Arc<dyn crate::traits::CustomCallable>> {
247        if self.local_env.is_empty() {
248            return None;
249        }
250        match self.local_env.lookup(name)? {
251            LocalBinding::Callable(c) => Some(c),
252            LocalBinding::Value(_) => None,
253        }
254    }
255
256    pub fn resolve_local_name(&self, name: &str) -> Option<LocalBinding> {
257        self.local_env.lookup(name)
258    }
259
260    pub fn resolve_range_view<'c>(
261        &'c self,
262        reference: &ReferenceType,
263        current_sheet: &str,
264    ) -> Result<crate::engine::range_view::RangeView<'c>, ExcelError> {
265        self.context.resolve_range_view(reference, current_sheet)
266    }
267
268    /// Evaluate an AST node in a reference context and return a ReferenceType.
269    /// This is used for range combinators (e.g., ":"), by-ref argument flows,
270    /// and spill planning. Functions that can return references must set
271    /// `FnCaps::RETURNS_REFERENCE` and override `eval_reference`.
272    pub fn evaluate_ast_as_reference(&self, node: &ASTNode) -> Result<ReferenceType, ExcelError> {
273        match &node.node_type {
274            ASTNodeType::Reference { reference, .. } => {
275                self.reference_for_current_offset(reference)
276            }
277            ASTNodeType::Function { name, args } => {
278                if let Some(fun) = self.context.get_function("", name) {
279                    // Build handles; allow function to decide reference semantics
280                    let handles: Vec<ArgumentHandle> =
281                        args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
282                    let fctx = DefaultFunctionContext::new_with_sheet(
283                        self.context,
284                        None,
285                        self.current_sheet,
286                    );
287                    if let Some(res) = fun.eval_reference(&handles, &fctx) {
288                        res
289                    } else {
290                        Err(ExcelError::new(ExcelErrorKind::Ref)
291                            .with_message("Function does not return a reference"))
292                    }
293                } else {
294                    Err(ExcelError::new(ExcelErrorKind::Name)
295                        .with_message(format!("Unknown function: {name}")))
296                }
297            }
298            ASTNodeType::BinaryOp { op, left, right } if op == ":" => {
299                let lref = self.evaluate_ast_as_reference(left)?;
300                let rref = self.evaluate_ast_as_reference(right)?;
301                crate::reference::combine_references(&lref, &rref)
302            }
303            ASTNodeType::Array(_)
304            | ASTNodeType::UnaryOp { .. }
305            | ASTNodeType::BinaryOp { .. }
306            | ASTNodeType::Call { .. }
307            | ASTNodeType::Literal(_)
308            | ASTNodeType::Omitted => Err(ExcelError::new(ExcelErrorKind::Ref)
309                .with_message("Expression cannot be used as a reference")),
310        }
311    }
312
313    pub(crate) fn try_evaluate_ast_as_reference(
314        &self,
315        node: &ASTNode,
316    ) -> Option<Result<ReferenceType, ExcelError>> {
317        let ASTNodeType::Function { name, args } = &node.node_type else {
318            return Some(self.evaluate_ast_as_reference(node));
319        };
320        let fun = match self.context.get_function("", name) {
321            Some(fun) => fun,
322            None => {
323                return Some(Err(ExcelError::new(ExcelErrorKind::Name)
324                    .with_message(format!("Unknown function: {name}"))));
325            }
326        };
327        let handles: Vec<ArgumentHandle> = args
328            .iter()
329            .map(|arg| ArgumentHandle::new(arg, self))
330            .collect();
331        let fctx = DefaultFunctionContext::new_with_sheet(self.context, None, self.current_sheet);
332        fun.eval_reference(&handles, &fctx)
333    }
334
335    pub(crate) fn evaluate_arena_ast_as_reference(
336        &self,
337        node_id: AstNodeId,
338        data_store: &DataStore,
339        sheet_registry: &SheetRegistry,
340    ) -> Result<ReferenceType, ExcelError> {
341        let node = data_store.get_node(node_id).ok_or_else(|| {
342            ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
343        })?;
344
345        match node {
346            AstNodeData::Reference { ref_type, .. } => {
347                let reference =
348                    data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry);
349                self.reference_for_current_offset(&reference)
350            }
351            AstNodeData::Function { name_id, .. } => {
352                let name = data_store.resolve_ast_string(*name_id);
353                let fun = self.context.get_function("", name).ok_or_else(|| {
354                    ExcelError::new(ExcelErrorKind::Name)
355                        .with_message(format!("Unknown function: {name}"))
356                })?;
357
358                let args = data_store.get_args(node_id).ok_or_else(|| {
359                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
360                })?;
361
362                let handles: Vec<ArgumentHandle> = args
363                    .iter()
364                    .copied()
365                    .map(|arg_id| {
366                        ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry)
367                    })
368                    .collect();
369
370                let fctx =
371                    DefaultFunctionContext::new_with_sheet(self.context, None, self.current_sheet);
372
373                fun.eval_reference(&handles, &fctx).ok_or_else(|| {
374                    ExcelError::new(ExcelErrorKind::Ref)
375                        .with_message("Function does not return a reference")
376                })?
377            }
378            AstNodeData::BinaryOp {
379                op_id,
380                left_id,
381                right_id,
382            } => {
383                let op = data_store.resolve_ast_string(*op_id);
384                if op != ":" {
385                    return Err(ExcelError::new(ExcelErrorKind::Ref)
386                        .with_message("Expression cannot be used as a reference"));
387                }
388                let lref =
389                    self.evaluate_arena_ast_as_reference(*left_id, data_store, sheet_registry)?;
390                let rref =
391                    self.evaluate_arena_ast_as_reference(*right_id, data_store, sheet_registry)?;
392                crate::reference::combine_references(&lref, &rref)
393            }
394            _ => Err(ExcelError::new(ExcelErrorKind::Ref)
395                .with_message("Expression cannot be used as a reference")),
396        }
397    }
398
399    pub(crate) fn try_evaluate_arena_ast_as_reference(
400        &self,
401        node_id: AstNodeId,
402        data_store: &DataStore,
403        sheet_registry: &SheetRegistry,
404    ) -> Option<Result<ReferenceType, ExcelError>> {
405        let node = match data_store.get_node(node_id) {
406            Some(node) => node,
407            None => {
408                return Some(Err(
409                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
410                ));
411            }
412        };
413        let AstNodeData::Function { name_id, .. } = node else {
414            return Some(self.evaluate_arena_ast_as_reference(node_id, data_store, sheet_registry));
415        };
416        let name = data_store.resolve_ast_string(*name_id);
417        let fun = match self.context.get_function("", name) {
418            Some(fun) => fun,
419            None => {
420                return Some(Err(ExcelError::new(ExcelErrorKind::Name)
421                    .with_message(format!("Unknown function: {name}"))));
422            }
423        };
424        let args = match data_store.get_args(node_id) {
425            Some(args) => args,
426            None => {
427                return Some(Err(
428                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
429                ));
430            }
431        };
432        let handles: Vec<ArgumentHandle> = args
433            .iter()
434            .copied()
435            .map(|arg_id| ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry))
436            .collect();
437        let fctx = DefaultFunctionContext::new_with_sheet(self.context, None, self.current_sheet);
438        fun.eval_reference(&handles, &fctx)
439    }
440
441    /* ===================  public  =================== */
442    pub fn evaluate_ast(&self, node: &ASTNode) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
443        self.evaluate_ast_uncached(node)
444    }
445
446    pub(crate) fn evaluate_ast_with_offset(
447        &self,
448        node: &ASTNode,
449        row_delta: i64,
450        col_delta: i64,
451    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
452        let offset = Self {
453            context: self.context,
454            current_sheet: self.current_sheet,
455            current_cell: self.current_cell,
456            local_env: self.local_env.clone(),
457            reference_row_delta: row_delta,
458            reference_col_delta: col_delta,
459            disable_ast_planner: true,
460            parameter_bindings: self.parameter_bindings,
461        };
462        offset.evaluate_ast_uncached(node)
463    }
464
465    pub(crate) fn reference_for_current_offset(
466        &self,
467        reference: &ReferenceType,
468    ) -> Result<ReferenceType, ExcelError> {
469        self.effective_reference(reference)
470            .map(|reference| reference.into_owned())
471    }
472
473    pub(crate) fn evaluate_arena_ast_with_offset(
474        &self,
475        node_id: AstNodeId,
476        row_delta: i64,
477        col_delta: i64,
478        data_store: &DataStore,
479        sheet_registry: &SheetRegistry,
480    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
481        let offset = Self {
482            context: self.context,
483            current_sheet: self.current_sheet,
484            current_cell: self.current_cell,
485            local_env: self.local_env.clone(),
486            reference_row_delta: row_delta,
487            reference_col_delta: col_delta,
488            disable_ast_planner: true,
489            parameter_bindings: self.parameter_bindings,
490        };
491        offset.evaluate_arena_ast(node_id, data_store, sheet_registry)
492    }
493
494    pub(crate) fn evaluate_arena_ast(
495        &self,
496        node_id: AstNodeId,
497        data_store: &DataStore,
498        sheet_registry: &SheetRegistry,
499    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
500        let node = data_store.get_node(node_id).ok_or_else(|| {
501            ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
502        })?;
503
504        match node {
505            AstNodeData::Literal(vref) => {
506                if let Some(bindings) = self.parameter_bindings
507                    && let Some(slot_id) = bindings.literal_slots_by_node.get(&node_id)
508                    && let Some(value) = bindings.literal_values.get(slot_id.0 as usize)
509                {
510                    return Ok(crate::traits::CalcValue::Scalar(value.clone()));
511                }
512                Ok(crate::traits::CalcValue::Scalar(
513                    data_store.retrieve_value(*vref),
514                ))
515            }
516            AstNodeData::Omitted => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
517            AstNodeData::Reference { ref_type, .. } => {
518                if self.local_env.is_empty()
519                    && let CompactRefType::Cell {
520                        sheet,
521                        row,
522                        col,
523                        row_abs,
524                        col_abs,
525                    } = ref_type
526                    && *row > 0
527                    && *col > 0
528                {
529                    let sheet_name = match sheet {
530                        Some(SheetKey::Id(id)) => Some(sheet_registry.name(*id)),
531                        Some(SheetKey::Name(name_id)) => {
532                            Some(data_store.resolve_ast_string(*name_id))
533                        }
534                        None => None,
535                    };
536                    let row = shift_axis_for_offset(*row, self.reference_row_delta, *row_abs)?;
537                    let col = shift_axis_for_offset(*col, self.reference_col_delta, *col_abs)?;
538                    let value = self.context.resolve_cell_reference_value(
539                        sheet_name,
540                        row,
541                        col,
542                        self.current_sheet,
543                    )?;
544                    Ok(crate::traits::CalcValue::Scalar(value))
545                } else {
546                    let reference =
547                        data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry);
548                    let reference = self.effective_reference(&reference)?;
549                    if let Some(local) = self.resolve_local_reference(&reference) {
550                        return Ok(local);
551                    }
552                    self.eval_reference_to_calc(&reference)
553                }
554            }
555            AstNodeData::UnaryOp { op_id, expr_id } => {
556                let expr = self.evaluate_arena_ast(*expr_id, data_store, sheet_registry)?;
557
558                let op = data_store.resolve_ast_string(*op_id);
559                if op == "@" {
560                    // Prefer reference-aware implicit intersection so we don't depend on
561                    // RangeView absolute coordinates (important for lightweight test contexts).
562                    if let Some(AstNodeData::Reference { ref_type, .. }) =
563                        data_store.get_node(*expr_id)
564                    {
565                        let reference = data_store
566                            .reconstruct_reference_type_for_eval(ref_type, sheet_registry);
567                        let v = self.implicit_intersection_from_reference(&reference);
568                        return Ok(crate::traits::CalcValue::Scalar(v));
569                    }
570
571                    let v = self.eval_implicit_intersection_calc(expr);
572                    return Ok(crate::traits::CalcValue::Scalar(v));
573                }
574                // For now, materialize for operators. Future: virtual range ops.
575                let v = expr.into_literal();
576                match v {
577                    LiteralValue::Array(arr) => self
578                        .map_array(arr, |cell| self.eval_unary_scalar(op, cell))
579                        .map(crate::traits::CalcValue::Scalar),
580                    other => self
581                        .eval_unary_scalar(op, other)
582                        .map(crate::traits::CalcValue::Scalar),
583                }
584            }
585            AstNodeData::BinaryOp {
586                op_id,
587                left_id,
588                right_id,
589            } => {
590                let op = data_store.resolve_ast_string(*op_id);
591                if op == ":" {
592                    let lref =
593                        self.evaluate_arena_ast_as_reference(*left_id, data_store, sheet_registry)?;
594                    let rref = self.evaluate_arena_ast_as_reference(
595                        *right_id,
596                        data_store,
597                        sheet_registry,
598                    )?;
599                    return match crate::reference::combine_references(&lref, &rref) {
600                        Ok(_r) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
601                            ExcelError::new(ExcelErrorKind::Ref).with_message(
602                                "Reference produced by ':' cannot be used directly as a value",
603                            ),
604                        ))),
605                        Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
606                    };
607                }
608
609                let left = self
610                    .evaluate_arena_ast(*left_id, data_store, sheet_registry)?
611                    .into_literal();
612                let right = self
613                    .evaluate_arena_ast(*right_id, data_store, sheet_registry)?
614                    .into_literal();
615
616                if matches!(op, "=" | "<>" | ">" | "<" | ">=" | "<=") {
617                    return self
618                        .compare(op, left, right)
619                        .map(crate::traits::CalcValue::Scalar);
620                }
621
622                match op {
623                    "+" => self
624                        .add_sub_date_aware('+', left, right)
625                        .map(crate::traits::CalcValue::Scalar),
626                    "-" => self
627                        .add_sub_date_aware('-', left, right)
628                        .map(crate::traits::CalcValue::Scalar),
629                    "*" => self
630                        .numeric_binary(left, right, |a, b| a * b)
631                        .map(crate::traits::CalcValue::Scalar),
632                    "/" => self
633                        .divide(left, right)
634                        .map(crate::traits::CalcValue::Scalar),
635                    "^" => self
636                        .power(left, right)
637                        .map(crate::traits::CalcValue::Scalar),
638                    "&" => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
639                        format!(
640                            "{}{}",
641                            crate::coercion::to_text_invariant(&left),
642                            crate::coercion::to_text_invariant(&right)
643                        ),
644                    ))),
645                    _ => Err(ExcelError::new(ExcelErrorKind::NImpl)
646                        .with_message(format!("Binary op '{op}'"))),
647                }
648            }
649            AstNodeData::Array { .. } => {
650                let (rows, cols, elements) =
651                    data_store.get_array_elems(node_id).ok_or_else(|| {
652                        ExcelError::new(ExcelErrorKind::Value).with_message("Invalid array")
653                    })?;
654
655                let rows_usize = rows as usize;
656                let cols_usize = cols as usize;
657                let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows_usize);
658                for r in 0..rows_usize {
659                    let mut row = Vec::with_capacity(cols_usize);
660                    for c in 0..cols_usize {
661                        let idx = r * cols_usize + c;
662                        if let Some(&elem_id) = elements.get(idx) {
663                            row.push(
664                                self.evaluate_arena_ast(elem_id, data_store, sheet_registry)?
665                                    .into_literal(),
666                            );
667                        }
668                    }
669                    out.push(row);
670                }
671
672                Ok(crate::traits::CalcValue::Range(
673                    crate::engine::range_view::RangeView::from_owned_rows(
674                        out,
675                        self.context.date_system(),
676                    ),
677                ))
678            }
679            AstNodeData::Function { name_id, .. } => {
680                let name = data_store.resolve_ast_string(*name_id);
681                let args = data_store.get_args(node_id).ok_or_else(|| {
682                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
683                })?;
684
685                if let Some(fun) = self.context.get_function("", name) {
686                    let handles: Vec<ArgumentHandle> = args
687                        .iter()
688                        .copied()
689                        .map(|arg_id| {
690                            ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry)
691                        })
692                        .collect();
693
694                    let fctx = DefaultFunctionContext::new_with_sheet(
695                        self.context,
696                        self.current_cell,
697                        self.current_sheet,
698                    );
699
700                    return fun.dispatch(&handles, &fctx);
701                }
702
703                if let Some(callable) = self.resolve_local_callable(name) {
704                    let mut eval_args = Vec::with_capacity(args.len());
705                    for arg_id in args {
706                        eval_args.push(
707                            self.evaluate_arena_ast(*arg_id, data_store, sheet_registry)?
708                                .into_literal(),
709                        );
710                    }
711                    return callable.invoke(self, &eval_args);
712                }
713
714                Err(ExcelError::new(ExcelErrorKind::Name)
715                    .with_message(format!("Unknown function: {name}")))
716            }
717        }
718    }
719
720    fn evaluate_ast_uncached(
721        &self,
722        node: &ASTNode,
723    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
724        if self.disable_ast_planner {
725            return self.eval_tree_uncached(node);
726        }
727
728        // Plan-aware evaluation: build a plan for this node and execute accordingly.
729        // Provide the planner with a lightweight range-dimension probe and function lookup
730        // so it can select chunked reduction and arg-parallel strategies where appropriate.
731        let current_sheet = self.current_sheet.to_string();
732        let range_probe = |reference: &ReferenceType| {
733            probe_range_dimensions(self.context, &current_sheet, reference)
734        };
735        let fn_lookup = |ns: &str, name: &str| self.context.get_function(ns, name);
736
737        let mut planner = crate::planner::Planner::new(crate::planner::PlanConfig::default())
738            .with_range_probe(&range_probe)
739            .with_function_lookup(&fn_lookup);
740        let plan = planner.plan(node);
741        self.eval_with_plan(node, &plan.root)
742    }
743
744    fn eval_tree_uncached(
745        &self,
746        node: &ASTNode,
747    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
748        match &node.node_type {
749            ASTNodeType::Literal(v) => Ok(crate::traits::CalcValue::Scalar(v.clone())),
750            ASTNodeType::Omitted => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
751            ASTNodeType::Reference { reference, .. } => self.eval_ast_reference_to_calc(reference),
752            ASTNodeType::UnaryOp { op, expr } => self
753                .eval_unary(op, expr)
754                .map(crate::traits::CalcValue::Scalar),
755            ASTNodeType::BinaryOp { op, left, right } => self
756                .eval_binary(op, left, right)
757                .map(crate::traits::CalcValue::Scalar),
758            ASTNodeType::Function { name, args } => self.eval_function_to_calc(name, args),
759            ASTNodeType::Call { .. } => Err(ExcelError::new(ExcelErrorKind::NImpl)
760                .with_message("Immediate-invocation calls are not yet supported")),
761            ASTNodeType::Array(rows) => self.eval_array_literal_to_calc(rows),
762        }
763    }
764
765    fn eval_with_plan(
766        &self,
767        node: &ASTNode,
768        plan_node: &crate::planner::PlanNode,
769    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
770        match &node.node_type {
771            ASTNodeType::Literal(v) => Ok(crate::traits::CalcValue::Scalar(v.clone())),
772            ASTNodeType::Omitted => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
773            ASTNodeType::Reference { reference, .. } => self.eval_ast_reference_to_calc(reference),
774            ASTNodeType::UnaryOp { op, expr } => {
775                // For now, reuse existing unary implementation (which recurses).
776                // In a later phase, we can map plan_node.children[0].
777                self.eval_unary(op, expr)
778                    .map(crate::traits::CalcValue::Scalar)
779            }
780            ASTNodeType::BinaryOp { op, left, right } => self
781                .eval_binary(op, left, right)
782                .map(crate::traits::CalcValue::Scalar),
783            ASTNodeType::Function { name, args } => {
784                let strategy = plan_node.strategy;
785                if let Some(fun) = self.context.get_function("", name) {
786                    use crate::function::FnCaps;
787                    use crate::planner::ExecStrategy;
788                    let caps = fun.caps();
789
790                    // Short-circuit or volatile: always sequential
791                    if caps.contains(FnCaps::SHORT_CIRCUIT) || caps.contains(FnCaps::VOLATILE) {
792                        return self.eval_function_to_calc(name, args);
793                    }
794
795                    // Windowed/chunked strategies are handled by the unified `eval()` path.
796
797                    // Arg-parallel: prewarm subexpressions and then dispatch
798                    if matches!(strategy, ExecStrategy::ArgParallel)
799                        && caps.contains(FnCaps::PARALLEL_ARGS)
800                    {
801                        // Sequential prewarm of subexpressions (safe without Sync bounds)
802                        for arg in args {
803                            match &arg.node_type {
804                                ASTNodeType::Reference { reference, .. } => {
805                                    if let Ok(reference) = self.effective_reference(reference) {
806                                        let _ = self
807                                            .context
808                                            .resolve_range_view(&reference, self.current_sheet);
809                                    }
810                                }
811                                _ => {
812                                    let _ = self.evaluate_ast(arg);
813                                }
814                            }
815                        }
816                        return self.eval_function_to_calc(name, args);
817                    }
818
819                    // Default path
820                    return self.eval_function_to_calc(name, args);
821                }
822                self.eval_function_to_calc(name, args)
823            }
824            ASTNodeType::Call { .. } => Err(ExcelError::new(ExcelErrorKind::NImpl)
825                .with_message("Immediate-invocation calls are not yet supported")),
826            ASTNodeType::Array(rows) => self.eval_array_literal_to_calc(rows),
827        }
828    }
829
830    /* ===================  reference  =================== */
831    fn eval_ast_reference_to_calc(
832        &self,
833        reference: &ReferenceType,
834    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
835        if !self.local_env.is_empty() {
836            let reference = self.effective_reference(reference)?;
837            if let Some(local) = self.resolve_local_reference(&reference) {
838                return Ok(local);
839            }
840            return self.eval_reference_to_calc(&reference);
841        }
842
843        if let ReferenceType::Cell {
844            sheet,
845            row,
846            col,
847            row_abs,
848            col_abs,
849        } = reference
850        {
851            let row = shift_axis_for_offset(*row, self.reference_row_delta, *row_abs)?;
852            let col = shift_axis_for_offset(*col, self.reference_col_delta, *col_abs)?;
853            return Ok(crate::traits::CalcValue::Scalar(
854                self.context.resolve_cell_reference_value(
855                    sheet.as_deref(),
856                    row,
857                    col,
858                    self.current_sheet,
859                )?,
860            ));
861        }
862
863        let reference = self.effective_reference(reference)?;
864        self.eval_reference_to_calc(&reference)
865    }
866
867    fn eval_reference_to_calc(
868        &self,
869        reference: &ReferenceType,
870    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
871        if let ReferenceType::Cell {
872            sheet, row, col, ..
873        } = reference
874        {
875            return Ok(crate::traits::CalcValue::Scalar(
876                self.context.resolve_cell_reference_value(
877                    sheet.as_deref(),
878                    *row,
879                    *col,
880                    self.current_sheet,
881                )?,
882            ));
883        }
884
885        let view = self
886            .context
887            .resolve_range_view(reference, self.current_sheet)?
888            .with_cancel_token(self.context.cancellation_token());
889        Ok(crate::traits::CalcValue::Range(view))
890    }
891
892    fn eval_reference(&self, reference: &ReferenceType) -> Result<LiteralValue, ExcelError> {
893        self.eval_reference_to_calc(reference)
894            .map(|cv| cv.into_literal())
895    }
896
897    /* ===================  unary ops  =================== */
898    fn eval_unary(&self, op: &str, expr: &ASTNode) -> Result<LiteralValue, ExcelError> {
899        if op == "@" {
900            if let ASTNodeType::Reference { reference, .. } = &expr.node_type {
901                let reference = self.effective_reference(reference)?;
902                return Ok(self.implicit_intersection_from_reference(&reference));
903            }
904
905            let cv = self.evaluate_ast(expr)?;
906            return Ok(self.eval_implicit_intersection_calc(cv));
907        }
908
909        let v = self.evaluate_ast(expr)?.into_literal();
910        match v {
911            LiteralValue::Array(arr) => {
912                self.map_array(arr, |cell| self.eval_unary_scalar(op, cell))
913            }
914            other => self.eval_unary_scalar(op, other),
915        }
916    }
917
918    fn eval_unary_scalar(&self, op: &str, v: LiteralValue) -> Result<LiteralValue, ExcelError> {
919        match op {
920            // Excel/LibreOffice treat unary `+` as a pass-through (identity) operator,
921            // not as a numeric coercion. `=+"2014F"` returns the text "2014F"; only the
922            // unary `-` form coerces operands to numbers. The `=+A1` idiom is common in
923            // finance models (Lotus 1-2-3 carry-over) and must preserve text labels.
924            "+" => Ok(v),
925            "-" => self.apply_number_unary(v, |n| -n),
926            "%" => self.apply_number_unary(v, |n| n / 100.0),
927            _ => {
928                Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!("Unary op '{op}'")))
929            }
930        }
931    }
932
933    fn eval_implicit_intersection_calc(&self, cv: crate::traits::CalcValue<'a>) -> LiteralValue {
934        let (cur_r0, cur_c0) = match self.current_cell {
935            Some(cell) => (cell.coord.row() as usize, cell.coord.col() as usize),
936            None => (0usize, 0usize),
937        };
938
939        match cv {
940            crate::traits::CalcValue::Scalar(v) => match v {
941                LiteralValue::Array(arr) => {
942                    if arr.is_empty() || arr.first().map(|r| r.is_empty()).unwrap_or(true) {
943                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
944                    }
945                    arr[0][0].clone()
946                }
947                other => other,
948            },
949            crate::traits::CalcValue::Range(rv) => {
950                if rv.is_empty() {
951                    return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
952                }
953
954                // Array results (array literals and many dynamic-array functions) are materialized
955                // into an owned RangeView with a temporary backing sheet ("__tmp").
956                // For explicit @, interpret these as anchored at the formula cell and select the
957                // top-left element.
958                if rv.sheet_name() == "__tmp" {
959                    return rv.get_cell(0, 0);
960                }
961
962                if let Some(v) = rv.as_1x1() {
963                    return v;
964                }
965
966                let (rows, cols) = rv.dims();
967                let sr = rv.start_row();
968                let sc = rv.start_col();
969                let er = rv.end_row();
970                let ec = rv.end_col();
971
972                // Excel-compatible implicit intersection (simplified):
973                // - Nx1: pick by row
974                // - 1xM: pick by column
975                // - NxM: pick by (row,col)
976                if cols == 1 {
977                    if cur_r0 < sr || cur_r0 > er {
978                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
979                    }
980                    let rel_r = cur_r0 - sr;
981                    return rv.get_cell(rel_r, 0);
982                }
983
984                if rows == 1 {
985                    if cur_c0 < sc || cur_c0 > ec {
986                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
987                    }
988                    let rel_c = cur_c0 - sc;
989                    return rv.get_cell(0, rel_c);
990                }
991
992                if cur_r0 < sr || cur_r0 > er || cur_c0 < sc || cur_c0 > ec {
993                    return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
994                }
995                let rel_r = cur_r0 - sr;
996                let rel_c = cur_c0 - sc;
997                rv.get_cell(rel_r, rel_c)
998            }
999            crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
1000                ExcelError::new(ExcelErrorKind::Calc).with_message("LAMBDA value must be invoked"),
1001            ),
1002        }
1003    }
1004
1005    fn implicit_intersection_from_reference(&self, reference: &ReferenceType) -> LiteralValue {
1006        let (cur_r1, cur_c1) = match self.current_cell {
1007            Some(cell) => (
1008                cell.coord.row().saturating_add(1),
1009                cell.coord.col().saturating_add(1),
1010            ),
1011            None => (1u32, 1u32),
1012        };
1013
1014        match reference {
1015            ReferenceType::Cell {
1016                sheet, row, col, ..
1017            } => {
1018                let sheet_name = sheet.as_deref().unwrap_or(self.current_sheet);
1019                match self
1020                    .context
1021                    .resolve_cell_reference(Some(sheet_name), *row, *col)
1022                {
1023                    Ok(v) => v,
1024                    Err(e) => LiteralValue::Error(e),
1025                }
1026            }
1027            ReferenceType::Range {
1028                sheet,
1029                start_row,
1030                start_col,
1031                end_row,
1032                end_col,
1033                ..
1034            } => {
1035                let sheet_name = sheet.as_deref().unwrap_or(self.current_sheet);
1036
1037                let (sr, sc, er, ec) = match (start_row, start_col, end_row, end_col) {
1038                    (Some(sr), Some(sc), Some(er), Some(ec)) => (*sr, *sc, *er, *ec),
1039                    _ => {
1040                        // For open-ended/infinite ranges, fall back to the RangeView-based path.
1041                        // This path may be less precise in minimal test contexts.
1042                        let cv = match self.eval_reference_to_calc(reference) {
1043                            Ok(cv) => cv,
1044                            Err(e) => return LiteralValue::Error(e),
1045                        };
1046                        return self.eval_implicit_intersection_calc(cv);
1047                    }
1048                };
1049
1050                // Normalize bounds (A10:A1 is legal syntax; treat as swapped).
1051                let (mut sr, mut er) = (sr, er);
1052                let (mut sc, mut ec) = (sc, ec);
1053                if sr > er {
1054                    std::mem::swap(&mut sr, &mut er);
1055                }
1056                if sc > ec {
1057                    std::mem::swap(&mut sc, &mut ec);
1058                }
1059
1060                let pick = if sc == ec {
1061                    // Column vector: intersect by row
1062                    if cur_r1 < sr || cur_r1 > er {
1063                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1064                    }
1065                    (cur_r1, sc)
1066                } else if sr == er {
1067                    // Row vector: intersect by column
1068                    if cur_c1 < sc || cur_c1 > ec {
1069                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1070                    }
1071                    (sr, cur_c1)
1072                } else {
1073                    // 2D: require both axes
1074                    if cur_r1 < sr || cur_r1 > er || cur_c1 < sc || cur_c1 > ec {
1075                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1076                    }
1077                    (cur_r1, cur_c1)
1078                };
1079
1080                match self
1081                    .context
1082                    .resolve_cell_reference(Some(sheet_name), pick.0, pick.1)
1083                {
1084                    Ok(v) => v,
1085                    Err(e) => LiteralValue::Error(e),
1086                }
1087            }
1088            // Named ranges / tables / external: fall back to materializing and intersecting.
1089            other => {
1090                let cv = match self.eval_reference_to_calc(other) {
1091                    Ok(cv) => cv,
1092                    Err(e) => return LiteralValue::Error(e),
1093                };
1094                self.eval_implicit_intersection_calc(cv)
1095            }
1096        }
1097    }
1098
1099    fn apply_number_unary<F>(&self, v: LiteralValue, f: F) -> Result<LiteralValue, ExcelError>
1100    where
1101        F: Fn(f64) -> f64,
1102    {
1103        match crate::coercion::to_arithmetic_number_with_locale(
1104            &v,
1105            &self.context.locale(),
1106            self.context.date_system(),
1107        ) {
1108            Ok(n) => match crate::coercion::sanitize_numeric(f(n)) {
1109                Ok(n2) => Ok(LiteralValue::Number(n2)),
1110                Err(e) => Ok(LiteralValue::Error(e)),
1111            },
1112            Err(e) => Ok(LiteralValue::Error(e)),
1113        }
1114    }
1115
1116    /* ===================  binary ops  =================== */
1117    fn eval_binary(
1118        &self,
1119        op: &str,
1120        left: &ASTNode,
1121        right: &ASTNode,
1122    ) -> Result<LiteralValue, ExcelError> {
1123        // Comparisons use dedicated path.
1124        if matches!(op, "=" | "<>" | ">" | "<" | ">=" | "<=") {
1125            let l = self.evaluate_ast(left)?.into_literal();
1126            let r = self.evaluate_ast(right)?.into_literal();
1127            return self.compare(op, l, r);
1128        }
1129
1130        let l_val = self.evaluate_ast(left)?.into_literal();
1131        let r_val = self.evaluate_ast(right)?.into_literal();
1132
1133        match op {
1134            "+" => self.add_sub_date_aware('+', l_val, r_val),
1135            "-" => self.add_sub_date_aware('-', l_val, r_val),
1136            "*" => self.numeric_binary(l_val, r_val, |a, b| a * b),
1137            "/" => self.divide(l_val, r_val),
1138            "^" => self.power(l_val, r_val),
1139            "&" => Ok(LiteralValue::Text(format!(
1140                "{}{}",
1141                crate::coercion::to_text_invariant(&l_val),
1142                crate::coercion::to_text_invariant(&r_val)
1143            ))),
1144            ":" => {
1145                // Compute a combined reference; in value context return #REF! for now.
1146                let lref = self.evaluate_ast_as_reference(left)?;
1147                let rref = self.evaluate_ast_as_reference(right)?;
1148                match crate::reference::combine_references(&lref, &rref) {
1149                    Ok(_r) => Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
1150                        "Reference produced by ':' cannot be used directly as a value",
1151                    )),
1152                    Err(e) => Ok(LiteralValue::Error(e)),
1153                }
1154            }
1155            _ => {
1156                Err(ExcelError::new(ExcelErrorKind::NImpl)
1157                    .with_message(format!("Binary op '{op}'")))
1158            }
1159        }
1160    }
1161
1162    fn add_sub_date_aware(
1163        &self,
1164        op: char,
1165        left: LiteralValue,
1166        right: LiteralValue,
1167    ) -> Result<LiteralValue, ExcelError> {
1168        debug_assert!(op == '+' || op == '-');
1169
1170        self.broadcast_apply(left, right, |l, r| {
1171            use LiteralValue::*;
1172
1173            let date_system = self.context.date_system();
1174
1175            let date_like_serial = |v: &LiteralValue| -> Option<f64> {
1176                match v {
1177                    Date(d) => Some(formualizer_common::date_to_serial_for(date_system, d)),
1178                    DateTime(dt) => {
1179                        Some(formualizer_common::datetime_to_serial_for(date_system, dt))
1180                    }
1181                    _ => None,
1182                }
1183            };
1184
1185            let to_num = |v: &LiteralValue| -> Result<f64, ExcelError> {
1186                crate::coercion::to_arithmetic_number_with_locale(
1187                    v,
1188                    &self.context.locale(),
1189                    date_system,
1190                )
1191            };
1192
1193            let serial_to_literal = |serial: f64| -> LiteralValue {
1194                match crate::coercion::sanitize_numeric(serial) {
1195                    Ok(serial) => {
1196                        match formualizer_common::try_serial_to_datetime_for(date_system, serial) {
1197                            Ok(dt) => {
1198                                if dt.time() == chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap() {
1199                                    Date(dt.date())
1200                                } else {
1201                                    DateTime(dt)
1202                                }
1203                            }
1204                            Err(e) => Error(e),
1205                        }
1206                    }
1207                    Err(e) => Error(e),
1208                }
1209            };
1210
1211            // Date +/- number => date (propagate temporal tag)
1212            if let Some(ls) = date_like_serial(&l) {
1213                match op {
1214                    '+' => {
1215                        let rn = to_num(&r)?;
1216                        return Ok(serial_to_literal(ls + rn));
1217                    }
1218                    '-' => {
1219                        // Date - Date => numeric day delta (Excel-compatible)
1220                        if let Some(rs) = date_like_serial(&r) {
1221                            return Ok(Number(ls - rs));
1222                        }
1223                        let rn = to_num(&r)?;
1224                        return Ok(serial_to_literal(ls - rn));
1225                    }
1226                    _ => unreachable!(),
1227                }
1228            }
1229
1230            // Number + Date => date (commutative)
1231            if op == '+'
1232                && let Some(rs) = date_like_serial(&r)
1233            {
1234                let ln = to_num(&l)?;
1235                return Ok(serial_to_literal(ln + rs));
1236            }
1237
1238            // Fallback: regular numeric operation
1239            self.numeric_binary(l, r, |a, b| if op == '+' { a + b } else { a - b })
1240        })
1241    }
1242
1243    /* ===================  function calls  =================== */
1244    fn eval_function_to_calc(
1245        &self,
1246        name: &str,
1247        args: &[ASTNode],
1248    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1249        if let Some(fun) = self.context.get_function("", name) {
1250            let handles: Vec<ArgumentHandle> =
1251                args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
1252            // Use the function's built-in dispatch method with a narrow FunctionContext
1253            let fctx = DefaultFunctionContext::new_with_sheet(
1254                self.context,
1255                self.current_cell,
1256                self.current_sheet,
1257            );
1258            return fun.dispatch(&handles, &fctx);
1259        }
1260
1261        if let Some(callable) = self.resolve_local_callable(name) {
1262            let mut eval_args = Vec::with_capacity(args.len());
1263            for arg in args {
1264                eval_args.push(self.evaluate_ast(arg)?.into_literal());
1265            }
1266            return callable.invoke(self, &eval_args);
1267        }
1268
1269        // Include the function name in the error message for better debugging
1270        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1271            ExcelError::new(ExcelErrorKind::Name).with_message(format!("Unknown function: {name}")),
1272        )))
1273    }
1274
1275    fn eval_function(&self, name: &str, args: &[ASTNode]) -> Result<LiteralValue, ExcelError> {
1276        self.eval_function_to_calc(name, args)
1277            .map(|cv| cv.into_literal())
1278    }
1279
1280    pub fn function_context(&self, cell_ref: Option<&CellRef>) -> DefaultFunctionContext<'_> {
1281        DefaultFunctionContext::new_with_sheet(self.context, cell_ref.cloned(), self.current_sheet)
1282    }
1283
1284    /* ===================  array literal  =================== */
1285    fn eval_array_literal_to_calc(
1286        &self,
1287        rows: &[Vec<ASTNode>],
1288    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1289        let mut out = Vec::with_capacity(rows.len());
1290        for row in rows {
1291            let mut r = Vec::with_capacity(row.len());
1292            for cell in row {
1293                r.push(self.evaluate_ast(cell)?.into_literal());
1294            }
1295            out.push(r);
1296        }
1297        Ok(crate::traits::CalcValue::Range(
1298            crate::engine::range_view::RangeView::from_owned_rows(out, self.context.date_system()),
1299        ))
1300    }
1301
1302    fn eval_array_literal(&self, rows: &[Vec<ASTNode>]) -> Result<LiteralValue, ExcelError> {
1303        self.eval_array_literal_to_calc(rows)
1304            .map(|cv| cv.into_literal())
1305    }
1306
1307    /* ===================  helpers  =================== */
1308    fn numeric_binary<F>(
1309        &self,
1310        left: LiteralValue,
1311        right: LiteralValue,
1312        f: F,
1313    ) -> Result<LiteralValue, ExcelError>
1314    where
1315        F: Fn(f64, f64) -> f64 + Copy,
1316    {
1317        self.broadcast_apply(left, right, |l, r| {
1318            let a = crate::coercion::to_arithmetic_number_with_locale(
1319                &l,
1320                &self.context.locale(),
1321                self.context.date_system(),
1322            );
1323            let b = crate::coercion::to_arithmetic_number_with_locale(
1324                &r,
1325                &self.context.locale(),
1326                self.context.date_system(),
1327            );
1328            match (a, b) {
1329                (Ok(a), Ok(b)) => match crate::coercion::sanitize_numeric(f(a, b)) {
1330                    Ok(n2) => Ok(LiteralValue::Number(n2)),
1331                    Err(e) => Ok(LiteralValue::Error(e)),
1332                },
1333                (Err(e), _) | (_, Err(e)) => Ok(LiteralValue::Error(e)),
1334            }
1335        })
1336    }
1337
1338    fn divide(&self, left: LiteralValue, right: LiteralValue) -> Result<LiteralValue, ExcelError> {
1339        self.broadcast_apply(left, right, |l, r| {
1340            let ln = crate::coercion::to_arithmetic_number_with_locale(
1341                &l,
1342                &self.context.locale(),
1343                self.context.date_system(),
1344            );
1345            let rn = crate::coercion::to_arithmetic_number_with_locale(
1346                &r,
1347                &self.context.locale(),
1348                self.context.date_system(),
1349            );
1350            let (a, b) = match (ln, rn) {
1351                (Ok(a), Ok(b)) => (a, b),
1352                (Err(e), _) | (_, Err(e)) => return Ok(LiteralValue::Error(e)),
1353            };
1354            if b == 0.0 {
1355                return Ok(LiteralValue::Error(ExcelError::from_error_string(
1356                    "#DIV/0!",
1357                )));
1358            }
1359            match crate::coercion::sanitize_numeric(a / b) {
1360                Ok(n) => Ok(LiteralValue::Number(n)),
1361                Err(e) => Ok(LiteralValue::Error(e)),
1362            }
1363        })
1364    }
1365
1366    fn power(&self, left: LiteralValue, right: LiteralValue) -> Result<LiteralValue, ExcelError> {
1367        self.broadcast_apply(left, right, |l, r| {
1368            let ln = crate::coercion::to_arithmetic_number_with_locale(
1369                &l,
1370                &self.context.locale(),
1371                self.context.date_system(),
1372            );
1373            let rn = crate::coercion::to_arithmetic_number_with_locale(
1374                &r,
1375                &self.context.locale(),
1376                self.context.date_system(),
1377            );
1378            let (a, b) = match (ln, rn) {
1379                (Ok(a), Ok(b)) => (a, b),
1380                (Err(e), _) | (_, Err(e)) => return Ok(LiteralValue::Error(e)),
1381            };
1382            // Excel domain: negative base with non-integer exponent -> #NUM!
1383            if a < 0.0 && b.fract() != 0.0 {
1384                return Ok(LiteralValue::Error(ExcelError::new_num()));
1385            }
1386            match crate::coercion::sanitize_numeric(a.powf(b)) {
1387                Ok(n) => Ok(LiteralValue::Number(n)),
1388                Err(e) => Ok(LiteralValue::Error(e)),
1389            }
1390        })
1391    }
1392
1393    fn map_array<F>(&self, arr: Vec<Vec<LiteralValue>>, f: F) -> Result<LiteralValue, ExcelError>
1394    where
1395        F: Fn(LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
1396    {
1397        let mut out = Vec::with_capacity(arr.len());
1398        for row in arr {
1399            let mut new_row = Vec::with_capacity(row.len());
1400            for cell in row {
1401                new_row.push(match f(cell) {
1402                    Ok(v) => v,
1403                    Err(e) => LiteralValue::Error(e),
1404                });
1405            }
1406            out.push(new_row);
1407        }
1408        Ok(LiteralValue::Array(out))
1409    }
1410
1411    fn combine_arrays<F>(
1412        &self,
1413        l: Vec<Vec<LiteralValue>>,
1414        r: Vec<Vec<LiteralValue>>,
1415        f: F,
1416    ) -> Result<LiteralValue, ExcelError>
1417    where
1418        F: Fn(LiteralValue, LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
1419    {
1420        // Use strict broadcasting across dimensions
1421        let l_shape = (l.len(), l.first().map(|r| r.len()).unwrap_or(0));
1422        let r_shape = (r.len(), r.first().map(|r| r.len()).unwrap_or(0));
1423        let target = match broadcast_shape(&[l_shape, r_shape]) {
1424            Ok(s) => s,
1425            Err(e) => return Ok(LiteralValue::Error(e)),
1426        };
1427
1428        let mut out = Vec::with_capacity(target.0);
1429        for i in 0..target.0 {
1430            let mut row = Vec::with_capacity(target.1);
1431            for j in 0..target.1 {
1432                let (li, lj) = project_index((i, j), l_shape);
1433                let (ri, rj) = project_index((i, j), r_shape);
1434                let lv = l
1435                    .get(li)
1436                    .and_then(|r| r.get(lj))
1437                    .cloned()
1438                    .unwrap_or(LiteralValue::Empty);
1439                let rv = r
1440                    .get(ri)
1441                    .and_then(|r| r.get(rj))
1442                    .cloned()
1443                    .unwrap_or(LiteralValue::Empty);
1444                row.push(match f(lv, rv) {
1445                    Ok(v) => v,
1446                    Err(e) => LiteralValue::Error(e),
1447                });
1448            }
1449            out.push(row);
1450        }
1451        Ok(LiteralValue::Array(out))
1452    }
1453
1454    fn broadcast_apply<F>(
1455        &self,
1456        left: LiteralValue,
1457        right: LiteralValue,
1458        f: F,
1459    ) -> Result<LiteralValue, ExcelError>
1460    where
1461        F: Fn(LiteralValue, LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
1462    {
1463        use LiteralValue::*;
1464        match (left, right) {
1465            (Array(l), Array(r)) => self.combine_arrays(l, r, f),
1466            (Array(arr), v) => {
1467                let shape_l = (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0));
1468                let shape_r = (1usize, 1usize);
1469                let target = match broadcast_shape(&[shape_l, shape_r]) {
1470                    Ok(s) => s,
1471                    Err(e) => return Ok(LiteralValue::Error(e)),
1472                };
1473                let mut out = Vec::with_capacity(target.0);
1474                for i in 0..target.0 {
1475                    let mut row = Vec::with_capacity(target.1);
1476                    for j in 0..target.1 {
1477                        let (li, lj) = project_index((i, j), shape_l);
1478                        let lv = arr
1479                            .get(li)
1480                            .and_then(|r| r.get(lj))
1481                            .cloned()
1482                            .unwrap_or(LiteralValue::Empty);
1483                        row.push(match f(lv, v.clone()) {
1484                            Ok(vv) => vv,
1485                            Err(e) => LiteralValue::Error(e),
1486                        });
1487                    }
1488                    out.push(row);
1489                }
1490                Ok(LiteralValue::Array(out))
1491            }
1492            (v, Array(arr)) => {
1493                let shape_l = (1usize, 1usize);
1494                let shape_r = (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0));
1495                let target = match broadcast_shape(&[shape_l, shape_r]) {
1496                    Ok(s) => s,
1497                    Err(e) => return Ok(LiteralValue::Error(e)),
1498                };
1499                let mut out = Vec::with_capacity(target.0);
1500                for i in 0..target.0 {
1501                    let mut row = Vec::with_capacity(target.1);
1502                    for j in 0..target.1 {
1503                        let (ri, rj) = project_index((i, j), shape_r);
1504                        let rv = arr
1505                            .get(ri)
1506                            .and_then(|r| r.get(rj))
1507                            .cloned()
1508                            .unwrap_or(LiteralValue::Empty);
1509                        row.push(match f(v.clone(), rv) {
1510                            Ok(vv) => vv,
1511                            Err(e) => LiteralValue::Error(e),
1512                        });
1513                    }
1514                    out.push(row);
1515                }
1516                Ok(LiteralValue::Array(out))
1517            }
1518            (l, r) => f(l, r),
1519        }
1520    }
1521
1522    /* ---------- coercion helpers ---------- */
1523    fn coerce_number(&self, v: &LiteralValue) -> Result<f64, ExcelError> {
1524        coercion::to_number_lenient(v)
1525    }
1526
1527    fn coerce_text(&self, v: &LiteralValue) -> String {
1528        coercion::to_text_invariant(v)
1529    }
1530
1531    /* ---------- comparison ---------- */
1532    fn compare(
1533        &self,
1534        op: &str,
1535        left: LiteralValue,
1536        right: LiteralValue,
1537    ) -> Result<LiteralValue, ExcelError> {
1538        use LiteralValue::*;
1539        if matches!(left, Error(_)) {
1540            return Ok(left);
1541        }
1542        if matches!(right, Error(_)) {
1543            return Ok(right);
1544        }
1545
1546        // arrays: element‑wise with broadcasting
1547        match (left, right) {
1548            (Array(l), Array(r)) => self.combine_arrays(l, r, |a, b| self.compare(op, a, b)),
1549            (Array(arr), v) => self.broadcast_apply(Array(arr), v, |a, b| self.compare(op, a, b)),
1550            (v, Array(arr)) => self.broadcast_apply(v, Array(arr), |a, b| self.compare(op, a, b)),
1551            (l, r) => {
1552                let res = match (l, r) {
1553                    (Number(a), Number(b)) => self.cmp_f64(a, b, op),
1554                    (Int(a), Number(b)) => self.cmp_f64(a as f64, b, op),
1555                    (Number(a), Int(b)) => self.cmp_f64(a, b as f64, op),
1556                    (Boolean(a), Boolean(b)) => {
1557                        self.cmp_f64(if a { 1.0 } else { 0.0 }, if b { 1.0 } else { 0.0 }, op)
1558                    }
1559                    (Text(a), Text(b)) => self.cmp_text(&a, &b, op),
1560                    (a, b) => {
1561                        // fallback to numeric coercion or text compare
1562                        let an = crate::coercion::to_number_lenient_with_locale(
1563                            &a,
1564                            &self.context.locale(),
1565                        )
1566                        .ok();
1567                        let bn = crate::coercion::to_number_lenient_with_locale(
1568                            &b,
1569                            &self.context.locale(),
1570                        )
1571                        .ok();
1572                        if let (Some(a), Some(b)) = (an, bn) {
1573                            self.cmp_f64(a, b, op)
1574                        } else {
1575                            self.cmp_text(
1576                                &crate::coercion::to_text_invariant(&a),
1577                                &crate::coercion::to_text_invariant(&b),
1578                                op,
1579                            )
1580                        }
1581                    }
1582                };
1583                Ok(LiteralValue::Boolean(res))
1584            }
1585        }
1586    }
1587
1588    fn cmp_f64(&self, a: f64, b: f64, op: &str) -> bool {
1589        match op {
1590            "=" => a == b,
1591            "<>" => a != b,
1592            ">" => a > b,
1593            "<" => a < b,
1594            ">=" => a >= b,
1595            "<=" => a <= b,
1596            _ => unreachable!(),
1597        }
1598    }
1599    fn cmp_text(&self, a: &str, b: &str, op: &str) -> bool {
1600        let loc = self.context.locale();
1601        let (a, b) = (loc.fold_case_invariant(a), loc.fold_case_invariant(b));
1602        self.cmp_f64(
1603            a.cmp(&b) as i32 as f64,
1604            0.0,
1605            match op {
1606                "=" => "=",
1607                "<>" => "<>",
1608                ">" => ">",
1609                "<" => "<",
1610                ">=" => ">=",
1611                "<=" => "<=",
1612                _ => unreachable!(),
1613            },
1614        )
1615    }
1616}
1617
1618fn relocate_reference_for_offset(
1619    reference: &ReferenceType,
1620    row_delta: i64,
1621    col_delta: i64,
1622) -> Result<ReferenceType, ExcelError> {
1623    match reference {
1624        ReferenceType::Cell {
1625            sheet,
1626            row,
1627            col,
1628            row_abs,
1629            col_abs,
1630        } => Ok(ReferenceType::Cell {
1631            sheet: sheet.clone(),
1632            row: shift_axis_for_offset(*row, row_delta, *row_abs)?,
1633            col: shift_axis_for_offset(*col, col_delta, *col_abs)?,
1634            row_abs: *row_abs,
1635            col_abs: *col_abs,
1636        }),
1637        ReferenceType::Range {
1638            sheet,
1639            start_row,
1640            start_col,
1641            end_row,
1642            end_col,
1643            start_row_abs,
1644            start_col_abs,
1645            end_row_abs,
1646            end_col_abs,
1647        } => Ok(ReferenceType::Range {
1648            sheet: sheet.clone(),
1649            start_row: shift_optional_axis_for_offset(*start_row, row_delta, *start_row_abs)?,
1650            start_col: shift_optional_axis_for_offset(*start_col, col_delta, *start_col_abs)?,
1651            end_row: shift_optional_axis_for_offset(*end_row, row_delta, *end_row_abs)?,
1652            end_col: shift_optional_axis_for_offset(*end_col, col_delta, *end_col_abs)?,
1653            start_row_abs: *start_row_abs,
1654            start_col_abs: *start_col_abs,
1655            end_row_abs: *end_row_abs,
1656            end_col_abs: *end_col_abs,
1657        }),
1658        // Defined names are placement-invariant: a relocated copy of the
1659        // formula references the same name, resolved at evaluation time.
1660        ReferenceType::NamedRange(name) => Ok(ReferenceType::NamedRange(name.clone())),
1661        ReferenceType::Table(_)
1662        | ReferenceType::Cell3D { .. }
1663        | ReferenceType::Range3D { .. }
1664        | ReferenceType::External(_) => Err(unsupported_reference_relocation_error()),
1665    }
1666}
1667
1668fn shift_optional_axis_for_offset(
1669    value: Option<u32>,
1670    delta: i64,
1671    is_absolute: bool,
1672) -> Result<Option<u32>, ExcelError> {
1673    value
1674        .map(|value| shift_axis_for_offset(value, delta, is_absolute))
1675        .transpose()
1676}
1677
1678fn shift_axis_for_offset(value: u32, delta: i64, is_absolute: bool) -> Result<u32, ExcelError> {
1679    if is_absolute {
1680        return Ok(value);
1681    }
1682    let shifted = i64::from(value) + delta;
1683    if shifted < 1 || shifted > i64::from(u32::MAX) {
1684        return Err(unsupported_reference_relocation_error());
1685    }
1686    Ok(shifted as u32)
1687}
1688
1689fn unsupported_reference_relocation_error() -> ExcelError {
1690    ExcelError::new(ExcelErrorKind::Ref)
1691        .with_message("Unsupported reference relocation for FormulaPlane span evaluation")
1692}