mirror of
https://github.com/RGBCube/cstree
synced 2025-07-27 17:17:45 +00:00
Add derive macro for Syntax
(used to be Language
) (#51)
This commit is contained in:
parent
2aa543036f
commit
c5279bae7d
70 changed files with 1459 additions and 899 deletions
18
cstree-derive/Cargo.toml
Normal file
18
cstree-derive/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "cstree_derive"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "cstree_derive"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.56"
|
||||
quote = "1.0.26"
|
||||
syn = { version = "2.0.14" }
|
1
cstree-derive/LICENSE-APACHE
Symbolic link
1
cstree-derive/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../LICENSE-APACHE
|
1
cstree-derive/LICENSE-MIT
Symbolic link
1
cstree-derive/LICENSE-MIT
Symbolic link
|
@ -0,0 +1 @@
|
|||
../LICENSE-MIT
|
1
cstree-derive/README.md
Symbolic link
1
cstree-derive/README.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../README.md
|
56
cstree-derive/src/errors.rs
Normal file
56
cstree-derive/src/errors.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use std::{cell::RefCell, fmt, thread};
|
||||
|
||||
use quote::ToTokens;
|
||||
|
||||
/// Context to collect multiple errors and output them all after parsing in order to not abort
|
||||
/// immediately on the first error.
|
||||
///
|
||||
/// Ensures that the errors are handled using [`check`](ErrorContext::check) by otherwise panicking
|
||||
/// on `Drop`.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct ErrorContext {
|
||||
errors: RefCell<Option<Vec<syn::Error>>>,
|
||||
}
|
||||
|
||||
impl ErrorContext {
|
||||
/// Create a new context.
|
||||
///
|
||||
/// This context contains no errors, but will still trigger a panic if it is not `check`ed.
|
||||
pub fn new() -> Self {
|
||||
ErrorContext {
|
||||
errors: RefCell::new(Some(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an error to the context that points to `source`.
|
||||
pub fn error_at<S: ToTokens, T: fmt::Display>(&self, source: S, msg: T) {
|
||||
self.errors
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
// Transform `ToTokens` here so we don't monomorphize `new_spanned` so much.
|
||||
.push(syn::Error::new_spanned(source.into_token_stream(), msg));
|
||||
}
|
||||
|
||||
/// Add a `syn` parse error directly.
|
||||
pub fn syn_error(&self, err: syn::Error) {
|
||||
self.errors.borrow_mut().as_mut().unwrap().push(err);
|
||||
}
|
||||
|
||||
/// Consume the context, producing a formatted error string if there are errors.
|
||||
pub fn check(self) -> Result<(), Vec<syn::Error>> {
|
||||
let errors = self.errors.borrow_mut().take().unwrap();
|
||||
match errors.len() {
|
||||
0 => Ok(()),
|
||||
_ => Err(errors),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ErrorContext {
|
||||
fn drop(&mut self) {
|
||||
if !thread::panicking() && self.errors.borrow().is_some() {
|
||||
panic!("forgot to check for errors");
|
||||
}
|
||||
}
|
||||
}
|
74
cstree-derive/src/lib.rs
Normal file
74
cstree-derive/src/lib.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use errors::ErrorContext;
|
||||
use parsing::SyntaxKindEnum;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, quote_spanned};
|
||||
use syn::{parse_macro_input, spanned::Spanned, DeriveInput};
|
||||
|
||||
mod errors;
|
||||
mod parsing;
|
||||
mod symbols;
|
||||
|
||||
use symbols::*;
|
||||
|
||||
#[proc_macro_derive(Syntax, attributes(static_text))]
|
||||
pub fn language(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
expand_syntax(ast).unwrap_or_else(to_compile_errors).into()
|
||||
}
|
||||
|
||||
fn expand_syntax(ast: DeriveInput) -> Result<TokenStream, Vec<syn::Error>> {
|
||||
let error_handler = ErrorContext::new();
|
||||
let Ok(syntax_kind_enum) = SyntaxKindEnum::parse_from_ast(&error_handler, &ast) else {
|
||||
return Err(error_handler.check().unwrap_err());
|
||||
};
|
||||
|
||||
// Check that the `enum` is `#[repr(u32)]`
|
||||
match &syntax_kind_enum.repr {
|
||||
Some(repr) if repr == U32 => (),
|
||||
Some(_) | None => error_handler.error_at(
|
||||
syntax_kind_enum.source,
|
||||
"syntax kind definitions must be `#[repr(u32)]` to derive `Syntax`",
|
||||
),
|
||||
}
|
||||
|
||||
error_handler.check()?;
|
||||
|
||||
let name = &syntax_kind_enum.name;
|
||||
let variant_count = syntax_kind_enum.variants.len() as u32;
|
||||
let static_texts = syntax_kind_enum.variants.iter().map(|variant| {
|
||||
let variant_name = &variant.name;
|
||||
let static_text = match variant.static_text.as_deref() {
|
||||
Some(text) => quote!(::core::option::Option::Some(#text)),
|
||||
None => quote!(::core::option::Option::None),
|
||||
};
|
||||
quote_spanned!(variant.source.span()=>
|
||||
#name :: #variant_name => #static_text,
|
||||
)
|
||||
});
|
||||
let trait_impl = quote_spanned! { syntax_kind_enum.source.span()=>
|
||||
#[automatically_derived]
|
||||
impl ::cstree::Syntax for #name {
|
||||
fn from_raw(raw: ::cstree::RawSyntaxKind) -> Self {
|
||||
assert!(raw.0 < #variant_count, "Invalid raw syntax kind: {}", raw.0);
|
||||
// Safety: discriminant is valid by the assert above
|
||||
unsafe { ::std::mem::transmute::<u32, #name>(raw.0) }
|
||||
}
|
||||
|
||||
fn into_raw(self) -> ::cstree::RawSyntaxKind {
|
||||
::cstree::RawSyntaxKind(self as u32)
|
||||
}
|
||||
|
||||
fn static_text(self) -> ::core::option::Option<&'static str> {
|
||||
match self {
|
||||
#( #static_texts )*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(trait_impl)
|
||||
}
|
||||
|
||||
fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
|
||||
let compile_errors = errors.iter().map(syn::Error::to_compile_error);
|
||||
quote!(#(#compile_errors)*)
|
||||
}
|
131
cstree-derive/src/parsing.rs
Normal file
131
cstree-derive/src/parsing.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
mod attributes;
|
||||
|
||||
use syn::{punctuated::Punctuated, Token};
|
||||
|
||||
use crate::{errors::ErrorContext, symbols::*};
|
||||
|
||||
use self::attributes::Attr;
|
||||
|
||||
/// Convenience for recording errors inside `ErrorContext` instead of the `Err` variant of the `Result`.
|
||||
pub(crate) type Result<T, E = ()> = std::result::Result<T, E>;
|
||||
|
||||
pub(crate) struct SyntaxKindEnum<'i> {
|
||||
pub(crate) name: syn::Ident,
|
||||
pub(crate) repr: Option<syn::Ident>,
|
||||
pub(crate) variants: Vec<SyntaxKindVariant<'i>>,
|
||||
pub(crate) source: &'i syn::DeriveInput,
|
||||
}
|
||||
|
||||
impl<'i> SyntaxKindEnum<'i> {
|
||||
pub(crate) fn parse_from_ast(error_handler: &ErrorContext, item: &'i syn::DeriveInput) -> Result<Self> {
|
||||
let syn::Data::Enum(data) = &item.data else {
|
||||
error_handler.error_at(item, "`Syntax` can only be derived on enums");
|
||||
return Err(());
|
||||
};
|
||||
|
||||
let name = item.ident.clone();
|
||||
|
||||
let mut repr = Attr::none(error_handler, REPR);
|
||||
for repr_attr in item.attrs.iter().filter(|&attr| attr.path().is_ident(&REPR)) {
|
||||
if let syn::Meta::List(nested) = &repr_attr.meta {
|
||||
if let Ok(nested) = nested.parse_args_with(Punctuated::<syn::Meta, Token![,]>::parse_terminated) {
|
||||
for meta in nested {
|
||||
if let syn::Meta::Path(path) = meta {
|
||||
if let Some(ident) = path.get_ident() {
|
||||
repr.set(repr_attr, ident.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let variants = data
|
||||
.variants
|
||||
.iter()
|
||||
.map(|variant| SyntaxKindVariant::parse_from_ast(error_handler, variant))
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
repr: repr.get(),
|
||||
variants,
|
||||
source: item,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SyntaxKindVariant<'i> {
|
||||
pub(crate) name: syn::Ident,
|
||||
pub(crate) static_text: Option<String>,
|
||||
pub(crate) source: &'i syn::Variant,
|
||||
}
|
||||
|
||||
impl<'i> SyntaxKindVariant<'i> {
|
||||
pub(crate) fn parse_from_ast(error_handler: &ErrorContext, variant: &'i syn::Variant) -> Self {
|
||||
let name = variant.ident.clone();
|
||||
|
||||
// Check that `variant` is a unit variant
|
||||
match &variant.fields {
|
||||
syn::Fields::Unit => (),
|
||||
syn::Fields::Named(_) | syn::Fields::Unnamed(_) => {
|
||||
error_handler.error_at(variant, "syntax kinds with fields are not supported");
|
||||
}
|
||||
}
|
||||
|
||||
// Check that discriminants are unaltered
|
||||
if variant.discriminant.is_some() {
|
||||
error_handler.error_at(
|
||||
variant,
|
||||
"syntax kinds are not allowed to have custom discriminant values",
|
||||
);
|
||||
}
|
||||
|
||||
let mut static_text = Attr::none(error_handler, STATIC_TEXT);
|
||||
for text in variant
|
||||
.attrs
|
||||
.iter()
|
||||
.flat_map(|attr| get_static_text(error_handler, attr))
|
||||
{
|
||||
static_text.set(&text, text.value());
|
||||
}
|
||||
Self {
|
||||
name,
|
||||
static_text: static_text.get(),
|
||||
source: variant,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_static_text(error_handler: &ErrorContext, attr: &syn::Attribute) -> Option<syn::LitStr> {
|
||||
use syn::Meta::*;
|
||||
|
||||
if attr.path() != STATIC_TEXT {
|
||||
return None;
|
||||
}
|
||||
|
||||
match &attr.meta {
|
||||
List(list) => match list.parse_args() {
|
||||
Ok(lit) => Some(lit),
|
||||
Err(e) => {
|
||||
error_handler.error_at(
|
||||
list,
|
||||
"argument to `static_text` must be a string literal: `#[static_text(\"...\")]`",
|
||||
);
|
||||
error_handler.syn_error(e);
|
||||
None
|
||||
}
|
||||
},
|
||||
Path(_) => {
|
||||
error_handler.error_at(attr, "missing text for `static_text`: try `#[static_text(\"...\")]`");
|
||||
None
|
||||
}
|
||||
NameValue(_) => {
|
||||
error_handler.error_at(
|
||||
attr,
|
||||
"`static_text` takes the text as a function argument: `#[static_text(\"...\")]`",
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
59
cstree-derive/src/parsing/attributes.rs
Normal file
59
cstree-derive/src/parsing/attributes.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
#![allow(unused)]
|
||||
|
||||
use super::*;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::ToTokens;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Attr<'i, T> {
|
||||
error_handler: &'i ErrorContext,
|
||||
name: Symbol,
|
||||
tokens: TokenStream,
|
||||
value: Option<T>,
|
||||
}
|
||||
|
||||
impl<'i, T> Attr<'i, T> {
|
||||
pub(super) fn none(error_handler: &'i ErrorContext, name: Symbol) -> Self {
|
||||
Attr {
|
||||
error_handler,
|
||||
name,
|
||||
tokens: TokenStream::new(),
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set<S: ToTokens>(&mut self, source: S, value: T) {
|
||||
let tokens = source.into_token_stream();
|
||||
|
||||
if self.value.is_some() {
|
||||
self.error_handler
|
||||
.error_at(tokens, format!("duplicate attribute: `{}`", self.name));
|
||||
} else {
|
||||
self.tokens = tokens;
|
||||
self.value = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_opt<S: ToTokens>(&mut self, source: S, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
self.set(source, value);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_if_none(&mut self, value: T) {
|
||||
if self.value.is_none() {
|
||||
self.value = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get(self) -> Option<T> {
|
||||
self.value
|
||||
}
|
||||
|
||||
pub(super) fn get_with_tokens(self) -> Option<(TokenStream, T)> {
|
||||
match self.value {
|
||||
Some(v) => Some((self.tokens, v)),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
50
cstree-derive/src/symbols.rs
Normal file
50
cstree-derive/src/symbols.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use std::fmt::{self};
|
||||
use syn::{Ident, Path};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Symbol(&'static str);
|
||||
|
||||
pub const STATIC_TEXT: Symbol = Symbol("static_text");
|
||||
pub const REPR: Symbol = Symbol("repr");
|
||||
pub const U32: Symbol = Symbol("u32");
|
||||
|
||||
impl Symbol {
|
||||
pub const fn new(text: &'static str) -> Self {
|
||||
Self(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Symbol> for Ident {
|
||||
fn eq(&self, word: &Symbol) -> bool {
|
||||
self == word.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<Symbol> for &'a Ident {
|
||||
fn eq(&self, word: &Symbol) -> bool {
|
||||
*self == word.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Symbol> for Path {
|
||||
fn eq(&self, word: &Symbol) -> bool {
|
||||
self.is_ident(word.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<Symbol> for &'a Path {
|
||||
fn eq(&self, word: &Symbol) -> bool {
|
||||
self.is_ident(word.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Symbol {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str(self.0)
|
||||
}
|
||||
}
|
||||
impl fmt::Debug for Symbol {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.debug_tuple("Symbol").field(&self.0).finish()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue