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        self.edges.add_vertex(addr, vertex_id.0);
170        vertex_id
171    }
172
173    pub(super) fn allocate_name_vertex(&mut self, scope: NameScope) -> VertexId {
174        // Scope is lookup metadata, not an address: a workbook-scoped name is not a
175        // resident of the default sheet.
176        let scope_sheet_id = match scope {
177            NameScope::Sheet(id) => id,
178            NameScope::Workbook => self.default_sheet_id,
179        };
180        let vertex_id = self.allocate_symbol_vertex(VertexKind::NamedScalar, scope_sheet_id);
181        self.mark_vertex_dirty(vertex_id);
182        vertex_id
183    }
184
185    // Named Range Methods
186
187    pub(crate) fn validate_define_name(
188        &self,
189        name: &str,
190        scope: NameScope,
191    ) -> Result<(), ExcelError> {
192        if !is_valid_excel_name(name) {
193            return Err(
194                ExcelError::new(ExcelErrorKind::Name).with_message(format!("Invalid name: {name}"))
195            );
196        }
197
198        let lookup_key = self.name_lookup_key(name);
199        match scope {
200            NameScope::Workbook => {
201                if let Some(existing) = self.named_ranges_lookup.get(&lookup_key) {
202                    return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
203                        "Name collision under normalization: '{name}' conflicts with '{existing}'"
204                    )));
205                }
206            }
207            NameScope::Sheet(sheet_id) => {
208                if let Some(existing) = self.sheet_named_ranges_lookup.get(&(sheet_id, lookup_key))
209                {
210                    return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
211                        "Name collision under normalization in sheet: '{name}' conflicts with '{existing}'"
212                    )));
213                }
214            }
215        }
216        Ok(())
217    }
218
219    pub(crate) fn validate_existing_name(
220        &self,
221        name: &str,
222        scope: NameScope,
223    ) -> Result<(), ExcelError> {
224        self.canonical_name_in_scope(scope, name)
225            .map(|_| ())
226            .ok_or_else(|| {
227                ExcelError::new(ExcelErrorKind::Name)
228                    .with_message(format!("Name not found: {name}"))
229            })
230    }
231
232    /// Define a new named range
233    pub fn define_name(
234        &mut self,
235        name: &str,
236        definition: NamedDefinition,
237        scope: NameScope,
238    ) -> Result<(), ExcelError> {
239        self.validate_define_name(name, scope)?;
240
241        let mut final_definition = definition;
242        // Extract dependencies if formula
243        if let NamedDefinition::Formula { ref ast, .. } = final_definition {
244            let (deps, range_deps, _, _) = self.extract_dependencies(
245                ast,
246                match scope {
247                    NameScope::Sheet(id) => id,
248                    NameScope::Workbook => self.default_sheet_id,
249                },
250            )?;
251            final_definition = NamedDefinition::Formula {
252                ast: ast.clone(),
253                dependencies: deps,
254                range_deps,
255            };
256        }
257
258        // Allocate vertex only after dependency extraction succeeds
259        let vertex_id = self.allocate_name_vertex(scope);
260
261        let named_range = NamedRange {
262            definition: final_definition,
263            scope,
264            dependents: FxHashSet::default(),
265            vertex: vertex_id,
266        };
267
268        if matches!(named_range.definition, NamedDefinition::Range(_)) {
269            self.store.set_kind(vertex_id, VertexKind::NamedArray);
270        } else {
271            self.store.set_kind(vertex_id, VertexKind::NamedScalar);
272        }
273
274        // Formula dependencies are re-extracted here to share registration with update/reindex paths.
275        let referenced_names =
276            self.rebuild_name_dependencies(vertex_id, &named_range.definition, scope)?;
277        if !referenced_names.is_empty() {
278            self.attach_vertex_to_names(vertex_id, &referenced_names);
279        }
280
281        let key = name.to_string();
282
283        match scope {
284            NameScope::Workbook => {
285                self.named_ranges.insert(key.clone(), named_range);
286                self.named_ranges_lookup
287                    .insert(self.name_lookup_key(&key), key.clone());
288            }
289            NameScope::Sheet(id) => {
290                self.sheet_named_ranges
291                    .insert((id, key.clone()), named_range);
292                self.sheet_named_ranges_lookup
293                    .insert((id, self.name_lookup_key(&key)), key.clone());
294            }
295        }
296
297        self.name_vertex_lookup.insert(vertex_id, (scope, key));
298        self.resolve_pending_name_references(scope, name);
299        self.bump_symbol_revision();
300
301        Ok(())
302    }
303
304    /// Iterate workbook-scoped named ranges (for bindings/testing)
305    pub fn named_ranges_iter(&self) -> impl Iterator<Item = (&String, &NamedRange)> {
306        self.named_ranges.iter()
307    }
308
309    /// Iterate sheet-scoped named ranges (for bindings/testing)
310    pub fn sheet_named_ranges_iter(
311        &self,
312    ) -> impl Iterator<Item = (&(SheetId, String), &NamedRange)> {
313        self.sheet_named_ranges.iter()
314    }
315
316    /// Resolve a name in an explicit [`NameScope`].
317    ///
318    /// [`NameScope::Sheet`] looks in that sheet's names first and falls back to
319    /// workbook scope, matching Excel's shadowing rules. [`NameScope::Workbook`]
320    /// looks in workbook-scoped names **only**: a sheet-scoped name is invisible
321    /// to a workbook-scope query even when it is scoped to the default sheet.
322    ///
323    /// This is the single owned derivation for name scoping. A caller with no
324    /// sheet context asks for [`NameScope::Workbook`], never for the default
325    /// sheet's scope - substituting the default sheet for missing context is
326    /// what leaked references onto unrelated sheets in issue #110.
327    pub fn resolve_name_entry_in_scope(&self, name: &str, scope: NameScope) -> Option<&NamedRange> {
328        let workbook_entry = || {
329            if self.config.case_sensitive_names {
330                self.named_ranges.get(name)
331            } else {
332                self.named_ranges_lookup
333                    .get(&self.name_lookup_key(name))
334                    .and_then(|canon| self.named_ranges.get(canon))
335            }
336        };
337
338        match scope {
339            NameScope::Workbook => workbook_entry(),
340            NameScope::Sheet(current_sheet) => {
341                if self.config.case_sensitive_names {
342                    self.sheet_named_ranges
343                        .get(&(current_sheet, name.to_string()))
344                        .or_else(workbook_entry)
345                } else {
346                    let key = self.name_lookup_key(name);
347                    self.sheet_named_ranges_lookup
348                        .get(&(current_sheet, key))
349                        .and_then(|canon| {
350                            self.sheet_named_ranges.get(&(current_sheet, canon.clone()))
351                        })
352                        .or_else(workbook_entry)
353                }
354            }
355        }
356    }
357
358    /// Resolve a name as seen from `current_sheet`: sheet scope shadows
359    /// workbook scope. Equivalent to [`Self::resolve_name_entry_in_scope`] with
360    /// [`NameScope::Sheet`].
361    pub fn resolve_name_entry(&self, name: &str, current_sheet: SheetId) -> Option<&NamedRange> {
362        self.resolve_name_entry_in_scope(name, NameScope::Sheet(current_sheet))
363    }
364
365    /// Resolve a named range to its definition
366    pub fn resolve_name(&self, name: &str, current_sheet: SheetId) -> Option<&NamedDefinition> {
367        self.resolve_name_entry(name, current_sheet)
368            .map(|nr| &nr.definition)
369    }
370
371    /// The folded lookup key (see [`Self::name_lookup_key`]) of the name
372    /// represented by `vertex`, if it is a name vertex. Used by SCC tasks for
373    /// deterministic member ordering and live name-read matching (RFC #112).
374    pub(crate) fn name_key_for_vertex(&self, vertex: VertexId) -> Option<String> {
375        self.name_vertex_lookup
376            .get(&vertex)
377            .map(|(_, name)| self.name_lookup_key(name))
378    }
379
380    pub fn named_range_by_vertex(&self, vertex: VertexId) -> Option<&NamedRange> {
381        self.name_vertex_lookup
382            .get(&vertex)
383            .and_then(|(scope, name)| match scope {
384                NameScope::Workbook => self.named_ranges.get(name),
385                NameScope::Sheet(sheet_id) => {
386                    self.sheet_named_ranges.get(&(*sheet_id, name.clone()))
387                }
388            })
389    }
390
391    /// Update an existing named range definition
392    pub fn update_name(
393        &mut self,
394        name: &str,
395        new_definition: NamedDefinition,
396        scope: NameScope,
397    ) -> Result<(), ExcelError> {
398        let Some(canon_name) = self.canonical_name_in_scope(scope, name) else {
399            return Err(ExcelError::new(ExcelErrorKind::Name)
400                .with_message(format!("Name not found: {name}")));
401        };
402
403        // First collect dependents to avoid borrow checker issues
404        let dependents_to_dirty = match scope {
405            NameScope::Workbook => self
406                .named_ranges
407                .get(&canon_name)
408                .map(|nr| nr.dependents.iter().copied().collect::<Vec<_>>()),
409            NameScope::Sheet(id) => self
410                .sheet_named_ranges
411                .get(&(id, canon_name.clone()))
412                .map(|nr| nr.dependents.iter().copied().collect::<Vec<_>>()),
413        };
414
415        if let Some(dependents) = dependents_to_dirty {
416            // Dirty every dependent WITH propagation (#365). A dependent may
417            // itself be a formula-backed name vertex; everything downstream of
418            // it has to recompute against the new binding. A non-propagating
419            // mark stops after one hop and leaves cells that read the
420            // dependent name serving stale values.
421            self.mark_dirty_many(&dependents);
422
423            // Now update the definition
424            let named_range = match scope {
425                NameScope::Workbook => self.named_ranges.get_mut(&canon_name),
426                NameScope::Sheet(id) => self.sheet_named_ranges.get_mut(&(id, canon_name.clone())),
427            };
428
429            let mut update_data: Option<(VertexId, NameScope, NamedDefinition, bool)> = None;
430            if let Some(named_range) = named_range {
431                named_range.definition = new_definition;
432                let is_range = matches!(named_range.definition, NamedDefinition::Range(_));
433                update_data = Some((
434                    named_range.vertex,
435                    named_range.scope,
436                    named_range.definition.clone(),
437                    is_range,
438                ));
439            }
440
441            if let Some((vertex, scope_value, definition_snapshot, is_range)) = update_data {
442                self.detach_vertex_from_names(vertex);
443
444                if is_range {
445                    self.store.set_kind(vertex, VertexKind::NamedArray);
446                } else {
447                    self.store.set_kind(vertex, VertexKind::NamedScalar);
448                }
449
450                let referenced_names =
451                    self.rebuild_name_dependencies(vertex, &definition_snapshot, scope_value)?;
452                if !referenced_names.is_empty() {
453                    self.attach_vertex_to_names(vertex, &referenced_names);
454                }
455                // Propagate from the rebound name vertex itself, after its
456                // edges are current, so the transitive closure is reached.
457                self.mark_dirty_many(&[vertex]);
458            }
459
460            self.bump_symbol_revision();
461            Ok(())
462        } else {
463            Err(ExcelError::new(ExcelErrorKind::Name)
464                .with_message(format!("Name not found: {name}")))
465        }
466    }
467
468    /// Delete a named range
469    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), ExcelError> {
470        let Some(canon_name) = self.canonical_name_in_scope(scope, name) else {
471            return Err(ExcelError::new(ExcelErrorKind::Name)
472                .with_message(format!("Name not found: {name}")));
473        };
474
475        let named_range = match scope {
476            NameScope::Workbook => {
477                let removed = self.named_ranges.remove(&canon_name);
478                let key = self.name_lookup_key(&canon_name);
479                self.named_ranges_lookup.remove(&key);
480                removed
481            }
482            NameScope::Sheet(id) => {
483                let removed = self.sheet_named_ranges.remove(&(id, canon_name.clone()));
484                let key = self.name_lookup_key(&canon_name);
485                self.sheet_named_ranges_lookup.remove(&(id, key));
486                removed
487            }
488        };
489
490        if let Some(named_range) = named_range {
491            let mut affected: FxHashSet<VertexId> = FxHashSet::default();
492            for &vertex_id in &named_range.dependents {
493                affected.insert(vertex_id);
494            }
495            for (vertex_id, names) in self.vertex_to_names.iter() {
496                if names.contains(&named_range.vertex) {
497                    affected.insert(*vertex_id);
498                }
499            }
500            let formulas_to_rebuild = affected
501                .iter()
502                .filter(|&&vertex_id| self.get_cell_ref_for_vertex(vertex_id).is_some())
503                .filter_map(|&vertex_id| self.get_formula(vertex_id).map(|ast| (vertex_id, ast)))
504                .collect::<Vec<_>>();
505            // Symbol (name) vertices are excluded from `formulas_to_rebuild`
506            // by the cell-ref filter above, but a formula-backed name that
507            // referenced the deleted name needs the same treatment (#365):
508            // its dependency edges must be re-extracted so it re-resolves to
509            // #NAME? now and can be healed by a later define.
510            let names_to_rebuild = affected
511                .iter()
512                .filter(|&&vertex_id| vertex_id != named_range.vertex)
513                .filter_map(|&vertex_id| {
514                    self.named_range_by_vertex(vertex_id)
515                        .map(|nr| (vertex_id, nr.definition.clone(), nr.scope))
516                })
517                .collect::<Vec<_>>();
518            let dirty_sources = affected
519                .iter()
520                .copied()
521                .filter(|&vertex_id| vertex_id != named_range.vertex)
522                .collect::<Vec<_>>();
523            for vertex_id in affected {
524                if let Some(names) = self.vertex_to_names.get_mut(&vertex_id) {
525                    names.retain(|vid| *vid != named_range.vertex);
526                    if names.is_empty() {
527                        self.vertex_to_names.remove(&vertex_id);
528                    }
529                }
530            }
531            self.mark_named_vertex_deleted(&named_range);
532            // Re-extract cell-formula dependencies after the registry entry is gone. This
533            // preserves fallback-to-workbook resolution for a deleted sheet name and records
534            // an unresolved pending-name link otherwise, allowing a later define to heal the
535            // formula without requiring re-ingest.
536            for (vertex_id, ast) in formulas_to_rebuild {
537                self.rebuild_formula_dependencies(vertex_id, &ast);
538            }
539            for (vertex_id, definition, name_scope) in names_to_rebuild {
540                let referenced_names =
541                    self.rebuild_name_dependencies(vertex_id, &definition, name_scope)?;
542                if !referenced_names.is_empty() {
543                    self.attach_vertex_to_names(vertex_id, &referenced_names);
544                }
545            }
546            // Dirty the affected set WITH propagation, once every dependency
547            // edge above is current, so cells reading a formula-backed
548            // dependent name recompute instead of serving a cached value.
549            self.mark_dirty_many(&dirty_sources);
550            self.bump_symbol_revision();
551            Ok(())
552        } else {
553            Err(ExcelError::new(ExcelErrorKind::Name)
554                .with_message(format!("Name not found: {name}")))
555        }
556    }
557
558    pub(super) fn detach_vertex_from_names(&mut self, vertex: VertexId) {
559        if let Some(prior) = self.vertex_to_names.remove(&vertex) {
560            for name_vertex in prior {
561                if let Some((scope, name)) = self.name_vertex_lookup.get(&name_vertex).cloned() {
562                    match scope {
563                        NameScope::Workbook => {
564                            if let Some(entry) = self.named_ranges.get_mut(&name) {
565                                entry.dependents.remove(&vertex);
566                            }
567                        }
568                        NameScope::Sheet(sheet_id) => {
569                            if let Some(entry) =
570                                self.sheet_named_ranges.get_mut(&(sheet_id, name.clone()))
571                            {
572                                entry.dependents.remove(&vertex);
573                            }
574                        }
575                    }
576                }
577            }
578        }
579    }
580
581    pub(crate) fn attach_vertex_to_names(&mut self, vertex: VertexId, names: &[VertexId]) {
582        if names.is_empty() {
583            return;
584        }
585        let mut unique = FxHashSet::default();
586        let mut recorded = Vec::new();
587        for &name_vertex in names {
588            if !unique.insert(name_vertex) {
589                continue;
590            }
591            if let Some((scope, name)) = self.name_vertex_lookup.get(&name_vertex).cloned() {
592                match scope {
593                    NameScope::Workbook => {
594                        if let Some(entry) = self.named_ranges.get_mut(&name) {
595                            entry.dependents.insert(vertex);
596                        }
597                    }
598                    NameScope::Sheet(sheet_id) => {
599                        if let Some(entry) =
600                            self.sheet_named_ranges.get_mut(&(sheet_id, name.clone()))
601                        {
602                            entry.dependents.insert(vertex);
603                        }
604                    }
605                }
606                recorded.push(name_vertex);
607            }
608        }
609        if !recorded.is_empty() {
610            self.vertex_to_names.insert(vertex, recorded);
611        }
612    }
613
614    pub(super) fn unregister_name_cell_dependencies(&mut self, name_vertex: VertexId) {
615        if let Some(prev) = self.name_to_cell_dependencies.remove(&name_vertex) {
616            for dep in prev {
617                if let Some(set) = self.cell_to_name_dependents.get_mut(&dep) {
618                    set.remove(&name_vertex);
619                    if set.is_empty() {
620                        self.cell_to_name_dependents.remove(&dep);
621                    }
622                }
623            }
624        }
625    }
626
627    pub(super) fn register_name_cell_dependencies(
628        &mut self,
629        name_vertex: VertexId,
630        dependencies: &[VertexId],
631    ) {
632        self.unregister_name_cell_dependencies(name_vertex);
633        if dependencies.is_empty() {
634            return;
635        }
636        for dep in dependencies {
637            self.cell_to_name_dependents
638                .entry(*dep)
639                .or_default()
640                .insert(name_vertex);
641        }
642        self.name_to_cell_dependencies
643            .insert(name_vertex, dependencies.to_vec());
644    }
645
646    pub(crate) fn record_pending_name_reference(
647        &mut self,
648        sheet_id: SheetId,
649        name: &str,
650        formula_vertex: VertexId,
651    ) {
652        let key = self.name_lookup_key(name);
653        self.pending_name_links
654            .entry(key.clone())
655            .or_default()
656            .insert((sheet_id, formula_vertex));
657        self.vertex_to_pending_names
658            .entry(formula_vertex)
659            .or_default()
660            .insert(key);
661    }
662
663    pub(crate) fn clear_pending_name_references(&mut self, formula_vertex: VertexId) {
664        let Some(keys) = self.vertex_to_pending_names.remove(&formula_vertex) else {
665            return;
666        };
667
668        for key in keys {
669            let mut remove_key = false;
670            if let Some(entries) = self.pending_name_links.get_mut(&key) {
671                entries.retain(|(_, vertex_id)| *vertex_id != formula_vertex);
672                remove_key = entries.is_empty();
673            }
674            if remove_key {
675                self.pending_name_links.remove(&key);
676            }
677        }
678    }
679
680    pub(super) fn resolve_pending_name_references(&mut self, scope: NameScope, name: &str) {
681        let key = self.name_lookup_key(name);
682        if let Some(entries) = self.pending_name_links.remove(&key) {
683            for (sheet_id, formula_vertex) in entries {
684                let attach = match scope {
685                    NameScope::Workbook => true,
686                    NameScope::Sheet(expected) => expected == sheet_id,
687                };
688                if attach {
689                    if let Some(ast) = self.get_formula(formula_vertex) {
690                        self.rebuild_formula_dependencies(formula_vertex, &ast);
691                    } else {
692                        self.clear_pending_name_references(formula_vertex);
693                    }
694                } else {
695                    self.record_pending_name_reference(sheet_id, name, formula_vertex);
696                }
697            }
698        }
699    }
700
701    pub(super) fn name_depends_on_vertex(
702        &self,
703        name_vertex: VertexId,
704        target: VertexId,
705        visited: &mut FxHashSet<VertexId>,
706    ) -> bool {
707        if !visited.insert(name_vertex) {
708            return false;
709        }
710
711        for dependency in self.edges.out_edges(name_vertex).iter().copied() {
712            if dependency == target {
713                return true;
714            }
715
716            if matches!(
717                self.store.kind(dependency),
718                VertexKind::NamedScalar | VertexKind::NamedArray
719            ) && self.name_depends_on_vertex(dependency, target, visited)
720            {
721                return true;
722            }
723        }
724
725        false
726    }
727
728    pub(super) fn rebuild_name_dependencies(
729        &mut self,
730        vertex: VertexId,
731        definition: &NamedDefinition,
732        scope: NameScope,
733    ) -> Result<Vec<VertexId>, ExcelError> {
734        let formula_dependencies = if let NamedDefinition::Formula { ast, .. } = definition {
735            let current_sheet_id = match scope {
736                NameScope::Sheet(id) => id,
737                NameScope::Workbook => self.default_sheet_id,
738            };
739            let (dependencies, range_dependencies, _, _, _pending_names) =
740                self.extract_dependencies_with_pending_names(ast, current_sheet_id)?;
741            Some((dependencies, range_dependencies))
742        } else {
743            None
744        };
745
746        self.remove_dependent_edges(vertex);
747        self.unregister_name_cell_dependencies(vertex);
748
749        let mut dependencies: Vec<VertexId> = Vec::new();
750        let mut range_dependencies: Vec<SharedRangeRef<'static>> = Vec::new();
751        let mut placeholders = Vec::new();
752
753        match definition {
754            NamedDefinition::Cell(cell_ref) => {
755                let vertex_id = self.get_or_create_vertex(cell_ref, &mut placeholders);
756                dependencies.push(vertex_id);
757            }
758            NamedDefinition::Range(range_ref) => {
759                let height = range_ref
760                    .end
761                    .coord
762                    .row()
763                    .saturating_sub(range_ref.start.coord.row())
764                    + 1;
765                let width = range_ref
766                    .end
767                    .coord
768                    .col()
769                    .saturating_sub(range_ref.start.coord.col())
770                    + 1;
771                let size = (width * height) as usize;
772
773                if size <= self.config.range_expansion_limit {
774                    for row in range_ref.start.coord.row()..=range_ref.end.coord.row() {
775                        for col in range_ref.start.coord.col()..=range_ref.end.coord.col() {
776                            let coord = Coord::new(row, col, true, true);
777                            let addr = CellRef::new(range_ref.start.sheet_id, coord);
778                            let vertex_id = self.get_or_create_vertex(&addr, &mut placeholders);
779                            dependencies.push(vertex_id);
780                        }
781                    }
782                } else {
783                    let sheet_loc = SharedSheetLocator::Id(range_ref.start.sheet_id);
784                    let sr = formualizer_common::AxisBound::new(
785                        range_ref.start.coord.row(),
786                        range_ref.start.coord.row_abs(),
787                    );
788                    let sc = formualizer_common::AxisBound::new(
789                        range_ref.start.coord.col(),
790                        range_ref.start.coord.col_abs(),
791                    );
792                    let er = formualizer_common::AxisBound::new(
793                        range_ref.end.coord.row(),
794                        range_ref.end.coord.row_abs(),
795                    );
796                    let ec = formualizer_common::AxisBound::new(
797                        range_ref.end.coord.col(),
798                        range_ref.end.coord.col_abs(),
799                    );
800                    if let Ok(r) = SharedRangeRef::from_parts(
801                        sheet_loc,
802                        Some(sr),
803                        Some(sc),
804                        Some(er),
805                        Some(ec),
806                    ) {
807                        range_dependencies.push(r.into_owned());
808                    }
809                }
810            }
811            NamedDefinition::Literal(_) => {
812                // No dependencies.
813            }
814            NamedDefinition::Formula { .. } => {
815                let Some((formula_deps, range_deps)) = formula_dependencies else {
816                    return Err(ExcelError::new(ExcelErrorKind::Error)
817                        .with_message("Internal error: formula dependencies were not extracted"));
818                };
819                dependencies.extend(formula_deps);
820                range_dependencies.extend(range_deps);
821            }
822        }
823
824        if !dependencies.is_empty() {
825            self.add_dependent_edges(vertex, &dependencies);
826        }
827        self.register_name_cell_dependencies(vertex, &dependencies);
828
829        if !range_dependencies.is_empty() {
830            let sheet_id = match scope {
831                NameScope::Sheet(id) => id,
832                NameScope::Workbook => self.default_sheet_id,
833            };
834            self.add_range_dependent_edges(vertex, &range_dependencies, sheet_id);
835        }
836
837        Ok(dependencies
838            .iter()
839            .filter(|vid| {
840                matches!(
841                    self.store.kind(**vid),
842                    VertexKind::NamedScalar | VertexKind::NamedArray
843                )
844            })
845            .copied()
846            .collect())
847    }
848
849    pub fn adjust_named_ranges(
850        &mut self,
851        operation: &crate::engine::graph::editor::reference_adjuster::ShiftOperation,
852    ) -> Result<(), ExcelError> {
853        let adjuster = crate::engine::graph::editor::reference_adjuster::ReferenceAdjuster::new();
854
855        let changed = !self.named_ranges.is_empty() || !self.sheet_named_ranges.is_empty();
856        // Workbook-scoped formulas bind unqualified references to the default sheet.
857        let workbook_context =
858            crate::engine::graph::editor::reference_adjuster::ReferenceContext::new(
859                self.default_sheet_id,
860                &self.sheet_reg,
861            );
862        // Adjust cloned definitions first so a future fallible definition kind
863        // cannot leave the name table half-adjusted.
864        let mut adjusted_named_ranges = self.named_ranges.clone();
865        let mut adjusted_sheet_named_ranges = self.sheet_named_ranges.clone();
866        for named_range in adjusted_named_ranges.values_mut() {
867            adjust_named_definition(
868                &mut named_range.definition,
869                &adjuster,
870                operation,
871                &workbook_context,
872            )?;
873        }
874
875        // Sheet-scoped formulas bind unqualified references to their scope sheet.
876        for ((scope_sheet_id, _), named_range) in adjusted_sheet_named_ranges.iter_mut() {
877            let context = crate::engine::graph::editor::reference_adjuster::ReferenceContext::new(
878                *scope_sheet_id,
879                &self.sheet_reg,
880            );
881            adjust_named_definition(&mut named_range.definition, &adjuster, operation, &context)?;
882        }
883        let changed_names: Vec<_> = adjusted_named_ranges
884            .iter()
885            .filter_map(|(key, adjusted)| {
886                self.named_ranges
887                    .get(key)
888                    .is_some_and(|current| current.definition != adjusted.definition)
889                    .then_some((adjusted.vertex, adjusted.scope, adjusted.definition.clone()))
890            })
891            .chain(
892                adjusted_sheet_named_ranges
893                    .iter()
894                    .filter_map(|(key, adjusted)| {
895                        self.sheet_named_ranges
896                            .get(key)
897                            .is_some_and(|current| current.definition != adjusted.definition)
898                            .then_some((
899                                adjusted.vertex,
900                                adjusted.scope,
901                                adjusted.definition.clone(),
902                            ))
903                    }),
904            )
905            .collect();
906        self.named_ranges = adjusted_named_ranges;
907        self.sheet_named_ranges = adjusted_sheet_named_ranges;
908        for &(vertex, scope, ref definition) in &changed_names {
909            self.detach_vertex_from_names(vertex);
910            self.store.set_kind(
911                vertex,
912                if matches!(definition, NamedDefinition::Range(_)) {
913                    VertexKind::NamedArray
914                } else {
915                    VertexKind::NamedScalar
916                },
917            );
918            let referenced_names = self.rebuild_name_dependencies(vertex, definition, scope)?;
919            if !referenced_names.is_empty() {
920                self.attach_vertex_to_names(vertex, &referenced_names);
921            }
922        }
923        self.mark_dirty_many(
924            &changed_names
925                .iter()
926                .map(|(vertex, _, _)| *vertex)
927                .collect::<Vec<_>>(),
928        );
929        if changed {
930            self.bump_symbol_revision();
931        }
932
933        Ok(())
934    }
935
936    /// Mark a vertex as having a #NAME! error
937    pub fn mark_as_name_error(&mut self, vertex_id: VertexId) {
938        // Mark the vertex as dirty
939        self.mark_vertex_dirty(vertex_id);
940    }
941
942    pub(super) fn mark_named_vertex_deleted(&mut self, named_range: &NamedRange) {
943        self.detach_vertex_from_names(named_range.vertex);
944        self.remove_dependent_edges(named_range.vertex);
945        self.unregister_name_cell_dependencies(named_range.vertex);
946        self.store.mark_deleted(named_range.vertex, true);
947        self.vertex_values.remove(&named_range.vertex);
948        self.vertex_formulas.remove(&named_range.vertex);
949        self.clear_formula_vertex_dirty(named_range.vertex);
950        self.volatile_vertices.remove(&named_range.vertex);
951        self.vertex_to_names.remove(&named_range.vertex);
952        self.name_vertex_lookup.remove(&named_range.vertex);
953    }
954}