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