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 deliberately keep the legacy `Pin` policy for absolute
50/// anchors: formula reference adjustment now tracks absolute refs through
51/// structural shifts (issue #168), but flipping named-range definitions to
52/// the same semantics is a separate policy decision that has not been made —
53/// see `AbsShiftPolicy` and the #168 discussion.
54fn adjust_named_definition(
55    definition: &mut NamedDefinition,
56    adjuster: &crate::engine::graph::editor::reference_adjuster::ReferenceAdjuster,
57    operation: &crate::engine::graph::editor::reference_adjuster::ShiftOperation,
58    context: &crate::engine::graph::editor::reference_adjuster::ReferenceContext<'_>,
59) -> Result<(), ExcelError> {
60    use crate::engine::graph::editor::reference_adjuster::AbsShiftPolicy;
61    match definition {
62        NamedDefinition::Cell(cell_ref) => {
63            if let Some(adjusted) =
64                adjuster.adjust_cell_ref_with_policy(cell_ref, operation, AbsShiftPolicy::Pin)
65            {
66                *cell_ref = adjusted;
67            } else {
68                return Err(ExcelError::new(ExcelErrorKind::Ref));
69            }
70        }
71        NamedDefinition::Range(range_ref) => {
72            let adjusted_start = adjuster.adjust_cell_ref_with_policy(
73                &range_ref.start,
74                operation,
75                AbsShiftPolicy::Pin,
76            );
77            let adjusted_end = adjuster.adjust_cell_ref_with_policy(
78                &range_ref.end,
79                operation,
80                AbsShiftPolicy::Pin,
81            );
82
83            if let (Some(start), Some(end)) = (adjusted_start, adjusted_end) {
84                range_ref.start = start;
85                range_ref.end = end;
86            } else {
87                return Err(ExcelError::new(ExcelErrorKind::Ref));
88            }
89        }
90        NamedDefinition::Literal(_) => {
91            // Constant names are not affected by structural shifts.
92        }
93        NamedDefinition::Formula {
94            ast,
95            dependencies,
96            range_deps,
97        } => {
98            let adjusted_ast = adjuster.adjust_ast_with_policy_in_context(
99                ast,
100                operation,
101                AbsShiftPolicy::Pin,
102                context,
103            );
104            *ast = adjusted_ast;
105
106            dependencies.clear();
107            range_deps.clear();
108        }
109    }
110    Ok(())
111}
112
113impl DependencyGraph {
114    #[inline]
115    pub(crate) fn name_lookup_key(&self, name: &str) -> String {
116        if self.config.case_sensitive_names {
117            name.to_string()
118        } else {
119            normalize_name_key(name)
120        }
121    }
122
123    fn canonical_name_in_scope(&self, scope: NameScope, name: &str) -> Option<String> {
124        let key = self.name_lookup_key(name);
125        match scope {
126            NameScope::Workbook => self.named_ranges_lookup.get(&key).cloned(),
127            NameScope::Sheet(sheet_id) => self
128                .sheet_named_ranges_lookup
129                .get(&(sheet_id, key))
130                .cloned(),
131        }
132    }
133
134    fn next_name_coord(&mut self) -> AbsCoord {
135        let seq = self.name_vertex_seq;
136        self.name_vertex_seq = self.name_vertex_seq.wrapping_add(1);
137        let row = (seq / 16_384).min(0x000F_FFFF);
138        let col = seq % 16_384;
139        AbsCoord::new(row, col)
140    }
141
142    pub(super) fn allocate_name_vertex(&mut self, scope: NameScope) -> VertexId {
143        let coord = self.next_name_coord();
144        let sheet_id = match scope {
145            NameScope::Sheet(id) => id,
146            NameScope::Workbook => self.default_sheet_id,
147        };
148        let vertex_id = self.store.allocate(coord, sheet_id, 0x01);
149        self.store.set_kind(vertex_id, VertexKind::NamedScalar);
150        self.mark_vertex_dirty(vertex_id);
151        self.edges.add_vertex(coord, vertex_id.0);
152        vertex_id
153    }
154
155    // Named Range Methods
156
157    pub(crate) fn validate_define_name(
158        &self,
159        name: &str,
160        scope: NameScope,
161    ) -> Result<(), ExcelError> {
162        if !is_valid_excel_name(name) {
163            return Err(
164                ExcelError::new(ExcelErrorKind::Name).with_message(format!("Invalid name: {name}"))
165            );
166        }
167
168        let lookup_key = self.name_lookup_key(name);
169        match scope {
170            NameScope::Workbook => {
171                if let Some(existing) = self.named_ranges_lookup.get(&lookup_key) {
172                    return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
173                        "Name collision under normalization: '{name}' conflicts with '{existing}'"
174                    )));
175                }
176            }
177            NameScope::Sheet(sheet_id) => {
178                if let Some(existing) = self.sheet_named_ranges_lookup.get(&(sheet_id, lookup_key))
179                {
180                    return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
181                        "Name collision under normalization in sheet: '{name}' conflicts with '{existing}'"
182                    )));
183                }
184            }
185        }
186        Ok(())
187    }
188
189    pub(crate) fn validate_existing_name(
190        &self,
191        name: &str,
192        scope: NameScope,
193    ) -> Result<(), ExcelError> {
194        self.canonical_name_in_scope(scope, name)
195            .map(|_| ())
196            .ok_or_else(|| {
197                ExcelError::new(ExcelErrorKind::Name)
198                    .with_message(format!("Name not found: {name}"))
199            })
200    }
201
202    /// Define a new named range
203    pub fn define_name(
204        &mut self,
205        name: &str,
206        definition: NamedDefinition,
207        scope: NameScope,
208    ) -> Result<(), ExcelError> {
209        self.validate_define_name(name, scope)?;
210
211        let mut final_definition = definition;
212        // Extract dependencies if formula
213        if let NamedDefinition::Formula { ref ast, .. } = final_definition {
214            let (deps, range_deps, _, _) = self.extract_dependencies(
215                ast,
216                match scope {
217                    NameScope::Sheet(id) => id,
218                    NameScope::Workbook => self.default_sheet_id,
219                },
220            )?;
221            final_definition = NamedDefinition::Formula {
222                ast: ast.clone(),
223                dependencies: deps,
224                range_deps,
225            };
226        }
227
228        // Allocate vertex only after dependency extraction succeeds
229        let vertex_id = self.allocate_name_vertex(scope);
230
231        let named_range = NamedRange {
232            definition: final_definition,
233            scope,
234            dependents: FxHashSet::default(),
235            vertex: vertex_id,
236        };
237
238        if matches!(named_range.definition, NamedDefinition::Range(_)) {
239            self.store.set_kind(vertex_id, VertexKind::NamedArray);
240        } else {
241            self.store.set_kind(vertex_id, VertexKind::NamedScalar);
242        }
243
244        let referenced_names =
245            self.rebuild_name_dependencies(vertex_id, &named_range.definition, scope);
246        if !referenced_names.is_empty() {
247            self.attach_vertex_to_names(vertex_id, &referenced_names);
248        }
249
250        let key = name.to_string();
251
252        match scope {
253            NameScope::Workbook => {
254                self.named_ranges.insert(key.clone(), named_range);
255                self.named_ranges_lookup
256                    .insert(self.name_lookup_key(&key), key.clone());
257            }
258            NameScope::Sheet(id) => {
259                self.sheet_named_ranges
260                    .insert((id, key.clone()), named_range);
261                self.sheet_named_ranges_lookup
262                    .insert((id, self.name_lookup_key(&key)), key.clone());
263            }
264        }
265
266        self.name_vertex_lookup.insert(vertex_id, (scope, key));
267        self.resolve_pending_name_references(scope, name);
268        self.bump_symbol_revision();
269
270        Ok(())
271    }
272
273    /// Iterate workbook-scoped named ranges (for bindings/testing)
274    pub fn named_ranges_iter(&self) -> impl Iterator<Item = (&String, &NamedRange)> {
275        self.named_ranges.iter()
276    }
277
278    /// Iterate sheet-scoped named ranges (for bindings/testing)
279    pub fn sheet_named_ranges_iter(
280        &self,
281    ) -> impl Iterator<Item = (&(SheetId, String), &NamedRange)> {
282        self.sheet_named_ranges.iter()
283    }
284
285    /// Resolve a name in an explicit [`NameScope`].
286    ///
287    /// [`NameScope::Sheet`] looks in that sheet's names first and falls back to
288    /// workbook scope, matching Excel's shadowing rules. [`NameScope::Workbook`]
289    /// looks in workbook-scoped names **only**: a sheet-scoped name is invisible
290    /// to a workbook-scope query even when it is scoped to the default sheet.
291    ///
292    /// This is the single owned derivation for name scoping. A caller with no
293    /// sheet context asks for [`NameScope::Workbook`], never for the default
294    /// sheet's scope - substituting the default sheet for missing context is
295    /// what leaked references onto unrelated sheets in issue #110.
296    pub fn resolve_name_entry_in_scope(&self, name: &str, scope: NameScope) -> Option<&NamedRange> {
297        let workbook_entry = || {
298            if self.config.case_sensitive_names {
299                self.named_ranges.get(name)
300            } else {
301                self.named_ranges_lookup
302                    .get(&self.name_lookup_key(name))
303                    .and_then(|canon| self.named_ranges.get(canon))
304            }
305        };
306
307        match scope {
308            NameScope::Workbook => workbook_entry(),
309            NameScope::Sheet(current_sheet) => {
310                if self.config.case_sensitive_names {
311                    self.sheet_named_ranges
312                        .get(&(current_sheet, name.to_string()))
313                        .or_else(workbook_entry)
314                } else {
315                    let key = self.name_lookup_key(name);
316                    self.sheet_named_ranges_lookup
317                        .get(&(current_sheet, key))
318                        .and_then(|canon| {
319                            self.sheet_named_ranges.get(&(current_sheet, canon.clone()))
320                        })
321                        .or_else(workbook_entry)
322                }
323            }
324        }
325    }
326
327    /// Resolve a name as seen from `current_sheet`: sheet scope shadows
328    /// workbook scope. Equivalent to [`Self::resolve_name_entry_in_scope`] with
329    /// [`NameScope::Sheet`].
330    pub fn resolve_name_entry(&self, name: &str, current_sheet: SheetId) -> Option<&NamedRange> {
331        self.resolve_name_entry_in_scope(name, NameScope::Sheet(current_sheet))
332    }
333
334    /// Resolve a named range to its definition
335    pub fn resolve_name(&self, name: &str, current_sheet: SheetId) -> Option<&NamedDefinition> {
336        self.resolve_name_entry(name, current_sheet)
337            .map(|nr| &nr.definition)
338    }
339
340    /// The folded lookup key (see [`Self::name_lookup_key`]) of the name
341    /// represented by `vertex`, if it is a name vertex. Used by SCC tasks for
342    /// deterministic member ordering and live name-read matching (RFC #112).
343    pub(crate) fn name_key_for_vertex(&self, vertex: VertexId) -> Option<String> {
344        self.name_vertex_lookup
345            .get(&vertex)
346            .map(|(_, name)| self.name_lookup_key(name))
347    }
348
349    pub fn named_range_by_vertex(&self, vertex: VertexId) -> Option<&NamedRange> {
350        self.name_vertex_lookup
351            .get(&vertex)
352            .and_then(|(scope, name)| match scope {
353                NameScope::Workbook => self.named_ranges.get(name),
354                NameScope::Sheet(sheet_id) => {
355                    self.sheet_named_ranges.get(&(*sheet_id, name.clone()))
356                }
357            })
358    }
359
360    /// Update an existing named range definition
361    pub fn update_name(
362        &mut self,
363        name: &str,
364        new_definition: NamedDefinition,
365        scope: NameScope,
366    ) -> Result<(), ExcelError> {
367        let Some(canon_name) = self.canonical_name_in_scope(scope, name) else {
368            return Err(ExcelError::new(ExcelErrorKind::Name)
369                .with_message(format!("Name not found: {name}")));
370        };
371
372        // First collect dependents to avoid borrow checker issues
373        let dependents_to_dirty = match scope {
374            NameScope::Workbook => self
375                .named_ranges
376                .get(&canon_name)
377                .map(|nr| nr.dependents.iter().copied().collect::<Vec<_>>()),
378            NameScope::Sheet(id) => self
379                .sheet_named_ranges
380                .get(&(id, canon_name.clone()))
381                .map(|nr| nr.dependents.iter().copied().collect::<Vec<_>>()),
382        };
383
384        if let Some(dependents) = dependents_to_dirty {
385            // Mark all dependents as dirty
386            for vertex_id in dependents {
387                self.mark_vertex_dirty(vertex_id);
388            }
389
390            // Now update the definition
391            let named_range = match scope {
392                NameScope::Workbook => self.named_ranges.get_mut(&canon_name),
393                NameScope::Sheet(id) => self.sheet_named_ranges.get_mut(&(id, canon_name.clone())),
394            };
395
396            let mut update_data: Option<(VertexId, NameScope, NamedDefinition, bool)> = None;
397            if let Some(named_range) = named_range {
398                named_range.definition = new_definition;
399                let is_range = matches!(named_range.definition, NamedDefinition::Range(_));
400                update_data = Some((
401                    named_range.vertex,
402                    named_range.scope,
403                    named_range.definition.clone(),
404                    is_range,
405                ));
406            }
407
408            if let Some((vertex, scope_value, definition_snapshot, is_range)) = update_data {
409                self.detach_vertex_from_names(vertex);
410
411                if is_range {
412                    self.store.set_kind(vertex, VertexKind::NamedArray);
413                } else {
414                    self.store.set_kind(vertex, VertexKind::NamedScalar);
415                }
416                self.mark_vertex_dirty(vertex);
417
418                let referenced_names =
419                    self.rebuild_name_dependencies(vertex, &definition_snapshot, scope_value);
420                if !referenced_names.is_empty() {
421                    self.attach_vertex_to_names(vertex, &referenced_names);
422                }
423            }
424
425            self.bump_symbol_revision();
426            Ok(())
427        } else {
428            Err(ExcelError::new(ExcelErrorKind::Name)
429                .with_message(format!("Name not found: {name}")))
430        }
431    }
432
433    /// Delete a named range
434    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), ExcelError> {
435        let Some(canon_name) = self.canonical_name_in_scope(scope, name) else {
436            return Err(ExcelError::new(ExcelErrorKind::Name)
437                .with_message(format!("Name not found: {name}")));
438        };
439
440        let named_range = match scope {
441            NameScope::Workbook => {
442                let removed = self.named_ranges.remove(&canon_name);
443                let key = self.name_lookup_key(&canon_name);
444                self.named_ranges_lookup.remove(&key);
445                removed
446            }
447            NameScope::Sheet(id) => {
448                let removed = self.sheet_named_ranges.remove(&(id, canon_name.clone()));
449                let key = self.name_lookup_key(&canon_name);
450                self.sheet_named_ranges_lookup.remove(&(id, key));
451                removed
452            }
453        };
454
455        if let Some(named_range) = named_range {
456            let mut affected: FxHashSet<VertexId> = FxHashSet::default();
457            for &vertex_id in &named_range.dependents {
458                affected.insert(vertex_id);
459            }
460            for (vertex_id, names) in self.vertex_to_names.iter() {
461                if names.contains(&named_range.vertex) {
462                    affected.insert(*vertex_id);
463                }
464            }
465            for vertex_id in affected {
466                self.mark_vertex_dirty(vertex_id);
467                if let Some(names) = self.vertex_to_names.get_mut(&vertex_id) {
468                    names.retain(|vid| *vid != named_range.vertex);
469                    if names.is_empty() {
470                        self.vertex_to_names.remove(&vertex_id);
471                    }
472                }
473            }
474            self.mark_named_vertex_deleted(&named_range);
475            self.bump_symbol_revision();
476            Ok(())
477        } else {
478            Err(ExcelError::new(ExcelErrorKind::Name)
479                .with_message(format!("Name not found: {name}")))
480        }
481    }
482
483    pub(super) fn detach_vertex_from_names(&mut self, vertex: VertexId) {
484        if let Some(prior) = self.vertex_to_names.remove(&vertex) {
485            for name_vertex in prior {
486                if let Some((scope, name)) = self.name_vertex_lookup.get(&name_vertex).cloned() {
487                    match scope {
488                        NameScope::Workbook => {
489                            if let Some(entry) = self.named_ranges.get_mut(&name) {
490                                entry.dependents.remove(&vertex);
491                            }
492                        }
493                        NameScope::Sheet(sheet_id) => {
494                            if let Some(entry) =
495                                self.sheet_named_ranges.get_mut(&(sheet_id, name.clone()))
496                            {
497                                entry.dependents.remove(&vertex);
498                            }
499                        }
500                    }
501                }
502            }
503        }
504    }
505
506    pub(crate) fn attach_vertex_to_names(&mut self, vertex: VertexId, names: &[VertexId]) {
507        if names.is_empty() {
508            return;
509        }
510        let mut unique = FxHashSet::default();
511        let mut recorded = Vec::new();
512        for &name_vertex in names {
513            if !unique.insert(name_vertex) {
514                continue;
515            }
516            if let Some((scope, name)) = self.name_vertex_lookup.get(&name_vertex).cloned() {
517                match scope {
518                    NameScope::Workbook => {
519                        if let Some(entry) = self.named_ranges.get_mut(&name) {
520                            entry.dependents.insert(vertex);
521                        }
522                    }
523                    NameScope::Sheet(sheet_id) => {
524                        if let Some(entry) =
525                            self.sheet_named_ranges.get_mut(&(sheet_id, name.clone()))
526                        {
527                            entry.dependents.insert(vertex);
528                        }
529                    }
530                }
531                recorded.push(name_vertex);
532            }
533        }
534        if !recorded.is_empty() {
535            self.vertex_to_names.insert(vertex, recorded);
536        }
537    }
538
539    pub(super) fn unregister_name_cell_dependencies(&mut self, name_vertex: VertexId) {
540        if let Some(prev) = self.name_to_cell_dependencies.remove(&name_vertex) {
541            for dep in prev {
542                if let Some(set) = self.cell_to_name_dependents.get_mut(&dep) {
543                    set.remove(&name_vertex);
544                    if set.is_empty() {
545                        self.cell_to_name_dependents.remove(&dep);
546                    }
547                }
548            }
549        }
550    }
551
552    pub(super) fn register_name_cell_dependencies(
553        &mut self,
554        name_vertex: VertexId,
555        dependencies: &[VertexId],
556    ) {
557        self.unregister_name_cell_dependencies(name_vertex);
558        if dependencies.is_empty() {
559            return;
560        }
561        for dep in dependencies {
562            self.cell_to_name_dependents
563                .entry(*dep)
564                .or_default()
565                .insert(name_vertex);
566        }
567        self.name_to_cell_dependencies
568            .insert(name_vertex, dependencies.to_vec());
569    }
570
571    pub(crate) fn record_pending_name_reference(
572        &mut self,
573        sheet_id: SheetId,
574        name: &str,
575        formula_vertex: VertexId,
576    ) {
577        let key = self.name_lookup_key(name);
578        self.pending_name_links
579            .entry(key.clone())
580            .or_default()
581            .insert((sheet_id, formula_vertex));
582        self.vertex_to_pending_names
583            .entry(formula_vertex)
584            .or_default()
585            .insert(key);
586    }
587
588    pub(crate) fn clear_pending_name_references(&mut self, formula_vertex: VertexId) {
589        let Some(keys) = self.vertex_to_pending_names.remove(&formula_vertex) else {
590            return;
591        };
592
593        for key in keys {
594            let mut remove_key = false;
595            if let Some(entries) = self.pending_name_links.get_mut(&key) {
596                entries.retain(|(_, vertex_id)| *vertex_id != formula_vertex);
597                remove_key = entries.is_empty();
598            }
599            if remove_key {
600                self.pending_name_links.remove(&key);
601            }
602        }
603    }
604
605    pub(super) fn resolve_pending_name_references(&mut self, scope: NameScope, name: &str) {
606        let key = self.name_lookup_key(name);
607        if let Some(entries) = self.pending_name_links.remove(&key) {
608            for (sheet_id, formula_vertex) in entries {
609                let attach = match scope {
610                    NameScope::Workbook => true,
611                    NameScope::Sheet(expected) => expected == sheet_id,
612                };
613                if attach {
614                    if let Some(ast) = self.get_formula(formula_vertex) {
615                        self.rebuild_formula_dependencies(formula_vertex, &ast);
616                    } else {
617                        self.clear_pending_name_references(formula_vertex);
618                    }
619                } else {
620                    self.record_pending_name_reference(sheet_id, name, formula_vertex);
621                }
622            }
623        }
624    }
625
626    pub(super) fn name_depends_on_vertex(
627        &self,
628        name_vertex: VertexId,
629        target: VertexId,
630        visited: &mut FxHashSet<VertexId>,
631    ) -> bool {
632        if !visited.insert(name_vertex) {
633            return false;
634        }
635
636        for dependency in self.edges.out_edges(name_vertex).iter().copied() {
637            if dependency == target {
638                return true;
639            }
640
641            if matches!(
642                self.store.kind(dependency),
643                VertexKind::NamedScalar | VertexKind::NamedArray
644            ) && self.name_depends_on_vertex(dependency, target, visited)
645            {
646                return true;
647            }
648        }
649
650        false
651    }
652
653    pub(super) fn rebuild_name_dependencies(
654        &mut self,
655        vertex: VertexId,
656        definition: &NamedDefinition,
657        scope: NameScope,
658    ) -> Vec<VertexId> {
659        self.remove_dependent_edges(vertex);
660        self.unregister_name_cell_dependencies(vertex);
661
662        let mut dependencies: Vec<VertexId> = Vec::new();
663        let mut range_dependencies: Vec<SharedRangeRef<'static>> = Vec::new();
664        let mut placeholders = Vec::new();
665
666        match definition {
667            NamedDefinition::Cell(cell_ref) => {
668                let vertex_id = self.get_or_create_vertex(cell_ref, &mut placeholders);
669                dependencies.push(vertex_id);
670            }
671            NamedDefinition::Range(range_ref) => {
672                let height = range_ref
673                    .end
674                    .coord
675                    .row()
676                    .saturating_sub(range_ref.start.coord.row())
677                    + 1;
678                let width = range_ref
679                    .end
680                    .coord
681                    .col()
682                    .saturating_sub(range_ref.start.coord.col())
683                    + 1;
684                let size = (width * height) as usize;
685
686                if size <= self.config.range_expansion_limit {
687                    for row in range_ref.start.coord.row()..=range_ref.end.coord.row() {
688                        for col in range_ref.start.coord.col()..=range_ref.end.coord.col() {
689                            let coord = Coord::new(row, col, true, true);
690                            let addr = CellRef::new(range_ref.start.sheet_id, coord);
691                            let vertex_id = self.get_or_create_vertex(&addr, &mut placeholders);
692                            dependencies.push(vertex_id);
693                        }
694                    }
695                } else {
696                    let sheet_loc = SharedSheetLocator::Id(range_ref.start.sheet_id);
697                    let sr = formualizer_common::AxisBound::new(
698                        range_ref.start.coord.row(),
699                        range_ref.start.coord.row_abs(),
700                    );
701                    let sc = formualizer_common::AxisBound::new(
702                        range_ref.start.coord.col(),
703                        range_ref.start.coord.col_abs(),
704                    );
705                    let er = formualizer_common::AxisBound::new(
706                        range_ref.end.coord.row(),
707                        range_ref.end.coord.row_abs(),
708                    );
709                    let ec = formualizer_common::AxisBound::new(
710                        range_ref.end.coord.col(),
711                        range_ref.end.coord.col_abs(),
712                    );
713                    if let Ok(r) = SharedRangeRef::from_parts(
714                        sheet_loc,
715                        Some(sr),
716                        Some(sc),
717                        Some(er),
718                        Some(ec),
719                    ) {
720                        range_dependencies.push(r.into_owned());
721                    }
722                }
723            }
724            NamedDefinition::Literal(_) => {
725                // No dependencies.
726            }
727            NamedDefinition::Formula {
728                dependencies: formula_deps,
729                range_deps,
730                ..
731            } => {
732                dependencies.extend(formula_deps.iter().copied());
733                range_dependencies.extend(range_deps.iter().cloned());
734            }
735        }
736
737        if !dependencies.is_empty() {
738            self.add_dependent_edges(vertex, &dependencies);
739        }
740        self.register_name_cell_dependencies(vertex, &dependencies);
741
742        if !range_dependencies.is_empty() {
743            let sheet_id = match scope {
744                NameScope::Sheet(id) => id,
745                NameScope::Workbook => self.default_sheet_id,
746            };
747            self.add_range_dependent_edges(vertex, &range_dependencies, sheet_id);
748        }
749
750        dependencies
751            .iter()
752            .filter(|vid| {
753                matches!(
754                    self.store.kind(**vid),
755                    VertexKind::NamedScalar | VertexKind::NamedArray
756                )
757            })
758            .copied()
759            .collect()
760    }
761
762    pub fn adjust_named_ranges(
763        &mut self,
764        operation: &crate::engine::graph::editor::reference_adjuster::ShiftOperation,
765    ) -> Result<(), ExcelError> {
766        let adjuster = crate::engine::graph::editor::reference_adjuster::ReferenceAdjuster::new();
767
768        let changed = !self.named_ranges.is_empty() || !self.sheet_named_ranges.is_empty();
769        // Workbook-scoped formulas bind unqualified references to the default sheet.
770        let workbook_context =
771            crate::engine::graph::editor::reference_adjuster::ReferenceContext::new(
772                self.default_sheet_id,
773                &self.sheet_reg,
774            );
775        for named_range in self.named_ranges.values_mut() {
776            adjust_named_definition(
777                &mut named_range.definition,
778                &adjuster,
779                operation,
780                &workbook_context,
781            )?;
782        }
783
784        // Sheet-scoped formulas bind unqualified references to their scope sheet.
785        for ((scope_sheet_id, _), named_range) in self.sheet_named_ranges.iter_mut() {
786            let context = crate::engine::graph::editor::reference_adjuster::ReferenceContext::new(
787                *scope_sheet_id,
788                &self.sheet_reg,
789            );
790            adjust_named_definition(&mut named_range.definition, &adjuster, operation, &context)?;
791        }
792        if changed {
793            self.bump_symbol_revision();
794        }
795
796        Ok(())
797    }
798
799    /// Mark a vertex as having a #NAME! error
800    pub fn mark_as_name_error(&mut self, vertex_id: VertexId) {
801        // Mark the vertex as dirty
802        self.mark_vertex_dirty(vertex_id);
803    }
804
805    pub(super) fn mark_named_vertex_deleted(&mut self, named_range: &NamedRange) {
806        self.detach_vertex_from_names(named_range.vertex);
807        self.remove_dependent_edges(named_range.vertex);
808        self.unregister_name_cell_dependencies(named_range.vertex);
809        self.store.mark_deleted(named_range.vertex, true);
810        self.vertex_values.remove(&named_range.vertex);
811        self.vertex_formulas.remove(&named_range.vertex);
812        self.clear_formula_vertex_dirty(named_range.vertex);
813        self.volatile_vertices.remove(&named_range.vertex);
814        self.vertex_to_names.remove(&named_range.vertex);
815        self.name_vertex_lookup.remove(&named_range.vertex);
816    }
817}