inline_json5_macro/
lib.rs

1use loess::{
2	Errors, Input, IntoTokens, PeekFrom, SimpleSpanned, grammar, parse_all, parse_once,
3	quote_into_mixed_site,
4};
5use proc_macro2::{Delimiter, Group, Literal, Span, TokenStream, TokenTree};
6
7mod tokens;
8use tokens::{
9	Braces, Colon, Comma, Delimited, False, Identifier, Infinity, Minus, NaN, Null, NumberLiteral,
10	Plus, SquareBrackets, String, True,
11};
12
13#[proc_macro]
14pub fn json5(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
15	hook_panics();
16	json5_(input.into()).into()
17}
18
19// Having this as separate function makes type inference a bit easier.
20fn json5_(input: TokenStream) -> TokenStream {
21	let mut input = Input {
22		tokens: input.into_iter().collect(),
23		end: Span::call_site(), // Use `Span::end` of the last input token instead once that's stable!
24	};
25	let mut errors = Errors::new();
26
27	let Ok(SquareBrackets { contents: root, .. }) = parse_once(&mut input, &mut errors) else {
28		return errors.collect_tokens(&TokenStream::new());
29	};
30
31	// `parse_all` checks that the input was fully consumed when the iterator is dropped.
32	let value = parse_all::<Value>(&mut input, &mut errors).next();
33
34	// Since we have a (non-empty) `root`, this is *fully* hygienic.
35	// Otherwise, it would expect `::core` to be the standard library `core` crate.
36	let mut output = errors.collect_tokens(&root);
37
38	if let Some(value) = value {
39		value.into_tokens(&root, &mut output)
40	} else {
41		quote_into_mixed_site!(Span::mixed_site(), &root, &mut output, [
42			{#error "Expected JSON5 value."}
43			{#root}::json::JsonValue::Null
44		])
45	}
46
47	// This is a value macro but those `compile_error!`s are statements,
48	// so let's wrap the output in a block!
49	TokenTree::Group(Group::new(Delimiter::Brace, output)).into()
50}
51
52// Note that `grammar!`'s `PeekFrom` implementations only check for the first field.
53grammar! {
54	enum Value: PopFrom, IntoTokens {
55		String(String),
56		Number(Number),
57		Object(Object),
58		Array(Array),
59		True(True),
60		False(False),
61		Null(Null),
62	} else "Expected JSON5 value."; // The error message that's used when no variant is peeked successfully.
63
64	struct Array: PeekFrom, PopFrom (SquareBrackets<Delimited<Value, Comma>>);
65
66	struct Object: PeekFrom, PopFrom (Braces<Delimited<Property, Comma>>);
67
68	struct Property: PopFrom {
69		key: Key,
70		colon: Colon,
71		value: Value,
72	}
73
74	enum Key: PopFrom {
75		Identifier(Identifier),
76		String(String),
77	} else "Expected key (plain identifier or string literal).";
78
79	enum Number: PeekFrom, PopFrom {
80		NaN(NaN),
81		NotNaN(NotNaN),
82	} else "Expected Number."; // Should be unreachable.
83
84	struct NotNaN: PopFrom {
85		sign: Option<Sign>,
86		amount: Amount,
87	}
88
89	enum Sign: PeekFrom, PopFrom {
90		Plus(Plus),
91		Minus(Minus),
92	} else "Expected Sign."; // Should be unreachable.
93
94	enum Amount: PeekFrom, PopFrom {
95		Finite(NumberLiteral), // This could be a structured variant too, but `grammar!` doesn't support that yet.
96		Infinity(Infinity),
97	} else "Expected number literal or `infinity`.";
98}
99
100/// [`NotNaN`] starts with an [`Option`], which isn't peekable since that's error-prone in [`grammar!`].
101///
102/// As such, this is implemented manually. (Maybe I'll add a way to have this automatically,
103/// but it's unlikely since that might have an impact on compile time that's disproportionate to the work done.)
104impl PeekFrom for NotNaN {
105	fn peek_from(input: &Input) -> bool {
106		Sign::peek_from(input) || Amount::peek_from(input)
107	}
108}
109
110// Alternatively, you could `{#match …, … }` in a template for `Value`, but here it's convenient to split the branches.
111
112impl IntoTokens for Object {
113	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
114		quote_into_mixed_site!(self.0.span.join(), root, tokens, [
115			{#root}::json::JsonValue::Object({
116				let mut object = {#root}::json::object::Object::with_capacity(
117					// Whoops, I forgot to implement `IntoTokens` for `Literal`.
118					{#paste Literal::usize_unsuffixed(self.0.contents.0.len()) }
119				);
120				{#for (property, comma) in self.0.contents.0,
121					{#let Property { key, colon, value } = property;}
122					object.insert(
123						{#paste key }
124						{#located_at colon.0.span(), , }
125						{#paste value }
126					)
127					{#located_at comma.map(|comma| comma.0.span()).unwrap_or(self.0.span.close()), ; }
128				}
129				object
130			})
131		])
132	}
133}
134
135impl IntoTokens for Key {
136	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
137		match self {
138			Key::Identifier(identifier) => {
139				let mut s = Literal::string(&identifier.0.to_string());
140				s.set_span(identifier.0.span());
141				s.into_tokens(root, tokens)
142			}
143			Key::String(s) => s.0.into_tokens(root, tokens),
144		}
145	}
146}
147
148impl IntoTokens for Array {
149	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
150		quote_into_mixed_site!(self.0.span.join(), root, tokens, [
151			{#root}::json::JsonValue::Array({
152				let mut vec = {#root}::std::vec::Vec::with_capacity(
153					// Whoops, I forgot to implement `IntoTokens` for `Literal`.
154					{#paste Literal::usize_unsuffixed(self.0.contents.0.len()) }
155				);
156				{#for (item, comma) in self.0.contents.0,
157					vec.push({#paste item })
158					{#located_at comma.map(|comma| comma.0.span()).unwrap_or(self.0.span.close()), ; }
159				}
160				vec
161			})
162		])
163	}
164}
165
166impl IntoTokens for Number {
167	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
168		let span = match &self {
169			Number::NaN(nan) => nan.0.span(),
170			Number::NotNaN(NotNaN {
171				sign: Some(sign),
172				amount,
173			}) => sign.span().join(amount.span()).unwrap_or(amount.span()),
174			Number::NotNaN(NotNaN { sign: None, amount }) => amount.span(),
175		};
176		quote_into_mixed_site!(span, root, tokens, [
177			{#root}::json::JsonValue::Number(
178				{#match self,
179					Self::NaN(_) => { {#root}::json::number::NAN }
180					Self::NotNaN(NotNaN { sign, amount }) => {
181						{#root}::core::convert::From::from(
182							{#match (sign, amount),
183								(Some(Sign::Minus(_)), Amount::Infinity(_)) => {
184									{#root}::core::f64::NEG_INFINITY
185								}
186								(_, Amount::Infinity(_)) => {
187									{#root}::core::f64::INFINITY
188								}
189								(Some(Sign::Minus(minus)), amount) => {
190									{#paste minus } {#paste amount }
191								}
192								(_, amount) => { {#paste amount } }
193							}
194						)
195					}
196				}
197			)
198		])
199	}
200}
201
202impl SimpleSpanned for Sign {
203	fn span(&self) -> Span {
204		match self {
205			Sign::Plus(plus) => &plus.0,
206			Sign::Minus(minus) => &minus.0,
207		}
208		.span()
209	}
210
211	fn set_span(&mut self, span: Span) {
212		match self {
213			Sign::Plus(plus) => &mut plus.0,
214			Sign::Minus(minus) => &mut minus.0,
215		}
216		.set_span(span)
217	}
218}
219
220impl Amount {
221	fn span(&self) -> Span {
222		match self {
223			Amount::Finite(NumberLiteral(dot, literal)) => dot
224				.as_ref()
225				.and_then(|dot| dot.0.span().join(literal.span()))
226				.unwrap_or(literal.span()),
227			Amount::Infinity(infinity) => infinity.0.span(),
228		}
229	}
230}
231
232impl IntoTokens for Amount {
233	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
234		let Self::Finite(finite) = self else {
235			unreachable!("Handled by Number.")
236		};
237		finite.into_tokens(root, tokens)
238	}
239}
240
241impl IntoTokens for True {
242	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
243		quote_into_mixed_site!(self.0.span(), root, tokens, [
244			{#root}::json::JsonValue::Boolean(true)
245		]);
246	}
247}
248
249impl IntoTokens for False {
250	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
251		quote_into_mixed_site!(self.0.span(), root, tokens, [
252			{#root}::json::JsonValue::Boolean(false)
253		]);
254	}
255}
256
257impl IntoTokens for Null {
258	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
259		quote_into_mixed_site!(self.0.span(), root, tokens, [
260			{#root}::json::JsonValue::Null
261		]);
262	}
263}
264
265impl IntoTokens for String {
266	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
267		quote_into_mixed_site!(self.0.span(), root, tokens, [
268			{#root}::json::JsonValue::String(
269				{#paste self.0 }.to_string()
270			)
271		]);
272	}
273}
274
275/// This is just a convenience to get the source location when a panic occurs.
276fn hook_panics() {
277	std::panic::set_hook(Box::new(|panic_info| {
278		let location = panic_info.location();
279
280		let payload = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
281			s
282		} else if let Some(s) = panic_info.payload().downcast_ref::<::std::string::String>() {
283			s.as_str()
284		} else {
285			"(unknown panic type)"
286		};
287		eprintln!(
288			"proc macro panic at {}:{}\n\n{}",
289			location.map(|l| l.file()).unwrap_or("None"),
290			location
291				.map(|l| l.line().to_string())
292				.unwrap_or_else(|| "None".to_string()),
293			payload
294		);
295	}))
296}