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