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