Skip to main content

formualizer_eval/
traits.rs

1use crate::engine::lookup_index_cache::{LookupAxis, LookupIndex};
2use crate::engine::range_view::RangeView;
3use crate::engine::row_visibility::VisibilityMaskMode;
4pub use crate::function::Function;
5use crate::interpreter::Interpreter;
6use crate::reference::CellRef;
7use formualizer_common::{
8    LiteralValue,
9    error::{ExcelError, ExcelErrorKind},
10};
11use std::any::Any;
12use std::borrow::Cow;
13use std::fmt::Debug;
14use std::sync::Arc;
15
16use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType, TableSpecifier};
17
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct ReferenceInfo {
20    /// Excel-style 1-based index of the first sheet covered by the reference.
21    pub first_sheet_index: Option<usize>,
22    /// Number of sheets covered by the reference (`1` for ordinary references, `N` for 3D refs).
23    pub sheet_count: Option<usize>,
24    /// Top-left / first cell addressed by the reference, when it resolves to a concrete cell.
25    pub first_cell: Option<CellRef>,
26}
27
28/* ───────────────────────────── Range ───────────────────────────── */
29
30pub trait Range: Debug + Send + Sync {
31    fn get(&self, row: usize, col: usize) -> Result<LiteralValue, ExcelError>;
32    fn dimensions(&self) -> (usize, usize);
33
34    fn is_sparse(&self) -> bool {
35        false
36    }
37
38    // Handle infinite ranges (A:A, 1:1)
39    fn is_infinite(&self) -> bool {
40        false
41    }
42
43    fn materialise(&self) -> Cow<'_, [Vec<LiteralValue>]> {
44        Cow::Owned(
45            (0..self.dimensions().0)
46                .map(|r| {
47                    (0..self.dimensions().1)
48                        .map(|c| self.get(r, c).unwrap_or(LiteralValue::Empty))
49                        .collect()
50                })
51                .collect(),
52        )
53    }
54
55    fn iter_cells<'a>(&'a self) -> Box<dyn Iterator<Item = LiteralValue> + 'a> {
56        let (rows, cols) = self.dimensions();
57        Box::new((0..rows).flat_map(move |r| (0..cols).map(move |c| self.get(r, c).unwrap())))
58    }
59    fn iter_rows<'a>(&'a self) -> Box<dyn Iterator<Item = Vec<LiteralValue>> + 'a> {
60        let (rows, cols) = self.dimensions();
61        Box::new((0..rows).map(move |r| (0..cols).map(|c| self.get(r, c).unwrap()).collect()))
62    }
63
64    /* down-cast hook for SIMD back-ends */
65    fn as_any(&self) -> &dyn Any;
66}
67
68/* blanket dyn passthrough */
69impl Range for Box<dyn Range> {
70    fn get(&self, r: usize, c: usize) -> Result<LiteralValue, ExcelError> {
71        (**self).get(r, c)
72    }
73    fn dimensions(&self) -> (usize, usize) {
74        (**self).dimensions()
75    }
76    fn is_sparse(&self) -> bool {
77        (**self).is_sparse()
78    }
79    fn materialise(&self) -> Cow<'_, [Vec<LiteralValue>]> {
80        (**self).materialise()
81    }
82    fn iter_cells<'a>(&'a self) -> Box<dyn Iterator<Item = LiteralValue> + 'a> {
83        (**self).iter_cells()
84    }
85    fn iter_rows<'a>(&'a self) -> Box<dyn Iterator<Item = Vec<LiteralValue>> + 'a> {
86        (**self).iter_rows()
87    }
88    fn as_any(&self) -> &dyn Any {
89        (**self).as_any()
90    }
91}
92
93/* ────────────────────── ArgumentHandle helpers ───────────────────── */
94
95pub type CowValue<'a> = Cow<'a, LiteralValue>;
96
97pub trait CustomCallable: Send + Sync {
98    fn arity(&self) -> usize;
99
100    fn invoke<'ctx>(
101        &self,
102        interp: &Interpreter<'ctx>,
103        args: &[LiteralValue],
104    ) -> Result<CalcValue<'ctx>, ExcelError>;
105
106    /// Invoke with each argument's optional bound reference alongside the
107    /// already-materialized values.
108    ///
109    /// `references[i]` is the spreadsheet reference argument `i` was written
110    /// as, when it was written as one; `None` otherwise, and a short slice is
111    /// read as `None` for every missing position. A callable that binds its
112    /// arguments to locals uses this to bind a range-valued parameter as
113    /// [`crate::interpreter::LocalBinding::ValueWithReference`], so a by-ref
114    /// argument slot inside the body sees the range rather than a lifted
115    /// array.
116    ///
117    /// Defaults to [`Self::invoke`], so an implementor that does not care
118    /// about references needs no change.
119    fn invoke_with_references<'ctx>(
120        &self,
121        interp: &Interpreter<'ctx>,
122        args: &[LiteralValue],
123        references: &[Option<ReferenceType>],
124    ) -> Result<CalcValue<'ctx>, ExcelError> {
125        let _ = references;
126        self.invoke(interp, args)
127    }
128}
129
130#[derive(Clone)]
131pub enum CalcValue<'a> {
132    Scalar(LiteralValue),
133    /// Scalar carrying an eval-internal number-format annotation.
134    AnnotatedScalar(LiteralValue, crate::format::FormatId),
135    Range(RangeView<'a>),
136    Callable(Arc<dyn CustomCallable>),
137}
138
139/// The result of resolving an argument where either a reference or a value is accepted.
140///
141/// Reference-shaped syntax is resolved without first evaluating it as a value.
142/// All other syntax is evaluated through [`ArgumentHandle::value`] and retains
143/// its `CalcValue` discriminant.
144#[derive(Clone)]
145pub(crate) enum ResolvedArgument<'a> {
146    Range(RangeView<'a>),
147    ReferenceError(ExcelError),
148    Value(CalcValue<'a>),
149}
150
151impl std::fmt::Debug for ResolvedArgument<'_> {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        match self {
154            Self::Range(view) => f.debug_tuple("Range").field(view).finish(),
155            Self::ReferenceError(error) => f.debug_tuple("ReferenceError").field(error).finish(),
156            Self::Value(value) => f.debug_tuple("Value").field(value).finish(),
157        }
158    }
159}
160
161impl<'a> std::fmt::Debug for CalcValue<'a> {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        match self {
164            CalcValue::Scalar(v) => f.debug_tuple("Scalar").field(v).finish(),
165            CalcValue::AnnotatedScalar(v, format) => f
166                .debug_tuple("AnnotatedScalar")
167                .field(v)
168                .field(format)
169                .finish(),
170            CalcValue::Range(rv) => {
171                let (r, c) = rv.dims();
172                f.debug_tuple("Range").field(&(r, c)).finish()
173            }
174            CalcValue::Callable(_) => f.write_str("Callable(<opaque>)"),
175        }
176    }
177}
178
179// Thread-local instrumentation keeps serial publication regressions deterministic
180// without counting materialization performed by unrelated parallel tests.
181#[cfg(test)]
182thread_local! {
183    pub(crate) static RANGE_MATERIALIZED_CELLS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
184}
185
186impl<'a> CalcValue<'a> {
187    pub fn into_literal(self) -> LiteralValue {
188        match self {
189            CalcValue::Scalar(s) | CalcValue::AnnotatedScalar(s, _) => s,
190            CalcValue::Range(rv) => {
191                let (rows, cols) = rv.dims();
192                #[cfg(test)]
193                RANGE_MATERIALIZED_CELLS.with(|count| count.set(count.get() + rows * cols));
194                if rows == 1 && cols == 1 {
195                    rv.get_cell(0, 0)
196                } else if let Some(error) = rv.materialization_error() {
197                    LiteralValue::Error(error)
198                } else {
199                    let mut data = Vec::with_capacity(rows);
200                    for row_idx in 0..rows {
201                        let mut row = Vec::with_capacity(cols);
202                        for col_idx in 0..cols {
203                            row.push(rv.get_cell(row_idx, col_idx));
204                        }
205                        data.push(row);
206                    }
207                    LiteralValue::Array(data)
208                }
209            }
210            CalcValue::Callable(_) => LiteralValue::Error(
211                ExcelError::new(ExcelErrorKind::Calc).with_message("LAMBDA value must be invoked"),
212            ),
213        }
214    }
215
216    pub fn as_scalar(&self) -> Option<&LiteralValue> {
217        match self {
218            CalcValue::Scalar(s) | CalcValue::AnnotatedScalar(s, _) => Some(s),
219            _ => None,
220        }
221    }
222
223    pub fn format_id(&self) -> Option<crate::format::FormatId> {
224        match self {
225            CalcValue::AnnotatedScalar(_, format) => Some(*format),
226            _ => None,
227        }
228    }
229
230    pub fn with_format(self, format: Option<crate::format::FormatId>) -> Self {
231        let format = format.filter(|id| *id != crate::format::FormatId::GENERAL);
232        match (self, format) {
233            (CalcValue::Scalar(value) | CalcValue::AnnotatedScalar(value, _), Some(id)) => {
234                CalcValue::AnnotatedScalar(value, id)
235            }
236            (CalcValue::AnnotatedScalar(value, _), None) => CalcValue::Scalar(value),
237            (other, _) => other,
238        }
239    }
240
241    pub fn into_scalar_parts(self) -> (LiteralValue, Option<crate::format::FormatId>) {
242        match self {
243            CalcValue::Scalar(value) => (value, None),
244            CalcValue::AnnotatedScalar(value, format) => (value, Some(format)),
245            other => (other.into_literal(), None),
246        }
247    }
248
249    pub fn as_range(&self) -> Option<&RangeView<'a>> {
250        match self {
251            CalcValue::Range(r) => Some(r),
252            _ => None,
253        }
254    }
255
256    pub fn as_callable(&self) -> Option<&Arc<dyn CustomCallable>> {
257        match self {
258            CalcValue::Callable(c) => Some(c),
259            _ => None,
260        }
261    }
262
263    pub fn into_owned(self) -> LiteralValue {
264        self.into_literal()
265    }
266}
267
268impl From<CalcValue<'_>> for LiteralValue {
269    fn from(val: CalcValue<'_>) -> Self {
270        val.into_literal()
271    }
272}
273
274impl<'a> PartialEq<LiteralValue> for CalcValue<'a> {
275    fn eq(&self, other: &LiteralValue) -> bool {
276        match self {
277            CalcValue::Scalar(s) | CalcValue::AnnotatedScalar(s, _) => s == other,
278            CalcValue::Range(rv) => match other {
279                LiteralValue::Array(arr) => {
280                    let (rows, cols) = rv.dims();
281                    if arr.len() != rows {
282                        return false;
283                    }
284                    for (r, row) in arr.iter().enumerate() {
285                        if row.len() != cols {
286                            return false;
287                        }
288                        for (c, cell) in row.iter().enumerate() {
289                            if &rv.get_cell(r, c) != cell {
290                                return false;
291                            }
292                        }
293                    }
294                    true
295                }
296                _ => {
297                    let (rows, cols) = rv.dims();
298                    rows == 1 && cols == 1 && &rv.get_cell(0, 0) == other
299                }
300            },
301            CalcValue::Callable(_) => false,
302        }
303    }
304}
305
306impl<'a> PartialEq<CalcValue<'a>> for LiteralValue {
307    fn eq(&self, other: &CalcValue<'a>) -> bool {
308        other == self
309    }
310}
311
312pub enum EvaluatedArg<'a> {
313    LiteralValue(CowValue<'a>),
314    Range(Box<dyn Range>),
315}
316
317enum ArgumentExpr<'a> {
318    Ast(&'a ASTNode),
319    Arena {
320        id: crate::engine::arena::AstNodeId,
321        data_store: &'a crate::engine::arena::DataStore,
322        sheet_registry: &'a crate::engine::sheet_registry::SheetRegistry,
323    },
324}
325
326pub struct ArgumentHandle<'a, 'b> {
327    expr: ArgumentExpr<'a>,
328    interp: &'a Interpreter<'b>,
329    caches: std::cell::OnceCell<Box<ArgumentCaches<'b>>>,
330    /// Validation and builtin evaluation both read arguments. Memoization keeps
331    /// nested calls linear instead of re-evaluating a leaf 2^depth times.
332    /// Handles are per-call/per-evaluation; value_with_env deliberately bypasses
333    /// this memo because its local environment may change.
334    /// Scalar results/errors are hot; AST/reference/resolution caches stay cold.
335    cached_value: std::cell::OnceCell<Result<MemoizedValue<'b>, ExcelError>>,
336}
337
338/// Keep the common scalar memo inline without paying for RangeView in every handle.
339enum MemoizedValue<'b> {
340    Scalar(LiteralValue, Option<crate::format::FormatId>),
341    Other(Box<CalcValue<'b>>),
342}
343
344impl<'b> MemoizedValue<'b> {
345    fn new(value: CalcValue<'b>) -> Self {
346        match value {
347            CalcValue::Scalar(value) => Self::Scalar(value, None),
348            CalcValue::AnnotatedScalar(value, format) => Self::Scalar(value, Some(format)),
349            other => Self::Other(Box::new(other)),
350        }
351    }
352
353    fn value(&self) -> CalcValue<'b> {
354        match self {
355            Self::Scalar(value, None) => CalcValue::Scalar(value.clone()),
356            Self::Scalar(value, Some(format)) => CalcValue::AnnotatedScalar(value.clone(), *format),
357            Self::Other(value) => (**value).clone(),
358        }
359    }
360}
361
362#[derive(Default)]
363struct ArgumentCaches<'b> {
364    cached_ast: std::cell::OnceCell<ASTNode>,
365    cached_ref: std::cell::OnceCell<ReferenceType>,
366    cached_reference_or_value:
367        std::cell::OnceCell<Result<crate::function::FunctionResolution<'b>, ExcelError>>,
368    cached_resolved: std::cell::OnceCell<Result<ResolvedArgument<'b>, ExcelError>>,
369}
370
371impl<'a, 'b> ArgumentHandle<'a, 'b> {
372    fn caches(&self) -> &ArgumentCaches<'b> {
373        self.caches
374            .get_or_init(|| Box::new(ArgumentCaches::default()))
375    }
376
377    pub(crate) fn new(node: &'a ASTNode, interp: &'a Interpreter<'b>) -> Self {
378        Self {
379            expr: ArgumentExpr::Ast(node),
380            interp,
381            caches: std::cell::OnceCell::new(),
382            cached_value: std::cell::OnceCell::new(),
383        }
384    }
385
386    pub(crate) fn new_arena(
387        id: crate::engine::arena::AstNodeId,
388        interp: &'a Interpreter<'b>,
389        data_store: &'a crate::engine::arena::DataStore,
390        sheet_registry: &'a crate::engine::sheet_registry::SheetRegistry,
391    ) -> Self {
392        Self {
393            expr: ArgumentExpr::Arena {
394                id,
395                data_store,
396                sheet_registry,
397            },
398            interp,
399            caches: std::cell::OnceCell::new(),
400            cached_value: std::cell::OnceCell::new(),
401        }
402    }
403
404    /// Workbook date system in force for the evaluation this argument belongs to.
405    ///
406    /// Lets value-collecting helpers resolve date literals to serials without
407    /// threading a `DateSystem` (or the whole `FunctionContext`) through every
408    /// call site.
409    pub(crate) fn date_system(&self) -> crate::engine::DateSystem {
410        self.interp.context.date_system()
411    }
412
413    /// Returns whether this handle represents an explicitly omitted argument slot.
414    ///
415    /// This is false for absent arguments, explicit empty text, and blank references.
416    pub fn is_omitted(&self) -> bool {
417        match &self.expr {
418            ArgumentExpr::Ast(node) => matches!(node.node_type, ASTNodeType::Omitted),
419            ArgumentExpr::Arena { id, data_store, .. } => matches!(
420                data_store.get_node(*id),
421                Some(crate::engine::arena::AstNodeData::Omitted)
422            ),
423        }
424    }
425
426    /// The reference this argument spells directly, found without evaluating
427    /// anything: a cell or range reference, a defined name, or a LET/LAMBDA
428    /// local bound to a range. `None` for a computed argument (a function
429    /// call, an operator, a literal) and for a reference that fails to
430    /// resolve.
431    ///
432    /// This is not [`Self::as_reference`]: its arena arm resolves through
433    /// `reference_for_eval`, which evaluates a `Function` or `BinaryOp` node
434    /// (such as `OFFSET(...)` or `A1:A3*1`), and its AST arm returns the
435    /// written reference without the current family offset. Here the node kind
436    /// is checked first, so nothing is evaluated, and a bare reference resolves
437    /// the same way on the AST and arena paths.
438    pub(crate) fn bare_reference(&self) -> Option<ReferenceType> {
439        let is_reference = match &self.expr {
440            ArgumentExpr::Ast(node) => matches!(node.node_type, ASTNodeType::Reference { .. }),
441            ArgumentExpr::Arena { id, data_store, .. } => matches!(
442                data_store.get_node(*id),
443                Some(crate::engine::arena::AstNodeData::Reference { .. })
444            ),
445        };
446        if !is_reference {
447            return None;
448        }
449        self.as_reference_or_eval().ok()
450    }
451
452    /// The spill rectangle of the anchor this argument names, as `ANCHORARRAY`
453    /// takes it: the argument must be written as a reference (a single cell,
454    /// a name, or a LET/LAMBDA local bound to one), and is resolved through
455    /// the same context hook as the `#` operator. Anything else is `#REF!`.
456    pub(crate) fn spill_reference(&self) -> Result<ReferenceType, ExcelError> {
457        match &self.expr {
458            ArgumentExpr::Ast(node) => self.interp.ast_spill_reference(node),
459            ArgumentExpr::Arena {
460                id,
461                data_store,
462                sheet_registry,
463            } => self
464                .interp
465                .arena_spill_reference(*id, data_store, sheet_registry),
466        }
467    }
468
469    /// Returns whether this argument resolves as a spreadsheet reference rather than a value.
470    ///
471    /// This uses the interpreter's reference-resolution path, so reference-returning functions
472    /// are included only when they actually produce a reference. A computed array remains a value
473    /// even though both it and a cell range are represented by [`CalcValue::Range`].
474    pub(crate) fn has_reference_semantics(&self) -> bool {
475        self.reference_attempt().is_some()
476    }
477
478    /// Return whether this argument's syntax may produce a spreadsheet reference.
479    ///
480    /// Unlike [`Self::has_reference_semantics`], this does not evaluate a
481    /// reference-returning function to discover which arm it selects.
482    pub(crate) fn may_return_reference(&self) -> bool {
483        match &self.expr {
484            ArgumentExpr::Ast(node) => match &node.node_type {
485                ASTNodeType::Reference { reference, .. } => match reference {
486                    // A local is a reference only when it was bound to one.
487                    ReferenceType::NamedRange(name)
488                        if self.interp.resolve_local_name(name).is_some() =>
489                    {
490                        self.interp.resolve_local_bound_reference(name).is_some()
491                    }
492                    _ => true,
493                },
494                ASTNodeType::BinaryOp { op, .. } => *op == formualizer_parse::BinaryOperator::Range,
495                ASTNodeType::UnaryOp { op, .. } => *op == formualizer_parse::UnaryOperator::Spill,
496                ASTNodeType::Function { name, .. } => self
497                    .interp
498                    .ast_function_capabilities(name)
499                    .is_some_and(|caps| caps.contains(crate::function::FnCaps::RETURNS_REFERENCE)),
500                _ => false,
501            },
502            ArgumentExpr::Arena { id, data_store, .. } => match data_store.get_node(*id) {
503                Some(crate::engine::arena::AstNodeData::Reference { ref_type, .. }) => {
504                    let ref_type = data_store.compact_ref(*ref_type);
505                    match ref_type {
506                        // Same rule as the AST branch above.
507                        crate::engine::arena::CompactRefType::NamedRange(name_id)
508                            if self
509                                .interp
510                                .resolve_local_name(data_store.resolve_ast_string(*name_id))
511                                .is_some() =>
512                        {
513                            self.interp
514                                .resolve_local_bound_reference(
515                                    data_store.resolve_ast_string(*name_id),
516                                )
517                                .is_some()
518                        }
519                        _ => true,
520                    }
521                }
522                Some(crate::engine::arena::AstNodeData::BinaryOp { operator, .. }) => {
523                    *operator == formualizer_parse::BinaryOperator::Range
524                }
525                Some(crate::engine::arena::AstNodeData::UnaryOp { operator, .. }) => {
526                    *operator == formualizer_parse::UnaryOperator::Spill
527                }
528                Some(crate::engine::arena::AstNodeData::Function { name_id, .. }) => self
529                    .interp
530                    .arena_function_capabilities(*name_id, data_store)
531                    .is_some_and(|caps| caps.contains(crate::function::FnCaps::RETURNS_REFERENCE)),
532                _ => false,
533            },
534        }
535    }
536
537    pub fn value(&self) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
538        self.cached_value
539            .get_or_init(|| self.compute_value().map(MemoizedValue::new))
540            .as_ref()
541            .map(MemoizedValue::value)
542            .map_err(Clone::clone)
543    }
544
545    /// Resolves a scalar that is about to be coerced to text.
546    ///
547    /// Omitted arguments materialize as numeric zero through `value()`, which is
548    /// correct for Any/numeric consumers and aggregates. Text consumers must use
549    /// this boundary so omission becomes empty text without changing explicit 0.
550    pub(crate) fn value_for_text(&self) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
551        if self.is_omitted() {
552            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
553                String::new(),
554            )))
555        } else {
556            self.value()
557        }
558    }
559
560    pub(crate) fn resolve_once_for_text(&self) -> Result<ResolvedArgument<'b>, ExcelError> {
561        if self.is_omitted() {
562            Ok(ResolvedArgument::Value(crate::traits::CalcValue::Scalar(
563                LiteralValue::Text(String::new()),
564            )))
565        } else {
566            self.resolve_once()
567        }
568    }
569
570    fn compute_value(&self) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
571        match &self.expr {
572            ArgumentExpr::Ast(node) => match &node.node_type {
573                ASTNodeType::Literal(v) => Ok(crate::traits::CalcValue::Scalar(v.clone())),
574                // With no schema-level text policy, Number(0) is the neutral Any-policy
575                // materialization. Text consumers resolve through `value_for_text`.
576                ASTNodeType::Omitted => {
577                    Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)))
578                }
579                _ => self.interp.evaluate_ast(node),
580            },
581            ArgumentExpr::Arena {
582                id,
583                data_store,
584                sheet_registry,
585            } => {
586                if matches!(
587                    data_store.get_node(*id),
588                    Some(crate::engine::arena::AstNodeData::Omitted)
589                ) {
590                    Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)))
591                } else {
592                    self.interp
593                        .evaluate_arena_ast(*id, data_store, sheet_registry)
594                }
595            }
596        }
597    }
598
599    pub fn value_with_env(
600        &self,
601        env: crate::interpreter::LocalEnv,
602    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
603        let scoped = self.interp.with_local_env(env);
604        match &self.expr {
605            ArgumentExpr::Ast(node) => match &node.node_type {
606                ASTNodeType::Literal(v) => Ok(crate::traits::CalcValue::Scalar(v.clone())),
607                ASTNodeType::Omitted => {
608                    Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)))
609                }
610                _ => scoped.evaluate_ast(node),
611            },
612            ArgumentExpr::Arena {
613                id,
614                data_store,
615                sheet_registry,
616            } => {
617                if matches!(
618                    data_store.get_node(*id),
619                    Some(crate::engine::arena::AstNodeData::Omitted)
620                ) {
621                    Ok(crate::traits::CalcValue::Scalar(LiteralValue::Number(0.0)))
622                } else {
623                    scoped.evaluate_arena_ast(*id, data_store, sheet_registry)
624                }
625            }
626        }
627    }
628
629    pub fn current_env(&self) -> crate::interpreter::LocalEnv {
630        self.interp.local_env().clone()
631    }
632
633    /// The spreadsheet reference this binding expression names, evaluated under
634    /// `env`, so a LET/LAMBDA local can keep the range it was bound to.
635    ///
636    /// Only syntactic reference expressions qualify: a computed value has no
637    /// reference to preserve, and a reference-returning function is left to the
638    /// ordinary by-ref path rather than evaluated a second time at bind time.
639    pub(crate) fn bound_reference_in_env(
640        &self,
641        env: &crate::interpreter::LocalEnv,
642    ) -> Option<ReferenceType> {
643        match &self.expr {
644            ArgumentExpr::Ast(node) => match &node.node_type {
645                ASTNodeType::Reference { reference, .. } => {
646                    if let ReferenceType::NamedRange(name) = reference
647                        && let Some(binding) = env.lookup(name)
648                    {
649                        // Rebinding a local: carry its reference forward, if any.
650                        return match binding {
651                            crate::interpreter::LocalBinding::ValueWithReference {
652                                reference,
653                                ..
654                            } => Some(reference),
655                            _ => None,
656                        };
657                    }
658                    self.interp.reference_for_current_offset(reference).ok()
659                }
660                ASTNodeType::BinaryOp { op, .. }
661                    if *op == formualizer_parse::BinaryOperator::Range =>
662                {
663                    self.interp
664                        .with_local_env(env.clone())
665                        .evaluate_ast_as_reference(node)
666                        .ok()
667                }
668                ASTNodeType::UnaryOp { op, expr }
669                    if *op == formualizer_parse::UnaryOperator::Spill =>
670                {
671                    self.interp
672                        .with_local_env(env.clone())
673                        .ast_spill_reference(expr)
674                        .ok()
675                }
676                _ => None,
677            },
678            ArgumentExpr::Arena {
679                id,
680                data_store,
681                sheet_registry,
682            } => match data_store.get_node(*id) {
683                Some(crate::engine::arena::AstNodeData::Reference { ref_type, .. }) => {
684                    let ref_type = data_store.compact_ref(*ref_type);
685                    if let crate::engine::arena::CompactRefType::NamedRange(name_id) = ref_type
686                        && let Some(binding) = env.lookup(data_store.resolve_ast_string(*name_id))
687                    {
688                        return match binding {
689                            crate::interpreter::LocalBinding::ValueWithReference {
690                                reference,
691                                ..
692                            } => Some(reference),
693                            _ => None,
694                        };
695                    }
696                    let reference =
697                        data_store.reconstruct_reference_type_for_eval(ref_type, sheet_registry);
698                    self.interp.reference_for_current_offset(&reference).ok()
699                }
700                Some(crate::engine::arena::AstNodeData::BinaryOp { operator, .. })
701                    if *operator == formualizer_parse::BinaryOperator::Range =>
702                {
703                    self.interp
704                        .with_local_env(env.clone())
705                        .evaluate_arena_ast_as_reference(*id, data_store, sheet_registry)
706                        .ok()
707                }
708                Some(crate::engine::arena::AstNodeData::UnaryOp { operator, expr_id })
709                    if *operator == formualizer_parse::UnaryOperator::Spill =>
710                {
711                    self.interp
712                        .with_local_env(env.clone())
713                        .arena_spill_reference(*expr_id, data_store, sheet_registry)
714                        .ok()
715                }
716                _ => None,
717            },
718        }
719    }
720
721    pub fn inline_array_literal(&self) -> Result<Option<Vec<Vec<LiteralValue>>>, ExcelError> {
722        match &self.expr {
723            ArgumentExpr::Ast(node) => match &node.node_type {
724                ASTNodeType::Literal(LiteralValue::Array(arr)) => Ok(Some(arr.clone())),
725                _ => Ok(None),
726            },
727            ArgumentExpr::Arena {
728                id,
729                data_store,
730                sheet_registry,
731            } => {
732                let node = data_store.get_node(*id).ok_or_else(|| {
733                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
734                })?;
735                match node {
736                    crate::engine::arena::AstNodeData::Literal(vref) => {
737                        match data_store.retrieve_value(*vref) {
738                            LiteralValue::Array(arr) => Ok(Some(arr)),
739                            _ => Ok(None),
740                        }
741                    }
742                    _ => {
743                        // preserve existing behavior: only a literal array (not a computed array)
744                        // is treated as "inline array literal".
745                        let _ = sheet_registry;
746                        Ok(None)
747                    }
748                }
749            }
750        }
751    }
752
753    /// Resolve a name that a LET/LAMBDA local may shadow, on the by-ref path.
754    ///
755    /// `Some(Ok(reference))` when the local was bound to a reference expression.
756    /// That includes a bare workbook defined name: a local bound to `K` carries
757    /// `NamedRange("K")`, and the consumer resolves that workbook name exactly as
758    /// it would resolve `K` written in place, whatever `K` is defined as.
759    /// `Some(Err(#VALUE!))` when the local was bound to a computed value, so the
760    /// consumer's own error path runs instead of the workbook-name route (which
761    /// would report the local's spelling as an undefined name). A consumer that
762    /// has a value fallback (MATCH) takes it; one that does not (OFFSET, ROW,
763    /// COLUMN) answers `#VALUE!`, as Excel does. `None` when the name is not
764    /// locally bound and the workbook-name route is correct.
765    ///
766    /// [`Self::reference_attempt`] and [`Self::may_return_reference`] apply the
767    /// same rule, so a reference-returning selector (`IF`, `CHOOSE`) passes a
768    /// range-bound local through as its range. A local bound to a computed value
769    /// stays on the value path everywhere, which keeps `=LET(p,FALSE,OR(p))` a
770    /// boolean.
771    fn local_named_reference(&self, name: &str) -> Option<Result<ReferenceType, ExcelError>> {
772        // `None` here sends the name down the ordinary workbook-name route.
773        self.interp.resolve_local_name(name)?;
774        Some(
775            self.interp
776                .resolve_local_bound_reference(name)
777                .ok_or_else(|| {
778                    ExcelError::new(ExcelErrorKind::Value)
779                        .with_message("LET/LAMBDA local is not a reference")
780                }),
781        )
782    }
783
784    fn reference_for_eval(&self) -> Result<ReferenceType, ExcelError> {
785        match &self.expr {
786            ArgumentExpr::Ast(node) => match &node.node_type {
787                ASTNodeType::Reference { reference, .. } => {
788                    // A LET/LAMBDA local shadows any workbook name of the same
789                    // spelling, so it must never take the named-range route.
790                    if let ReferenceType::NamedRange(name) = reference
791                        && let Some(local) = self.local_named_reference(name)
792                    {
793                        return local;
794                    }
795                    self.interp.reference_for_current_offset(reference)
796                }
797                ASTNodeType::Function { .. } | ASTNodeType::BinaryOp { .. } => {
798                    self.interp.evaluate_ast_as_reference(node)
799                }
800                ASTNodeType::UnaryOp { op, expr }
801                    if *op == formualizer_parse::UnaryOperator::Spill =>
802                {
803                    self.interp.ast_spill_reference(expr)
804                }
805                _ => Err(ExcelError::new(ExcelErrorKind::Ref)
806                    .with_message("Expected a reference (by-ref argument)")),
807            },
808            ArgumentExpr::Arena {
809                id,
810                data_store,
811                sheet_registry,
812            } => {
813                let node = data_store.get_node(*id).ok_or_else(|| {
814                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
815                })?;
816                match node {
817                    crate::engine::arena::AstNodeData::Reference { ref_type, .. } => {
818                        let ref_type = data_store.compact_ref(*ref_type);
819                        // Same local-shadowing rule as the AST branch above.
820                        if let crate::engine::arena::CompactRefType::NamedRange(name_id) = ref_type
821                            && let Some(local) =
822                                self.local_named_reference(data_store.resolve_ast_string(*name_id))
823                        {
824                            return local;
825                        }
826                        let reference = data_store
827                            .reconstruct_reference_type_for_eval(ref_type, sheet_registry);
828                        self.interp.reference_for_current_offset(&reference)
829                    }
830                    crate::engine::arena::AstNodeData::Function { .. }
831                    | crate::engine::arena::AstNodeData::BinaryOp { .. } => self
832                        .interp
833                        .evaluate_arena_ast_as_reference(*id, data_store, sheet_registry),
834                    crate::engine::arena::AstNodeData::UnaryOp { operator, expr_id }
835                        if *operator == formualizer_parse::UnaryOperator::Spill =>
836                    {
837                        self.interp
838                            .arena_spill_reference(*expr_id, data_store, sheet_registry)
839                    }
840                    _ => Err(ExcelError::new(ExcelErrorKind::Ref)
841                        .with_message("Expected a reference (by-ref argument)")),
842                }
843            }
844        }
845    }
846
847    fn function_resolution_attempt(
848        &self,
849    ) -> Option<Result<crate::function::FunctionResolution<'b>, ExcelError>> {
850        match &self.expr {
851            ArgumentExpr::Ast(node) => {
852                let ASTNodeType::Function { name, args } = &node.node_type else {
853                    return None;
854                };
855                if !self
856                    .interp
857                    .ast_function_capabilities(name)
858                    .is_some_and(|caps| caps.contains(crate::function::FnCaps::RETURNS_REFERENCE))
859                {
860                    return None;
861                }
862                let fun = match self.interp.context.get_function("", name) {
863                    Some(fun) => fun,
864                    None => {
865                        return Some(Err(ExcelError::new(ExcelErrorKind::Name)
866                            .with_message(format!("Unknown function: {name}"))));
867                    }
868                };
869                let handles: Vec<_> = args
870                    .iter()
871                    .map(|arg| ArgumentHandle::new(arg, self.interp))
872                    .collect();
873                let ctx = DefaultFunctionContext::new_with_sheet(
874                    self.interp.context,
875                    None,
876                    self.interp.current_sheet(),
877                );
878                Some(fun.resolve_reference_or_value(&handles, &ctx, &|| self.value()))
879            }
880            ArgumentExpr::Arena {
881                id,
882                data_store,
883                sheet_registry,
884            } => {
885                let node = match data_store.get_node(*id) {
886                    Some(node) => node,
887                    None => {
888                        return Some(Err(
889                            ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
890                        ));
891                    }
892                };
893                let crate::engine::arena::AstNodeData::Function { name_id, .. } = node else {
894                    return None;
895                };
896                let name = data_store.resolve_ast_string(*name_id);
897                if !self
898                    .interp
899                    .ast_function_capabilities(name)
900                    .is_some_and(|caps| caps.contains(crate::function::FnCaps::RETURNS_REFERENCE))
901                {
902                    return None;
903                }
904                let fun = match self.interp.context.get_function("", name) {
905                    Some(fun) => fun,
906                    None => {
907                        return Some(Err(ExcelError::new(ExcelErrorKind::Name)
908                            .with_message(format!("Unknown function: {name}"))));
909                    }
910                };
911                let args = match data_store.get_args(*id) {
912                    Some(args) => args,
913                    None => {
914                        return Some(Err(ExcelError::new(ExcelErrorKind::Value)
915                            .with_message("Missing function args")));
916                    }
917                };
918                let handles: Vec<_> = args
919                    .iter()
920                    .copied()
921                    .map(|arg_id| {
922                        ArgumentHandle::new_arena(arg_id, self.interp, data_store, sheet_registry)
923                    })
924                    .collect();
925                let ctx = DefaultFunctionContext::new_with_sheet(
926                    self.interp.context,
927                    None,
928                    self.interp.current_sheet(),
929                );
930                Some(fun.resolve_reference_or_value(&handles, &ctx, &|| self.value()))
931            }
932        }
933    }
934
935    fn reference_attempt(&self) -> Option<Result<ReferenceType, ExcelError>> {
936        match &self.expr {
937            ArgumentExpr::Ast(node) => match &node.node_type {
938                ASTNodeType::Reference { reference, .. } => {
939                    // A LET/LAMBDA local shadows any workbook name of the same
940                    // spelling, so it never takes the named-range route. A
941                    // local bound to a range is that range; any other local
942                    // resolves on the value path.
943                    if let ReferenceType::NamedRange(name) = reference
944                        && self.interp.resolve_local_name(name).is_some()
945                    {
946                        return self.interp.resolve_local_bound_reference(name).map(Ok);
947                    }
948                    Some(self.interp.reference_for_current_offset(reference))
949                }
950                ASTNodeType::BinaryOp { op, .. }
951                    if *op == formualizer_parse::BinaryOperator::Range =>
952                {
953                    Some(self.interp.evaluate_ast_as_reference(node))
954                }
955                ASTNodeType::UnaryOp { op, expr }
956                    if *op == formualizer_parse::UnaryOperator::Spill =>
957                {
958                    Some(self.interp.ast_spill_reference(expr))
959                }
960                ASTNodeType::Function { name, .. }
961                    if self
962                        .interp
963                        .ast_function_capabilities(name)
964                        .is_some_and(|caps| {
965                            caps.contains(crate::function::FnCaps::RETURNS_REFERENCE)
966                        }) =>
967                {
968                    self.interp.try_evaluate_ast_as_reference(node)
969                }
970                _ => None,
971            },
972            ArgumentExpr::Arena {
973                id,
974                data_store,
975                sheet_registry,
976            } => {
977                let node = match data_store.get_node(*id) {
978                    Some(node) => node,
979                    None => {
980                        return Some(Err(
981                            ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
982                        ));
983                    }
984                };
985                match node {
986                    crate::engine::arena::AstNodeData::Reference { ref_type, .. } => {
987                        let ref_type = data_store.compact_ref(*ref_type);
988                        // Same local-shadowing rule as the AST branch above.
989                        if let crate::engine::arena::CompactRefType::NamedRange(name_id) = ref_type
990                        {
991                            let name = data_store.resolve_ast_string(*name_id);
992                            if self.interp.resolve_local_name(name).is_some() {
993                                return self.interp.resolve_local_bound_reference(name).map(Ok);
994                            }
995                        }
996                        let reference = data_store
997                            .reconstruct_reference_type_for_eval(ref_type, sheet_registry);
998                        Some(self.interp.reference_for_current_offset(&reference))
999                    }
1000                    crate::engine::arena::AstNodeData::BinaryOp { operator, .. }
1001                        if *operator == formualizer_parse::BinaryOperator::Range =>
1002                    {
1003                        Some(self.interp.evaluate_arena_ast_as_reference(
1004                            *id,
1005                            data_store,
1006                            sheet_registry,
1007                        ))
1008                    }
1009                    crate::engine::arena::AstNodeData::UnaryOp { operator, expr_id }
1010                        if *operator == formualizer_parse::UnaryOperator::Spill =>
1011                    {
1012                        Some(self.interp.arena_spill_reference(
1013                            *expr_id,
1014                            data_store,
1015                            sheet_registry,
1016                        ))
1017                    }
1018                    crate::engine::arena::AstNodeData::Function { name_id, .. } => {
1019                        if self
1020                            .interp
1021                            .arena_function_capabilities(*name_id, data_store)
1022                            .is_some_and(|caps| {
1023                                caps.contains(crate::function::FnCaps::RETURNS_REFERENCE)
1024                            })
1025                        {
1026                            self.interp.try_evaluate_arena_ast_as_reference(
1027                                *id,
1028                                data_store,
1029                                sheet_registry,
1030                            )
1031                        } else {
1032                            None
1033                        }
1034                    }
1035                    _ => None,
1036                }
1037            }
1038        }
1039    }
1040
1041    pub(crate) fn resolve_reference_or_value(
1042        &self,
1043    ) -> Result<crate::function::FunctionResolution<'b>, ExcelError> {
1044        let resolved = self
1045            .caches()
1046            .cached_reference_or_value
1047            .get_or_init(|| self.compute_reference_or_value())
1048            .clone();
1049        if let Ok(crate::function::FunctionResolution::Value(value)) = &resolved {
1050            let _ = self.cached_value.set(Ok(MemoizedValue::new(value.clone())));
1051        }
1052        resolved
1053    }
1054
1055    fn compute_reference_or_value(
1056        &self,
1057    ) -> Result<crate::function::FunctionResolution<'b>, ExcelError> {
1058        if let Some(result) = self.function_resolution_attempt() {
1059            return result;
1060        }
1061        if let Some(reference) = self.reference_attempt() {
1062            return Ok(match reference {
1063                Ok(reference) => crate::function::FunctionResolution::Reference(reference),
1064                Err(error) => crate::function::FunctionResolution::ReferenceError(error),
1065            });
1066        }
1067        self.value().map(crate::function::FunctionResolution::Value)
1068    }
1069
1070    /// Resolve this argument once without using a failed range conversion as type dispatch.
1071    ///
1072    /// Direct references and the `:` operator take the reference path. Functions
1073    /// with `RETURNS_REFERENCE` first attempt reference evaluation, but fall back
1074    /// to their cached value when `eval_reference` returns `None`.
1075    pub(crate) fn resolve_once(&self) -> Result<ResolvedArgument<'b>, ExcelError> {
1076        self.caches()
1077            .cached_resolved
1078            .get_or_init(|| self.compute_resolved_argument())
1079            .clone()
1080    }
1081
1082    /// Attaches the context's cancellation token and materialisation budget.
1083    pub(crate) fn with_context_cancel_token(&self, view: RangeView<'b>) -> RangeView<'b> {
1084        let view = view.with_cell_budget(self.interp.context.materialized_array_cells());
1085        match self.interp.context.cancellation_token() {
1086            Some(token) => view.with_cancel_token(Some(token)),
1087            None => view,
1088        }
1089    }
1090
1091    fn compute_resolved_argument(&self) -> Result<ResolvedArgument<'b>, ExcelError> {
1092        if let ArgumentExpr::Arena { id, data_store, .. } = &self.expr
1093            && let Some(result) = self.interp.resolve_arena_argument_range(*id, data_store)
1094        {
1095            return match result {
1096                Ok(view) => Ok(ResolvedArgument::Range(
1097                    self.with_context_cancel_token(view),
1098                )),
1099                Err(error) if error.kind == ExcelErrorKind::Cancelled => Err(error),
1100                Err(error) => Ok(ResolvedArgument::ReferenceError(error)),
1101            };
1102        }
1103        let value = match self.resolve_reference_or_value()? {
1104            crate::function::FunctionResolution::Reference(reference) => {
1105                return match self
1106                    .interp
1107                    .context
1108                    .resolve_range_view(&reference, self.interp.current_sheet())
1109                {
1110                    Ok(view) => Ok(ResolvedArgument::Range(
1111                        self.with_context_cancel_token(view),
1112                    )),
1113                    Err(error) if error.kind == ExcelErrorKind::Cancelled => Err(error),
1114                    Err(error) => Ok(ResolvedArgument::ReferenceError(error)),
1115                };
1116            }
1117            crate::function::FunctionResolution::ReferenceError(error)
1118                if error.kind == ExcelErrorKind::Cancelled =>
1119            {
1120                return Err(error);
1121            }
1122            crate::function::FunctionResolution::ReferenceError(error) => {
1123                return Ok(ResolvedArgument::ReferenceError(error));
1124            }
1125            crate::function::FunctionResolution::Value(value) => value,
1126        };
1127
1128        match value {
1129            CalcValue::Range(view) => Ok(ResolvedArgument::Range(
1130                self.with_context_cancel_token(view),
1131            )),
1132            CalcValue::Scalar(LiteralValue::Array(rows)) => {
1133                let view = RangeView::try_from_owned_rows(
1134                    rows,
1135                    self.interp.context.date_system(),
1136                    self.interp.context.cancellation_token(),
1137                )?;
1138                Ok(ResolvedArgument::Range(view))
1139            }
1140            other => Ok(ResolvedArgument::Value(other)),
1141        }
1142    }
1143
1144    /// `#NUM!` when a `rows x cols` copy exceeds the context's materialisation
1145    /// budget.
1146    fn check_materialized_cells(&self, rows: usize, cols: usize) -> Result<(), ExcelError> {
1147        match crate::engine::resource_ledger::materialized_cells_exceed(
1148            rows,
1149            cols,
1150            self.interp.context.materialized_array_cells(),
1151        ) {
1152            Some(error) => Err(error),
1153            None => Ok(()),
1154        }
1155    }
1156
1157    pub fn range(&self) -> Result<Box<dyn Range>, ExcelError> {
1158        match &self.expr {
1159            ArgumentExpr::Ast(node) => match &node.node_type {
1160                ASTNodeType::Reference { reference, .. } => {
1161                    // Prefer RangeView since it has explicit current-sheet context.
1162                    let reference = self.interp.reference_for_current_offset(reference)?;
1163                    let view = self
1164                        .interp
1165                        .context
1166                        .resolve_range_view(&reference, self.interp.current_sheet())?;
1167                    let (rows, cols) = view.dims();
1168                    self.check_materialized_cells(rows, cols)?;
1169                    let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows);
1170                    view.for_each_row(&mut |row| {
1171                        let row_data: Vec<LiteralValue> = (0..cols)
1172                            .map(|c| row.get(c).cloned().unwrap_or(LiteralValue::Empty))
1173                            .collect();
1174                        out.push(row_data);
1175                        Ok(())
1176                    })?;
1177                    Ok(Box::new(InMemoryRange::new(out)))
1178                }
1179                ASTNodeType::Function { .. } | ASTNodeType::BinaryOp { .. } => {
1180                    let reference = self.reference_for_eval()?;
1181                    let view = self
1182                        .interp
1183                        .context
1184                        .resolve_range_view(&reference, self.interp.current_sheet())?;
1185                    let (rows, cols) = view.dims();
1186                    self.check_materialized_cells(rows, cols)?;
1187                    let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows);
1188                    view.for_each_row(&mut |row| {
1189                        let row_data: Vec<LiteralValue> = (0..cols)
1190                            .map(|c| row.get(c).cloned().unwrap_or(LiteralValue::Empty))
1191                            .collect();
1192                        out.push(row_data);
1193                        Ok(())
1194                    })?;
1195                    Ok(Box::new(InMemoryRange::new(out)))
1196                }
1197                ASTNodeType::Array(rows) => {
1198                    let mut materialized = Vec::new();
1199                    for row in rows {
1200                        let mut materialized_row = Vec::new();
1201                        for cell in row {
1202                            materialized_row.push(self.interp.evaluate_ast(cell)?.into_literal());
1203                        }
1204                        materialized.push(materialized_row);
1205                    }
1206                    Ok(Box::new(InMemoryRange::new(materialized)))
1207                }
1208                _ => Err(ExcelError::new(ExcelErrorKind::Ref)
1209                    .with_message(format!("Expected a range, got {:?}", node.node_type))),
1210            },
1211            ArgumentExpr::Arena { id, data_store, .. } => {
1212                let node = data_store.get_node(*id).ok_or_else(|| {
1213                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
1214                })?;
1215
1216                match node {
1217                    crate::engine::arena::AstNodeData::Reference { .. }
1218                    | crate::engine::arena::AstNodeData::Function { .. }
1219                    | crate::engine::arena::AstNodeData::BinaryOp { .. } => {
1220                        let reference = self.reference_for_eval()?;
1221                        let view = self
1222                            .interp
1223                            .context
1224                            .resolve_range_view(&reference, self.interp.current_sheet())?;
1225                        let (rows, cols) = view.dims();
1226                        self.check_materialized_cells(rows, cols)?;
1227                        let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows);
1228                        view.for_each_row(&mut |row| {
1229                            let row_data: Vec<LiteralValue> = (0..cols)
1230                                .map(|c| row.get(c).cloned().unwrap_or(LiteralValue::Empty))
1231                                .collect();
1232                            out.push(row_data);
1233                            Ok(())
1234                        })?;
1235                        Ok(Box::new(InMemoryRange::new(out)))
1236                    }
1237                    crate::engine::arena::AstNodeData::Array { .. } => {
1238                        let (rows, cols, elements) =
1239                            data_store.get_array_elems(*id).ok_or_else(|| {
1240                                ExcelError::new(ExcelErrorKind::Value).with_message("Invalid array")
1241                            })?;
1242                        let rows_usize = rows as usize;
1243                        let cols_usize = cols as usize;
1244                        let mut materialized: Vec<Vec<LiteralValue>> =
1245                            Vec::with_capacity(rows_usize);
1246                        for r in 0..rows_usize {
1247                            let mut row = Vec::with_capacity(cols_usize);
1248                            for c in 0..cols_usize {
1249                                let idx = r * cols_usize + c;
1250                                let elem_id = elements.get(idx).copied().ok_or_else(|| {
1251                                    ExcelError::new(ExcelErrorKind::Value)
1252                                        .with_message("Invalid array")
1253                                })?;
1254                                let v = self.interp.evaluate_arena_ast(
1255                                    elem_id,
1256                                    data_store,
1257                                    self.sheet_registry(),
1258                                )?;
1259                                row.push(v.into_literal());
1260                            }
1261                            materialized.push(row);
1262                        }
1263                        Ok(Box::new(InMemoryRange::new(materialized)))
1264                    }
1265                    _ => Err(ExcelError::new(ExcelErrorKind::Ref)
1266                        .with_message("Argument cannot be interpreted as a range.")),
1267                }
1268            }
1269        }
1270    }
1271
1272    fn sheet_registry(&self) -> &crate::engine::sheet_registry::SheetRegistry {
1273        match &self.expr {
1274            ArgumentExpr::Ast(_) => {
1275                // Not needed; used only in arena flows.
1276                unreachable!("sheet_registry only used for arena ArgumentHandle")
1277            }
1278            ArgumentExpr::Arena { sheet_registry, .. } => sheet_registry,
1279        }
1280    }
1281
1282    /// Resolve this argument to a [`RangeView`].
1283    ///
1284    /// Delegates to [`Self::resolve_once`] so reference-shaped and computed
1285    /// arguments share one cached resolution path. A reference keeps its lazy
1286    /// view, while a computed argument (`B1:B3="x"`, `SEQUENCE(3)`, `{1,2}`)
1287    /// resolves through the same single evaluation the rest of argument
1288    /// preparation uses instead of being rejected for not being a reference.
1289    pub fn range_view(&self) -> Result<RangeView<'b>, ExcelError> {
1290        match self.resolve_once()? {
1291            ResolvedArgument::Range(view) => Ok(view),
1292            // A genuine reference failure (`OFFSET(A1,-1,0)`) stays an error
1293            // rather than being masked by re-evaluating the node as a value.
1294            ResolvedArgument::ReferenceError(error) => Err(error),
1295            // `resolve_once` already folds range-shaped and array values into
1296            // `Range`, so these two arms are defensive. They still apply the
1297            // cancellation token so the invariant changing could never silently
1298            // drop cancellation on this path.
1299            ResolvedArgument::Value(CalcValue::Range(view)) => {
1300                Ok(self.with_context_cancel_token(view))
1301            }
1302            ResolvedArgument::Value(CalcValue::Scalar(LiteralValue::Array(rows))) => {
1303                RangeView::try_from_owned_rows(
1304                    rows,
1305                    self.interp.context.date_system(),
1306                    self.interp.context.cancellation_token(),
1307                )
1308            }
1309            ResolvedArgument::Value(_) => Err(ExcelError::new(ExcelErrorKind::Ref)
1310                .with_message("Argument cannot be interpreted as a range.")),
1311        }
1312    }
1313
1314    /// Resolve this argument to a [`RangeView`], promoting a scalar to a 1x1 view.
1315    ///
1316    /// Excel treats a scalar handed to a range-consuming function as a 1x1 array,
1317    /// so `=TRANSPOSE(2)` is `2` rather than an error. [`Self::range_view`] rejects
1318    /// scalars, and a function that wants the Excel behaviour opts in here.
1319    ///
1320    /// This is deliberately *not* the behaviour of `range_view` itself. Several
1321    /// builtins use a `range_view` failure as type dispatch, where "scalar" and
1322    /// "1x1 range" mean genuinely different things -- `MEDIAN(TRUE)` is `1`
1323    /// because a direct scalar is coerced while a range cell of the same type is
1324    /// skipped, and a D-function's scalar criteria argument is an error rather
1325    /// than an empty criteria block that matches every row. Promoting inside
1326    /// `range_view` would silently change those answers. The rule for choosing
1327    /// between the two: a function that distinguishes a scalar argument from a
1328    /// 1x1 range keeps `range_view`.
1329    ///
1330    /// An error scalar propagates as an error rather than becoming a 1x1 view
1331    /// containing it, so `=TRANSPOSE(NA())` is `#N/A` instead of being masked as
1332    /// `#REF!`.
1333    pub fn range_view_or_scalar(&self) -> Result<RangeView<'b>, ExcelError> {
1334        match self.resolve_once()? {
1335            ResolvedArgument::Range(view) => Ok(view),
1336            ResolvedArgument::ReferenceError(error) => Err(error),
1337            ResolvedArgument::Value(CalcValue::Range(view)) => {
1338                Ok(self.with_context_cancel_token(view))
1339            }
1340            ResolvedArgument::Value(CalcValue::Scalar(LiteralValue::Array(rows))) => {
1341                RangeView::try_from_owned_rows(
1342                    rows,
1343                    self.interp.context.date_system(),
1344                    self.interp.context.cancellation_token(),
1345                )
1346            }
1347            // Preserve the argument's own error instead of reporting the shape
1348            // mismatch that rejecting it would produce.
1349            ResolvedArgument::Value(CalcValue::Scalar(LiteralValue::Error(error))) => Err(error),
1350            ResolvedArgument::Value(
1351                CalcValue::Scalar(scalar) | CalcValue::AnnotatedScalar(scalar, _),
1352            ) => RangeView::try_from_owned_rows(
1353                vec![vec![scalar]],
1354                self.interp.context.date_system(),
1355                self.interp.context.cancellation_token(),
1356            ),
1357            // A lambda is not a value that can stand in for a 1x1 array.
1358            ResolvedArgument::Value(CalcValue::Callable(_)) => {
1359                Err(ExcelError::new(ExcelErrorKind::Ref)
1360                    .with_message("Argument cannot be interpreted as a range."))
1361            }
1362        }
1363    }
1364
1365    pub fn value_or_range(&self) -> Result<EvaluatedArg<'_>, ExcelError> {
1366        self.range().map(EvaluatedArg::Range).or_else(|_| {
1367            self.value()
1368                .map(|cv| EvaluatedArg::LiteralValue(Cow::Owned(cv.into_literal())))
1369        })
1370    }
1371
1372    /// Lazily iterate values for this argument in row-major expansion order.
1373    /// - Reference: stream via RangeView (row-major)
1374    /// - Array literal: evaluate each element lazily per cell
1375    /// - Scalar/other expressions: a single value
1376    pub fn lazy_values_owned(
1377        &'a self,
1378    ) -> Result<Box<dyn Iterator<Item = LiteralValue> + 'a>, ExcelError> {
1379        // Reference-shaped syntax usually names a range, but a bare LET/LAMBDA
1380        // local has the same syntax and may be bound to a scalar. Resolve once
1381        // and stream whichever shape came back instead of insisting on a range.
1382        fn resolved_values<'a, 'b>(
1383            handle: &'a ArgumentHandle<'a, 'b>,
1384        ) -> Result<Box<dyn Iterator<Item = LiteralValue> + 'a>, ExcelError> {
1385            match handle.resolve_once()? {
1386                ResolvedArgument::Range(view) => {
1387                    let mut values = Vec::new();
1388                    view.for_each_cell(&mut |value| {
1389                        values.push(value.clone());
1390                        Ok(())
1391                    })?;
1392                    Ok(Box::new(values.into_iter()))
1393                }
1394                ResolvedArgument::ReferenceError(error) => Err(error),
1395                ResolvedArgument::Value(value) => {
1396                    Ok(Box::new(std::iter::once(value.into_literal())))
1397                }
1398            }
1399        }
1400
1401        match &self.expr {
1402            ArgumentExpr::Ast(node) => match &node.node_type {
1403                ASTNodeType::Reference { .. } => resolved_values(self),
1404                ASTNodeType::Array(rows) => {
1405                    struct ArrayEvalIter<'a, 'b> {
1406                        rows: &'a [Vec<ASTNode>],
1407                        r: usize,
1408                        c: usize,
1409                        interp: &'a Interpreter<'b>,
1410                    }
1411                    impl<'a, 'b> Iterator for ArrayEvalIter<'a, 'b> {
1412                        type Item = LiteralValue;
1413                        fn next(&mut self) -> Option<Self::Item> {
1414                            if self.rows.is_empty() {
1415                                return None;
1416                            }
1417                            let rows = self.rows;
1418                            let mut r = self.r;
1419                            let mut c = self.c;
1420                            if r >= rows.len() {
1421                                return None;
1422                            }
1423                            let node = &rows[r][c];
1424                            // advance indices
1425                            c += 1;
1426                            if c >= rows[r].len() {
1427                                r += 1;
1428                                c = 0;
1429                            }
1430                            self.r = r;
1431                            self.c = c;
1432                            match self.interp.evaluate_ast(node) {
1433                                Ok(cv) => Some(cv.into_literal()),
1434                                Err(e) => Some(LiteralValue::Error(e)),
1435                            }
1436                        }
1437                    }
1438                    let it = ArrayEvalIter {
1439                        rows,
1440                        r: 0,
1441                        c: 0,
1442                        interp: self.interp,
1443                    };
1444                    Ok(Box::new(it))
1445                }
1446                _ => {
1447                    // Single value expression
1448                    let v = self.value()?.into_literal();
1449                    Ok(Box::new(std::iter::once(v)))
1450                }
1451            },
1452            ArgumentExpr::Arena {
1453                id,
1454                data_store,
1455                sheet_registry,
1456            } => {
1457                let node = data_store.get_node(*id).ok_or_else(|| {
1458                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
1459                })?;
1460
1461                match node {
1462                    crate::engine::arena::AstNodeData::Reference { .. } => resolved_values(self),
1463                    crate::engine::arena::AstNodeData::Array { .. } => {
1464                        let (rows, cols, elements) =
1465                            data_store.get_array_elems(*id).ok_or_else(|| {
1466                                ExcelError::new(ExcelErrorKind::Value).with_message("Invalid array")
1467                            })?;
1468
1469                        struct ArenaArrayEvalIter<'a, 'b> {
1470                            elements: &'a [crate::engine::arena::AstNodeId],
1471                            idx: usize,
1472                            interp: &'a Interpreter<'b>,
1473                            data_store: &'a crate::engine::arena::DataStore,
1474                            sheet_registry: &'a crate::engine::sheet_registry::SheetRegistry,
1475                        }
1476
1477                        impl<'a, 'b> Iterator for ArenaArrayEvalIter<'a, 'b> {
1478                            type Item = LiteralValue;
1479
1480                            fn next(&mut self) -> Option<Self::Item> {
1481                                let id = self.elements.get(self.idx).copied()?;
1482                                self.idx += 1;
1483                                match self.interp.evaluate_arena_ast(
1484                                    id,
1485                                    self.data_store,
1486                                    self.sheet_registry,
1487                                ) {
1488                                    Ok(cv) => Some(cv.into_literal()),
1489                                    Err(e) => Some(LiteralValue::Error(e)),
1490                                }
1491                            }
1492                        }
1493
1494                        let _ = (rows, cols);
1495                        let it = ArenaArrayEvalIter {
1496                            elements,
1497                            idx: 0,
1498                            interp: self.interp,
1499                            data_store,
1500                            sheet_registry,
1501                        };
1502                        Ok(Box::new(it))
1503                    }
1504                    _ => {
1505                        let v = self
1506                            .interp
1507                            .evaluate_arena_ast(*id, data_store, sheet_registry)?;
1508                        Ok(Box::new(std::iter::once(v.into_literal())))
1509                    }
1510                }
1511            }
1512        }
1513    }
1514
1515    pub fn ast(&self) -> &ASTNode {
1516        match &self.expr {
1517            ArgumentExpr::Ast(node) => node,
1518            ArgumentExpr::Arena {
1519                id,
1520                data_store,
1521                sheet_registry,
1522            } => self.caches().cached_ast.get_or_init(|| {
1523                data_store
1524                    .retrieve_ast(*id, sheet_registry)
1525                    .unwrap_or_else(|| ASTNode {
1526                        node_type: ASTNodeType::Literal(LiteralValue::Error(
1527                            ExcelError::new(ExcelErrorKind::Value)
1528                                .with_message("Missing formula AST"),
1529                        )),
1530                        source_token: None,
1531                        contains_volatile: false,
1532                    })
1533            }),
1534        }
1535    }
1536
1537    /// Returns the raw reference from the AST when this argument is a reference.
1538    /// This does not evaluate the reference or materialize values.
1539    pub fn as_reference(&self) -> Result<&ReferenceType, ExcelError> {
1540        match &self.expr {
1541            ArgumentExpr::Ast(node) => match &node.node_type {
1542                ASTNodeType::Reference { reference, .. } => {
1543                    // Same local-shadowing rule as `reference_for_eval`.
1544                    if let ReferenceType::NamedRange(name) = reference
1545                        && let Some(local) = self.local_named_reference(name)
1546                    {
1547                        let bound = local?;
1548                        return Ok(self.caches().cached_ref.get_or_init(|| bound));
1549                    }
1550                    Ok(reference)
1551                }
1552                _ => Err(ExcelError::new(ExcelErrorKind::Ref)
1553                    .with_message("Expected a reference (by-ref argument)")),
1554            },
1555            ArgumentExpr::Arena { .. } => {
1556                let reference = self.reference_for_eval()?;
1557                Ok(self.caches().cached_ref.get_or_init(|| reference))
1558            }
1559        }
1560    }
1561
1562    /// Returns a `ReferenceType` if this argument is a reference or a function that
1563    /// can yield a reference via `eval_reference`. Materializes no values.
1564    pub fn as_reference_or_eval(&self) -> Result<ReferenceType, ExcelError> {
1565        match &self.expr {
1566            ArgumentExpr::Ast(node) => match &node.node_type {
1567                ASTNodeType::Reference { reference, .. } => {
1568                    // Same local-shadowing rule as `reference_for_eval`.
1569                    if let ReferenceType::NamedRange(name) = reference
1570                        && let Some(local) = self.local_named_reference(name)
1571                    {
1572                        return local;
1573                    }
1574                    self.interp.reference_for_current_offset(reference)
1575                }
1576                ASTNodeType::Function { .. } | ASTNodeType::BinaryOp { .. } => {
1577                    self.interp.evaluate_ast_as_reference(node)
1578                }
1579                ASTNodeType::UnaryOp { op, expr }
1580                    if *op == formualizer_parse::UnaryOperator::Spill =>
1581                {
1582                    self.interp.ast_spill_reference(expr)
1583                }
1584                _ => Err(ExcelError::new(ExcelErrorKind::Ref)
1585                    .with_message("Argument is not a reference")),
1586            },
1587            ArgumentExpr::Arena {
1588                id,
1589                data_store,
1590                sheet_registry,
1591            } => {
1592                let node = data_store.get_node(*id).ok_or_else(|| {
1593                    ExcelError::new(ExcelErrorKind::Value).with_message("Missing AST node")
1594                })?;
1595
1596                match node {
1597                    crate::engine::arena::AstNodeData::Reference { .. } => {
1598                        self.reference_for_eval()
1599                    }
1600                    crate::engine::arena::AstNodeData::Function { .. }
1601                    | crate::engine::arena::AstNodeData::BinaryOp { .. } => self
1602                        .interp
1603                        .evaluate_arena_ast_as_reference(*id, data_store, sheet_registry),
1604                    crate::engine::arena::AstNodeData::UnaryOp { operator, expr_id }
1605                        if *operator == formualizer_parse::UnaryOperator::Spill =>
1606                    {
1607                        self.interp
1608                            .arena_spill_reference(*expr_id, data_store, sheet_registry)
1609                    }
1610                    _ => Err(ExcelError::new(ExcelErrorKind::Ref)
1611                        .with_message("Argument is not a reference")),
1612                }
1613            }
1614        }
1615    }
1616
1617    /* tiny validator helper for macro */
1618    pub fn matches_kind(&self, k: formualizer_common::ArgKind) -> Result<bool, ExcelError> {
1619        Ok(match k {
1620            formualizer_common::ArgKind::Any => true,
1621            formualizer_common::ArgKind::Range => self.range().is_ok(),
1622            formualizer_common::ArgKind::Number => matches!(
1623                self.value()?.into_literal(),
1624                LiteralValue::Number(_) | LiteralValue::Int(_)
1625            ),
1626            formualizer_common::ArgKind::Text => {
1627                matches!(self.value()?.into_literal(), LiteralValue::Text(_))
1628            }
1629            formualizer_common::ArgKind::Logical => {
1630                matches!(self.value()?.into_literal(), LiteralValue::Boolean(_))
1631            }
1632        })
1633    }
1634}
1635
1636/* simple Vec-backed range */
1637#[derive(Debug, Clone)]
1638pub struct InMemoryRange {
1639    data: Vec<Vec<LiteralValue>>,
1640}
1641impl InMemoryRange {
1642    pub fn new(d: Vec<Vec<LiteralValue>>) -> Self {
1643        Self { data: d }
1644    }
1645}
1646impl Range for InMemoryRange {
1647    fn get(&self, r: usize, c: usize) -> Result<LiteralValue, ExcelError> {
1648        Ok(self
1649            .data
1650            .get(r)
1651            .and_then(|row| row.get(c))
1652            .cloned()
1653            .unwrap_or(LiteralValue::Empty))
1654    }
1655    fn dimensions(&self) -> (usize, usize) {
1656        (self.data.len(), self.data.first().map_or(0, |r| r.len()))
1657    }
1658    fn as_any(&self) -> &dyn Any {
1659        self
1660    }
1661}
1662
1663/* ───────────────────────── Table abstraction ───────────────────────── */
1664
1665pub trait Table: Debug + Send + Sync {
1666    fn get_cell(&self, row: usize, column: &str) -> Result<LiteralValue, ExcelError>;
1667    fn get_column(&self, column: &str) -> Result<Box<dyn Range>, ExcelError>;
1668    /// Ordered list of column names
1669    fn columns(&self) -> Vec<String> {
1670        vec![]
1671    }
1672    /// Number of data rows (excluding headers/totals)
1673    fn data_height(&self) -> usize {
1674        0
1675    }
1676    /// Whether the table has a header row
1677    fn has_headers(&self) -> bool {
1678        false
1679    }
1680    /// Whether the table has a totals row
1681    fn has_totals(&self) -> bool {
1682        false
1683    }
1684    /// Headers row as a 1xW range
1685    fn headers_row(&self) -> Option<Box<dyn Range>> {
1686        None
1687    }
1688    /// Totals row as a 1xW range, if present
1689    fn totals_row(&self) -> Option<Box<dyn Range>> {
1690        None
1691    }
1692    /// Entire data body as HxW range
1693    fn data_body(&self) -> Option<Box<dyn Range>> {
1694        None
1695    }
1696    fn clone_box(&self) -> Box<dyn Table>;
1697}
1698impl Table for Box<dyn Table> {
1699    fn get_cell(&self, r: usize, c: &str) -> Result<LiteralValue, ExcelError> {
1700        (**self).get_cell(r, c)
1701    }
1702    fn get_column(&self, c: &str) -> Result<Box<dyn Range>, ExcelError> {
1703        (**self).get_column(c)
1704    }
1705    fn columns(&self) -> Vec<String> {
1706        (**self).columns()
1707    }
1708    fn data_height(&self) -> usize {
1709        (**self).data_height()
1710    }
1711    fn has_headers(&self) -> bool {
1712        (**self).has_headers()
1713    }
1714    fn has_totals(&self) -> bool {
1715        (**self).has_totals()
1716    }
1717    fn headers_row(&self) -> Option<Box<dyn Range>> {
1718        (**self).headers_row()
1719    }
1720    fn totals_row(&self) -> Option<Box<dyn Range>> {
1721        (**self).totals_row()
1722    }
1723    fn data_body(&self) -> Option<Box<dyn Range>> {
1724        (**self).data_body()
1725    }
1726    fn clone_box(&self) -> Box<dyn Table> {
1727        (**self).clone_box()
1728    }
1729}
1730
1731/* ─────────────────────── Resolver super-trait ─────────────────────── */
1732
1733pub trait ReferenceResolver: Send + Sync {
1734    fn resolve_cell_reference(
1735        &self,
1736        sheet: Option<&str>,
1737        row: u32,
1738        col: u32,
1739    ) -> Result<LiteralValue, ExcelError>;
1740}
1741pub trait RangeResolver: Send + Sync {
1742    fn resolve_range_reference(
1743        &self,
1744        sheet: Option<&str>,
1745        sr: Option<u32>,
1746        sc: Option<u32>,
1747        er: Option<u32>,
1748        ec: Option<u32>,
1749    ) -> Result<Box<dyn Range>, ExcelError>;
1750}
1751pub trait NamedRangeResolver: Send + Sync {
1752    fn resolve_named_range_reference(
1753        &self,
1754        name: &str,
1755    ) -> Result<Vec<Vec<LiteralValue>>, ExcelError>;
1756}
1757pub trait TableResolver: Send + Sync {
1758    fn resolve_table_reference(
1759        &self,
1760        tref: &formualizer_parse::parser::TableReference,
1761    ) -> Result<Box<dyn Table>, ExcelError>;
1762}
1763
1764pub trait SourceResolver: Send + Sync {
1765    fn source_scalar_version(&self, _name: &str) -> Option<u64> {
1766        None
1767    }
1768
1769    fn resolve_source_scalar(&self, name: &str) -> Result<LiteralValue, ExcelError> {
1770        Err(ExcelError::new(ExcelErrorKind::NImpl)
1771            .with_message(format!("Source scalar not supported: {name}")))
1772    }
1773
1774    fn source_table_version(&self, _name: &str) -> Option<u64> {
1775        None
1776    }
1777
1778    fn resolve_source_table(&self, name: &str) -> Result<Box<dyn Table>, ExcelError> {
1779        Err(ExcelError::new(ExcelErrorKind::NImpl)
1780            .with_message(format!("Source table not supported: {name}")))
1781    }
1782}
1783
1784pub trait Resolver: ReferenceResolver + RangeResolver + NamedRangeResolver + TableResolver {
1785    fn resolve_range_like(&self, r: &ReferenceType) -> Result<Box<dyn Range>, ExcelError> {
1786        match r {
1787            ReferenceType::Range {
1788                sheet,
1789                start_row,
1790                start_col,
1791                end_row,
1792                end_col,
1793                ..
1794            } => self.resolve_range_reference(
1795                sheet.as_deref(),
1796                *start_row,
1797                *start_col,
1798                *end_row,
1799                *end_col,
1800            ),
1801            ReferenceType::External(_) => Err(ExcelError::new(ExcelErrorKind::NImpl)
1802                .with_message("External references are not supported by Resolver".to_string())),
1803            ReferenceType::Table(tref) => {
1804                let t = self.resolve_table_reference(tref)?;
1805                match &tref.specifier {
1806                    Some(TableSpecifier::Column(c)) => t.get_column(c),
1807                    Some(TableSpecifier::ColumnRange(start, end)) => {
1808                        // Build a rectangular range from start..=end columns in table order
1809                        let cols = t.columns();
1810                        let start_key = start.to_lowercase();
1811                        let end_key = end.to_lowercase();
1812                        let start_idx = cols.iter().position(|n| n.to_lowercase() == start_key);
1813                        let end_idx = cols.iter().position(|n| n.to_lowercase() == end_key);
1814                        if let (Some(mut si), Some(mut ei)) = (start_idx, end_idx) {
1815                            if si > ei {
1816                                std::mem::swap(&mut si, &mut ei);
1817                            }
1818                            // Materialize by stacking columns into a 2D array
1819                            let h = t.data_height();
1820                            let w = ei - si + 1;
1821                            let mut rows = vec![vec![LiteralValue::Empty; w]; h];
1822                            for (offset, ci) in (si..=ei).enumerate() {
1823                                let cname = &cols[ci];
1824                                let col_range = t.get_column(cname)?;
1825                                let (rh, _) = col_range.dimensions();
1826                                for (r, row) in rows.iter_mut().enumerate().take(h.min(rh)) {
1827                                    row[offset] = col_range.get(r, 0)?;
1828                                }
1829                            }
1830                            Ok(Box::new(InMemoryRange::new(rows)))
1831                        } else {
1832                            Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
1833                                "Column range refers to unknown column(s)".to_string(),
1834                            ))
1835                        }
1836                    }
1837                    Some(TableSpecifier::SpecialItem(
1838                        formualizer_parse::parser::SpecialItem::Headers,
1839                    )) => {
1840                        if let Some(h) = t.headers_row() {
1841                            Ok(h)
1842                        } else {
1843                            Ok(Box::new(InMemoryRange::new(vec![])))
1844                        }
1845                    }
1846                    Some(TableSpecifier::SpecialItem(
1847                        formualizer_parse::parser::SpecialItem::Totals,
1848                    )) => {
1849                        if let Some(tr) = t.totals_row() {
1850                            Ok(tr)
1851                        } else {
1852                            Ok(Box::new(InMemoryRange::new(vec![])))
1853                        }
1854                    }
1855                    Some(TableSpecifier::SpecialItem(
1856                        formualizer_parse::parser::SpecialItem::Data,
1857                    )) => {
1858                        if let Some(body) = t.data_body() {
1859                            Ok(body)
1860                        } else {
1861                            Ok(Box::new(InMemoryRange::new(vec![])))
1862                        }
1863                    }
1864                    Some(TableSpecifier::SpecialItem(
1865                        formualizer_parse::parser::SpecialItem::All,
1866                    )) => {
1867                        // Equivalent to TableSpecifier::All handling
1868                        let mut out: Vec<Vec<LiteralValue>> = Vec::new();
1869                        if let Some(h) = t.headers_row() {
1870                            out.extend(h.iter_rows());
1871                        }
1872                        if let Some(body) = t.data_body() {
1873                            out.extend(body.iter_rows());
1874                        }
1875                        if let Some(tr) = t.totals_row() {
1876                            out.extend(tr.iter_rows());
1877                        }
1878                        Ok(Box::new(InMemoryRange::new(out)))
1879                    }
1880                    Some(TableSpecifier::SpecialItem(
1881                        formualizer_parse::parser::SpecialItem::ThisRow,
1882                    )) => Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
1883                        "@ (This Row) requires table-aware context; not yet supported".to_string(),
1884                    )),
1885                    Some(TableSpecifier::All) => {
1886                        // Concatenate headers (if any), data, totals (if any)
1887                        let mut out: Vec<Vec<LiteralValue>> = Vec::new();
1888                        if let Some(h) = t.headers_row() {
1889                            out.extend(h.iter_rows());
1890                        }
1891                        if let Some(body) = t.data_body() {
1892                            out.extend(body.iter_rows());
1893                        }
1894                        if let Some(tr) = t.totals_row() {
1895                            out.extend(tr.iter_rows());
1896                        }
1897                        Ok(Box::new(InMemoryRange::new(out)))
1898                    }
1899                    Some(TableSpecifier::Data) => {
1900                        if let Some(body) = t.data_body() {
1901                            Ok(body)
1902                        } else {
1903                            Ok(Box::new(InMemoryRange::new(vec![])))
1904                        }
1905                    }
1906                    // Defer complex combinations and row selectors for tranche 1
1907                    Some(TableSpecifier::Combination(_)) => Err(ExcelError::new(
1908                        ExcelErrorKind::NImpl,
1909                    )
1910                    .with_message("Complex structured references not yet supported".to_string())),
1911                    Some(TableSpecifier::Row(_)) => Err(ExcelError::new(ExcelErrorKind::NImpl)
1912                        .with_message("Row selectors (@/index) not yet supported".to_string())),
1913                    Some(TableSpecifier::Headers) | Some(TableSpecifier::Totals) => {
1914                        Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
1915                            "Legacy Headers/Totals variants not used; use SpecialItem".to_string(),
1916                        ))
1917                    }
1918                    None => Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
1919                        "Table reference without specifier is unsupported".to_string(),
1920                    )),
1921                }
1922            }
1923            ReferenceType::NamedRange(n) => {
1924                let v = self.resolve_named_range_reference(n)?;
1925                Ok(Box::new(InMemoryRange::new(v)))
1926            }
1927            ReferenceType::Cell {
1928                sheet, row, col, ..
1929            } => {
1930                let v = self.resolve_cell_reference(sheet.as_deref(), *row, *col)?;
1931                Ok(Box::new(InMemoryRange::new(vec![vec![v]])))
1932            }
1933            ReferenceType::Cell3D { .. } | ReferenceType::Range3D { .. } => {
1934                Err(ExcelError::new(ExcelErrorKind::NImpl)
1935                    .with_message("3D references are not yet supported".to_string()))
1936            }
1937        }
1938    }
1939}
1940
1941/* ───────────────────── EvaluationContext = Resolver+Fns ───────────── */
1942
1943pub trait FunctionProvider: Send + Sync {
1944    fn get_function(&self, ns: &str, name: &str) -> Option<Arc<dyn Function>>;
1945
1946    #[doc(hidden)]
1947    fn get_function_for_planning(&self, _ns: &str, _name: &str) -> Option<Arc<dyn Function>> {
1948        None
1949    }
1950
1951    /// Monotonic revision for runtime function resolution and semantics used by
1952    /// compressed planning. Providers that cannot supply one fail closed.
1953    #[doc(hidden)]
1954    fn planning_semantic_revision(&self) -> Option<u64> {
1955        None
1956    }
1957
1958    #[doc(hidden)]
1959    fn function_capabilities(&self, ns: &str, name: &str) -> Option<crate::function::FnCaps> {
1960        self.get_function(ns, name).map(|function| function.caps())
1961    }
1962
1963    fn function_semantic_identity(
1964        &self,
1965        ns: &str,
1966        name: &str,
1967        arity: usize,
1968    ) -> Option<crate::function_contract::FunctionSemanticIdentity> {
1969        crate::function_registry::resolve_semantic_identity(self, ns, name, arity)
1970    }
1971}
1972
1973pub trait EvaluationContext: Resolver + FunctionProvider + SourceResolver {
1974    /// Get access to the shared thread pool for parallel evaluation
1975    /// Returns None if parallel evaluation is disabled or unavailable
1976    fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
1977        None
1978    }
1979
1980    /// Returns the optional shared cancellation handle for this evaluation.
1981    ///
1982    /// Custom context authors may return a clone: clones share the same signal
1983    /// without allocating. Consumers should retrieve the handle once before a
1984    /// hot loop and poll [`crate::engine::CancelToken::is_cancelled`]
1985    /// periodically.
1986    fn cancellation_token(&self) -> Option<crate::engine::CancelToken> {
1987        None
1988    }
1989
1990    /// Optional chunk size hint for streaming visitors.
1991    fn chunk_hint(&self) -> Option<usize> {
1992        None
1993    }
1994
1995    /// Most cells a range or array value may have when it is materialised as
1996    /// a dense array (`evaluation_budgets.scratch.materialized_array_cells`);
1997    /// larger arrays evaluate to `#NUM!`. The default is 2^24 cells.
1998    fn materialized_array_cells(&self) -> u64 {
1999        crate::engine::resource_ledger::DEFAULT_MATERIALIZED_ARRAY_CELLS
2000    }
2001
2002    /// Resolve a reference into a `RangeView` with clear bounds.
2003    /// Implementations should resolve un/partially bounded references using used-region.
2004    fn resolve_range_view<'c>(
2005        &'c self,
2006        _reference: &ReferenceType,
2007        _current_sheet: &str,
2008    ) -> Result<RangeView<'c>, ExcelError> {
2009        Err(ExcelError::new(ExcelErrorKind::NImpl))
2010    }
2011
2012    /// Resolve a single-cell reference as a scalar value.
2013    ///
2014    /// Default implementation preserves existing reference semantics by routing through
2015    /// `resolve_range_view` and extracting a 1x1 value.
2016    fn resolve_cell_reference_value(
2017        &self,
2018        sheet: Option<&str>,
2019        row: u32,
2020        col: u32,
2021        current_sheet: &str,
2022    ) -> Result<LiteralValue, ExcelError> {
2023        let reference = ReferenceType::Cell {
2024            sheet: sheet.map(str::to_string),
2025            row,
2026            col,
2027            row_abs: true,
2028            col_abs: true,
2029        };
2030        let view = self.resolve_range_view(&reference, current_sheet)?;
2031        Ok(view.as_1x1().unwrap_or(LiteralValue::Empty))
2032    }
2033
2034    /// Resolve the effective number-format annotation of a scalar cell read.
2035    fn resolve_cell_format(
2036        &self,
2037        _sheet: Option<&str>,
2038        _row: u32,
2039        _col: u32,
2040        _current_sheet: &str,
2041    ) -> Option<crate::format::FormatId> {
2042        None
2043    }
2044
2045    /// A cell reference's value and format in one call: exactly
2046    /// `resolve_cell_reference_value` then `resolve_cell_format` (the
2047    /// default does that). Contexts that answer both from one lookup
2048    /// override it; wrappers that record reads need not.
2049    #[doc(hidden)]
2050    fn resolve_cell_reference_value_formatted(
2051        &self,
2052        sheet: Option<&str>,
2053        row: u32,
2054        col: u32,
2055        current_sheet: &str,
2056    ) -> Result<(LiteralValue, Option<crate::format::FormatId>), ExcelError> {
2057        let value = self.resolve_cell_reference_value(sheet, row, col, current_sheet)?;
2058        Ok((
2059            value,
2060            self.resolve_cell_format(sheet, row, col, current_sheet),
2061        ))
2062    }
2063
2064    /// Resolve an interned format id to its reported class.
2065    fn format_class(
2066        &self,
2067        format: crate::format::FormatId,
2068    ) -> Option<formualizer_common::numfmt::FormatClass> {
2069        formualizer_common::numfmt::NumberFormat::builtin(format.0)
2070            .map(|format| format.class().clone())
2071    }
2072
2073    /// Record a formula cell's derived scalar format during alternate scalar evaluation paths.
2074    fn record_cell_derived_format(
2075        &self,
2076        _sheet: &str,
2077        _row: u32,
2078        _col: u32,
2079        _format: Option<crate::format::FormatId>,
2080    ) {
2081    }
2082
2083    /// Locale provider: invariant by default
2084    fn locale(&self) -> crate::locale::Locale {
2085        crate::locale::Locale::invariant()
2086    }
2087
2088    /// Number of active sheets in the workbook, if known.
2089    fn workbook_sheet_count(&self) -> Option<usize> {
2090        None
2091    }
2092
2093    /// Excel-style 1-based active-sheet index for a sheet name, if known.
2094    fn sheet_index_by_name(&self, _sheet: &str) -> Option<usize> {
2095        None
2096    }
2097
2098    /// Excel-style 1-based active-sheet index for the current formula sheet, if known.
2099    fn current_sheet_index(&self, current_sheet: &str) -> Option<usize> {
2100        self.sheet_index_by_name(current_sheet)
2101    }
2102
2103    /// Inspect reference metadata without materializing referenced values.
2104    fn inspect_reference(
2105        &self,
2106        _reference: &ReferenceType,
2107        _current_sheet: &str,
2108    ) -> Result<Option<ReferenceInfo>, ExcelError> {
2109        Ok(None)
2110    }
2111
2112    /// Retrieve formula text for a concrete cell, if that cell stores a formula.
2113    fn formula_text_at_cell(&self, _cell: CellRef) -> Result<Option<String>, ExcelError> {
2114        Ok(None)
2115    }
2116
2117    /// Resolve a spill-range reference (`A1#`, `ANCHORARRAY(A1)`) to the
2118    /// anchor's current committed spill rectangle.
2119    ///
2120    /// `anchor` is the operand as written, relocated for the current cell:
2121    /// a single-cell [`ReferenceType::Cell`] or a [`ReferenceType::NamedRange`]
2122    /// that must name one cell. The result is a real sheet reference, so
2123    /// callers read it through the ordinary reference path.
2124    ///
2125    /// Policy: `#REF!` for any other operand and for an anchor without a
2126    /// current spill (a value, an empty cell, a scalar result, or a blocked or
2127    /// oversized spill). The default, for contexts without a spill registry,
2128    /// is `#REF!`.
2129    fn resolve_spill_reference(
2130        &self,
2131        _anchor: &ReferenceType,
2132        _current_sheet: &str,
2133    ) -> Result<ReferenceType, ExcelError> {
2134        Err(ExcelError::new(ExcelErrorKind::Ref)
2135            .with_message("Spill references are not available in this context"))
2136    }
2137
2138    /// Clock provider for volatile date/time builtins.
2139    ///
2140    /// Default when `system-clock` feature is enabled: `SystemClock(Local)` for
2141    /// Excel-compatible wall-clock behaviour.
2142    ///
2143    /// Default when `system-clock` is **disabled** (portable wasm profile): a
2144    /// UTC epoch `FixedClock`. Implementors that need real wall-clock time should
2145    /// override this method and inject an appropriate `ClockProvider`.
2146    fn clock(&self) -> &dyn crate::timezone::ClockProvider {
2147        #[cfg(feature = "system-clock")]
2148        {
2149            static DEFAULT_CLOCK: std::sync::OnceLock<crate::timezone::SystemClock> =
2150                std::sync::OnceLock::new();
2151            DEFAULT_CLOCK.get_or_init(|| {
2152                crate::timezone::SystemClock::new(crate::timezone::TimeZoneSpec::default())
2153            })
2154        }
2155        #[cfg(not(feature = "system-clock"))]
2156        {
2157            static DEFAULT_CLOCK: std::sync::OnceLock<crate::timezone::FixedClock> =
2158                std::sync::OnceLock::new();
2159            DEFAULT_CLOCK.get_or_init(|| {
2160                crate::timezone::FixedClock::new(
2161                    chrono::DateTime::UNIX_EPOCH,
2162                    crate::timezone::TimeZoneSpec::Utc,
2163                )
2164            })
2165        }
2166    }
2167
2168    /// Timezone spec for date/time functions.
2169    ///
2170    /// Default: derived from `clock()`.
2171    fn timezone(&self) -> &crate::timezone::TimeZoneSpec {
2172        self.clock().timezone()
2173    }
2174
2175    /// Volatile granularity. Default Always for backwards compatibility.
2176    fn volatile_level(&self) -> VolatileLevel {
2177        VolatileLevel::Always
2178    }
2179
2180    /// A stable workbook seed for RNG composition.
2181    fn workbook_seed(&self) -> u64 {
2182        0xF0F0_D0D0_AAAA_5555
2183    }
2184
2185    /// Recalc epoch that increments on each full recalc when appropriate.
2186    fn recalc_epoch(&self) -> u64 {
2187        0
2188    }
2189
2190    /* ─────────────── Future-proof IO/backends hooks (default no-op) ─────────────── */
2191
2192    /// Optional: Return the min/max used rows for a set of columns on a sheet.
2193    /// When None, the backend does not provide used-region hints.
2194    fn used_rows_for_columns(
2195        &self,
2196        _sheet: &str,
2197        _start_col: u32,
2198        _end_col: u32,
2199    ) -> Option<(u32, u32)> {
2200        None
2201    }
2202
2203    /// Optional: Return the min/max used columns for a set of rows on a sheet.
2204    /// When None, the backend does not provide used-region hints.
2205    fn used_cols_for_rows(
2206        &self,
2207        _sheet: &str,
2208        _start_row: u32,
2209        _end_row: u32,
2210    ) -> Option<(u32, u32)> {
2211        None
2212    }
2213
2214    /// Optional: Physical sheet bounds (max rows, max cols) if known.
2215    fn sheet_bounds(&self, _sheet: &str) -> Option<(u32, u32)> {
2216        None
2217    }
2218
2219    /// Monotonic identifier for the current data snapshot; increments on mutation.
2220    fn data_snapshot_id(&self) -> u64 {
2221        0
2222    }
2223
2224    /// Backend capability advertisement for IO/adapters.
2225    fn backend_caps(&self) -> BackendCaps {
2226        BackendCaps::default()
2227    }
2228
2229    // Flats removed
2230
2231    /// Workbook date system selection (1900 vs 1904).
2232    /// Defaults to 1900 for compatibility.
2233    fn date_system(&self) -> crate::engine::DateSystem {
2234        crate::engine::DateSystem::Excel1900
2235    }
2236
2237    /// Optional: Build or fetch an exact-match lookup index over an Arrow-backed view.
2238    /// Implementations should return None if not supported or unsafe.
2239    fn build_lookup_index(
2240        &self,
2241        _view: &RangeView<'_>,
2242        _axis: LookupAxis,
2243    ) -> Option<std::sync::Arc<LookupIndex>> {
2244        None
2245    }
2246
2247    /// Optional: Build or fetch a cached boolean mask for a criterion over an Arrow-backed view.
2248    /// Implementations should return None if not supported.
2249    fn build_criteria_mask(
2250        &self,
2251        _view: &RangeView<'_>,
2252        _col_in_view: usize,
2253        _pred: &crate::args::CriteriaPredicate,
2254    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
2255        None
2256    }
2257
2258    /// Optional: Build row-visibility mask aligned to `view` rows.
2259    /// Returns None if not supported by the underlying context.
2260    fn build_row_visibility_mask(
2261        &self,
2262        _view: &RangeView<'_>,
2263        _mode: VisibilityMaskMode,
2264    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
2265        None
2266    }
2267
2268    /// Optional: the cells of a sheet-backed `view` whose formulas call
2269    /// SUBTOTAL (and AGGREGATE when `include_aggregate`), as sorted,
2270    /// disjoint `(column, first_row, last_row)` offset intervals within the
2271    /// view (rows inclusive). SUBTOTAL and AGGREGATE skip
2272    /// these cells (Excel ignores nested subtotals). `None` when the context
2273    /// does not track formulas.
2274    fn nested_subtotal_cells(
2275        &self,
2276        _view: &RangeView<'_>,
2277        _include_aggregate: bool,
2278    ) -> Option<Vec<(usize, usize, usize)>> {
2279        None
2280    }
2281}
2282
2283/// Minimal backend capability descriptor for planning and adapters.
2284#[derive(Copy, Clone, Debug, Default)]
2285pub struct BackendCaps {
2286    /// Provides lazy access (// TODO REMOVE?)
2287    pub streaming: bool,
2288    /// Can compute used-region for rows/columns
2289    pub used_region: bool,
2290    /// Supports write-back mutations via external sink
2291    pub write: bool,
2292    /// Provides table metadata/streaming beyond basic column access
2293    pub tables: bool,
2294    /// May provide asynchronous/lazy remote streams (reserved)
2295    pub async_stream: bool,
2296}
2297
2298/* ───────────────────── FunctionContext (narrow) ───────────────────── */
2299
2300#[derive(Copy, Clone, Debug, Eq, PartialEq)]
2301pub enum VolatileLevel {
2302    /// Value can change at any edit; seed excludes recalc_epoch by default.
2303    Always,
2304    /// Value changes per recalculation; seed should include recalc_epoch.
2305    OnRecalc,
2306    /// Value changes per open; seed uses only workbook_seed.
2307    OnOpen,
2308}
2309
2310/// Minimal context exposed to functions (no engine/graph APIs)
2311pub trait FunctionContext<'ctx> {
2312    fn locale(&self) -> crate::locale::Locale;
2313    fn timezone(&self) -> &crate::timezone::TimeZoneSpec;
2314    fn clock(&self) -> &dyn crate::timezone::ClockProvider;
2315    fn thread_pool(&self) -> Option<&std::sync::Arc<rayon::ThreadPool>>;
2316    /// Returns the optional shared cancellation handle for this evaluation.
2317    ///
2318    /// Custom function authors should retrieve this once before a hot loop and
2319    /// poll [`crate::engine::CancelToken::is_cancelled`] periodically. Cloning
2320    /// the handle shares the same signal without allocating.
2321    fn cancellation_token(&self) -> Option<crate::engine::CancelToken>;
2322    fn chunk_hint(&self) -> Option<usize>;
2323
2324    /// Most cells a range or array value may have when it is materialised as
2325    /// a dense array; larger arrays evaluate to `#NUM!`. See
2326    /// [`EvaluationContext::materialized_array_cells`].
2327    fn materialized_array_cells(&self) -> u64 {
2328        crate::engine::resource_ledger::DEFAULT_MATERIALIZED_ARRAY_CELLS
2329    }
2330
2331    /// Current formula sheet name.
2332    fn current_sheet(&self) -> &str;
2333
2334    fn workbook_sheet_count(&self) -> Option<usize> {
2335        None
2336    }
2337
2338    fn sheet_index_by_name(&self, _sheet: &str) -> Option<usize> {
2339        None
2340    }
2341
2342    fn current_sheet_index(&self) -> Option<usize> {
2343        self.sheet_index_by_name(self.current_sheet())
2344    }
2345
2346    fn inspect_reference(
2347        &self,
2348        _reference: &ReferenceType,
2349    ) -> Result<Option<ReferenceInfo>, ExcelError> {
2350        Ok(None)
2351    }
2352
2353    fn formula_text_at_cell(&self, _cell: CellRef) -> Result<Option<String>, ExcelError> {
2354        Ok(None)
2355    }
2356
2357    fn volatile_level(&self) -> VolatileLevel;
2358    fn workbook_seed(&self) -> u64;
2359    fn recalc_epoch(&self) -> u64;
2360    fn current_cell(&self) -> Option<CellRef>;
2361
2362    /// Resolve a reference into a RangeView using the underlying engine context.
2363    fn resolve_range_view(
2364        &self,
2365        _reference: &ReferenceType,
2366        _current_sheet: &str,
2367    ) -> Result<RangeView<'ctx>, ExcelError>;
2368
2369    // Flats removed
2370
2371    /// Deterministic RNG seeded for the current evaluation site and function salt.
2372    fn rng_for_current(&self, fn_salt: u64) -> rand::rngs::SmallRng {
2373        use crate::rng::{compose_seed, small_rng_from_lanes};
2374        let (sheet_id, row, col) = self
2375            .current_cell()
2376            .map(|c| (c.sheet_id as u32, c.coord.row(), c.coord.col()))
2377            .unwrap_or((0, 0, 0));
2378        // Include epoch only for OnRecalc
2379        let epoch = match self.volatile_level() {
2380            VolatileLevel::OnRecalc => self.recalc_epoch(),
2381            _ => 0,
2382        };
2383        let (l0, l1) = compose_seed(self.workbook_seed(), sheet_id, row, col, fn_salt, epoch);
2384        small_rng_from_lanes(l0, l1)
2385    }
2386
2387    /// Workbook date system selection (1900 vs 1904).
2388    fn date_system(&self) -> crate::engine::DateSystem {
2389        crate::engine::DateSystem::Excel1900
2390    }
2391
2392    /// Optional: Build or fetch an exact-match lookup index over an Arrow-backed view.
2393    /// Returns None if not supported by the underlying context.
2394    fn get_lookup_index(
2395        &self,
2396        _view: &RangeView<'_>,
2397        _axis: LookupAxis,
2398    ) -> Option<std::sync::Arc<LookupIndex>> {
2399        None
2400    }
2401
2402    /// Optional: Build or fetch a cached boolean mask for a criterion over an Arrow-backed view.
2403    /// Returns None if not supported by the underlying context.
2404    fn get_criteria_mask(
2405        &self,
2406        _view: &RangeView<'_>,
2407        _col_in_view: usize,
2408        _pred: &crate::args::CriteriaPredicate,
2409    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
2410        None
2411    }
2412
2413    /// Optional: Build row-visibility mask aligned to `view` rows.
2414    fn get_row_visibility_mask(
2415        &self,
2416        _view: &RangeView<'_>,
2417        _mode: VisibilityMaskMode,
2418    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
2419        None
2420    }
2421
2422    /// Optional: cells of `view` holding nested SUBTOTAL (and AGGREGATE when
2423    /// `include_aggregate`) formulas; see
2424    /// [`EvaluationContext::nested_subtotal_cells`].
2425    fn get_nested_subtotal_cells(
2426        &self,
2427        _view: &RangeView<'_>,
2428        _include_aggregate: bool,
2429    ) -> Option<Vec<(usize, usize, usize)>> {
2430        None
2431    }
2432}
2433
2434/// Default adapter that wraps an EvaluationContext and provides the narrow FunctionContext.
2435pub struct DefaultFunctionContext<'a> {
2436    pub base: &'a dyn EvaluationContext,
2437    pub current: Option<CellRef>,
2438    pub current_sheet: &'a str,
2439}
2440
2441impl<'a> DefaultFunctionContext<'a> {
2442    pub fn new(
2443        base: &'a dyn EvaluationContext,
2444        current: Option<CellRef>,
2445        current_sheet: &'a str,
2446    ) -> Self {
2447        Self {
2448            base,
2449            current,
2450            current_sheet,
2451        }
2452    }
2453
2454    pub fn new_with_sheet(
2455        base: &'a dyn EvaluationContext,
2456        current: Option<CellRef>,
2457        current_sheet: &'a str,
2458    ) -> Self {
2459        Self::new(base, current, current_sheet)
2460    }
2461}
2462
2463impl<'a> FunctionContext<'a> for DefaultFunctionContext<'a> {
2464    fn locale(&self) -> crate::locale::Locale {
2465        self.base.locale()
2466    }
2467
2468    fn current_sheet(&self) -> &str {
2469        self.current_sheet
2470    }
2471
2472    fn workbook_sheet_count(&self) -> Option<usize> {
2473        self.base.workbook_sheet_count()
2474    }
2475
2476    fn sheet_index_by_name(&self, sheet: &str) -> Option<usize> {
2477        self.base.sheet_index_by_name(sheet)
2478    }
2479
2480    fn current_sheet_index(&self) -> Option<usize> {
2481        self.base.current_sheet_index(self.current_sheet)
2482    }
2483
2484    fn inspect_reference(
2485        &self,
2486        reference: &ReferenceType,
2487    ) -> Result<Option<ReferenceInfo>, ExcelError> {
2488        self.base.inspect_reference(reference, self.current_sheet)
2489    }
2490
2491    fn formula_text_at_cell(&self, cell: CellRef) -> Result<Option<String>, ExcelError> {
2492        self.base.formula_text_at_cell(cell)
2493    }
2494
2495    fn timezone(&self) -> &crate::timezone::TimeZoneSpec {
2496        self.base.timezone()
2497    }
2498
2499    fn clock(&self) -> &dyn crate::timezone::ClockProvider {
2500        self.base.clock()
2501    }
2502    fn thread_pool(&self) -> Option<&std::sync::Arc<rayon::ThreadPool>> {
2503        self.base.thread_pool()
2504    }
2505    fn cancellation_token(&self) -> Option<crate::engine::CancelToken> {
2506        self.base.cancellation_token()
2507    }
2508    fn chunk_hint(&self) -> Option<usize> {
2509        self.base.chunk_hint()
2510    }
2511    fn materialized_array_cells(&self) -> u64 {
2512        self.base.materialized_array_cells()
2513    }
2514
2515    fn volatile_level(&self) -> VolatileLevel {
2516        self.base.volatile_level()
2517    }
2518    fn workbook_seed(&self) -> u64 {
2519        self.base.workbook_seed()
2520    }
2521    fn recalc_epoch(&self) -> u64 {
2522        self.base.recalc_epoch()
2523    }
2524    fn current_cell(&self) -> Option<CellRef> {
2525        self.current
2526    }
2527
2528    fn resolve_range_view(
2529        &self,
2530        reference: &ReferenceType,
2531        current_sheet: &str,
2532    ) -> Result<RangeView<'a>, ExcelError> {
2533        self.base
2534            .resolve_range_view(reference, current_sheet)
2535            .map(|view| view.with_cell_budget(self.base.materialized_array_cells()))
2536    }
2537
2538    // Flats removed
2539
2540    fn date_system(&self) -> crate::engine::DateSystem {
2541        self.base.date_system()
2542    }
2543
2544    fn get_lookup_index(
2545        &self,
2546        view: &RangeView<'_>,
2547        axis: LookupAxis,
2548    ) -> Option<std::sync::Arc<LookupIndex>> {
2549        self.base.build_lookup_index(view, axis)
2550    }
2551
2552    fn get_criteria_mask(
2553        &self,
2554        view: &RangeView<'_>,
2555        col_in_view: usize,
2556        pred: &crate::args::CriteriaPredicate,
2557    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
2558        self.base.build_criteria_mask(view, col_in_view, pred)
2559    }
2560
2561    fn get_row_visibility_mask(
2562        &self,
2563        view: &RangeView<'_>,
2564        mode: VisibilityMaskMode,
2565    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
2566        self.base.build_row_visibility_mask(view, mode)
2567    }
2568
2569    fn get_nested_subtotal_cells(
2570        &self,
2571        view: &RangeView<'_>,
2572        include_aggregate: bool,
2573    ) -> Option<Vec<(usize, usize, usize)>> {
2574        self.base.nested_subtotal_cells(view, include_aggregate)
2575    }
2576}
2577
2578#[cfg(test)]
2579mod argument_handle_size_tests {
2580    use super::*;
2581    #[expect(dead_code, reason = "layout-only regression fixture")]
2582    struct LegacyArgumentHandle<'a, 'b> {
2583        expr: ArgumentExpr<'a>,
2584        interp: &'a Interpreter<'b>,
2585        cached_ast: std::cell::OnceCell<ASTNode>,
2586        cached_ref: std::cell::OnceCell<ReferenceType>,
2587        cached_reference_or_value:
2588            std::cell::OnceCell<Result<crate::function::FunctionResolution<'b>, ExcelError>>,
2589        cached_resolved: std::cell::OnceCell<Result<ResolvedArgument<'b>, ExcelError>>,
2590        /// Memoized result of [`Self::value`]. `Function::dispatch` evaluates
2591        /// every argument once during schema validation and the function's `eval`
2592        /// evaluates it again — without this cache that re-entry compounds to
2593        /// 2^depth evaluations of the innermost node for nested non-short-circuit
2594        /// calls (measured: depth 12 ⇒ 4096 evaluations). The handle is created
2595        /// per call site and per evaluation, so the memo can never go stale
2596        /// across recalcs. `value_with_env` is intentionally NOT memoized (the
2597        /// local env changes the result).
2598        cached_value: std::cell::OnceCell<Result<crate::traits::CalcValue<'b>, ExcelError>>,
2599    }
2600
2601    #[test]
2602    fn argument_handle_fits_inline_call_storage() {
2603        let before = std::mem::size_of::<LegacyArgumentHandle<'_, '_>>();
2604        let after = std::mem::size_of::<ArgumentHandle<'_, '_>>();
2605        assert_eq!(after, 80, "ArgumentHandle layout changed");
2606        assert!(after < before / 4, "cold caches must stay out of line");
2607    }
2608}