use alloc::{string::ToString, sync::Arc};
use core::{
fmt,
hash::{Hash, Hasher},
str::FromStr,
};
use miden_core::utils::{
ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
};
use miden_debug_types::{SourceSpan, Span, Spanned};
#[derive(Debug, thiserror::Error)]
pub enum IdentError {
#[error("invalid identifier: cannot be empty")]
Empty,
#[error(
"invalid identifier '{ident}': must contain only unicode alphanumeric or ascii graphic characters"
)]
InvalidChars { ident: Arc<str> },
#[error("invalid identifier: length exceeds the maximum of {max} bytes")]
InvalidLength { max: usize },
#[error("invalid identifier: {0}")]
Casing(CaseKindError),
}
#[derive(Debug, thiserror::Error)]
pub enum CaseKindError {
#[error(
"only uppercase characters or underscores are allowed, and must start with an alphabetic character"
)]
Screaming,
#[error(
"only lowercase characters or underscores are allowed, and must start with an alphabetic character"
)]
Snake,
#[error(
"only alphanumeric characters are allowed, and must start with a lowercase alphabetic character"
)]
Camel,
}
#[derive(Clone)]
#[cfg_attr(
all(feature = "arbitrary", test),
miden_test_serde_macros::serde_test(winter_serde(true))
)]
pub struct Ident {
span: SourceSpan,
name: Arc<str>,
}
impl Ident {
pub fn new(source: impl AsRef<str>) -> Result<Self, IdentError> {
source.as_ref().parse()
}
pub fn new_with_span(span: SourceSpan, source: impl AsRef<str>) -> Result<Self, IdentError> {
source.as_ref().parse::<Self>().map(|id| id.with_span(span))
}
pub fn with_span(mut self, span: SourceSpan) -> Self {
self.span = span;
self
}
pub fn from_raw_parts(name: Span<Arc<str>>) -> Self {
let (span, name) = name.into_parts();
Self { span, name }
}
pub fn into_inner(self) -> Arc<str> {
self.name
}
pub fn as_str(&self) -> &str {
self.name.as_ref()
}
pub fn validate(source: impl AsRef<str>) -> Result<(), IdentError> {
let source = source.as_ref();
if source.is_empty() {
return Err(IdentError::Empty);
}
if !source.chars().all(|c| c.is_ascii_graphic() || c.is_alphanumeric()) {
return Err(IdentError::InvalidChars { ident: source.into() });
}
Ok(())
}
}
impl fmt::Debug for Ident {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("Ident").field(&self.name).finish()
}
}
impl Eq for Ident {}
impl PartialEq for Ident {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Ord for Ident {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.name.cmp(&other.name)
}
}
impl PartialOrd for Ident {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Hash for Ident {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl Spanned for Ident {
fn span(&self) -> SourceSpan {
self.span
}
}
impl core::ops::Deref for Ident {
type Target = str;
fn deref(&self) -> &Self::Target {
self.name.as_ref()
}
}
impl AsRef<str> for Ident {
#[inline]
fn as_ref(&self) -> &str {
&self.name
}
}
impl fmt::Display for Ident {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.name, f)
}
}
impl FromStr for Ident {
type Err = IdentError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::validate(s)?;
let name = Arc::from(s.to_string().into_boxed_str());
Ok(Self { span: SourceSpan::default(), name })
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Ident {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Ident {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let name = <&'de str as serde::Deserialize>::deserialize(deserializer)?;
Self::new(name).map_err(serde::de::Error::custom)
}
}
impl Serializable for Ident {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write_usize(self.len());
target.write_bytes(self.as_bytes());
}
}
impl Deserializable for Ident {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
use alloc::string::ToString;
let len = source.read_usize()?;
let bytes = source.read_slice(len)?;
let id = core::str::from_utf8(bytes)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
Self::new(id).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
#[cfg(feature = "arbitrary")]
pub(crate) mod testing {
use alloc::string::String;
use proptest::{char::CharStrategy, collection::vec, prelude::*};
use super::*;
impl Arbitrary for Ident {
type Parameters = ();
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
ident_any_random_length().boxed()
}
type Strategy = BoxedStrategy<Self>;
}
const SPECIAL: [char; 32] = const {
let mut buf = ['a'; 32];
let mut idx = 0;
let mut range_idx = 0;
while range_idx < SPECIAL_RANGES.len() {
let range = &SPECIAL_RANGES[range_idx];
range_idx += 1;
let mut j = *range.start() as u32;
let end = *range.end() as u32;
while j <= end {
unsafe {
buf[idx] = char::from_u32_unchecked(j);
}
idx += 1;
j += 1;
}
}
buf
};
const SPECIAL_RANGES: &[core::ops::RangeInclusive<char>] =
&['!'..='/', ':'..='@', '['..='`', '{'..='~'];
const PREFERRED_RANGES: &[core::ops::RangeInclusive<char>] = &['a'..='z', 'A'..='Z'];
const EXTRA_RANGES: &[core::ops::RangeInclusive<char>] = &['0'..='9', 'à'..='ö', 'ø'..='ÿ'];
prop_compose! {
fn ident_chars()
(c in CharStrategy::new_borrowed(
&SPECIAL,
PREFERRED_RANGES,
EXTRA_RANGES
)) -> char {
c
}
}
prop_compose! {
fn ident_raw_any(length: u32)
(chars in vec(ident_chars(), 1..=(length as usize))) -> String {
String::from_iter(chars)
}
}
prop_compose! {
pub fn ident_any(length: u32)
(raw in ident_raw_any(length)
.prop_filter(
"identifiers must be valid",
|s| Ident::validate(s).is_ok()
)
) -> Ident {
Ident::from_raw_parts(Span::new(SourceSpan::UNKNOWN, raw.into_boxed_str().into()))
}
}
prop_compose! {
pub fn ident_any_random_length()
(length in 1..u8::MAX)
(id in ident_any(length as u32)) -> Ident {
id
}
}
}