diff --git a/evaluator/nixjit/src/lib.rs b/evaluator/nixjit/src/lib.rs index f16cea5..df87da3 100644 --- a/evaluator/nixjit/src/lib.rs +++ b/evaluator/nixjit/src/lib.rs @@ -3,12 +3,6 @@ //! This crate orchestrates the entire process of parsing, analyzing, //! and evaluating Nix expressions. It integrates all the other `nixjit_*` //! components to provide a complete Nix evaluation environment. -//! -//! The primary workflow is demonstrated in the tests within `test.rs`: -//! 1. Parse Nix source code into an `rnix` AST. -//! 2. "Downgrade" the AST into the High-Level IR (HIR). -//! 3. "Resolve" the HIR into the Low-Level IR (LIR), handling variable lookups. -//! 4. "Evaluate" the LIR to produce a final value. #[cfg(test)] mod test; diff --git a/evaluator/nixjit/src/test.rs b/evaluator/nixjit/src/test.rs index 5426ab0..2aa192b 100644 --- a/evaluator/nixjit/src/test.rs +++ b/evaluator/nixjit/src/test.rs @@ -8,10 +8,7 @@ use nixjit_value::{AttrSet, Const, List, Symbol, Value}; #[inline] fn test_expr(expr: &str, expected: Value) { println!("{expr}"); - assert_eq!( - Context::new().eval(expr).unwrap(), - expected - ); + assert_eq!(Context::new().eval(expr).unwrap(), expected); } macro_rules! map { diff --git a/evaluator/nixjit_context/src/lib.rs b/evaluator/nixjit_context/src/lib.rs index a8b1c88..279df84 100644 --- a/evaluator/nixjit_context/src/lib.rs +++ b/evaluator/nixjit_context/src/lib.rs @@ -1,3 +1,13 @@ +//! The central evaluation context for the nixjit interpreter. +//! +//! This module defines the `Context` struct, which holds all the state +//! necessary for the evaluation of a Nix expression. It manages the +//! Intermediate Representations (IRs), scopes, evaluation stack, and +//! the Just-In-Time (JIT) compiler. +//! +//! The `Context` implements various traits (`DowngradeContext`, `ResolveContext`, etc.) +//! to provide the necessary services for each stage of the compilation and +//! evaluation pipeline. use std::cell::{OnceCell, RefCell}; use std::rc::Rc; @@ -19,15 +29,22 @@ use nixjit_lir::{Lir, LookupResult, Resolve, ResolveContext}; use nixjit_jit::{JITCompiler, JITContext, JITFunc}; use replace_with::replace_with_and_return; +/// Represents a lexical scope during name resolution. enum Scope { + /// A `with` expression scope. With, + /// A `let` binding scope, mapping variable names to their expression IDs. Let(HashMap), + /// A function argument scope. `Some` holds the name of the argument set if present. Arg(Option), } +/// Represents an expression at different stages of compilation. #[derive(Debug, Unwrap)] enum Ir { + /// An expression in the High-Level Intermediate Representation (HIR). Hir(Hir), + /// An expression in the Low-Level Intermediate Representation (LIR). Lir(Lir), } @@ -81,20 +98,37 @@ impl Ir { } } +/// The main evaluation context. +/// +/// This struct orchestrates the entire Nix expression evaluation process, +/// from parsing and semantic analysis to interpretation and JIT compilation. pub struct Context { + /// Arena for all expressions, which can be either HIR or LIR. + /// `RefCell` is used for interior mutability to allow on-demand resolution. irs: Vec>, + /// Tracks whether an `ExprId` has been resolved from HIR to LIR. resolved: Vec, + /// The stack of lexical scopes used for name resolution. scopes: Vec, + /// The number of arguments in the current function call scope. args_count: usize, + /// A table of primitive operation implementations. primops: Vec) -> Result>, + /// Maps a function's body `ExprId` to its parameter definition. funcs: HashMap, + /// A dependency graph between expressions. graph: DiGraph, + /// Maps an `ExprId` to its corresponding `NodeIndex` in the dependency graph. nodes: Vec, + /// The call stack for function evaluation, where each frame holds arguments. stack: Vec>, + /// A stack of namespaces for `with` expressions during evaluation. with_scopes: Vec>>, + /// The Just-In-Time (JIT) compiler. jit: JITCompiler, + /// A cache for JIT-compiled functions, indexed by `ExprId`. compiled: Vec>>, } @@ -111,9 +145,7 @@ impl Default for Context { .enumerate() .map(|(id, (k, _))| (k.to_string(), unsafe { ExprId::from(id) })) .chain(global.iter().enumerate().map(|(idx, (k, _, _))| { - (k.to_string(), unsafe { - ExprId::from(idx + CONSTS_LEN) - }) + (k.to_string(), unsafe { ExprId::from(idx + CONSTS_LEN) }) })) .chain(core::iter::once(("builtins".to_string(), unsafe { ExprId::from(CONSTS_LEN + GLOBAL_LEN + SCOPED_LEN) @@ -162,10 +194,18 @@ impl Default for Context { } impl Context { + /// Creates a new, default `Context`. pub fn new() -> Self { Self::default() } + /// The main entry point for evaluating a Nix expression string. + /// + /// This function performs the following steps: + /// 1. Parses the expression string into an `rnix` AST. + /// 2. Downgrades the AST to the High-Level IR (HIR). + /// 3. Resolves the HIR to the Low-Level IR (LIR). + /// 4. Evaluates the LIR to produce a final `Value`. pub fn eval(mut self, expr: &str) -> Result { let root = rnix::Root::parse(expr); if !root.errors().is_empty() { @@ -364,9 +404,7 @@ impl EvalContext for Context { } fn call_primop(&mut self, id: nixjit_ir::PrimOpId, args: Vec) -> Result { - unsafe { - (self.primops.get_unchecked(id.raw()))(self, args) - } + unsafe { (self.primops.get_unchecked(id.raw()))(self, args) } } } diff --git a/evaluator/nixjit_error/src/lib.rs b/evaluator/nixjit_error/src/lib.rs index 6b0c551..c24e18a 100644 --- a/evaluator/nixjit_error/src/lib.rs +++ b/evaluator/nixjit_error/src/lib.rs @@ -1,17 +1,43 @@ +//! This crate defines the centralized error types and `Result` alias used +//! throughout the entire `nixjit` evaluation pipeline. By consolidating error +//! handling here, we ensure a consistent approach to reporting failures across +//! different stages of processing, from parsing to final evaluation. + use thiserror::Error; +/// A specialized `Result` type used for all fallible operations within the +/// `nixjit` crates. It defaults to the crate's `Error` type. pub type Result = core::result::Result; +/// The primary error enum, encompassing all potential failures that can occur +/// during the lifecycle of a Nix expression's evaluation. #[derive(Error, Debug)] pub enum Error { + /// An error occurred during the initial parsing phase. This typically + /// indicates a syntax error in the Nix source code, as detected by the + /// `rnix` parser. #[error("error occurred during parse stage: {0}")] ParseError(String), + + /// An error occurred while "downgrading" the `rnix` AST to the + /// High-Level IR (HIR). This can happen if the AST has a structure that is + /// syntactically valid but semantically incorrect for our IR. #[error("error occurred during downgrade stage: {0}")] DowngradeError(String), + + /// An error occurred during the variable resolution phase, where the HIR is + /// converted to the Low-Level IR (LIR). This is most commonly caused by + /// an unbound or undefined variable. #[error("error occurred during variable resolve stage: {0}")] ResolutionError(String), + + /// An error occurred during the final evaluation of the LIR. This covers + /// all runtime errors, such as type mismatches (e.g., adding a string to + /// an integer), division by zero, or failed `assert` statements. #[error("error occurred during evaluation stage: {0}")] EvalError(String), - #[error("unknown error")] + + /// A catch-all for any error that does not fit into the other categories. + #[error("an unknown or unexpected error occurred")] Unknown, } diff --git a/evaluator/nixjit_eval/src/lib.rs b/evaluator/nixjit_eval/src/lib.rs index 354dbab..7186f75 100644 --- a/evaluator/nixjit_eval/src/lib.rs +++ b/evaluator/nixjit_eval/src/lib.rs @@ -1,3 +1,12 @@ +//! This module defines the core traits and logic for evaluating the LIR. +//! +//! The central components are: +//! - `EvalContext`: A trait that defines the environment and operations needed for evaluation. +//! It manages the evaluation stack, scopes, and primop calls. +//! - `Evaluate`: A trait implemented by LIR nodes to define how they are evaluated. +//! - `Value`: An enum representing all possible values during evaluation. This is an +//! internal representation, distinct from the public-facing `nixjit_value::Value`. + use std::rc::Rc; use hashbrown::HashMap; @@ -5,37 +14,59 @@ use hashbrown::HashMap; use nixjit_error::{Error, Result}; use nixjit_ir::{self as ir, ArgIdx, ExprId, PrimOpId}; use nixjit_lir as lir; -use nixjit_value::{Const, Symbol}; +use nixjit_value::{Const, Symbol, format_symbol}; pub use crate::value::*; mod value; +/// A trait defining the context in which LIR expressions are evaluated. pub trait EvalContext: Sized { + /// Evaluates an expression by its ID. fn eval(&mut self, expr: &ExprId) -> Result; + + /// Enters a `with` scope for the duration of a closure's execution. fn with_with_env( &mut self, namespace: Rc>, f: impl FnOnce(&mut Self) -> T, ) -> T; - fn with_args_env(&mut self, args: Vec, f: impl FnOnce(&mut Self) -> T) -> (Vec, T); + + /// Pushes a new set of arguments onto the stack for a function call. + fn with_args_env( + &mut self, + args: Vec, + f: impl FnOnce(&mut Self) -> T, + ) -> (Vec, T); + + /// Looks up an identifier in the current `with` scope chain. fn lookup_with<'a>(&'a self, ident: &str) -> Option<&'a Value>; + + /// Looks up a function argument by its index on the current stack frame. fn lookup_arg<'a>(&'a self, idx: ArgIdx) -> &'a Value; + + /// Pops the current stack frame, returning the arguments. fn pop_frame(&mut self) -> Vec; + + /// Calls a primitive operation (builtin) by its ID. fn call_primop(&mut self, id: PrimOpId, args: Vec) -> Result; } +/// A trait for types that can be evaluated within an `EvalContext`. pub trait Evaluate { + /// Performs the evaluation. fn eval(&self, ctx: &mut Ctx) -> Result; } impl Evaluate for ExprId { + /// Evaluating an `ExprId` simply delegates to the context. fn eval(&self, ctx: &mut Ctx) -> Result { ctx.eval(self) } } impl Evaluate for lir::Lir { + /// Evaluates an LIR node by dispatching to the specific implementation for its variant. fn eval(&self, ctx: &mut Ctx) -> Result { use lir::Lir::*; match self { @@ -63,6 +94,7 @@ impl Evaluate for lir::Lir { } impl Evaluate for ir::AttrSet { + /// Evaluates an `AttrSet` by evaluating all its static and dynamic attributes. fn eval(&self, ctx: &mut Ctx) -> Result { let mut attrs = AttrSet::new( self.stcs @@ -79,24 +111,26 @@ impl Evaluate for ir::AttrSet { let v_eval_result = v.eval(ctx)?; attrs.push_attr(k.unwrap_string(), v_eval_result); } - let result = Value::AttrSet(attrs.into()).ok(); - Ok(result.unwrap()) + let result = Value::AttrSet(attrs.into()); + Ok(result) } } impl Evaluate for ir::List { + /// Evaluates a `List` by evaluating all its items. fn eval(&self, ctx: &mut Ctx) -> Result { let items = self .items .iter() .map(|val| val.eval(ctx)) .collect::>>()?; - let result = Value::List(List::from(items).into()).ok(); - Ok(result.unwrap()) + let result = Value::List(List::from(items).into()); + Ok(result) } } impl Evaluate for ir::HasAttr { + /// Evaluates a `HasAttr` by evaluating the LHS and the attribute path, then performing the check. fn eval(&self, ctx: &mut Ctx) -> Result { use ir::Attr::*; let mut val = self.lhs.eval(ctx)?; @@ -110,12 +144,12 @@ impl Evaluate for ir::HasAttr { } }) }))?; - let result = val.ok(); - Ok(result.unwrap()) + Ok(val) } } impl Evaluate for ir::BinOp { + /// Evaluates a `BinOp` by evaluating the LHS and RHS, then performing the operation. fn eval(&self, ctx: &mut Ctx) -> Result { use ir::BinOpKind::*; let mut lhs = self.lhs.eval(ctx)?; @@ -166,6 +200,7 @@ impl Evaluate for ir::BinOp { } impl Evaluate for ir::UnOp { + /// Evaluates a `UnOp` by evaluating the RHS and performing the operation. fn eval(&self, ctx: &mut Ctx) -> Result { use ir::UnOpKind::*; let mut rhs = self.rhs.eval(ctx)?; @@ -182,6 +217,8 @@ impl Evaluate for ir::UnOp { } impl Evaluate for ir::Select { + /// Evaluates a `Select` by evaluating the expression, the path, and the default value (if any), + /// then performing the selection. fn eval(&self, ctx: &mut Ctx) -> Result { use ir::Attr::*; let mut val = self.expr.eval(ctx)?; @@ -212,18 +249,20 @@ impl Evaluate for ir::Select { }) }))?; } - let result = val.ok(); - Ok(result.unwrap()) + Ok(val) } } impl Evaluate for ir::If { + /// Evaluates an `If` by evaluating the condition and then either the consequence or the alternative. fn eval(&self, ctx: &mut Ctx) -> Result { - // TODO: Error Handling let cond = self.cond.eval(ctx)?; - let cond = cond - .try_unwrap_bool() - .map_err(|_| Error::EvalError(format!("expected a boolean but found ...")))?; + let cond = cond.as_ref().try_unwrap_bool().map_err(|_| { + Error::EvalError(format!( + "if-condition must be a boolean, but got {}", + cond.typename() + )) + })?; if cond { self.consq.eval(ctx) @@ -234,27 +273,27 @@ impl Evaluate for ir::If { } impl Evaluate for ir::Call { + /// Evaluates a `Call` by evaluating the function and its arguments, then performing the call. fn eval(&self, ctx: &mut Ctx) -> Result { let mut func = self.func.eval(ctx)?; - // FIXME: ? let ctx_mut = unsafe { &mut *(ctx as *mut Ctx) }; - func.call( - self.args - .iter() - .map(|arg| arg.eval(ctx)), - ctx_mut, - )?; - Ok(func.ok().unwrap()) + func.call(self.args.iter().map(|arg| arg.eval(ctx)), ctx_mut)?; + Ok(func) } } impl Evaluate for ir::With { + /// Evaluates a `With` by evaluating the namespace, entering a `with` scope, + /// and then evaluating the body. fn eval(&self, ctx: &mut Ctx) -> Result { let namespace = self.namespace.eval(ctx)?; + let typename = namespace.typename(); ctx.with_with_env( namespace .try_unwrap_attr_set() - .map_err(|_| Error::EvalError(format!("expected a set but found ...")))? + .map_err(|_| { + Error::EvalError(format!("'with' expects a set, but got {}", typename)) + })? .into_inner(), |ctx| self.expr.eval(ctx), ) @@ -262,20 +301,27 @@ impl Evaluate for ir::With { } impl Evaluate for ir::Assert { + /// Evaluates an `Assert` by evaluating the condition. If true, it evaluates and + /// returns the body; otherwise, it returns an error. fn eval(&self, ctx: &mut Ctx) -> Result { let cond = self.assertion.eval(ctx)?; - let cond = cond - .try_unwrap_bool() - .map_err(|_| Error::EvalError(format!("expected a boolean but found ...")))?; + let cond = cond.as_ref().try_unwrap_bool().map_err(|_| { + Error::EvalError(format!( + "assertion condition must be a boolean, but got {}", + cond.typename() + )) + })?; if cond { self.expr.eval(ctx) } else { - Err(Error::EvalError("assertion failed".to_string())) + Ok(Value::Catchable("assertion failed".to_string())) } } } impl Evaluate for ir::ConcatStrings { + /// Evaluates a `ConcatStrings` by evaluating each part, coercing it to a string, + /// and then concatenating the results. fn eval(&self, ctx: &mut Ctx) -> Result { let mut parts = self .parts @@ -283,7 +329,7 @@ impl Evaluate for ir::ConcatStrings { .map(|part| { let mut part = part.eval(ctx)?; part.coerce_to_string(); - part.ok() + Ok(part) }) .collect::>>()? .into_iter(); @@ -292,44 +338,44 @@ impl Evaluate for ir::ConcatStrings { a.concat_string(b); a }); - Ok(result.ok().unwrap()) + Ok(result) } } impl Evaluate for ir::Str { + /// Evaluates a `Str` literal into a `Value::String`. fn eval(&self, _: &mut Ctx) -> Result { - let result = Value::String(self.val.clone()).ok(); - Ok(result.unwrap()) + Ok(Value::String(self.val.clone())) } } impl Evaluate for ir::Const { + /// Evaluates a `Const` literal into its corresponding `Value` variant. fn eval(&self, _: &mut Ctx) -> Result { let result = match self.val { Const::Null => Value::Null, Const::Int(x) => Value::Int(x), Const::Float(x) => Value::Float(x), Const::Bool(x) => Value::Bool(x), - } - .ok(); - Ok(result.unwrap()) + }; + Ok(result) } } impl Evaluate for ir::Var { + /// Evaluates a `Var` by looking it up in the `with` scope chain. + /// This is for variables that could not be resolved statically. fn eval(&self, ctx: &mut Ctx) -> Result { ctx.lookup_with(&self.sym) .ok_or_else(|| { - Error::EvalError(format!( - "variable {} not found", - Symbol::from(self.sym.clone()) - )) + Error::EvalError(format!("undefined variable '{}'", format_symbol(&self.sym))) }) .map(|val| val.clone()) } } impl Evaluate for ir::Path { + /// Evaluates a `Path`. (Currently a TODO). fn eval(&self, ctx: &mut Ctx) -> Result { todo!() } diff --git a/evaluator/nixjit_eval/src/value/attrset.rs b/evaluator/nixjit_eval/src/value/attrset.rs index e0fd691..1c7efdb 100644 --- a/evaluator/nixjit_eval/src/value/attrset.rs +++ b/evaluator/nixjit_eval/src/value/attrset.rs @@ -1,3 +1,5 @@ +//! Defines the runtime representation of an attribute set (a map). + use core::ops::Deref; use std::fmt::Debug; use std::rc::Rc; @@ -7,12 +9,16 @@ use hashbrown::{HashMap, HashSet}; use itertools::Itertools; use nixjit_error::{Error, Result}; -use nixjit_value as p; use nixjit_value::Symbol; +use nixjit_value::{self as p, format_symbol}; use super::Value; use crate::EvalContext; +/// A wrapper around a `HashMap` representing a Nix attribute set. +/// +/// It uses `#[repr(transparent)]` to ensure it has the same memory layout +/// as `HashMap`. #[repr(transparent)] #[derive(Constructor, PartialEq)] pub struct AttrSet { @@ -56,16 +62,24 @@ impl Deref for AttrSet { } impl AttrSet { + /// Creates a new `AttrSet` with a specified initial capacity. pub fn with_capacity(cap: usize) -> Self { AttrSet { data: HashMap::with_capacity(cap), } } + /// Inserts an attribute, overwriting any existing attribute with the same name. pub fn push_attr_force(&mut self, sym: String, val: Value) { self.data.insert(sym, val); } + /// Inserts an attribute. + /// + /// # Panics + /// + /// This method currently uses `todo!()` and will panic if the attribute + /// already exists, indicating that duplicate attribute handling is not yet implemented. pub fn push_attr(&mut self, sym: String, val: Value) { if self.data.get(&sym).is_some() { todo!() @@ -73,6 +87,10 @@ impl AttrSet { self.data.insert(sym, val); } + /// Performs a deep selection of an attribute from a nested set. + /// + /// It traverses the attribute path and returns the final value, or an error + /// if any intermediate attribute does not exist or is not a set. pub fn select( &self, mut path: impl DoubleEndedIterator>, @@ -84,7 +102,7 @@ impl AttrSet { let Some(Value::AttrSet(attrs)) = data.get(&item) else { return Err(Error::EvalError(format!( "attribute '{}' not found", - Symbol::from(item) + format_symbol(item) ))); }; data = attrs.as_inner(); @@ -95,6 +113,7 @@ impl AttrSet { }) } + /// Checks if an attribute path exists within the set. pub fn has_attr( &self, mut path: impl DoubleEndedIterator>, @@ -110,24 +129,38 @@ impl AttrSet { Ok(data.get(&last?).is_some()) } + /// Merges another `AttrSet` into this one, with attributes from `other` + /// overwriting existing ones. This corresponds to the `//` operator in Nix. pub fn update(&mut self, other: &Self) { for (k, v) in other.data.iter() { self.push_attr_force(k.clone(), v.clone()) } } + /// Returns a reference to the inner `HashMap`. pub fn as_inner(&self) -> &HashMap { &self.data } + /// Converts an `Rc` to an `Rc>` without allocation. + /// + /// # Safety + /// + /// This is safe because `AttrSet` is `#[repr(transparent)]`. pub fn into_inner(self: Rc) -> Rc> { unsafe { core::mem::transmute(self) } } + /// Creates an `AttrSet` from a `HashMap`. pub fn from_inner(data: HashMap) -> Self { Self { data } } + /// Performs a deep equality comparison between two `AttrSet`s. + /// + /// It recursively compares the contents of both sets, ensuring that both keys + /// and values are identical. The attributes are sorted before comparison to + /// ensure a consistent result. pub fn eq_impl(&self, other: &Self) -> bool { self.data.iter().len() == other.data.iter().len() && std::iter::zip( @@ -137,6 +170,7 @@ impl AttrSet { .all(|((k1, v1), (k2, v2))| k1 == k2 && v1.eq_impl(v2)) } + /// Converts the `AttrSet` to its public-facing representation. pub fn to_public(&self, seen: &mut HashSet) -> p::Value { p::Value::AttrSet(p::AttrSet::new( self.data diff --git a/evaluator/nixjit_eval/src/value/func.rs b/evaluator/nixjit_eval/src/value/func.rs index ffc9966..1d3fbd5 100644 --- a/evaluator/nixjit_eval/src/value/func.rs +++ b/evaluator/nixjit_eval/src/value/func.rs @@ -1,3 +1,4 @@ +//! Defines the runtime representation of a partially applied function. use std::rc::Rc; use derive_more::Constructor; @@ -8,10 +9,17 @@ use nixjit_ir::ExprId; use super::Value; use crate::EvalContext; +/// Represents a partially applied user-defined function. +/// +/// This struct captures the state of a function that has received some, but not +/// all, of its expected arguments. #[derive(Debug, Constructor)] pub struct FuncApp { + /// The expression ID of the function body to be executed. pub body: ExprId, + /// The arguments that have already been applied to the function. pub args: Vec, + /// The lexical scope (stack frame) captured at the time of the initial call. pub frame: Vec, } @@ -26,6 +34,10 @@ impl Clone for FuncApp { } impl FuncApp { + /// Applies more arguments to a partially applied function. + /// + /// It takes an iterator of new arguments, appends them to the existing ones, + /// and re-evaluates the function body within its captured environment. pub fn call( self: &mut Rc, mut iter: impl Iterator> + ExactSizeIterator, diff --git a/evaluator/nixjit_eval/src/value/list.rs b/evaluator/nixjit_eval/src/value/list.rs index 7dee374..3cbd186 100644 --- a/evaluator/nixjit_eval/src/value/list.rs +++ b/evaluator/nixjit_eval/src/value/list.rs @@ -1,3 +1,5 @@ +//! Defines the runtime representation of a list. + use std::fmt::Debug; use std::ops::Deref; @@ -9,6 +11,7 @@ use nixjit_value::Value as PubValue; use super::Value; use crate::EvalContext; +/// A wrapper around a `Vec` representing a Nix list. #[derive(Default)] pub struct List { data: Vec, @@ -46,35 +49,43 @@ impl Deref for List { } impl List { + /// Creates a new, empty `List`. pub fn new() -> Self { List { data: Vec::new() } } + /// Creates a new `List` with a specified initial capacity. pub fn with_capacity(cap: usize) -> Self { List { data: Vec::with_capacity(cap), } } + /// Appends an element to the back of the list. pub fn push(&mut self, elem: Value) { self.data.push(elem); } + /// Appends all elements from another `List` to this one. + /// This corresponds to the `++` operator in Nix. pub fn concat(&mut self, other: &Self) { for elem in other.data.iter() { self.data.push(elem.clone()); } } + /// Consumes the `List` and returns the inner `Vec`. pub fn into_inner(self) -> Vec { self.data } + /// Performs a deep equality comparison between two `List`s. pub fn eq_impl(&self, other: &Self) -> bool { self.len() == other.len() && core::iter::zip(self.iter(), other.iter()).all(|(a, b)| a.eq_impl(b)) } + /// Converts the `List` to its public-facing representation. pub fn to_public(&self, seen: &mut HashSet) -> PubValue { PubValue::List(PubList::new( self.data diff --git a/evaluator/nixjit_eval/src/value/mod.rs b/evaluator/nixjit_eval/src/value/mod.rs index 8cf3997..6cde4a7 100644 --- a/evaluator/nixjit_eval/src/value/mod.rs +++ b/evaluator/nixjit_eval/src/value/mod.rs @@ -1,3 +1,14 @@ +//! Defines the internal representation of values during evaluation. +//! +//! This module introduces the `Value` enum, which is the cornerstone of the +//! interpreter's runtime. It represents all possible data types that can exist +//! during the evaluation of a Nix expression. This is an internal, mutable +//! representation, distinct from the public-facing `nixjit_value::Value`. +//! +//! The module also provides `ValueAsRef` for non-owning references and +//! implementations for various operations like arithmetic, comparison, and +//! function calls. + use std::fmt::Debug; use std::hash::Hash; use std::process::abort; @@ -26,6 +37,11 @@ pub use func::*; pub use list::List; pub use primop::*; +/// The internal, C-compatible representation of a Nix value during evaluation. +/// +/// This enum is designed for efficient manipulation within the interpreter and +/// JIT-compiled code. It uses `#[repr(C, u64)]` to ensure a predictable layout, +/// with the discriminant serving as a type tag. #[repr(C, u64)] #[derive(IsVariant, TryUnwrap, Unwrap)] pub enum Value { @@ -143,6 +159,9 @@ impl PartialEq for Value { impl Eq for Value {} +/// A non-owning reference to a `Value`. +/// +/// This is used to avoid unnecessary cloning when inspecting values. #[derive(IsVariant, TryUnwrap, Unwrap, Clone)] pub enum ValueAsRef<'v> { Int(i64), @@ -161,6 +180,7 @@ pub enum ValueAsRef<'v> { } impl Value { + /// Returns a `ValueAsRef`, providing a non-owning view of the value. pub fn as_ref(&self) -> ValueAsRef<'_> { use Value::*; use ValueAsRef as R; @@ -182,10 +202,12 @@ impl Value { } } impl Value { + /// Wraps the `Value` in a `Result::Ok`. pub fn ok(self) -> Result { Ok(self) } + /// Returns the name of the value's type. pub fn typename(&self) -> &'static str { use Value::*; match self { @@ -205,6 +227,7 @@ impl Value { } } + /// Checks if the value is a callable entity (a function or primop). pub fn callable(&self) -> bool { match self { Value::PrimOp(_) | Value::PrimOpApp(_) | Value::Func(_) => true, @@ -213,7 +236,16 @@ impl Value { } } - pub fn call(&mut self, mut iter: impl Iterator> + ExactSizeIterator, ctx: &mut Ctx) -> Result<()> { + /// Performs a function call on the `Value`. + /// + /// This method handles calling functions, primops, and their partially + /// applied variants. It manages argument application and delegates to the + /// `EvalContext` for the actual execution. + pub fn call( + &mut self, + mut iter: impl Iterator> + ExactSizeIterator, + ctx: &mut Ctx, + ) -> Result<()> { use Value::*; *self = match self { &mut PrimOp(func) => { @@ -254,7 +286,8 @@ impl Value { } else if let Value::FuncApp(func) = val { let mut func = Rc::unwrap_or_clone(func); func.args.push(args.pop().unwrap()); - let (ret_args, ret) = ctx.with_args_env(func.args, |ctx| ctx.eval(&func.body)); + let (ret_args, ret) = + ctx.with_args_env(func.args, |ctx| ctx.eval(&func.body)); args = ret_args; val = ret?; } @@ -264,7 +297,9 @@ impl Value { PrimOpApp(func) => func.call(iter.collect::>()?, ctx), FuncApp(func) => func.call(iter, ctx), Catchable(_) => return Ok(()), - _ => Err(Error::EvalError("attempt to call something which is not a function but ...".to_string())) + _ => Err(Error::EvalError( + "attempt to call something which is not a function but ...".to_string(), + )), }?; Ok(()) } @@ -520,6 +555,12 @@ impl Value { self } + /// Converts the internal `Value` to its public-facing, serializable + /// representation from the `nixjit_value` crate. + /// + /// The `seen` set is used to detect and handle cycles in data structures + /// like attribute sets and lists, replacing subsequent encounters with + /// `PubValue::Repeated`. pub fn to_public(&self, seen: &mut HashSet) -> PubValue { use Value::*; if seen.contains(self) { diff --git a/evaluator/nixjit_eval/src/value/primop.rs b/evaluator/nixjit_eval/src/value/primop.rs index 06faee3..26437ad 100644 --- a/evaluator/nixjit_eval/src/value/primop.rs +++ b/evaluator/nixjit_eval/src/value/primop.rs @@ -1,3 +1,5 @@ +//! Defines the runtime representation of a partially applied primitive operation. + use std::rc::Rc; use derive_more::Constructor; @@ -8,15 +10,28 @@ use nixjit_ir::PrimOpId; use super::Value; use crate::EvalContext; +/// Represents a partially applied primitive operation (builtin function). +/// +/// This struct holds the state of a primop that has received some, but not +/// all, of its required arguments. #[derive(Debug, Clone, Constructor)] pub struct PrimOpApp { + /// The name of the primitive operation. pub name: &'static str, + /// The number of remaining arguments the primop expects. arity: usize, + /// The unique ID of the primop. id: PrimOpId, + /// The arguments that have already been applied. args: Vec, } impl PrimOpApp { + /// Applies more arguments to a partially applied primop. + /// + /// If enough arguments are provided to satisfy the primop's arity, it is + /// executed. Otherwise, it returns a new `PrimOpApp` with the combined + /// arguments. pub fn call( self: &mut Rc, args: Vec, diff --git a/evaluator/nixjit_eval/src/value/string.rs b/evaluator/nixjit_eval/src/value/string.rs index 34c8c12..d2dfab0 100644 --- a/evaluator/nixjit_eval/src/value/string.rs +++ b/evaluator/nixjit_eval/src/value/string.rs @@ -1,10 +1,19 @@ +//! Defines a placeholder for Nix's contextful strings. +//! +//! In Nix, strings can carry a "context" which affects how they are +//! handled, particularly with regards to path resolution. This module +//! provides the basic structures for this feature, although it is +//! currently a work in progress. + // TODO: Contextful String +/// Represents the context associated with a string. pub struct StringContext { context: Vec<()>, } impl StringContext { + /// Creates a new, empty `StringContext`. pub fn new() -> StringContext { StringContext { context: Vec::new(), @@ -12,12 +21,14 @@ impl StringContext { } } +/// A string that carries an associated context. pub struct ContextfulString { string: String, context: StringContext, } impl ContextfulString { + /// Creates a new `ContextfulString` from a standard `String`. pub fn new(string: String) -> ContextfulString { ContextfulString { string, diff --git a/evaluator/nixjit_hir/src/downgrade.rs b/evaluator/nixjit_hir/src/downgrade.rs index a2fcd19..797761d 100644 --- a/evaluator/nixjit_hir/src/downgrade.rs +++ b/evaluator/nixjit_hir/src/downgrade.rs @@ -58,6 +58,7 @@ impl Downgrade for Expr { } } +/// Downgrades an `assert` expression. impl Downgrade for ast::Assert { fn downgrade(self, ctx: &mut Ctx) -> Result { let assertion = self.condition().unwrap().downgrade(ctx)?; @@ -66,6 +67,7 @@ impl Downgrade for ast::Assert { } } +/// Downgrades an `if-then-else` expression. impl Downgrade for ast::IfElse { fn downgrade(self, ctx: &mut Ctx) -> Result { let cond = self.condition().unwrap().downgrade(ctx)?; @@ -131,6 +133,7 @@ impl Downgrade for ast::Str { } } +/// Downgrades a literal value (integer, float, or URI). impl Downgrade for ast::Literal { fn downgrade(self, ctx: &mut Ctx) -> Result { Ok(ctx.new_expr(match self.kind() { @@ -144,6 +147,7 @@ impl Downgrade for ast::Literal { } } +/// Downgrades an identifier to a variable lookup. impl Downgrade for ast::Ident { fn downgrade(self, ctx: &mut Ctx) -> Result { let sym = self.ident_token().unwrap().to_string(); @@ -151,6 +155,7 @@ impl Downgrade for ast::Ident { } } +/// Downgrades an attribute set. impl Downgrade for ast::AttrSet { fn downgrade(self, ctx: &mut Ctx) -> Result { let rec = self.rec_token().is_some(); @@ -160,6 +165,7 @@ impl Downgrade for ast::AttrSet { } } +/// Downgrades a list. impl Downgrade for ast::List { fn downgrade(self, ctx: &mut Ctx) -> Result { let mut items = Vec::with_capacity(self.items().size_hint().0); @@ -170,6 +176,7 @@ impl Downgrade for ast::List { } } +/// Downgrades a binary operation. impl Downgrade for ast::BinOp { fn downgrade(self, ctx: &mut Ctx) -> Result { let lhs = self.lhs().unwrap().downgrade(ctx)?; @@ -179,6 +186,7 @@ impl Downgrade for ast::BinOp { } } +/// Downgrades a "has attribute" (`?`) expression. impl Downgrade for ast::HasAttr { fn downgrade(self, ctx: &mut Ctx) -> Result { let lhs = self.expr().unwrap().downgrade(ctx)?; @@ -187,6 +195,7 @@ impl Downgrade for ast::HasAttr { } } +/// Downgrades a unary operation. impl Downgrade for ast::UnaryOp { fn downgrade(self, ctx: &mut Ctx) -> Result { let rhs = self.expr().unwrap().downgrade(ctx)?; @@ -195,6 +204,7 @@ impl Downgrade for ast::UnaryOp { } } +/// Downgrades an attribute selection (`.`). impl Downgrade for ast::Select { fn downgrade(self, ctx: &mut Ctx) -> Result { let expr = self.expr().unwrap().downgrade(ctx)?; @@ -235,6 +245,7 @@ impl Downgrade for ast::LegacyLet { } } +/// Downgrades a `let ... in ...` expression. impl Downgrade for ast::LetIn { fn downgrade(self, ctx: &mut Ctx) -> Result { let body = self.body().unwrap().downgrade(ctx)?; @@ -243,6 +254,7 @@ impl Downgrade for ast::LetIn { } } +/// Downgrades a `with` expression. impl Downgrade for ast::With { fn downgrade(self, ctx: &mut Ctx) -> Result { let namespace = self.namespace().unwrap().downgrade(ctx)?; @@ -251,6 +263,8 @@ impl Downgrade for ast::With { } } +/// Downgrades a lambda (function) expression. +/// This involves desugaring pattern-matching arguments into `let` bindings. impl Downgrade for ast::Lambda { fn downgrade(self, ctx: &mut Ctx) -> Result { let param = downgrade_param(self.param().unwrap(), ctx)?; @@ -261,6 +275,7 @@ impl Downgrade for ast::Lambda { let allowed; match param { Param::Ident(id) => { + // Simple case: `x: body` ident = Some(id); required = None; allowed = None; @@ -270,6 +285,7 @@ impl Downgrade for ast::Lambda { ellipsis, alias, } => { + // Complex case: `{ a, b ? 2, ... }@args: body` ident = alias.clone(); required = Some( formals @@ -279,7 +295,7 @@ impl Downgrade for ast::Lambda { .collect(), ); allowed = if ellipsis { - None + None // `...` means any attribute is allowed. } else { Some(formals.iter().map(|(k, _)| k.clone()).collect()) }; @@ -319,7 +335,7 @@ impl Downgrade for ast::Lambda { } } - let param = ir::Param { + let param = IrParam { ident, required, allowed, diff --git a/evaluator/nixjit_hir/src/lib.rs b/evaluator/nixjit_hir/src/lib.rs index d42174f..914637e 100644 --- a/evaluator/nixjit_hir/src/lib.rs +++ b/evaluator/nixjit_hir/src/lib.rs @@ -6,10 +6,11 @@ //! towards the lower-level IR (`nixjit_lir`). //! //! The key components are: -//! - `Hir`: An enum representing all possible expression types in the HIR. +//! - `Hir`: An enum representing all possible expression types in the HIR. This is +//! generated by the `ir!` macro. //! - `Downgrade`: A trait for converting `rnix::ast` nodes into HIR expressions. //! - `DowngradeContext`: A trait that provides the necessary context for the conversion, -//! such as allocating new expressions and functions. +//! such as allocating new expressions. use derive_more::{IsVariant, TryUnwrap, Unwrap}; use hashbrown::HashMap; @@ -17,7 +18,7 @@ use hashbrown::HashMap; use nixjit_error::{Error, Result}; use nixjit_ir::{ Assert, Attr, AttrSet, BinOp, Call, ConcatStrings, Const, ExprId, Func, HasAttr, If, List, - Path, Select, Str, UnOp, Var, With, + Param as IrParam, Path, Select, Str, UnOp, Var, With, }; use nixjit_macros::ir; use nixjit_value::format_symbol; @@ -30,7 +31,7 @@ pub use downgrade::Downgrade; /// A context for the AST-to-HIR downgrading process. /// -/// This trait abstracts the storage of HIR expressions and functions, allowing the +/// This trait abstracts the storage of HIR expressions, allowing the /// `downgrade` implementations to be generic over the specific context implementation. pub trait DowngradeContext { /// Allocates a new HIR expression in the context and returns its ID. @@ -43,6 +44,8 @@ pub trait DowngradeContext { fn with_expr_mut(&mut self, id: &ExprId, f: impl FnOnce(&mut Hir, &mut Self) -> T) -> T; } +// The `ir!` macro generates the `Hir` enum and related structs and traits. +// This reduces boilerplate for defining the IR structure. ir! { Hir, @@ -80,10 +83,12 @@ ir! { Path, // Represents a `let ... in ...` binding. Let { pub bindings: HashMap, pub body: ExprId }, - // Represents a function argument lookup. + // Represents a function argument lookup within the body of a function. Arg, } +/// A placeholder struct for the `Arg` HIR variant. It signifies that at this point +/// in the expression tree, we should be looking up a function argument. #[derive(Debug)] pub struct Arg; @@ -91,8 +96,8 @@ pub struct Arg; trait Attrs { /// Inserts a value into the attribute set at a given path. /// - /// # Example - /// `insert([a, b], value)` corresponds to `a.b = value;`. + /// This method handles the creation of nested attribute sets as needed. + /// For example, `insert([a, b], value)` on an empty set results in `{ a = { b = value; }; }`. fn insert( &mut self, path: Vec, @@ -129,10 +134,10 @@ impl Attrs for AttrSet { expr.as_mut() .try_unwrap_attr_set() .map_err(|_| { - // This path segment exists but is not an attrset. + // This path segment exists but is not an attrset, which is an error. Error::DowngradeError(format!( - r#"attribute '{}' already defined"#, - format_symbol(&ident) + "attribute '{}' already defined but is not an attribute set", + format_symbol(ident) )) }) .and_then(|attrs| attrs._insert(path, name, value, ctx)) @@ -147,7 +152,8 @@ impl Attrs for AttrSet { Ok(()) } Attr::Dynamic(dynamic) => { - // If the next attribute is a dynamic expression. + // If the next attribute is a dynamic expression, we must create a new sub-attrset. + // We cannot merge with existing dynamic attributes at this stage. let mut attrs = AttrSet::default(); attrs._insert(path, name, value, ctx)?; self.dyns.push((dynamic, ctx.new_expr(attrs.to_hir()))); @@ -160,8 +166,8 @@ impl Attrs for AttrSet { Attr::Str(ident) => { if self.stcs.insert(ident.clone(), value).is_some() { return Err(Error::DowngradeError(format!( - r#"attribute '{}' already defined"#, - format_symbol(&ident) + "attribute '{}' already defined", + format_symbol(ident) ))); } } @@ -186,6 +192,7 @@ impl Attrs for AttrSet { } } +/// Represents the different kinds of parameters a function can have in the HIR stage. #[derive(Debug)] enum Param { /// A simple parameter, e.g., `x: ...`. diff --git a/evaluator/nixjit_hir/src/utils.rs b/evaluator/nixjit_hir/src/utils.rs index 4156d5a..e9dafb1 100644 --- a/evaluator/nixjit_hir/src/utils.rs +++ b/evaluator/nixjit_hir/src/utils.rs @@ -10,9 +10,8 @@ use rnix::ast; use nixjit_error::{Error, Result}; use nixjit_ir::{Attr, AttrSet, ConcatStrings, ExprId, Select, Str, Var}; -use super::ToHir; use super::downgrade::Downgrade; -use super::{Attrs, DowngradeContext, Param}; +use super::{Attrs, DowngradeContext, Param, ToHir}; /// Downgrades a function parameter from the AST. pub fn downgrade_param(param: ast::Param, ctx: &mut impl DowngradeContext) -> Result { @@ -215,7 +214,7 @@ pub fn downgrade_static_attrpathvalue( let path = downgrade_attrpath(value.attrpath().unwrap(), ctx)?; if path.iter().any(|attr| matches!(attr, Attr::Dynamic(_))) { return Err(Error::DowngradeError( - "dynamic attributes not allowed".to_string(), + "dynamic attributes not allowed in let bindings".to_string(), )); } let value = value.value().unwrap().downgrade(ctx)?; diff --git a/evaluator/nixjit_ir/src/lib.rs b/evaluator/nixjit_ir/src/lib.rs index 529001f..a74f3e3 100644 --- a/evaluator/nixjit_ir/src/lib.rs +++ b/evaluator/nixjit_ir/src/lib.rs @@ -17,21 +17,36 @@ use hashbrown::{HashMap, HashSet}; use nixjit_value::Const as PubConst; /// A type-safe wrapper for an index into an expression table. +/// +/// Using a newtype wrapper like this prevents accidentally mixing up different kinds of indices. #[repr(transparent)] #[derive(Debug, PartialEq, Eq, Hash)] pub struct ExprId(usize); impl ExprId { + /// Creates a clone of the `ExprId`. + /// + /// # Safety + /// This is a shallow copy of the index. The caller must ensure that the lifetime + /// and validity of the expression being referenced are handled correctly. #[inline(always)] pub unsafe fn clone(&self) -> Self { Self(self.0) } + /// Returns the raw `usize` index. + /// + /// # Safety + /// The caller is responsible for using this index correctly and not causing out-of-bounds access. #[inline(always)] pub unsafe fn raw(self) -> usize { self.0 } + /// Creates an `ExprId` from a raw `usize` index. + /// + /// # Safety + /// The caller must ensure that the provided index is valid for the expression table. #[inline(always)] pub unsafe fn from(id: usize) -> Self { Self(id) @@ -44,11 +59,19 @@ impl ExprId { pub struct PrimOpId(usize); impl PrimOpId { + /// Returns the raw `usize` index. + /// + /// # Safety + /// The caller is responsible for using this index correctly. #[inline(always)] pub unsafe fn raw(self) -> usize { self.0 } + /// Creates a `PrimOpId` from a raw `usize` index. + /// + /// # Safety + /// The caller must ensure that the provided index is valid. #[inline(always)] pub unsafe fn from(id: usize) -> Self { Self(id) @@ -61,11 +84,19 @@ impl PrimOpId { pub struct ArgIdx(usize); impl ArgIdx { + /// Returns the raw `usize` index. + /// + /// # Safety + /// The caller is responsible for using this index correctly. #[inline(always)] pub unsafe fn raw(self) -> usize { self.0 } + /// Creates an `ArgIdx` from a raw `usize` index. + /// + /// # Safety + /// The caller must ensure that the provided index is valid. #[inline(always)] pub unsafe fn from(idx: usize) -> Self { Self(idx) @@ -87,8 +118,10 @@ pub struct AttrSet { #[derive(Debug, TryUnwrap)] pub enum Attr { /// A dynamic attribute key, which is an expression that must evaluate to a string. + /// Example: `attrs.${key}` Dynamic(ExprId), /// A static attribute key. + /// Example: `attrs.key` Str(String), } @@ -220,13 +253,20 @@ pub struct If { pub struct Func { /// The body of the function pub body: ExprId, + /// The parameter specification for the function. pub param: Param, } +/// Describes the parameters of a function. #[derive(Debug)] pub struct Param { + /// The name of the argument if it's a simple identifier (e.g., `x: ...`). + /// Also used for the alias in a pattern (e.g., `args @ { ... }`). pub ident: Option, + /// The set of required parameter names for a pattern-matching function. pub required: Option>, + /// The set of all allowed parameter names for a non-ellipsis pattern-matching function. + /// If `None`, any attribute is allowed (ellipsis `...` is present). pub allowed: Option>, } diff --git a/evaluator/nixjit_jit/src/compile.rs b/evaluator/nixjit_jit/src/compile.rs index a41c678..b21d6e2 100644 --- a/evaluator/nixjit_jit/src/compile.rs +++ b/evaluator/nixjit_jit/src/compile.rs @@ -1,13 +1,29 @@ +//! This module defines the `JITCompile` trait and its implementations for +//! various IR types. It provides the translation from LIR to Cranelift IR. + use cranelift::codegen::ir::{self, StackSlot}; use cranelift::prelude::*; -use nixjit_eval::{EvalContext, Value}; +use nixjit_eval::Value; use nixjit_ir::*; use nixjit_lir::Lir; use super::{Context, JITContext}; +/// A trait for compiling IR nodes to Cranelift IR. +/// +/// This trait defines how different IR nodes should be compiled to +/// Cranelift IR instructions that can be executed by the JIT compiler. pub trait JITCompile { + /// Compiles the IR node to Cranelift IR. + /// + /// # Arguments + /// * `ctx` - The compilation context + /// * `engine` - The evaluation context value + /// * `env` - The environment value + /// + /// # Returns + /// A stack slot containing the compiled result fn compile(&self, ctx: &mut Context, engine: ir::Value, env: ir::Value) -> StackSlot; } @@ -24,6 +40,9 @@ impl JITCompile for Lir { } impl JITCompile for AttrSet { + /// Compiles an attribute set to Cranelift IR. + /// + /// This creates a new attribute set and compiles all static attributes into it. fn compile(&self, ctx: &mut Context, engine: ir::Value, env: ir::Value) -> StackSlot { let attrs = ctx.create_attrs(); for (k, v) in self.stcs.iter() { @@ -35,6 +54,9 @@ impl JITCompile for AttrSet { } impl JITCompile for List { + /// Compiles a list to Cranelift IR. + /// + /// This creates a new list by compiling all items and storing them in an array. fn compile(&self, ctx: &mut Context, engine: ir::Value, env: ir::Value) -> StackSlot { let array = ctx.alloc_array(self.items.len()); for (i, item) in self.items.iter().enumerate() { @@ -67,6 +89,11 @@ impl JITCompile for HasAttr { } impl JITCompile for BinOp { + /// Compiles a binary operation to Cranelift IR. + /// + /// This implementation handles various binary operations like addition, subtraction, + /// division, logical AND/OR, and equality checks. It generates code that checks + /// the types of operands and performs the appropriate operation. fn compile(&self, ctx: &mut Context, engine: ir::Value, env: ir::Value) -> StackSlot { use BinOpKind::*; let lhs = self.lhs.compile(ctx, engine, env); @@ -328,6 +355,9 @@ impl JITCompile for UnOp { } impl JITCompile for Attr { + /// Compiles an attribute key to Cranelift IR. + /// + /// An attribute can be either a static string or a dynamic expression. fn compile(&self, ctx: &mut Context, engine: ir::Value, env: ir::Value) -> StackSlot { use Attr::*; match self { @@ -338,6 +368,10 @@ impl JITCompile for Attr { } impl JITCompile for Select { + /// Compiles an attribute selection to Cranelift IR. + /// + /// This compiles the expression to select from, builds the attribute path, + /// and calls the select helper function. fn compile(&self, ctx: &mut Context, engine: ir::Value, env: ir::Value) -> StackSlot { let val = self.expr.compile(ctx, engine, env); let attrpath = ctx.alloc_array(self.attrpath.len()); @@ -366,6 +400,10 @@ impl JITCompile for Select { } impl JITCompile for If { + /// Compiles an if-expression to Cranelift IR. + /// + /// This generates code that evaluates the condition, checks that it's a boolean, + /// and then jumps to the appropriate branch (true or false). fn compile(&self, ctx: &mut Context, engine: ir::Value, env: ir::Value) -> StackSlot { let cond = self.cond.compile(ctx, engine, env); let cond_type = ctx.builder.ins().stack_load(types::I64, cond, 0); @@ -424,6 +462,10 @@ impl JITCompile for If { } impl JITCompile for Call { + /// Compiles a function call to Cranelift IR. + /// + /// This compiles the function expression and all arguments, builds an argument array, + /// and calls the call helper function. fn compile(&self, ctx: &mut Context, engine: ir::Value, env: ir::Value) -> StackSlot { let func = self.func.compile(ctx, engine, env); let args = ctx.alloc_array(self.args.len()); @@ -452,6 +494,10 @@ impl JITCompile for Call { } impl JITCompile for With { + /// Compiles a `with` expression to Cranelift IR. + /// + /// This enters a new `with` scope with the compiled namespace, compiles the body expression, + /// and then exits the `with` scope. fn compile(&self, ctx: &mut Context, engine: ir::Value, env: ir::Value) -> StackSlot { let namespace = self.namespace.compile(ctx, engine, env); ctx.enter_with(env, namespace); @@ -475,6 +521,10 @@ impl JITCompile for ConcatStrings { } impl JITCompile for Const { + /// Compiles a constant value to Cranelift IR. + /// + /// This handles boolean, integer, float, and null constants by storing + /// their values and type tags in a stack slot. fn compile(&self, ctx: &mut Context, engine: ir::Value, env: ir::Value) -> StackSlot { use nixjit_value::Const::*; let slot = ctx.alloca(); @@ -507,12 +557,18 @@ impl JITCompile for Const { } impl JITCompile for Str { + /// Compiles a string literal to Cranelift IR. + /// + /// This creates a string value from the string literal. fn compile(&self, ctx: &mut Context, engine: ir::Value, env: ir::Value) -> StackSlot { ctx.create_string(&self.val) } } impl JITCompile for Var { + /// Compiles a variable lookup to Cranelift IR. + /// + /// This looks up a variable by its symbol in the current environment. fn compile(&self, ctx: &mut Context, engine: ir::Value, env: ir::Value) -> StackSlot { ctx.lookup(env, &self.sym) } diff --git a/evaluator/nixjit_jit/src/helpers.rs b/evaluator/nixjit_jit/src/helpers.rs index 4512823..c8a2396 100644 --- a/evaluator/nixjit_jit/src/helpers.rs +++ b/evaluator/nixjit_jit/src/helpers.rs @@ -1,3 +1,8 @@ +//! Helper functions for the JIT compiler. +//! +//! These functions are called from JIT-compiled code to perform operations +//! that are difficult or unsafe to do directly in the generated IR. + use core::{slice, str}; use std::alloc::Layout; use std::alloc::alloc; @@ -10,6 +15,10 @@ use nixjit_eval::{AttrSet, EvalContext, List, Value}; use super::JITContext; +/// Helper function to call a function with arguments. +/// +/// This function is called from JIT-compiled code to perform function calls. +/// It takes a function value and an array of arguments, and executes the call. pub extern "C" fn helper_call( func: &mut Value, args_ptr: *mut Value, @@ -22,6 +31,9 @@ pub extern "C" fn helper_call( func.call(args.into_iter().map(Ok), ctx).unwrap(); } +/// Helper function to look up a value in the evaluation stack. +/// +/// This function is called from JIT-compiled code to access values in the evaluation stack. pub extern "C" fn helper_lookup_stack( ctx: &Ctx, offset: usize, @@ -30,6 +42,9 @@ pub extern "C" fn helper_lookup_stack( ret.write(ctx.lookup_stack(offset).clone()); } +/// Helper function to look up a function argument. +/// +/// This function is called from JIT-compiled code to access function arguments. pub extern "C" fn helper_lookup_arg( ctx: &Ctx, offset: usize, @@ -38,6 +53,10 @@ pub extern "C" fn helper_lookup_arg( ret.write(JITContext::lookup_arg(ctx, offset).clone()); } +/// Helper function to look up a variable by name. +/// +/// This function is called from JIT-compiled code to perform variable lookups +/// in the current scope and `with` expression scopes. pub extern "C" fn helper_lookup( ctx: &Ctx, sym_ptr: *const u8, @@ -56,6 +75,10 @@ pub extern "C" fn helper_lookup( } } +/// Helper function to perform attribute selection. +/// +/// This function is called from JIT-compiled code to select attributes from +/// an attribute set using a path of attribute names. pub extern "C" fn helper_select( val: &mut Value, path_ptr: *mut Value, @@ -70,6 +93,10 @@ pub extern "C" fn helper_select( .unwrap(); } +/// Helper function to perform attribute selection with a default value. +/// +/// This function is called from JIT-compiled code to select attributes from +/// an attribute set, returning a default value if the selection fails. pub extern "C" fn helper_select_with_default( val: &mut Value, path_ptr: *mut Value, @@ -88,10 +115,17 @@ pub extern "C" fn helper_select_with_default( .unwrap(); } +/// Helper function to check equality between two values. +/// +/// This function is called from JIT-compiled code to perform equality comparisons. pub extern "C" fn helper_eq(lhs: &mut Value, rhs: &Value) { lhs.eq(rhs); } +/// Helper function to create a string value. +/// +/// This function is called from JIT-compiled code to create string values +/// from raw byte arrays. pub unsafe extern "C" fn helper_create_string( ptr: *const u8, len: usize, @@ -104,6 +138,10 @@ pub unsafe extern "C" fn helper_create_string( } } +/// Helper function to create a list value. +/// +/// This function is called from JIT-compiled code to create list values +/// from arrays of values. pub unsafe extern "C" fn helper_create_list( ptr: *mut Value, len: usize, @@ -116,12 +154,19 @@ pub unsafe extern "C" fn helper_create_list( } } +/// Helper function to create an attribute set. +/// +/// This function is called from JIT-compiled code to create a new, empty attribute set. pub unsafe extern "C" fn helper_create_attrs( ret: &mut MaybeUninit>, ) { ret.write(HashMap::new()); } +/// Helper function to add an attribute to an attribute set. +/// +/// This function is called from JIT-compiled code to insert a key-value pair +/// into an attribute set. pub unsafe extern "C" fn helper_push_attr( attrs: &mut HashMap, sym_ptr: *const u8, @@ -136,6 +181,10 @@ pub unsafe extern "C" fn helper_push_attr( } } +/// Helper function to finalize an attribute set. +/// +/// This function is called from JIT-compiled code to convert a HashMap into +/// a proper attribute set value. pub unsafe extern "C" fn helper_finalize_attrs( attrs: NonNull>, ret: &mut MaybeUninit, @@ -145,6 +194,10 @@ pub unsafe extern "C" fn helper_finalize_attrs( )); } +/// Helper function to enter a `with` expression scope. +/// +/// This function is called from JIT-compiled code to enter a new `with` scope +/// with the given namespace. pub unsafe extern "C" fn helper_enter_with( ctx: &mut Ctx, namespace: NonNull, @@ -152,14 +205,24 @@ pub unsafe extern "C" fn helper_enter_with( ctx.enter_with(unsafe { namespace.read() }.unwrap_attr_set().into_inner()); } +/// Helper function to exit a `with` expression scope. +/// +/// This function is called from JIT-compiled code to exit the current `with` scope. pub unsafe extern "C" fn helper_exit_with(ctx: &mut Ctx) { ctx.exit_with(); } +/// Helper function to allocate an array of values. +/// +/// This function is called from JIT-compiled code to allocate memory for +/// arrays of values, such as function arguments or list elements. pub unsafe extern "C" fn helper_alloc_array(len: usize) -> *mut u8 { unsafe { alloc(Layout::array::(len).unwrap()) } } +/// Helper function for debugging. +/// +/// This function is called from JIT-compiled code to print a value for debugging purposes. pub extern "C" fn helper_dbg(value: &Value) { println!("{value:?}") } diff --git a/evaluator/nixjit_jit/src/lib.rs b/evaluator/nixjit_jit/src/lib.rs index f80b689..daa661b 100644 --- a/evaluator/nixjit_jit/src/lib.rs +++ b/evaluator/nixjit_jit/src/lib.rs @@ -1,3 +1,14 @@ +//! The Just-In-Time (JIT) compilation module for nixjit. +//! +//! This module provides functionality to compile Low-Level IR (LIR) expressions +//! into optimized machine code using Cranelift. The JIT compiler translates +//! Nix expressions into efficient native code for faster evaluation. +//! +//! The main components are: +//! - `JITCompiler`: The core compiler that manages the compilation process +//! - `JITContext`: A trait that provides the execution context for JIT-compiled code +//! - `Context`: An internal compilation context used during code generation + use std::marker::PhantomData; use std::ops::Deref; use std::rc::Rc; @@ -18,15 +29,33 @@ mod helpers; pub use compile::JITCompile; use helpers::*; -pub trait JITContext: EvalContext + Sized { +/// A trait that provides the execution context for JIT-compiled code. +/// +/// This trait extends `EvalContext` with additional methods needed +/// for JIT compilation, such as stack and argument lookups, and +/// managing `with` expression scopes. +pub trait JITContext: EvalContext { + /// Looks up a value in the evaluation stack by offset. fn lookup_stack(&self, offset: usize) -> &Value; + /// Looks up a function argument by offset. fn lookup_arg(&self, offset: usize) -> &Value; + /// Enters a `with` expression scope with the given namespace. fn enter_with(&mut self, namespace: Rc>); + /// Exits the current `with` expression scope. fn exit_with(&mut self); } +/// Type alias for a JIT-compiled function. +/// +/// This represents a function pointer to JIT-compiled code that takes +/// a context pointer and a mutable value pointer as arguments. type F = unsafe extern "C" fn(*const Ctx, *mut Value); +/// A JIT-compiled function. +/// +/// This struct holds a function pointer to the compiled code and +/// a set of strings that were used during compilation, which need +/// to be kept alive for the function to work correctly. pub struct JITFunc { func: F, strings: HashSet, @@ -39,10 +68,18 @@ impl Deref for JITFunc { } } +/// The internal compilation context used during code generation. +/// +/// This context holds references to the compiler, the Cranelift function builder, +/// and manages resources like stack slots and string literals during compilation. struct Context<'comp, 'ctx, Ctx: JITContext> { + /// Reference to the JIT compiler. pub compiler: &'comp mut JITCompiler, + /// The Cranelift function builder used to generate IR. pub builder: FunctionBuilder<'ctx>, + /// Stack slots available for reuse. free_slots: Vec, + /// String literals used during compilation. strings: HashSet, } @@ -374,6 +411,7 @@ impl<'comp, 'ctx, Ctx: JITContext> Context<'comp, 'ctx, Ctx> { } } +/// The main JIT compiler that manages the compilation process. pub struct JITCompiler { ctx: codegen::Context, module: JITModule, diff --git a/evaluator/nixjit_lir/src/lib.rs b/evaluator/nixjit_lir/src/lib.rs index 2209c91..c5a1f87 100644 --- a/evaluator/nixjit_lir/src/lib.rs +++ b/evaluator/nixjit_lir/src/lib.rs @@ -1,3 +1,16 @@ +//! The Low-level Intermediate Representation (LIR) for nixjit. +//! +//! This module defines the LIR, which is a more resolved and explicit representation +//! than the HIR. The key transformation from HIR to LIR is the resolution of variable +//! lookups. In the LIR, variable references are either resolved to a specific expression, +//! a function argument, or are left as-is for dynamic lookup in a `with` environment. +//! +//! Key components: +//! - `Lir`: An enum representing all LIR expression types, generated by the `ir!` macro. +//! - `Resolve`: A trait for converting HIR nodes into LIR expressions. +//! - `ResolveContext`: A trait providing the context for resolution, including scope +//! management and dependency tracking. + use derive_more::{IsVariant, TryUnwrap, Unwrap}; use nixjit_error::{Error, Result}; @@ -6,6 +19,7 @@ use nixjit_ir::*; use nixjit_macros::ir; use nixjit_value::format_symbol; +// The `ir!` macro generates the `Lir` enum and related structs and traits. ir! { Lir, @@ -30,35 +44,58 @@ ir! { ArgRef(ArgIdx), } -#[derive(Debug)] -pub struct Builtins; - +/// Represents the result of a variable lookup within the `ResolveContext`. #[derive(Debug)] pub enum LookupResult { + /// The variable was found and resolved to a specific expression. Expr(ExprId), + /// The variable was found and resolved to a function argument. Arg(ArgIdx), + /// The variable could not be resolved statically, likely due to a `with` expression. + /// The lookup must be performed dynamically at evaluation time. Unknown, + /// The variable was not found in any scope. NotFound, } +/// A context for the HIR-to-LIR resolution process. +/// +/// This trait abstracts the environment in which expressions are resolved, managing +/// scopes, dependencies, and the resolution of expressions themselves. pub trait ResolveContext { + /// Records a dependency of one expression on another. fn new_dep(&mut self, expr: &ExprId, dep: ExprId); + + /// Creates a new function, associating a parameter specification with a body expression. fn new_func(&mut self, body: &ExprId, param: Param); + + /// Triggers the resolution of a given expression. fn resolve(&mut self, expr: &ExprId) -> Result<()>; + + /// Looks up a variable by name in the current scope. fn lookup(&self, name: &str) -> LookupResult; + + /// Enters a `with` scope for the duration of a closure's execution. fn with_with_env(&mut self, f: impl FnOnce(&mut Self) -> T) -> (bool, T); + + /// Enters a `let` scope with a given set of bindings for the duration of a closure. fn with_let_env<'a, T>( &mut self, bindings: impl Iterator, f: impl FnOnce(&mut Self) -> T, ) -> T; + + /// Enters a function parameter scope for the duration of a closure. fn with_param_env(&mut self, ident: Option, f: impl FnOnce(&mut Self) -> T) -> T; } +/// A trait for converting (resolving) an HIR node into an LIR expression. pub trait Resolve { + /// Performs the resolution. fn resolve(self, ctx: &mut Ctx) -> Result; } +/// The main entry point for resolving any HIR expression. impl Resolve for hir::Hir { fn resolve(self, ctx: &mut Ctx) -> Result { use hir::Hir::*; @@ -80,14 +117,21 @@ impl Resolve for hir::Hir { Var(x) => x.resolve(ctx), Path(x) => x.resolve(ctx), Let(x) => x.resolve(ctx), + // The `Arg` in HIR is a placeholder. During resolution, it's replaced by + // a reference to the *current* function's argument. We assume index 0 + // here, as the context manages the actual argument index. Arg(_) => unsafe { Ok(Lir::ArgRef(ArgIdx::from(0))) }, } } } +/// Resolves an `AttrSet`. If it's recursive, resolution is more complex (and currently a TODO). +/// Otherwise, it resolves all key and value expressions. impl Resolve for AttrSet { fn resolve(self, ctx: &mut Ctx) -> Result { if self.rec { + // TODO: Implement resolution for recursive attribute sets. + // This requires setting up a recursive scope where attributes can refer to each other. todo!() } else { for (_, v) in self.stcs.iter() { @@ -102,6 +146,7 @@ impl Resolve for AttrSet { } } +/// Resolves a `List` by resolving each of its items. impl Resolve for List { fn resolve(self, ctx: &mut Ctx) -> Result { for item in self.items.iter() { @@ -111,18 +156,20 @@ impl Resolve for List { } } +/// Resolves a `HasAttr` expression by resolving the LHS and any dynamic attributes in the path. impl Resolve for HasAttr { fn resolve(self, ctx: &mut Ctx) -> Result { ctx.resolve(&self.lhs)?; for attr in self.rhs.iter() { if let Attr::Dynamic(expr) = attr { - ctx.resolve(&expr)?; + ctx.resolve(expr)?; } } Ok(self.to_lir()) } } +/// Resolves a `BinOp` by resolving its left and right hand sides. impl Resolve for BinOp { fn resolve(self, ctx: &mut Ctx) -> Result { ctx.resolve(&self.lhs)?; @@ -131,6 +178,7 @@ impl Resolve for BinOp { } } +/// Resolves a `UnOp` by resolving its right hand side. impl Resolve for UnOp { fn resolve(self, ctx: &mut Ctx) -> Result { ctx.resolve(&self.rhs)?; @@ -138,12 +186,14 @@ impl Resolve for UnOp { } } +/// Resolves a `Select` by resolving the expression being selected from, any dynamic +/// attributes in the path, and the default value if it exists. impl Resolve for Select { fn resolve(self, ctx: &mut Ctx) -> Result { ctx.resolve(&self.expr)?; for attr in self.attrpath.iter() { if let Attr::Dynamic(expr) = attr { - ctx.resolve(&expr)?; + ctx.resolve(expr)?; } } if let Some(ref expr) = self.default { @@ -153,6 +203,7 @@ impl Resolve for Select { } } +/// Resolves an `If` expression by resolving the condition, consequence, and alternative. impl Resolve for If { fn resolve(self, ctx: &mut Ctx) -> Result { ctx.resolve(&self.cond)?; @@ -162,6 +213,8 @@ impl Resolve for If { } } +/// Resolves a `Func` by resolving its body within a new parameter scope. +/// It then registers the function with the context. impl Resolve for Func { fn resolve(self, ctx: &mut Ctx) -> Result { ctx.with_param_env(self.param.ident.clone(), |ctx| ctx.resolve(&self.body))?; @@ -170,6 +223,7 @@ impl Resolve for Func { } } +/// Resolves a `Call` by resolving the function and all of its arguments. impl Resolve for Call { fn resolve(self, ctx: &mut Ctx) -> Result { ctx.resolve(&self.func)?; @@ -180,11 +234,15 @@ impl Resolve for Call { } } +/// Resolves a `With` expression by resolving the namespace and the body. +/// The body is resolved within a special "with" scope. impl Resolve for With { fn resolve(self, ctx: &mut Ctx) -> Result { ctx.resolve(&self.namespace)?; let (env_used, res) = ctx.with_with_env(|ctx| ctx.resolve(&self.expr)); res?; + // Optimization: if the `with` environment was not actually used by any variable + // lookup in the body, we can elide the `With` node entirely. if env_used { Ok(self.to_lir()) } else { @@ -193,6 +251,7 @@ impl Resolve for With { } } +/// Resolves an `Assert` by resolving the assertion condition and the body. impl Resolve for Assert { fn resolve(self, ctx: &mut Ctx) -> Result { ctx.resolve(&self.assertion)?; @@ -201,6 +260,7 @@ impl Resolve for Assert { } } +/// Resolves a `ConcatStrings` by resolving each part. impl Resolve for ConcatStrings { fn resolve(self, ctx: &mut Ctx) -> Result { for part in self.parts.iter() { @@ -210,6 +270,7 @@ impl Resolve for ConcatStrings { } } +/// Resolves a `Var` by looking it up in the current context. impl Resolve for Var { fn resolve(self, ctx: &mut Ctx) -> Result { use LookupResult::*; @@ -225,6 +286,7 @@ impl Resolve for Var { } } +/// Resolves a `Path` by resolving the underlying expression that defines the path's content. impl Resolve for Path { fn resolve(self, ctx: &mut Ctx) -> Result { ctx.resolve(&self.expr)?; @@ -232,6 +294,8 @@ impl Resolve for Path { } } +/// Resolves a `Let` expression by creating a new scope for the bindings, resolving +/// the bindings and the body, and then returning a reference to the body. impl Resolve for hir::Let { fn resolve(self, ctx: &mut Ctx) -> Result { ctx.with_let_env(self.bindings.iter(), |ctx| { @@ -240,6 +304,7 @@ impl Resolve for hir::Let { } ctx.resolve(&self.body) })?; + // The `let` expression itself evaluates to its body. Ok(Lir::ExprRef(self.body)) } } diff --git a/evaluator/nixjit_macros/src/builtins.rs b/evaluator/nixjit_macros/src/builtins.rs index a9079b4..0808f90 100644 --- a/evaluator/nixjit_macros/src/builtins.rs +++ b/evaluator/nixjit_macros/src/builtins.rs @@ -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`. + 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 { + /// 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 Builtins { + /// 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`. 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: &mut Ctx, mut args: Vec<::nixjit_eval::Value>) -> ::nixjit_error::Result<::nixjit_eval::Value> { if args.len() != #arg_count { diff --git a/evaluator/nixjit_macros/src/ir.rs b/evaluator/nixjit_macros/src/ir.rs index bb85c43..41c84de 100644 --- a/evaluator/nixjit_macros/src/ir.rs +++ b/evaluator/nixjit_macros/src/ir.rs @@ -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, } @@ -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 { + // The macro input is expected to be: `IrName, Variant1, Variant2, ...` let base_name = input.parse()?; input.parse::()?; 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 )* }; diff --git a/evaluator/nixjit_macros/src/lib.rs b/evaluator/nixjit_macros/src/lib.rs index 69eed95..e1b95ae 100644 --- a/evaluator/nixjit_macros/src/lib.rs +++ b/evaluator/nixjit_macros/src/lib.rs @@ -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) diff --git a/evaluator/nixjit_value/src/lib.rs b/evaluator/nixjit_value/src/lib.rs index 376d415..d7f2d0f 100644 --- a/evaluator/nixjit_value/src/lib.rs +++ b/evaluator/nixjit_value/src/lib.rs @@ -1,3 +1,9 @@ +//! Defines the public-facing data structures for Nix values. +//! +//! These types are used to represent the final result of an evaluation and are +//! designed to be user-friendly and serializable. They are distinct from the +//! internal `Value` types used during evaluation in `nixjit_eval`. + use core::fmt::{Debug, Display, Formatter, Result as FmtResult}; use core::hash::Hash; use core::ops::Deref; @@ -9,8 +15,11 @@ use std::sync::LazyLock; use derive_more::{Constructor, IsVariant, Unwrap}; use regex::Regex; +/// Represents errors thrown by `assert` and `throw` expressions in Nix. +/// These errors can potentially be caught and handled by `builtins.tryEval`. #[derive(Clone, Debug, PartialEq, Constructor, Hash)] pub struct Catchable { + /// The error message. msg: String, } @@ -26,11 +35,16 @@ impl Display for Catchable { } } +/// Represents a constant, primitive value in Nix. #[derive(Debug, Clone, Copy, PartialEq, IsVariant, Unwrap)] pub enum Const { + /// A boolean value (`true` or `false`). Bool(bool), + /// A 64-bit signed integer. Int(i64), + /// A 64-bit floating-point number. Float(f64), + /// The `null` value. Null, } @@ -64,6 +78,7 @@ impl From for Const { } } +/// Represents a Nix symbol, which is used as a key in attribute sets. #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Constructor)] pub struct Symbol(String); @@ -73,9 +88,11 @@ impl> From for Symbol { } } -pub fn format_symbol<'a>(sym: &'a str) -> Cow<'a, str> { - if REGEX.is_match(sym) { - Cow::Borrowed(sym) +/// Formats a string slice as a Nix symbol, quoting it if necessary. +pub fn format_symbol<'a>(sym: impl Into>) -> Cow<'a, str> { + let sym = sym.into(); + if REGEX.is_match(&sym) { + sym } else { Cow::Owned(format!(r#""{sym}""#)) } @@ -92,8 +109,9 @@ impl Display for Symbol { } static REGEX: LazyLock = - LazyLock::new(|| Regex::new(r#"^[a-zA-Z\_][a-zA-Z0-9\_\'\-]*$"#).unwrap()); + LazyLock::new(|| Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_'-]*$").unwrap()); impl Symbol { + /// Checks if the symbol is a "normal" identifier that doesn't require quotes. fn normal(&self) -> bool { REGEX.is_match(self) } @@ -107,15 +125,18 @@ impl Deref for Symbol { } impl Symbol { + /// Consumes the `Symbol`, returning its inner `String`. pub fn into_inner(self) -> String { self.0 } + /// Returns a reference to the inner `String`. pub fn as_inner(&self) -> &String { &self.0 } } +/// Represents a Nix attribute set, which is a map from symbols to values. #[derive(Constructor, Clone, PartialEq)] pub struct AttrSet { data: BTreeMap, @@ -124,15 +145,16 @@ pub struct AttrSet { impl Debug for AttrSet { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { use Value::*; - write!(f, "{{ ")?; + write!(f, "{{")?; for (k, v) in self.data.iter() { + write!(f, " {k:?} = ")?; match v { - List(_) => write!(f, "{k:?} = [ ... ]; ")?, - AttrSet(_) => write!(f, "{k:?} = {{ ... }}; ")?, - v => write!(f, "{k:?} = {v:?}; ")?, + List(_) => write!(f, "[ ... ];")?, + AttrSet(_) => write!(f, "{{ ... }};")?, + v => write!(f, "{v:?};")?, } } - write!(f, "}}") + write!(f, " }}") } } @@ -140,19 +162,24 @@ impl Display for AttrSet { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { use Value::*; write!(f, "{{ ")?; + let mut first = true; for (k, v) in self.data.iter() { + if !first { + write!(f, "; ")?; + } write!(f, "{k} = ")?; match v { AttrSet(_) => write!(f, "{{ ... }}"), List(_) => write!(f, "[ ... ]"), v => write!(f, "{v}"), }?; - write!(f, "; ")?; + first = false; } - write!(f, "}}") + write!(f, " }}") } } +/// Represents a Nix list, which is a vector of values. #[derive(Constructor, Clone, Debug, PartialEq)] pub struct List { data: Vec, @@ -168,17 +195,29 @@ impl Display for List { } } +/// Represents any possible Nix value that can be returned from an evaluation. #[derive(IsVariant, Unwrap, Clone, Debug, PartialEq)] pub enum Value { + /// A constant value (int, float, bool, null). Const(Const), + /// A string value. String(String), + /// An attribute set. AttrSet(AttrSet), + /// A list. List(List), + /// A catchable error. Catchable(Catchable), + /// A thunk, representing a delayed computation. Thunk, + /// A function (lambda). Func, + /// A primitive (built-in) operation. PrimOp(&'static str), + /// A partially applied primitive operation. PrimOpApp(&'static str), + /// A marker for a value that has been seen before during serialization, to break cycles. + /// This is used to prevent infinite recursion when printing or serializing cyclic data structures. Repeated, } @@ -187,7 +226,7 @@ impl Display for Value { use Value::*; match self { Const(x) => write!(f, "{x}"), - String(x) => write!(f, "{x}"), + String(x) => write!(f, r#""{x}""#), AttrSet(x) => write!(f, "{x}"), List(x) => write!(f, "{x}"), Catchable(x) => write!(f, "{x}"), diff --git a/flake.nix b/flake.nix index 4b1ef86..f083931 100644 --- a/flake.nix +++ b/flake.nix @@ -10,7 +10,7 @@ in { devShells = forAllSystems (system: - let pkgs = import nixpkgs { inherit system; }; in + let pkgs = import nixpkgs { inherit system; config.allowUnfree = true; }; in { default = pkgs.mkShell { packages = with pkgs; [ @@ -26,6 +26,7 @@ gdb valgrind gemini-cli + claude-code ]; }; }