1
Fork 0
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:
DQ 2023-04-18 20:10:35 +02:00 committed by GitHub
parent 2aa543036f
commit c5279bae7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 1459 additions and 899 deletions

18
cstree-derive/Cargo.toml Normal file
View 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" }

View file

@ -0,0 +1 @@
../LICENSE-APACHE

1
cstree-derive/LICENSE-MIT Symbolic link
View file

@ -0,0 +1 @@
../LICENSE-MIT

1
cstree-derive/README.md Symbolic link
View file

@ -0,0 +1 @@
../README.md

View 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
View 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)*)
}

View 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
}
}
}

View 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,
}
}
}

View 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()
}
}