#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum NameAtomError {
#[error("name atom cannot be empty")]
Empty,
#[error("name atom cannot contain `.`")]
ContainsDot,
}
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct NameAtom(String);
impl NameAtom {
pub fn parse(s: impl Into<String>) -> Result<Self, NameAtomError> {
let s = s.into();
if s.is_empty() {
return Err(NameAtomError::Empty);
}
if s.contains('.') {
return Err(NameAtomError::ContainsDot);
}
Ok(Self(s))
}
#[must_use]
pub(crate) fn new_unchecked_for_parser(s: String) -> Self {
debug_assert!(Self::parse(s.as_str()).is_ok());
Self(s)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl std::fmt::Debug for NameAtom {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.0, f)
}
}
impl std::fmt::Display for NameAtom {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::ops::Deref for NameAtom {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl PartialEq<str> for NameAtom {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&str> for NameAtom {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<String> for NameAtom {
fn eq(&self, other: &String) -> bool {
self.as_str() == other
}
}
impl PartialEq<NameAtom> for str {
fn eq(&self, other: &NameAtom) -> bool {
self == other.as_str()
}
}
impl PartialEq<NameAtom> for &str {
fn eq(&self, other: &NameAtom) -> bool {
*self == other.as_str()
}
}
impl AsRef<str> for NameAtom {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::borrow::Borrow<str> for NameAtom {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl From<NameAtom> for String {
fn from(atom: NameAtom) -> Self {
atom.into_inner()
}
}
impl From<&NameAtom> for String {
fn from(atom: &NameAtom) -> Self {
atom.as_str().to_string()
}
}
impl From<NameAtom> for std::borrow::Cow<'_, str> {
fn from(atom: NameAtom) -> Self {
Self::Owned(atom.into_inner())
}
}
impl TryFrom<String> for NameAtom {
type Error = NameAtomError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl TryFrom<&str> for NameAtom {
type Error = NameAtomError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
use std::marker::PhantomData;
pub trait NameNamespace:
std::fmt::Debug + Clone + Copy + PartialEq + Eq + std::hash::Hash + PartialOrd + Ord + 'static
{
const DISPLAY_NAME: &'static str;
}
pub mod namespace {
use super::NameNamespace;
macro_rules! define_namespace {
($($(#[$meta:meta])* $Name:ident => $display:literal;)+) => {
$(
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum $Name {}
impl NameNamespace for $Name {
const DISPLAY_NAME: &'static str = $display;
}
)+
};
}
define_namespace! {
Decl => "DeclName";
Dim => "DimName";
Unit => "UnitName";
StructType => "StructTypeName";
Index => "IndexName";
Fn => "FnName";
Field => "FieldName";
IndexVariant => "IndexVariantName";
Constructor => "ConstructorName";
GenericParam => "GenericParamName";
DimVar => "DimVarName";
Local => "LocalName";
ModuleAlias => "ModuleAliasName";
PlotProperty => "PlotPropertyName";
}
}
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct NameDef<Ns: NameNamespace> {
atom: NameAtom,
_ns: PhantomData<Ns>,
}
impl<Ns: NameNamespace> std::fmt::Debug for NameDef<Ns> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.atom, f)
}
}
impl<Ns: NameNamespace> NameDef<Ns> {
#[must_use]
#[expect(
clippy::panic,
reason = "infallible constructor documents invalid input panic"
)]
pub fn new(s: impl Into<String>) -> Self {
Self::try_new(s).unwrap_or_else(|err| {
panic!("invalid {} leaf name: {err}", Ns::DISPLAY_NAME);
})
}
pub fn try_new(s: impl Into<String>) -> Result<Self, NameAtomError> {
NameAtom::parse(s).map(Self::from_atom)
}
#[must_use]
pub const fn from_atom(atom: NameAtom) -> Self {
Self {
atom,
_ns: PhantomData,
}
}
#[must_use]
pub const fn atom(&self) -> &NameAtom {
&self.atom
}
#[must_use]
pub fn as_str(&self) -> &str {
self.atom.as_str()
}
#[must_use]
pub fn into_atom(self) -> NameAtom {
self.atom
}
#[must_use]
pub fn into_inner(self) -> String {
self.atom.into_inner()
}
}
impl<Ns: NameNamespace> std::fmt::Display for NameDef<Ns> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl<Ns: NameNamespace> PartialEq<str> for NameDef<Ns> {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl<Ns: NameNamespace> PartialEq<&str> for NameDef<Ns> {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl<Ns: NameNamespace> AsRef<str> for NameDef<Ns> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<Ns: NameNamespace> std::borrow::Borrow<str> for NameDef<Ns> {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl<Ns: NameNamespace> From<NameAtom> for NameDef<Ns> {
fn from(atom: NameAtom) -> Self {
Self::from_atom(atom)
}
}
impl<Ns: NameNamespace> From<String> for NameDef<Ns> {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl<Ns: NameNamespace> From<&str> for NameDef<Ns> {
fn from(s: &str) -> Self {
Self::new(s)
}
}
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ResolvedName<Ns: NameNamespace> {
owner: crate::dag_id::DagId,
name: NameAtom,
_ns: PhantomData<Ns>,
}
impl<Ns: NameNamespace> std::fmt::Debug for ResolvedName<Ns> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResolvedName")
.field("namespace", &Ns::DISPLAY_NAME)
.field("owner", &self.owner)
.field("name", &self.name)
.finish()
}
}
impl<Ns: NameNamespace> ResolvedName<Ns> {
#[must_use]
pub const fn new(owner: crate::dag_id::DagId, name: NameAtom) -> Self {
Self {
owner,
name,
_ns: PhantomData,
}
}
#[must_use]
pub fn from_def(owner: crate::dag_id::DagId, name: NameDef<Ns>) -> Self {
Self::new(owner, name.into_atom())
}
#[must_use]
pub const fn owner(&self) -> &crate::dag_id::DagId {
&self.owner
}
#[must_use]
pub const fn atom(&self) -> &NameAtom {
&self.name
}
#[must_use]
pub fn as_str(&self) -> &str {
self.name.as_str()
}
#[must_use]
pub fn to_unowned_def_name(&self) -> NameDef<Ns> {
NameDef::from_atom(self.name.clone())
}
#[must_use]
pub fn into_parts(self) -> (crate::dag_id::DagId, NameAtom) {
(self.owner, self.name)
}
}
impl<Ns: NameNamespace> std::fmt::Display for ResolvedName<Ns> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.owner, self.name)
}
}
pub type DeclName = NameDef<namespace::Decl>;
pub type DimName = NameDef<namespace::Dim>;
pub type UnitName = NameDef<namespace::Unit>;
pub type StructTypeName = NameDef<namespace::StructType>;
pub type IndexName = NameDef<namespace::Index>;
pub type FnName = NameDef<namespace::Fn>;
pub type FieldName = NameDef<namespace::Field>;
pub type IndexVariantName = NameDef<namespace::IndexVariant>;
pub type ConstructorName = NameDef<namespace::Constructor>;
pub type GenericParamName = NameDef<namespace::GenericParam>;
pub type DimVarName = NameDef<namespace::DimVar>;
pub type LocalName = NameDef<namespace::Local>;
pub type ModuleAliasName = NameDef<namespace::ModuleAlias>;
pub type PlotPropertyName = NameDef<namespace::PlotProperty>;
impl IndexVariantName {
#[must_use]
pub fn range_step(n: impl std::fmt::Display) -> Self {
Self::new(format!("#{n}"))
}
#[must_use]
pub fn qualified_by(&self, index: &IndexName) -> QualifiedIndexVariantName {
QualifiedIndexVariantName::new(index.clone(), self.clone())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct QualifiedIndexVariantName {
index: IndexName,
variant: IndexVariantName,
}
impl QualifiedIndexVariantName {
#[must_use]
pub const fn new(index: IndexName, variant: IndexVariantName) -> Self {
Self { index, variant }
}
#[must_use]
pub const fn index(&self) -> &IndexName {
&self.index
}
#[must_use]
pub const fn variant(&self) -> &IndexVariantName {
&self.variant
}
}
impl std::fmt::Display for QualifiedIndexVariantName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.index, self.variant)
}
}
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ResolvedIndexVariant {
index: ResolvedName<namespace::Index>,
variant: IndexVariantName,
}
impl ResolvedIndexVariant {
#[must_use]
pub const fn new(index: ResolvedName<namespace::Index>, variant: IndexVariantName) -> Self {
Self { index, variant }
}
#[must_use]
pub const fn index(&self) -> &ResolvedName<namespace::Index> {
&self.index
}
#[must_use]
pub const fn variant(&self) -> &IndexVariantName {
&self.variant
}
#[must_use]
pub fn into_parts(self) -> (ResolvedName<namespace::Index>, IndexVariantName) {
(self.index, self.variant)
}
}
impl std::fmt::Debug for ResolvedIndexVariant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResolvedIndexVariant")
.field("index", &self.index)
.field("variant", &self.variant)
.finish()
}
}
impl std::fmt::Display for ResolvedIndexVariant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.index, self.variant)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TimeScaleName(crate::registry::time_scale::TimeScale);
impl TimeScaleName {
#[must_use]
pub const fn new(scale: crate::registry::time_scale::TimeScale) -> Self {
Self(scale)
}
#[must_use]
pub const fn scale(self) -> crate::registry::time_scale::TimeScale {
self.0
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.0.name()
}
}
impl std::fmt::Display for TimeScaleName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl AsRef<str> for TimeScaleName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ScopedName {
qualifier: Arc<[Arc<str>]>,
member: Arc<str>,
}
impl ScopedName {
#[must_use]
pub fn local(member: impl Into<Arc<str>>) -> Self {
Self {
qualifier: Arc::from([] as [Arc<str>; 0]),
member: member.into(),
}
}
#[must_use]
pub fn qualified(module: impl Into<Arc<str>>, member: impl Into<Arc<str>>) -> Self {
Self::qualified_path([module], member)
}
#[must_use]
pub fn qualified_path(
qualifier: impl IntoIterator<Item = impl Into<Arc<str>>>,
member: impl Into<Arc<str>>,
) -> Self {
Self {
qualifier: qualifier.into_iter().map(Into::into).collect(),
member: member.into(),
}
}
#[must_use]
pub fn member(&self) -> &str {
&self.member
}
#[must_use]
pub fn qualifier(&self) -> &[Arc<str>] {
&self.qualifier
}
#[must_use]
pub fn is_qualified(&self) -> bool {
!self.qualifier.is_empty()
}
#[must_use]
pub fn with_prefix(&self, prefix: &str) -> Self {
Self::qualified(prefix, Arc::clone(&self.member))
}
}
impl std::fmt::Display for ScopedName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for segment in self.qualifier.iter() {
f.write_str(segment)?;
f.write_str(".")?;
}
f.write_str(&self.member)
}
}
impl From<NameAtom> for ScopedName {
fn from(atom: NameAtom) -> Self {
Self::local(atom.into_inner())
}
}
impl From<String> for ScopedName {
fn from(s: String) -> Self {
Self::local(s)
}
}
impl From<DeclName> for ScopedName {
fn from(name: DeclName) -> Self {
Self::local(name.into_inner())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct UnitRef {
qualifier: Option<ModuleAliasName>,
name: UnitName,
}
impl UnitRef {
#[must_use]
pub fn local(name: impl Into<UnitName>) -> Self {
Self {
qualifier: None,
name: name.into(),
}
}
#[must_use]
pub const fn qualified(qualifier: ModuleAliasName, name: UnitName) -> Self {
Self {
qualifier: Some(qualifier),
name,
}
}
#[must_use]
pub const fn qualifier(&self) -> Option<&ModuleAliasName> {
self.qualifier.as_ref()
}
#[must_use]
pub const fn name(&self) -> &UnitName {
&self.name
}
#[must_use]
pub const fn is_qualified(&self) -> bool {
self.qualifier.is_some()
}
}
impl From<UnitName> for UnitRef {
fn from(name: UnitName) -> Self {
Self::local(name)
}
}
impl From<NameAtom> for UnitRef {
fn from(atom: NameAtom) -> Self {
Self::local(UnitName::from_atom(atom))
}
}
impl std::fmt::Display for UnitRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(qualifier) = &self.qualifier {
write!(f, "{qualifier}.")?;
}
write!(f, "{}", self.name)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct NamePath {
segments: crate::syntax::non_empty::NonEmpty<NameAtom>,
}
impl NamePath {
#[must_use]
pub const fn new(segments: crate::syntax::non_empty::NonEmpty<NameAtom>) -> Self {
Self { segments }
}
#[must_use]
pub fn local(atom: NameAtom) -> Self {
Self::new(crate::syntax::non_empty::NonEmpty::singleton(atom))
}
#[must_use]
pub fn qualified_path(qualifier: impl IntoIterator<Item = NameAtom>, leaf: NameAtom) -> Self {
let mut segments: Vec<NameAtom> = qualifier.into_iter().collect();
segments.push(leaf);
let first = segments.remove(0);
Self::new(crate::syntax::non_empty::NonEmpty::new(first, segments))
}
#[must_use]
pub fn segments(&self) -> &[NameAtom] {
self.segments.as_slice()
}
#[must_use]
pub fn into_segments(self) -> crate::syntax::non_empty::NonEmpty<NameAtom> {
self.segments
}
#[must_use]
pub const fn len(&self) -> usize {
self.segments.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
false
}
#[must_use]
pub const fn is_bare(&self) -> bool {
self.segments.len() == 1
}
#[must_use]
pub fn leaf(&self) -> &NameAtom {
self.segments.last()
}
#[must_use]
pub fn as_bare(&self) -> Option<&NameAtom> {
match self.segments.as_slice() {
[atom] => Some(atom),
_ => None,
}
}
#[must_use]
pub fn split_last(&self) -> (&[NameAtom], &NameAtom) {
let (leaf, qualifier) = self.segments.split_last();
(qualifier, leaf)
}
#[must_use]
pub fn qualifier_segments(&self) -> &[NameAtom] {
self.split_last().0
}
#[must_use]
pub fn qualifier_and_leaf(&self) -> Option<(&[NameAtom], &NameAtom)> {
let (qualifier, leaf) = self.split_last();
(!qualifier.is_empty()).then_some((qualifier, leaf))
}
#[must_use]
pub fn display_path(&self) -> String {
self.segments
.iter()
.map(NameAtom::as_str)
.collect::<Vec<_>>()
.join(".")
}
}
impl From<NameAtom> for NamePath {
fn from(atom: NameAtom) -> Self {
Self::local(atom)
}
}
impl From<IndexName> for NamePath {
fn from(name: IndexName) -> Self {
Self::local(name.into_atom())
}
}
impl From<String> for NamePath {
#[expect(
clippy::panic,
reason = "From<String> is a convenience for trusted leaf names"
)]
fn from(s: String) -> Self {
Self::local(NameAtom::parse(s).unwrap_or_else(|err| {
panic!("invalid NamePath leaf name: {err}");
}))
}
}
impl From<&str> for NamePath {
fn from(s: &str) -> Self {
Self::from(s.to_string())
}
}
impl std::fmt::Display for NamePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (idx, segment) in self.segments.iter().enumerate() {
if idx > 0 {
f.write_str(".")?;
}
f.write_str(segment.as_str())?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn name_atom_rejects_dotted_paths() {
assert_eq!(
NameAtom::parse("module.Value"),
Err(NameAtomError::ContainsDot)
);
assert_eq!(
DeclName::try_new("module.Value"),
Err(NameAtomError::ContainsDot)
);
}
#[test]
fn name_atom_accepts_internal_leaf_names() {
let atom = NameAtom::parse("#0").unwrap();
assert_eq!(atom.as_str(), "#0");
}
#[test]
fn newtype_display() {
let name = DeclName::new("dry_mass");
assert_eq!(format!("{name}"), "dry_mass");
}
#[test]
fn newtype_as_str() {
let name = DimName::new("Length");
assert_eq!(name.as_str(), "Length");
}
#[test]
fn newtype_into_inner() {
let name = UnitName::new("km");
assert_eq!(name.into_inner(), "km");
}
#[test]
fn newtype_hash_map_borrow_lookup() {
let mut map = HashMap::new();
map.insert(DeclName::new("x"), 42);
assert_eq!(map.get("x"), Some(&42));
}
#[test]
fn newtype_from_string() {
let name: FieldName = "dv1".to_string().into();
assert_eq!(name.as_str(), "dv1");
}
#[test]
fn newtype_from_str() {
let name: IndexVariantName = "Departure".into();
assert_eq!(name.as_str(), "Departure");
}
#[test]
fn newtype_equality() {
assert_eq!(IndexName::new("Maneuver"), IndexName::new("Maneuver"));
assert_ne!(IndexName::new("Maneuver"), IndexName::new("Phase"));
}
#[test]
fn newtype_ord() {
let a = FnName::new("alpha");
let b = FnName::new("beta");
assert!(a < b);
}
#[test]
fn name_path_preserves_qualifier_and_leaf() {
let path = NamePath::qualified_path(
[NameAtom::parse("module").unwrap()],
NameAtom::parse("Index").unwrap(),
);
assert_eq!(path.display_path(), "module.Index");
assert_eq!(path.leaf().as_str(), "Index");
assert_eq!(
path.qualifier_segments()
.iter()
.map(NameAtom::as_str)
.collect::<Vec<_>>(),
["module"]
);
}
#[test]
fn name_def_aliases_keep_namespace_and_leaf_invariant() {
let decl = DeclName::new("x");
let index = IndexName::new("x");
assert_eq!(decl.as_str(), index.as_str());
assert_eq!(
DeclName::try_new("module.x"),
Err(NameAtomError::ContainsDot)
);
assert_eq!(
IndexName::try_new("module.x"),
Err(NameAtomError::ContainsDot)
);
}
#[test]
fn resolved_name_carries_canonical_owner_and_leaf() {
let name = DeclName::new("dry_mass");
let resolved = ResolvedName::<namespace::Decl>::from_def(
crate::dag_id::DagId::new("helpers", ["mass"]),
name,
);
assert_eq!(resolved.owner().to_string(), "helpers.mass");
assert_eq!(resolved.as_str(), "dry_mass");
assert_eq!(resolved.to_string(), "helpers.mass.dry_mass");
assert_eq!(resolved.to_unowned_def_name(), DeclName::new("dry_mass"));
}
#[test]
fn resolved_index_variant_carries_resolved_index_owner() {
let index = ResolvedName::<namespace::Index>::from_def(
crate::dag_id::DagId::root("mission"),
IndexName::new("Phase"),
);
let variant = ResolvedIndexVariant::new(index, IndexVariantName::new("Burn"));
assert_eq!(variant.index().owner().to_string(), "mission");
assert_eq!(variant.index().as_str(), "Phase");
assert_eq!(variant.variant().as_str(), "Burn");
assert_eq!(variant.to_string(), "mission.Phase.Burn");
}
#[test]
fn scoped_name_qualified_display_uses_dot() {
let name = ScopedName::qualified("module", "x");
assert_eq!(format!("{name}"), "module.x");
assert_eq!(name.member(), "x");
assert_eq!(
name.qualifier().iter().map(|s| &**s).collect::<Vec<_>>(),
["module"]
);
}
#[test]
fn scoped_name_supports_nested_qualifier_path() {
let name = ScopedName::qualified_path(["helpers", "math"], "G0");
assert_eq!(format!("{name}"), "helpers.math.G0");
assert_eq!(name.member(), "G0");
assert_eq!(
name.qualifier().iter().map(|s| &**s).collect::<Vec<_>>(),
["helpers", "math"]
);
}
}