Skip to main content

graphcal_compiler/syntax/
names.rs

1//! Typed name atoms and namespace-specific name wrappers.
2//!
3//! Source identifiers are path segments first; semantic namespace wrappers are
4//! layered on top only at definition or resolution boundaries. The wrappers in
5//! this module therefore store a [`NameAtom`] rather than an arbitrary flat
6//! string, making it impossible to represent a dotted path as a leaf name.
7
8/// Error returned when constructing a [`NameAtom`] from invalid text.
9#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
10pub enum NameAtomError {
11    /// Name atoms are leaf segments and cannot be empty.
12    #[error("name atom cannot be empty")]
13    Empty,
14    /// Dots separate path segments; they are not valid inside a single atom.
15    #[error("name atom cannot contain `.`")]
16    ContainsDot,
17}
18
19/// A single name segment with no path separators.
20///
21/// `NameAtom` deliberately models only the leaf/segment invariant. It does not
22/// attempt to encode the full lexer grammar because some internal names, such
23/// as synthetic range variants (`#0`, `#1`, ...), are not source identifiers but
24/// still must never contain `.`.
25#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
26pub struct NameAtom(String);
27
28impl NameAtom {
29    /// Parse a raw string into a single name segment.
30    ///
31    /// # Errors
32    ///
33    /// Returns [`NameAtomError::Empty`] for empty strings and
34    /// [`NameAtomError::ContainsDot`] when the text contains a path separator.
35    pub fn parse(s: impl Into<String>) -> Result<Self, NameAtomError> {
36        let s = s.into();
37        if s.is_empty() {
38            return Err(NameAtomError::Empty);
39        }
40        if s.contains('.') {
41            return Err(NameAtomError::ContainsDot);
42        }
43        Ok(Self(s))
44    }
45
46    /// Construct an atom from lexer-produced identifier text.
47    ///
48    /// The parser has already tokenized this as a single `IDENT`, so the same
49    /// invariant is asserted here without making parser code handle an
50    /// impossible error path.
51    #[must_use]
52    pub(crate) fn new_unchecked_for_parser(s: String) -> Self {
53        debug_assert!(Self::parse(s.as_str()).is_ok());
54        Self(s)
55    }
56
57    /// Get the underlying string slice.
58    #[must_use]
59    pub fn as_str(&self) -> &str {
60        &self.0
61    }
62
63    /// Consume and return the inner `String`.
64    #[must_use]
65    pub fn into_inner(self) -> String {
66        self.0
67    }
68}
69
70impl std::fmt::Debug for NameAtom {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        std::fmt::Debug::fmt(&self.0, f)
73    }
74}
75
76impl std::fmt::Display for NameAtom {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        f.write_str(&self.0)
79    }
80}
81
82impl std::ops::Deref for NameAtom {
83    type Target = str;
84
85    fn deref(&self) -> &Self::Target {
86        self.as_str()
87    }
88}
89
90impl PartialEq<str> for NameAtom {
91    fn eq(&self, other: &str) -> bool {
92        self.as_str() == other
93    }
94}
95
96impl PartialEq<&str> for NameAtom {
97    fn eq(&self, other: &&str) -> bool {
98        self.as_str() == *other
99    }
100}
101
102impl PartialEq<String> for NameAtom {
103    fn eq(&self, other: &String) -> bool {
104        self.as_str() == other
105    }
106}
107
108impl PartialEq<NameAtom> for str {
109    fn eq(&self, other: &NameAtom) -> bool {
110        self == other.as_str()
111    }
112}
113
114impl PartialEq<NameAtom> for &str {
115    fn eq(&self, other: &NameAtom) -> bool {
116        *self == other.as_str()
117    }
118}
119
120impl AsRef<str> for NameAtom {
121    fn as_ref(&self) -> &str {
122        self.as_str()
123    }
124}
125
126impl std::borrow::Borrow<str> for NameAtom {
127    fn borrow(&self) -> &str {
128        self.as_str()
129    }
130}
131
132impl From<NameAtom> for String {
133    fn from(atom: NameAtom) -> Self {
134        atom.into_inner()
135    }
136}
137
138impl From<&NameAtom> for String {
139    fn from(atom: &NameAtom) -> Self {
140        atom.as_str().to_string()
141    }
142}
143
144impl From<NameAtom> for std::borrow::Cow<'_, str> {
145    fn from(atom: NameAtom) -> Self {
146        Self::Owned(atom.into_inner())
147    }
148}
149
150impl TryFrom<String> for NameAtom {
151    type Error = NameAtomError;
152
153    fn try_from(value: String) -> Result<Self, Self::Error> {
154        Self::parse(value)
155    }
156}
157
158impl TryFrom<&str> for NameAtom {
159    type Error = NameAtomError;
160
161    fn try_from(value: &str) -> Result<Self, Self::Error> {
162        Self::parse(value)
163    }
164}
165
166use std::marker::PhantomData;
167
168/// Marker trait for a semantic name namespace.
169///
170/// Namespaces are zero-sized marker types used by [`NameDef`] and
171/// [`ResolvedName`] to make it impossible to mix, for example, a function name
172/// with an index name. The marker's [`NameNamespace::DISPLAY_NAME`] is used
173/// only for diagnostics and panic messages at construction boundaries.
174pub trait NameNamespace:
175    std::fmt::Debug + Clone + Copy + PartialEq + Eq + std::hash::Hash + PartialOrd + Ord + 'static
176{
177    /// Human-readable alias/newtype name for this namespace.
178    const DISPLAY_NAME: &'static str;
179}
180
181/// Semantic name namespace markers.
182///
183/// These are marker types only; values of these types are never constructed.
184pub mod namespace {
185    use super::NameNamespace;
186
187    macro_rules! define_namespace {
188        ($($(#[$meta:meta])* $Name:ident => $display:literal;)+) => {
189            $(
190                $(#[$meta])*
191                #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
192                pub enum $Name {}
193
194                impl NameNamespace for $Name {
195                    const DISPLAY_NAME: &'static str = $display;
196                }
197            )+
198        };
199    }
200
201    define_namespace! {
202        /// Const/param/node declaration namespace.
203        Decl => "DeclName";
204        /// Dimension namespace.
205        Dim => "DimName";
206        /// Unit namespace.
207        Unit => "UnitName";
208        /// Struct/tagged-union type namespace.
209        StructType => "StructTypeName";
210        /// Index type namespace.
211        Index => "IndexName";
212        /// Function namespace.
213        Fn => "FnName";
214        /// Struct/constructor field namespace.
215        Field => "FieldName";
216        /// Index variant namespace.
217        IndexVariant => "IndexVariantName";
218        /// Tagged-union constructor namespace.
219        Constructor => "ConstructorName";
220        /// Generic parameter namespace.
221        GenericParam => "GenericParamName";
222        /// Built-in dimension-variable namespace.
223        DimVar => "DimVarName";
224        /// Local expression-binding namespace.
225        Local => "LocalName";
226        /// Module alias namespace.
227        ModuleAlias => "ModuleAliasName";
228        /// Plot/figure/layer property namespace.
229        PlotProperty => "PlotPropertyName";
230    }
231}
232
233/// A definition-site leaf name in a semantic namespace.
234///
235/// `NameDef<Ns>` is intentionally a single [`NameAtom`]. It is suitable for
236/// names introduced by syntax positions whose namespace is fixed by the
237/// grammar, such as `type Foo`, `index Phase`, or `unit m`. Reference positions
238/// that may be qualified should stay as [`NamePath`]
239/// / [`IdentPath`](crate::syntax::ast::IdentPath) until module-aware
240/// resolution can produce a [`ResolvedName`].
241#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
242pub struct NameDef<Ns: NameNamespace> {
243    atom: NameAtom,
244    _ns: PhantomData<Ns>,
245}
246
247impl<Ns: NameNamespace> std::fmt::Debug for NameDef<Ns> {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        // Delegate to the inner string's Debug so that Vec<DeclName> formats
250        // as ["foo", "bar"] rather than [NameDef { ... }].
251        std::fmt::Debug::fmt(&self.atom, f)
252    }
253}
254
255impl<Ns: NameNamespace> NameDef<Ns> {
256    /// Create a new leaf name from a string.
257    ///
258    /// # Panics
259    ///
260    /// Panics if the string is empty or contains `.`. Use [`Self::try_new`]
261    /// when validating external input.
262    #[must_use]
263    #[expect(
264        clippy::panic,
265        reason = "infallible constructor documents invalid input panic"
266    )]
267    pub fn new(s: impl Into<String>) -> Self {
268        Self::try_new(s).unwrap_or_else(|err| {
269            panic!("invalid {} leaf name: {err}", Ns::DISPLAY_NAME);
270        })
271    }
272
273    /// Try to create a new leaf name from a string.
274    ///
275    /// # Errors
276    ///
277    /// Returns [`NameAtomError`] when the string is empty or contains a path
278    /// separator.
279    pub fn try_new(s: impl Into<String>) -> Result<Self, NameAtomError> {
280        NameAtom::parse(s).map(Self::from_atom)
281    }
282
283    /// Create this namespace-specific name from an existing atom.
284    #[must_use]
285    pub const fn from_atom(atom: NameAtom) -> Self {
286        Self {
287            atom,
288            _ns: PhantomData,
289        }
290    }
291
292    /// Get the underlying atom.
293    #[must_use]
294    pub const fn atom(&self) -> &NameAtom {
295        &self.atom
296    }
297
298    /// Get the underlying string slice.
299    #[must_use]
300    pub fn as_str(&self) -> &str {
301        self.atom.as_str()
302    }
303
304    /// Consume and return the inner atom.
305    #[must_use]
306    pub fn into_atom(self) -> NameAtom {
307        self.atom
308    }
309
310    /// Consume and return the inner `String`.
311    #[must_use]
312    pub fn into_inner(self) -> String {
313        self.atom.into_inner()
314    }
315}
316
317impl<Ns: NameNamespace> std::fmt::Display for NameDef<Ns> {
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        f.write_str(self.as_str())
320    }
321}
322
323impl<Ns: NameNamespace> PartialEq<str> for NameDef<Ns> {
324    fn eq(&self, other: &str) -> bool {
325        self.as_str() == other
326    }
327}
328
329impl<Ns: NameNamespace> PartialEq<&str> for NameDef<Ns> {
330    fn eq(&self, other: &&str) -> bool {
331        self.as_str() == *other
332    }
333}
334
335impl<Ns: NameNamespace> AsRef<str> for NameDef<Ns> {
336    fn as_ref(&self) -> &str {
337        self.as_str()
338    }
339}
340
341impl<Ns: NameNamespace> std::borrow::Borrow<str> for NameDef<Ns> {
342    fn borrow(&self) -> &str {
343        self.as_str()
344    }
345}
346
347impl<Ns: NameNamespace> From<NameAtom> for NameDef<Ns> {
348    fn from(atom: NameAtom) -> Self {
349        Self::from_atom(atom)
350    }
351}
352
353impl<Ns: NameNamespace> From<String> for NameDef<Ns> {
354    fn from(s: String) -> Self {
355        Self::new(s)
356    }
357}
358
359impl<Ns: NameNamespace> From<&str> for NameDef<Ns> {
360    fn from(s: &str) -> Self {
361        Self::new(s)
362    }
363}
364
365/// A fully resolved reference in a semantic namespace.
366///
367/// Unlike [`NamePath`], this no longer stores source qualifier text. The
368/// `owner` is the canonical DAG/module identity chosen by module-aware
369/// resolution; `name` is the declaration leaf inside that owner.
370#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
371pub struct ResolvedName<Ns: NameNamespace> {
372    owner: crate::dag_id::DagId,
373    name: NameAtom,
374    _ns: PhantomData<Ns>,
375}
376
377impl<Ns: NameNamespace> std::fmt::Debug for ResolvedName<Ns> {
378    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
379        f.debug_struct("ResolvedName")
380            .field("namespace", &Ns::DISPLAY_NAME)
381            .field("owner", &self.owner)
382            .field("name", &self.name)
383            .finish()
384    }
385}
386
387impl<Ns: NameNamespace> ResolvedName<Ns> {
388    /// Construct a resolved name from its canonical owner and leaf atom.
389    #[must_use]
390    pub const fn new(owner: crate::dag_id::DagId, name: NameAtom) -> Self {
391        Self {
392            owner,
393            name,
394            _ns: PhantomData,
395        }
396    }
397
398    /// Resolve an existing definition-site name into a canonical owner.
399    #[must_use]
400    pub fn from_def(owner: crate::dag_id::DagId, name: NameDef<Ns>) -> Self {
401        Self::new(owner, name.into_atom())
402    }
403
404    /// The canonical DAG/module that owns this name.
405    #[must_use]
406    pub const fn owner(&self) -> &crate::dag_id::DagId {
407        &self.owner
408    }
409
410    /// The leaf atom inside [`Self::owner`].
411    #[must_use]
412    pub const fn atom(&self) -> &NameAtom {
413        &self.name
414    }
415
416    /// The leaf string inside [`Self::owner`].
417    #[must_use]
418    pub fn as_str(&self) -> &str {
419        self.name.as_str()
420    }
421
422    /// Return the unowned definition-site leaf in the same namespace.
423    ///
424    /// This deliberately drops the canonical owner. Use it only at explicit
425    /// standalone registry, diagnostic, or serialization boundaries that
426    /// cannot yet carry [`ResolvedName`] itself.
427    #[must_use]
428    pub fn to_unowned_def_name(&self) -> NameDef<Ns> {
429        NameDef::from_atom(self.name.clone())
430    }
431
432    /// Consume this value and return the canonical owner plus leaf atom.
433    #[must_use]
434    pub fn into_parts(self) -> (crate::dag_id::DagId, NameAtom) {
435        (self.owner, self.name)
436    }
437}
438
439impl<Ns: NameNamespace> std::fmt::Display for ResolvedName<Ns> {
440    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441        write!(f, "{}.{}", self.owner, self.name)
442    }
443}
444
445/// Name of a const, param, or node declaration (e.g., `"G0"`, `"dry_mass"`, `"dv_total"`).
446pub type DeclName = NameDef<namespace::Decl>;
447
448/// Name of a dimension (e.g., `"Length"`, `"Velocity"`).
449pub type DimName = NameDef<namespace::Dim>;
450
451/// Name of a unit (e.g., `"m"`, `"km"`, `"hour"`).
452pub type UnitName = NameDef<namespace::Unit>;
453
454/// Name of a struct type (e.g., `"TransferResult"`).
455pub type StructTypeName = NameDef<namespace::StructType>;
456
457/// Name of an index type (e.g., `"Maneuver"`).
458pub type IndexName = NameDef<namespace::Index>;
459
460/// Name of a function (e.g., `"sqrt"`, `"lerp"`).
461pub type FnName = NameDef<namespace::Fn>;
462
463/// Name of a struct field (e.g., `"dv1"`, `"altitude"`).
464pub type FieldName = NameDef<namespace::Field>;
465
466/// Name of an index variant (e.g., `"Departure"`, `"Correction"`).
467pub type IndexVariantName = NameDef<namespace::IndexVariant>;
468
469/// Name of a tagged-union constructor (e.g., `"LowThrust"`, `"Coast"`).
470///
471/// Constructors live in a *separate namespace* from types: a single lexeme can
472/// name both a type and a constructor (and will, once the single-variant sugar
473/// lands). Keeping these distinct marker namespaces enforces the boundary at
474/// the type level.
475pub type ConstructorName = NameDef<namespace::Constructor>;
476
477/// Name of a generic type parameter (e.g., `"D"`, `"I"`).
478pub type GenericParamName = NameDef<namespace::GenericParam>;
479
480/// Name of a dimension variable in a built-in function signature (e.g., `"D"`).
481///
482/// Built-in signatures use these variables to relate argument and result
483/// dimensions, such as `sqrt: D -> D^(1/2)` or `min: (D, D) -> D`.
484pub type DimVarName = NameDef<namespace::DimVar>;
485
486/// Name of a local expression binding (e.g., `"x"`, `"stage_mass"`).
487pub type LocalName = NameDef<namespace::Local>;
488
489/// Name of a module alias introduced by an import/include declaration (e.g., `"constants"`, `"std"`).
490pub type ModuleAliasName = NameDef<namespace::ModuleAlias>;
491
492/// Name of an open plot/figure/layer property (e.g., `"title"`, `"width"`, `"stroke_width"`).
493pub type PlotPropertyName = NameDef<namespace::PlotProperty>;
494
495impl IndexVariantName {
496    /// Build the variant name for the `n`-th step of a range index
497    /// (`#0`, `#1`, …). Centralises the `"#"`-prefix format so registry,
498    /// parser, and evaluator can't disagree on it.
499    #[must_use]
500    pub fn range_step(n: impl std::fmt::Display) -> Self {
501        Self::new(format!("#{n}"))
502    }
503
504    /// Pair this variant with its index name for qualified rendering.
505    #[must_use]
506    pub fn qualified_by(&self, index: &IndexName) -> QualifiedIndexVariantName {
507        QualifiedIndexVariantName::new(index.clone(), self.clone())
508    }
509}
510
511/// A fully qualified index variant name, rendered as `Index.Variant`.
512#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
513pub struct QualifiedIndexVariantName {
514    index: IndexName,
515    variant: IndexVariantName,
516}
517
518impl QualifiedIndexVariantName {
519    /// Create a qualified index variant name from its index and variant parts.
520    #[must_use]
521    pub const fn new(index: IndexName, variant: IndexVariantName) -> Self {
522        Self { index, variant }
523    }
524
525    /// The index/type part of the qualified variant.
526    #[must_use]
527    pub const fn index(&self) -> &IndexName {
528        &self.index
529    }
530
531    /// The variant/constructor part of the qualified variant.
532    #[must_use]
533    pub const fn variant(&self) -> &IndexVariantName {
534        &self.variant
535    }
536}
537
538impl std::fmt::Display for QualifiedIndexVariantName {
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        write!(f, "{}.{}", self.index, self.variant)
541    }
542}
543
544/// A fully resolved index variant reference.
545///
546/// Index variants are owned by an index declaration rather than directly by a
547/// DAG/module. This type therefore resolves the index itself to a canonical
548/// owner, then stores the variant as a leaf in that index's variant set.
549#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
550pub struct ResolvedIndexVariant {
551    index: ResolvedName<namespace::Index>,
552    variant: IndexVariantName,
553}
554
555impl ResolvedIndexVariant {
556    /// Create a resolved index-variant reference from its resolved index and
557    /// variant leaf.
558    #[must_use]
559    pub const fn new(index: ResolvedName<namespace::Index>, variant: IndexVariantName) -> Self {
560        Self { index, variant }
561    }
562
563    /// The resolved index that owns this variant.
564    #[must_use]
565    pub const fn index(&self) -> &ResolvedName<namespace::Index> {
566        &self.index
567    }
568
569    /// The variant leaf inside [`Self::index`].
570    #[must_use]
571    pub const fn variant(&self) -> &IndexVariantName {
572        &self.variant
573    }
574
575    /// Consume this value and return its typed parts.
576    #[must_use]
577    pub fn into_parts(self) -> (ResolvedName<namespace::Index>, IndexVariantName) {
578        (self.index, self.variant)
579    }
580}
581
582impl std::fmt::Debug for ResolvedIndexVariant {
583    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584        f.debug_struct("ResolvedIndexVariant")
585            .field("index", &self.index)
586            .field("variant", &self.variant)
587            .finish()
588    }
589}
590
591impl std::fmt::Display for ResolvedIndexVariant {
592    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
593        write!(f, "{}.{}", self.index, self.variant)
594    }
595}
596
597/// Name of a built-in datetime time scale (e.g., `"UTC"`, `"TAI"`, `"TDB"`).
598#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
599pub struct TimeScaleName(crate::registry::time_scale::TimeScale);
600
601impl TimeScaleName {
602    /// Create a time-scale name from an already-validated time scale.
603    #[must_use]
604    pub const fn new(scale: crate::registry::time_scale::TimeScale) -> Self {
605        Self(scale)
606    }
607
608    /// Get the underlying time scale.
609    #[must_use]
610    pub const fn scale(self) -> crate::registry::time_scale::TimeScale {
611        self.0
612    }
613
614    /// Get the canonical time-scale name.
615    #[must_use]
616    pub const fn as_str(self) -> &'static str {
617        self.0.name()
618    }
619}
620
621impl std::fmt::Display for TimeScaleName {
622    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
623        f.write_str(self.as_str())
624    }
625}
626
627impl AsRef<str> for TimeScaleName {
628    fn as_ref(&self) -> &str {
629        self.as_str()
630    }
631}
632
633// --- Module-scoped names ---
634
635use std::sync::Arc;
636
637/// A declaration name that may optionally be qualified by a module path.
638///
639/// The qualifier is stored as structured path segments, not as a flat
640/// dot-separated string. This allows arbitrary-depth qualification such as
641/// `helpers.math.G0` while keeping the declaration member (`G0`) directly
642/// accessible and distinct from the qualifier.
643///
644/// The `Display` impl renders `qualifier: ["helpers", "math"], member: "G0"`
645/// as `helpers.math.G0`. That serialized form is for boundary use only
646/// (diagnostics, debug output, third-party APIs); the compiler core should use
647/// the typed accessors instead of splitting strings.
648#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
649pub struct ScopedName {
650    /// Module/path segments that qualify `member`. Empty for a local name.
651    qualifier: Arc<[Arc<str>]>,
652    /// The declaration/member name inside the qualifier scope.
653    member: Arc<str>,
654}
655
656impl ScopedName {
657    /// Create an unqualified local name.
658    #[must_use]
659    pub fn local(member: impl Into<Arc<str>>) -> Self {
660        Self {
661            qualifier: Arc::from([] as [Arc<str>; 0]),
662            member: member.into(),
663        }
664    }
665
666    /// Create a name qualified by a single module segment.
667    #[must_use]
668    pub fn qualified(module: impl Into<Arc<str>>, member: impl Into<Arc<str>>) -> Self {
669        Self::qualified_path([module], member)
670    }
671
672    /// Create a name qualified by an arbitrary-depth module path.
673    #[must_use]
674    pub fn qualified_path(
675        qualifier: impl IntoIterator<Item = impl Into<Arc<str>>>,
676        member: impl Into<Arc<str>>,
677    ) -> Self {
678        Self {
679            qualifier: qualifier.into_iter().map(Into::into).collect(),
680            member: member.into(),
681        }
682    }
683
684    /// Returns the member (leaf declaration) part of the name.
685    ///
686    /// For `x` this returns `"x"`; for `helpers.math.x` this also returns
687    /// `"x"`.
688    #[must_use]
689    pub fn member(&self) -> &str {
690        &self.member
691    }
692
693    /// Returns the qualifier path segments. Empty means this name is local.
694    #[must_use]
695    pub fn qualifier(&self) -> &[Arc<str>] {
696        &self.qualifier
697    }
698
699    /// Returns whether this is a qualified name.
700    #[must_use]
701    pub fn is_qualified(&self) -> bool {
702        !self.qualifier.is_empty()
703    }
704
705    /// Qualify a name with a single-segment prefix, replacing any existing
706    /// qualifier while preserving the member.
707    ///
708    /// `x.with_prefix("p")` → `p.x`.
709    /// `m.x.with_prefix("p")` → `p.x`.
710    #[must_use]
711    pub fn with_prefix(&self, prefix: &str) -> Self {
712        Self::qualified(prefix, Arc::clone(&self.member))
713    }
714}
715
716impl std::fmt::Display for ScopedName {
717    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718        for segment in self.qualifier.iter() {
719            f.write_str(segment)?;
720            f.write_str(".")?;
721        }
722        f.write_str(&self.member)
723    }
724}
725
726impl From<NameAtom> for ScopedName {
727    /// Wrap a bare atom as a local `ScopedName`. This is what
728    /// [`crate::syntax::ast::Ident::into_spanned`] uses to lift parser
729    /// identifiers into the typed name; qualified forms are constructed
730    /// explicitly via [`ScopedName::qualified`] or [`ScopedName::qualified_path`].
731    fn from(atom: NameAtom) -> Self {
732        Self::local(atom.into_inner())
733    }
734}
735
736impl From<String> for ScopedName {
737    /// Wrap a bare string as a local `ScopedName`.
738    fn from(s: String) -> Self {
739        Self::local(s)
740    }
741}
742
743impl From<DeclName> for ScopedName {
744    /// Wrap a `DeclName` as a local `ScopedName`. Use this at the resolver →
745    /// IR boundary where resolver keys (local `DeclName`s) become IR keys
746    /// (`ScopedName`s).
747    fn from(name: DeclName) -> Self {
748        Self::local(name.into_inner())
749    }
750}
751
752/// A unit reference, optionally qualified by a module alias.
753///
754/// Unit references follow the same scoping rules as every other imported
755/// category: a bare name (`mile`) refers to a local declaration, a selective
756/// import, or a prelude unit; a qualified name (`u.mile`) refers to a `pub`
757/// unit of the module imported as `u`. The qualifier is at most one module
758/// alias — unit references never nest deeper.
759///
760/// The `Display` impl renders `u.mile` / `mile` for diagnostics and
761/// formatting boundaries only; the compiler core matches on the typed parts.
762#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
763pub struct UnitRef {
764    /// Module alias qualifying `name`, or `None` for a file-local reference.
765    qualifier: Option<ModuleAliasName>,
766    /// The unit leaf name inside the qualifier scope.
767    name: UnitName,
768}
769
770impl UnitRef {
771    /// Create an unqualified (file-local, selective-import, or prelude) unit reference.
772    #[must_use]
773    pub fn local(name: impl Into<UnitName>) -> Self {
774        Self {
775            qualifier: None,
776            name: name.into(),
777        }
778    }
779
780    /// Create a unit reference qualified by a module alias (`u.mile`).
781    #[must_use]
782    pub const fn qualified(qualifier: ModuleAliasName, name: UnitName) -> Self {
783        Self {
784            qualifier: Some(qualifier),
785            name,
786        }
787    }
788
789    /// The module alias qualifying this reference, if any.
790    #[must_use]
791    pub const fn qualifier(&self) -> Option<&ModuleAliasName> {
792        self.qualifier.as_ref()
793    }
794
795    /// The unit leaf name.
796    #[must_use]
797    pub const fn name(&self) -> &UnitName {
798        &self.name
799    }
800
801    /// Returns whether this reference is module-qualified.
802    #[must_use]
803    pub const fn is_qualified(&self) -> bool {
804        self.qualifier.is_some()
805    }
806}
807
808impl From<UnitName> for UnitRef {
809    /// Wrap a bare unit name as a local reference. Definition sites always
810    /// produce local references; qualified forms are constructed explicitly
811    /// via [`UnitRef::qualified`].
812    fn from(name: UnitName) -> Self {
813        Self::local(name)
814    }
815}
816
817impl From<NameAtom> for UnitRef {
818    /// Wrap a bare atom as a local unit reference. This is what
819    /// [`crate::syntax::ast::Ident::into_spanned`] uses to lift parser
820    /// identifiers into the typed reference.
821    fn from(atom: NameAtom) -> Self {
822        Self::local(UnitName::from_atom(atom))
823    }
824}
825
826impl std::fmt::Display for UnitRef {
827    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
828        if let Some(qualifier) = &self.qualifier {
829            write!(f, "{qualifier}.")?;
830        }
831        write!(f, "{}", self.name)
832    }
833}
834
835/// A syntactic non-empty dot-separated name path.
836///
837/// `NamePath` preserves source-level path shape (`Foo`, `module.Foo`,
838/// `module.Index.Variant`) without assigning a semantic namespace to any
839/// segment. It is appropriate for unresolved reference positions that do not
840/// need per-segment spans. Use [`crate::syntax::ast::IdentPath`] when the AST
841/// must retain source spans for each segment.
842#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
843pub struct NamePath {
844    segments: crate::syntax::non_empty::NonEmpty<NameAtom>,
845}
846
847impl NamePath {
848    /// Construct a path from already-validated atoms.
849    #[must_use]
850    pub const fn new(segments: crate::syntax::non_empty::NonEmpty<NameAtom>) -> Self {
851        Self { segments }
852    }
853
854    /// Construct a one-segment path.
855    #[must_use]
856    pub fn local(atom: NameAtom) -> Self {
857        Self::new(crate::syntax::non_empty::NonEmpty::singleton(atom))
858    }
859
860    /// Construct a path from qualifier atoms plus a leaf atom.
861    #[must_use]
862    pub fn qualified_path(qualifier: impl IntoIterator<Item = NameAtom>, leaf: NameAtom) -> Self {
863        let mut segments: Vec<NameAtom> = qualifier.into_iter().collect();
864        segments.push(leaf);
865        let first = segments.remove(0);
866        Self::new(crate::syntax::non_empty::NonEmpty::new(first, segments))
867    }
868
869    /// Borrow all path segments in source order.
870    #[must_use]
871    pub fn segments(&self) -> &[NameAtom] {
872        self.segments.as_slice()
873    }
874
875    /// Consume and return all path segments.
876    #[must_use]
877    pub fn into_segments(self) -> crate::syntax::non_empty::NonEmpty<NameAtom> {
878        self.segments
879    }
880
881    /// Number of path segments. Always at least 1.
882    #[must_use]
883    pub const fn len(&self) -> usize {
884        self.segments.len()
885    }
886
887    /// Returns `false`; provided for API compatibility with sequence-like code.
888    #[must_use]
889    pub const fn is_empty(&self) -> bool {
890        false
891    }
892
893    /// Returns whether this is a one-segment path.
894    #[must_use]
895    pub const fn is_bare(&self) -> bool {
896        self.segments.len() == 1
897    }
898
899    /// Returns the leaf segment.
900    #[must_use]
901    pub fn leaf(&self) -> &NameAtom {
902        self.segments.last()
903    }
904
905    /// Returns the only segment when this is a bare path.
906    #[must_use]
907    pub fn as_bare(&self) -> Option<&NameAtom> {
908        match self.segments.as_slice() {
909            [atom] => Some(atom),
910            _ => None,
911        }
912    }
913
914    /// Split the path into qualifier segments and leaf segment.
915    ///
916    /// The qualifier slice is empty for one-segment paths.
917    #[must_use]
918    pub fn split_last(&self) -> (&[NameAtom], &NameAtom) {
919        let (leaf, qualifier) = self.segments.split_last();
920        (qualifier, leaf)
921    }
922
923    /// Returns the qualifier segments before the leaf. Empty for bare paths.
924    #[must_use]
925    pub fn qualifier_segments(&self) -> &[NameAtom] {
926        self.split_last().0
927    }
928
929    /// Returns qualifier segments and leaf only when this path is qualified.
930    #[must_use]
931    pub fn qualifier_and_leaf(&self) -> Option<(&[NameAtom], &NameAtom)> {
932        let (qualifier, leaf) = self.split_last();
933        (!qualifier.is_empty()).then_some((qualifier, leaf))
934    }
935
936    /// Human-readable path string for diagnostics and formatting boundaries.
937    #[must_use]
938    pub fn display_path(&self) -> String {
939        self.segments
940            .iter()
941            .map(NameAtom::as_str)
942            .collect::<Vec<_>>()
943            .join(".")
944    }
945}
946
947impl From<NameAtom> for NamePath {
948    fn from(atom: NameAtom) -> Self {
949        Self::local(atom)
950    }
951}
952
953impl From<IndexName> for NamePath {
954    fn from(name: IndexName) -> Self {
955        Self::local(name.into_atom())
956    }
957}
958
959impl From<String> for NamePath {
960    #[expect(
961        clippy::panic,
962        reason = "From<String> is a convenience for trusted leaf names"
963    )]
964    fn from(s: String) -> Self {
965        Self::local(NameAtom::parse(s).unwrap_or_else(|err| {
966            panic!("invalid NamePath leaf name: {err}");
967        }))
968    }
969}
970
971impl From<&str> for NamePath {
972    fn from(s: &str) -> Self {
973        Self::from(s.to_string())
974    }
975}
976
977impl std::fmt::Display for NamePath {
978    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
979        for (idx, segment) in self.segments.iter().enumerate() {
980            if idx > 0 {
981                f.write_str(".")?;
982            }
983            f.write_str(segment.as_str())?;
984        }
985        Ok(())
986    }
987}
988
989#[cfg(test)]
990mod tests {
991    use super::*;
992    use std::collections::HashMap;
993
994    #[test]
995    fn name_atom_rejects_dotted_paths() {
996        assert_eq!(
997            NameAtom::parse("module.Value"),
998            Err(NameAtomError::ContainsDot)
999        );
1000        assert_eq!(
1001            DeclName::try_new("module.Value"),
1002            Err(NameAtomError::ContainsDot)
1003        );
1004    }
1005
1006    #[test]
1007    fn name_atom_accepts_internal_leaf_names() {
1008        let atom = NameAtom::parse("#0").unwrap();
1009        assert_eq!(atom.as_str(), "#0");
1010    }
1011
1012    #[test]
1013    fn newtype_display() {
1014        let name = DeclName::new("dry_mass");
1015        assert_eq!(format!("{name}"), "dry_mass");
1016    }
1017
1018    #[test]
1019    fn newtype_as_str() {
1020        let name = DimName::new("Length");
1021        assert_eq!(name.as_str(), "Length");
1022    }
1023
1024    #[test]
1025    fn newtype_into_inner() {
1026        let name = UnitName::new("km");
1027        assert_eq!(name.into_inner(), "km");
1028    }
1029
1030    #[test]
1031    fn newtype_hash_map_borrow_lookup() {
1032        let mut map = HashMap::new();
1033        map.insert(DeclName::new("x"), 42);
1034        // Lookup with &str via Borrow<str>
1035        assert_eq!(map.get("x"), Some(&42));
1036    }
1037
1038    #[test]
1039    fn newtype_from_string() {
1040        let name: FieldName = "dv1".to_string().into();
1041        assert_eq!(name.as_str(), "dv1");
1042    }
1043
1044    #[test]
1045    fn newtype_from_str() {
1046        let name: IndexVariantName = "Departure".into();
1047        assert_eq!(name.as_str(), "Departure");
1048    }
1049
1050    #[test]
1051    fn newtype_equality() {
1052        assert_eq!(IndexName::new("Maneuver"), IndexName::new("Maneuver"));
1053        assert_ne!(IndexName::new("Maneuver"), IndexName::new("Phase"));
1054    }
1055
1056    #[test]
1057    fn newtype_ord() {
1058        let a = FnName::new("alpha");
1059        let b = FnName::new("beta");
1060        assert!(a < b);
1061    }
1062
1063    #[test]
1064    fn name_path_preserves_qualifier_and_leaf() {
1065        let path = NamePath::qualified_path(
1066            [NameAtom::parse("module").unwrap()],
1067            NameAtom::parse("Index").unwrap(),
1068        );
1069        assert_eq!(path.display_path(), "module.Index");
1070        assert_eq!(path.leaf().as_str(), "Index");
1071        assert_eq!(
1072            path.qualifier_segments()
1073                .iter()
1074                .map(NameAtom::as_str)
1075                .collect::<Vec<_>>(),
1076            ["module"]
1077        );
1078    }
1079
1080    #[test]
1081    fn name_def_aliases_keep_namespace_and_leaf_invariant() {
1082        let decl = DeclName::new("x");
1083        let index = IndexName::new("x");
1084
1085        assert_eq!(decl.as_str(), index.as_str());
1086        assert_eq!(
1087            DeclName::try_new("module.x"),
1088            Err(NameAtomError::ContainsDot)
1089        );
1090        assert_eq!(
1091            IndexName::try_new("module.x"),
1092            Err(NameAtomError::ContainsDot)
1093        );
1094    }
1095
1096    #[test]
1097    fn resolved_name_carries_canonical_owner_and_leaf() {
1098        let name = DeclName::new("dry_mass");
1099        let resolved = ResolvedName::<namespace::Decl>::from_def(
1100            crate::dag_id::DagId::new("helpers", ["mass"]),
1101            name,
1102        );
1103
1104        assert_eq!(resolved.owner().to_string(), "helpers.mass");
1105        assert_eq!(resolved.as_str(), "dry_mass");
1106        assert_eq!(resolved.to_string(), "helpers.mass.dry_mass");
1107        assert_eq!(resolved.to_unowned_def_name(), DeclName::new("dry_mass"));
1108    }
1109
1110    #[test]
1111    fn resolved_index_variant_carries_resolved_index_owner() {
1112        let index = ResolvedName::<namespace::Index>::from_def(
1113            crate::dag_id::DagId::root("mission"),
1114            IndexName::new("Phase"),
1115        );
1116        let variant = ResolvedIndexVariant::new(index, IndexVariantName::new("Burn"));
1117
1118        assert_eq!(variant.index().owner().to_string(), "mission");
1119        assert_eq!(variant.index().as_str(), "Phase");
1120        assert_eq!(variant.variant().as_str(), "Burn");
1121        assert_eq!(variant.to_string(), "mission.Phase.Burn");
1122    }
1123
1124    #[test]
1125    fn scoped_name_qualified_display_uses_dot() {
1126        let name = ScopedName::qualified("module", "x");
1127        assert_eq!(format!("{name}"), "module.x");
1128        assert_eq!(name.member(), "x");
1129        assert_eq!(
1130            name.qualifier().iter().map(|s| &**s).collect::<Vec<_>>(),
1131            ["module"]
1132        );
1133    }
1134
1135    #[test]
1136    fn scoped_name_supports_nested_qualifier_path() {
1137        let name = ScopedName::qualified_path(["helpers", "math"], "G0");
1138        assert_eq!(format!("{name}"), "helpers.math.G0");
1139        assert_eq!(name.member(), "G0");
1140        assert_eq!(
1141            name.qualifier().iter().map(|s| &**s).collect::<Vec<_>>(),
1142            ["helpers", "math"]
1143        );
1144    }
1145}