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	Parentheses, 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		InlineRust(InlineRust),
63	} else "Expected JSON5 value."; // The error message that's used when no variant is peeked successfully.
64
65	struct Array: PeekFrom, PopFrom (SquareBrackets<Delimited<Value, Comma>>);
66
67	struct Object: PeekFrom, PopFrom (Braces<Delimited<Property, Comma>>);
68
69	struct Property: PopFrom {
70		key: Key,
71		colon: Colon,
72		value: Value,
73	}
74
75	enum Key: PopFrom {
76		Identifier(Identifier),
77		String(String),
78	} else "Expected key (plain identifier or string literal).";
79
80	enum Number: PeekFrom, PopFrom {
81		NaN(NaN),
82		NotNaN(NotNaN),
83	} else "Expected Number."; // Should be unreachable.
84
85	struct NotNaN: PopFrom {
86		sign: Option<Sign>,
87		amount: Amount,
88	}
89
90	enum Sign: PeekFrom, PopFrom {
91		Plus(Plus),
92		Minus(Minus),
93	} else "Expected Sign."; // Should be unreachable.
94
95	enum Amount: PeekFrom, PopFrom {
96		Finite(NumberLiteral), // This could be a structured variant too, but `grammar!` doesn't support that yet.
97		Infinity(Infinity),
98	} else "Expected number literal or `infinity`.";
99
100	struct InlineRust: PeekFrom, PopFrom (Parentheses);
101}
102
103impl Value {
104	/// Uses for narrowing the input range in which property errors are reported.
105	fn span(&self) -> Span {
106		match self {
107			Value::String(s) => s.0.span(),
108			Value::Number(number) => match number {
109				Number::NaN(nan) => nan.0.span(),
110				Number::NotNaN(not_nan) => not_nan.amount.span(),
111			},
112			Value::Object(object) => object.0.span.join(),
113			Value::Array(array) => array.0.span.join(),
114			Value::True(t) => t.0.span(),
115			Value::False(f) => f.0.span(),
116			Value::Null(n) => n.0.span(),
117			Value::InlineRust(inline_rust) => inline_rust.0.span.join(),
118		}
119	}
120}
121
122/// [`NotNaN`] starts with an [`Option`], which isn't peekable since that's error-prone in [`grammar!`].
123///
124/// As such, this is implemented manually. (Maybe I'll add a way to have this automatically,
125/// but it's unlikely since that might have an impact on compile time that's disproportionate to the work done.)
126impl PeekFrom for NotNaN {
127	fn peek_from(input: &Input) -> bool {
128		Sign::peek_from(input) || Amount::peek_from(input)
129	}
130}
131
132// Alternatively, you could `{#match …, … }` in a template for `Value`, but here it's convenient to split the branches.
133
134impl IntoTokens for Object {
135	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
136		quote_into_mixed_site!(self.0.span.join(), root, tokens, [
137			{#root}::json::JsonValue::Object({
138				let mut object = {#root}::json::object::Object::with_capacity(
139					// Whoops, I forgot to implement `IntoTokens` for `Literal`.
140					{#paste Literal::usize_unsuffixed(self.0.contents.0.len()) }
141				);
142				{#for (property, comma) in self.0.contents.0,
143					{#let Property { key, colon, value } = property;}
144					{#located_at value.span(),
145						object.insert(
146							{#paste key }
147							{#located_at colon.0.span(), , }
148							{#paste value }
149						)
150					}
151					{#located_at comma.map(|comma| comma.0.span()).unwrap_or(self.0.span.close()), ; }
152				}
153				object
154			})
155		])
156	}
157}
158
159impl IntoTokens for Key {
160	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
161		match self {
162			Key::Identifier(identifier) => {
163				let mut s = Literal::string(&identifier.0.to_string());
164				s.set_span(identifier.0.span());
165				s.into_tokens(root, tokens)
166			}
167			Key::String(s) => s.0.into_tokens(root, tokens),
168		}
169	}
170}
171
172impl IntoTokens for Array {
173	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
174		quote_into_mixed_site!(self.0.span.join(), root, tokens, [
175			{#root}::json::JsonValue::Array({
176				let mut vec = {#root}::std::vec::Vec::with_capacity(
177					// Whoops, I forgot to implement `IntoTokens` for `Literal`.
178					{#paste Literal::usize_unsuffixed(self.0.contents.0.len()) }
179				);
180				{#for (item, comma) in self.0.contents.0,
181					{#located_at item.span(),
182						vec.push({#paste item })
183					}
184					{#located_at comma.map(|comma| comma.0.span()).unwrap_or(self.0.span.close()), ; }
185				}
186				vec
187			})
188		])
189	}
190}
191
192impl IntoTokens for Number {
193	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
194		let span = match &self {
195			Number::NaN(nan) => nan.0.span(),
196			Number::NotNaN(NotNaN {
197				sign: Some(sign),
198				amount,
199			}) => sign.span().join(amount.span()).unwrap_or(amount.span()),
200			Number::NotNaN(NotNaN { sign: None, amount }) => amount.span(),
201		};
202		quote_into_mixed_site!(span, root, tokens, [
203			{#root}::json::JsonValue::Number(
204				{#match self,
205					Self::NaN(_) => { {#root}::json::number::NAN }
206					Self::NotNaN(NotNaN { sign, amount }) => {
207						{#root}::core::convert::From::from(
208							{#match (sign, amount),
209								(Some(Sign::Minus(_)), Amount::Infinity(_)) => {
210									{#root}::core::f64::NEG_INFINITY
211								}
212								(_, Amount::Infinity(_)) => {
213									{#root}::core::f64::INFINITY
214								}
215								(Some(Sign::Minus(minus)), amount) => {
216									{#paste minus } {#paste amount }
217								}
218								(_, amount) => { {#paste amount } }
219							}
220						)
221					}
222				}
223			)
224		])
225	}
226}
227
228impl SimpleSpanned for Sign {
229	fn span(&self) -> Span {
230		match self {
231			Sign::Plus(plus) => &plus.0,
232			Sign::Minus(minus) => &minus.0,
233		}
234		.span()
235	}
236
237	fn set_span(&mut self, span: Span) {
238		match self {
239			Sign::Plus(plus) => &mut plus.0,
240			Sign::Minus(minus) => &mut minus.0,
241		}
242		.set_span(span)
243	}
244}
245
246impl Amount {
247	fn span(&self) -> Span {
248		match self {
249			Amount::Finite(NumberLiteral(dot, literal)) => dot
250				.as_ref()
251				.and_then(|dot| dot.0.span().join(literal.span()))
252				.unwrap_or(literal.span()),
253			Amount::Infinity(infinity) => infinity.0.span(),
254		}
255	}
256}
257
258impl IntoTokens for Amount {
259	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
260		let Self::Finite(finite) = self else {
261			unreachable!("Handled by Number.")
262		};
263		finite.into_tokens(root, tokens)
264	}
265}
266
267impl IntoTokens for True {
268	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
269		quote_into_mixed_site!(self.0.span(), root, tokens, [
270			{#root}::json::JsonValue::Boolean(true)
271		]);
272	}
273}
274
275impl IntoTokens for False {
276	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
277		quote_into_mixed_site!(self.0.span(), root, tokens, [
278			{#root}::json::JsonValue::Boolean(false)
279		]);
280	}
281}
282
283impl IntoTokens for Null {
284	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
285		quote_into_mixed_site!(self.0.span(), root, tokens, [
286			{#root}::json::JsonValue::Null
287		]);
288	}
289}
290
291impl IntoTokens for String {
292	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
293		quote_into_mixed_site!(self.0.span(), root, tokens, [
294			{#root}::json::JsonValue::String(
295				{#paste self.0 }.to_string()
296			)
297		]);
298	}
299}
300
301impl IntoTokens for InlineRust {
302	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
303		// Unwrap from parentheses.
304		self.0.contents.into_tokens(root, tokens)
305	}
306}
307
308/// This is just a convenience to get the source location when a panic occurs.
309fn hook_panics() {
310	std::panic::set_hook(Box::new(|panic_info| {
311		let location = panic_info.location();
312
313		let payload = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
314			s
315		} else if let Some(s) = panic_info.payload().downcast_ref::<::std::string::String>() {
316			s.as_str()
317		} else {
318			"(unknown panic type)"
319		};
320		eprintln!(
321			"proc macro panic at {}:{}\n\n{}",
322			location.map(|l| l.file()).unwrap_or("None"),
323			location
324				.map(|l| l.line().to_string())
325				.unwrap_or_else(|| "None".to_string()),
326			payload
327		);
328	}))
329}