feat: init
This commit is contained in:
13
nix-js-macros/Cargo.toml
Normal file
13
nix-js-macros/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "nix-js-macros"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
convert_case = "0.8"
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
259
nix-js-macros/src/builtins.rs
Normal file
259
nix-js-macros/src/builtins.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
//! Implements the `#[builtins]` procedural macro attribute.
|
||||
//!
|
||||
//! This macro simplifies the process of defining built-in functions (primops)
|
||||
//! for the Nix interpreter. It inspects the functions inside a `mod` block
|
||||
//! and generates the necessary boilerplate to make them callable from Nix code.
|
||||
//!
|
||||
//! Specifically, it generates:
|
||||
//! 1. A `Builtins` struct containing arrays of constant values and function pointers.
|
||||
//! 2. A wrapper function for each user-defined function. This wrapper handles:
|
||||
//! - Arity (argument count) checking.
|
||||
//! - Type conversion from the generic `nixjit_eval::Value` into the
|
||||
//! specific types expected by the user's function.
|
||||
//! - Calling the user's function with the converted arguments.
|
||||
//! - Wrapping the return value back into a `Result<nixjit_eval::Value>`.
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::Span;
|
||||
use quote::{ToTokens, format_ident, quote};
|
||||
use syn::{
|
||||
FnArg, Ident, Item, ItemConst, ItemFn, ItemMod, Pat, PatIdent, PatType, Type, Visibility,
|
||||
parse_macro_input,
|
||||
};
|
||||
|
||||
/// The implementation of the `#[builtins]` macro.
|
||||
pub fn builtins_impl(input: TokenStream) -> TokenStream {
|
||||
let item_mod = parse_macro_input!(input as ItemMod);
|
||||
let mod_name = &item_mod.ident;
|
||||
let visibility = &item_mod.vis;
|
||||
|
||||
let (_brace, items) = match item_mod.content {
|
||||
Some(content) => content,
|
||||
None => {
|
||||
return syn::Error::new_spanned(
|
||||
item_mod,
|
||||
"`#[builtins]` macro can only be used on an inline module: `mod name { ... }`",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
}
|
||||
};
|
||||
|
||||
let mut pub_item_mod = Vec::new();
|
||||
let mut global = Vec::new();
|
||||
let mut scoped = Vec::new();
|
||||
let mut wrappers = Vec::new();
|
||||
|
||||
// Iterate over the items (functions, consts) in the user's module.
|
||||
for item in &items {
|
||||
match item {
|
||||
Item::Const(item_const) => {
|
||||
let (primop, wrapper) = match generate_const_wrapper(mod_name, item_const) {
|
||||
Ok(result) => result,
|
||||
Err(e) => return e.to_compile_error().into(),
|
||||
};
|
||||
// Public functions are added to the global scope, private ones to a scoped set.
|
||||
if matches!(item_const.vis, Visibility::Public(_)) {
|
||||
global.push(primop);
|
||||
} else {
|
||||
scoped.push(primop);
|
||||
}
|
||||
wrappers.push(wrapper);
|
||||
}
|
||||
Item::Fn(item_fn) => {
|
||||
// Handle function definitions. These become primops.
|
||||
let (primop, wrapper) = match generate_primop_wrapper(mod_name, item_fn) {
|
||||
Ok(result) => result,
|
||||
Err(e) => return e.to_compile_error().into(),
|
||||
};
|
||||
// Public functions are added to the global scope, private ones to a scoped set.
|
||||
if matches!(item_fn.vis, Visibility::Public(_)) {
|
||||
global.push(primop);
|
||||
pub_item_mod.push(quote! { #item_fn });
|
||||
} else {
|
||||
scoped.push(primop);
|
||||
pub_item_mod.push(quote! {
|
||||
pub #item_fn
|
||||
});
|
||||
}
|
||||
wrappers.push(wrapper);
|
||||
}
|
||||
// Other items are passed through unchanged.
|
||||
item => pub_item_mod.push(item.to_token_stream()),
|
||||
}
|
||||
}
|
||||
|
||||
let global_len = global.len();
|
||||
let scoped_len = scoped.len();
|
||||
|
||||
// Assemble the final generated code.
|
||||
let output = quote! {
|
||||
// Re-create the user's module, now with generated wrappers.
|
||||
#visibility mod #mod_name {
|
||||
#(#pub_item_mod)*
|
||||
#(#wrappers)*
|
||||
pub const GLOBAL_LEN: usize = #global_len;
|
||||
pub const SCOPED_LEN: usize = #scoped_len;
|
||||
}
|
||||
|
||||
/// A struct containing all the built-in constants and functions.
|
||||
pub struct Builtins<Ctx: BuiltinsContext> {
|
||||
/// Global functions available in the global scope.
|
||||
pub global: [(&'static str, usize, fn(&mut Ctx, ::nixjit_eval::Args) -> ::nixjit_error::Result<::nixjit_eval::Value>); #mod_name::GLOBAL_LEN],
|
||||
/// Scoped functions, typically available under the `builtins` attribute set.
|
||||
pub scoped: [(&'static str, usize, fn(&mut Ctx, ::nixjit_eval::Args) -> ::nixjit_error::Result<::nixjit_eval::Value>); #mod_name::SCOPED_LEN],
|
||||
}
|
||||
|
||||
impl<Ctx: BuiltinsContext> Builtins<Ctx> {
|
||||
/// Creates a new instance of the `Builtins` struct.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
global: [#(#global,)*],
|
||||
scoped: [#(#scoped,)*],
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
output.into()
|
||||
}
|
||||
|
||||
fn generate_const_wrapper(
|
||||
mod_name: &Ident,
|
||||
item_const: &ItemConst,
|
||||
) -> syn::Result<(proc_macro2::TokenStream, proc_macro2::TokenStream)> {
|
||||
let const_name = &item_const.ident;
|
||||
let const_val = &item_const.expr;
|
||||
let name_str = const_name
|
||||
.to_string()
|
||||
.from_case(Case::UpperSnake)
|
||||
.to_case(Case::Camel);
|
||||
let const_name = format_ident!("{name_str}");
|
||||
let wrapper_name = format_ident!("wrapper_{}", const_name);
|
||||
|
||||
let fn_type = quote! { fn(&mut Ctx, ::nixjit_eval::Args) -> ::nixjit_error::Result<::nixjit_eval::Value> };
|
||||
|
||||
// The primop metadata tuple: (name, arity, wrapper_function_pointer)
|
||||
let primop = quote! { (#name_str, 0, #mod_name::#wrapper_name as #fn_type) };
|
||||
|
||||
// The generated wrapper function.
|
||||
let wrapper = quote! {
|
||||
pub fn #wrapper_name<Ctx: BuiltinsContext>(ctx: &mut Ctx, mut args: ::nixjit_eval::Args) -> ::nixjit_error::Result<::nixjit_eval::Value> {
|
||||
Ok(#const_val.into())
|
||||
}
|
||||
};
|
||||
|
||||
Ok((primop, wrapper))
|
||||
}
|
||||
/// Generates the primop metadata and the wrapper function for a single user-defined function.
|
||||
fn generate_primop_wrapper(
|
||||
mod_name: &Ident,
|
||||
item_fn: &ItemFn,
|
||||
) -> syn::Result<(proc_macro2::TokenStream, proc_macro2::TokenStream)> {
|
||||
let fn_name = &item_fn.sig.ident;
|
||||
let name_str = fn_name
|
||||
.to_string()
|
||||
.from_case(Case::Snake)
|
||||
.to_case(Case::Camel);
|
||||
let wrapper_name = format_ident!("wrapper_{}", fn_name);
|
||||
|
||||
let mut user_args = item_fn.sig.inputs.iter().peekable();
|
||||
|
||||
// Check if the first argument is a context `&mut Ctx`.
|
||||
let has_ctx = if let Some(FnArg::Typed(first_arg)) = user_args.peek() {
|
||||
if let (Type::Reference(_), Pat::Ident(PatIdent { ident, .. })) =
|
||||
(&*first_arg.ty, &*first_arg.pat)
|
||||
{
|
||||
if ident == "ctx" {
|
||||
user_args.next();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
fn_name,
|
||||
"A builtin function must not have a receiver argument",
|
||||
));
|
||||
};
|
||||
|
||||
// Collect the remaining arguments.
|
||||
let arg_pats: Vec<_> = user_args.collect();
|
||||
let arg_count = arg_pats.len();
|
||||
|
||||
let arg_unpacks = arg_pats.iter().enumerate().map(|(i, arg)| {
|
||||
let arg_name = format_ident!("_arg{}", i, span = Span::call_site());
|
||||
let arg_ty = match &arg {
|
||||
FnArg::Typed(PatType { ty, .. }) => ty,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
quote! {
|
||||
let #arg_name: #arg_ty = args.next().ok_or_else(|| ::nixjit_error::Error::eval_error("Not enough arguments provided".to_string()))?
|
||||
.try_into().map_err(|e| ::nixjit_error::Error::eval_error(format!("Argument type conversion failed: {}", e)))?;
|
||||
}
|
||||
});
|
||||
|
||||
// Get the names of the arguments to pass to the user's function.
|
||||
let arg_names: Vec<_> = arg_pats
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, arg)| match &arg {
|
||||
FnArg::Typed(PatType { .. }) => {
|
||||
format_ident!("_arg{}", i, span = Span::call_site())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Construct the argument list for the final call.
|
||||
let mut call_args = quote! { #(#arg_names),* };
|
||||
if has_ctx {
|
||||
call_args = quote! { ctx, #(#arg_names),* };
|
||||
}
|
||||
|
||||
// Check if the user's function already returns a `Result`.
|
||||
let returns_result = match &item_fn.sig.output {
|
||||
syn::ReturnType::Type(_, ty) => {
|
||||
if let Type::Path(type_path) = &**ty {
|
||||
type_path.path.segments.iter().any(|s| s.ident == "Result")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// Wrap the call expression in `Ok(...)` if it doesn't return a `Result`.
|
||||
let call_expr = if returns_result {
|
||||
quote! { #fn_name(#call_args) }
|
||||
} else {
|
||||
quote! { Ok(#fn_name(#call_args).into()) }
|
||||
};
|
||||
|
||||
let arity = arg_names.len();
|
||||
let fn_type = quote! { fn(&mut Ctx, ::nixjit_eval::Args) -> ::nixjit_error::Result<::nixjit_eval::Value> };
|
||||
|
||||
// The primop metadata tuple: (name, arity, wrapper_function_pointer)
|
||||
let primop = quote! { (#name_str, #arity, #mod_name::#wrapper_name as #fn_type) };
|
||||
|
||||
// The generated wrapper function.
|
||||
let wrapper = quote! {
|
||||
pub fn #wrapper_name<Ctx: BuiltinsContext>(ctx: &mut Ctx, mut args: ::nixjit_eval::Args) -> ::nixjit_error::Result<::nixjit_eval::Value> {
|
||||
if args.len() != #arg_count {
|
||||
return Err(::nixjit_error::Error::eval_error(format!("Function '{}' expects {} arguments, but received {}", #name_str, #arg_count, args.len())));
|
||||
}
|
||||
|
||||
let mut args = args.into_iter();
|
||||
#(#arg_unpacks)*
|
||||
|
||||
#call_expr
|
||||
}
|
||||
};
|
||||
|
||||
Ok((primop, wrapper))
|
||||
}
|
||||
203
nix-js-macros/src/ir.rs
Normal file
203
nix-js-macros/src/ir.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
//! Implements the `ir!` procedural macro.
|
||||
//!
|
||||
//! This macro is designed to reduce the boilerplate associated with defining
|
||||
//! an Intermediate Representation (IR) that follows a specific pattern. It generates:
|
||||
//! 1. An enum representing the different kinds of IR nodes.
|
||||
//! 2. Structs for each of the variants that have fields.
|
||||
//! 3. `Ref` and `Mut` versions of the main enum for ergonomic pattern matching on references.
|
||||
//! 4. `From` implementations to easily convert from a struct variant (e.g., `BinOp`) to the main enum (`Ir::BinOp`).
|
||||
//! 5. A `To[IrName]` trait to provide a convenient `.to_ir()` method on the variant structs.
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{
|
||||
FieldsNamed, Ident, Token, Type, parenthesized,
|
||||
parse::{Parse, ParseStream, Result},
|
||||
punctuated::Punctuated,
|
||||
token,
|
||||
};
|
||||
|
||||
/// Represents one of the variants passed to the `ir!` macro.
|
||||
pub enum VariantInput {
|
||||
/// A unit-like variant, e.g., `Arg`.
|
||||
Unit(Ident),
|
||||
/// A tuple-like variant with one unnamed field, e.g., `ExprRef(ExprId)`.
|
||||
Tuple(Ident, Type),
|
||||
/// A struct-like variant with named fields, e.g., `BinOp { lhs: ExprId, rhs: ExprId, kind: BinOpKind }`.
|
||||
Struct(Ident, FieldsNamed),
|
||||
}
|
||||
|
||||
/// The top-level input for the `ir!` macro.
|
||||
pub struct MacroInput {
|
||||
/// The name of the main IR enum to be generated (e.g., `Ir`).
|
||||
pub base_name: Ident,
|
||||
/// The list of variants for the enum.
|
||||
pub variants: Punctuated<VariantInput, Token![,]>,
|
||||
}
|
||||
|
||||
impl Parse for VariantInput {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let name: Ident = input.parse()?;
|
||||
|
||||
if input.peek(token::Paren) {
|
||||
// Parse a tuple-like variant: `Variant(Type)`
|
||||
let content;
|
||||
parenthesized!(content in input);
|
||||
let ty: Type = content.parse()?;
|
||||
|
||||
if !content.is_empty() {
|
||||
return Err(content.error("Expected a single type inside parentheses"));
|
||||
}
|
||||
|
||||
Ok(VariantInput::Tuple(name, ty))
|
||||
} else if input.peek(token::Brace) {
|
||||
// Parse a struct-like variant: `Variant { field: Type, ... }`
|
||||
let fields: FieldsNamed = input.parse()?;
|
||||
Ok(VariantInput::Struct(name, fields))
|
||||
} else {
|
||||
// Parse a unit-like variant: `Variant`
|
||||
Ok(VariantInput::Unit(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for MacroInput {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
// The macro input is expected to be: `IrName, Variant1, Variant2, ...`
|
||||
let base_name = input.parse()?;
|
||||
input.parse::<Token![,]>()?;
|
||||
let variants = Punctuated::parse_terminated(input)?;
|
||||
|
||||
Ok(MacroInput {
|
||||
base_name,
|
||||
variants,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The implementation of the `ir!` macro.
|
||||
pub fn ir_impl(input: TokenStream) -> TokenStream {
|
||||
let parsed_input = syn::parse_macro_input!(input as MacroInput);
|
||||
|
||||
let base_name = &parsed_input.base_name;
|
||||
let ref_name = format_ident!("{}Ref", base_name);
|
||||
let mut_name = format_ident!("{}Mut", base_name);
|
||||
let to_trait_name = format_ident!("To{}", base_name);
|
||||
let to_trait_fn_name = format_ident!("to_{}", base_name.to_string().to_case(Case::Snake));
|
||||
|
||||
let mut enum_variants = Vec::new();
|
||||
let mut struct_defs = Vec::new();
|
||||
let mut ref_variants = Vec::new();
|
||||
let mut mut_variants = Vec::new();
|
||||
let mut as_ref_arms = Vec::new();
|
||||
let mut as_mut_arms = Vec::new();
|
||||
let mut from_impls = Vec::new();
|
||||
let mut to_trait_impls = Vec::new();
|
||||
|
||||
for variant in parsed_input.variants {
|
||||
match variant {
|
||||
VariantInput::Unit(name) => {
|
||||
let inner_type = name.clone();
|
||||
enum_variants.push(quote! { #name(#inner_type) });
|
||||
ref_variants.push(quote! { #name(&'a #inner_type) });
|
||||
mut_variants.push(quote! { #name(&'a mut #inner_type) });
|
||||
as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) });
|
||||
as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) });
|
||||
from_impls.push(quote! {
|
||||
impl From<#inner_type> for #base_name {
|
||||
fn from(val: #inner_type) -> Self { #base_name::#name(val) }
|
||||
}
|
||||
});
|
||||
to_trait_impls.push(quote! {
|
||||
impl #to_trait_name for #name {
|
||||
fn #to_trait_fn_name(self) -> #base_name { #base_name::from(self) }
|
||||
}
|
||||
});
|
||||
}
|
||||
VariantInput::Tuple(name, ty) => {
|
||||
enum_variants.push(quote! { #name(#ty) });
|
||||
ref_variants.push(quote! { #name(&'a #ty) });
|
||||
mut_variants.push(quote! { #name(&'a mut #ty) });
|
||||
as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) });
|
||||
as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) });
|
||||
}
|
||||
VariantInput::Struct(name, fields) => {
|
||||
let inner_type = name.clone();
|
||||
struct_defs.push(quote! {
|
||||
#[derive(Debug)]
|
||||
pub struct #name #fields
|
||||
});
|
||||
enum_variants.push(quote! { #name(#inner_type) });
|
||||
ref_variants.push(quote! { #name(&'a #inner_type) });
|
||||
mut_variants.push(quote! { #name(&'a mut #inner_type) });
|
||||
as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) });
|
||||
as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) });
|
||||
from_impls.push(quote! {
|
||||
impl From<#inner_type> for #base_name {
|
||||
fn from(val: #inner_type) -> Self { #base_name::#name(val) }
|
||||
}
|
||||
});
|
||||
to_trait_impls.push(quote! {
|
||||
impl #to_trait_name for #name {
|
||||
fn #to_trait_fn_name(self) -> #base_name { #base_name::from(self) }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble the final generated code.
|
||||
let expanded = quote! {
|
||||
/// The main IR enum, generated by the `ir!` macro.
|
||||
#[derive(Debug, IsVariant, Unwrap, TryUnwrap)]
|
||||
pub enum #base_name {
|
||||
#( #enum_variants ),*
|
||||
}
|
||||
|
||||
// The struct definitions for the enum variants.
|
||||
#( #struct_defs )*
|
||||
|
||||
/// An immutable reference version of the IR enum.
|
||||
#[derive(Debug, IsVariant, Unwrap, TryUnwrap)]
|
||||
pub enum #ref_name<'a> {
|
||||
#( #ref_variants ),*
|
||||
}
|
||||
|
||||
/// A mutable reference version of the IR enum.
|
||||
#[derive(Debug, IsVariant, Unwrap, TryUnwrap)]
|
||||
pub enum #mut_name<'a> {
|
||||
#( #mut_variants ),*
|
||||
}
|
||||
|
||||
impl #base_name {
|
||||
/// Converts a `&Ir` into a `IrRef`.
|
||||
pub fn as_ref(&self) -> #ref_name<'_> {
|
||||
match self {
|
||||
#( #as_ref_arms ),*
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a `&mut Ir` into a `IrMut`.
|
||||
pub fn as_mut(&mut self) -> #mut_name<'_> {
|
||||
match self {
|
||||
#( #as_mut_arms ),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `From` implementations for converting variant structs into the main enum.
|
||||
#( #from_impls )*
|
||||
|
||||
/// A trait for converting a variant struct into the main IR enum.
|
||||
pub trait #to_trait_name {
|
||||
/// Performs the conversion.
|
||||
fn #to_trait_fn_name(self) -> #base_name;
|
||||
}
|
||||
|
||||
// Implement the `ToIr` trait for each variant struct.
|
||||
#( #to_trait_impls )*
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
23
nix-js-macros/src/lib.rs
Normal file
23
nix-js-macros/src/lib.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
//! This crate provides procedural macros for the nixjit project.
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
mod builtins;
|
||||
mod ir;
|
||||
|
||||
/// A procedural macro to reduce boilerplate when defining an Intermediate Representation (IR).
|
||||
///
|
||||
/// It generates an enum for the IR, along with `Ref` and `Mut` variants,
|
||||
/// `From` implementations, and a `ToIr` trait.
|
||||
#[proc_macro]
|
||||
pub fn ir(input: TokenStream) -> TokenStream {
|
||||
ir::ir_impl(input)
|
||||
}
|
||||
|
||||
/// A procedural macro attribute to simplify the definition of built-in functions.
|
||||
///
|
||||
/// It generates the necessary boilerplate to wrap functions and expose them
|
||||
/// to the evaluation engine, handling argument type conversions and arity checking.
|
||||
#[proc_macro_attribute]
|
||||
pub fn builtins(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
builtins::builtins_impl(input)
|
||||
}
|
||||
Reference in New Issue
Block a user