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