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