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 formualizer_parse::{BinaryOperator, UnaryOperator};
10use rustc_hash::FxHashMap;
11use std::{borrow::Cow, sync::Arc};
12
13use crate::engine::arena::ast::{CALL_NODE_NAME, SheetKey};
14use crate::engine::arena::{AstNodeData, AstNodeId, CompactRefType, DataStore};
15use crate::engine::sheet_registry::SheetRegistry;
16use crate::engine::template::canonical::LiteralSlotId;
17use crate::engine::used_extent::{
18    ExtentPolicy, OpenRangeBounds, resolve_used_extent_with_fallback,
19};
20
21/// A spill reference whose operand is not a single-cell reference or name.
22fn spill_operand_error() -> ExcelError {
23    ExcelError::new(ExcelErrorKind::Ref)
24        .with_message("Spill reference operand must be a single cell or a name")
25}
26
27/// Postfix calls (`LAMBDA(x,x+1)(B1)`) are parsed and stored but not evaluated;
28/// the parsed tree and its arena copy fail the same way.
29fn call_expression_error() -> ExcelError {
30    ExcelError::new(ExcelErrorKind::NImpl)
31        .with_message("Immediate-invocation calls are not yet supported")
32}
33
34pub(crate) fn probe_range_dimensions<C: EvaluationContext + ?Sized>(
35    context: &C,
36    current_sheet: &str,
37    reference: &ReferenceType,
38) -> Option<(u32, u32)> {
39    match reference {
40        ReferenceType::Range {
41            sheet,
42            start_row,
43            start_col,
44            end_row,
45            end_col,
46            ..
47        } => {
48            let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
49            let extent = resolve_used_extent_with_fallback(
50                OpenRangeBounds {
51                    start_row: *start_row,
52                    start_column: *start_col,
53                    end_row: *end_row,
54                    end_column: *end_col,
55                },
56                ExtentPolicy::EvaluationCompat {
57                    fallback_row: None,
58                    fallback_column: None,
59                },
60                || context.sheet_bounds(sheet_name).map(|bounds| bounds.0),
61                || context.sheet_bounds(sheet_name).map(|bounds| bounds.1),
62                |first, last| context.used_rows_for_columns(sheet_name, first, last),
63                |first, last| context.used_cols_for_rows(sheet_name, first, last),
64            );
65            let Some(extent) = extent else {
66                return Some((0, 0));
67            };
68            Some((
69                extent.end_row - extent.start_row + 1,
70                extent.end_column - extent.start_column + 1,
71            ))
72        }
73        ReferenceType::Cell { .. } => Some((1, 1)),
74        _ => None,
75    }
76}
77
78#[derive(Clone)]
79pub enum LocalBinding {
80    Value(LiteralValue),
81    /// A local bound to a spreadsheet reference expression.
82    ///
83    /// `value` is exactly what [`LocalBinding::Value`] would have carried, so the
84    /// value path is unchanged; `reference` additionally lets a by-ref argument
85    /// slot see the range the local was bound to.
86    ValueWithReference {
87        value: LiteralValue,
88        reference: ReferenceType,
89    },
90    Callable(Arc<dyn crate::traits::CustomCallable>),
91}
92
93#[derive(Clone, Default)]
94pub struct LocalEnv {
95    head: Option<Arc<EnvFrame>>,
96}
97
98#[derive(Clone)]
99struct EnvFrame {
100    parent: Option<Arc<EnvFrame>>,
101    bindings: FxHashMap<String, LocalBinding>,
102}
103
104impl LocalEnv {
105    #[inline(always)]
106    pub fn is_empty(&self) -> bool {
107        self.head.is_none()
108    }
109
110    fn norm(name: &str) -> String {
111        name.to_ascii_uppercase()
112    }
113
114    pub fn lookup(&self, name: &str) -> Option<LocalBinding> {
115        self.head.as_ref()?;
116        let key = Self::norm(name);
117        let mut cur = self.head.as_ref().cloned();
118        while let Some(frame) = cur {
119            if let Some(v) = frame.bindings.get(&key) {
120                return Some(v.clone());
121            }
122            cur = frame.parent.clone();
123        }
124        None
125    }
126
127    pub fn with_binding(&self, name: &str, value: LocalBinding) -> Self {
128        let mut bindings = FxHashMap::default();
129        bindings.insert(Self::norm(name), value);
130        Self {
131            head: Some(Arc::new(EnvFrame {
132                parent: self.head.clone(),
133                bindings,
134            })),
135        }
136    }
137}
138
139#[derive(Clone, Copy)]
140pub(crate) struct InterpreterParameterBindings<'a> {
141    pub(crate) literal_slots_by_node: &'a FxHashMap<AstNodeId, LiteralSlotId>,
142    pub(crate) literal_values: &'a [LiteralValue],
143    /// Program 3: values of a family template's run-invariant calls,
144    /// computed once per run (the same cells for every member).
145    pub(crate) invariant_values: Option<&'a InvariantValues>,
146}
147
148/// Values of run-invariant calls by node (see `InterpreterParameterBindings`).
149pub(crate) type InvariantValues =
150    FxHashMap<AstNodeId, (LiteralValue, Option<crate::format::FormatId>)>;
151
152/// Internal fast path; recording contexts deliberately retain their read hooks.
153pub(crate) trait ArenaCellResolver {
154    fn resolve_arena_range(
155        &self,
156        reference: &CompactRefType,
157        current: crate::SheetId,
158        row_delta: i64,
159        col_delta: i64,
160    ) -> Result<crate::engine::range_view::RangeView<'_>, ExcelError>;
161
162    fn resolve_arena_cell(
163        &self,
164        sheet: Option<SheetKey>,
165        current: crate::SheetId,
166        row: u32,
167        col: u32,
168    ) -> Result<(LiteralValue, Option<crate::format::FormatId>), ExcelError>;
169}
170
171type ResolvedCallable = (
172    Option<std::sync::Arc<dyn crate::function::Function>>,
173    Option<crate::function::FnCaps>,
174);
175
176/// Immutable function snapshot shared by every interpreter in one evaluation request.
177#[derive(Default)]
178pub(crate) struct ResolvedFunctions {
179    functions: Vec<Option<std::sync::Arc<dyn crate::function::Function>>>,
180    slots: rustc_hash::FxHashMap<crate::engine::arena::StringId, usize>,
181    revision: Option<(u64, Option<u64>, u64)>,
182    ast_revision: Option<(u64, Option<u64>, u64, usize, u64)>,
183    ast_functions: rustc_hash::FxHashMap<Box<str>, ResolvedCallable>,
184    capabilities: Vec<Option<crate::function::FnCaps>>,
185}
186
187impl ResolvedFunctions {
188    pub(crate) fn refresh(
189        &mut self,
190        store: &DataStore,
191        provider: &dyn crate::traits::FunctionProvider,
192    ) {
193        let revision = (
194            crate::function_registry::semantic_epoch_lock_free(),
195            provider.planning_semantic_revision(),
196            store.resolution_revision(),
197        );
198        if self.revision != Some(revision) || revision.1.is_none() {
199            self.functions.clear();
200            self.capabilities.clear();
201            self.slots.clear();
202            self.ast_revision = None;
203        }
204        // New names append without invalidating existing slots. Non-function
205        // edits do not change the reset revision or allocate here.
206        for id in store.function_names() {
207            if self.slots.contains_key(&id) {
208                continue;
209            }
210            let name = store.resolve_ast_string(id);
211            let function = if name == CALL_NODE_NAME {
212                None
213            } else {
214                provider.get_function("", name)
215            };
216            let caps = function.as_ref().map(|function| function.caps());
217            self.slots.insert(id, self.functions.len());
218            self.functions.push(function);
219            self.capabilities.push(caps);
220        }
221        // Capture before resolving so racing registration invalidates next pass.
222        self.revision = Some(revision);
223    }
224
225    /// Whether the table describes `store` and `provider` as they are now:
226    /// same registry epoch, a known and unchanged provider revision, same
227    /// arena reset revision, and every function name in the arena resolved.
228    /// Callers outside an evaluation request (which refreshes the table) use
229    /// this to decide between the table and live provider lookups.
230    pub(crate) fn is_current(
231        &self,
232        store: &DataStore,
233        provider: &dyn crate::traits::FunctionProvider,
234    ) -> bool {
235        let Some((epoch, Some(provider_revision), arena_revision)) = self.revision else {
236            return false;
237        };
238        epoch == crate::function_registry::semantic_epoch_lock_free()
239            && provider.planning_semantic_revision() == Some(provider_revision)
240            && store.resolution_revision() == arena_revision
241            && store
242                .function_names()
243                .all(|id| self.slots.contains_key(&id))
244    }
245
246    pub(crate) fn refresh_ast_functions<'n>(
247        &mut self,
248        store: &DataStore,
249        provider: &dyn crate::traits::FunctionProvider,
250        named: impl Iterator<Item = &'n ASTNode>,
251        symbol_revision: u64,
252    ) {
253        let Some((epoch, provider_revision, arena_revision)) = self.revision else {
254            return;
255        };
256        let revision = (
257            epoch,
258            provider_revision,
259            arena_revision,
260            self.functions.len(),
261            symbol_revision,
262        );
263        if self.ast_revision == Some(revision) {
264            return;
265        }
266        fn collect(node: &ASTNode, names: &mut std::collections::BTreeSet<String>) {
267            match &node.node_type {
268                ASTNodeType::Function { name, args } => {
269                    names.insert(name.clone());
270                    for arg in args {
271                        collect(arg, names);
272                    }
273                }
274                ASTNodeType::UnaryOp { expr, .. } => collect(expr, names),
275                ASTNodeType::BinaryOp { left, right, .. } => {
276                    collect(left, names);
277                    collect(right, names);
278                }
279                ASTNodeType::Array(rows) => {
280                    for node in rows.iter().flatten() {
281                        collect(node, names);
282                    }
283                }
284                _ => {}
285            }
286        }
287        let mut names = std::collections::BTreeSet::new();
288        let definitions_changed = self
289            .ast_revision
290            .is_none_or(|previous| previous.4 != symbol_revision);
291        if definitions_changed {
292            for node in named {
293                collect(node, &mut names);
294            }
295            self.ast_functions.clear();
296        }
297        for (id, slot) in &self.slots {
298            let name = store.resolve_ast_string(*id);
299            if self.ast_functions.contains_key(name) {
300                continue;
301            }
302            let function = self.functions.get(*slot).cloned().flatten();
303            let caps = self.capabilities.get(*slot).copied().flatten();
304            self.ast_functions.insert(name.into(), (function, caps));
305        }
306        for name in names {
307            self.ast_functions
308                .entry(name.into_boxed_str())
309                .or_insert_with_key(|name| {
310                    let function = provider.get_function("", name);
311                    let caps = function.as_ref().map(|function| function.caps());
312                    (function, caps)
313                });
314        }
315        self.ast_revision = Some(revision);
316    }
317
318    /// Runtime snapshot lookup for run planners; intentionally not the provider's
319    /// potentially distinct `get_function_for_planning` contract.
320    pub(crate) fn get_named(&self, name: &str) -> Option<&dyn crate::function::Function> {
321        self.ast_functions.get(name)?.0.as_deref()
322    }
323
324    pub(crate) fn get(
325        &self,
326        id: crate::engine::arena::StringId,
327    ) -> Option<&dyn crate::function::Function> {
328        self.functions
329            .get(*self.slots.get(&id)?)
330            .and_then(|f| f.as_deref())
331    }
332}
333
334enum ResolvedFunction<'a> {
335    Borrowed(&'a dyn crate::function::Function),
336    Owned(std::sync::Arc<dyn crate::function::Function>),
337}
338impl std::ops::Deref for ResolvedFunction<'_> {
339    type Target = dyn crate::function::Function;
340    fn deref(&self) -> &Self::Target {
341        match self {
342            Self::Borrowed(f) => *f,
343            Self::Owned(f) => f.as_ref(),
344        }
345    }
346}
347
348pub struct Interpreter<'a> {
349    pub context: &'a dyn EvaluationContext,
350    resolved_functions: Option<&'a ResolvedFunctions>,
351    arena_cells: Option<&'a dyn ArenaCellResolver>,
352    current_sheet: &'a str,
353    current_cell: Option<crate::CellRef>,
354    local_env: LocalEnv,
355    reference_row_delta: i64,
356    reference_col_delta: i64,
357    disable_ast_planner: bool,
358    parameter_bindings: Option<InterpreterParameterBindings<'a>>,
359}
360
361impl<'a> Interpreter<'a> {
362    pub fn new(context: &'a dyn EvaluationContext, current_sheet: &'a str) -> Self {
363        Self {
364            context,
365            resolved_functions: None,
366            arena_cells: None,
367            current_sheet,
368            current_cell: None,
369            local_env: LocalEnv::default(),
370            reference_row_delta: 0,
371            reference_col_delta: 0,
372            disable_ast_planner: false,
373            parameter_bindings: None,
374        }
375    }
376
377    pub fn new_with_cell(
378        context: &'a dyn EvaluationContext,
379        current_sheet: &'a str,
380        cell: crate::CellRef,
381    ) -> Self {
382        Self {
383            context,
384            resolved_functions: None,
385            arena_cells: None,
386            current_sheet,
387            current_cell: Some(cell),
388            local_env: LocalEnv::default(),
389            reference_row_delta: 0,
390            reference_col_delta: 0,
391            disable_ast_planner: false,
392            parameter_bindings: None,
393        }
394    }
395
396    pub(crate) fn with_arena_cells(mut self, resolver: &'a dyn ArenaCellResolver) -> Self {
397        self.arena_cells = Some(resolver);
398        self
399    }
400
401    pub(crate) fn with_resolved_functions(mut self, table: &'a ResolvedFunctions) -> Self {
402        self.resolved_functions = Some(table);
403        self
404    }
405
406    fn ast_function(&self, name: &str) -> Option<ResolvedFunction<'_>> {
407        if let Some(table) = self.resolved_functions
408            && let Some(function) = table.ast_functions.get(name)
409        {
410            return function.0.as_deref().map(ResolvedFunction::Borrowed);
411        }
412        self.context
413            .get_function("", name)
414            .map(ResolvedFunction::Owned)
415    }
416
417    pub(crate) fn ast_function_capabilities(&self, name: &str) -> Option<crate::function::FnCaps> {
418        if let Some(table) = self.resolved_functions
419            && let Some(caps) = table.ast_functions.get(name)
420        {
421            return caps.1;
422        }
423        self.context.function_capabilities("", name)
424    }
425
426    fn ast_function_arc(
427        &self,
428        ns: &str,
429        name: &str,
430    ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
431        if ns.is_empty()
432            && let Some(table) = self.resolved_functions
433            && let Some(function) = table.ast_functions.get(name)
434        {
435            return function.0.clone();
436        }
437        self.context.get_function(ns, name)
438    }
439
440    fn arena_function(
441        &self,
442        id: crate::engine::arena::StringId,
443        store: &DataStore,
444    ) -> Option<ResolvedFunction<'_>> {
445        if let Some(table) = self.resolved_functions {
446            table.get(id).map(ResolvedFunction::Borrowed)
447        } else {
448            let name = store.resolve_ast_string(id);
449            if name == CALL_NODE_NAME {
450                return None;
451            }
452            self.context
453                .get_function("", name)
454                .map(ResolvedFunction::Owned)
455        }
456    }
457
458    pub(crate) fn arena_function_capabilities(
459        &self,
460        id: crate::engine::arena::StringId,
461        store: &DataStore,
462    ) -> Option<crate::function::FnCaps> {
463        if let Some(table) = self.resolved_functions {
464            table
465                .capabilities
466                .get(*table.slots.get(&id)?)
467                .copied()
468                .flatten()
469        } else {
470            self.context
471                .function_capabilities("", store.resolve_ast_string(id))
472        }
473    }
474
475    pub fn current_sheet(&self) -> &'a str {
476        self.current_sheet
477    }
478
479    pub fn local_env(&self) -> &LocalEnv {
480        &self.local_env
481    }
482
483    pub(crate) fn with_current_cell(&self, cell: crate::CellRef) -> Self {
484        Self {
485            context: self.context,
486            resolved_functions: self.resolved_functions,
487            arena_cells: self.arena_cells,
488            current_sheet: self.current_sheet,
489            current_cell: Some(cell),
490            local_env: self.local_env.clone(),
491            reference_row_delta: self.reference_row_delta,
492            reference_col_delta: self.reference_col_delta,
493            disable_ast_planner: self.disable_ast_planner,
494            parameter_bindings: self.parameter_bindings,
495        }
496    }
497
498    pub fn with_local_env(&self, env: LocalEnv) -> Self {
499        Self {
500            context: self.context,
501            resolved_functions: self.resolved_functions,
502            arena_cells: self.arena_cells,
503            current_sheet: self.current_sheet,
504            current_cell: self.current_cell,
505            local_env: env,
506            reference_row_delta: self.reference_row_delta,
507            reference_col_delta: self.reference_col_delta,
508            disable_ast_planner: self.disable_ast_planner,
509            parameter_bindings: self.parameter_bindings,
510        }
511    }
512
513    pub(crate) fn with_parameter_bindings(
514        &self,
515        bindings: InterpreterParameterBindings<'a>,
516    ) -> Self {
517        Self {
518            context: self.context,
519            resolved_functions: self.resolved_functions,
520            arena_cells: self.arena_cells,
521            current_sheet: self.current_sheet,
522            current_cell: self.current_cell,
523            local_env: self.local_env.clone(),
524            reference_row_delta: self.reference_row_delta,
525            reference_col_delta: self.reference_col_delta,
526            disable_ast_planner: self.disable_ast_planner,
527            parameter_bindings: Some(bindings),
528        }
529    }
530
531    fn effective_reference<'r>(
532        &self,
533        reference: &'r ReferenceType,
534    ) -> Result<Cow<'r, ReferenceType>, ExcelError> {
535        if self.reference_row_delta == 0 && self.reference_col_delta == 0 {
536            return Ok(Cow::Borrowed(reference));
537        }
538
539        Ok(Cow::Owned(relocate_reference_for_offset(
540            reference,
541            self.reference_row_delta,
542            self.reference_col_delta,
543        )?))
544    }
545
546    fn resolve_local_reference(
547        &self,
548        reference: &ReferenceType,
549    ) -> Option<crate::traits::CalcValue<'a>> {
550        if self.local_env.is_empty() {
551            return None;
552        }
553        let name = match reference {
554            ReferenceType::NamedRange(name) => name,
555            _ => return None,
556        };
557        match self.local_env.lookup(name)? {
558            LocalBinding::Value(v) => Some(crate::traits::CalcValue::Scalar(v)),
559            LocalBinding::ValueWithReference { value, .. } => {
560                Some(crate::traits::CalcValue::Scalar(value))
561            }
562            LocalBinding::Callable(c) => Some(crate::traits::CalcValue::Callable(c)),
563        }
564    }
565
566    fn resolve_local_callable(&self, name: &str) -> Option<Arc<dyn crate::traits::CustomCallable>> {
567        if self.local_env.is_empty() {
568            return None;
569        }
570        match self.local_env.lookup(name)? {
571            LocalBinding::Callable(c) => Some(c),
572            LocalBinding::Value(_) | LocalBinding::ValueWithReference { .. } => None,
573        }
574    }
575
576    pub fn resolve_local_name(&self, name: &str) -> Option<LocalBinding> {
577        self.local_env.lookup(name)
578    }
579
580    /// The spreadsheet reference a LET/LAMBDA local was bound to, when it was
581    /// bound to a reference expression rather than a computed value.
582    pub(crate) fn resolve_local_bound_reference(&self, name: &str) -> Option<ReferenceType> {
583        if self.local_env.is_empty() {
584            return None;
585        }
586        match self.local_env.lookup(name)? {
587            LocalBinding::ValueWithReference { reference, .. } => Some(reference),
588            LocalBinding::Value(_) | LocalBinding::Callable(_) => None,
589        }
590    }
591
592    pub fn resolve_range_view<'c>(
593        &'c self,
594        reference: &ReferenceType,
595        current_sheet: &str,
596    ) -> Result<crate::engine::range_view::RangeView<'c>, ExcelError> {
597        self.context
598            .resolve_range_view(reference, current_sheet)
599            .map(|view| view.with_cell_budget(self.context.materialized_array_cells()))
600    }
601
602    /// Evaluate an AST node in a reference context and return a ReferenceType.
603    /// This is used for range combinators (e.g., ":"), by-ref argument flows,
604    /// and spill planning. Functions that can return references must set
605    /// `FnCaps::RETURNS_REFERENCE` and override `eval_reference`.
606    pub fn evaluate_ast_as_reference(&self, node: &ASTNode) -> Result<ReferenceType, ExcelError> {
607        match &node.node_type {
608            ASTNodeType::Reference { reference, .. } => {
609                self.reference_for_current_offset(reference)
610            }
611            ASTNodeType::Function { name, args } => {
612                if let Some(fun) = self.ast_function(name) {
613                    // Build handles; allow function to decide reference semantics
614                    let handles: smallvec::SmallVec<[ArgumentHandle; 4]> =
615                        args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
616                    let fctx = DefaultFunctionContext::new_with_sheet(
617                        self.context,
618                        None,
619                        self.current_sheet,
620                    );
621                    if let Some(res) = fun.eval_reference(&handles, &fctx) {
622                        res
623                    } else {
624                        Err(ExcelError::new(ExcelErrorKind::Ref)
625                            .with_message("Function does not return a reference"))
626                    }
627                } else {
628                    Err(ExcelError::new(ExcelErrorKind::Name)
629                        .with_message(format!("Unknown function: {name}")))
630                }
631            }
632            ASTNodeType::BinaryOp { op, left, right } if *op == BinaryOperator::Range => {
633                let lref = self.evaluate_ast_as_reference(left)?;
634                let rref = self.evaluate_ast_as_reference(right)?;
635                crate::reference::combine_references(&lref, &rref)
636            }
637            ASTNodeType::UnaryOp { op, expr } if *op == UnaryOperator::Spill => {
638                self.ast_spill_reference(expr)
639            }
640            ASTNodeType::Array(_)
641            | ASTNodeType::UnaryOp { .. }
642            | ASTNodeType::BinaryOp { .. }
643            | ASTNodeType::Call { .. }
644            | ASTNodeType::Literal(_)
645            | ASTNodeType::Omitted => Err(ExcelError::new(ExcelErrorKind::Ref)
646                .with_message("Expression cannot be used as a reference")),
647        }
648    }
649
650    pub(crate) fn try_evaluate_ast_as_reference(
651        &self,
652        node: &ASTNode,
653    ) -> Option<Result<ReferenceType, ExcelError>> {
654        let ASTNodeType::Function { name, args } = &node.node_type else {
655            return Some(self.evaluate_ast_as_reference(node));
656        };
657        let fun = match self.ast_function(name) {
658            Some(fun) => fun,
659            None => {
660                return Some(Err(ExcelError::new(ExcelErrorKind::Name)
661                    .with_message(format!("Unknown function: {name}"))));
662            }
663        };
664        let handles: smallvec::SmallVec<[ArgumentHandle; 4]> = args
665            .iter()
666            .map(|arg| ArgumentHandle::new(arg, self))
667            .collect();
668        let fctx = DefaultFunctionContext::new_with_sheet(self.context, None, self.current_sheet);
669        fun.eval_reference(&handles, &fctx)
670    }
671
672    pub(crate) fn evaluate_arena_ast_as_reference(
673        &self,
674        node_id: AstNodeId,
675        data_store: &DataStore,
676        sheet_registry: &SheetRegistry,
677    ) -> Result<ReferenceType, ExcelError> {
678        let node = data_store.get_node(node_id).ok_or_else(|| {
679            ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
680        })?;
681
682        match node {
683            AstNodeData::Reference { ref_type, .. } => {
684                let ref_type = data_store.compact_ref(*ref_type);
685                let reference =
686                    data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry);
687                self.reference_for_current_offset(&reference)
688            }
689            AstNodeData::Function { name_id, .. } => {
690                let fun = self.arena_function(*name_id, data_store).ok_or_else(|| {
691                    let name = data_store.resolve_ast_string(*name_id);
692                    if name == CALL_NODE_NAME {
693                        call_expression_error()
694                    } else {
695                        ExcelError::new(ExcelErrorKind::Name)
696                            .with_message(format!("Unknown function: {name}"))
697                    }
698                })?;
699
700                let args = data_store.get_args(node_id).ok_or_else(|| {
701                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
702                })?;
703
704                let handles: smallvec::SmallVec<[ArgumentHandle; 4]> = args
705                    .iter()
706                    .copied()
707                    .map(|arg_id| {
708                        ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry)
709                    })
710                    .collect();
711
712                let fctx =
713                    DefaultFunctionContext::new_with_sheet(self.context, None, self.current_sheet);
714
715                fun.eval_reference(&handles, &fctx).ok_or_else(|| {
716                    ExcelError::new(ExcelErrorKind::Ref)
717                        .with_message("Function does not return a reference")
718                })?
719            }
720            AstNodeData::BinaryOp {
721                operator,
722                left_id,
723                right_id,
724            } => {
725                let op = *operator;
726                if op != BinaryOperator::Range {
727                    return Err(ExcelError::new(ExcelErrorKind::Ref)
728                        .with_message("Expression cannot be used as a reference"));
729                }
730                let lref =
731                    self.evaluate_arena_ast_as_reference(*left_id, data_store, sheet_registry)?;
732                let rref =
733                    self.evaluate_arena_ast_as_reference(*right_id, data_store, sheet_registry)?;
734                crate::reference::combine_references(&lref, &rref)
735            }
736            AstNodeData::UnaryOp { operator, expr_id } if *operator == UnaryOperator::Spill => {
737                self.arena_spill_reference(*expr_id, data_store, sheet_registry)
738            }
739            _ => Err(ExcelError::new(ExcelErrorKind::Ref)
740                .with_message("Expression cannot be used as a reference")),
741        }
742    }
743
744    /// The anchor a spill operand names: a single-cell reference or a name,
745    /// including a LET/LAMBDA local bound to one. Anything else is `#REF!`.
746    fn spill_anchor_operand(&self, reference: &ReferenceType) -> Result<ReferenceType, ExcelError> {
747        let anchor = match reference {
748            ReferenceType::NamedRange(name) if self.resolve_local_name(name).is_some() => self
749                .resolve_local_bound_reference(name)
750                .ok_or_else(spill_operand_error)?,
751            _ => self
752                .reference_for_current_offset(reference)
753                .map_err(|_| spill_operand_error())?,
754        };
755        match anchor {
756            ReferenceType::Cell { .. } | ReferenceType::NamedRange(_) => Ok(anchor),
757            _ => Err(spill_operand_error()),
758        }
759    }
760
761    /// Resolve a spill reference (`A1#`, `ANCHORARRAY(A1)`) whose operand is
762    /// the written `reference`, to the anchor's current spill rectangle.
763    pub(crate) fn resolve_spill_reference(
764        &self,
765        reference: &ReferenceType,
766    ) -> Result<ReferenceType, ExcelError> {
767        let anchor = self.spill_anchor_operand(reference)?;
768        self.context
769            .resolve_spill_reference(&anchor, self.current_sheet)
770    }
771
772    /// The `#` operator on an AST operand: only a written reference qualifies.
773    pub(crate) fn ast_spill_reference(
774        &self,
775        operand: &ASTNode,
776    ) -> Result<ReferenceType, ExcelError> {
777        match &operand.node_type {
778            ASTNodeType::Reference { reference, .. } => self.resolve_spill_reference(reference),
779            _ => Err(spill_operand_error()),
780        }
781    }
782
783    /// The `#` operator on an arena operand: only a written reference qualifies.
784    pub(crate) fn arena_spill_reference(
785        &self,
786        operand: AstNodeId,
787        data_store: &DataStore,
788        sheet_registry: &SheetRegistry,
789    ) -> Result<ReferenceType, ExcelError> {
790        match data_store.get_node(operand) {
791            Some(AstNodeData::Reference { ref_type, .. }) => {
792                let ref_type = data_store.compact_ref(*ref_type);
793                let reference =
794                    data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry);
795                self.resolve_spill_reference(&reference)
796            }
797            _ => Err(spill_operand_error()),
798        }
799    }
800
801    pub(crate) fn try_evaluate_arena_ast_as_reference(
802        &self,
803        node_id: AstNodeId,
804        data_store: &DataStore,
805        sheet_registry: &SheetRegistry,
806    ) -> Option<Result<ReferenceType, ExcelError>> {
807        let node = match data_store.get_node(node_id) {
808            Some(node) => node,
809            None => {
810                return Some(Err(
811                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
812                ));
813            }
814        };
815        let AstNodeData::Function { name_id, .. } = node else {
816            return Some(self.evaluate_arena_ast_as_reference(node_id, data_store, sheet_registry));
817        };
818        let fun = match self.arena_function(*name_id, data_store) {
819            Some(fun) => fun,
820            None => {
821                let name = data_store.resolve_ast_string(*name_id);
822                if name == CALL_NODE_NAME {
823                    return Some(Err(call_expression_error()));
824                }
825                return Some(Err(ExcelError::new(ExcelErrorKind::Name)
826                    .with_message(format!("Unknown function: {name}"))));
827            }
828        };
829        let args = match data_store.get_args(node_id) {
830            Some(args) => args,
831            None => {
832                return Some(Err(
833                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
834                ));
835            }
836        };
837        let handles: smallvec::SmallVec<[ArgumentHandle; 4]> = args
838            .iter()
839            .copied()
840            .map(|arg_id| ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry))
841            .collect();
842        let fctx = DefaultFunctionContext::new_with_sheet(self.context, None, self.current_sheet);
843        fun.eval_reference(&handles, &fctx)
844    }
845
846    /* ===================  public  =================== */
847    pub fn evaluate_ast(&self, node: &ASTNode) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
848        self.evaluate_ast_uncached(node)
849    }
850
851    pub(crate) fn evaluate_ast_with_offset(
852        &self,
853        node: &ASTNode,
854        row_delta: i64,
855        col_delta: i64,
856    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
857        let offset = Self {
858            context: self.context,
859            resolved_functions: self.resolved_functions,
860            arena_cells: self.arena_cells,
861            current_sheet: self.current_sheet,
862            current_cell: self.current_cell,
863            local_env: self.local_env.clone(),
864            reference_row_delta: row_delta,
865            reference_col_delta: col_delta,
866            disable_ast_planner: true,
867            parameter_bindings: self.parameter_bindings,
868        };
869        offset.evaluate_ast_uncached(node)
870    }
871
872    pub(crate) fn reference_for_current_offset(
873        &self,
874        reference: &ReferenceType,
875    ) -> Result<ReferenceType, ExcelError> {
876        self.effective_reference(reference)
877            .map(|reference| reference.into_owned())
878    }
879
880    /// Evaluate a formula given as a template plus offset (a compressed
881    /// family member, or a formula on its own cell at offset zero).
882    pub(crate) fn evaluate_formula_view(
883        &self,
884        view: crate::engine::graph::FormulaView,
885        data_store: &DataStore,
886        sheet_registry: &SheetRegistry,
887    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
888        if view.row_delta == 0 && view.col_delta == 0 {
889            self.evaluate_arena_ast(view.template, data_store, sheet_registry)
890        } else {
891            self.evaluate_arena_ast_with_offset(
892                view.template,
893                view.row_delta,
894                view.col_delta,
895                data_store,
896                sheet_registry,
897            )
898        }
899    }
900
901    pub(crate) fn evaluate_arena_ast_with_offset(
902        &self,
903        node_id: AstNodeId,
904        row_delta: i64,
905        col_delta: i64,
906        data_store: &DataStore,
907        sheet_registry: &SheetRegistry,
908    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
909        let offset = Self {
910            context: self.context,
911            resolved_functions: self.resolved_functions,
912            arena_cells: self.arena_cells,
913            current_sheet: self.current_sheet,
914            current_cell: self.current_cell,
915            local_env: self.local_env.clone(),
916            reference_row_delta: row_delta,
917            reference_col_delta: col_delta,
918            disable_ast_planner: true,
919            parameter_bindings: self.parameter_bindings,
920        };
921        offset.evaluate_arena_ast(node_id, data_store, sheet_registry)
922    }
923
924    #[inline]
925    fn annotated(
926        value: LiteralValue,
927        format: Option<crate::format::FormatId>,
928    ) -> crate::traits::CalcValue<'a> {
929        match format {
930            Some(format) => crate::traits::CalcValue::AnnotatedScalar(value, format),
931            None => crate::traits::CalcValue::Scalar(value),
932        }
933    }
934
935    fn annotate_cell_value(
936        &self,
937        sheet: Option<&str>,
938        row: u32,
939        col: u32,
940        value: LiteralValue,
941    ) -> crate::traits::CalcValue<'a> {
942        match self
943            .context
944            .resolve_cell_format(sheet, row, col, self.current_sheet)
945        {
946            Some(format) => crate::traits::CalcValue::AnnotatedScalar(value, format),
947            None => crate::traits::CalcValue::Scalar(value),
948        }
949    }
950
951    fn binary_format(
952        &self,
953        op: char,
954        left: Option<crate::format::FormatId>,
955        right: Option<crate::format::FormatId>,
956    ) -> Option<crate::format::FormatId> {
957        use formualizer_common::numfmt::FormatClass;
958        let class =
959            |id: Option<crate::format::FormatId>| id.and_then(|id| self.context.format_class(id));
960        let left = class(left);
961        let right = class(right);
962        let is_plain = |class: &Option<FormatClass>| {
963            matches!(
964                class,
965                None | Some(FormatClass::General | FormatClass::Number { .. })
966            )
967        };
968        // This table is intentionally closed. LibreOffice measurement establishes
969        // Date+Time and Date+Percent; unlisted pairs (including Date+Date,
970        // Duration+Date, Date+Currency, DateTime+Time, and Date+Text) drop the
971        // annotation rather than guessing a display class.
972        match (op, left.as_ref(), right.as_ref()) {
973            ('+', Some(FormatClass::Date), Some(FormatClass::Time))
974            | ('+', Some(FormatClass::Time), Some(FormatClass::Date)) => {
975                Some(crate::format::FormatId::DATETIME)
976            }
977            ('+', Some(FormatClass::Date), Some(FormatClass::Percent { .. }))
978            | ('+', Some(FormatClass::Percent { .. }), Some(FormatClass::Date)) => {
979                Some(crate::format::FormatId::DATE)
980            }
981            ('+' | '-', Some(FormatClass::Date), r) if is_plain(&r.cloned()) => {
982                Some(crate::format::FormatId::DATE)
983            }
984            ('+', l, Some(FormatClass::Date)) if is_plain(&l.cloned()) => {
985                Some(crate::format::FormatId::DATE)
986            }
987            ('+' | '-', Some(FormatClass::Time), r) if is_plain(&r.cloned()) => {
988                Some(crate::format::FormatId::TIME)
989            }
990            ('+', l, Some(FormatClass::Time)) if is_plain(&l.cloned()) => {
991                Some(crate::format::FormatId::TIME)
992            }
993            ('+' | '-', Some(FormatClass::DateTime), r) if is_plain(&r.cloned()) => {
994                Some(crate::format::FormatId::DATETIME)
995            }
996            ('+', l, Some(FormatClass::DateTime)) if is_plain(&l.cloned()) => {
997                Some(crate::format::FormatId::DATETIME)
998            }
999            ('+' | '-', Some(FormatClass::Duration), r) if is_plain(&r.cloned()) => {
1000                Some(crate::format::FormatId::DURATION)
1001            }
1002            ('+', l, Some(FormatClass::Duration)) if is_plain(&l.cloned()) => {
1003                Some(crate::format::FormatId::DURATION)
1004            }
1005            _ => None,
1006        }
1007    }
1008
1009    fn annotate_numeric_result(
1010        &self,
1011        value: LiteralValue,
1012        format: Option<crate::format::FormatId>,
1013    ) -> crate::traits::CalcValue<'a> {
1014        match (value, format) {
1015            (value @ LiteralValue::Number(_), Some(format)) => {
1016                crate::traits::CalcValue::AnnotatedScalar(value, format)
1017            }
1018            (value, _) => crate::traits::CalcValue::Scalar(value),
1019        }
1020    }
1021
1022    /// A unary operator other than `@` on an evaluated operand (shared by
1023    /// the AST walk and the elementwise lift).
1024    pub(crate) fn apply_unary_op(
1025        &self,
1026        op: UnaryOperator,
1027        expr: crate::traits::CalcValue<'a>,
1028    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1029        // For now, materialize for operators. Future: virtual range ops.
1030        let v = expr.into_literal();
1031        match v {
1032            LiteralValue::Array(arr) => self
1033                .map_array(arr, |cell| self.eval_unary_scalar(op, cell))
1034                .map(crate::traits::CalcValue::Scalar),
1035            other => self
1036                .eval_unary_scalar(op, other)
1037                .map(crate::traits::CalcValue::Scalar),
1038        }
1039    }
1040
1041    /// A binary operator other than `:` on evaluated operands and their
1042    /// format annotations (shared by the AST walk and the elementwise lift).
1043    pub(crate) fn apply_binary_op(
1044        &self,
1045        op: BinaryOperator,
1046        left: LiteralValue,
1047        left_format: Option<crate::format::FormatId>,
1048        right: LiteralValue,
1049        right_format: Option<crate::format::FormatId>,
1050    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1051        if matches!(
1052            op,
1053            BinaryOperator::Equal
1054                | BinaryOperator::NotEqual
1055                | BinaryOperator::Greater
1056                | BinaryOperator::Less
1057                | BinaryOperator::GreaterEqual
1058                | BinaryOperator::LessEqual
1059        ) {
1060            return self
1061                .compare(op, left, right)
1062                .map(crate::traits::CalcValue::Scalar);
1063        }
1064
1065        match op {
1066            BinaryOperator::Add | BinaryOperator::Subtract => {
1067                let byte = if op == BinaryOperator::Add {
1068                    b'+'
1069                } else {
1070                    b'-'
1071                };
1072                let value = self.numeric_binary(left, right, byte)?;
1073                if left_format.is_none() && right_format.is_none() {
1074                    // The closed temporal-format algebra cannot produce an
1075                    // annotation from two unformatted operands. Avoid both its
1076                    // classification/matching and the numeric annotation pass.
1077                    Ok(crate::traits::CalcValue::Scalar(value))
1078                } else {
1079                    Ok(self.annotate_numeric_result(
1080                        value,
1081                        self.binary_format(char::from(byte), left_format, right_format),
1082                    ))
1083                }
1084            }
1085            BinaryOperator::Multiply => self
1086                .numeric_binary(left, right, b'*')
1087                .map(crate::traits::CalcValue::Scalar),
1088            BinaryOperator::Divide => self
1089                .numeric_binary(left, right, b'/')
1090                .map(crate::traits::CalcValue::Scalar),
1091            BinaryOperator::Power => self
1092                .numeric_binary(left, right, b'^')
1093                .map(crate::traits::CalcValue::Scalar),
1094            BinaryOperator::Concat => self
1095                .concat_values(left, right)
1096                .map(crate::traits::CalcValue::Scalar),
1097            _ => {
1098                Err(ExcelError::new(ExcelErrorKind::NImpl)
1099                    .with_message(format!("Binary op '{op}'")))
1100            }
1101        }
1102    }
1103
1104    /// Resolve only a direct compact range, retaining local/recording fallbacks.
1105    /// The argument caller attaches its context's cancellation and cell budget.
1106    pub(crate) fn resolve_arena_argument_range(
1107        &self,
1108        node_id: AstNodeId,
1109        data_store: &DataStore,
1110    ) -> Option<Result<crate::engine::range_view::RangeView<'a>, ExcelError>> {
1111        let resolver = self.arena_cells?;
1112        let cell = self.current_cell?;
1113        if !self.local_env.is_empty() {
1114            return None;
1115        }
1116        let AstNodeData::Reference { ref_type, .. } = data_store.get_node(node_id)? else {
1117            return None;
1118        };
1119        let reference = data_store.compact_ref(*ref_type);
1120        if !matches!(reference, CompactRefType::Range { .. }) {
1121            return None;
1122        }
1123        Some(resolver.resolve_arena_range(
1124            reference,
1125            cell.sheet_id,
1126            self.reference_row_delta,
1127            self.reference_col_delta,
1128        ))
1129    }
1130
1131    pub(crate) fn evaluate_arena_ast(
1132        &self,
1133        node_id: AstNodeId,
1134        data_store: &DataStore,
1135        sheet_registry: &SheetRegistry,
1136    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1137        let node = data_store.get_node(node_id).ok_or_else(|| {
1138            ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
1139        })?;
1140
1141        match node {
1142            AstNodeData::Literal(vref) => {
1143                if let Some(bindings) = self.parameter_bindings
1144                    && let Some(slot_id) = bindings.literal_slots_by_node.get(&node_id)
1145                    && let Some(value) = bindings.literal_values.get(slot_id.0 as usize)
1146                {
1147                    return Ok(crate::traits::CalcValue::Scalar(value.clone()));
1148                }
1149                Ok(crate::traits::CalcValue::Scalar(
1150                    data_store.retrieve_value(*vref),
1151                ))
1152            }
1153            AstNodeData::Omitted => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
1154            AstNodeData::Reference { ref_type, .. } => {
1155                let ref_type = data_store.compact_ref(*ref_type);
1156                if self.local_env.is_empty()
1157                    && matches!(ref_type, CompactRefType::Range { .. })
1158                    && let (Some(resolver), Some(cell)) = (self.arena_cells, self.current_cell)
1159                {
1160                    let view = resolver
1161                        .resolve_arena_range(
1162                            ref_type,
1163                            cell.sheet_id,
1164                            self.reference_row_delta,
1165                            self.reference_col_delta,
1166                        )?
1167                        .with_cancel_token(self.context.cancellation_token())
1168                        .with_cell_budget(self.context.materialized_array_cells());
1169                    return Ok(crate::traits::CalcValue::Range(view));
1170                }
1171                if self.local_env.is_empty()
1172                    && let CompactRefType::Cell {
1173                        sheet,
1174                        row,
1175                        col,
1176                        row_abs,
1177                        col_abs,
1178                    } = ref_type
1179                    && *row > 0
1180                    && *col > 0
1181                {
1182                    let row = shift_axis_for_offset(*row, self.reference_row_delta, *row_abs)?;
1183                    let col = shift_axis_for_offset(*col, self.reference_col_delta, *col_abs)?;
1184                    if let (Some(resolver), Some(cell)) = (self.arena_cells, self.current_cell) {
1185                        let (value, format) =
1186                            resolver.resolve_arena_cell(*sheet, cell.sheet_id, row, col)?;
1187                        return Ok(Self::annotated(value, format));
1188                    }
1189                    let sheet_name = match sheet {
1190                        Some(SheetKey::Id(id)) => Some(sheet_registry.name(*id)),
1191                        Some(SheetKey::Name(name_id)) => {
1192                            Some(data_store.resolve_ast_string(*name_id))
1193                        }
1194                        None => None,
1195                    };
1196                    let (value, format) = self.context.resolve_cell_reference_value_formatted(
1197                        sheet_name,
1198                        row,
1199                        col,
1200                        self.current_sheet,
1201                    )?;
1202                    Ok(Self::annotated(value, format))
1203                } else {
1204                    let reference =
1205                        data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry);
1206                    let reference = self.effective_reference(&reference)?;
1207                    if let Some(local) = self.resolve_local_reference(&reference) {
1208                        return Ok(local);
1209                    }
1210                    self.eval_reference_to_calc(&reference)
1211                }
1212            }
1213            AstNodeData::UnaryOp { operator, expr_id } => {
1214                let op = *operator;
1215                if op == UnaryOperator::Spill {
1216                    let reference =
1217                        self.arena_spill_reference(*expr_id, data_store, sheet_registry)?;
1218                    return self.eval_reference_to_calc(&reference);
1219                }
1220                if op == UnaryOperator::ImplicitIntersection {
1221                    // Prefer reference-aware implicit intersection so we don't depend on
1222                    // RangeView absolute coordinates (important for lightweight test contexts).
1223                    // The reference is placed at this cell (a shared family's
1224                    // arena reference is in its origin frame), as on the
1225                    // plain-reference path above.
1226                    if let Some(AstNodeData::Reference { ref_type, .. }) =
1227                        data_store.get_node(*expr_id)
1228                    {
1229                        let reference = data_store.reconstruct_reference_type_for_eval(
1230                            data_store.compact_ref(*ref_type),
1231                            sheet_registry,
1232                        );
1233                        let v = match self.effective_reference(&reference) {
1234                            Ok(reference) => self.implicit_intersection_from_reference(&reference),
1235                            Err(e) => LiteralValue::Error(e),
1236                        };
1237                        return Ok(crate::traits::CalcValue::Scalar(v));
1238                    }
1239
1240                    let expr = self.evaluate_arena_ast(*expr_id, data_store, sheet_registry)?;
1241                    let v = self.eval_implicit_intersection_calc(expr);
1242                    return Ok(crate::traits::CalcValue::Scalar(v));
1243                }
1244                let expr = self.evaluate_arena_ast(*expr_id, data_store, sheet_registry)?;
1245                self.apply_unary_op(op, expr)
1246            }
1247            AstNodeData::BinaryOp {
1248                operator,
1249                left_id,
1250                right_id,
1251            } => {
1252                let op = *operator;
1253                if op == BinaryOperator::Range {
1254                    let lref =
1255                        self.evaluate_arena_ast_as_reference(*left_id, data_store, sheet_registry)?;
1256                    let rref = self.evaluate_arena_ast_as_reference(
1257                        *right_id,
1258                        data_store,
1259                        sheet_registry,
1260                    )?;
1261                    return match crate::reference::combine_references(&lref, &rref) {
1262                        Ok(_r) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1263                            ExcelError::new(ExcelErrorKind::Ref).with_message(
1264                                "Reference produced by ':' cannot be used directly as a value",
1265                            ),
1266                        ))),
1267                        Err(e) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
1268                    };
1269                }
1270
1271                let left_calc = self.evaluate_arena_ast(*left_id, data_store, sheet_registry)?;
1272                let left_format = left_calc.format_id();
1273                let left = left_calc.into_literal();
1274                let right_calc = self.evaluate_arena_ast(*right_id, data_store, sheet_registry)?;
1275                let right_format = right_calc.format_id();
1276                let right = right_calc.into_literal();
1277                self.apply_binary_op(op, left, left_format, right, right_format)
1278            }
1279            AstNodeData::Array { .. } => {
1280                let (rows, cols, elements) =
1281                    data_store.get_array_elems(node_id).ok_or_else(|| {
1282                        ExcelError::new(ExcelErrorKind::Value).with_message("Invalid array")
1283                    })?;
1284
1285                let rows_usize = rows as usize;
1286                let cols_usize = cols as usize;
1287                let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows_usize);
1288                for r in 0..rows_usize {
1289                    let mut row = Vec::with_capacity(cols_usize);
1290                    for c in 0..cols_usize {
1291                        let idx = r * cols_usize + c;
1292                        if let Some(&elem_id) = elements.get(idx) {
1293                            row.push(
1294                                self.evaluate_arena_ast(elem_id, data_store, sheet_registry)?
1295                                    .into_literal(),
1296                            );
1297                        }
1298                    }
1299                    out.push(row);
1300                }
1301
1302                Ok(crate::traits::CalcValue::Range(
1303                    crate::engine::range_view::RangeView::from_owned_rows(
1304                        out,
1305                        self.context.date_system(),
1306                    ),
1307                ))
1308            }
1309            AstNodeData::Function { name_id, .. } => {
1310                if let Some(bindings) = self.parameter_bindings
1311                    && let Some(values) = bindings.invariant_values
1312                    && let Some((value, format)) = values.get(&node_id)
1313                {
1314                    return Ok(Self::annotated(value.clone(), *format));
1315                }
1316                let function = self.arena_function(*name_id, data_store);
1317                if function.is_none() && data_store.resolve_ast_string(*name_id) == CALL_NODE_NAME {
1318                    return Err(call_expression_error());
1319                }
1320                let args = data_store.get_args(node_id).ok_or_else(|| {
1321                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing function args")
1322                })?;
1323
1324                if let Some(fun) = function {
1325                    let handles: smallvec::SmallVec<[ArgumentHandle; 4]> = args
1326                        .iter()
1327                        .copied()
1328                        .map(|arg_id| {
1329                            ArgumentHandle::new_arena(arg_id, self, data_store, sheet_registry)
1330                        })
1331                        .collect();
1332
1333                    let fctx = DefaultFunctionContext::new_with_sheet(
1334                        self.context,
1335                        self.current_cell,
1336                        self.current_sheet,
1337                    );
1338
1339                    return fun.dispatch(&handles, &fctx);
1340                }
1341
1342                let name = data_store.resolve_ast_string(*name_id);
1343                if let Some(callable) = self.resolve_local_callable(name) {
1344                    let mut eval_args = Vec::with_capacity(args.len());
1345                    let mut arg_references = Vec::with_capacity(args.len());
1346                    for arg_id in args {
1347                        // Same rule as the AST twin above.
1348                        arg_references.push(
1349                            ArgumentHandle::new_arena(*arg_id, self, data_store, sheet_registry)
1350                                .bound_reference_in_env(&self.local_env),
1351                        );
1352                        eval_args.push(
1353                            self.evaluate_arena_ast(*arg_id, data_store, sheet_registry)?
1354                                .into_literal(),
1355                        );
1356                    }
1357                    return callable.invoke_with_references(self, &eval_args, &arg_references);
1358                }
1359
1360                Err(ExcelError::new(ExcelErrorKind::Name)
1361                    .with_message(format!("Unknown function: {name}")))
1362            }
1363        }
1364    }
1365
1366    fn evaluate_ast_uncached(
1367        &self,
1368        node: &ASTNode,
1369    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1370        if self.disable_ast_planner {
1371            return self.eval_tree_uncached(node);
1372        }
1373
1374        // Plan-aware evaluation: build a plan for this node and execute accordingly.
1375        // Provide the planner with a lightweight range-dimension probe and function lookup
1376        // so it can select chunked reduction and arg-parallel strategies where appropriate.
1377        let current_sheet = self.current_sheet.to_string();
1378        let range_probe = |reference: &ReferenceType| {
1379            probe_range_dimensions(self.context, &current_sheet, reference)
1380        };
1381        let fn_lookup = |ns: &str, name: &str| self.ast_function_arc(ns, name);
1382
1383        let mut planner = crate::planner::Planner::new(crate::planner::PlanConfig::default())
1384            .with_range_probe(&range_probe)
1385            .with_function_lookup(&fn_lookup);
1386        let plan = planner.plan(node);
1387        self.eval_with_plan(node, &plan.root)
1388    }
1389
1390    fn eval_tree_uncached(
1391        &self,
1392        node: &ASTNode,
1393    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1394        match &node.node_type {
1395            ASTNodeType::Literal(v) => Ok(crate::traits::CalcValue::Scalar(v.clone())),
1396            ASTNodeType::Omitted => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
1397            ASTNodeType::Reference { reference, .. } => self.eval_ast_reference_to_calc(reference),
1398            ASTNodeType::UnaryOp { op, expr } if *op == UnaryOperator::Spill => {
1399                self.eval_reference_to_calc(&self.ast_spill_reference(expr)?)
1400            }
1401            ASTNodeType::UnaryOp { op, expr } => self
1402                .eval_unary(*op, expr)
1403                .map(crate::traits::CalcValue::Scalar),
1404            ASTNodeType::BinaryOp { op, left, right } => self.eval_binary(*op, left, right),
1405            ASTNodeType::Function { name, args } => self.eval_function_to_calc(name, args),
1406            ASTNodeType::Call { .. } => Err(call_expression_error()),
1407            ASTNodeType::Array(rows) => self.eval_array_literal_to_calc(rows),
1408        }
1409    }
1410
1411    fn eval_with_plan(
1412        &self,
1413        node: &ASTNode,
1414        plan_node: &crate::planner::PlanNode,
1415    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1416        match &node.node_type {
1417            ASTNodeType::Literal(v) => Ok(crate::traits::CalcValue::Scalar(v.clone())),
1418            ASTNodeType::Omitted => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0))),
1419            ASTNodeType::Reference { reference, .. } => self.eval_ast_reference_to_calc(reference),
1420            ASTNodeType::UnaryOp { op, expr } if *op == UnaryOperator::Spill => {
1421                self.eval_reference_to_calc(&self.ast_spill_reference(expr)?)
1422            }
1423            ASTNodeType::UnaryOp { op, expr } => {
1424                // For now, reuse existing unary implementation (which recurses).
1425                // In a later phase, we can map plan_node.children[0].
1426                self.eval_unary(*op, expr)
1427                    .map(crate::traits::CalcValue::Scalar)
1428            }
1429            ASTNodeType::BinaryOp { op, left, right } => self.eval_binary(*op, left, right),
1430            ASTNodeType::Function { name, args } => {
1431                let strategy = plan_node.strategy;
1432                if let Some(fun) = self.ast_function(name) {
1433                    use crate::function::FnCaps;
1434                    use crate::planner::ExecStrategy;
1435                    let caps = fun.caps();
1436
1437                    // Short-circuit or volatile: always sequential
1438                    if caps.contains(FnCaps::SHORT_CIRCUIT) || caps.contains(FnCaps::VOLATILE) {
1439                        return self.eval_function_to_calc(name, args);
1440                    }
1441
1442                    // Windowed/chunked strategies are handled by the unified `eval()` path.
1443
1444                    // Arg-parallel: prewarm subexpressions and then dispatch
1445                    if matches!(strategy, ExecStrategy::ArgParallel)
1446                        && caps.contains(FnCaps::PARALLEL_ARGS)
1447                    {
1448                        // Sequential prewarm of subexpressions (safe without Sync bounds)
1449                        for arg in args {
1450                            match &arg.node_type {
1451                                ASTNodeType::Reference { reference, .. } => {
1452                                    if let Ok(reference) = self.effective_reference(reference) {
1453                                        let _ = self
1454                                            .context
1455                                            .resolve_range_view(&reference, self.current_sheet);
1456                                    }
1457                                }
1458                                _ => {
1459                                    let _ = self.evaluate_ast(arg);
1460                                }
1461                            }
1462                        }
1463                        return self.eval_function_to_calc(name, args);
1464                    }
1465
1466                    // Default path
1467                    return self.eval_function_to_calc(name, args);
1468                }
1469                self.eval_function_to_calc(name, args)
1470            }
1471            ASTNodeType::Call { .. } => Err(call_expression_error()),
1472            ASTNodeType::Array(rows) => self.eval_array_literal_to_calc(rows),
1473        }
1474    }
1475
1476    /* ===================  reference  =================== */
1477    fn eval_ast_reference_to_calc(
1478        &self,
1479        reference: &ReferenceType,
1480    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1481        if !self.local_env.is_empty() {
1482            let reference = self.effective_reference(reference)?;
1483            if let Some(local) = self.resolve_local_reference(&reference) {
1484                return Ok(local);
1485            }
1486            return self.eval_reference_to_calc(&reference);
1487        }
1488
1489        if let ReferenceType::Cell {
1490            sheet,
1491            row,
1492            col,
1493            row_abs,
1494            col_abs,
1495        } = reference
1496        {
1497            let row = shift_axis_for_offset(*row, self.reference_row_delta, *row_abs)?;
1498            let col = shift_axis_for_offset(*col, self.reference_col_delta, *col_abs)?;
1499            let (value, format) = self.context.resolve_cell_reference_value_formatted(
1500                sheet.as_deref(),
1501                row,
1502                col,
1503                self.current_sheet,
1504            )?;
1505            return Ok(Self::annotated(value, format));
1506        }
1507
1508        let reference = self.effective_reference(reference)?;
1509        self.eval_reference_to_calc(&reference)
1510    }
1511
1512    fn eval_reference_to_calc(
1513        &self,
1514        reference: &ReferenceType,
1515    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1516        if let ReferenceType::Cell {
1517            sheet, row, col, ..
1518        } = reference
1519        {
1520            let value = self.context.resolve_cell_reference_value(
1521                sheet.as_deref(),
1522                *row,
1523                *col,
1524                self.current_sheet,
1525            )?;
1526            return Ok(self.annotate_cell_value(sheet.as_deref(), *row, *col, value));
1527        }
1528
1529        let view = self
1530            .context
1531            .resolve_range_view(reference, self.current_sheet)?
1532            .with_cancel_token(self.context.cancellation_token())
1533            .with_cell_budget(self.context.materialized_array_cells());
1534        Ok(crate::traits::CalcValue::Range(view))
1535    }
1536
1537    fn eval_reference(&self, reference: &ReferenceType) -> Result<LiteralValue, ExcelError> {
1538        self.eval_reference_to_calc(reference)
1539            .map(|cv| cv.into_literal())
1540    }
1541
1542    /* ===================  unary ops  =================== */
1543    fn eval_unary(&self, op: UnaryOperator, expr: &ASTNode) -> Result<LiteralValue, ExcelError> {
1544        if op == UnaryOperator::ImplicitIntersection {
1545            if let ASTNodeType::Reference { reference, .. } = &expr.node_type {
1546                let reference = self.effective_reference(reference)?;
1547                return Ok(self.implicit_intersection_from_reference(&reference));
1548            }
1549
1550            let cv = self.evaluate_ast(expr)?;
1551            return Ok(self.eval_implicit_intersection_calc(cv));
1552        }
1553
1554        let v = self.evaluate_ast(expr)?.into_literal();
1555        match v {
1556            LiteralValue::Array(arr) => {
1557                self.map_array(arr, |cell| self.eval_unary_scalar(op, cell))
1558            }
1559            other => self.eval_unary_scalar(op, other),
1560        }
1561    }
1562
1563    fn eval_unary_scalar(
1564        &self,
1565        op: UnaryOperator,
1566        v: LiteralValue,
1567    ) -> Result<LiteralValue, ExcelError> {
1568        match op {
1569            // Excel/LibreOffice treat unary `+` as a pass-through (identity) operator,
1570            // not as a numeric coercion. `=+"2014F"` returns the text "2014F"; only the
1571            // unary `-` form coerces operands to numbers. The `=+A1` idiom is common in
1572            // finance models (Lotus 1-2-3 carry-over) and must preserve text labels.
1573            UnaryOperator::Plus => Ok(v),
1574            UnaryOperator::Minus => self.apply_number_unary(v, b'-'),
1575            UnaryOperator::Percent => self.apply_number_unary(v, b'%'),
1576            _ => {
1577                Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!("Unary op '{op}'")))
1578            }
1579        }
1580    }
1581
1582    fn eval_implicit_intersection_calc(&self, cv: crate::traits::CalcValue<'a>) -> LiteralValue {
1583        let (cur_r0, cur_c0) = match self.current_cell {
1584            Some(cell) => (cell.coord.row() as usize, cell.coord.col() as usize),
1585            None => (0usize, 0usize),
1586        };
1587
1588        match cv {
1589            crate::traits::CalcValue::Scalar(v)
1590            | crate::traits::CalcValue::AnnotatedScalar(v, _) => match v {
1591                LiteralValue::Array(arr) => {
1592                    if arr.is_empty() || arr.first().map(|r| r.is_empty()).unwrap_or(true) {
1593                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1594                    }
1595                    arr[0][0].clone()
1596                }
1597                other => other,
1598            },
1599            crate::traits::CalcValue::Range(rv) => {
1600                if rv.is_empty() {
1601                    return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1602                }
1603
1604                // Array results (array literals and many dynamic-array functions) are materialized
1605                // into an owned RangeView with a temporary backing sheet ("__tmp").
1606                // For explicit @, interpret these as anchored at the formula cell and select the
1607                // top-left element.
1608                if rv.sheet_name() == "__tmp" {
1609                    return rv.get_cell(0, 0);
1610                }
1611
1612                if let Some(v) = rv.as_1x1() {
1613                    return v;
1614                }
1615
1616                let (rows, cols) = rv.dims();
1617                let sr = rv.start_row();
1618                let sc = rv.start_col();
1619                let er = rv.end_row();
1620                let ec = rv.end_col();
1621
1622                // Excel-compatible implicit intersection (simplified):
1623                // - Nx1: pick by row
1624                // - 1xM: pick by column
1625                // - NxM: pick by (row,col)
1626                if cols == 1 {
1627                    if cur_r0 < sr || cur_r0 > er {
1628                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1629                    }
1630                    let rel_r = cur_r0 - sr;
1631                    return rv.get_cell(rel_r, 0);
1632                }
1633
1634                if rows == 1 {
1635                    if cur_c0 < sc || cur_c0 > ec {
1636                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1637                    }
1638                    let rel_c = cur_c0 - sc;
1639                    return rv.get_cell(0, rel_c);
1640                }
1641
1642                if cur_r0 < sr || cur_r0 > er || cur_c0 < sc || cur_c0 > ec {
1643                    return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1644                }
1645                let rel_r = cur_r0 - sr;
1646                let rel_c = cur_c0 - sc;
1647                rv.get_cell(rel_r, rel_c)
1648            }
1649            crate::traits::CalcValue::Callable(_) => LiteralValue::Error(
1650                ExcelError::new(ExcelErrorKind::Calc).with_message("LAMBDA value must be invoked"),
1651            ),
1652        }
1653    }
1654
1655    fn implicit_intersection_from_reference(&self, reference: &ReferenceType) -> LiteralValue {
1656        let (cur_r1, cur_c1) = match self.current_cell {
1657            Some(cell) => (
1658                cell.coord.row().saturating_add(1),
1659                cell.coord.col().saturating_add(1),
1660            ),
1661            None => (1u32, 1u32),
1662        };
1663
1664        match reference {
1665            ReferenceType::Cell {
1666                sheet, row, col, ..
1667            } => {
1668                let sheet_name = sheet.as_deref().unwrap_or(self.current_sheet);
1669                match self
1670                    .context
1671                    .resolve_cell_reference(Some(sheet_name), *row, *col)
1672                {
1673                    Ok(v) => v,
1674                    Err(e) => LiteralValue::Error(e),
1675                }
1676            }
1677            ReferenceType::Range {
1678                sheet,
1679                start_row,
1680                start_col,
1681                end_row,
1682                end_col,
1683                ..
1684            } => {
1685                let sheet_name = sheet.as_deref().unwrap_or(self.current_sheet);
1686
1687                // Open-ended references (`A:A`, `21:21`, `A5:A`) intersect by
1688                // their declared sheet bounds, not by a used-region view: a
1689                // whole row's intersection with column G is G on that row even
1690                // when the row's used cells end before G.
1691                const SHEET_ROWS: u32 = 1_048_576;
1692                const SHEET_COLS: u32 = 16_384;
1693                let (sr, sc, er, ec) = (
1694                    start_row.unwrap_or(1),
1695                    start_col.unwrap_or(1),
1696                    end_row.unwrap_or(SHEET_ROWS),
1697                    end_col.unwrap_or(SHEET_COLS),
1698                );
1699
1700                // Normalize bounds (A10:A1 is legal syntax; treat as swapped).
1701                let (mut sr, mut er) = (sr, er);
1702                let (mut sc, mut ec) = (sc, ec);
1703                if sr > er {
1704                    std::mem::swap(&mut sr, &mut er);
1705                }
1706                if sc > ec {
1707                    std::mem::swap(&mut sc, &mut ec);
1708                }
1709
1710                let pick = if sc == ec {
1711                    // Column vector: intersect by row
1712                    if cur_r1 < sr || cur_r1 > er {
1713                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1714                    }
1715                    (cur_r1, sc)
1716                } else if sr == er {
1717                    // Row vector: intersect by column
1718                    if cur_c1 < sc || cur_c1 > ec {
1719                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1720                    }
1721                    (sr, cur_c1)
1722                } else {
1723                    // 2D: require both axes
1724                    if cur_r1 < sr || cur_r1 > er || cur_c1 < sc || cur_c1 > ec {
1725                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1726                    }
1727                    (cur_r1, cur_c1)
1728                };
1729
1730                match self
1731                    .context
1732                    .resolve_cell_reference(Some(sheet_name), pick.0, pick.1)
1733                {
1734                    Ok(v) => v,
1735                    Err(e) => LiteralValue::Error(e),
1736                }
1737            }
1738            // A bounded external range intersects by its declared bounds, like
1739            // a range on a worksheet; its values come from the source view.
1740            ReferenceType::External(ext) => {
1741                let formualizer_parse::parser::ExternalRefKind::Range {
1742                    start_row: Some(sr),
1743                    start_col: Some(sc),
1744                    end_row: Some(er),
1745                    end_col: Some(ec),
1746                    ..
1747                } = ext.kind
1748                else {
1749                    return match self.eval_reference_to_calc(reference) {
1750                        Ok(cv) => self.eval_implicit_intersection_calc(cv),
1751                        Err(e) => LiteralValue::Error(e),
1752                    };
1753                };
1754                let (sr, er) = (sr.min(er), sr.max(er));
1755                let (sc, ec) = (sc.min(ec), sc.max(ec));
1756                let (row, col) = if sc == ec {
1757                    if cur_r1 < sr || cur_r1 > er {
1758                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1759                    }
1760                    (cur_r1, sc)
1761                } else if sr == er {
1762                    if cur_c1 < sc || cur_c1 > ec {
1763                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1764                    }
1765                    (sr, cur_c1)
1766                } else {
1767                    if cur_r1 < sr || cur_r1 > er || cur_c1 < sc || cur_c1 > ec {
1768                        return LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value));
1769                    }
1770                    (cur_r1, cur_c1)
1771                };
1772                match self.eval_reference_to_calc(reference) {
1773                    Ok(crate::traits::CalcValue::Range(rv)) => {
1774                        rv.get_cell((row - sr) as usize, (col - sc) as usize)
1775                    }
1776                    Ok(cv) => self.eval_implicit_intersection_calc(cv),
1777                    Err(e) => LiteralValue::Error(e),
1778                }
1779            }
1780            // Named ranges / tables: fall back to materializing and intersecting.
1781            other => {
1782                let cv = match self.eval_reference_to_calc(other) {
1783                    Ok(cv) => cv,
1784                    Err(e) => return LiteralValue::Error(e),
1785                };
1786                self.eval_implicit_intersection_calc(cv)
1787            }
1788        }
1789    }
1790
1791    fn apply_number_unary(&self, v: LiteralValue, op: u8) -> Result<LiteralValue, ExcelError> {
1792        match crate::coercion::to_arithmetic_number_with_locale(
1793            &v,
1794            &self.context.locale(),
1795            self.context.date_system(),
1796        ) {
1797            Ok(n) => match unary_f64(op, n) {
1798                Ok(n2) => Ok(LiteralValue::Number(n2)),
1799                Err(e) => Ok(LiteralValue::Error(e)),
1800            },
1801            Err(e) => Ok(LiteralValue::Error(e)),
1802        }
1803    }
1804
1805    /* ===================  binary ops  =================== */
1806    fn eval_binary(
1807        &self,
1808        op: BinaryOperator,
1809        left_node: &ASTNode,
1810        right_node: &ASTNode,
1811    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1812        let left_calc = self.evaluate_ast(left_node)?;
1813        let left_format = left_calc.format_id();
1814        let left = left_calc.into_literal();
1815        let right_calc = self.evaluate_ast(right_node)?;
1816        let right_format = right_calc.format_id();
1817        let right = right_calc.into_literal();
1818        if op == BinaryOperator::Range {
1819            let left_ref = self.evaluate_ast_as_reference(left_node)?;
1820            let right_ref = self.evaluate_ast_as_reference(right_node)?;
1821            return match crate::reference::combine_references(&left_ref, &right_ref) {
1822                Ok(_) => Err(ExcelError::new(ExcelErrorKind::Ref)
1823                    .with_message("Reference produced by ':' cannot be used directly as a value")),
1824                Err(error) => Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(error))),
1825            };
1826        }
1827        self.apply_binary_op(op, left, left_format, right, right_format)
1828    }
1829
1830    /* ===================  function calls  =================== */
1831    fn eval_function_to_calc(
1832        &self,
1833        name: &str,
1834        args: &[ASTNode],
1835    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1836        if let Some(fun) = self.ast_function(name) {
1837            let handles: smallvec::SmallVec<[ArgumentHandle; 4]> =
1838                args.iter().map(|n| ArgumentHandle::new(n, self)).collect();
1839            // Use the function's built-in dispatch method with a narrow FunctionContext
1840            let fctx = DefaultFunctionContext::new_with_sheet(
1841                self.context,
1842                self.current_cell,
1843                self.current_sheet,
1844            );
1845            return fun.dispatch(&handles, &fctx);
1846        }
1847
1848        if let Some(callable) = self.resolve_local_callable(name) {
1849            let mut eval_args = Vec::with_capacity(args.len());
1850            let mut arg_references = Vec::with_capacity(args.len());
1851            for arg in args {
1852                // Same rule `LET` uses to keep a bound range: a
1853                // syntactic reference expression, or a local that itself
1854                // carries a bound reference; nothing else.
1855                arg_references
1856                    .push(ArgumentHandle::new(arg, self).bound_reference_in_env(&self.local_env));
1857                eval_args.push(self.evaluate_ast(arg)?.into_literal());
1858            }
1859            return callable.invoke_with_references(self, &eval_args, &arg_references);
1860        }
1861
1862        // Include the function name in the error message for better debugging
1863        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
1864            ExcelError::new(ExcelErrorKind::Name).with_message(format!("Unknown function: {name}")),
1865        )))
1866    }
1867
1868    fn eval_function(&self, name: &str, args: &[ASTNode]) -> Result<LiteralValue, ExcelError> {
1869        self.eval_function_to_calc(name, args)
1870            .map(|cv| cv.into_literal())
1871    }
1872
1873    pub fn function_context(&self, cell_ref: Option<&CellRef>) -> DefaultFunctionContext<'_> {
1874        DefaultFunctionContext::new_with_sheet(self.context, cell_ref.cloned(), self.current_sheet)
1875    }
1876
1877    /* ===================  array literal  =================== */
1878    fn eval_array_literal_to_calc(
1879        &self,
1880        rows: &[Vec<ASTNode>],
1881    ) -> Result<crate::traits::CalcValue<'a>, ExcelError> {
1882        let mut out = Vec::with_capacity(rows.len());
1883        for row in rows {
1884            let mut r = Vec::with_capacity(row.len());
1885            for cell in row {
1886                r.push(self.evaluate_ast(cell)?.into_literal());
1887            }
1888            out.push(r);
1889        }
1890        Ok(crate::traits::CalcValue::Range(
1891            crate::engine::range_view::RangeView::from_owned_rows(out, self.context.date_system()),
1892        ))
1893    }
1894
1895    fn eval_array_literal(&self, rows: &[Vec<ASTNode>]) -> Result<LiteralValue, ExcelError> {
1896        self.eval_array_literal_to_calc(rows)
1897            .map(|cv| cv.into_literal())
1898    }
1899
1900    /// `+ - * / ^` on two operands: arrays broadcast, each element pair is
1901    /// coerced to numbers (first error wins, left first) and combined by
1902    /// [`arith_f64`].
1903    fn numeric_binary(
1904        &self,
1905        left: LiteralValue,
1906        right: LiteralValue,
1907        op: u8,
1908    ) -> Result<LiteralValue, ExcelError> {
1909        self.broadcast_apply(left, right, |l, r| {
1910            let a = crate::coercion::to_arithmetic_number_with_locale(
1911                &l,
1912                &self.context.locale(),
1913                self.context.date_system(),
1914            );
1915            let b = crate::coercion::to_arithmetic_number_with_locale(
1916                &r,
1917                &self.context.locale(),
1918                self.context.date_system(),
1919            );
1920            match (a, b) {
1921                (Ok(a), Ok(b)) => match arith_f64(op, a, b) {
1922                    Ok(n) => Ok(LiteralValue::Number(n)),
1923                    Err(e) => Ok(LiteralValue::Error(e)),
1924                },
1925                (Err(e), _) | (_, Err(e)) => Ok(LiteralValue::Error(e)),
1926            }
1927        })
1928    }
1929
1930    /// The `&` operator. Like the arithmetic operators it works element by
1931    /// element over arrays and ranges, and an error operand is the result (the
1932    /// left one when both are errors) instead of being spelled into the text.
1933    fn concat_values(
1934        &self,
1935        left: LiteralValue,
1936        right: LiteralValue,
1937    ) -> Result<LiteralValue, ExcelError> {
1938        fn concat_scalar(
1939            left: LiteralValue,
1940            right: LiteralValue,
1941        ) -> Result<LiteralValue, ExcelError> {
1942            Ok(match (left, right) {
1943                (LiteralValue::Error(error), _) | (_, LiteralValue::Error(error)) => {
1944                    LiteralValue::Error(error)
1945                }
1946                (left, right) => LiteralValue::Text(format!(
1947                    "{}{}",
1948                    crate::coercion::to_text_invariant(&left),
1949                    crate::coercion::to_text_invariant(&right)
1950                )),
1951            })
1952        }
1953        // Scalars (every member of a lifted family) skip the broadcast.
1954        if matches!(left, LiteralValue::Array(_)) || matches!(right, LiteralValue::Array(_)) {
1955            self.broadcast_apply(left, right, concat_scalar)
1956        } else {
1957            concat_scalar(left, right)
1958        }
1959    }
1960
1961    fn map_array<F>(&self, arr: Vec<Vec<LiteralValue>>, f: F) -> Result<LiteralValue, ExcelError>
1962    where
1963        F: Fn(LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
1964    {
1965        let mut out = Vec::with_capacity(arr.len());
1966        for row in arr {
1967            let mut new_row = Vec::with_capacity(row.len());
1968            for cell in row {
1969                new_row.push(match f(cell) {
1970                    Ok(v) => v,
1971                    Err(e) => LiteralValue::Error(e),
1972                });
1973            }
1974            out.push(new_row);
1975        }
1976        Ok(LiteralValue::Array(out))
1977    }
1978
1979    fn combine_arrays<F>(
1980        &self,
1981        l: Vec<Vec<LiteralValue>>,
1982        r: Vec<Vec<LiteralValue>>,
1983        f: F,
1984    ) -> Result<LiteralValue, ExcelError>
1985    where
1986        F: Fn(LiteralValue, LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
1987    {
1988        // Use strict broadcasting across dimensions
1989        let l_shape = (l.len(), l.first().map(|r| r.len()).unwrap_or(0));
1990        let r_shape = (r.len(), r.first().map(|r| r.len()).unwrap_or(0));
1991        let target = match broadcast_shape(&[l_shape, r_shape]) {
1992            Ok(s) => s,
1993            Err(e) => return Ok(LiteralValue::Error(e)),
1994        };
1995        // A column and a row broadcast to their product: check it before
1996        // allocating (the inputs were checked when they were materialised).
1997        if let Some(e) = crate::engine::resource_ledger::materialized_cells_exceed(
1998            target.0,
1999            target.1,
2000            self.context.materialized_array_cells(),
2001        ) {
2002            return Ok(LiteralValue::Error(e));
2003        }
2004
2005        let mut out = Vec::with_capacity(target.0);
2006        for i in 0..target.0 {
2007            let mut row = Vec::with_capacity(target.1);
2008            for j in 0..target.1 {
2009                let (li, lj) = project_index((i, j), l_shape);
2010                let (ri, rj) = project_index((i, j), r_shape);
2011                let lv = l
2012                    .get(li)
2013                    .and_then(|r| r.get(lj))
2014                    .cloned()
2015                    .unwrap_or(LiteralValue::Empty);
2016                let rv = r
2017                    .get(ri)
2018                    .and_then(|r| r.get(rj))
2019                    .cloned()
2020                    .unwrap_or(LiteralValue::Empty);
2021                row.push(match f(lv, rv) {
2022                    Ok(v) => v,
2023                    Err(e) => LiteralValue::Error(e),
2024                });
2025            }
2026            out.push(row);
2027        }
2028        Ok(LiteralValue::Array(out))
2029    }
2030
2031    fn broadcast_apply<F>(
2032        &self,
2033        left: LiteralValue,
2034        right: LiteralValue,
2035        f: F,
2036    ) -> Result<LiteralValue, ExcelError>
2037    where
2038        F: Fn(LiteralValue, LiteralValue) -> Result<LiteralValue, ExcelError> + Copy,
2039    {
2040        use LiteralValue::*;
2041        match (left, right) {
2042            (Array(l), Array(r)) => self.combine_arrays(l, r, f),
2043            (Array(arr), v) => {
2044                let shape_l = (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0));
2045                let shape_r = (1usize, 1usize);
2046                let target = match broadcast_shape(&[shape_l, shape_r]) {
2047                    Ok(s) => s,
2048                    Err(e) => return Ok(LiteralValue::Error(e)),
2049                };
2050                let mut out = Vec::with_capacity(target.0);
2051                for i in 0..target.0 {
2052                    let mut row = Vec::with_capacity(target.1);
2053                    for j in 0..target.1 {
2054                        let (li, lj) = project_index((i, j), shape_l);
2055                        let lv = arr
2056                            .get(li)
2057                            .and_then(|r| r.get(lj))
2058                            .cloned()
2059                            .unwrap_or(LiteralValue::Empty);
2060                        row.push(match f(lv, v.clone()) {
2061                            Ok(vv) => vv,
2062                            Err(e) => LiteralValue::Error(e),
2063                        });
2064                    }
2065                    out.push(row);
2066                }
2067                Ok(LiteralValue::Array(out))
2068            }
2069            (v, Array(arr)) => {
2070                let shape_l = (1usize, 1usize);
2071                let shape_r = (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0));
2072                let target = match broadcast_shape(&[shape_l, shape_r]) {
2073                    Ok(s) => s,
2074                    Err(e) => return Ok(LiteralValue::Error(e)),
2075                };
2076                let mut out = Vec::with_capacity(target.0);
2077                for i in 0..target.0 {
2078                    let mut row = Vec::with_capacity(target.1);
2079                    for j in 0..target.1 {
2080                        let (ri, rj) = project_index((i, j), shape_r);
2081                        let rv = arr
2082                            .get(ri)
2083                            .and_then(|r| r.get(rj))
2084                            .cloned()
2085                            .unwrap_or(LiteralValue::Empty);
2086                        row.push(match f(v.clone(), rv) {
2087                            Ok(vv) => vv,
2088                            Err(e) => LiteralValue::Error(e),
2089                        });
2090                    }
2091                    out.push(row);
2092                }
2093                Ok(LiteralValue::Array(out))
2094            }
2095            (l, r) => f(l, r),
2096        }
2097    }
2098
2099    /* ---------- coercion helpers ---------- */
2100    fn coerce_number(&self, v: &LiteralValue) -> Result<f64, ExcelError> {
2101        coercion::to_number_lenient(v)
2102    }
2103
2104    fn coerce_text(&self, v: &LiteralValue) -> String {
2105        coercion::to_text_invariant(v)
2106    }
2107
2108    /* ---------- comparison ---------- */
2109    fn compare(
2110        &self,
2111        op: BinaryOperator,
2112        left: LiteralValue,
2113        right: LiteralValue,
2114    ) -> Result<LiteralValue, ExcelError> {
2115        use LiteralValue::*;
2116        if matches!(left, Error(_)) {
2117            return Ok(left);
2118        }
2119        if matches!(right, Error(_)) {
2120            return Ok(right);
2121        }
2122
2123        // arrays: element‑wise with broadcasting
2124        match (left, right) {
2125            (Array(l), Array(r)) => self.combine_arrays(l, r, |a, b| self.compare(op, a, b)),
2126            (Array(arr), v) => self.broadcast_apply(Array(arr), v, |a, b| self.compare(op, a, b)),
2127            (v, Array(arr)) => self.broadcast_apply(v, Array(arr), |a, b| self.compare(op, a, b)),
2128            (l, r) => {
2129                let res = match (l, r) {
2130                    // Same-rank fast paths. These agree with `cmp_ranked` below
2131                    // and exist only to avoid the rank/`Empty` dance.
2132                    (Number(a), Number(b)) => self.cmp_f64(a, b, op),
2133                    (Int(a), Int(b)) => self.cmp_f64(a as f64, b as f64, op),
2134                    (Int(a), Number(b)) => self.cmp_f64(a as f64, b, op),
2135                    (Number(a), Int(b)) => self.cmp_f64(a, b as f64, op),
2136                    (Boolean(a), Boolean(b)) => {
2137                        self.cmp_f64(if a { 1.0 } else { 0.0 }, if b { 1.0 } else { 0.0 }, op)
2138                    }
2139                    (Text(a), Text(b)) => self.cmp_text(&a, &b, op),
2140                    (a, b) => self.cmp_ranked(a, b, op),
2141                };
2142                Ok(LiteralValue::Boolean(res))
2143            }
2144        }
2145    }
2146
2147    /// Mixed-type relational comparison.
2148    ///
2149    /// Excel does not coerce across types in `<`, `<=`, `>`, `>=`, `=`, `<>`.
2150    /// It applies a pure type rank, identical for all six operators:
2151    ///
2152    /// ```text
2153    /// number  <  text  <  boolean
2154    /// ```
2155    ///
2156    /// Only when both operands share a rank are they compared within the type
2157    /// (measured in Excel for Mac 16.105.3, en_US). Consequences of the rank: `TRUE=1` is FALSE, `1<TRUE` is TRUE,
2158    /// `"5"=5` is FALSE (numeric text never coerces), `"5">4` is TRUE,
2159    /// `"TRUE"=TRUE` is FALSE, and `"Z"<FALSE` is TRUE.
2160    ///
2161    /// Errors never reach here: `compare` short-circuits them above.
2162    fn cmp_ranked(&self, l: LiteralValue, r: LiteralValue, op: BinaryOperator) -> bool {
2163        use LiteralValue::*;
2164
2165        // A never-written cell is polymorphic rather than ranked: it adopts the
2166        // other operand's type and behaves as that type's zero value, so
2167        // `Z1=0`, `Z1=""`, `Z1=FALSE` and `Z1<TRUE` are all TRUE.
2168        let (l, r) = match (l, r) {
2169            (Empty, Empty) => (Number(0.0), Number(0.0)),
2170            (Empty, r) => (empty_as_zero_of(&r), r),
2171            (l, Empty) => {
2172                let e = empty_as_zero_of(&l);
2173                (l, e)
2174            }
2175            (l, r) => (l, r),
2176        };
2177
2178        let (lr, rr) = (excel_type_rank(&l), excel_type_rank(&r));
2179        if lr != rr {
2180            return self.cmp_f64(lr as f64, rr as f64, op);
2181        }
2182
2183        match (l, r) {
2184            (Text(a), Text(b)) => self.cmp_text(&a, &b, op),
2185            (Boolean(a), Boolean(b)) => {
2186                self.cmp_f64(if a { 1.0 } else { 0.0 }, if b { 1.0 } else { 0.0 }, op)
2187            }
2188            // Same rank, number class: Int/Number and the date/time/duration
2189            // serial-bearing variants. Text is never parsed here.
2190            (a, b) => {
2191                let an = crate::coercion::to_number_strict(&a).ok();
2192                let bn = crate::coercion::to_number_strict(&b).ok();
2193                if let (Some(a), Some(b)) = (an, bn) {
2194                    self.cmp_f64(a, b, op)
2195                } else {
2196                    // Only `Pending` (and any future non-numeric, non-text,
2197                    // non-boolean variant) lands here; keep the legacy text
2198                    // fallback so those pairs behave as before.
2199                    self.cmp_text(
2200                        &crate::coercion::to_text_invariant(&a),
2201                        &crate::coercion::to_text_invariant(&b),
2202                        op,
2203                    )
2204                }
2205            }
2206        }
2207    }
2208
2209    fn cmp_f64(&self, a: f64, b: f64, op: BinaryOperator) -> bool {
2210        cmp_f64(a, b, op)
2211    }
2212    fn cmp_text(&self, a: &str, b: &str, op: BinaryOperator) -> bool {
2213        let loc = self.context.locale();
2214        let (a, b) = (loc.fold_case_invariant(a), loc.fold_case_invariant(b));
2215        self.cmp_f64(a.cmp(&b) as i32 as f64, 0.0, op)
2216    }
2217}
2218
2219/// A numeric comparison operator on two numbers (shared by the walk and
2220/// the typed lift).
2221#[inline]
2222pub(crate) fn cmp_f64(a: f64, b: f64, op: BinaryOperator) -> bool {
2223    match op {
2224        BinaryOperator::Equal => a == b,
2225        BinaryOperator::NotEqual => a != b,
2226        BinaryOperator::Greater => a > b,
2227        BinaryOperator::Less => a < b,
2228        BinaryOperator::GreaterEqual => a >= b,
2229        BinaryOperator::LessEqual => a <= b,
2230        _ => unreachable!(),
2231    }
2232}
2233
2234/// `+ - * / ^` on two numbers after coercion: the result, or the error
2235/// the operator yields (`#DIV/0!`, `#NUM!` for a negative base with a
2236/// fractional exponent or a non-finite result). Shared by the walk and
2237/// the typed lift, so both are bit-identical.
2238#[inline]
2239pub(crate) fn arith_f64(op: u8, a: f64, b: f64) -> Result<f64, ExcelError> {
2240    let v = match op {
2241        b'+' => a + b,
2242        b'-' => a - b,
2243        b'*' => a * b,
2244        b'/' => {
2245            if b == 0.0 {
2246                return Err(ExcelError::from_error_string("#DIV/0!"));
2247            }
2248            a / b
2249        }
2250        b'^' => {
2251            // Excel domain: negative base with non-integer exponent -> #NUM!
2252            if a < 0.0 && b.fract() != 0.0 {
2253                return Err(ExcelError::new_num());
2254            }
2255            a.powf(b)
2256        }
2257        _ => unreachable!("arithmetic operator"),
2258    };
2259    crate::coercion::sanitize_numeric(v)
2260}
2261
2262/// Unary `-` and `%` on a number after coercion (shared by the walk and
2263/// the typed lift).
2264#[inline]
2265pub(crate) fn unary_f64(op: u8, n: f64) -> Result<f64, ExcelError> {
2266    crate::coercion::sanitize_numeric(match op {
2267        b'-' => -n,
2268        b'%' => n / 100.0,
2269        _ => unreachable!("unary operator"),
2270    })
2271}
2272
2273/// Excel's relational type rank: `number < text < boolean`.
2274///
2275/// The number class covers `Int`, `Number` and the serial-bearing temporal
2276/// variants (`Date`, `DateTime`, `Time`, `Duration`), because on a sheet a date
2277/// cell *is* a number. `Empty` is deliberately not ranked here — it is
2278/// polymorphic and must be resolved against the other operand before ranking
2279/// (see `Interpreter::cmp_ranked`). `Error` never reaches ranking.
2280fn excel_type_rank(v: &LiteralValue) -> u8 {
2281    match v {
2282        LiteralValue::Text(_) => 1,
2283        LiteralValue::Boolean(_) => 2,
2284        _ => 0,
2285    }
2286}
2287
2288/// The zero value of `other`'s type, used to give a blank operand the type of
2289/// whatever it is compared against: `0` against a number, `""` against text,
2290/// `FALSE` against a boolean.
2291fn empty_as_zero_of(other: &LiteralValue) -> LiteralValue {
2292    match other {
2293        LiteralValue::Text(_) => LiteralValue::Text(String::new()),
2294        LiteralValue::Boolean(_) => LiteralValue::Boolean(false),
2295        _ => LiteralValue::Number(0.0),
2296    }
2297}
2298
2299fn relocate_reference_for_offset(
2300    reference: &ReferenceType,
2301    row_delta: i64,
2302    col_delta: i64,
2303) -> Result<ReferenceType, ExcelError> {
2304    match reference {
2305        ReferenceType::Cell {
2306            sheet,
2307            row,
2308            col,
2309            row_abs,
2310            col_abs,
2311        } => Ok(ReferenceType::Cell {
2312            sheet: sheet.clone(),
2313            row: shift_axis_for_offset(*row, row_delta, *row_abs)?,
2314            col: shift_axis_for_offset(*col, col_delta, *col_abs)?,
2315            row_abs: *row_abs,
2316            col_abs: *col_abs,
2317        }),
2318        ReferenceType::Range {
2319            sheet,
2320            start_row,
2321            start_col,
2322            end_row,
2323            end_col,
2324            start_row_abs,
2325            start_col_abs,
2326            end_row_abs,
2327            end_col_abs,
2328        } => Ok(ReferenceType::Range {
2329            sheet: sheet.clone(),
2330            start_row: shift_optional_axis_for_offset(*start_row, row_delta, *start_row_abs)?,
2331            start_col: shift_optional_axis_for_offset(*start_col, col_delta, *start_col_abs)?,
2332            end_row: shift_optional_axis_for_offset(*end_row, row_delta, *end_row_abs)?,
2333            end_col: shift_optional_axis_for_offset(*end_col, col_delta, *end_col_abs)?,
2334            start_row_abs: *start_row_abs,
2335            start_col_abs: *start_col_abs,
2336            end_row_abs: *end_row_abs,
2337            end_col_abs: *end_col_abs,
2338        }),
2339        // Defined names are placement-invariant: a relocated copy of the
2340        // formula references the same name, resolved at evaluation time.
2341        ReferenceType::NamedRange(name) => Ok(ReferenceType::NamedRange(name.clone())),
2342        ReferenceType::Table(_)
2343        | ReferenceType::Cell3D { .. }
2344        | ReferenceType::Range3D { .. }
2345        | ReferenceType::External(_) => Err(unsupported_reference_relocation_error()),
2346    }
2347}
2348
2349fn shift_optional_axis_for_offset(
2350    value: Option<u32>,
2351    delta: i64,
2352    is_absolute: bool,
2353) -> Result<Option<u32>, ExcelError> {
2354    value
2355        .map(|value| shift_axis_for_offset(value, delta, is_absolute))
2356        .transpose()
2357}
2358
2359pub(crate) fn shift_axis_for_offset(
2360    value: u32,
2361    delta: i64,
2362    is_absolute: bool,
2363) -> Result<u32, ExcelError> {
2364    if is_absolute {
2365        return Ok(value);
2366    }
2367    let shifted = i64::from(value) + delta;
2368    if shifted < 1 || shifted > i64::from(u32::MAX) {
2369        return Err(unsupported_reference_relocation_error());
2370    }
2371    Ok(shifted as u32)
2372}
2373
2374fn unsupported_reference_relocation_error() -> ExcelError {
2375    ExcelError::new(ExcelErrorKind::Ref)
2376        .with_message("Unsupported reference relocation for FormulaPlane span evaluation")
2377}
2378
2379#[cfg(test)]
2380mod format_algebra_tests {
2381    use super::*;
2382
2383    #[test]
2384    fn compact_argument_ranges_match_owned_resolution() -> Result<(), Box<dyn std::error::Error>> {
2385        use crate::traits::ResolvedArgument;
2386        let mut config = EvalConfig::default();
2387        config.evaluation_budgets.scratch.materialized_array_cells = Some(4);
2388        let mut engine = Engine::new(TestWorkbook::new(), config);
2389        let sheet_id = engine.add_sheet("Other")?;
2390        engine.set_cell_value("Other", 2, 2, LiteralValue::Number(17.0))?;
2391        engine.set_cell_value("Other", 4, 3, LiteralValue::Text("end".into()))?;
2392        let template = formualizer_parse::parser::parse("=A1:C3")?;
2393        let cell = crate::CellRef::new(
2394            sheet_id,
2395            crate::reference::Coord::from_excel(1, 1, true, true),
2396        );
2397        for qualified in [false, true] {
2398            for open_rows in [false, true] {
2399                for open_cols in [false, true] {
2400                    for absolute in [false, true] {
2401                        for delta in [-2, 0, 2] {
2402                            let reference = ReferenceType::Range {
2403                                sheet: qualified.then(|| "Other".to_string()),
2404                                start_row: (!open_rows).then_some(1),
2405                                start_col: (!open_cols).then_some(1),
2406                                end_row: (!open_rows).then_some(3),
2407                                end_col: (!open_cols).then_some(3),
2408                                start_row_abs: absolute,
2409                                start_col_abs: absolute,
2410                                end_row_abs: absolute,
2411                                end_col_abs: absolute,
2412                            };
2413                            let mut ast = template.clone();
2414                            ast.node_type = ASTNodeType::Reference {
2415                                original: reference.to_string(),
2416                                reference,
2417                            };
2418                            let mut store = DataStore::new();
2419                            let registry = engine.graph.sheet_reg();
2420                            let id = store.store_ast(&ast, registry);
2421                            let mut slow =
2422                                Interpreter::new(&engine, "Other").with_current_cell(cell);
2423                            slow.reference_row_delta = delta;
2424                            slow.reference_col_delta = delta;
2425                            let fast = slow.with_current_cell(cell).with_arena_cells(&engine);
2426                            assert!(fast.resolve_arena_argument_range(id, &store).is_some());
2427                            assert!(slow.resolve_arena_argument_range(id, &store).is_none());
2428                            let a = ArgumentHandle::new_arena(id, &fast, &store, registry)
2429                                .resolve_once();
2430                            let b = ArgumentHandle::new_arena(id, &slow, &store, registry)
2431                                .resolve_once();
2432                            match (a, b) {
2433                                (
2434                                    Ok(ResolvedArgument::Range(a)),
2435                                    Ok(ResolvedArgument::Range(b)),
2436                                ) => {
2437                                    assert_eq!(a.dims(), b.dims());
2438                                    assert_eq!(
2439                                        (a.start_row(), a.start_col()),
2440                                        (b.start_row(), b.start_col())
2441                                    );
2442                                    assert_eq!(
2443                                        a.materialization_error().map(|e| e.kind),
2444                                        b.materialization_error().map(|e| e.kind)
2445                                    );
2446                                    let (rows, cols) = a.dims();
2447                                    for row in 0..rows.min(8) {
2448                                        for col in 0..cols.min(8) {
2449                                            assert_eq!(a.get_cell(row, col), b.get_cell(row, col));
2450                                        }
2451                                    }
2452                                }
2453                                (
2454                                    Ok(ResolvedArgument::ReferenceError(a)),
2455                                    Ok(ResolvedArgument::ReferenceError(b)),
2456                                ) => assert_eq!(a, b),
2457                                (Err(a), Err(b)) => assert_eq!(a, b),
2458                                (a, b) => assert_eq!(format!("{a:?}"), format!("{b:?}")),
2459                            }
2460                        }
2461                    }
2462                }
2463            }
2464        }
2465        Ok(())
2466    }
2467
2468    #[test]
2469    fn compact_ranges_match_owned_resolution_after_relocation() -> Result<(), ExcelError> {
2470        let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
2471        let sheet_id = engine.add_sheet("Other")?;
2472        engine.set_cell_value("Other", 2, 2, LiteralValue::Number(17.0))?;
2473        engine.set_cell_value("Other", 4, 3, LiteralValue::Text("end".into()))?;
2474        for qualified in [false, true] {
2475            for open_rows in [false, true] {
2476                for open_cols in [false, true] {
2477                    for absolute in [false, true] {
2478                        for delta in [-2, 0, 2] {
2479                            let compact = CompactRefType::Range {
2480                                sheet: qualified.then_some(SheetKey::Id(sheet_id)),
2481                                start_row: if open_rows { 0 } else { 1 },
2482                                start_col: if open_cols { 0 } else { 1 },
2483                                end_row: if open_rows { u32::MAX } else { 3 },
2484                                end_col: if open_cols { u32::MAX } else { 3 },
2485                                start_row_abs: absolute,
2486                                start_col_abs: absolute,
2487                                end_row_abs: absolute,
2488                                end_col_abs: absolute,
2489                            };
2490                            let owned = ReferenceType::Range {
2491                                sheet: qualified.then(|| "Other".to_string()),
2492                                start_row: (!open_rows).then_some(1),
2493                                start_col: (!open_cols).then_some(1),
2494                                end_row: (!open_rows).then_some(3),
2495                                end_col: (!open_cols).then_some(3),
2496                                start_row_abs: absolute,
2497                                start_col_abs: absolute,
2498                                end_row_abs: absolute,
2499                                end_col_abs: absolute,
2500                            };
2501                            let expected = relocate_reference_for_offset(&owned, delta, delta)
2502                                .and_then(|r| {
2503                                    crate::traits::EvaluationContext::resolve_range_view(
2504                                        &engine, &r, "Other",
2505                                    )
2506                                });
2507                            let actual =
2508                                engine.resolve_arena_range(&compact, sheet_id, delta, delta);
2509                            match (actual, expected) {
2510                                (Ok(a), Ok(b)) => {
2511                                    assert_eq!(a.dims(), b.dims());
2512                                    assert_eq!(
2513                                        (a.start_row(), a.start_col()),
2514                                        (b.start_row(), b.start_col())
2515                                    );
2516                                    let (rows, cols) = a.dims();
2517                                    for row in 0..rows.min(8) {
2518                                        for col in 0..cols.min(8) {
2519                                            assert_eq!(a.get_cell(row, col), b.get_cell(row, col));
2520                                        }
2521                                    }
2522                                }
2523                                (Err(a), Err(b)) => assert_eq!(a.kind, b.kind),
2524                                (a, b) => assert_eq!(format!("{a:?}"), format!("{b:?}")),
2525                            }
2526                        }
2527                    }
2528                }
2529            }
2530        }
2531        engine.rename_sheet(sheet_id, "Renamed")?;
2532        assert!(
2533            engine
2534                .arrow_sheet_by_id(sheet_id)
2535                .is_some_and(|s| s.name.as_ref() == "Renamed")
2536        );
2537        assert!(engine.arrow_sheet_by_id_mut(sheet_id).is_some());
2538        let other = engine.add_sheet("Third")?;
2539        engine.sheet_store_mut().sheets.reverse();
2540        assert!(
2541            engine
2542                .arrow_sheet_by_id(sheet_id)
2543                .is_some_and(|s| s.name.as_ref() == "Renamed")
2544        );
2545        engine.remove_sheet(other)?;
2546        assert!(engine.arrow_sheet_by_id(sheet_id).is_some());
2547        Ok(())
2548    }
2549
2550    use crate::engine::{EvalConfig, eval::Engine};
2551    use crate::format::FormatId;
2552    use crate::test_workbook::TestWorkbook;
2553
2554    #[test]
2555    fn binary_fastpath_preserves_values_and_format_annotations() {
2556        let engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
2557        let interpreter = Interpreter::new(&engine, "Sheet1");
2558        let values = [
2559            LiteralValue::Number(2.5),
2560            LiteralValue::Int(2),
2561            LiteralValue::Text("3".into()),
2562            LiteralValue::Text("not numeric".into()),
2563            LiteralValue::Boolean(true),
2564            LiteralValue::Empty,
2565            LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)),
2566            LiteralValue::Array(vec![vec![LiteralValue::Number(1.0)]]),
2567        ];
2568        let formats = [
2569            None,
2570            Some(FormatId::DATE),
2571            Some(FormatId::TIME),
2572            Some(FormatId(49)),
2573        ];
2574        for (op, byte) in [
2575            (BinaryOperator::Add, b'+'),
2576            (BinaryOperator::Subtract, b'-'),
2577        ] {
2578            for left in &values {
2579                for right in &values {
2580                    for left_format in formats {
2581                        for right_format in formats {
2582                            let expected = interpreter
2583                                .numeric_binary(left.clone(), right.clone(), byte)
2584                                .map(|value| {
2585                                    interpreter.annotate_numeric_result(
2586                                        value,
2587                                        interpreter.binary_format(
2588                                            char::from(byte),
2589                                            left_format,
2590                                            right_format,
2591                                        ),
2592                                    )
2593                                });
2594                            let actual = interpreter.apply_binary_op(
2595                                op,
2596                                left.clone(),
2597                                left_format,
2598                                right.clone(),
2599                                right_format,
2600                            );
2601                            match (actual, expected) {
2602                                (Ok(actual), Ok(expected)) => {
2603                                    assert_eq!(actual.format_id(), expected.format_id());
2604                                    assert_eq!(actual.into_literal(), expected.into_literal());
2605                                }
2606                                (Err(actual), Err(expected)) => assert_eq!(actual, expected),
2607                                _ => panic!("operator result kind changed"),
2608                            }
2609                        }
2610                    }
2611                }
2612            }
2613        }
2614    }
2615
2616    #[test]
2617    fn temporal_binary_format_algebra_pins_positive_and_negative_cases() {
2618        let engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
2619        let interpreter = Interpreter::new(&engine, "Sheet1");
2620
2621        assert_eq!(
2622            interpreter.binary_format('+', Some(FormatId::DATE), Some(FormatId::TIME)),
2623            Some(FormatId::DATETIME)
2624        );
2625        assert_eq!(
2626            interpreter.binary_format('+', Some(FormatId::DATE), Some(FormatId(9))),
2627            Some(FormatId::DATE),
2628            "Date + Percent follows the measured temporal-wins rule"
2629        );
2630        assert_eq!(
2631            interpreter.binary_format('-', Some(FormatId::DATE), Some(FormatId::DATE)),
2632            None,
2633            "Date - Date is an unformatted duration in days"
2634        );
2635        assert_eq!(
2636            interpreter.binary_format('+', Some(FormatId::DATE), Some(FormatId(49))),
2637            None,
2638            "Date + Text must not acquire a temporal annotation"
2639        );
2640        for (left, right) in [
2641            (FormatId::DATE, FormatId::DATE),
2642            (FormatId::DURATION, FormatId::DATE),
2643            (FormatId::DATE, FormatId(5)),
2644            (FormatId::DATETIME, FormatId::TIME),
2645        ] {
2646            assert_eq!(
2647                interpreter.binary_format('+', Some(left), Some(right)),
2648                None
2649            );
2650        }
2651    }
2652}
2653
2654#[cfg(test)]
2655mod resolved_function_tests {
2656    use super::*;
2657    use crate::function::Function;
2658    use crate::traits::FunctionProvider;
2659    use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
2660    use std::sync::{Arc, RwLock};
2661
2662    struct TestFunction;
2663    impl Function for TestFunction {
2664        fn name(&self) -> &'static str {
2665            "FN_TABLE_GLOBAL_REFRESH_TEST"
2666        }
2667        fn eval<'a, 'b, 'c>(
2668            &self,
2669            _: &'c [ArgumentHandle<'a, 'b>],
2670            _: &dyn crate::traits::FunctionContext<'b>,
2671        ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
2672            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(1.0)))
2673        }
2674    }
2675    #[derive(Default)]
2676    struct Provider {
2677        generation: AtomicU64,
2678        calls: AtomicUsize,
2679        function: RwLock<Option<Arc<dyn Function>>>,
2680    }
2681    impl FunctionProvider for Provider {
2682        fn planning_semantic_revision(&self) -> Option<u64> {
2683            Some(self.generation.load(Ordering::Acquire))
2684        }
2685        fn function_capabilities(&self, _: &str, _: &str) -> Option<crate::function::FnCaps> {
2686            self.function.read().unwrap().as_ref().map(|f| f.caps())
2687        }
2688        fn get_function(&self, _: &str, _: &str) -> Option<Arc<dyn Function>> {
2689            self.calls.fetch_add(1, Ordering::Relaxed);
2690            self.function.read().unwrap().clone()
2691        }
2692    }
2693    fn arena(name: &str) -> (DataStore, crate::engine::arena::StringId) {
2694        let mut store = DataStore::new();
2695        let registry = SheetRegistry::new();
2696        let ast = ASTNode::new(
2697            ASTNodeType::Function {
2698                name: name.into(),
2699                args: vec![],
2700            },
2701            None,
2702        );
2703        let node = store.store_ast(&ast, &registry);
2704        let Some(AstNodeData::Function { name_id, .. }) = store.get_node(node) else {
2705            panic!("function node")
2706        };
2707        let id = *name_id;
2708        (store, id)
2709    }
2710    #[test]
2711    fn function_table_tracks_distinct_names_not_interner_size() {
2712        let mut store = DataStore::new();
2713        let registry = SheetRegistry::new();
2714        for row in 1..=200_000 {
2715            store.store_ast(
2716                &formualizer_parse::parser::parse(format!("=A{row}+1")).unwrap(),
2717                &registry,
2718            );
2719        }
2720        store.store_ast(
2721            &formualizer_parse::parser::parse("=ABS(1)").unwrap(),
2722            &registry,
2723        );
2724        let mut table = ResolvedFunctions::default();
2725        table.refresh(&store, &Provider::default());
2726        assert_eq!(table.functions.len(), 1);
2727        assert_eq!(table.capabilities.len(), 1);
2728        assert_eq!(table.slots.len(), 1);
2729    }
2730
2731    #[test]
2732    fn formula_edits_preserve_existing_function_slots() {
2733        let provider = Provider::default();
2734        let (mut store, id) = arena("EXISTING");
2735        let mut table = ResolvedFunctions::default();
2736        table.refresh(&store, &provider);
2737        let slot = table.slots[&id];
2738        let revision = table.revision;
2739        store.store_ast(
2740            &formualizer_parse::parser::parse("=EXISTING()+1").unwrap(),
2741            &SheetRegistry::new(),
2742        );
2743        table.refresh(&store, &provider);
2744        // Unrelated registry tests may advance the global epoch concurrently.
2745        // Slot reuse is guaranteed only while that semantic revision is stable.
2746        if table.revision == revision {
2747            assert_eq!(provider.calls.load(Ordering::Relaxed), 1);
2748        }
2749        let previous_calls = provider.calls.load(Ordering::Relaxed);
2750        let previous_revision = table.revision;
2751        store.store_ast(
2752            &formualizer_parse::parser::parse("=NEW_NAME()").unwrap(),
2753            &SheetRegistry::new(),
2754        );
2755        table.refresh(&store, &provider);
2756        if table.revision == previous_revision {
2757            assert_eq!(provider.calls.load(Ordering::Relaxed), previous_calls + 1);
2758        }
2759        if table.revision == revision {
2760            assert_eq!(table.slots[&id], slot);
2761        }
2762        assert_eq!(table.functions.len(), 2);
2763    }
2764
2765    #[test]
2766    fn unversioned_provider_is_resolved_fresh_each_request() {
2767        struct Unversioned(Provider);
2768        impl FunctionProvider for Unversioned {
2769            fn get_function(&self, ns: &str, name: &str) -> Option<Arc<dyn Function>> {
2770                self.0.get_function(ns, name)
2771            }
2772        }
2773        let provider = Unversioned(Provider::default());
2774        let (store, id) = arena("CHANGING");
2775        let mut table = ResolvedFunctions::default();
2776        table.refresh(&store, &provider);
2777        assert!(table.get(id).is_none());
2778        *provider.0.function.write().unwrap() = Some(Arc::new(TestFunction));
2779        table.refresh(&store, &provider);
2780        assert!(table.get(id).is_some());
2781        let previous = table.functions[0].clone().unwrap();
2782        *provider.0.function.write().unwrap() = Some(Arc::new(TestFunction));
2783        table.refresh(&store, &provider);
2784        assert!(!Arc::ptr_eq(
2785            &previous,
2786            table.functions[0].as_ref().unwrap()
2787        ));
2788    }
2789
2790    #[test]
2791    fn hits_and_misses_are_read_only_and_local_generations_refresh() {
2792        let provider = Provider::default();
2793        let (store, id) = arena("MISSING_THEN_REGISTERED");
2794        let mut table = ResolvedFunctions::default();
2795        table.refresh(&store, &provider);
2796        for _ in 0..10 {
2797            assert!(table.get(id).is_none());
2798        }
2799        assert_eq!(provider.calls.load(Ordering::Relaxed), 1);
2800        *provider.function.write().unwrap() = Some(Arc::new(TestFunction));
2801        provider.generation.fetch_add(1, Ordering::Release);
2802        // Registration during a pass cannot mutate the published snapshot.
2803        assert!(table.get(id).is_none());
2804        table.refresh(&store, &provider);
2805        assert!(table.get(id).is_some());
2806        let calls = provider.calls.load(Ordering::Relaxed);
2807        for _ in 0..10 {
2808            assert!(table.get(id).is_some());
2809        }
2810        assert_eq!(provider.calls.load(Ordering::Relaxed), calls);
2811        *provider.function.write().unwrap() = None;
2812        provider.generation.fetch_add(1, Ordering::Release);
2813        assert!(table.get(id).is_some());
2814        table.refresh(&store, &provider);
2815        assert!(table.get(id).is_none());
2816    }
2817    #[test]
2818    fn global_epoch_and_excel_prefix_aliases_refresh() {
2819        struct Global;
2820        impl FunctionProvider for Global {
2821            fn get_function(&self, ns: &str, name: &str) -> Option<Arc<dyn Function>> {
2822                crate::function_registry::get(ns, name)
2823            }
2824        }
2825        let (store, id) = arena("_xlfn.FN_TABLE_GLOBAL_REFRESH_TEST");
2826        let mut table = ResolvedFunctions::default();
2827        table.refresh(&store, &Global);
2828        assert!(table.get(id).is_none());
2829        crate::function_registry::register_function(Arc::new(TestFunction));
2830        table.refresh(&store, &Global);
2831        assert_eq!(
2832            table.get(id).unwrap().name(),
2833            "FN_TABLE_GLOBAL_REFRESH_TEST"
2834        );
2835        let previous = table
2836            .functions
2837            .get(*table.slots.get(&id).unwrap())
2838            .unwrap()
2839            .as_ref()
2840            .unwrap()
2841            .clone();
2842        crate::function_registry::register_function(Arc::new(TestFunction));
2843        table.refresh(&store, &Global);
2844        let replacement = table
2845            .functions
2846            .get(*table.slots.get(&id).unwrap())
2847            .unwrap()
2848            .as_ref()
2849            .unwrap();
2850        assert!(!Arc::ptr_eq(&previous, replacement));
2851    }
2852}