diff --git a/Cargo.lock b/Cargo.lock index fa19e0d4..bf681846 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -282,6 +282,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dissimilar" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" + [[package]] name = "either" version = "1.13.0" @@ -370,6 +376,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "hashbrown" version = "0.12.3" @@ -648,6 +660,7 @@ dependencies = [ "inkwell", "insta", "itertools", + "nac3core_derive", "nac3parser", "parking_lot", "rayon", @@ -657,6 +670,18 @@ dependencies = [ "test-case", ] +[[package]] +name = "nac3core_derive" +version = "0.1.0" +dependencies = [ + "nac3core", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", + "trybuild", +] + [[package]] name = "nac3ld" version = "0.1.0" @@ -822,6 +847,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.89" @@ -1076,6 +1125,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_yaml" version = "0.8.26" @@ -1199,6 +1257,12 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "target-triple" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" + [[package]] name = "tempfile" version = "3.14.0" @@ -1222,6 +1286,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "test-case" version = "1.2.3" @@ -1255,6 +1328,56 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "trybuild" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dcd332a5496c026f1e14b7f3d2b7bd98e509660c04239c58b0ba38a12daded4" +dependencies = [ + "dissimilar", + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "typenum" version = "1.17.0" @@ -1478,6 +1601,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 765ab391..7a28185c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "nac3ast", "nac3parser", "nac3core", + "nac3core/nac3core_derive", "nac3standalone", "nac3artiq", "runkernel", diff --git a/nac3core/Cargo.toml b/nac3core/Cargo.toml index cd7ef9f1..460cd2fc 100644 --- a/nac3core/Cargo.toml +++ b/nac3core/Cargo.toml @@ -5,6 +5,8 @@ authors = ["M-Labs"] edition = "2021" [features] +default = ["derive"] +derive = ["dep:nac3core_derive"] no-escape-analysis = [] tracing = [] @@ -14,6 +16,7 @@ crossbeam = "0.8" indexmap = "2.6" parking_lot = "0.12" rayon = "1.10" +nac3core_derive = { path = "nac3core_derive", optional = true } nac3parser = { path = "../nac3parser" } strum = "0.26" strum_macros = "0.26" diff --git a/nac3core/nac3core_derive/Cargo.toml b/nac3core/nac3core_derive/Cargo.toml new file mode 100644 index 00000000..adf4ad8e --- /dev/null +++ b/nac3core/nac3core_derive/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "nac3core_derive" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[[test]] +name = "structfields_tests" +path = "tests/structfields_test.rs" + +[dev-dependencies] +nac3core = { path = ".." } +trybuild = { version = "1.0", features = ["diff"] } + +[dependencies] +proc-macro2 = "1.0" +proc-macro-error = "1.0" +syn = "2.0" +quote = "1.0" diff --git a/nac3core/nac3core_derive/src/lib.rs b/nac3core/nac3core_derive/src/lib.rs new file mode 100644 index 00000000..2f21b00b --- /dev/null +++ b/nac3core/nac3core_derive/src/lib.rs @@ -0,0 +1,231 @@ +use proc_macro::TokenStream; +use proc_macro_error::{abort, proc_macro_error}; +use quote::quote; +use syn::spanned::Spanned; +use syn::{ + parse_macro_input, Data, DataStruct, Expr, ExprPath, GenericArgument, Ident, LitStr, + PathArguments, Type, TypePath, +}; + +/// Extracts all generic arguments of a [`Type`] into a [`Vec`]. +/// +/// Returns [`Some`] of a possibly-empty [`Vec`] if the path of `ty` matches with +/// `expected_ty_name`, otherwise returns [`None`]. +fn extract_generic_args(expected_ty_name: &'static str, ty: &Type) -> Option> { + let Type::Path(TypePath { qself: None, path, .. }) = ty else { + return None; + }; + + let segments = &path.segments; + if segments.len() != 1 { + return None; + }; + + let segment = segments.iter().next().unwrap(); + if segment.ident != expected_ty_name { + return None; + } + + let PathArguments::AngleBracketed(path_args) = &segment.arguments else { + return Some(Vec::new()); + }; + let args = &path_args.args; + + Some(args.iter().cloned().collect::>()) +} + +/// Normalizes a value expression for use when creating an instance of this structure, returning a +/// [`proc_macro2::TokenStream`] of tokens representing the normalized expression. +fn normalize_value_expr(expr: &Expr) -> proc_macro2::TokenStream { + match &expr { + Expr::Path(ExprPath { qself: None, path, .. }) => { + let Ok(ident) = path.require_ident() else { + abort!( + path, + format!( + "Expected one of `size_t`, `usize`, or an implicit call expression in #[value_type(...)], found {}", + quote!(#expr).to_string(), + ) + ) + }; + + if ident == "usize" || ident == "size_t" { + let llvm_usize = Ident::new("llvm_usize", ident.span()); + quote! { #llvm_usize } + } else { + abort!( + path, + format!( + "Expected one of `size_t`, `usize`, or an implicit call expression in #[value_type(...)], found {}", + quote!(#expr).to_string(), + ) + ) + } + } + + Expr::Call(_) | Expr::MethodCall(_) => { + quote! { ctx.#expr } + } + + _ => { + abort!( + expr, + format!( + "Expected one of `size_t`, `usize`, or an implicit call expression in #[value_type(...)], found {}", + quote!(#expr).to_string(), + ) + ) + } + } +} + +/// Derives an implementation of `codegen::types::structure::StructFields`. +/// +/// The benefit of using `#[derive(StructFields)]` is that all index- or order-dependent logic required by +/// `impl StructFields` is automatically generated by this implementation, including the field index as required by +/// `StructField::new` and the fields as returned by `StructFields::to_vec`. +/// +/// # Prerequisites +/// +/// In order to derive from [`StructFields`], you must implement (or derive) [`Eq`] and [`Copy`] as required by +/// `StructFields`. +/// +/// Moreover, `#[derive(StructFields)]` can only be used for `struct`s with named fields, and may only contain fields +/// with either `StructField` or [`PhantomData`] types. +/// +/// # Attributes for [`StructFields`] +/// +/// Each `StructField` field must be declared with the `#[value_type(...)]` attribute. The argument of `value_type` +/// accepts either an expression returning an instance of `inkwell::types::BasicType` without the +/// `inkwell::context::Context` instance prefix, or the reserved identifiers `usize` and `size_t` referring to an +/// `inkwell::types::IntType` of the platform-dependent integer size. +/// +/// # Example +/// +/// The following is an example of an LLVM slice implemented using `#[derive(StructFields)]`. +/// +/// ``` +/// use nac3core::{ +/// codegen::types::structure::StructField, +/// inkwell::{ +/// values::{IntValue, PointerValue}, +/// AddressSpace, +/// }, +/// }; +/// use nac3core_derive::StructFields; +/// +/// // All classes that implement StructFields must also implement Eq and Copy +/// #[derive(PartialEq, Eq, Clone, Copy, StructFields)] +/// pub struct SliceValue<'ctx> { +/// // Declares ptr have a value type of i8* +/// #[value_type(i8_type().ptr_type(AddressSpace::default()))] +/// ptr: StructField<'ctx, PointerValue<'ctx>>, +/// +/// // Declares len have a value type of usize, depending on the target compilation platform +/// #[value_type(usize)] +/// len: StructField<'ctx, IntValue<'ctx>>, +/// } +/// ``` +#[proc_macro_derive(StructFields, attributes(value_type))] +#[proc_macro_error] +pub fn derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as syn::DeriveInput); + let ident = &input.ident; + + let Data::Struct(DataStruct { fields, .. }) = &input.data else { + abort!(input, "Only structs with named fields are supported"); + }; + if let Err(err_span) = + fields + .iter() + .try_for_each(|field| if field.ident.is_some() { Ok(()) } else { Err(field.span()) }) + { + abort!(err_span, "Only structs with named fields are supported"); + }; + + // Check if struct<'ctx> + if input.generics.params.len() != 1 { + abort!(input.generics, "Expected exactly 1 generic parameter") + } + + let phantom_info = fields + .iter() + .filter(|field| extract_generic_args("PhantomData", &field.ty).is_some()) + .map(|field| field.ident.as_ref().unwrap()) + .cloned() + .collect::>(); + + let field_info = fields + .iter() + .filter(|field| extract_generic_args("PhantomData", &field.ty).is_none()) + .map(|field| { + let ident = field.ident.as_ref().unwrap(); + let ty = &field.ty; + + let Some(_) = extract_generic_args("StructField", ty) else { + abort!(field, "Only StructField and PhantomData are allowed") + }; + + let attrs = &field.attrs; + let Some(value_type_attr) = + attrs.iter().find(|attr| attr.path().is_ident("value_type")) + else { + abort!(field, "Expected #[value_type(...)] attribute for field"); + }; + + let Ok(value_type_expr) = value_type_attr.parse_args::() else { + abort!(value_type_attr, "Expected expression in #[value_type(...)]"); + }; + + let value_expr_toks = normalize_value_expr(&value_type_expr); + + (ident.clone(), value_expr_toks) + }) + .collect::>(); + + // `<*>::new` impl of `StructField` and `PhantomData` for `StructFields::new` + let phantoms_create = phantom_info + .iter() + .map(|id| quote! { #id: ::std::marker::PhantomData }) + .collect::>(); + let fields_create = field_info + .iter() + .map(|(id, ty)| { + let id_lit = LitStr::new(&id.to_string(), id.span()); + quote! { + #id: ::nac3core::codegen::types::structure::StructField::create( + &mut counter, + #id_lit, + #ty, + ) + } + }) + .collect::>(); + + // `.into()` impl of `StructField` for `StructFields::to_vec` + let fields_into = + field_info.iter().map(|(id, _)| quote! { self.#id.into() }).collect::>(); + + let impl_block = quote! { + impl<'ctx> ::nac3core::codegen::types::structure::StructFields<'ctx> for #ident<'ctx> { + fn new(ctx: impl ::nac3core::inkwell::context::AsContextRef<'ctx>, llvm_usize: ::nac3core::inkwell::types::IntType<'ctx>) -> Self { + let ctx = unsafe { ::nac3core::inkwell::context::ContextRef::new(ctx.as_ctx_ref()) }; + + let mut counter = ::nac3core::codegen::types::structure::FieldIndexCounter::default(); + + #ident { + #(#fields_create),* + #(#phantoms_create),* + } + } + + fn to_vec(&self) -> ::std::vec::Vec<(&'static str, ::nac3core::inkwell::types::BasicTypeEnum<'ctx>)> { + vec![ + #(#fields_into),* + ] + } + } + }; + + impl_block.into() +} diff --git a/nac3core/nac3core_derive/tests/structfields_empty.rs b/nac3core/nac3core_derive/tests/structfields_empty.rs new file mode 100644 index 00000000..0a3b19b4 --- /dev/null +++ b/nac3core/nac3core_derive/tests/structfields_empty.rs @@ -0,0 +1,9 @@ +use nac3core_derive::StructFields; +use std::marker::PhantomData; + +#[derive(PartialEq, Eq, Clone, Copy, StructFields)] +pub struct EmptyValue<'ctx> { + _phantom: PhantomData<&'ctx ()>, +} + +fn main() {} diff --git a/nac3core/nac3core_derive/tests/structfields_slice.rs b/nac3core/nac3core_derive/tests/structfields_slice.rs new file mode 100644 index 00000000..a1914592 --- /dev/null +++ b/nac3core/nac3core_derive/tests/structfields_slice.rs @@ -0,0 +1,18 @@ +use nac3core::{ + codegen::types::structure::StructField, + inkwell::{ + values::{IntValue, PointerValue}, + AddressSpace, + }, +}; +use nac3core_derive::StructFields; + +#[derive(PartialEq, Eq, Clone, Copy, StructFields)] +pub struct SliceValue<'ctx> { + #[value_type(i8_type().ptr_type(AddressSpace::default()))] + ptr: StructField<'ctx, PointerValue<'ctx>>, + #[value_type(usize)] + len: StructField<'ctx, IntValue<'ctx>>, +} + +fn main() {} diff --git a/nac3core/nac3core_derive/tests/structfields_test.rs b/nac3core/nac3core_derive/tests/structfields_test.rs new file mode 100644 index 00000000..e7a14728 --- /dev/null +++ b/nac3core/nac3core_derive/tests/structfields_test.rs @@ -0,0 +1,6 @@ +#[test] +fn test_parse_empty() { + let t = trybuild::TestCases::new(); + t.pass("tests/structfields_empty.rs"); + t.pass("tests/structfields_slice.rs"); +}