Skip to main content

formualizer_eval/
traits.rs

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