make the proc macro DSL parser far more readable

This commit is contained in:
Iris System 2024-09-13 23:58:56 +12:00
parent fce23c2b90
commit 737d6d3216
3 changed files with 90 additions and 49 deletions

1
Cargo.lock generated
View file

@ -475,6 +475,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77",
] ]
[[package]] [[package]]

View file

@ -9,3 +9,4 @@ proc-macro = true
[dependencies] [dependencies]
quote = "1.0" quote = "1.0"
proc-macro2 = "1.0" proc-macro2 = "1.0"
syn = "2.0"

View file

@ -1,57 +1,95 @@
use proc_macro2::{Delimiter, TokenStream, TokenTree, Ident, Literal, Span}; use proc_macro2::{Delimiter, TokenStream, TokenTree, Literal, Span};
use quote::quote; use syn::parse::{Parse, ParseStream, Result as ParseResult};
use syn::{parse_macro_input, Token, Ident};
use quote::{quote, quote_spanned};
fn make_command( enum CommandToken {
tokens: Vec<TokenStream>, /// "typed argument" being a member of the `Token` enum in the
help: Literal, /// command parser crate.
cb: Literal, ///
) -> TokenStream { /// prefixed with `@` in the command macro.
quote! { TypedArgument(Ident, Span),
Command { tokens: vec![#(#tokens),*], help: #help.to_string(), cb: #cb.to_string() }
/// interpreted as a literal string in the command input.
///
/// no prefix in the command macro.
Literal(Literal, Span),
}
impl Parse for CommandToken {
fn parse(input: ParseStream) -> ParseResult<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![@]) {
// typed argument
input.parse::<Token![@]>()?;
let ident = input.parse::<Ident>()?;
Ok(Self::TypedArgument(ident.clone(), ident.span()))
} else if lookahead.peek(Ident) {
// literal string
let ident = input.parse::<Ident>()?;
let lit = Literal::string(&format!("{ident}"));
Ok(Self::Literal(lit, ident.span()))
} else {
Err(input.error("expected a command token"))
}
} }
} }
fn command_from_stream(stream: TokenStream) -> TokenStream { impl Into<TokenStream> for CommandToken {
let mut part = 0; fn into(self) -> TokenStream {
let mut found_tokens: Vec<TokenStream> = Vec::new(); match self {
let mut found_cb: Option<Literal> = None; Self::TypedArgument(ident, span) => quote_spanned! {span=>
let mut found_help: Option<Literal> = None; Token::#ident
},
let mut is_token_lit = false; Self::Literal(lit, span) => quote_spanned! {span=>
let mut tokens = stream.clone().into_iter(); Token::Value(vec![ #lit.to_string(), ])
'a: loop { },
let cur_token = tokens.next(); }.into()
match cur_token { }
None if part == 2 && found_help.is_some() => break 'a, }
Some(TokenTree::Ident(ident)) if part == 0 => {
found_tokens.push(if is_token_lit { struct Command {
quote! { Token::#ident }.into() tokens: Vec<CommandToken>,
} else { help: Literal,
let lit = Literal::string(&format!("{ident}")); cb: Literal,
quote! { Token::Value(vec![#lit.to_string() ]) } }
});
// reset this impl Parse for Command {
is_token_lit = false; fn parse(input: ParseStream) -> ParseResult<Self> {
let mut tokens = Vec::<CommandToken>::new();
loop {
if input.peek(Token![,]) {
break;
} }
Some(TokenTree::Punct(punct)) if part == 0 && format!("{punct}") == "@" => {
is_token_lit = true tokens.push(input.parse::<CommandToken>()?);
} }
Some(TokenTree::Punct(punct)) input.parse::<Token![,]>()?;
if ((part == 0 && found_tokens.len() > 0) || (part == 1 && found_cb.is_some()))
&& format!("{punct}") == "," => let cb_ident = input.parse::<Ident>()?;
{ let cb = Literal::string(&format!("{cb_ident}"));
part += 1 input.parse::<Token![,]>()?;
}
Some(TokenTree::Ident(ident)) if part == 1 => { let help = input.parse::<Literal>()?;
found_cb = Some(Literal::string(&format!("{ident}")))
} Ok(Self {
Some(TokenTree::Literal(lit)) if part == 2 => { tokens,
found_help = Some(lit) cb,
} help,
_ => panic!("invalid command definition: {stream}"), })
}
}
impl Into<TokenStream> for Command {
fn into(self) -> TokenStream {
let Self { tokens, help, cb } = self;
let tokens = tokens.into_iter().map(Into::into).collect::<Vec<TokenStream>>();
quote! {
Command { tokens: vec![#(#tokens),*], help: #help.to_string(), cb: #cb.to_string() }
} }
} }
make_command(found_tokens, found_help.unwrap(), found_cb.unwrap())
} }
#[proc_macro] #[proc_macro]
@ -70,7 +108,8 @@ pub fn commands(stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
// //
match top_level_tokens.next() { match top_level_tokens.next() {
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => { Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => {
commands.push(command_from_stream(group.stream())); let group_stream: proc_macro::TokenStream = group.stream().into();
commands.push(parse_macro_input!(group_stream as Command).into());
} }
_ => panic!("contents of commands! macro is invalid"), _ => panic!("contents of commands! macro is invalid"),
} }
@ -83,8 +122,8 @@ pub fn commands(stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
let command_registrations = commands let command_registrations = commands
.iter() .iter()
.map(|v| -> proc_macro2::TokenStream { quote! { tree.register_command(#v); }.into() }) .map(|v| -> TokenStream { quote! { tree.register_command(#v); }.into() })
.collect::<proc_macro2::TokenStream>(); .collect::<TokenStream>();
let res = quote! { let res = quote! {
lazy_static::lazy_static! { lazy_static::lazy_static! {