Skip to content

Commit 4965482

Browse files
committed
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.
1 parent a1d75ac commit 4965482

File tree

5 files changed

+154
-1
lines changed

5 files changed

+154
-1
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ version = "1.1.0"
1111
[dependencies]
1212
serde = { version = "1.0.25", optional = true }
1313
serde_test = { version = "1.0", optional = true }
14+
ascii_macros = { version = "1.1.0", path = "proc_macros/" }
1415

1516
[features]
1617
default = ["std"]

proc_macros/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[lib]
2+
proc-macro = true
3+
4+
[package]
5+
name = "ascii_macros"
6+
version = "1.1.0"
7+
edition = "2024"
8+
9+
[dependencies]
10+
quote = "1.0.*"
11+
syn = "2.0.*"

proc_macros/src/lib.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use proc_macro::TokenStream;
2+
use quote::quote;
3+
use syn::{LitStr, LitChar};
4+
5+
#[proc_macro]
6+
pub fn literal(input: TokenStream) -> TokenStream {
7+
if let Ok(str) = syn::parse(input.clone()) {
8+
str_literal(str)
9+
} else if let Ok(char) = syn::parse(input.clone()) {
10+
char_literal(char)
11+
} else {
12+
quote!{ compile_error!{"Expected a string or char literal."} }.into()
13+
}
14+
}
15+
16+
fn str_literal(expr: LitStr) -> TokenStream {
17+
let s = expr.value();
18+
19+
if !s.is_ascii() {
20+
return quote!{ compile_error!{"String is not valid ascii."} }.into()
21+
}
22+
23+
let chars = s.as_bytes().iter();
24+
let len = s.as_bytes().len();
25+
26+
// SAFETY: all elements of `chars` are valid ascii, therefore casting from `u8` to `AsciiChar` is
27+
// safe.
28+
quote!{
29+
{
30+
const __LIT: &'static [::ascii::AsciiChar; #len] = &[
31+
#(unsafe { ::core::mem::transmute::<u8, ::ascii::AsciiChar>(#chars) }),*
32+
];
33+
__LIT
34+
}
35+
}.into()
36+
}
37+
38+
fn char_literal(expr: LitChar) -> TokenStream {
39+
let s = expr.value();
40+
41+
if !s.is_ascii() {
42+
return quote!{ compile_error!{"Char is not valid ascii."} }.into()
43+
}
44+
45+
let char = s as u8;
46+
47+
// SAFETY: `char` is an ascii byte, therefore casting from `u8` to `AsciiChar` is safe.
48+
quote!{
49+
{
50+
const __LIT: ::ascii::AsciiChar = {
51+
unsafe { ::core::mem::transmute::<u8, ::ascii::AsciiChar>(#char) }
52+
};
53+
__LIT
54+
}
55+
}.into()
56+
}

src/lib.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
//!
1818
//! The minimum Rust version for 1.2.\* releases is 1.56.1.
1919
//! Later 1.y.0 releases might require newer Rust versions, but the three most
20-
//! recent stable releases at the time of publishing will always be supported.
20+
//! recent stable releases at the time of publishing will always be supported.
2121
//! For example this means that if the current stable Rust version is 1.70 when
2222
//! ascii 1.3.0 is released, then ascii 1.3.\* will not require a newer
2323
//! Rust version than 1.68.
@@ -80,3 +80,72 @@ pub use ascii_str::{Chars, CharsMut, CharsRef};
8080
#[cfg(feature = "alloc")]
8181
pub use ascii_string::{AsciiString, FromAsciiError, IntoAsciiString};
8282
pub use free_functions::{caret_decode, caret_encode};
83+
84+
extern crate ascii_macros;
85+
86+
/// Creates a `&'static [AsciiChar; N]` from a string literal, or an [`AsciiChar`] from a
87+
/// char literal.
88+
///
89+
/// This is identical to how a byte string literal `b"abc"` creates a `&'static [u8; N]`, and a
90+
/// byte char literal `b"z"` creates a `u8`.
91+
///
92+
/// This can be used in `const` contexts. If the string/char is not valid ASCII, produces
93+
/// a **compile-time error**.
94+
///
95+
/// # Examples
96+
/// ```
97+
/// # use ascii::{AsciiChar, AsciiStr};
98+
/// const HELLO: &[AsciiChar; 15] = ascii::literal!("Hello in ASCII!");
99+
/// assert_eq!(AsciiStr::new(HELLO).as_str(), "Hello in ASCII!");
100+
/// ```
101+
///
102+
/// ```
103+
/// # use ascii::{AsciiChar, AsciiStr};
104+
/// // Can also coerce to an `&'static [AsciiChar]`, just like a byte string literal can.
105+
/// const SLICE: &[AsciiChar] = ascii::literal!("Slice of ASCII\n");
106+
/// # assert_eq!(AsciiStr::new(SLICE).as_str(), "Slice of ASCII\n");
107+
/// ```
108+
///
109+
/// ```
110+
/// # use ascii::{AsciiChar, AsciiStr};
111+
/// // Or directly to a `&AsciiStr`.
112+
/// const STR: &AsciiStr = AsciiStr::new(ascii::literal!("Str of ASCII"));
113+
/// # assert_eq!(STR.as_str(), "Str of ASCII");
114+
/// ```
115+
///
116+
/// ```
117+
/// # use ascii::{AsciiChar, AsciiStr};
118+
/// // A char literal produces an `AsciiChar`.
119+
/// const CHAR: AsciiChar = ascii::literal!('@');
120+
/// # assert_eq!(CHAR, AsciiChar::At);
121+
/// ```
122+
///
123+
/// ```compile_fail
124+
/// // This doesn't compile!
125+
/// let oops = ascii_literal::literal!("💥");
126+
/// ```
127+
pub use ascii_macros::literal;
128+
129+
#[allow(dead_code)]
130+
/// ```compile_fail
131+
/// let _ = ascii::literal!("kaboom 💥");
132+
/// ```
133+
fn test_literal_compile_fail_1() {}
134+
135+
#[allow(dead_code)]
136+
/// ```compile_fail
137+
/// let _ = ascii::literal!("Torbjørn");
138+
/// ```
139+
fn test_literal_compile_fail_2() {}
140+
141+
#[allow(dead_code)]
142+
/// ```compile_fail
143+
/// let _ = ascii::literal!('é');
144+
/// ```
145+
fn test_literal_compile_fail_3() {}
146+
147+
#[allow(dead_code)]
148+
/// ```compile_fail
149+
/// let _ = ascii::literal!(invalid,);
150+
/// ```
151+
fn test_literal_compile_fail_4() {}

tests.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,19 @@ fn extend_from_iterator() {
148148
s.extend(&[AsciiChar::LineFeed]);
149149
assert_eq!(s, "abcabconetwothreeASCIIASCIIASCII\n");
150150
}
151+
152+
#[test]
153+
fn literal() {
154+
use ascii::literal;
155+
156+
const MESSAGE: &[AsciiChar; 15] = literal!("Hello in ASCII!");
157+
let ascii_str: &AsciiStr = MESSAGE.into();
158+
assert_eq!(ascii_str.as_str(), "Hello in ASCII!");
159+
160+
const EMPTY: &[AsciiChar; 0] = literal!("");
161+
let ascii_str: &AsciiStr = EMPTY.into();
162+
assert_eq!(ascii_str.as_str(), "");
163+
164+
const CHAR: AsciiChar = literal!('Z');
165+
assert_eq!(CHAR.as_byte(), b'Z');
166+
}

0 commit comments

Comments
 (0)