Skip to main content

graphcal_compiler/ir/resolve/
mod.rs

1mod deps;
2pub(crate) mod names;
3#[cfg(test)]
4mod tests;
5
6use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use miette::NamedSource;
10
11use crate::desugar::desugared_ast::{
12    AssertBody, AttributeArg, DeclKind, DimExpr, Expr, ExprKind, File, IndexExpr, TypeDeclBody,
13    TypeExpr, TypeExprKind,
14};
15use crate::registry::error::GraphcalError;
16use crate::registry::prelude::{
17    PRELUDE_BUILTIN_TYPE_NAMES, PRELUDE_DIMENSION_NAMES, PRELUDE_UNIT_NAMES,
18};
19use crate::registry::resolve_types::{
20    ResolvedAssertEntry, ResolvedConstEntry, ResolvedFigureEntry, ResolvedLayerEntry,
21    ResolvedNodeEntry, ResolvedParamEntry, ResolvedPlotEntry,
22};
23use crate::syntax::attribute::AttributeName;
24use crate::syntax::names::{DeclName, NameAtom};
25use crate::syntax::span::Span;
26
27// Re-export types and constants from graphcal-registry's resolve_types module.
28pub use crate::registry::resolve_types::{
29    DeclCategory, ExpectedFail, ExpectedFailKey, ExpectedFailKeyPart, ImportedValueNames,
30    ResolvedFile, is_aggregation_fn, is_time_scale_name,
31};
32pub use crate::syntax::names::ScopedName;
33
34// Re-export items from submodules (crate-internal only).
35pub use deps::{collect_graph_ref_names, collect_graph_refs, contains_graph_ref};
36
37// Import helpers from submodules for use within this file.
38use names::parse_expected_fail_args;
39
40fn register_value_namespace_name(
41    value_names: &mut HashMap<ScopedName, Span>,
42    name: String,
43    span: Span,
44    src: &NamedSource<Arc<String>>,
45) -> Result<(), GraphcalError> {
46    let scoped_name = ScopedName::local(name.clone());
47    if let Some(first_span) = value_names.get(&scoped_name) {
48        return Err(GraphcalError::DuplicateName {
49            name,
50            src: src.clone(),
51            duplicate: span.into(),
52            first: (*first_span).into(),
53        });
54    }
55    value_names.insert(scoped_name, span);
56    Ok(())
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60enum ExclusiveUniverse {
61    Type,
62    Index,
63    Value,
64}
65
66type ExclusiveUniverseBinding = (ExclusiveUniverse, Span);
67
68fn register_exclusive_universe_name(
69    occupied: &mut HashMap<NameAtom, ExclusiveUniverseBinding>,
70    atom: &NameAtom,
71    universe: ExclusiveUniverse,
72    span: Span,
73    src: &NamedSource<Arc<String>>,
74) -> Result<(), GraphcalError> {
75    occupied
76        .insert(atom.clone(), (universe, span))
77        .map_or(Ok(()), |first| {
78            Err(GraphcalError::DuplicateName {
79                name: atom.to_string(),
80                src: src.clone(),
81                duplicate: span.into(),
82                first: first.1.into(),
83            })
84        })
85}
86
87fn check_builtin_name_shadowing(
88    file: &File,
89    src: &NamedSource<Arc<String>>,
90) -> Result<(), GraphcalError> {
91    for decl in &file.declarations {
92        let shadowed = match &decl.kind {
93            DeclKind::BaseDimension(d) if is_builtin_type_name(d.name.value.as_str()) => {
94                Some(("dimension", d.name.value.to_string(), d.name.span))
95            }
96            DeclKind::Dimension(d) if is_builtin_type_name(d.name.value.as_str()) => {
97                Some(("dimension", d.name.value.to_string(), d.name.span))
98            }
99            DeclKind::Type(t) if is_builtin_type_name(t.name.value.as_str()) => {
100                Some(("type", t.name.value.to_string(), t.name.span))
101            }
102            DeclKind::Index(i) if is_builtin_type_name(i.name.value.as_str()) => {
103                Some(("index", i.name.value.to_string(), i.name.span))
104            }
105            DeclKind::Unit(u) if PRELUDE_UNIT_NAMES.contains(&u.name.value.as_str()) => {
106                Some(("unit", u.name.value.to_string(), u.name.span))
107            }
108            _ => None,
109        };
110
111        if let Some((kind, name, span)) = shadowed {
112            return Err(GraphcalError::BuiltinNameShadowed {
113                kind,
114                name,
115                src: src.clone(),
116                span: span.into(),
117            });
118        }
119    }
120
121    Ok(())
122}
123
124fn is_builtin_type_name(name: &str) -> bool {
125    PRELUDE_DIMENSION_NAMES.contains(&name) || PRELUDE_BUILTIN_TYPE_NAMES.contains(&name)
126}
127
128fn check_exclusive_universe_collisions(
129    file: &File,
130    src: &NamedSource<Arc<String>>,
131    names: &HashMap<ScopedName, Span>,
132) -> Result<(), GraphcalError> {
133    let mut occupied = names
134        .iter()
135        .filter(|(name, _)| !name.is_qualified())
136        .map(|(name, span)| {
137            (
138                DeclName::new(name.member()).into_atom(),
139                (ExclusiveUniverse::Value, *span),
140            )
141        })
142        .collect::<HashMap<_, _>>();
143
144    for (atom, universe, span) in file
145        .declarations
146        .iter()
147        .filter_map(|decl| exclusive_universe_decl(&decl.kind))
148    {
149        register_exclusive_universe_name(&mut occupied, atom, universe, span, src)?;
150    }
151
152    Ok(())
153}
154
155fn exclusive_universe_decl(decl: &DeclKind) -> Option<(&NameAtom, ExclusiveUniverse, Span)> {
156    match decl {
157        DeclKind::Param(p) => Some((p.name.value.atom(), ExclusiveUniverse::Value, p.name.span)),
158        DeclKind::Node(n) => Some((n.name.value.atom(), ExclusiveUniverse::Value, n.name.span)),
159        DeclKind::ConstNode(c) => {
160            Some((c.name.value.atom(), ExclusiveUniverse::Value, c.name.span))
161        }
162        DeclKind::Assert(a) => Some((a.name.value.atom(), ExclusiveUniverse::Value, a.name.span)),
163        DeclKind::Plot(p) => Some((p.name.value.atom(), ExclusiveUniverse::Value, p.name.span)),
164        DeclKind::Figure(f) => Some((f.name.value.atom(), ExclusiveUniverse::Value, f.name.span)),
165        DeclKind::Layer(l) => Some((l.name.value.atom(), ExclusiveUniverse::Value, l.name.span)),
166        DeclKind::Dag(d) => Some((d.name.value.atom(), ExclusiveUniverse::Value, d.name.span)),
167        DeclKind::BaseDimension(d) => {
168            Some((d.name.value.atom(), ExclusiveUniverse::Type, d.name.span))
169        }
170        DeclKind::Dimension(d) => Some((d.name.value.atom(), ExclusiveUniverse::Type, d.name.span)),
171        DeclKind::Type(t) => Some((t.name.value.atom(), ExclusiveUniverse::Type, t.name.span)),
172        DeclKind::Index(i) => Some((i.name.value.atom(), ExclusiveUniverse::Index, i.name.span)),
173        DeclKind::Unit(_) | DeclKind::Import(_) | DeclKind::Include(_) => None,
174        DeclKind::Sugar(_) => crate::syntax::desugar::unreachable_post_desugar(),
175    }
176}
177
178fn check_value_namespace_collisions(
179    file: &File,
180    src: &NamedSource<Arc<String>>,
181    names: &HashMap<ScopedName, Span>,
182) -> Result<(), GraphcalError> {
183    let mut value_names: HashMap<ScopedName, Span> = names.clone();
184
185    for decl in &file.declarations {
186        match &decl.kind {
187            DeclKind::Param(p) => register_value_namespace_name(
188                &mut value_names,
189                p.name.value.to_string(),
190                p.name.span,
191                src,
192            )?,
193            DeclKind::Node(n) => register_value_namespace_name(
194                &mut value_names,
195                n.name.value.to_string(),
196                n.name.span,
197                src,
198            )?,
199            DeclKind::ConstNode(c) => register_value_namespace_name(
200                &mut value_names,
201                c.name.value.to_string(),
202                c.name.span,
203                src,
204            )?,
205            DeclKind::Assert(a) => register_value_namespace_name(
206                &mut value_names,
207                a.name.value.to_string(),
208                a.name.span,
209                src,
210            )?,
211            DeclKind::Plot(p) => register_value_namespace_name(
212                &mut value_names,
213                p.name.value.to_string(),
214                p.name.span,
215                src,
216            )?,
217            DeclKind::Figure(f) => register_value_namespace_name(
218                &mut value_names,
219                f.name.value.to_string(),
220                f.name.span,
221                src,
222            )?,
223            DeclKind::Layer(l) => register_value_namespace_name(
224                &mut value_names,
225                l.name.value.to_string(),
226                l.name.span,
227                src,
228            )?,
229            DeclKind::Type(t) => {
230                if let TypeDeclBody::Constructors(members) = &t.body {
231                    for member in members {
232                        register_value_namespace_name(
233                            &mut value_names,
234                            member.name.value.to_string(),
235                            member.name.span,
236                            src,
237                        )?;
238                    }
239                }
240            }
241            DeclKind::BaseDimension(_)
242            | DeclKind::Dimension(_)
243            | DeclKind::Unit(_)
244            | DeclKind::Index(_)
245            | DeclKind::Import(_)
246            | DeclKind::Include(_)
247            | DeclKind::Dag(_) => {}
248            DeclKind::Sugar(_) => crate::syntax::desugar::unreachable_post_desugar(),
249        }
250    }
251
252    Ok(())
253}
254
255/// Result of collecting local declarations from the AST.
256struct CollectedDeclarations {
257    consts: Vec<ResolvedConstEntry>,
258    params: Vec<ResolvedParamEntry>,
259    nodes: Vec<ResolvedNodeEntry>,
260    asserts: Vec<ResolvedAssertEntry>,
261    plots: Vec<ResolvedPlotEntry>,
262    figures: Vec<ResolvedFigureEntry>,
263    layers: Vec<ResolvedLayerEntry>,
264    source_order: Vec<(DeclName, DeclCategory)>,
265    assert_names: HashSet<DeclName>,
266    pub_names: HashSet<DeclName>,
267}
268
269/// Collect all local declarations and check for duplicates.
270///
271/// Returns the collected declarations and the names map for further processing.
272#[expect(
273    clippy::too_many_lines,
274    reason = "complex declaration collection with multiple passes"
275)]
276fn collect_local_declarations(
277    file: &File,
278    src: &NamedSource<Arc<String>>,
279    names: &mut HashMap<ScopedName, Span>,
280) -> Result<CollectedDeclarations, GraphcalError> {
281    let mut consts = Vec::new();
282    let mut params = Vec::new();
283    let mut nodes = Vec::new();
284    let mut asserts = Vec::new();
285    let mut plots = Vec::new();
286    let mut figures = Vec::new();
287    let mut layers = Vec::new();
288    let mut source_order: Vec<(DeclName, DeclCategory)> = Vec::new();
289    let mut assert_names: HashSet<DeclName> = HashSet::new();
290
291    check_builtin_name_shadowing(file, src)?;
292    check_exclusive_universe_collisions(file, src, names)?;
293    check_value_namespace_collisions(file, src, names)?;
294
295    // Collect names of all visible declarations. Explicit `pub`/`pub(bind)`
296    // declarations contribute; params are implicitly visible+bindable under
297    // A5 and always contribute.
298    let mut pub_names: HashSet<DeclName> = HashSet::new();
299    for decl in &file.declarations {
300        let is_visible = match &decl.kind {
301            DeclKind::Param(_) => true,
302            DeclKind::Node(d) => d.visibility.is_public(),
303            DeclKind::ConstNode(d) => d.visibility.is_public(),
304            DeclKind::BaseDimension(d) => d.visibility.is_public(),
305            DeclKind::Dimension(d) => d.visibility.is_public(),
306            DeclKind::Unit(d) => d.visibility.is_public(),
307            DeclKind::Type(d) => d.visibility.is_public(),
308            DeclKind::Index(d) => d.visibility.is_public(),
309            DeclKind::Import(d) => d.visibility.is_public(),
310            DeclKind::Include(d) => d.visibility.is_public(),
311            DeclKind::Dag(d) => d.visibility.is_public(),
312            DeclKind::Assert(d) => d.visibility.is_public(),
313            DeclKind::Plot(d) => d.visibility.is_public(),
314            DeclKind::Figure(d) => d.visibility.is_public(),
315            DeclKind::Layer(d) => d.visibility.is_public(),
316            DeclKind::Sugar(_) => false,
317        };
318        if !is_visible {
319            continue;
320        }
321        let Some((name, _)) = decl.kind.name_and_span() else {
322            continue;
323        };
324        pub_names.insert(DeclName::new(name));
325    }
326
327    // Validate: required `index`, `type`, `dim` must be `pub(bind)` (V002).
328    //
329    // Required `param` is excluded from this check: per axiom A5 §4.0,
330    // `param` is implicitly V=visible + B=bindable and never carries a
331    // visibility annotation. The parser rejects `pub`/`pub(bind)` on
332    // `param`.
333    for decl in &file.declarations {
334        match &decl.kind {
335            DeclKind::Index(idx) if idx.kind.is_required() && !idx.visibility.is_bindable() => {
336                return Err(GraphcalError::RequiredItemMustBeBindable {
337                    kind: "index".to_string(),
338                    name: idx.name.value.to_string(),
339                    src: src.clone(),
340                    span: idx.name.span.into(),
341                });
342            }
343            DeclKind::Type(t)
344                if matches!(t.body, TypeDeclBody::Required) && !t.visibility.is_bindable() =>
345            {
346                return Err(GraphcalError::RequiredItemMustBeBindable {
347                    kind: "type".to_string(),
348                    name: t.name.value.to_string(),
349                    src: src.clone(),
350                    span: t.name.span.into(),
351                });
352            }
353            DeclKind::Dimension(d) if d.definition.is_none() && !d.visibility.is_bindable() => {
354                return Err(GraphcalError::RequiredItemMustBeBindable {
355                    kind: "dim".to_string(),
356                    name: d.name.value.to_string(),
357                    src: src.clone(),
358                    span: d.name.span.into(),
359                });
360            }
361            _ => {}
362        }
363    }
364
365    // First pass: collect all declarations and check for duplicates
366    for decl in &file.declarations {
367        // Dimension and Unit declarations are handled by the registry, not the resolver
368        let (name, name_span) = match &decl.kind {
369            DeclKind::Param(p) => (p.name.value.to_string(), p.name.span),
370            DeclKind::Node(n) => (n.name.value.to_string(), n.name.span),
371            DeclKind::ConstNode(c) => (c.name.value.to_string(), c.name.span),
372            DeclKind::Assert(a) => (a.name.value.to_string(), a.name.span),
373            DeclKind::Plot(p) => (p.name.value.to_string(), p.name.span),
374            DeclKind::Figure(f) => (f.name.value.to_string(), f.name.span),
375            DeclKind::Layer(l) => (l.name.value.to_string(), l.name.span),
376            DeclKind::BaseDimension(_)
377            | DeclKind::Dimension(_)
378            | DeclKind::Unit(_)
379            | DeclKind::Type(_)
380            | DeclKind::Index(_)
381            | DeclKind::Import(_)
382            | DeclKind::Include(_)
383            | DeclKind::Dag(_) => {
384                continue;
385            }
386            DeclKind::Sugar(_) => crate::syntax::desugar::unreachable_post_desugar(),
387        };
388
389        let scoped_name = ScopedName::local(name.clone());
390        names.insert(scoped_name, name_span);
391
392        // Track source order and assert names
393        let category = match &decl.kind {
394            DeclKind::Param(_) => DeclCategory::Param,
395            DeclKind::ConstNode(_) => DeclCategory::Const,
396            DeclKind::Node(_) => DeclCategory::Node,
397            DeclKind::Assert(_) => {
398                assert_names.insert(DeclName::new(name.as_str()));
399                DeclCategory::Assert
400            }
401            DeclKind::Plot(_) => DeclCategory::Plot,
402            DeclKind::Figure(_) => DeclCategory::Figure,
403            DeclKind::Layer(_) => DeclCategory::Layer,
404            DeclKind::BaseDimension(_)
405            | DeclKind::Dimension(_)
406            | DeclKind::Unit(_)
407            | DeclKind::Type(_)
408            | DeclKind::Index(_)
409            | DeclKind::Import(_)
410            | DeclKind::Include(_)
411            | DeclKind::Dag(_) => {
412                // These declarations are handled earlier (continue'd before reaching here).
413                continue;
414            }
415            DeclKind::Sugar(_) => crate::syntax::desugar::unreachable_post_desugar(),
416        };
417        source_order.push((DeclName::new(name.as_str()), category));
418    }
419
420    // Second pass: collect declaration entries. Reference validation and
421    // dependency extraction happen after HIR lowering — this pass only
422    // gathers declaration bodies in source order.
423    for decl in &file.declarations {
424        match &decl.kind {
425            DeclKind::BaseDimension(_)
426            | DeclKind::Dimension(_)
427            | DeclKind::Unit(_)
428            | DeclKind::Type(_)
429            | DeclKind::Index(_)
430            | DeclKind::Import(_)
431            | DeclKind::Include(_)
432            | DeclKind::Dag(_) => {}
433            DeclKind::Sugar(_) => crate::syntax::desugar::unreachable_post_desugar(),
434            DeclKind::Assert(a) => {
435                asserts.push(ResolvedAssertEntry {
436                    name: a.name.value.to_string(),
437                    body: a.body.clone(),
438                    span: decl.span,
439                });
440            }
441            DeclKind::Plot(p) => {
442                plots.push(ResolvedPlotEntry {
443                    name: p.name.value.to_string(),
444                    decl: p.clone(),
445                    span: decl.span,
446                });
447            }
448            DeclKind::Figure(f) => {
449                figures.push(ResolvedFigureEntry {
450                    name: f.name.value.to_string(),
451                    decl: f.clone(),
452                    span: decl.span,
453                });
454            }
455            DeclKind::Layer(l) => {
456                layers.push(ResolvedLayerEntry {
457                    name: l.name.value.to_string(),
458                    decl: l.clone(),
459                    span: decl.span,
460                });
461            }
462            DeclKind::Param(p) => {
463                params.push(ResolvedParamEntry {
464                    name: p.name.value.to_string(),
465                    default_expr: p.value.clone(),
466                    span: decl.span,
467                });
468            }
469            DeclKind::ConstNode(c) => {
470                consts.push(ResolvedConstEntry {
471                    name: c.name.value.to_string(),
472                    expr: c.value.clone(),
473                    span: decl.span,
474                });
475            }
476            DeclKind::Node(n) => {
477                nodes.push(ResolvedNodeEntry {
478                    name: n.name.value.to_string(),
479                    expr: n.value.clone(),
480                    span: decl.span,
481                });
482            }
483        }
484    }
485
486    Ok(CollectedDeclarations {
487        consts,
488        params,
489        nodes,
490        asserts,
491        plots,
492        figures,
493        layers,
494        source_order,
495        assert_names,
496        pub_names,
497    })
498}
499
500/// Result of attribute validation.
501struct ValidatedAttributes {
502    assumes_map: HashMap<DeclName, Vec<DeclName>>,
503    expected_fail_map: HashMap<DeclName, ExpectedFail>,
504    /// Plot names carrying `#[hidden]`: evaluated and referenceable from
505    /// figures/layers, but excluded from standalone output (#847).
506    hidden_plots: HashSet<DeclName>,
507}
508
509/// Validate attributes and build `assumes_map` / `expected_fail_map`.
510#[expect(clippy::too_many_lines, reason = "comprehensive attribute validation")]
511fn validate_attributes(
512    file: &File,
513    src: &NamedSource<Arc<String>>,
514    assert_names: &HashSet<DeclName>,
515) -> Result<ValidatedAttributes, GraphcalError> {
516    let mut assumes_map: HashMap<DeclName, Vec<DeclName>> = HashMap::new();
517    let mut expected_fail_map: HashMap<DeclName, ExpectedFail> = HashMap::new();
518    let mut hidden_plots: HashSet<DeclName> = HashSet::new();
519
520    for decl in &file.declarations {
521        let decl_name: Option<DeclName> = match &decl.kind {
522            DeclKind::Param(p) => Some(p.name.value.clone()),
523            DeclKind::Node(n) => Some(n.name.value.clone()),
524            DeclKind::ConstNode(c) => Some(c.name.value.clone()),
525            DeclKind::Assert(a) => Some(a.name.value.clone()),
526            DeclKind::Plot(p) => Some(p.name.value.clone()),
527            DeclKind::Figure(f) => Some(f.name.value.clone()),
528            _ => None,
529        };
530        for attr in &decl.attributes {
531            let attr_name_str = attr.name.name.as_str();
532            let attr_name = attr_name_str.parse::<AttributeName>().map_err(|err| {
533                GraphcalError::UnknownAttribute {
534                    name: err.into_raw(),
535                    src: src.clone(),
536                    span: attr.span.into(),
537                }
538            })?;
539
540            match attr_name {
541                AttributeName::Assumes => {
542                    // #[assumes] is only valid on non-const node and param
543                    let kind = match &decl.kind {
544                        DeclKind::ConstNode(_) => Some("const node"),
545                        DeclKind::Param(_) | DeclKind::Node(_) => None,
546                        DeclKind::Assert(_) => Some("assert"),
547                        DeclKind::Plot(_) => Some("plot"),
548                        DeclKind::Figure(_) => Some("figure"),
549                        DeclKind::Layer(_) => Some("layer"),
550
551                        DeclKind::BaseDimension(_) | DeclKind::Dimension(_) => Some("dim"),
552                        DeclKind::Unit(_) => Some("unit"),
553                        DeclKind::Type(_) => Some("type"),
554                        DeclKind::Index(_) => Some("cat/range"),
555                        DeclKind::Import(_) => Some("import"),
556                        DeclKind::Include(_) => Some("include"),
557                        DeclKind::Dag(_) => Some("dag"),
558                        DeclKind::Sugar(_) => crate::syntax::desugar::unreachable_post_desugar(),
559                    };
560                    if let Some(kind) = kind {
561                        return Err(GraphcalError::InvalidAssumesTarget {
562                            kind: kind.to_string(),
563                            src: src.clone(),
564                            span: attr.span.into(),
565                        });
566                    }
567                    // Each argument must reference an existing assert declaration
568                    for arg in &attr.args {
569                        let ident = match arg {
570                            AttributeArg::Path { segments, .. } if segments.len() == 1 => {
571                                segments.first()
572                            }
573                            AttributeArg::Path { .. }
574                            | AttributeArg::RangeStep { .. }
575                            | AttributeArg::Group { .. } => {
576                                return Err(GraphcalError::EvalError {
577                                    message:
578                                        "`#[assumes(...)]` arguments must be plain identifiers"
579                                            .to_string(),
580                                    src: src.clone(),
581                                    span: arg.span().into(),
582                                });
583                            }
584                        };
585                        let arg_name = ident.name.as_str();
586                        if !assert_names.contains(arg_name) {
587                            return Err(GraphcalError::UnknownAssertInAssumes {
588                                name: arg_name.to_string(),
589                                src: src.clone(),
590                                span: ident.span.into(),
591                            });
592                        }
593                        if let Some(ref dname) = decl_name {
594                            assumes_map
595                                .entry(DeclName::new(arg_name))
596                                .or_default()
597                                .push(dname.clone());
598                        }
599                    }
600                }
601                AttributeName::ExpectedFail => {
602                    let kind = match &decl.kind {
603                        DeclKind::Assert(a) => {
604                            // Valid target — parse args and record
605                            let ef = parse_expected_fail_args(&attr.args, src)?;
606                            // #[expected_fail] (no args) on an indexed assertion is
607                            // an error — the user must specify which variants fail.
608                            if matches!(ef, ExpectedFail::All) {
609                                let is_indexed = matches!(
610                                    &a.body,
611                                    AssertBody::Expr(expr) if matches!(expr.kind, ExprKind::ForComp { .. })
612                                );
613                                if is_indexed {
614                                    return Err(GraphcalError::ExpectedFailAllOnIndexed {
615                                        src: src.clone(),
616                                        span: attr.span.into(),
617                                    });
618                                }
619                            }
620                            if let Some(ref dname) = decl_name {
621                                expected_fail_map.insert(dname.clone(), ef);
622                            }
623                            continue;
624                        }
625                        DeclKind::Param(_) => "param",
626                        DeclKind::ConstNode(_) => "const node",
627                        DeclKind::Node(_) => "node",
628                        DeclKind::Plot(_) => "plot",
629                        DeclKind::Figure(_) => "figure",
630                        DeclKind::Layer(_) => "layer",
631
632                        DeclKind::BaseDimension(_) | DeclKind::Dimension(_) => "dim",
633                        DeclKind::Unit(_) => "unit",
634                        DeclKind::Type(_) => "type",
635                        DeclKind::Index(_) => "cat/range",
636                        DeclKind::Import(_) => "import",
637                        DeclKind::Include(_) => "include",
638                        DeclKind::Dag(_) => "dag",
639                        DeclKind::Sugar(_) => crate::syntax::desugar::unreachable_post_desugar(),
640                    };
641                    return Err(GraphcalError::InvalidExpectedFailTarget {
642                        kind: kind.to_string(),
643                        src: src.clone(),
644                        span: attr.span.into(),
645                    });
646                }
647                AttributeName::Hidden => {
648                    // #[hidden] is plot-only: figures/layers cannot be
649                    // referenced by anything, so hiding one is equivalent to
650                    // deleting it; other declarations have no display axis.
651                    let kind = match &decl.kind {
652                        DeclKind::Plot(_) => None,
653                        DeclKind::Param(_) => Some("param"),
654                        DeclKind::ConstNode(_) => Some("const node"),
655                        DeclKind::Node(_) => Some("node"),
656                        DeclKind::Assert(_) => Some("assert"),
657                        DeclKind::Figure(_) => Some("figure"),
658                        DeclKind::Layer(_) => Some("layer"),
659                        DeclKind::BaseDimension(_) | DeclKind::Dimension(_) => Some("dim"),
660                        DeclKind::Unit(_) => Some("unit"),
661                        DeclKind::Type(_) => Some("type"),
662                        DeclKind::Index(_) => Some("cat/range"),
663                        DeclKind::Import(_) => Some("import"),
664                        DeclKind::Include(_) => Some("include"),
665                        DeclKind::Dag(_) => Some("dag"),
666                        DeclKind::Sugar(_) => crate::syntax::desugar::unreachable_post_desugar(),
667                    };
668                    if let Some(kind) = kind {
669                        return Err(GraphcalError::InvalidHiddenTarget {
670                            kind: kind.to_string(),
671                            src: src.clone(),
672                            span: attr.span.into(),
673                        });
674                    }
675                    if !attr.args.is_empty() {
676                        return Err(GraphcalError::EvalError {
677                            message: "`#[hidden]` takes no arguments".to_string(),
678                            src: src.clone(),
679                            span: attr.span.into(),
680                        });
681                    }
682                    if let Some(ref dname) = decl_name {
683                        hidden_plots.insert(dname.clone());
684                    }
685                }
686                AttributeName::Lazy => {
687                    // Recognized but semantics deferred — no validation needed
688                }
689            }
690        }
691    }
692
693    Ok(ValidatedAttributes {
694        assumes_map,
695        expected_fail_map,
696        hidden_plots,
697    })
698}
699
700/// Validate that every visible declaration names only visible type-system
701/// symbols in its written signature (V003 / A9 case 1).
702///
703/// A declaration's signature is checked when the declaration is visible
704/// at the library boundary: either explicitly `pub` / `pub(bind)`, or
705/// implicitly visible (`param`, per A5 §4.0).
706///
707/// Built-in type-system items (prelude dimensions like `Length`, and
708/// built-in types `Bool`, `Int`, `Dimensionless`, `Datetime`) are
709/// always considered visible.
710#[expect(
711    clippy::too_many_lines,
712    reason = "exhaustive declaration-kind validation is clearer in one pass"
713)]
714fn validate_private_in_public(
715    file: &File,
716    src: &NamedSource<Arc<String>>,
717    pub_names: &HashSet<DeclName>,
718) -> Result<(), GraphcalError> {
719    use crate::desugar::desugared_ast::IndexDeclKind;
720
721    // Collect all locally-declared type-system names (dims, indexes, types) with their spans.
722    let mut local_type_names: HashMap<String, Span> = HashMap::new();
723    for decl in &file.declarations {
724        let (name, span) = match &decl.kind {
725            DeclKind::BaseDimension(d) => (d.name.value.to_string(), d.name.span),
726            DeclKind::Dimension(d) => (d.name.value.to_string(), d.name.span),
727            DeclKind::Index(idx) => (idx.name.value.to_string(), idx.name.span),
728            DeclKind::Type(t) => (t.name.value.to_string(), t.name.span),
729            _ => continue,
730        };
731        local_type_names.insert(name, span);
732    }
733
734    // If there are no local type-system names, nothing to check.
735    if local_type_names.is_empty() {
736        return Ok(());
737    }
738
739    let emit = |pub_kind: &str,
740                pub_name: String,
741                pub_span: Span,
742                refs: &[(crate::syntax::names::NamePath, Span)]|
743     -> Result<(), GraphcalError> {
744        for (ref_path, ref_span) in refs {
745            // Only a bare (single-segment) path can name a local type-system
746            // declaration; qualified refs belong to another module.
747            let Some(ref_name) = ref_path.as_bare() else {
748                continue;
749            };
750            if local_type_names.contains_key(ref_name.as_str())
751                && !pub_names.contains(ref_name.as_str())
752            {
753                return Err(GraphcalError::PrivateInPublic {
754                    pub_kind: pub_kind.to_string(),
755                    pub_name,
756                    ref_kind: ref_kind_for(file, ref_name.as_str()).to_string(),
757                    ref_name: ref_name.to_string(),
758                    src: src.clone(),
759                    ref_span: (*ref_span).into(),
760                    pub_span: pub_span.into(),
761                });
762            }
763        }
764        Ok(())
765    };
766
767    for decl in &file.declarations {
768        // `param` is always visible (A5 §4.0); other kinds only when
769        // explicitly marked `pub` / `pub(bind)`.
770        let is_visible = match &decl.kind {
771            DeclKind::Param(_) => true,
772            DeclKind::Node(d) => d.visibility.is_public(),
773            DeclKind::ConstNode(d) => d.visibility.is_public(),
774            DeclKind::BaseDimension(d) => d.visibility.is_public(),
775            DeclKind::Dimension(d) => d.visibility.is_public(),
776            DeclKind::Unit(d) => d.visibility.is_public(),
777            DeclKind::Type(d) => d.visibility.is_public(),
778            DeclKind::Index(d) => d.visibility.is_public(),
779            DeclKind::Import(d) => d.visibility.is_public(),
780            DeclKind::Include(d) => d.visibility.is_public(),
781            DeclKind::Dag(d) => d.visibility.is_public(),
782            DeclKind::Assert(d) => d.visibility.is_public(),
783            DeclKind::Plot(d) => d.visibility.is_public(),
784            DeclKind::Figure(d) => d.visibility.is_public(),
785            DeclKind::Layer(d) => d.visibility.is_public(),
786            DeclKind::Sugar(_) => false,
787        };
788        if !is_visible {
789            continue;
790        }
791
792        let mut refs: Vec<(crate::syntax::names::NamePath, Span)> = Vec::new();
793        let (kind, name): (&str, String) = match &decl.kind {
794            DeclKind::Param(p) => {
795                collect_type_refs(&p.type_ann, &mut refs);
796                ("param", p.name.value.to_string())
797            }
798            DeclKind::Node(n) => {
799                collect_type_refs(&n.type_ann, &mut refs);
800                ("node", n.name.value.to_string())
801            }
802            DeclKind::ConstNode(c) => {
803                collect_type_refs(&c.type_ann, &mut refs);
804                ("const node", c.name.value.to_string())
805            }
806            DeclKind::Dimension(d) => {
807                if let Some(def) = &d.definition {
808                    collect_dim_refs(def, &mut refs);
809                }
810                ("dim", d.name.value.to_string())
811            }
812            DeclKind::Unit(u) => {
813                collect_dim_refs(&u.dim_type, &mut refs);
814                ("unit", u.name.value.to_string())
815            }
816            DeclKind::Type(t) => {
817                // Each constructor payload field type is part of the
818                // type's signature for A9 dependency tracking.
819                if let TypeDeclBody::Constructors(members) = &t.body {
820                    for member in members {
821                        if let Some(fields) = &member.payload {
822                            for field in fields {
823                                collect_type_refs(&field.type_ann, &mut refs);
824                            }
825                        }
826                    }
827                }
828                ("type", t.name.value.to_string())
829            }
830            DeclKind::Index(idx) => {
831                if let IndexDeclKind::RequiredRange { dimension } = &idx.kind {
832                    collect_dim_refs(dimension, &mut refs);
833                }
834                ("index", idx.name.value.to_string())
835            }
836            // Sink kinds have no written signature; bodies are not A9 case 1.
837            // BaseDimension has no body. Import/Include are use-sites. Dag is
838            // a use-site at the signature level.
839            _ => continue,
840        };
841
842        emit(kind, name, decl.span, &refs)?;
843    }
844    Ok(())
845}
846
847/// Recursively collect type-system references from a [`TypeExpr`].
848fn collect_type_refs(type_expr: &TypeExpr, refs: &mut Vec<(crate::syntax::names::NamePath, Span)>) {
849    match &type_expr.kind {
850        TypeExprKind::DimExpr(dim_expr) => collect_dim_refs(dim_expr, refs),
851        TypeExprKind::Indexed { base, indexes } => {
852            collect_type_refs(base, refs);
853            for idx in indexes {
854                if let IndexExpr::Name(path) = idx {
855                    refs.push((path.value.clone(), path.span));
856                }
857            }
858        }
859        TypeExprKind::TypeApplication { name, type_args } => {
860            refs.push((name.value.clone(), name.span));
861            for arg in type_args {
862                collect_type_refs(arg, refs);
863            }
864        }
865        TypeExprKind::DatetimeApplication { type_args } => {
866            // No top-level name to record — `Datetime` is built-in. Recurse
867            // into the args so any user-defined name reachable from the time
868            // scale expression is still collected.
869            for arg in type_args {
870                collect_type_refs(arg, refs);
871            }
872        }
873        TypeExprKind::Dimensionless
874        | TypeExprKind::Bool
875        | TypeExprKind::Int
876        | TypeExprKind::Datetime => {}
877    }
878}
879
880/// Collect every term name in a [`DimExpr`] as a `(name, span)` reference.
881fn collect_dim_refs(dim_expr: &DimExpr, refs: &mut Vec<(crate::syntax::names::NamePath, Span)>) {
882    for item in &dim_expr.terms {
883        refs.push((item.term.name.value.clone(), item.term.span));
884    }
885}
886
887/// Classify the owning declaration of a referenced name for diagnostic messages.
888fn ref_kind_for(file: &File, ref_name: &str) -> &'static str {
889    match file
890        .declarations
891        .iter()
892        .find(|d| match &d.kind {
893            DeclKind::BaseDimension(bd) => bd.name.value.as_str() == ref_name,
894            DeclKind::Dimension(d) => d.name.value.as_str() == ref_name,
895            DeclKind::Index(idx) => idx.name.value.as_str() == ref_name,
896            DeclKind::Type(t) => t.name.value.as_str() == ref_name,
897            _ => false,
898        })
899        .map(|d| &d.kind)
900    {
901        Some(DeclKind::BaseDimension(_) | DeclKind::Dimension(_)) => "dim",
902        Some(DeclKind::Index(_)) => "index",
903        Some(DeclKind::Type(_)) => "type",
904        _ => "item",
905    }
906}
907
908/// Declarations imported from other files, to be injected into the resolve scope.
909///
910/// These are treated as if they were declared locally, appearing before local declarations.
911#[derive(Debug, Default)]
912pub(crate) struct ImportedNames {
913    pub consts: Vec<(String, TypeExpr, Expr, Span)>,
914    pub params: Vec<(String, TypeExpr, Expr, Span)>,
915    pub nodes: Vec<(String, TypeExpr, Expr, Span)>,
916    pub asserts: Vec<(String, AssertBody, Span)>,
917}
918
919/// Collect declaration entries and validate declaration shells.
920///
921/// Reference resolution and dependency extraction happen in HIR lowering;
922/// this pass checks duplicates, visibility rules, and attributes.
923///
924/// # Errors
925///
926/// Returns a [`GraphcalError`] if duplicate names or invalid declaration
927/// shells are found.
928pub fn resolve(file: &File, src: &NamedSource<Arc<String>>) -> Result<ResolvedFile, GraphcalError> {
929    resolve_with_imports(file, src, &ImportedNames::default())
930}
931
932/// Resolve names with imported declarations injected into scope.
933///
934/// Imported declarations are prepended to the local declarations, so they appear
935/// first in eval order. The downstream pipeline (`dim_check`, `const_eval`, DAG, evaluate)
936/// works without changes because imported params/nodes become part of the DAG.
937///
938/// # Errors
939///
940/// Returns a [`GraphcalError`] if duplicate names or invalid declaration
941/// shells are found.
942pub(crate) fn resolve_with_imports(
943    file: &File,
944    src: &NamedSource<Arc<String>>,
945    imported: &ImportedNames,
946) -> Result<ResolvedFile, GraphcalError> {
947    let mut names: HashMap<ScopedName, Span> = HashMap::new();
948
949    // Pre-populate with imported names (they don't get duplicate-checked against
950    // each other here because they were validated in their source files).
951    for (name, _, _, span) in &imported.consts {
952        names.insert(ScopedName::local(name.as_str()), *span);
953    }
954    for (name, _, _, span) in &imported.params {
955        names.insert(ScopedName::local(name.as_str()), *span);
956    }
957    for (name, _, _, span) in &imported.nodes {
958        names.insert(ScopedName::local(name.as_str()), *span);
959    }
960    for (name, _, span) in &imported.asserts {
961        names.insert(ScopedName::local(name.as_str()), *span);
962    }
963
964    // Collect local declarations
965    let local = collect_local_declarations(file, src, &mut names)?;
966
967    // Build assert names (imported + local) for attribute validation
968    let mut all_assert_names: HashSet<DeclName> = HashSet::new();
969    for (name, _, _) in &imported.asserts {
970        all_assert_names.insert(DeclName::new(name.as_str()));
971    }
972    all_assert_names.extend(local.assert_names.iter().cloned());
973
974    // Prepend imported declarations so they appear before local ones in eval order.
975    // Strip TypeExpr from imported tuples and convert to entry types.
976    let mut all_consts: Vec<ResolvedConstEntry> = imported
977        .consts
978        .iter()
979        .map(|(name, _, expr, span)| ResolvedConstEntry {
980            name: name.clone(),
981            expr: expr.clone(),
982            span: *span,
983        })
984        .collect();
985    all_consts.extend(local.consts);
986    let mut all_params: Vec<ResolvedParamEntry> = imported
987        .params
988        .iter()
989        .map(|(name, _, expr, span)| ResolvedParamEntry {
990            name: name.clone(),
991            default_expr: Some(expr.clone()),
992            span: *span,
993        })
994        .collect();
995    all_params.extend(local.params);
996    let mut all_nodes: Vec<ResolvedNodeEntry> = imported
997        .nodes
998        .iter()
999        .map(|(name, _, expr, span)| ResolvedNodeEntry {
1000            name: name.clone(),
1001            expr: expr.clone(),
1002            span: *span,
1003        })
1004        .collect();
1005    all_nodes.extend(local.nodes);
1006    let mut all_asserts: Vec<ResolvedAssertEntry> = imported
1007        .asserts
1008        .iter()
1009        .map(|(name, body, span)| ResolvedAssertEntry {
1010            name: name.clone(),
1011            body: body.clone(),
1012            span: *span,
1013        })
1014        .collect();
1015    all_asserts.extend(local.asserts);
1016
1017    // Prepend imported source_order entries
1018    let mut all_source_order: Vec<(DeclName, DeclCategory)> = Vec::new();
1019    for (name, _, _, _) in &imported.consts {
1020        all_source_order.push((DeclName::new(name.as_str()), DeclCategory::Const));
1021    }
1022    for (name, _, _, _) in &imported.params {
1023        all_source_order.push((DeclName::new(name.as_str()), DeclCategory::Param));
1024    }
1025    for (name, _, _, _) in &imported.nodes {
1026        all_source_order.push((DeclName::new(name.as_str()), DeclCategory::Node));
1027    }
1028    for (name, _, _) in &imported.asserts {
1029        all_source_order.push((DeclName::new(name.as_str()), DeclCategory::Assert));
1030    }
1031    all_source_order.extend(local.source_order);
1032
1033    // Validate attributes and build assumes_map / expected_fail_map
1034    let validated = validate_attributes(file, src, &all_assert_names)?;
1035
1036    // Validate private-in-public: pub declarations must not reference private type-system items
1037    validate_private_in_public(file, src, &local.pub_names)?;
1038
1039    Ok(ResolvedFile {
1040        consts: all_consts,
1041        params: all_params,
1042        nodes: all_nodes,
1043        asserts: all_asserts,
1044        plots: local.plots,
1045        figures: local.figures,
1046        layers: local.layers,
1047        source_order: all_source_order,
1048        assert_names: all_assert_names,
1049        assumes_map: validated.assumes_map,
1050        expected_fail: validated.expected_fail_map,
1051        hidden_plots: validated.hidden_plots,
1052        pub_names: local.pub_names,
1053    })
1054}
1055
1056/// Resolve names with pre-evaluated imported value names in scope.
1057///
1058/// Unlike [`resolve_with_imports`], this does **not** inject imported expressions
1059/// into the DAG. Imported names are only used for scope checking (so that
1060/// references to imported values are recognized as valid). The actual values
1061/// are injected later via the execution plan.
1062///
1063/// # Errors
1064///
1065/// Returns a [`GraphcalError`] if duplicate names, unknown references, or
1066/// arity mismatches are found.
1067pub(crate) fn resolve_with_imported_values(
1068    file: &File,
1069    src: &NamedSource<Arc<String>>,
1070    imported: &ImportedValueNames,
1071) -> Result<ResolvedFile, GraphcalError> {
1072    let mut names: HashMap<ScopedName, Span> = HashMap::new();
1073
1074    // Pre-populate with imported names. The scope here mixes typed imported
1075    // `ScopedName`s (which may be `Qualified` for module aliases) with
1076    // local declarations; both share the same key type so the value-namespace
1077    // collision check sees the complete scope.
1078    for (name, span) in &imported.const_names {
1079        names.insert(name.clone(), *span);
1080    }
1081    for (name, span) in &imported.param_names {
1082        names.insert(name.clone(), *span);
1083    }
1084    for (name, span) in &imported.node_names {
1085        names.insert(name.clone(), *span);
1086    }
1087    for (name, span) in &imported.assert_names {
1088        names.insert(ScopedName::local(name.as_str()), *span);
1089    }
1090    for (name, span) in &imported.plot_names {
1091        names.insert(name.clone(), *span);
1092    }
1093
1094    // Collect local declarations
1095    let local = collect_local_declarations(file, src, &mut names)?;
1096
1097    // Build assert names (imported + local) for attribute validation
1098    let mut all_assert_names: HashSet<DeclName> = HashSet::new();
1099    for (name, _) in &imported.assert_names {
1100        all_assert_names.insert(name.clone());
1101    }
1102    all_assert_names.extend(local.assert_names.iter().cloned());
1103
1104    // Validate attributes and build assumes_map / expected_fail_map
1105    let validated = validate_attributes(file, src, &all_assert_names)?;
1106
1107    // Validate private-in-public: pub declarations must not reference private type-system items
1108    validate_private_in_public(file, src, &local.pub_names)?;
1109
1110    Ok(ResolvedFile {
1111        consts: local.consts,
1112        params: local.params,
1113        nodes: local.nodes,
1114        asserts: local.asserts,
1115        plots: local.plots,
1116        figures: local.figures,
1117        layers: local.layers,
1118        source_order: local.source_order,
1119        assert_names: all_assert_names,
1120        assumes_map: validated.assumes_map,
1121        expected_fail: validated.expected_fail_map,
1122        hidden_plots: validated.hidden_plots,
1123        pub_names: local.pub_names,
1124    })
1125}