Skip to main content

formualizer_eval/
traits.rs

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