Skip to main content

formualizer_eval/engine/arena/
ast.rs

1/// AST arena with structural sharing and deduplication
2/// Stores formula AST nodes efficiently with content-addressable storage
3use super::string_interner::{StringId, StringInterner};
4use super::value_ref::ValueRef;
5use formualizer_parse::parser::{ExternalRefKind, TableSpecifier};
6use rustc_hash::FxHashMap;
7use std::collections::hash_map::DefaultHasher;
8use std::fmt;
9use std::hash::{Hash, Hasher};
10
11/// Reference to an AST node in the arena
12#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
13pub struct AstNodeId(u32);
14
15impl AstNodeId {
16    pub fn as_u32(self) -> u32 {
17        self.0
18    }
19
20    pub(crate) const fn from_u32(raw: u32) -> Self {
21        Self(raw)
22    }
23}
24
25impl fmt::Display for AstNodeId {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(f, "AstNode({})", self.0)
28    }
29}
30
31#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
32pub struct TableSpecId(u32);
33
34impl TableSpecId {
35    pub fn as_u32(self) -> u32 {
36        self.0
37    }
38}
39
40/// Compact representation of AST nodes in the arena
41#[derive(Debug, Clone, PartialEq, Eq, Hash)]
42pub enum AstNodeData {
43    /// Literal value
44    Literal(ValueRef),
45
46    /// Explicitly omitted function argument.
47    Omitted,
48
49    /// Cell or range reference
50    Reference {
51        original_id: StringId,    // Original reference string
52        ref_type: CompactRefType, // Compact reference representation
53    },
54
55    /// Unary operation
56    UnaryOp { op_id: StringId, expr_id: AstNodeId },
57
58    /// Binary operation
59    BinaryOp {
60        op_id: StringId,
61        left_id: AstNodeId,
62        right_id: AstNodeId,
63    },
64
65    /// Function call
66    Function {
67        name_id: StringId,
68        args_offset: u32, // Index into args array
69        args_count: u16,  // Number of arguments
70    },
71
72    /// Array literal
73    Array {
74        rows: u16,
75        cols: u16,
76        elements_offset: u32, // Index into elements array
77    },
78}
79
80/// Identifies a sheet either by stable registry id or by unresolved name.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub enum SheetKey {
83    Id(u16),
84    Name(StringId),
85}
86
87/// Compact representation of reference types
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89pub enum CompactRefType {
90    Cell {
91        sheet: Option<SheetKey>,
92        row: u32,
93        col: u32,
94        row_abs: bool,
95        col_abs: bool,
96    },
97    Range {
98        sheet: Option<SheetKey>,
99        start_row: u32,
100        start_col: u32,
101        end_row: u32,
102        end_col: u32,
103        start_row_abs: bool,
104        start_col_abs: bool,
105        end_row_abs: bool,
106        end_col_abs: bool,
107    },
108    External {
109        raw_id: StringId,
110        book_id: StringId,
111        sheet_id: StringId,
112        kind: ExternalRefKind,
113    },
114    NamedRange(StringId),
115    Table {
116        name_id: StringId,
117        specifier_id: Option<TableSpecId>,
118    },
119    /// 3D cell reference (`Sheet1:Sheet3!A1`).
120    Cell3D {
121        sheet_first: StringId,
122        sheet_last: StringId,
123        row: u32,
124        col: u32,
125        row_abs: bool,
126        col_abs: bool,
127    },
128    /// 3D range reference (`Sheet1:Sheet3!A1:B2`).
129    Range3D {
130        sheet_first: StringId,
131        sheet_last: StringId,
132        start_row: u32,
133        start_col: u32,
134        end_row: u32,
135        end_col: u32,
136        start_row_abs: bool,
137        start_col_abs: bool,
138        end_row_abs: bool,
139        end_col_abs: bool,
140    },
141}
142
143/// Arena entry containing structural node data plus canonical metadata.
144///
145/// Phase 1 keeps metadata at its default value for existing raw interning
146/// paths. Future canonical interning paths populate `meta` via the arena
147/// canonicalization helpers.
148#[derive(Debug, Clone, PartialEq, Eq, Hash)]
149pub(crate) struct AstNodeEntry {
150    pub(crate) data: AstNodeData,
151    pub(crate) meta: AstNodeMetadata,
152}
153
154/// Canonical metadata associated with an arena AST node.
155#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
156pub(crate) struct AstNodeMetadata {
157    pub(crate) canonical_hash: u64,
158    pub(crate) labels: CanonicalLabels,
159}
160
161/// Compact bitset labels for arena-native canonicalization.
162#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
163pub(crate) struct CanonicalLabels {
164    pub(crate) flags: u64,
165    pub(crate) rejects: u64,
166}
167
168#[allow(dead_code)]
169impl CanonicalLabels {
170    pub(crate) const FLAG_RELATIVE_ONLY: u64 = 1 << 0;
171    pub(crate) const FLAG_ABSOLUTE_ONLY: u64 = 1 << 1;
172    pub(crate) const FLAG_MIXED_ANCHORS: u64 = 1 << 2;
173    pub(crate) const FLAG_VOLATILE: u64 = 1 << 3;
174    pub(crate) const FLAG_DYNAMIC: u64 = 1 << 4;
175    pub(crate) const FLAG_CONTAINS_STRUCTURED_REF: u64 = 1 << 5;
176    pub(crate) const FLAG_NEEDS_PLACEMENT_REWRITE: u64 = 1 << 6;
177    pub(crate) const FLAG_CONTAINS_NAME: u64 = 1 << 7;
178    pub(crate) const FLAG_CONTAINS_TABLE: u64 = 1 << 8;
179    pub(crate) const FLAG_CONTAINS_RANGE: u64 = 1 << 9;
180    pub(crate) const FLAG_CONTAINS_ARRAY: u64 = 1 << 10;
181    pub(crate) const FLAG_CONTAINS_LET_LAMBDA: u64 = 1 << 11;
182    pub(crate) const FLAG_CONTAINS_FUNCTION: u64 = 1 << 12;
183    pub(crate) const FLAG_EXPLICIT_SHEET: u64 = 1 << 13;
184    pub(crate) const FLAG_CURRENT_SHEET: u64 = 1 << 14;
185
186    // Reject bits mirror `CanonicalRejectReason` variants in
187    // `formula_plane/template_canonical.rs` by kind/variant order.
188    pub(crate) const REJECT_INVALID_PLACEMENT_ANCHOR: u64 = 1 << 0;
189    pub(crate) const REJECT_DYNAMIC_REFERENCE: u64 = 1 << 1;
190    pub(crate) const REJECT_UNKNOWN_OR_CUSTOM_FUNCTION: u64 = 1 << 2;
191    pub(crate) const REJECT_LOCAL_ENVIRONMENT: u64 = 1 << 3;
192    pub(crate) const REJECT_PARSER_VOLATILE_FLAG: u64 = 1 << 4;
193    pub(crate) const REJECT_VOLATILE_FUNCTION: u64 = 1 << 5;
194    pub(crate) const REJECT_REFERENCE_RETURNING_FUNCTION: u64 = 1 << 6;
195    pub(crate) const REJECT_ARRAY_OR_SPILL_FUNCTION: u64 = 1 << 7;
196    pub(crate) const REJECT_ARRAY_LITERAL: u64 = 1 << 8;
197    pub(crate) const REJECT_SPILL_REFERENCE: u64 = 1 << 9;
198    pub(crate) const REJECT_SPILL_RESULT_REGION_OPERATOR: u64 = 1 << 10;
199    pub(crate) const REJECT_IMPLICIT_INTERSECTION_OPERATOR: u64 = 1 << 11;
200    pub(crate) const REJECT_CALL_EXPRESSION: u64 = 1 << 12;
201    // Bit 13 (REJECT_NAMED_REFERENCE) retired: named references canonicalize
202    // by identity and are accepted/rejected at read-projection time instead.
203    pub(crate) const REJECT_STRUCTURED_REFERENCE: u64 = 1 << 14;
204    pub(crate) const REJECT_STRUCTURED_REFERENCE_CURRENT_ROW: u64 = 1 << 15;
205    pub(crate) const REJECT_THREE_D_REFERENCE: u64 = 1 << 16;
206    pub(crate) const REJECT_EXTERNAL_REFERENCE: u64 = 1 << 17;
207    pub(crate) const REJECT_OPEN_RANGE_REFERENCE: u64 = 1 << 18;
208    pub(crate) const REJECT_WHOLE_AXIS_REFERENCE: u64 = 1 << 19;
209    pub(crate) const REJECT_UNSUPPORTED_REFERENCE: u64 = 1 << 20;
210
211    pub(crate) fn has_flag(self, flag: u64) -> bool {
212        self.flags & flag != 0
213    }
214
215    pub(crate) fn has_reject(self, reject: u64) -> bool {
216        self.rejects & reject != 0
217    }
218}
219
220/// Arena for storing AST nodes with deduplication
221pub struct AstArena {
222    /// Node storage
223    nodes: Vec<AstNodeEntry>,
224
225    /// Hash -> node index for deduplication
226    dedup_map: FxHashMap<u64, AstNodeId>,
227
228    /// Function arguments storage (flattened)
229    function_args: Vec<AstNodeId>,
230
231    /// Array elements storage (flattened)
232    array_elements: Vec<AstNodeId>,
233
234    /// String pool for operators and function names
235    strings: StringInterner,
236
237    /// Structured table specifiers
238    table_specs: Vec<TableSpecifier>,
239    table_spec_dedup: FxHashMap<u64, TableSpecId>,
240
241    /// Statistics
242    dedup_hits: usize,
243}
244
245impl AstArena {
246    pub fn new() -> Self {
247        Self {
248            nodes: Vec::new(),
249            dedup_map: FxHashMap::default(),
250            function_args: Vec::new(),
251            array_elements: Vec::new(),
252            strings: StringInterner::new(),
253            table_specs: Vec::new(),
254            table_spec_dedup: FxHashMap::default(),
255            dedup_hits: 0,
256        }
257    }
258
259    pub fn with_capacity(node_cap: usize) -> Self {
260        Self {
261            nodes: Vec::with_capacity(node_cap),
262            dedup_map: FxHashMap::with_capacity_and_hasher(node_cap, Default::default()),
263            function_args: Vec::with_capacity(node_cap * 2), // Assume avg 2 args
264            array_elements: Vec::with_capacity(node_cap),
265            strings: StringInterner::with_capacity(node_cap / 10),
266            table_specs: Vec::new(),
267            table_spec_dedup: FxHashMap::default(),
268            dedup_hits: 0,
269        }
270    }
271
272    /// Insert a node, deduplicating if it already exists.
273    ///
274    /// Phase 1 preserves raw/literal interning semantics: metadata is filled
275    /// with zeros and is not part of the dedup key.
276    pub fn insert(&mut self, node: AstNodeData) -> AstNodeId {
277        self.insert_entry(node, AstNodeMetadata::default())
278    }
279
280    /// Insert a node with precomputed canonical metadata.
281    ///
282    /// This is unused in Phase 1 and reserved for the future canonical
283    /// interning path. Deduplication remains structural on `AstNodeData` only;
284    /// if a matching node already exists, the existing entry (and metadata)
285    /// wins.
286    #[allow(dead_code)]
287    pub(crate) fn insert_with_meta(
288        &mut self,
289        node: AstNodeData,
290        meta: AstNodeMetadata,
291    ) -> AstNodeId {
292        self.insert_entry(node, meta)
293    }
294
295    fn insert_entry(&mut self, node: AstNodeData, meta: AstNodeMetadata) -> AstNodeId {
296        // Compute structural hash. Metadata is deliberately excluded.
297        let hash = self.hash_node(&node);
298
299        // Check for existing node
300        if let Some(&id) = self.dedup_map.get(&hash) {
301            // Verify it's actually the same (handle hash collisions)
302            if self.nodes[id.0 as usize].data == node {
303                self.dedup_hits += 1;
304                return id;
305            }
306        }
307
308        // Add new node
309        let id = AstNodeId(self.nodes.len() as u32);
310        self.nodes.push(AstNodeEntry { data: node, meta });
311        self.dedup_map.insert(hash, id);
312        id
313    }
314
315    /// Insert a literal node
316    pub fn insert_literal(&mut self, value: ValueRef) -> AstNodeId {
317        self.insert(AstNodeData::Literal(value))
318    }
319
320    /// Insert an explicitly omitted argument node.
321    pub(crate) fn insert_omitted(&mut self) -> AstNodeId {
322        self.insert(AstNodeData::Omitted)
323    }
324
325    /// Insert a reference node
326    pub fn insert_reference(&mut self, original: &str, ref_type: CompactRefType) -> AstNodeId {
327        let original_id = self.strings.intern(original);
328        self.insert(AstNodeData::Reference {
329            original_id,
330            ref_type,
331        })
332    }
333
334    /// Insert a unary operation node
335    pub fn insert_unary_op(&mut self, op: &str, expr: AstNodeId) -> AstNodeId {
336        let op_id = self.strings.intern(op);
337        self.insert(AstNodeData::UnaryOp {
338            op_id,
339            expr_id: expr,
340        })
341    }
342
343    /// Insert a binary operation node
344    pub fn insert_binary_op(&mut self, op: &str, left: AstNodeId, right: AstNodeId) -> AstNodeId {
345        let op_id = self.strings.intern(op);
346        self.insert(AstNodeData::BinaryOp {
347            op_id,
348            left_id: left,
349            right_id: right,
350        })
351    }
352
353    /// Insert a function call node
354    pub fn insert_function(&mut self, name: &str, args: Vec<AstNodeId>) -> AstNodeId {
355        let name_id = self.strings.intern(name);
356        let args_offset = self.function_args.len() as u32;
357        let args_count = args.len() as u16;
358
359        self.function_args.extend(args);
360
361        self.insert(AstNodeData::Function {
362            name_id,
363            args_offset,
364            args_count,
365        })
366    }
367
368    /// Insert an array literal node
369    pub fn insert_array(&mut self, rows: u16, cols: u16, elements: Vec<AstNodeId>) -> AstNodeId {
370        assert_eq!(
371            elements.len(),
372            (rows * cols) as usize,
373            "Array dimensions don't match element count"
374        );
375
376        let elements_offset = self.array_elements.len() as u32;
377        self.array_elements.extend(elements);
378
379        self.insert(AstNodeData::Array {
380            rows,
381            cols,
382            elements_offset,
383        })
384    }
385
386    /// Get a node by ID
387    pub fn get(&self, id: AstNodeId) -> Option<&AstNodeData> {
388        self.nodes.get(id.0 as usize).map(|entry| &entry.data)
389    }
390
391    /// Get an arena entry by ID.
392    #[allow(dead_code)]
393    pub(crate) fn entry(&self, id: AstNodeId) -> Option<&AstNodeEntry> {
394        self.nodes.get(id.0 as usize)
395    }
396
397    /// Get canonical metadata for a node by ID.
398    #[allow(dead_code)]
399    pub(crate) fn metadata(&self, id: AstNodeId) -> Option<AstNodeMetadata> {
400        self.entry(id).map(|entry| entry.meta)
401    }
402
403    /// Get function arguments for a function node
404    pub fn get_function_args(&self, id: AstNodeId) -> Option<&[AstNodeId]> {
405        match self.get(id)? {
406            AstNodeData::Function {
407                args_offset,
408                args_count,
409                ..
410            } => {
411                let start = *args_offset as usize;
412                let end = start + *args_count as usize;
413                Some(&self.function_args[start..end])
414            }
415            _ => None,
416        }
417    }
418
419    /// Get array elements for an array node
420    pub fn get_array_elements(&self, id: AstNodeId) -> Option<&[AstNodeId]> {
421        match self.get(id)? {
422            AstNodeData::Array {
423                rows,
424                cols,
425                elements_offset,
426            } => {
427                let start = *elements_offset as usize;
428                let count = (*rows * *cols) as usize;
429                let end = start + count;
430                Some(&self.array_elements[start..end])
431            }
432            _ => None,
433        }
434    }
435
436    pub fn get_array_elements_info(&self, id: AstNodeId) -> Option<(u16, u16, &[AstNodeId])> {
437        match self.get(id)? {
438            AstNodeData::Array { rows, cols, .. } => {
439                let elements = self.get_array_elements(id)?;
440                Some((*rows, *cols, elements))
441            }
442            _ => None,
443        }
444    }
445
446    /// Resolve a string ID to its content
447    pub fn resolve_string(&self, id: StringId) -> &str {
448        self.strings.resolve(id)
449    }
450
451    /// Get the string interner (for external use)
452    pub fn strings(&self) -> &StringInterner {
453        &self.strings
454    }
455
456    /// Get mutable access to the string interner
457    pub fn strings_mut(&mut self) -> &mut StringInterner {
458        &mut self.strings
459    }
460
461    pub fn intern_table_specifier(&mut self, specifier: &TableSpecifier) -> TableSpecId {
462        let hash = {
463            let mut hasher = DefaultHasher::new();
464            specifier.hash(&mut hasher);
465            hasher.finish()
466        };
467
468        if let Some(&id) = self.table_spec_dedup.get(&hash)
469            && self
470                .table_specs
471                .get(id.0 as usize)
472                .is_some_and(|existing| existing == specifier)
473        {
474            return id;
475        }
476
477        let id = TableSpecId(self.table_specs.len() as u32);
478        self.table_specs.push(specifier.clone());
479        self.table_spec_dedup.insert(hash, id);
480        id
481    }
482
483    pub fn resolve_table_specifier(&self, id: TableSpecId) -> Option<&TableSpecifier> {
484        self.table_specs.get(id.0 as usize)
485    }
486
487    /// Compute hash for a node
488    fn hash_node(&self, node: &AstNodeData) -> u64 {
489        let mut hasher = DefaultHasher::new();
490        node.hash(&mut hasher);
491        hasher.finish()
492    }
493
494    /// Get statistics about the arena
495    pub fn stats(&self) -> AstArenaStats {
496        AstArenaStats {
497            node_count: self.nodes.len(),
498            dedup_hits: self.dedup_hits,
499            string_count: self.strings.len(),
500            table_spec_count: self.table_specs.len(),
501            total_args: self.function_args.len(),
502            total_array_elements: self.array_elements.len(),
503        }
504    }
505
506    /// Returns memory usage in bytes (approximate)
507    pub fn memory_usage(&self) -> usize {
508        self.nodes.capacity() * std::mem::size_of::<AstNodeEntry>()
509            + self.dedup_map.capacity() * (8 + 4) // hash + id
510            + self.function_args.capacity() * 4
511            + self.array_elements.capacity() * 4
512            + self.strings.memory_usage()
513            + self.table_specs.capacity() * std::mem::size_of::<TableSpecifier>()
514            + self.table_spec_dedup.capacity() * (8 + 4)
515    }
516
517    /// Clear all nodes from the arena
518    pub fn clear(&mut self) {
519        self.nodes.clear();
520        self.dedup_map.clear();
521        self.function_args.clear();
522        self.array_elements.clear();
523        self.strings.clear();
524        self.table_specs.clear();
525        self.table_spec_dedup.clear();
526        self.dedup_hits = 0;
527    }
528}
529
530impl Default for AstArena {
531    fn default() -> Self {
532        Self::new()
533    }
534}
535
536impl fmt::Debug for AstArena {
537    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
538        f.debug_struct("AstArena")
539            .field("nodes", &self.nodes.len())
540            .field("dedup_hits", &self.dedup_hits)
541            .field("strings", &self.strings.len())
542            .finish()
543    }
544}
545
546/// Statistics about the AST arena
547#[derive(Debug, Clone)]
548pub struct AstArenaStats {
549    pub node_count: usize,
550    pub dedup_hits: usize,
551    pub string_count: usize,
552    pub table_spec_count: usize,
553    pub total_args: usize,
554    pub total_array_elements: usize,
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560
561    #[test]
562    fn test_ast_arena_literal() {
563        let mut arena = AstArena::new();
564
565        let lit1 = arena.insert_literal(ValueRef::small_int(42).unwrap());
566        let lit2 = arena.insert_literal(ValueRef::boolean(true));
567
568        assert_ne!(lit1, lit2);
569
570        match arena.get(lit1) {
571            Some(AstNodeData::Literal(v)) => {
572                assert_eq!(v.as_small_int(), Some(42));
573            }
574            _ => panic!("Expected literal node"),
575        }
576    }
577
578    #[test]
579    fn test_ast_arena_deduplication() {
580        let mut arena = AstArena::new();
581
582        // Insert same literal twice
583        let lit1 = arena.insert_literal(ValueRef::small_int(42).unwrap());
584        let lit2 = arena.insert_literal(ValueRef::small_int(42).unwrap());
585
586        assert_eq!(lit1, lit2); // Should be deduplicated
587        assert_eq!(arena.stats().dedup_hits, 1);
588    }
589
590    #[test]
591    fn test_ast_arena_binary_op() {
592        let mut arena = AstArena::new();
593
594        let left = arena.insert_literal(ValueRef::small_int(1).unwrap());
595        let right = arena.insert_literal(ValueRef::small_int(2).unwrap());
596        let add = arena.insert_binary_op("+", left, right);
597
598        match arena.get(add) {
599            Some(AstNodeData::BinaryOp {
600                op_id,
601                left_id,
602                right_id,
603            }) => {
604                assert_eq!(arena.resolve_string(*op_id), "+");
605                assert_eq!(*left_id, left);
606                assert_eq!(*right_id, right);
607            }
608            _ => panic!("Expected binary op node"),
609        }
610    }
611
612    #[test]
613    fn test_ast_arena_function() {
614        let mut arena = AstArena::new();
615
616        let arg1 = arena.insert_literal(ValueRef::small_int(10).unwrap());
617        let arg2 = arena.insert_literal(ValueRef::small_int(20).unwrap());
618        let arg3 = arena.insert_literal(ValueRef::small_int(30).unwrap());
619
620        let func = arena.insert_function("SUM", vec![arg1, arg2, arg3]);
621
622        match arena.get(func) {
623            Some(AstNodeData::Function {
624                name_id,
625                args_count,
626                ..
627            }) => {
628                assert_eq!(arena.resolve_string(*name_id), "SUM");
629                assert_eq!(*args_count, 3);
630            }
631            _ => panic!("Expected function node"),
632        }
633
634        let args = arena.get_function_args(func).unwrap();
635        assert_eq!(args, &[arg1, arg2, arg3]);
636    }
637
638    #[test]
639    fn test_ast_arena_structural_sharing() {
640        let mut arena = AstArena::new();
641
642        // Create "A1" reference that will be shared
643        let a1_ref = arena.insert_reference(
644            "A1",
645            CompactRefType::Cell {
646                sheet: None,
647                row: 1,
648                col: 1,
649                row_abs: false,
650                col_abs: false,
651            },
652        );
653
654        // Create "A1 + 1"
655        let one = arena.insert_literal(ValueRef::small_int(1).unwrap());
656        let expr1 = arena.insert_binary_op("+", a1_ref, one);
657
658        // Create "A1 * 2"
659        let two = arena.insert_literal(ValueRef::small_int(2).unwrap());
660        let expr2 = arena.insert_binary_op("*", a1_ref, two);
661
662        // A1 reference should be shared
663        assert_eq!(arena.stats().node_count, 5); // A1, 1, +expr, 2, *expr
664
665        // Try to insert A1 again - should be deduplicated
666        let a1_ref2 = arena.insert_reference(
667            "A1",
668            CompactRefType::Cell {
669                sheet: None,
670                row: 1,
671                col: 1,
672                row_abs: false,
673                col_abs: false,
674            },
675        );
676        assert_eq!(a1_ref, a1_ref2);
677    }
678
679    #[test]
680    fn test_ast_arena_array() {
681        let mut arena = AstArena::new();
682
683        let elements = vec![
684            arena.insert_literal(ValueRef::small_int(1).unwrap()),
685            arena.insert_literal(ValueRef::small_int(2).unwrap()),
686            arena.insert_literal(ValueRef::small_int(3).unwrap()),
687            arena.insert_literal(ValueRef::small_int(4).unwrap()),
688        ];
689
690        let array = arena.insert_array(2, 2, elements.clone());
691
692        match arena.get(array) {
693            Some(AstNodeData::Array { rows, cols, .. }) => {
694                assert_eq!(*rows, 2);
695                assert_eq!(*cols, 2);
696            }
697            _ => panic!("Expected array node"),
698        }
699
700        let stored_elements = arena.get_array_elements(array).unwrap();
701        assert_eq!(stored_elements, &elements[..]);
702    }
703
704    #[test]
705    fn test_ast_arena_complex_expression() {
706        let mut arena = AstArena::new();
707
708        // Build: SUM(A1:A10) + IF(B1 > 0, C1, D1)
709
710        // A1:A10 range
711        let range = arena.insert_reference(
712            "A1:A10",
713            CompactRefType::Range {
714                sheet: None,
715                start_row: 1,
716                start_col: 1,
717                end_row: 10,
718                end_col: 1,
719                start_row_abs: false,
720                start_col_abs: false,
721                end_row_abs: false,
722                end_col_abs: false,
723            },
724        );
725
726        // SUM(A1:A10)
727        let sum = arena.insert_function("SUM", vec![range]);
728
729        // B1 reference
730        let b1 = arena.insert_reference(
731            "B1",
732            CompactRefType::Cell {
733                sheet: None,
734                row: 1,
735                col: 2,
736                row_abs: false,
737                col_abs: false,
738            },
739        );
740
741        // 0 literal
742        let zero = arena.insert_literal(ValueRef::small_int(0).unwrap());
743
744        // B1 > 0
745        let condition = arena.insert_binary_op(">", b1, zero);
746
747        // C1 and D1 references
748        let c1 = arena.insert_reference(
749            "C1",
750            CompactRefType::Cell {
751                sheet: None,
752                row: 1,
753                col: 3,
754                row_abs: false,
755                col_abs: false,
756            },
757        );
758        let d1 = arena.insert_reference(
759            "D1",
760            CompactRefType::Cell {
761                sheet: None,
762                row: 1,
763                col: 4,
764                row_abs: false,
765                col_abs: false,
766            },
767        );
768
769        // IF(B1 > 0, C1, D1)
770        let if_expr = arena.insert_function("IF", vec![condition, c1, d1]);
771
772        // Final: SUM(...) + IF(...)
773        let final_expr = arena.insert_binary_op("+", sum, if_expr);
774
775        // Verify structure
776        assert!(arena.get(final_expr).is_some());
777        // Note: zero literal gets deduplicated if used multiple times
778        // We have: range, sum, b1, zero, condition(>), c1, d1, if_expr, final_expr(+)
779        // That's 9 unique nodes (zero is deduplicated)
780        assert_eq!(arena.stats().node_count, 9); // All unique nodes except deduplicated zero
781    }
782
783    #[test]
784    fn test_ast_arena_string_deduplication() {
785        let mut arena = AstArena::new();
786
787        // Use same operator multiple times
788        let one = arena.insert_literal(ValueRef::small_int(1).unwrap());
789        let two = arena.insert_literal(ValueRef::small_int(2).unwrap());
790        let three = arena.insert_literal(ValueRef::small_int(3).unwrap());
791
792        let add1 = arena.insert_binary_op("+", one, two);
793        let add2 = arena.insert_binary_op("+", two, three);
794        let add3 = arena.insert_binary_op("+", one, three);
795
796        // "+" should be interned only once
797        assert_eq!(arena.strings().len(), 1);
798    }
799
800    #[test]
801    fn test_ast_arena_clear() {
802        let mut arena = AstArena::new();
803
804        arena.insert_literal(ValueRef::small_int(1).unwrap());
805        arena.insert_literal(ValueRef::small_int(2).unwrap());
806        let left = arena.insert_literal(ValueRef::small_int(3).unwrap());
807        let right = arena.insert_literal(ValueRef::small_int(4).unwrap());
808        arena.insert_binary_op("+", left, right);
809
810        assert_eq!(arena.stats().node_count, 5);
811
812        arena.clear();
813
814        assert_eq!(arena.stats().node_count, 0);
815        assert_eq!(arena.strings().len(), 0);
816    }
817}