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

@@ -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;

View File

@@ -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 {

View File

@@ -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<String, ExprId>),
/// A function argument scope. `Some` holds the name of the argument set if present.
Arg(Option<String>),
}
/// 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<RefCell<Ir>>,
/// Tracks whether an `ExprId` has been resolved from HIR to LIR.
resolved: Vec<bool>,
/// The stack of lexical scopes used for name resolution.
scopes: Vec<Scope>,
/// The number of arguments in the current function call scope.
args_count: usize,
/// A table of primitive operation implementations.
primops: Vec<fn(&mut Context, Vec<Value>) -> Result<Value>>,
/// Maps a function's body `ExprId` to its parameter definition.
funcs: HashMap<ExprId, Param>,
/// A dependency graph between expressions.
graph: DiGraph<ExprId, ()>,
/// Maps an `ExprId` to its corresponding `NodeIndex` in the dependency graph.
nodes: Vec<NodeIndex>,
/// The call stack for function evaluation, where each frame holds arguments.
stack: Vec<Vec<Value>>,
/// A stack of namespaces for `with` expressions during evaluation.
with_scopes: Vec<Rc<HashMap<String, Value>>>,
/// The Just-In-Time (JIT) compiler.
jit: JITCompiler<Self>,
/// A cache for JIT-compiled functions, indexed by `ExprId`.
compiled: Vec<OnceCell<JITFunc<Self>>>,
}
@@ -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<nixjit_value::Value> {
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<Value>) -> Result<Value> {
unsafe {
(self.primops.get_unchecked(id.raw()))(self, args)
}
unsafe { (self.primops.get_unchecked(id.raw()))(self, args) }
}
}

View File

@@ -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<T> = core::result::Result<T, Error>;
/// 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,
}

View File

@@ -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<Value>;
/// Enters a `with` scope for the duration of a closure's execution.
fn with_with_env<T>(
&mut self,
namespace: Rc<HashMap<String, Value>>,
f: impl FnOnce(&mut Self) -> T,
) -> T;
fn with_args_env<T>(&mut self, args: Vec<Value>, f: impl FnOnce(&mut Self) -> T) -> (Vec<Value>, T);
/// Pushes a new set of arguments onto the stack for a function call.
fn with_args_env<T>(
&mut self,
args: Vec<Value>,
f: impl FnOnce(&mut Self) -> T,
) -> (Vec<Value>, 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<Value>;
/// Calls a primitive operation (builtin) by its ID.
fn call_primop(&mut self, id: PrimOpId, args: Vec<Value>) -> Result<Value>;
}
/// A trait for types that can be evaluated within an `EvalContext`.
pub trait Evaluate<Ctx: EvalContext> {
/// Performs the evaluation.
fn eval(&self, ctx: &mut Ctx) -> Result<Value>;
}
impl<Ctx: EvalContext> Evaluate<Ctx> for ExprId {
/// Evaluating an `ExprId` simply delegates to the context.
fn eval(&self, ctx: &mut Ctx) -> Result<Value> {
ctx.eval(self)
}
}
impl<Ctx: EvalContext> Evaluate<Ctx> for lir::Lir {
/// Evaluates an LIR node by dispatching to the specific implementation for its variant.
fn eval(&self, ctx: &mut Ctx) -> Result<Value> {
use lir::Lir::*;
match self {
@@ -63,6 +94,7 @@ impl<Ctx: EvalContext> Evaluate<Ctx> for lir::Lir {
}
impl<Ctx: EvalContext> Evaluate<Ctx> for ir::AttrSet {
/// Evaluates an `AttrSet` by evaluating all its static and dynamic attributes.
fn eval(&self, ctx: &mut Ctx) -> Result<Value> {
let mut attrs = AttrSet::new(
self.stcs
@@ -79,24 +111,26 @@ impl<Ctx: EvalContext> Evaluate<Ctx> 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<Ctx: EvalContext> Evaluate<Ctx> for ir::List {
/// Evaluates a `List` by evaluating all its items.
fn eval(&self, ctx: &mut Ctx) -> Result<Value> {
let items = self
.items
.iter()
.map(|val| val.eval(ctx))
.collect::<Result<Vec<_>>>()?;
let result = Value::List(List::from(items).into()).ok();
Ok(result.unwrap())
let result = Value::List(List::from(items).into());
Ok(result)
}
}
impl<Ctx: EvalContext> Evaluate<Ctx> 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<Value> {
use ir::Attr::*;
let mut val = self.lhs.eval(ctx)?;
@@ -110,12 +144,12 @@ impl<Ctx: EvalContext> Evaluate<Ctx> for ir::HasAttr {
}
})
}))?;
let result = val.ok();
Ok(result.unwrap())
Ok(val)
}
}
impl<Ctx: EvalContext> Evaluate<Ctx> for ir::BinOp {
/// Evaluates a `BinOp` by evaluating the LHS and RHS, then performing the operation.
fn eval(&self, ctx: &mut Ctx) -> Result<Value> {
use ir::BinOpKind::*;
let mut lhs = self.lhs.eval(ctx)?;
@@ -166,6 +200,7 @@ impl<Ctx: EvalContext> Evaluate<Ctx> for ir::BinOp {
}
impl<Ctx: EvalContext> Evaluate<Ctx> for ir::UnOp {
/// Evaluates a `UnOp` by evaluating the RHS and performing the operation.
fn eval(&self, ctx: &mut Ctx) -> Result<Value> {
use ir::UnOpKind::*;
let mut rhs = self.rhs.eval(ctx)?;
@@ -182,6 +217,8 @@ impl<Ctx: EvalContext> Evaluate<Ctx> for ir::UnOp {
}
impl<Ctx: EvalContext> Evaluate<Ctx> 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<Value> {
use ir::Attr::*;
let mut val = self.expr.eval(ctx)?;
@@ -212,18 +249,20 @@ impl<Ctx: EvalContext> Evaluate<Ctx> for ir::Select {
})
}))?;
}
let result = val.ok();
Ok(result.unwrap())
Ok(val)
}
}
impl<Ctx: EvalContext> Evaluate<Ctx> 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<Value> {
// 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<Ctx: EvalContext> Evaluate<Ctx> for ir::If {
}
impl<Ctx: EvalContext> Evaluate<Ctx> for ir::Call {
/// Evaluates a `Call` by evaluating the function and its arguments, then performing the call.
fn eval(&self, ctx: &mut Ctx) -> Result<Value> {
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<Ctx: EvalContext> Evaluate<Ctx> 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<Value> {
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<Ctx: EvalContext> Evaluate<Ctx> for ir::With {
}
impl<Ctx: EvalContext> Evaluate<Ctx> 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<Value> {
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<Ctx: EvalContext> Evaluate<Ctx> 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<Value> {
let mut parts = self
.parts
@@ -283,7 +329,7 @@ impl<Ctx: EvalContext> Evaluate<Ctx> for ir::ConcatStrings {
.map(|part| {
let mut part = part.eval(ctx)?;
part.coerce_to_string();
part.ok()
Ok(part)
})
.collect::<Result<Vec<_>>>()?
.into_iter();
@@ -292,44 +338,44 @@ impl<Ctx: EvalContext> Evaluate<Ctx> for ir::ConcatStrings {
a.concat_string(b);
a
});
Ok(result.ok().unwrap())
Ok(result)
}
}
impl<Ctx: EvalContext> Evaluate<Ctx> for ir::Str {
/// Evaluates a `Str` literal into a `Value::String`.
fn eval(&self, _: &mut Ctx) -> Result<Value> {
let result = Value::String(self.val.clone()).ok();
Ok(result.unwrap())
Ok(Value::String(self.val.clone()))
}
}
impl<Ctx: EvalContext> Evaluate<Ctx> for ir::Const {
/// Evaluates a `Const` literal into its corresponding `Value` variant.
fn eval(&self, _: &mut Ctx) -> Result<Value> {
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<Ctx: EvalContext> Evaluate<Ctx> 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<Value> {
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<Ctx: EvalContext> Evaluate<Ctx> for ir::Path {
/// Evaluates a `Path`. (Currently a TODO).
fn eval(&self, ctx: &mut Ctx) -> Result<Value> {
todo!()
}

View File

@@ -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<String, Value>`.
#[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<Item = Result<String>>,
@@ -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<Item = Result<String>>,
@@ -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<String, Value> {
&self.data
}
/// Converts an `Rc<AttrSet>` to an `Rc<HashMap<String, Value>>` without allocation.
///
/// # Safety
///
/// This is safe because `AttrSet` is `#[repr(transparent)]`.
pub fn into_inner(self: Rc<Self>) -> Rc<HashMap<String, Value>> {
unsafe { core::mem::transmute(self) }
}
/// Creates an `AttrSet` from a `HashMap`.
pub fn from_inner(data: HashMap<String, Value>) -> 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<Value>) -> p::Value {
p::Value::AttrSet(p::AttrSet::new(
self.data

View File

@@ -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<Value>,
/// The lexical scope (stack frame) captured at the time of the initial call.
pub frame: Vec<Value>,
}
@@ -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<Ctx: EvalContext>(
self: &mut Rc<Self>,
mut iter: impl Iterator<Item = Result<Value>> + ExactSizeIterator,

View File

@@ -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<Value>` representing a Nix list.
#[derive(Default)]
pub struct List {
data: Vec<Value>,
@@ -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<Value>`.
pub fn into_inner(self) -> Vec<Value> {
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<Value>) -> PubValue {
PubValue::List(PubList::new(
self.data

View File

@@ -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<Self> {
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<Ctx: EvalContext>(&mut self, mut iter: impl Iterator<Item = Result<Value>> + 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<Ctx: EvalContext>(
&mut self,
mut iter: impl Iterator<Item = Result<Value>> + 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::<Result<_>>()?, 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<Value>) -> PubValue {
use Value::*;
if seen.contains(self) {

View File

@@ -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<Value>,
}
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<Self>,
args: Vec<Value>,

View File

@@ -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,

View File

@@ -58,6 +58,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for Expr {
}
}
/// Downgrades an `assert` expression.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Assert {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let assertion = self.condition().unwrap().downgrade(ctx)?;
@@ -66,6 +67,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Assert {
}
}
/// Downgrades an `if-then-else` expression.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::IfElse {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let cond = self.condition().unwrap().downgrade(ctx)?;
@@ -131,6 +133,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Str {
}
}
/// Downgrades a literal value (integer, float, or URI).
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Literal {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
Ok(ctx.new_expr(match self.kind() {
@@ -144,6 +147,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Literal {
}
}
/// Downgrades an identifier to a variable lookup.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Ident {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let sym = self.ident_token().unwrap().to_string();
@@ -151,6 +155,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Ident {
}
}
/// Downgrades an attribute set.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::AttrSet {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let rec = self.rec_token().is_some();
@@ -160,6 +165,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::AttrSet {
}
}
/// Downgrades a list.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::List {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let mut items = Vec::with_capacity(self.items().size_hint().0);
@@ -170,6 +176,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::List {
}
}
/// Downgrades a binary operation.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::BinOp {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let lhs = self.lhs().unwrap().downgrade(ctx)?;
@@ -179,6 +186,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::BinOp {
}
}
/// Downgrades a "has attribute" (`?`) expression.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::HasAttr {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let lhs = self.expr().unwrap().downgrade(ctx)?;
@@ -187,6 +195,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::HasAttr {
}
}
/// Downgrades a unary operation.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::UnaryOp {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let rhs = self.expr().unwrap().downgrade(ctx)?;
@@ -195,6 +204,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::UnaryOp {
}
}
/// Downgrades an attribute selection (`.`).
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Select {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let expr = self.expr().unwrap().downgrade(ctx)?;
@@ -235,6 +245,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::LegacyLet {
}
}
/// Downgrades a `let ... in ...` expression.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::LetIn {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let body = self.body().unwrap().downgrade(ctx)?;
@@ -243,6 +254,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::LetIn {
}
}
/// Downgrades a `with` expression.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::With {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let namespace = self.namespace().unwrap().downgrade(ctx)?;
@@ -251,6 +263,8 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::With {
}
}
/// Downgrades a lambda (function) expression.
/// This involves desugaring pattern-matching arguments into `let` bindings.
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let param = downgrade_param(self.param().unwrap(), ctx)?;
@@ -261,6 +275,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> 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<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
ellipsis,
alias,
} => {
// Complex case: `{ a, b ? 2, ... }@args: body`
ident = alias.clone();
required = Some(
formals
@@ -279,7 +295,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> 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<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
}
}
let param = ir::Param {
let param = IrParam {
ident,
required,
allowed,

View File

@@ -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<T>(&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<String, ExprId>, 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<Attr>,
@@ -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: ...`.

View File

@@ -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<Param> {
@@ -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)?;

View File

@@ -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<String>,
/// The set of required parameter names for a pattern-matching function.
pub required: Option<Vec<String>>,
/// 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<HashSet<String>>,
}

View File

@@ -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<Ctx: JITContext> {
/// 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<Ctx>, engine: ir::Value, env: ir::Value) -> StackSlot;
}
@@ -24,6 +40,9 @@ impl<Ctx: JITContext> JITCompile<Ctx> for Lir {
}
impl<Ctx: JITContext> JITCompile<Ctx> 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<Ctx>, engine: ir::Value, env: ir::Value) -> StackSlot {
let attrs = ctx.create_attrs();
for (k, v) in self.stcs.iter() {
@@ -35,6 +54,9 @@ impl<Ctx: JITContext> JITCompile<Ctx> for AttrSet {
}
impl<Ctx: JITContext> JITCompile<Ctx> 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<Ctx>, 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<Ctx: JITContext> JITCompile<Ctx> for HasAttr {
}
impl<Ctx: JITContext> JITCompile<Ctx> 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<Ctx>, engine: ir::Value, env: ir::Value) -> StackSlot {
use BinOpKind::*;
let lhs = self.lhs.compile(ctx, engine, env);
@@ -328,6 +355,9 @@ impl<Ctx: JITContext> JITCompile<Ctx> for UnOp {
}
impl<Ctx: JITContext> JITCompile<Ctx> 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<Ctx>, engine: ir::Value, env: ir::Value) -> StackSlot {
use Attr::*;
match self {
@@ -338,6 +368,10 @@ impl<Ctx: JITContext> JITCompile<Ctx> for Attr {
}
impl<Ctx: JITContext> JITCompile<Ctx> 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<Ctx>, 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<Ctx: JITContext> JITCompile<Ctx> for Select {
}
impl<Ctx: JITContext> JITCompile<Ctx> 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<Ctx>, 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<Ctx: JITContext> JITCompile<Ctx> for If {
}
impl<Ctx: JITContext> JITCompile<Ctx> 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<Ctx>, 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<Ctx: JITContext> JITCompile<Ctx> for Call {
}
impl<Ctx: JITContext> JITCompile<Ctx> 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<Ctx>, 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<Ctx: JITContext> JITCompile<Ctx> for ConcatStrings {
}
impl<Ctx: JITContext> JITCompile<Ctx> 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<Ctx>, engine: ir::Value, env: ir::Value) -> StackSlot {
use nixjit_value::Const::*;
let slot = ctx.alloca();
@@ -507,12 +557,18 @@ impl<Ctx: JITContext> JITCompile<Ctx> for Const {
}
impl<Ctx: JITContext> JITCompile<Ctx> for Str {
/// Compiles a string literal to Cranelift IR.
///
/// This creates a string value from the string literal.
fn compile(&self, ctx: &mut Context<Ctx>, engine: ir::Value, env: ir::Value) -> StackSlot {
ctx.create_string(&self.val)
}
}
impl<Ctx: JITContext> JITCompile<Ctx> 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<Ctx>, engine: ir::Value, env: ir::Value) -> StackSlot {
ctx.lookup(env, &self.sym)
}

View File

@@ -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<Ctx: JITContext>(
func: &mut Value,
args_ptr: *mut Value,
@@ -22,6 +31,9 @@ pub extern "C" fn helper_call<Ctx: JITContext>(
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: JITContext>(
ctx: &Ctx,
offset: usize,
@@ -30,6 +42,9 @@ pub extern "C" fn helper_lookup_stack<Ctx: JITContext>(
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: JITContext>(
ctx: &Ctx,
offset: usize,
@@ -38,6 +53,10 @@ pub extern "C" fn helper_lookup_arg<Ctx: JITContext>(
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: JITContext>(
ctx: &Ctx,
sym_ptr: *const u8,
@@ -56,6 +75,10 @@ pub extern "C" fn helper_lookup<Ctx: JITContext>(
}
}
/// 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<Ctx: JITContext>(
val: &mut Value,
path_ptr: *mut Value,
@@ -70,6 +93,10 @@ pub extern "C" fn helper_select<Ctx: JITContext>(
.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<Ctx: JITContext>(
val: &mut Value,
path_ptr: *mut Value,
@@ -88,10 +115,17 @@ pub extern "C" fn helper_select_with_default<Ctx: JITContext>(
.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<Ctx: JITContext>(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<Ctx: JITContext>(
ptr: *const u8,
len: usize,
@@ -104,6 +138,10 @@ pub unsafe extern "C" fn helper_create_string<Ctx: JITContext>(
}
}
/// 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<Ctx: JITContext>(
ptr: *mut Value,
len: usize,
@@ -116,12 +154,19 @@ pub unsafe extern "C" fn helper_create_list<Ctx: JITContext>(
}
}
/// 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<Ctx: JITContext>(
ret: &mut MaybeUninit<HashMap<String, Value>>,
) {
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<Ctx: JITContext>(
attrs: &mut HashMap<String, Value>,
sym_ptr: *const u8,
@@ -136,6 +181,10 @@ pub unsafe extern "C" fn helper_push_attr<Ctx: JITContext>(
}
}
/// 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<Ctx: JITContext>(
attrs: NonNull<HashMap<String, Value>>,
ret: &mut MaybeUninit<Value>,
@@ -145,6 +194,10 @@ pub unsafe extern "C" fn helper_finalize_attrs<Ctx: JITContext>(
));
}
/// 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: JITContext>(
ctx: &mut Ctx,
namespace: NonNull<Value>,
@@ -152,14 +205,24 @@ pub unsafe extern "C" fn helper_enter_with<Ctx: JITContext>(
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: JITContext>(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<Ctx: JITContext>(len: usize) -> *mut u8 {
unsafe { alloc(Layout::array::<Value>(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<Ctx: JITContext>(value: &Value) {
println!("{value:?}")
}

View File

@@ -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<HashMap<String, Value>>);
/// 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<Ctx> = 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<Ctx: JITContext> {
func: F<Ctx>,
strings: HashSet<String>,
@@ -39,10 +68,18 @@ impl<Ctx: JITContext> Deref for JITFunc<Ctx> {
}
}
/// 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<Ctx>,
/// The Cranelift function builder used to generate IR.
pub builder: FunctionBuilder<'ctx>,
/// Stack slots available for reuse.
free_slots: Vec<StackSlot>,
/// String literals used during compilation.
strings: HashSet<String>,
}
@@ -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: JITContext> {
ctx: codegen::Context,
module: JITModule,

View File

@@ -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<T>(&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<Item = (&'a String, &'a ExprId)>,
f: impl FnOnce(&mut Self) -> T,
) -> T;
/// Enters a function parameter scope for the duration of a closure.
fn with_param_env<T>(&mut self, ident: Option<String>, f: impl FnOnce(&mut Self) -> T) -> T;
}
/// A trait for converting (resolving) an HIR node into an LIR expression.
pub trait Resolve<Ctx: ResolveContext> {
/// Performs the resolution.
fn resolve(self, ctx: &mut Ctx) -> Result<Lir>;
}
/// The main entry point for resolving any HIR expression.
impl<Ctx: ResolveContext> Resolve<Ctx> for hir::Hir {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
use hir::Hir::*;
@@ -80,14 +117,21 @@ impl<Ctx: ResolveContext> Resolve<Ctx> 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<Ctx: ResolveContext> Resolve<Ctx> for AttrSet {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
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<Ctx: ResolveContext> Resolve<Ctx> for AttrSet {
}
}
/// Resolves a `List` by resolving each of its items.
impl<Ctx: ResolveContext> Resolve<Ctx> for List {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
for item in self.items.iter() {
@@ -111,18 +156,20 @@ impl<Ctx: ResolveContext> Resolve<Ctx> for List {
}
}
/// Resolves a `HasAttr` expression by resolving the LHS and any dynamic attributes in the path.
impl<Ctx: ResolveContext> Resolve<Ctx> for HasAttr {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
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<Ctx: ResolveContext> Resolve<Ctx> for BinOp {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
ctx.resolve(&self.lhs)?;
@@ -131,6 +178,7 @@ impl<Ctx: ResolveContext> Resolve<Ctx> for BinOp {
}
}
/// Resolves a `UnOp` by resolving its right hand side.
impl<Ctx: ResolveContext> Resolve<Ctx> for UnOp {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
ctx.resolve(&self.rhs)?;
@@ -138,12 +186,14 @@ impl<Ctx: ResolveContext> Resolve<Ctx> 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<Ctx: ResolveContext> Resolve<Ctx> for Select {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
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<Ctx: ResolveContext> Resolve<Ctx> for Select {
}
}
/// Resolves an `If` expression by resolving the condition, consequence, and alternative.
impl<Ctx: ResolveContext> Resolve<Ctx> for If {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
ctx.resolve(&self.cond)?;
@@ -162,6 +213,8 @@ impl<Ctx: ResolveContext> Resolve<Ctx> for If {
}
}
/// Resolves a `Func` by resolving its body within a new parameter scope.
/// It then registers the function with the context.
impl<Ctx: ResolveContext> Resolve<Ctx> for Func {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
ctx.with_param_env(self.param.ident.clone(), |ctx| ctx.resolve(&self.body))?;
@@ -170,6 +223,7 @@ impl<Ctx: ResolveContext> Resolve<Ctx> for Func {
}
}
/// Resolves a `Call` by resolving the function and all of its arguments.
impl<Ctx: ResolveContext> Resolve<Ctx> for Call {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
ctx.resolve(&self.func)?;
@@ -180,11 +234,15 @@ impl<Ctx: ResolveContext> Resolve<Ctx> for Call {
}
}
/// Resolves a `With` expression by resolving the namespace and the body.
/// The body is resolved within a special "with" scope.
impl<Ctx: ResolveContext> Resolve<Ctx> for With {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
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<Ctx: ResolveContext> Resolve<Ctx> for With {
}
}
/// Resolves an `Assert` by resolving the assertion condition and the body.
impl<Ctx: ResolveContext> Resolve<Ctx> for Assert {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
ctx.resolve(&self.assertion)?;
@@ -201,6 +260,7 @@ impl<Ctx: ResolveContext> Resolve<Ctx> for Assert {
}
}
/// Resolves a `ConcatStrings` by resolving each part.
impl<Ctx: ResolveContext> Resolve<Ctx> for ConcatStrings {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
for part in self.parts.iter() {
@@ -210,6 +270,7 @@ impl<Ctx: ResolveContext> Resolve<Ctx> for ConcatStrings {
}
}
/// Resolves a `Var` by looking it up in the current context.
impl<Ctx: ResolveContext> Resolve<Ctx> for Var {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
use LookupResult::*;
@@ -225,6 +286,7 @@ impl<Ctx: ResolveContext> Resolve<Ctx> for Var {
}
}
/// Resolves a `Path` by resolving the underlying expression that defines the path's content.
impl<Ctx: ResolveContext> Resolve<Ctx> for Path {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
ctx.resolve(&self.expr)?;
@@ -232,6 +294,8 @@ impl<Ctx: ResolveContext> Resolve<Ctx> 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<Ctx: ResolveContext> Resolve<Ctx> for hir::Let {
fn resolve(self, ctx: &mut Ctx) -> Result<Lir> {
ctx.with_let_env(self.bindings.iter(), |ctx| {
@@ -240,6 +304,7 @@ impl<Ctx: ResolveContext> Resolve<Ctx> for hir::Let {
}
ctx.resolve(&self.body)
})?;
// The `let` expression itself evaluates to its body.
Ok(Lir::ExprRef(self.body))
}
}

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)

View File

@@ -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<f64> 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<T: Into<String>> From<T> 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>>) -> 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<Regex> =
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<Symbol, Value>,
@@ -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<Value>,
@@ -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}"),

View File

@@ -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
];
};
}