Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
11 changes: 11 additions & 0 deletions proc_macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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.*"
56 changes: 56 additions & 0 deletions proc_macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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::<u8, ::ascii::AsciiChar>(#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::<u8, ::ascii::AsciiChar>(#char) }
};
__LIT
}
}.into()
}
38 changes: 33 additions & 5 deletions src/ascii_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Box<[AsciiChar]>> for Box<AsciiStr> {
#[inline]
Expand Down Expand Up @@ -499,6 +515,18 @@ impl AsMut<AsciiStr> for [AsciiChar] {
self.into()
}
}
impl<const N: usize> AsRef<AsciiStr> for [AsciiChar; N] {
#[inline]
fn as_ref(&self) -> &AsciiStr {
self.into()
}
}
impl<const N: usize> AsMut<AsciiStr> for [AsciiChar; N] {
#[inline]
fn as_mut(&mut self) -> &mut AsciiStr {
self.into()
}
}

impl<'a> From<&'a AsciiStr> for &'a [AsciiChar] {
#[inline]
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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));
}
Expand All @@ -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));
}
Expand Down
71 changes: 70 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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() {}
16 changes: 16 additions & 0 deletions tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}