feat: implement grammar

This commit is contained in:
Kristofers Solo 2025-09-09 11:40:56 +03:00
parent aaf5a081c1
commit 3d6d32af49
Signed by: kristoferssolo
GPG Key ID: 74FF8144483D82C8
7 changed files with 125 additions and 234 deletions

View File

@ -1,4 +1,4 @@
use proc_macro2::{Ident, TokenStream};
use unsynn::{Ident, TokenStream};
#[derive(Debug)]
pub struct StructInfo {

View File

@ -1,84 +0,0 @@
use crate::from_file::{
ast::StructInfo, error::FromFileError, parser::parse_from_file_default_attr,
};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
pub fn generate_impl(info: &StructInfo) -> Result<TokenStream, FromFileError> {
let name = &info.ident;
let vis = &info.vis;
let generics = &info.generics;
let file_ident = format_ident!("{name}File");
let mut file_fields = Vec::new();
let mut assignments = Vec::new();
for field in &info.fields {
let ident = &field.ident;
let ty = &field.ty;
let default_override = parse_from_file_default_attr(&field.attrs)?;
let shadow_ty = quote! { <#ty as fielcaster::FromFile>::Shadow };
file_fields.push(quote! { pub #ident: Option<#shadow_ty> });
if let Some(expr) = default_override {
assignments.push(quote! {
#ident: fide.#ident
.map(|inner| <#ty as filecaster::FromFile>::from_file(Some(inner)))
.unwrap_or(#expr)
});
} else {
assignments.push(quote! {
#ident: <#ty as filecaster::FromFile>::from_file(file.#ident)
});
}
}
let derive_clause = build_derive_clause();
Ok(quote! {
#derive_clause
#vis struct #file_ident #generics {
#(#file_fields),*
}
impl #generics filecaster::FromFile for #name #generics {
type Shadow = #file_ident #generics;
fn from_file(file: Option<Self::Shadow>) -> Self {
let file = file.unwrap_or_default();
Self {
#(#assignments),*
}
}
}
impl #generics From<Option<#file_ident #generics>> for #name #generics {
fn from(value: Option<#file_ident #generics>) -> Self {
<Self as filecaster::FromFile>::from_file(value)
}
}
impl #generics From<#file_ident #generics> for #name #generics {
fn from(value: #file_ident #generics) -> Self {
<Self as filecaster::FromFile>::from_file(Some(value))
}
}
})
}
fn build_derive_clause() -> TokenStream {
let mut traits = vec![quote! {Debug}, quote! {Clone}, quote! {Default}];
#[cfg(feature = "serde")]
{
traits.push(quote! { serde::Deserialize });
traits.push(quote! { serde::Serialize });
}
#[cfg(feature = "merge")]
{
traits.push(quote! { merge::Merge });
}
quote! { #[derive( #(#traits),* )] }
}

View File

@ -1,22 +1,11 @@
use proc_macro2::{Span, TokenStream};
use quote::quote_spanned;
use thiserror::Error;
use unsynn::TokenStream;
#[derive(Debug, Error)]
pub enum FromFileError {
#[error("FromFile only works on structs with named fields")]
NotNamedStruct { span: Span },
#[error("Invalid #[from_file] attribute format")]
InvalidAttribute { span: Span },
}
pub enum FromFileError {}
impl FromFileError {
pub fn to_compile_error(&self) -> TokenStream {
let msg = self.to_string();
match self {
FromFileError::NotNamedStruct { span } | FromFileError::InvalidAttribute { span } => {
quote_spanned!(*span => compile_error!(#msg))
}
}
todo!()
}
}

View File

@ -1,20 +1,127 @@
use unsynn::*;
keyword! {
KwStruct = "struct";
KwPub = "pub";
}
/*
pub struct Foo {
#[attr("value")]
pub bar: String,
}
*/
unsynn! {
pub struct Field {
pub attrs: Vec<TokenStream>,
pub name: Ident,
pub colon: Colon,
pub ty: TokenStream
pub struct Attribute {
pub pound: Pound, // #
pub bracket_group: BracketGroupContaining<TokenStream> // [attr("value")]
}
pub struct StructBody(pub CommaDelimitedVec<Field>);
pub struct Field {
pub attrs: Option<Vec<Attribute>>, // #[attr("value")]
pub vis: Optional<KwPub>, // pub
pub name: Ident, // bar
pub colon: Colon, // :
pub ty: Ident // String
}
pub struct StructBody(pub CommaDelimitedVec<Field>); // all fields
pub struct StructDef {
pub vis: TokenStream,
pub kw_struct: Ident,
pub name: Ident,
pub generics: TokenStream,
pub vis: Optional<KwPub>, // pub
pub kw_struct: KwStruct, // "struct" keyword
pub name: Ident, // Foo
pub generics: Optional<BracketGroupContaining<TokenStream>>,
pub body: BraceGroupContaining<StructBody>
}
}
#[cfg(test)]
mod tests {
use super::*;
use claims::assert_some;
const SAMPLE: &str = r#"
pub struct Foo {
#[attr("value")]
pub bar: String,
#[attr("number")]
pub baz: i32
}
"#;
#[test]
fn parse_attribute_roundup() {
let mut iter = r#"#[attr("value")]"#.to_token_iter();
let attr = iter
.parse::<Attribute>()
.expect("failed to parse Attribute");
assert_eq!(attr.pound.tokens_to_string(), "#".tokens_to_string());
let s = attr.bracket_group.tokens_to_string();
assert!(s.contains("attr"));
assert!(s.contains("\"value\""));
let rt = attr.tokens_to_string();
assert!(rt.contains("attr"));
assert!(rt.contains("\"value\""));
}
#[test]
fn parse_field_with_attr_and_vid_and_roundtrip() {
let mut iter = r#"#[attr("value")] pub bar: String"#.to_token_iter();
let field = iter.parse::<Field>().expect("failed to parse Field");
assert_some!(&field.attrs);
let attrs = field.attrs.as_ref().unwrap();
assert_eq!(attrs.len(), 1);
assert_eq!(field.name.tokens_to_string(), "bar".tokens_to_string());
assert_eq!(field.ty.tokens_to_string(), "String".tokens_to_string());
let tokens = field.tokens_to_string();
assert!(tokens.contains("attr"));
assert!(tokens.contains("bar"));
assert!(tokens.contains("String"));
}
#[test]
fn parse_struct_def_and_inspect_body() {
let mut iter = SAMPLE.to_token_iter();
dbg!(&iter);
let sdef = iter
.parse::<StructDef>()
.expect("faield to parse StructDef");
assert_eq!(
sdef.kw_struct.tokens_to_string(),
"struct".tokens_to_string()
);
assert_eq!(sdef.name.tokens_to_string(), "Foo".tokens_to_string());
let body = &sdef.body.content.0.0;
assert_eq!(body.len(), 2);
let field = &body[0].value;
assert_some!(field.attrs.as_ref());
assert_eq!(field.name.tokens_to_string(), "bar".tokens_to_string());
assert_eq!(field.ty.tokens_to_string(), "String".tokens_to_string());
let out = sdef.tokens_to_string();
assert!(out.contains("pub"));
assert!(out.contains("struct"));
assert!(out.contains("Foo"));
assert!(out.contains("attr"));
assert!(out.contains("bar"));
assert!(out.contains("String"));
}
#[test]
fn parse_failure_for_incomplete_struct() {
let mut iter = "pub struct Foo".to_token_iter();
let res = iter.parse::<StructDef>();
assert!(res.is_err(), "expected parse error for incomplete struct");
}
}

View File

@ -1,13 +1,11 @@
mod ast;
mod codegen;
mod error;
mod grammar;
mod parser;
use crate::from_file::{codegen::generate_impl, error::FromFileError, parser::parse_scruct_info};
use proc_macro2::TokenStream;
use crate::from_file::error::FromFileError;
use unsynn::TokenStream;
pub fn impl_from_file(input: TokenStream) -> Result<TokenStream, FromFileError> {
let info = parse_scruct_info(input)?;
generate_impl(&info)
todo!()
}

View File

@ -1,118 +0,0 @@
use std::iter::{Peekable, once};
use crate::from_file::ast::AttributeInfo;
use crate::from_file::grammar::{Field, StructDef};
use crate::from_file::{
ast::{FieldInfo, StructInfo},
error::FromFileError,
};
use proc_macro2::{Ident, Span, TokenStream};
use unsynn::TokenTree;
use unsynn::{IParse, ToTokens};
pub fn parse_scruct_info(input: TokenStream) -> Result<StructInfo, FromFileError> {
let mut iter = input.to_token_iter();
let def = iter
.parse::<StructDef>()
.map_err(|_| FromFileError::NotNamedStruct {
span: Span::call_site(),
})?;
Ok(def.into())
}
pub fn parse_from_file_default_attr(
attrs: &[AttributeInfo],
) -> Result<Option<TokenStream>, FromFileError> {
for attr in attrs {
if attr.path == "from_file" {
return extract_default_token(attr.tokens.clone())
.map(Some)
.ok_or_else(|| FromFileError::InvalidAttribute {
span: attr.path.span(),
});
}
}
Ok(None)
}
fn extract_default_token(tokens: TokenStream) -> Option<TokenStream> {
let mut iter = tokens.into_iter().peekable();
while let Some(TokenTree::Ident(id)) = iter.next() {
if id != "default" {
continue;
}
match iter.next() {
Some(TokenTree::Punct(eq)) if eq.as_char() == '=' => {
return Some(collect_until_commas(&mut iter));
}
_ => return None,
}
}
None
}
fn collect_until_commas<I>(iter: &mut Peekable<I>) -> TokenStream
where
I: Iterator<Item = TokenTree>,
{
let mut expr = TokenStream::new();
while let Some(tt) = iter.peek() {
let is_comma = matches!(tt, TokenTree::Punct(p) if p.as_char() ==',');
if is_comma {
iter.next();
break;
}
expr.extend(once(iter.next().unwrap()));
}
expr
}
impl From<StructDef> for StructInfo {
fn from(value: StructDef) -> Self {
Self {
ident: value.name,
vis: value.vis,
generics: value.generics,
fields: value
.body
.content
.0
.into_iter()
.map(|d| d.value.into())
.collect(),
}
}
}
impl From<Field> for FieldInfo {
fn from(value: Field) -> Self {
Self {
ident: value.name,
ty: value.ty,
attrs: value
.attrs
.into_iter()
.map(|ts| {
let path = extract_attr_path(ts.clone());
AttributeInfo { path, tokens: ts }
})
.collect(),
}
}
}
fn extract_attr_path(attr_tokens: TokenStream) -> Ident {
attr_tokens
.into_iter()
.find_map(|tt| {
if let TokenTree::Ident(id) = tt {
Some(id)
} else {
None
}
})
.unwrap_or_else(|| Ident::new("unknown", Span::call_site()))
}

View File

@ -96,11 +96,10 @@
mod from_file;
use crate::from_file::impl_from_file;
use proc_macro::TokenStream;
use proc_macro_error2::proc_macro_error;
use crate::from_file::impl_from_file;
/// Implements the [`FromFile`] trait.
///
/// This macro processes the `#[from_file]` attribute on structs to generate