chore: comment

This commit is contained in:
2025-08-07 21:00:32 +08:00
parent f946cb2fd1
commit 67cdcfea33
24 changed files with 734 additions and 105 deletions

View File

@@ -1,11 +1,25 @@
//! 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, Item, ItemFn, ItemMod, Pat, PatType, Type, Visibility, parse_macro_input,
};
use syn::{FnArg, Item, ItemFn, ItemMod, Pat, 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;
@@ -29,9 +43,11 @@ pub fn builtins_impl(input: TokenStream) -> TokenStream {
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) => {
// Handle `const` definitions. These are exposed as constants in Nix.
let name_str = item_const
.ident
.to_string()
@@ -47,10 +63,12 @@ pub fn builtins_impl(input: TokenStream) -> TokenStream {
);
}
Item::Fn(item_fn) => {
// Handle function definitions. These become primops.
let (primop, wrapper) = match generate_primop_wrapper(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 }.into());
@@ -65,6 +83,7 @@ pub fn builtins_impl(input: TokenStream) -> TokenStream {
}
wrappers.push(wrapper);
}
// Other items are passed through unchanged.
item => pub_item_mod.push(item.to_token_stream()),
}
}
@@ -72,7 +91,10 @@ pub fn builtins_impl(input: TokenStream) -> TokenStream {
let consts_len = consts.len();
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)*
@@ -81,13 +103,18 @@ pub fn builtins_impl(input: TokenStream) -> TokenStream {
pub const SCOPED_LEN: usize = #scoped_len;
}
/// A struct containing all the built-in constants and functions.
pub struct Builtins<Ctx: BuiltinsContext> {
/// Constant values available in the global scope.
pub consts: [(&'static str, ::nixjit_value::Const); #mod_name::CONSTS_LEN],
/// Global functions available in the global scope.
pub global: [(&'static str, usize, fn(&mut Ctx, Vec<::nixjit_eval::Value>) -> ::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, Vec<::nixjit_eval::Value>) -> ::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 {
consts: [#(#consts,)*],
@@ -101,6 +128,7 @@ pub fn builtins_impl(input: TokenStream) -> TokenStream {
output.into()
}
/// Generates the primop metadata and the wrapper function for a single user-defined function.
fn generate_primop_wrapper(
item_fn: &ItemFn,
) -> syn::Result<(proc_macro2::TokenStream, proc_macro2::TokenStream)> {
@@ -114,9 +142,10 @@ fn generate_primop_wrapper(
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(_) = *first_arg.ty {
user_args.next();
user_args.next(); // Consume the context argument
true
} else {
false
@@ -128,14 +157,18 @@ fn generate_primop_wrapper(
));
};
// Collect the remaining arguments.
let arg_pats: Vec<_> = user_args.rev().collect();
let arg_count = arg_pats.len();
// Generate code to unpack and convert arguments from the `Vec<Value>`.
let arg_unpacks = arg_pats.iter().enumerate().map(|(i, arg)| {
let arg_name = match &arg {
FnArg::Typed(PatType { pat, .. }) => {
if let Pat::Ident(pat_ident) = &**pat {
pat_ident.ident.clone()
} else {
// Create a placeholder name if the pattern is not a simple ident.
format_ident!("arg{}", i, span = Span::call_site())
}
}
@@ -152,6 +185,7 @@ fn generate_primop_wrapper(
}
});
// Get the names of the arguments to pass to the user's function.
let arg_names: Vec<_> = arg_pats
.iter()
.enumerate()
@@ -168,11 +202,13 @@ fn generate_primop_wrapper(
.rev()
.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 {
@@ -184,6 +220,7 @@ fn generate_primop_wrapper(
_ => 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 {
@@ -192,9 +229,11 @@ fn generate_primop_wrapper(
let arity = arg_names.len();
let fn_type = quote! { fn(&mut Ctx, Vec<::nixjit_eval::Value>) -> ::nixjit_error::Result<::nixjit_eval::Value> };
let primop =
quote! { (#name_str, #arity, #mod_name::#wrapper_name as #fn_type) };
// 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: Vec<::nixjit_eval::Value>) -> ::nixjit_error::Result<::nixjit_eval::Value> {
if args.len() != #arg_count {

View File

@@ -1,3 +1,13 @@
//! 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 (e.g., `Hir`, `Lir`).
//! 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 (`Hir::BinOp`).
//! 5. A `To[IrName]` trait to provide a convenient `.to_hir()` or `.to_lir()` method on the variant structs.
use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use quote::{format_ident, quote};
@@ -8,14 +18,21 @@ use syn::{
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., `Hir`).
pub base_name: Ident,
/// The list of variants for the enum.
pub variants: Punctuated<VariantInput, Token![,]>,
}
@@ -24,6 +41,7 @@ impl Parse for VariantInput {
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()?;
@@ -34,10 +52,11 @@ impl Parse for VariantInput {
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))
}
}
@@ -45,6 +64,7 @@ impl Parse for VariantInput {
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)?;
@@ -56,6 +76,7 @@ impl Parse for MacroInput {
}
}
/// The implementation of the `ir!` macro.
pub fn ir_impl(input: TokenStream) -> TokenStream {
let parsed_input = syn::parse_macro_input!(input as MacroInput);
@@ -126,31 +147,38 @@ pub fn ir_impl(input: TokenStream) -> TokenStream {
}
}
// 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 ),*
@@ -158,12 +186,16 @@ pub fn ir_impl(input: TokenStream) -> TokenStream {
}
}
// `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 )*
};

View File

@@ -1,13 +1,22 @@
//! 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 `ToHir` or `ToLir` 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)