From a1d75acef1321347b701e2a864f17b85db5ee6c4 Mon Sep 17 00:00:00 2001 From: andrepd Date: Mon, 17 Nov 2025 11:24:23 +0000 Subject: [PATCH 1/2] Impl From<[AsciiChar; N]> for AsciiStr, AsRef for [AsciiChar; N] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To make it more convenient to work with fixed-size ascii arrays (`[AsciiChar; N]`), add the following trait impls: - Just as there is an impl of `From` in the direction `[AsciiChar] → AsciiStr`, add also `[AsciiChar; N] → AsciiStr`. - Just as there is an impl of `AsRef` in the direction `[AsciiChar] → AsciiStr`, add also `[AsciiChar; N] → AsciiStr`. --- src/ascii_str.rs | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/ascii_str.rs b/src/ascii_str.rs index bfdf54e..7924e99 100644 --- a/src/ascii_str.rs +++ b/src/ascii_str.rs @@ -466,6 +466,22 @@ impl<'a> From<&'a mut [AsciiChar]> for &'a mut AsciiStr { unsafe { &mut *ptr } } } +impl<'a, const N: usize> From<&'a [AsciiChar; N]> for &'a AsciiStr { + #[inline] + fn from(array: &[AsciiChar; N]) -> &AsciiStr { + let slice = array.as_slice(); + let ptr = slice as *const [AsciiChar] as *const AsciiStr; + unsafe { &*ptr } + } +} +impl<'a, const N: usize> From<&'a mut [AsciiChar; N]> for &'a mut AsciiStr { + #[inline] + fn from(array: &mut [AsciiChar; N]) -> &mut AsciiStr { + let slice = array.as_mut_slice(); + let ptr = slice as *mut [AsciiChar] as *mut AsciiStr; + unsafe { &mut *ptr } + } +} #[cfg(feature = "alloc")] impl From> for Box { #[inline] @@ -499,6 +515,18 @@ impl AsMut for [AsciiChar] { self.into() } } +impl AsRef for [AsciiChar; N] { + #[inline] + fn as_ref(&self) -> &AsciiStr { + self.into() + } +} +impl AsMut for [AsciiChar; N] { + #[inline] + fn as_mut(&mut self) -> &mut AsciiStr { + self.into() + } +} impl<'a> From<&'a AsciiStr> for &'a [AsciiChar] { #[inline] @@ -1310,9 +1338,9 @@ mod tests { } let arr = [AsciiChar::A]; - let ascii_str = arr.as_ref().into(); + let ascii_str = arr.as_ref(); let mut mut_arr = arr; // Note: We need a second copy to prevent overlapping mutable borrows. - let mut_ascii_str = mut_arr.as_mut().into(); + let mut_ascii_str = mut_arr.as_mut(); let mut_arr_mut_ref: &mut [AsciiChar] = &mut [AsciiChar::A]; let mut string_bytes = [b'A']; let string_mut = unsafe { core::str::from_utf8_unchecked_mut(&mut string_bytes) }; // SAFETY: 'A' is a valid string. @@ -1345,7 +1373,7 @@ mod tests { c.as_ascii_str() } let arr = [AsciiChar::A]; - let ascii_str: &AsciiStr = arr.as_ref().into(); + let ascii_str: &AsciiStr = arr.as_ref(); let cstr = CString::new("A").unwrap(); assert_eq!(generic(&*cstr), Ok(ascii_str)); } @@ -1359,10 +1387,10 @@ mod tests { } let mut arr_mut = [AsciiChar::B]; - let mut ascii_str_mut: &mut AsciiStr = arr_mut.as_mut().into(); + let mut ascii_str_mut: &mut AsciiStr = arr_mut.as_mut(); // Need a second reference to prevent overlapping mutable borrows let mut arr_mut_2 = [AsciiChar::B]; - let ascii_str_mut_2: &mut AsciiStr = arr_mut_2.as_mut().into(); + let ascii_str_mut_2: &mut AsciiStr = arr_mut_2.as_mut(); assert_eq!(generic_mut(&mut ascii_str_mut), Ok(&mut *ascii_str_mut_2)); assert_eq!(generic_mut(ascii_str_mut), Ok(&mut *ascii_str_mut_2)); } From 4965482ae500248aa6809ea83d7a0ede711f9e5e Mon Sep 17 00:00:00 2001 From: andrepd Date: Mon, 17 Nov 2025 12:25:49 +0000 Subject: [PATCH 2/2] Add literal macro `literal!("An ascii string")` enables us to write a fixed-size, compile-time ascii string in the same manner as we would use `b"A byte string"` to write a fixed-size, compile-time byte string. This is the only way to (safely) have a const ascii string. --- Cargo.toml | 1 + proc_macros/Cargo.toml | 11 +++++++ proc_macros/src/lib.rs | 56 +++++++++++++++++++++++++++++++++ src/lib.rs | 71 +++++++++++++++++++++++++++++++++++++++++- tests.rs | 16 ++++++++++ 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 proc_macros/Cargo.toml create mode 100644 proc_macros/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 8ec25de..2112ed1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ version = "1.1.0" [dependencies] serde = { version = "1.0.25", optional = true } serde_test = { version = "1.0", optional = true } +ascii_macros = { version = "1.1.0", path = "proc_macros/" } [features] default = ["std"] diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml new file mode 100644 index 0000000..42a9f9b --- /dev/null +++ b/proc_macros/Cargo.toml @@ -0,0 +1,11 @@ +[lib] +proc-macro = true + +[package] +name = "ascii_macros" +version = "1.1.0" +edition = "2024" + +[dependencies] +quote = "1.0.*" +syn = "2.0.*" diff --git a/proc_macros/src/lib.rs b/proc_macros/src/lib.rs new file mode 100644 index 0000000..dd8867f --- /dev/null +++ b/proc_macros/src/lib.rs @@ -0,0 +1,56 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{LitStr, LitChar}; + +#[proc_macro] +pub fn literal(input: TokenStream) -> TokenStream { + if let Ok(str) = syn::parse(input.clone()) { + str_literal(str) + } else if let Ok(char) = syn::parse(input.clone()) { + char_literal(char) + } else { + quote!{ compile_error!{"Expected a string or char literal."} }.into() + } +} + +fn str_literal(expr: LitStr) -> TokenStream { + let s = expr.value(); + + if !s.is_ascii() { + return quote!{ compile_error!{"String is not valid ascii."} }.into() + } + + let chars = s.as_bytes().iter(); + let len = s.as_bytes().len(); + + // SAFETY: all elements of `chars` are valid ascii, therefore casting from `u8` to `AsciiChar` is + // safe. + quote!{ + { + const __LIT: &'static [::ascii::AsciiChar; #len] = &[ + #(unsafe { ::core::mem::transmute::(#chars) }),* + ]; + __LIT + } + }.into() +} + +fn char_literal(expr: LitChar) -> TokenStream { + let s = expr.value(); + + if !s.is_ascii() { + return quote!{ compile_error!{"Char is not valid ascii."} }.into() + } + + let char = s as u8; + + // SAFETY: `char` is an ascii byte, therefore casting from `u8` to `AsciiChar` is safe. + quote!{ + { + const __LIT: ::ascii::AsciiChar = { + unsafe { ::core::mem::transmute::(#char) } + }; + __LIT + } + }.into() +} diff --git a/src/lib.rs b/src/lib.rs index 2147d77..cb3a407 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ //! //! The minimum Rust version for 1.2.\* releases is 1.56.1. //! Later 1.y.0 releases might require newer Rust versions, but the three most -//! recent stable releases at the time of publishing will always be supported. +//! recent stable releases at the time of publishing will always be supported. //! For example this means that if the current stable Rust version is 1.70 when //! ascii 1.3.0 is released, then ascii 1.3.\* will not require a newer //! Rust version than 1.68. @@ -80,3 +80,72 @@ pub use ascii_str::{Chars, CharsMut, CharsRef}; #[cfg(feature = "alloc")] pub use ascii_string::{AsciiString, FromAsciiError, IntoAsciiString}; pub use free_functions::{caret_decode, caret_encode}; + +extern crate ascii_macros; + +/// Creates a `&'static [AsciiChar; N]` from a string literal, or an [`AsciiChar`] from a +/// char literal. +/// +/// This is identical to how a byte string literal `b"abc"` creates a `&'static [u8; N]`, and a +/// byte char literal `b"z"` creates a `u8`. +/// +/// This can be used in `const` contexts. If the string/char is not valid ASCII, produces +/// a **compile-time error**. +/// +/// # Examples +/// ``` +/// # use ascii::{AsciiChar, AsciiStr}; +/// const HELLO: &[AsciiChar; 15] = ascii::literal!("Hello in ASCII!"); +/// assert_eq!(AsciiStr::new(HELLO).as_str(), "Hello in ASCII!"); +/// ``` +/// +/// ``` +/// # use ascii::{AsciiChar, AsciiStr}; +/// // Can also coerce to an `&'static [AsciiChar]`, just like a byte string literal can. +/// const SLICE: &[AsciiChar] = ascii::literal!("Slice of ASCII\n"); +/// # assert_eq!(AsciiStr::new(SLICE).as_str(), "Slice of ASCII\n"); +/// ``` +/// +/// ``` +/// # use ascii::{AsciiChar, AsciiStr}; +/// // Or directly to a `&AsciiStr`. +/// const STR: &AsciiStr = AsciiStr::new(ascii::literal!("Str of ASCII")); +/// # assert_eq!(STR.as_str(), "Str of ASCII"); +/// ``` +/// +/// ``` +/// # use ascii::{AsciiChar, AsciiStr}; +/// // A char literal produces an `AsciiChar`. +/// const CHAR: AsciiChar = ascii::literal!('@'); +/// # assert_eq!(CHAR, AsciiChar::At); +/// ``` +/// +/// ```compile_fail +/// // This doesn't compile! +/// let oops = ascii_literal::literal!("💥"); +/// ``` +pub use ascii_macros::literal; + +#[allow(dead_code)] +/// ```compile_fail +/// let _ = ascii::literal!("kaboom 💥"); +/// ``` +fn test_literal_compile_fail_1() {} + +#[allow(dead_code)] +/// ```compile_fail +/// let _ = ascii::literal!("Torbjørn"); +/// ``` +fn test_literal_compile_fail_2() {} + +#[allow(dead_code)] +/// ```compile_fail +/// let _ = ascii::literal!('é'); +/// ``` +fn test_literal_compile_fail_3() {} + +#[allow(dead_code)] +/// ```compile_fail +/// let _ = ascii::literal!(invalid,); +/// ``` +fn test_literal_compile_fail_4() {} diff --git a/tests.rs b/tests.rs index b84e59c..d2014bf 100644 --- a/tests.rs +++ b/tests.rs @@ -148,3 +148,19 @@ fn extend_from_iterator() { s.extend(&[AsciiChar::LineFeed]); assert_eq!(s, "abcabconetwothreeASCIIASCIIASCII\n"); } + +#[test] +fn literal() { + use ascii::literal; + + const MESSAGE: &[AsciiChar; 15] = literal!("Hello in ASCII!"); + let ascii_str: &AsciiStr = MESSAGE.into(); + assert_eq!(ascii_str.as_str(), "Hello in ASCII!"); + + const EMPTY: &[AsciiChar; 0] = literal!(""); + let ascii_str: &AsciiStr = EMPTY.into(); + assert_eq!(ascii_str.as_str(), ""); + + const CHAR: AsciiChar = literal!('Z'); + assert_eq!(CHAR.as_byte(), b'Z'); +}