chore: comment
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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!()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: ...`.
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:?}")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 )*
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>,
|
||||
@@ -126,10 +147,11 @@ impl Debug for AttrSet {
|
||||
use Value::*;
|
||||
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, " }}")
|
||||
@@ -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, " }}")
|
||||
}
|
||||
}
|
||||
|
||||
/// 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}"),
|
||||
|
||||
@@ -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
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user