Skip to main content

formualizer_eval/engine/graph/
names.rs

1use super::*;
2use formualizer_common::parse_a1_1based;
3
4#[inline]
5fn normalize_name_key(name: &str) -> String {
6    name.to_lowercase()
7}
8
9/// Validate that a name conforms to Excel naming rules.
10fn is_valid_excel_name(name: &str) -> bool {
11    // Excel name rules:
12    // 1. Must start with a letter, underscore, or backslash
13    // 2. Can contain letters, numbers, periods, and underscores
14    // 3. Cannot be a cell reference (like A1, B2, etc.)
15    // 4. Cannot exceed 255 characters
16    // 5. Cannot contain spaces
17
18    if name.is_empty() || name.len() > 255 {
19        return false;
20    }
21
22    if parse_a1_1based(name).is_ok() {
23        return false;
24    }
25
26    let mut chars = name.chars();
27
28    // First character must be letter, underscore, or backslash
29    if let Some(first) = chars.next()
30        && !first.is_alphabetic()
31        && first != '_'
32        && first != '\\'
33    {
34        return false;
35    }
36
37    // Remaining characters must be letters, digits, periods, or underscores
38    for c in chars {
39        if !c.is_alphanumeric() && c != '.' && c != '_' {
40            return false;
41        }
42    }
43
44    true
45}
46
47/// Helper function to adjust a named definition during structural operations.
48///
49/// Named definitions track structural edits regardless of `$` anchors, matching
50/// formula references. Absolute markers affect copy/fill, not structural shifts.
51fn adjust_named_definition(
52    definition: &mut NamedDefinition,
53    adjuster: &crate::engine::graph::editor::reference_adjuster::ReferenceAdjuster,
54    operation: &crate::engine::graph::editor::reference_adjuster::ShiftOperation,
55    context: &crate::engine::graph::editor::reference_adjuster::ReferenceContext<'_>,
56) -> Result<(), ExcelError> {
57    use crate::engine::graph::editor::reference_adjuster::AbsShiftPolicy;
58    let mut invalidated = false;
59    match definition {
60        NamedDefinition::Cell(cell_ref) => {
61            if let Some(adjusted) =
62                adjuster.adjust_cell_ref_with_policy(cell_ref, operation, AbsShiftPolicy::Track)
63            {
64                *cell_ref = adjusted;
65            } else {
66                invalidated = true;
67            }
68        }
69        NamedDefinition::Range(range_ref) => {
70            let adjusted_start = adjuster.adjust_cell_ref_with_policy(
71                &range_ref.start,
72                operation,
73                AbsShiftPolicy::Track,
74            );
75            let adjusted_end = adjuster.adjust_cell_ref_with_policy(
76                &range_ref.end,
77                operation,
78                AbsShiftPolicy::Track,
79            );
80
81            if let (Some(start), Some(end)) = (adjusted_start, adjusted_end) {
82                range_ref.start = start;
83                range_ref.end = end;
84            } else {
85                invalidated = true;
86            }
87        }
88        NamedDefinition::Literal(_) => {
89            // Constant names are not affected by structural shifts.
90        }
91        NamedDefinition::Formula {
92            ast,
93            dependencies,
94            range_deps,
95        } => {
96            let adjusted_ast = adjuster.adjust_ast_with_policy_in_context(
97                ast,
98                operation,
99                AbsShiftPolicy::Track,
100                context,
101            );
102            *ast = adjusted_ast;
103
104            dependencies.clear();
105            range_deps.clear();
106        }
107    }
108    if invalidated {
109        *definition = NamedDefinition::Formula {
110            ast: formualizer_parse::parser::ASTNode::new(
111                formualizer_parse::parser::ASTNodeType::Literal(LiteralValue::Error(
112                    ExcelError::new(ExcelErrorKind::Ref),
113                )),
114                None,
115            ),
116            dependencies: Vec::new(),
117            range_deps: Vec::new(),
118        };
119    }
120    Ok(())
121}
122
123impl DependencyGraph {
124    #[inline]
125    pub(crate) fn name_lookup_key(&self, name: &str) -> String {
126        if self.config.case_sensitive_names {
127            name.to_string()
128        } else {
129            normalize_name_key(name)
130        }
131    }
132
133    fn canonical_name_in_scope(&self, scope: NameScope, name: &str) -> Option<String> {
134        let key = self.name_lookup_key(name);
135        match scope {
136            NameScope::Workbook => self.named_ranges_lookup.get(&key).cloned(),
137            NameScope::Sheet(sheet_id) => self
138                .sheet_named_ranges_lookup
139                .get(&(sheet_id, key))
140                .cloned(),
141        }
142    }
143
144    /// Allocate the next address in the symbol space.
145    ///
146    /// Symbols are identified by name and have no position. They used to be handed
147    /// fabricated grid coordinates on a real sheet, which let grid operations reach them
148    /// (#302, #304); a `SymbolAddr` is not a position and cannot be reached that way.
149    pub(super) fn next_symbol_addr(&mut self) -> VertexAddr {
150        let seq = self.symbol_vertex_seq;
151        self.symbol_vertex_seq = self.symbol_vertex_seq.wrapping_add(1);
152        VertexAddr::symbol(SymbolAddr::new(seq))
153    }
154
155    /// Allocate a vertex in the symbol address space.
156    ///
157    /// `scope_sheet_id` is recorded as lookup metadata only: it says which scope the symbol
158    /// answers queries for, never where it lives. Symbol vertices are absent from
159    /// `cell_to_vertex` and from every sheet index by construction, because they have no
160    /// grid address to key them by.
161    pub(super) fn allocate_symbol_vertex(
162        &mut self,
163        kind: VertexKind,
164        scope_sheet_id: SheetId,
165    ) -> VertexId {
166        let addr = self.next_symbol_addr();
167        let vertex_id = self.store.allocate(addr, scope_sheet_id, 0x01);
168        self.store.set_kind(vertex_id, kind);
169        #[cfg(any(test, feature = "legacy_oracle"))]
170        self.edges.add_vertex(addr, vertex_id.0);
171        vertex_id
172    }
173
174    pub(super) fn allocate_name_vertex(&mut self, scope: NameScope) -> VertexId {
175        // Scope is lookup metadata, not an address: a workbook-scoped name is not a
176        // resident of the default sheet.
177        let scope_sheet_id = match scope {
178            NameScope::Sheet(id) => id,
179            NameScope::Workbook => self.default_sheet_id,
180        };
181        let vertex_id = self.allocate_symbol_vertex(VertexKind::NamedScalar, scope_sheet_id);
182        self.mark_vertex_dirty(vertex_id);
183        vertex_id
184    }
185
186    // Named Range Methods
187
188    pub(crate) fn validate_define_name(
189        &self,
190        name: &str,
191        scope: NameScope,
192    ) -> Result<(), ExcelError> {
193        if !is_valid_excel_name(name) {
194            return Err(
195                ExcelError::new(ExcelErrorKind::Name).with_message(format!("Invalid name: {name}"))
196            );
197        }
198
199        let lookup_key = self.name_lookup_key(name);
200        match scope {
201            NameScope::Workbook => {
202                if let Some(existing) = self.named_ranges_lookup.get(&lookup_key) {
203                    return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
204                        "Name collision under normalization: '{name}' conflicts with '{existing}'"
205                    )));
206                }
207            }
208            NameScope::Sheet(sheet_id) => {
209                if let Some(existing) = self.sheet_named_ranges_lookup.get(&(sheet_id, lookup_key))
210                {
211                    return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
212                        "Name collision under normalization in sheet: '{name}' conflicts with '{existing}'"
213                    )));
214                }
215            }
216        }
217        Ok(())
218    }
219
220    pub(crate) fn validate_existing_name(
221        &self,
222        name: &str,
223        scope: NameScope,
224    ) -> Result<(), ExcelError> {
225        self.canonical_name_in_scope(scope, name)
226            .map(|_| ())
227            .ok_or_else(|| {
228                ExcelError::new(ExcelErrorKind::Name)
229                    .with_message(format!("Name not found: {name}"))
230            })
231    }
232
233    /// Define a new named range
234    pub fn define_name(
235        &mut self,
236        name: &str,
237        definition: NamedDefinition,
238        scope: NameScope,
239    ) -> Result<(), ExcelError> {
240        self.validate_define_name(name, scope)?;
241
242        let mut final_definition = definition;
243        // Extract dependencies if formula
244        if let NamedDefinition::Formula { ref ast, .. } = final_definition {
245            let (deps, range_deps, _, _) = self.extract_dependencies(
246                ast,
247                match scope {
248                    NameScope::Sheet(id) => id,
249                    NameScope::Workbook => self.default_sheet_id,
250                },
251            )?;
252            final_definition = NamedDefinition::Formula {
253                ast: ast.clone(),
254                dependencies: deps,
255                range_deps,
256            };
257        }
258
259        // Allocate vertex only after dependency extraction succeeds
260        let vertex_id = self.allocate_name_vertex(scope);
261
262        let named_range = NamedRange {
263            definition: final_definition,
264            scope,
265            #[cfg(any(test, feature = "legacy_oracle"))]
266            dependents: FxHashSet::default(),
267            vertex: vertex_id,
268        };
269
270        if matches!(named_range.definition, NamedDefinition::Range(_)) {
271            self.store.set_kind(vertex_id, VertexKind::NamedArray);
272        } else {
273            self.store.set_kind(vertex_id, VertexKind::NamedScalar);
274        }
275
276        // Formula dependencies are re-extracted here to share registration with update/reindex paths.
277        let referenced_names =
278            self.rebuild_name_dependencies(vertex_id, &named_range.definition, scope)?;
279        if !referenced_names.is_empty() {
280            self.attach_vertex_to_names(vertex_id, &referenced_names);
281        }
282
283        let key = name.to_string();
284
285        match scope {
286            NameScope::Workbook => {
287                self.named_ranges.insert(key.clone(), named_range);
288                self.named_ranges_lookup
289                    .insert(self.name_lookup_key(&key), key.clone());
290            }
291            NameScope::Sheet(id) => {
292                self.sheet_named_ranges
293                    .insert((id, key.clone()), named_range);
294                self.sheet_named_ranges_lookup
295                    .insert((id, self.name_lookup_key(&key)), key.clone());
296            }
297        }
298
299        self.name_vertex_lookup.insert(vertex_id, (scope, key));
300        // Logged before the pending readers re-bind: a sync they trigger
301        // gives the name its symbol node before extracting them.
302        self.authority_note_symbol(Some(vertex_id));
303        self.resolve_pending_name_references(scope, name);
304        self.bump_symbol_revision();
305
306        Ok(())
307    }
308
309    /// Iterate workbook-scoped named ranges (for bindings/testing)
310    pub fn named_ranges_iter(&self) -> impl Iterator<Item = (&String, &NamedRange)> {
311        self.named_ranges.iter()
312    }
313
314    /// Iterate sheet-scoped named ranges (for bindings/testing)
315    pub fn sheet_named_ranges_iter(
316        &self,
317    ) -> impl Iterator<Item = (&(SheetId, String), &NamedRange)> {
318        self.sheet_named_ranges.iter()
319    }
320
321    /// Resolve a name in an explicit [`NameScope`].
322    ///
323    /// [`NameScope::Sheet`] looks in that sheet's names first and falls back to
324    /// workbook scope, matching Excel's shadowing rules. [`NameScope::Workbook`]
325    /// looks in workbook-scoped names **only**: a sheet-scoped name is invisible
326    /// to a workbook-scope query even when it is scoped to the default sheet.
327    ///
328    /// This is the single owned derivation for name scoping. A caller with no
329    /// sheet context asks for [`NameScope::Workbook`], never for the default
330    /// sheet's scope - substituting the default sheet for missing context is
331    /// what leaked references onto unrelated sheets in issue #110.
332    pub fn resolve_name_entry_in_scope(&self, name: &str, scope: NameScope) -> Option<&NamedRange> {
333        let workbook_entry = || {
334            if self.config.case_sensitive_names {
335                self.named_ranges.get(name)
336            } else {
337                self.named_ranges_lookup
338                    .get(&self.name_lookup_key(name))
339                    .and_then(|canon| self.named_ranges.get(canon))
340            }
341        };
342
343        match scope {
344            NameScope::Workbook => workbook_entry(),
345            NameScope::Sheet(current_sheet) => {
346                if self.config.case_sensitive_names {
347                    self.sheet_named_ranges
348                        .get(&(current_sheet, name.to_string()))
349                        .or_else(workbook_entry)
350                } else {
351                    let key = self.name_lookup_key(name);
352                    self.sheet_named_ranges_lookup
353                        .get(&(current_sheet, key))
354                        .and_then(|canon| {
355                            self.sheet_named_ranges.get(&(current_sheet, canon.clone()))
356                        })
357                        .or_else(workbook_entry)
358                }
359            }
360        }
361    }
362
363    /// Resolve a name as seen from `current_sheet`: sheet scope shadows
364    /// workbook scope. Equivalent to [`Self::resolve_name_entry_in_scope`] with
365    /// [`NameScope::Sheet`].
366    pub fn resolve_name_entry(&self, name: &str, current_sheet: SheetId) -> Option<&NamedRange> {
367        self.resolve_name_entry_in_scope(name, NameScope::Sheet(current_sheet))
368    }
369
370    /// Resolve a named range to its definition
371    pub fn resolve_name(&self, name: &str, current_sheet: SheetId) -> Option<&NamedDefinition> {
372        self.resolve_name_entry(name, current_sheet)
373            .map(|nr| &nr.definition)
374    }
375
376    /// The folded lookup key (see [`Self::name_lookup_key`]) of the name
377    /// represented by `vertex`, if it is a name vertex. Used by SCC tasks for
378    /// deterministic member ordering and live name-read matching (RFC #112).
379    pub(crate) fn name_key_for_vertex(&self, vertex: VertexId) -> Option<String> {
380        self.name_vertex_lookup
381            .get(&vertex)
382            .map(|(_, name)| self.name_lookup_key(name))
383    }
384
385    pub fn named_range_by_vertex(&self, vertex: VertexId) -> Option<&NamedRange> {
386        self.name_vertex_lookup
387            .get(&vertex)
388            .and_then(|(scope, name)| match scope {
389                NameScope::Workbook => self.named_ranges.get(name),
390                NameScope::Sheet(sheet_id) => {
391                    self.sheet_named_ranges.get(&(*sheet_id, name.clone()))
392                }
393            })
394    }
395
396    /// Update an existing named range definition
397    pub fn update_name(
398        &mut self,
399        name: &str,
400        new_definition: NamedDefinition,
401        scope: NameScope,
402    ) -> Result<(), ExcelError> {
403        let Some(canon_name) = self.canonical_name_in_scope(scope, name) else {
404            return Err(ExcelError::new(ExcelErrorKind::Name)
405                .with_message(format!("Name not found: {name}")));
406        };
407
408        // First collect dependents to avoid borrow checker issues
409        let name_vertex = match scope {
410            NameScope::Workbook => self.named_ranges.get(&canon_name).map(|nr| nr.vertex),
411            NameScope::Sheet(id) => self
412                .sheet_named_ranges
413                .get(&(id, canon_name.clone()))
414                .map(|nr| nr.vertex),
415        };
416        // The name's direct readers (formulas and names), from the authority.
417        let dependents_to_dirty = name_vertex.map(|v| self.authority_symbol_readers(v));
418
419        if let Some(dependents) = dependents_to_dirty {
420            // Dirty every dependent WITH propagation (#365). A dependent may
421            // itself be a formula-backed name vertex; everything downstream of
422            // it has to recompute against the new binding. A non-propagating
423            // mark stops after one hop and leaves cells that read the
424            // dependent name serving stale values.
425            self.mark_dirty_many(&dependents);
426
427            // Now update the definition
428            let named_range = match scope {
429                NameScope::Workbook => self.named_ranges.get_mut(&canon_name),
430                NameScope::Sheet(id) => self.sheet_named_ranges.get_mut(&(id, canon_name.clone())),
431            };
432
433            let mut update_data: Option<(VertexId, NameScope, NamedDefinition, bool)> = None;
434            if let Some(named_range) = named_range {
435                named_range.definition = new_definition;
436                let is_range = matches!(named_range.definition, NamedDefinition::Range(_));
437                update_data = Some((
438                    named_range.vertex,
439                    named_range.scope,
440                    named_range.definition.clone(),
441                    is_range,
442                ));
443            }
444
445            if let Some((vertex, scope_value, definition_snapshot, is_range)) = update_data {
446                self.detach_vertex_from_names(vertex);
447
448                if is_range {
449                    self.store.set_kind(vertex, VertexKind::NamedArray);
450                } else {
451                    self.store.set_kind(vertex, VertexKind::NamedScalar);
452                }
453
454                let referenced_names =
455                    self.rebuild_name_dependencies(vertex, &definition_snapshot, scope_value)?;
456                if !referenced_names.is_empty() {
457                    self.attach_vertex_to_names(vertex, &referenced_names);
458                }
459                // Propagate from the rebound name vertex itself, after its
460                // edges are current, so the transitive closure is reached.
461                self.authority_note_symbol(Some(vertex));
462                self.mark_dirty_many(&[vertex]);
463            }
464
465            self.bump_symbol_revision();
466            Ok(())
467        } else {
468            Err(ExcelError::new(ExcelErrorKind::Name)
469                .with_message(format!("Name not found: {name}")))
470        }
471    }
472
473    /// Delete a named range
474    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), ExcelError> {
475        let Some(canon_name) = self.canonical_name_in_scope(scope, name) else {
476            return Err(ExcelError::new(ExcelErrorKind::Name)
477                .with_message(format!("Name not found: {name}")));
478        };
479
480        let named_range = match scope {
481            NameScope::Workbook => {
482                let removed = self.named_ranges.remove(&canon_name);
483                let key = self.name_lookup_key(&canon_name);
484                self.named_ranges_lookup.remove(&key);
485                removed
486            }
487            NameScope::Sheet(id) => {
488                let removed = self.sheet_named_ranges.remove(&(id, canon_name.clone()));
489                let key = self.name_lookup_key(&canon_name);
490                self.sheet_named_ranges_lookup.remove(&(id, key));
491                removed
492            }
493        };
494
495        if let Some(named_range) = named_range {
496            // The name's direct readers (formulas and names), from the
497            // authority (its symbol row is retired at the next sync).
498            let affected: FxHashSet<VertexId> = self
499                .authority_symbol_readers(named_range.vertex)
500                .into_iter()
501                .collect();
502            let formulas_to_rebuild = affected
503                .iter()
504                .filter(|&&vertex_id| self.get_cell_ref_for_vertex(vertex_id).is_some())
505                .filter_map(|&vertex_id| self.get_formula(vertex_id).map(|ast| (vertex_id, ast)))
506                .collect::<Vec<_>>();
507            // Symbol (name) vertices are excluded from `formulas_to_rebuild`
508            // by the cell-ref filter above, but a formula-backed name that
509            // referenced the deleted name needs the same treatment (#365):
510            // its dependency edges must be re-extracted so it re-resolves to
511            // #NAME? now and can be healed by a later define.
512            let names_to_rebuild = affected
513                .iter()
514                .filter(|&&vertex_id| vertex_id != named_range.vertex)
515                .filter_map(|&vertex_id| {
516                    self.named_range_by_vertex(vertex_id)
517                        .map(|nr| (vertex_id, nr.definition.clone(), nr.scope))
518                })
519                .collect::<Vec<_>>();
520            let dirty_sources = affected
521                .iter()
522                .copied()
523                .filter(|&vertex_id| vertex_id != named_range.vertex)
524                .collect::<Vec<_>>();
525            #[cfg(any(test, feature = "legacy_oracle"))]
526            for vertex_id in affected {
527                if let Some(names) = self.vertex_to_names.get_mut(&vertex_id) {
528                    names.retain(|vid| *vid != named_range.vertex);
529                    if names.is_empty() {
530                        self.vertex_to_names.remove(&vertex_id);
531                    }
532                }
533            }
534            self.mark_named_vertex_deleted(&named_range);
535            // Re-extract cell-formula dependencies after the registry entry is gone. This
536            // preserves fallback-to-workbook resolution for a deleted sheet name and records
537            // an unresolved pending-name link otherwise, allowing a later define to heal the
538            // formula without requiring re-ingest.
539            for (vertex_id, ast) in formulas_to_rebuild {
540                self.rebuild_formula_dependencies(vertex_id, &ast);
541            }
542            for (vertex_id, definition, name_scope) in names_to_rebuild {
543                let referenced_names =
544                    self.rebuild_name_dependencies(vertex_id, &definition, name_scope)?;
545                if !referenced_names.is_empty() {
546                    self.attach_vertex_to_names(vertex_id, &referenced_names);
547                }
548            }
549            // Dirty the affected set WITH propagation, once every dependency
550            // edge above is current, so cells reading a formula-backed
551            // dependent name recompute instead of serving a cached value.
552            self.authority_note_symbol(Some(named_range.vertex));
553            self.mark_dirty_many(&dirty_sources);
554            self.bump_symbol_revision();
555            Ok(())
556        } else {
557            Err(ExcelError::new(ExcelErrorKind::Name)
558                .with_message(format!("Name not found: {name}")))
559        }
560    }
561
562    pub(super) fn detach_vertex_from_names(&mut self, vertex: VertexId) {
563        #[cfg(not(any(test, feature = "legacy_oracle")))]
564        let _ = (&vertex,);
565        #[cfg(any(test, feature = "legacy_oracle"))]
566        {
567            if let Some(prior) = self.vertex_to_names.remove(&vertex) {
568                for name_vertex in prior {
569                    if let Some((scope, name)) = self.name_vertex_lookup.get(&name_vertex).cloned()
570                    {
571                        match scope {
572                            NameScope::Workbook => {
573                                if let Some(entry) = self.named_ranges.get_mut(&name) {
574                                    entry.dependents.remove(&vertex);
575                                }
576                            }
577                            NameScope::Sheet(sheet_id) => {
578                                if let Some(entry) =
579                                    self.sheet_named_ranges.get_mut(&(sheet_id, name.clone()))
580                                {
581                                    entry.dependents.remove(&vertex);
582                                }
583                            }
584                        }
585                    }
586                }
587            }
588        }
589    }
590
591    pub(crate) fn attach_vertex_to_names(&mut self, vertex: VertexId, names: &[VertexId]) {
592        #[cfg(not(any(test, feature = "legacy_oracle")))]
593        let _ = (&vertex, &names);
594        #[cfg(any(test, feature = "legacy_oracle"))]
595        {
596            if names.is_empty() {
597                return;
598            }
599            let mut unique = FxHashSet::default();
600            let mut recorded = Vec::new();
601            for &name_vertex in names {
602                if !unique.insert(name_vertex) {
603                    continue;
604                }
605                if let Some((scope, name)) = self.name_vertex_lookup.get(&name_vertex).cloned() {
606                    match scope {
607                        NameScope::Workbook => {
608                            if let Some(entry) = self.named_ranges.get_mut(&name) {
609                                entry.dependents.insert(vertex);
610                            }
611                        }
612                        NameScope::Sheet(sheet_id) => {
613                            if let Some(entry) =
614                                self.sheet_named_ranges.get_mut(&(sheet_id, name.clone()))
615                            {
616                                entry.dependents.insert(vertex);
617                            }
618                        }
619                    }
620                    recorded.push(name_vertex);
621                }
622            }
623            if !recorded.is_empty() {
624                self.vertex_to_names.insert(vertex, recorded);
625            }
626        }
627    }
628
629    pub(super) fn unregister_name_cell_dependencies(&mut self, name_vertex: VertexId) {
630        #[cfg(not(any(test, feature = "legacy_oracle")))]
631        let _ = (&name_vertex,);
632        #[cfg(any(test, feature = "legacy_oracle"))]
633        {
634            if let Some(prev) = self.name_to_cell_dependencies.remove(&name_vertex) {
635                for dep in prev {
636                    if let Some(set) = self.cell_to_name_dependents.get_mut(&dep) {
637                        set.remove(&name_vertex);
638                        if set.is_empty() {
639                            self.cell_to_name_dependents.remove(&dep);
640                        }
641                    }
642                }
643            }
644        }
645    }
646
647    pub(super) fn register_name_cell_dependencies(
648        &mut self,
649        name_vertex: VertexId,
650        dependencies: &[VertexId],
651    ) {
652        #[cfg(not(any(test, feature = "legacy_oracle")))]
653        let _ = (&name_vertex, &dependencies);
654        #[cfg(any(test, feature = "legacy_oracle"))]
655        {
656            self.unregister_name_cell_dependencies(name_vertex);
657            if dependencies.is_empty() {
658                return;
659            }
660            for dep in dependencies {
661                self.cell_to_name_dependents
662                    .entry(*dep)
663                    .or_default()
664                    .insert(name_vertex);
665            }
666            self.name_to_cell_dependencies
667                .insert(name_vertex, dependencies.to_vec());
668        }
669    }
670
671    pub(crate) fn record_pending_name_reference(
672        &mut self,
673        sheet_id: SheetId,
674        name: &str,
675        formula_vertex: VertexId,
676    ) {
677        self.materialize_vertex(formula_vertex);
678        let key = self.name_lookup_key(name);
679        self.pending_name_links
680            .entry(key.clone())
681            .or_default()
682            .insert((sheet_id, formula_vertex));
683        self.vertex_to_pending_names
684            .entry(formula_vertex)
685            .or_default()
686            .insert(key);
687    }
688
689    pub(crate) fn clear_pending_name_references(&mut self, formula_vertex: VertexId) {
690        let Some(keys) = self.vertex_to_pending_names.remove(&formula_vertex) else {
691            return;
692        };
693
694        for key in keys {
695            let mut remove_key = false;
696            if let Some(entries) = self.pending_name_links.get_mut(&key) {
697                entries.retain(|(_, vertex_id)| *vertex_id != formula_vertex);
698                remove_key = entries.is_empty();
699            }
700            if remove_key {
701                self.pending_name_links.remove(&key);
702            }
703        }
704    }
705
706    /// Pending-link key of an unbound sheet or table (`BestEffort`
707    /// preparation). The NUL prefix cannot occur in a valid defined name, so
708    /// it never collides with one; the name is folded like sheet and table
709    /// lookups are.
710    pub(crate) fn unbound_symbol_key(kind: &str, name: &str) -> String {
711        format!("\u{0}{kind}:{}", name.to_lowercase())
712    }
713
714    /// Re-bind formulas waiting on sheet or table `name` (`kind` is
715    /// `"sheet"` or `"table"`).
716    pub(crate) fn resolve_pending_symbol(&mut self, kind: &str, name: &str) {
717        if self.pending_name_links.is_empty() {
718            return;
719        }
720        let key = Self::unbound_symbol_key(kind, name);
721        self.resolve_pending_name_references(NameScope::Workbook, &key);
722    }
723
724    pub(super) fn resolve_pending_name_references(&mut self, scope: NameScope, name: &str) {
725        let key = self.name_lookup_key(name);
726        if let Some(entries) = self.pending_name_links.remove(&key) {
727            for (sheet_id, formula_vertex) in entries {
728                let attach = match scope {
729                    NameScope::Workbook => true,
730                    NameScope::Sheet(expected) => expected == sheet_id,
731                };
732                if attach {
733                    if let Some(ast) = self.get_formula(formula_vertex) {
734                        self.rebuild_formula_dependencies(formula_vertex, &ast);
735                    } else {
736                        self.clear_pending_name_references(formula_vertex);
737                    }
738                } else {
739                    self.record_pending_name_reference(sheet_id, name, formula_vertex);
740                }
741            }
742        }
743    }
744
745    /// Whether name `name_vertex` reads `target` directly or through other
746    /// names: the circular-name check at formula assignment. Re-derives the
747    /// edges legacy gave a name from its definition (a cell, a range within
748    /// `range_expansion_limit`, a formula's cell and name dependencies), so
749    /// it needs no stored dependency structure.
750    pub(super) fn name_depends_on_vertex(
751        &mut self,
752        name_vertex: VertexId,
753        target: VertexId,
754        visited: &mut FxHashSet<VertexId>,
755    ) -> bool {
756        if !visited.insert(name_vertex) {
757            return false;
758        }
759        let Some(entry) = self.named_range_by_vertex(name_vertex) else {
760            return false;
761        };
762        let (definition, scope) = (entry.definition.clone(), entry.scope);
763        let target_cell = self.get_cell_ref(target);
764        match definition {
765            NamedDefinition::Cell(cell_ref) => target_cell
766                .is_some_and(|t| t.sheet_id == cell_ref.sheet_id && t.coord == cell_ref.coord),
767            NamedDefinition::Range(range_ref) => {
768                let height = range_ref
769                    .end
770                    .coord
771                    .row()
772                    .saturating_sub(range_ref.start.coord.row())
773                    + 1;
774                let width = range_ref
775                    .end
776                    .coord
777                    .col()
778                    .saturating_sub(range_ref.start.coord.col())
779                    + 1;
780                let size = u64::from(width) * u64::from(height);
781                let limit = u64::try_from(self.config.range_expansion_limit).unwrap_or(u64::MAX);
782                size <= limit
783                    && target_cell.is_some_and(|t| {
784                        t.sheet_id == range_ref.start.sheet_id
785                            && (range_ref.start.coord.row()..=range_ref.end.coord.row())
786                                .contains(&t.coord.row())
787                            && (range_ref.start.coord.col()..=range_ref.end.coord.col())
788                                .contains(&t.coord.col())
789                    })
790            }
791            NamedDefinition::Literal(_) => false,
792            NamedDefinition::Formula { ast, .. } => {
793                let sheet = match scope {
794                    NameScope::Sheet(id) => id,
795                    NameScope::Workbook => self.default_sheet_id,
796                };
797                let Ok((dependencies, _, _, named, _)) =
798                    self.extract_dependencies_with_pending_names(&ast, sheet)
799                else {
800                    return false;
801                };
802                if dependencies.contains(&target) || named.contains(&target) {
803                    return true;
804                }
805                let mut nested: Vec<VertexId> = named;
806                nested.extend(dependencies.into_iter().filter(|&d| {
807                    matches!(
808                        self.store.kind(d),
809                        VertexKind::NamedScalar | VertexKind::NamedArray
810                    )
811                }));
812                nested
813                    .into_iter()
814                    .any(|n| self.name_depends_on_vertex(n, target, visited))
815            }
816        }
817    }
818
819    pub(super) fn rebuild_name_dependencies(
820        &mut self,
821        vertex: VertexId,
822        definition: &NamedDefinition,
823        scope: NameScope,
824    ) -> Result<Vec<VertexId>, ExcelError> {
825        let formula_dependencies = if let NamedDefinition::Formula { ast, .. } = definition {
826            let current_sheet_id = match scope {
827                NameScope::Sheet(id) => id,
828                NameScope::Workbook => self.default_sheet_id,
829            };
830            let (dependencies, range_dependencies, formula_vertexless, _, _pending_names) =
831                self.extract_dependencies_with_pending_names(ast, current_sheet_id)?;
832            Some((dependencies, range_dependencies, formula_vertexless))
833        } else {
834            None
835        };
836
837        self.remove_dependent_edges(vertex);
838        self.unregister_name_cell_dependencies(vertex);
839
840        let mut dependencies: Vec<VertexId> = Vec::new();
841        let mut range_dependencies: Vec<SharedRangeRef<'static>> = Vec::new();
842        // Referenced cells without a vertex (decision 27: references create
843        // none); counted as direct dependencies.
844        let mut vertexless: Vec<CellRef> = Vec::new();
845
846        match definition {
847            NamedDefinition::Cell(cell_ref) => match self.dep_vertex(cell_ref) {
848                Some(vertex_id) => dependencies.push(vertex_id),
849                None => vertexless.push(*cell_ref),
850            },
851            NamedDefinition::Range(range_ref) => {
852                let height = range_ref
853                    .end
854                    .coord
855                    .row()
856                    .saturating_sub(range_ref.start.coord.row())
857                    + 1;
858                let width = range_ref
859                    .end
860                    .coord
861                    .col()
862                    .saturating_sub(range_ref.start.coord.col())
863                    + 1;
864                // A whole sheet is 2^34 cells: the product must not wrap in
865                // u32, and the limit is compared before narrowing (usize is
866                // 32-bit on WASM).
867                let size = u64::from(width) * u64::from(height);
868                let limit = u64::try_from(self.config.range_expansion_limit).unwrap_or(u64::MAX);
869
870                if size <= limit {
871                    for row in range_ref.start.coord.row()..=range_ref.end.coord.row() {
872                        for col in range_ref.start.coord.col()..=range_ref.end.coord.col() {
873                            let coord = Coord::new(row, col, true, true);
874                            let addr = CellRef::new(range_ref.start.sheet_id, coord);
875                            match self.dep_vertex(&addr) {
876                                Some(vertex_id) => dependencies.push(vertex_id),
877                                None => vertexless.push(addr),
878                            }
879                        }
880                    }
881                } else {
882                    let sheet_loc = SharedSheetLocator::Id(range_ref.start.sheet_id);
883                    let sr = formualizer_common::AxisBound::new(
884                        range_ref.start.coord.row(),
885                        range_ref.start.coord.row_abs(),
886                    );
887                    let sc = formualizer_common::AxisBound::new(
888                        range_ref.start.coord.col(),
889                        range_ref.start.coord.col_abs(),
890                    );
891                    let er = formualizer_common::AxisBound::new(
892                        range_ref.end.coord.row(),
893                        range_ref.end.coord.row_abs(),
894                    );
895                    let ec = formualizer_common::AxisBound::new(
896                        range_ref.end.coord.col(),
897                        range_ref.end.coord.col_abs(),
898                    );
899                    if let Ok(r) = SharedRangeRef::from_parts(
900                        sheet_loc,
901                        Some(sr),
902                        Some(sc),
903                        Some(er),
904                        Some(ec),
905                    ) {
906                        range_dependencies.push(r.into_owned());
907                    }
908                }
909            }
910            NamedDefinition::Literal(_) => {
911                // No dependencies.
912            }
913            NamedDefinition::Formula { .. } => {
914                let Some((formula_deps, range_deps, formula_vertexless)) = formula_dependencies
915                else {
916                    return Err(ExcelError::new(ExcelErrorKind::Error)
917                        .with_message("Internal error: formula dependencies were not extracted"));
918                };
919                dependencies.extend(formula_deps);
920                range_dependencies.extend(range_deps);
921                vertexless.extend(formula_vertexless);
922            }
923        }
924
925        if !dependencies.is_empty() {
926            self.add_dependent_edges(vertex, &dependencies);
927        }
928        self.note_vertexless_deps(
929            vertex,
930            vertexless
931                .iter()
932                .map(|c| (c.sheet_id, c.coord.row(), c.coord.col())),
933        );
934        self.register_name_cell_dependencies(vertex, &dependencies);
935
936        if !range_dependencies.is_empty() {
937            let sheet_id = match scope {
938                NameScope::Sheet(id) => id,
939                NameScope::Workbook => self.default_sheet_id,
940            };
941            self.add_range_dependent_edges(vertex, &range_dependencies, sheet_id);
942        }
943
944        Ok(dependencies
945            .iter()
946            .filter(|vid| {
947                matches!(
948                    self.store.kind(**vid),
949                    VertexKind::NamedScalar | VertexKind::NamedArray
950                )
951            })
952            .copied()
953            .collect())
954    }
955
956    pub fn adjust_named_ranges(
957        &mut self,
958        operation: &crate::engine::graph::editor::reference_adjuster::ShiftOperation,
959    ) -> Result<(), ExcelError> {
960        let adjuster = crate::engine::graph::editor::reference_adjuster::ReferenceAdjuster::new();
961
962        let changed = !self.named_ranges.is_empty() || !self.sheet_named_ranges.is_empty();
963        // Workbook-scoped formulas bind unqualified references to the default sheet.
964        let workbook_context =
965            crate::engine::graph::editor::reference_adjuster::ReferenceContext::new(
966                self.default_sheet_id,
967                &self.sheet_reg,
968            );
969        // Adjust cloned definitions first so a future fallible definition kind
970        // cannot leave the name table half-adjusted.
971        let mut adjusted_named_ranges = self.named_ranges.clone();
972        let mut adjusted_sheet_named_ranges = self.sheet_named_ranges.clone();
973        for named_range in adjusted_named_ranges.values_mut() {
974            adjust_named_definition(
975                &mut named_range.definition,
976                &adjuster,
977                operation,
978                &workbook_context,
979            )?;
980        }
981
982        // Sheet-scoped formulas bind unqualified references to their scope sheet.
983        for ((scope_sheet_id, _), named_range) in adjusted_sheet_named_ranges.iter_mut() {
984            let context = crate::engine::graph::editor::reference_adjuster::ReferenceContext::new(
985                *scope_sheet_id,
986                &self.sheet_reg,
987            );
988            adjust_named_definition(&mut named_range.definition, &adjuster, operation, &context)?;
989        }
990        let changed_names: Vec<_> = adjusted_named_ranges
991            .iter()
992            .filter_map(|(key, adjusted)| {
993                self.named_ranges
994                    .get(key)
995                    .is_some_and(|current| current.definition != adjusted.definition)
996                    .then_some((adjusted.vertex, adjusted.scope, adjusted.definition.clone()))
997            })
998            .chain(
999                adjusted_sheet_named_ranges
1000                    .iter()
1001                    .filter_map(|(key, adjusted)| {
1002                        self.sheet_named_ranges
1003                            .get(key)
1004                            .is_some_and(|current| current.definition != adjusted.definition)
1005                            .then_some((
1006                                adjusted.vertex,
1007                                adjusted.scope,
1008                                adjusted.definition.clone(),
1009                            ))
1010                    }),
1011            )
1012            .collect();
1013        self.named_ranges = adjusted_named_ranges;
1014        self.sheet_named_ranges = adjusted_sheet_named_ranges;
1015        for &(vertex, scope, ref definition) in &changed_names {
1016            self.detach_vertex_from_names(vertex);
1017            self.store.set_kind(
1018                vertex,
1019                if matches!(definition, NamedDefinition::Range(_)) {
1020                    VertexKind::NamedArray
1021                } else {
1022                    VertexKind::NamedScalar
1023                },
1024            );
1025            let referenced_names = self.rebuild_name_dependencies(vertex, definition, scope)?;
1026            if !referenced_names.is_empty() {
1027                self.attach_vertex_to_names(vertex, &referenced_names);
1028            }
1029        }
1030        self.mark_dirty_many(
1031            &changed_names
1032                .iter()
1033                .map(|(vertex, _, _)| *vertex)
1034                .collect::<Vec<_>>(),
1035        );
1036        if changed {
1037            for &(vertex, _, _) in &changed_names {
1038                self.authority_note_symbol(Some(vertex));
1039            }
1040            self.bump_symbol_revision();
1041        }
1042
1043        Ok(())
1044    }
1045
1046    /// Mark a vertex as having a #NAME! error
1047    pub fn mark_as_name_error(&mut self, vertex_id: VertexId) {
1048        // Mark the vertex as dirty
1049        self.mark_vertex_dirty(vertex_id);
1050    }
1051
1052    pub(super) fn mark_named_vertex_deleted(&mut self, named_range: &NamedRange) {
1053        self.detach_vertex_from_names(named_range.vertex);
1054        self.remove_dependent_edges(named_range.vertex);
1055        self.unregister_name_cell_dependencies(named_range.vertex);
1056        self.store.mark_deleted(named_range.vertex, true);
1057        self.vertex_values.remove(&named_range.vertex);
1058        self.vertex_formulas.remove(&named_range.vertex);
1059        self.clear_formula_vertex_dirty(named_range.vertex);
1060        self.volatile_vertices.remove(&named_range.vertex);
1061        #[cfg(any(test, feature = "legacy_oracle"))]
1062        self.vertex_to_names.remove(&named_range.vertex);
1063        self.name_vertex_lookup.remove(&named_range.vertex);
1064    }
1065}