553 lines
19 KiB
Rust
553 lines
19 KiB
Rust
use std::borrow::Cow;
|
|
use std::collections::BTreeMap;
|
|
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
|
|
use std::ops::{Deref, DerefMut};
|
|
|
|
use gc_arena::Collect;
|
|
use num_enum::TryFromPrimitive;
|
|
|
|
macro_rules! define_builtins {
|
|
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
|
|
const BUILTINS: &[(&str, u8)] = &[
|
|
$(($name, $arity),)*
|
|
];
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, Collect)]
|
|
#[repr(u8)]
|
|
#[collect(require_static)]
|
|
pub enum BuiltinId {
|
|
$($variant,)*
|
|
}
|
|
|
|
impl BuiltinId {
|
|
pub const ALL: [Self; BUILTINS.len()] = [$(Self::$variant,)*];
|
|
}
|
|
};
|
|
}
|
|
|
|
define_builtins! {
|
|
("abort", Abort, 1),
|
|
("__add", Add, 2),
|
|
("__addErrorContext", AddErrorContext, 2),
|
|
("__all", All, 2),
|
|
("__any", Any, 2),
|
|
("__appendContext", AppendContext, 2),
|
|
("__attrNames", AttrNames, 1),
|
|
("__attrValues", AttrValues, 1),
|
|
("baseNameOf", BaseNameOf, 1),
|
|
("__bitAnd", BitAnd, 2),
|
|
("__bitOr", BitOr, 2),
|
|
("__bitXor", BitXor, 2),
|
|
("break", Break, 1),
|
|
("__catAttrs", CatAttrs, 2),
|
|
("__ceil", Ceil, 1),
|
|
("__compareVersions", CompareVersions, 2),
|
|
("__concatLists", ConcatLists, 1),
|
|
("__concatMap", ConcatMap, 2),
|
|
("__concatStringsSep", ConcatStringsSep, 2),
|
|
("__convertHash", ConvertHash, 1),
|
|
("__deepSeq", DeepSeq, 2),
|
|
("derivation", Derivation, 1),
|
|
("derivationStrict", DerivationStrict, 1),
|
|
("dirOf", DirOf, 1),
|
|
("__div", Div, 2),
|
|
("__elem", Elem, 2),
|
|
("__elemAt", ElemAt, 2),
|
|
("fetchGit", FetchGit, 1),
|
|
("fetchMercurial", FetchMercurial, 1),
|
|
("fetchTarball", FetchTarball, 1),
|
|
("fetchTree", FetchTree, 1),
|
|
("__fetchurl", FetchUrl, 1),
|
|
("__filter", Filter, 2),
|
|
("__filterSource", FilterSource, 2),
|
|
("__findFile", FindFile, 2),
|
|
("__floor", Floor, 1),
|
|
("__foldl'", FoldlStrict, 3),
|
|
("__fromJSON", FromJSON, 1),
|
|
("fromTOML", FromTOML, 1),
|
|
("__functionArgs", FunctionArgs, 1),
|
|
("__genList", GenList, 2),
|
|
("__genericClosure", GenericClosure, 1),
|
|
("__getAttr", GetAttr, 2),
|
|
("__getContext", GetContext, 1),
|
|
("__getEnv", GetEnv, 1),
|
|
("__groupBy", GroupBy, 2),
|
|
("__hasAttr", HasAttr, 2),
|
|
("__hasContext", HasContext, 1),
|
|
("__hashFile", HashFile, 2),
|
|
("__hashString", HashString, 2),
|
|
("__head", Head, 1),
|
|
("import", Import, 1),
|
|
("__intersectAttrs", IntersectAttrs, 2),
|
|
("__isAttrs", IsAttrs, 1),
|
|
("__isBool", IsBool, 1),
|
|
("__isFloat", IsFloat, 1),
|
|
("__isFunction", IsFunction, 1),
|
|
("__isInt", IsInt, 1),
|
|
("__isList", IsList, 1),
|
|
("isNull", IsNull, 1),
|
|
("__isPath", IsPath, 1),
|
|
("__isString", IsString, 1),
|
|
("__length", Length, 1),
|
|
("__lessThan", LessThan, 2),
|
|
("__listToAttrs", ListToAttrs, 1),
|
|
("map", Map, 2),
|
|
("__mapAttrs", MapAttrs, 2),
|
|
("__match", Match, 2),
|
|
("__mul", Mul, 2),
|
|
("__parseDrvName", ParseDrvName, 1),
|
|
("__partition", Partition, 2),
|
|
("__path", Path, 1),
|
|
("__pathExists", PathExists, 1),
|
|
("placeholder", Placeholder, 1),
|
|
("__readDir", ReadDir, 1),
|
|
("__readFile", ReadFile, 1),
|
|
("__readFileType", ReadFileType, 1),
|
|
("removeAttrs", RemoveAttrs, 2),
|
|
("__replaceStrings", ReplaceStrings, 3),
|
|
("scopedImport", ScopedImport, 2),
|
|
("__seq", Seq, 2),
|
|
("__sort", Sort, 2),
|
|
("__split", Split, 2),
|
|
("__splitVersion", SplitVersion, 1),
|
|
("__storePath", StorePath, 1),
|
|
("__stringLength", StringLength, 1),
|
|
("__sub", Sub, 2),
|
|
("__substring", Substring, 3),
|
|
("__tail", Tail, 1),
|
|
("throw", Throw, 1),
|
|
("__toFile", ToFile, 2),
|
|
("__toJSON", ToJSON, 1),
|
|
("__toPath", ToPath, 1),
|
|
("toString", ToString, 1),
|
|
("__toXML", ToXML, 1),
|
|
("__trace", Trace, 2),
|
|
("__tryEval", TryEval, 1),
|
|
("__typeOf", TypeOf, 1),
|
|
("__unsafeDiscardStringContext", UnsafeDiscardStringContext, 1),
|
|
("__unsafeDiscardOutputDependency", UnsafeDiscardOutputDependency, 1),
|
|
("__unsafeGetAttrPos", UnsafeGetAttrPos, 2),
|
|
("__warn", Warn, 2),
|
|
("__zipAttrsWith", ZipAttrsWith, 2),
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub struct BuiltinInfo {
|
|
pub name: &'static str,
|
|
pub global_name: &'static str,
|
|
pub global: bool,
|
|
pub arity: u8,
|
|
}
|
|
|
|
impl BuiltinId {
|
|
pub const TOTAL: usize = BUILTINS.len();
|
|
|
|
#[expect(
|
|
clippy::indexing_slicing,
|
|
reason = "a `BuiltinId` discriminant is always a valid index into the `BUILTINS` table"
|
|
)]
|
|
#[inline(always)]
|
|
pub fn info(self) -> BuiltinInfo {
|
|
let (global_name, arity) = BUILTINS[self as usize];
|
|
let (name, global) = global_name
|
|
.strip_prefix("__")
|
|
.map_or((global_name, true), |name| (name, false));
|
|
BuiltinInfo {
|
|
name,
|
|
global_name,
|
|
global,
|
|
arity,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[repr(transparent)]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)]
|
|
#[collect(require_static)]
|
|
pub struct StringId(pub string_interner::symbol::SymbolU32);
|
|
|
|
/// Represents a Nix symbol, which is used as a key in attribute sets.
|
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct Symbol<'a>(Cow<'a, str>);
|
|
|
|
pub type StaticSymbol = Symbol<'static>;
|
|
|
|
impl From<String> for Symbol<'_> {
|
|
fn from(value: String) -> Self {
|
|
Symbol(Cow::Owned(value))
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a str> for Symbol<'a> {
|
|
fn from(value: &'a str) -> Self {
|
|
Symbol(Cow::Borrowed(value))
|
|
}
|
|
}
|
|
|
|
/// 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 Symbol::NORMAL_REGEX.test(&sym) {
|
|
sym
|
|
} else {
|
|
Cow::Owned(escape_quote_string(&sym))
|
|
}
|
|
}
|
|
|
|
impl Display for Symbol<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
if self.normal() {
|
|
write!(f, "{}", self.0)
|
|
} else {
|
|
write!(f, "{}", escape_quote_string(&self.0))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Symbol<'_> {
|
|
const NORMAL_REGEX: ere::Regex<1> = ere::compile_regex!("^[a-zA-Z_][a-zA-Z0-9_'-]*$");
|
|
/// Checks if the symbol is a "normal" identifier that doesn't require quotes.
|
|
fn normal(&self) -> bool {
|
|
Self::NORMAL_REGEX.test(self)
|
|
}
|
|
}
|
|
|
|
impl Deref for Symbol<'_> {
|
|
type Target = str;
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
fn escape_quote_string(s: &str) -> String {
|
|
let mut ret = String::with_capacity(s.len() + 2);
|
|
ret.push('"');
|
|
let mut iter = s.chars().peekable();
|
|
while let Some(c) = iter.next() {
|
|
match c {
|
|
'\\' => ret.push_str("\\\\"),
|
|
'"' => ret.push_str("\\\""),
|
|
'\n' => ret.push_str("\\n"),
|
|
'\r' => ret.push_str("\\r"),
|
|
'\t' => ret.push_str("\\t"),
|
|
'$' if iter.peek() == Some(&'{') => ret.push_str("\\$"),
|
|
c => ret.push(c),
|
|
}
|
|
}
|
|
ret.push('"');
|
|
ret
|
|
}
|
|
|
|
/// Represents a Nix attribute set, which is a map from symbols to values.
|
|
#[derive(Default, Clone, PartialEq)]
|
|
pub struct AttrSet {
|
|
data: BTreeMap<StaticSymbol, Value>,
|
|
}
|
|
|
|
impl AttrSet {
|
|
pub fn new(data: BTreeMap<StaticSymbol, Value>) -> Self {
|
|
Self { data }
|
|
}
|
|
|
|
/// Gets a value by key (string or Symbol).
|
|
pub fn get<'a, 'sym: 'a>(&'a self, key: impl Into<Symbol<'sym>>) -> Option<&'a Value> {
|
|
self.data.get(&key.into())
|
|
}
|
|
|
|
/// Checks if a key exists in the attribute set.
|
|
pub fn contains_key<'a, 'sym: 'a>(&'a self, key: impl Into<Symbol<'sym>>) -> bool {
|
|
self.data.contains_key(&key.into())
|
|
}
|
|
}
|
|
|
|
impl Deref for AttrSet {
|
|
type Target = BTreeMap<StaticSymbol, Value>;
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.data
|
|
}
|
|
}
|
|
impl DerefMut for AttrSet {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.data
|
|
}
|
|
}
|
|
|
|
impl Debug for AttrSet {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
use Value::*;
|
|
write!(f, "{{")?;
|
|
for (k, v) in self.data.iter() {
|
|
write!(f, " {k:?} = ")?;
|
|
match v {
|
|
List(_) => write!(f, "[ ... ];")?,
|
|
AttrSet(_) => write!(f, "{{ ... }};")?,
|
|
v => write!(f, "{v:?};")?,
|
|
}
|
|
}
|
|
write!(f, " }}")
|
|
}
|
|
}
|
|
|
|
impl Display for AttrSet {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
use Value::*;
|
|
if self.data.len() > 1 {
|
|
writeln!(f, "{{")?;
|
|
for (k, v) in self.data.iter() {
|
|
write!(f, " {k} = ")?;
|
|
match v {
|
|
List(_) => writeln!(f, "[ ... ];")?,
|
|
AttrSet(_) => writeln!(f, "{{ ... }};")?,
|
|
v => writeln!(f, "{v};")?,
|
|
}
|
|
}
|
|
write!(f, "}}")
|
|
} else {
|
|
write!(f, "{{")?;
|
|
for (k, v) in self.data.iter() {
|
|
write!(f, " {k} = ")?;
|
|
match v {
|
|
List(_) => write!(f, "[ ... ];")?,
|
|
AttrSet(_) => write!(f, "{{ ... }};")?,
|
|
v => write!(f, "{v};")?,
|
|
}
|
|
}
|
|
write!(f, " }}")
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AttrSet {
|
|
pub fn display_compat(&self) -> AttrSetCompatDisplay<'_> {
|
|
AttrSetCompatDisplay(self)
|
|
}
|
|
}
|
|
|
|
pub struct AttrSetCompatDisplay<'a>(&'a AttrSet);
|
|
|
|
impl Display for AttrSetCompatDisplay<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
write!(f, "{{")?;
|
|
for (k, v) in self.0.data.iter() {
|
|
write!(f, " {k} = {};", v.display_compat())?;
|
|
}
|
|
write!(f, " }}")
|
|
}
|
|
}
|
|
|
|
/// Represents a Nix list, which is a vector of values.
|
|
#[derive(Default, Clone, Debug, PartialEq)]
|
|
pub struct List {
|
|
data: Vec<Value>,
|
|
}
|
|
|
|
impl List {
|
|
pub fn new(data: Vec<Value>) -> Self {
|
|
Self { data }
|
|
}
|
|
}
|
|
|
|
impl Deref for List {
|
|
type Target = Vec<Value>;
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.data
|
|
}
|
|
}
|
|
impl DerefMut for List {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.data
|
|
}
|
|
}
|
|
|
|
impl Display for List {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
use Value::*;
|
|
if self.data.len() > 1 {
|
|
writeln!(f, "[")?;
|
|
for v in self.data.iter() {
|
|
match v {
|
|
List(_) => writeln!(f, " [ ... ]")?,
|
|
AttrSet(_) => writeln!(f, " {{ ... }}")?,
|
|
v => writeln!(f, " {v}")?,
|
|
}
|
|
}
|
|
write!(f, "]")
|
|
} else {
|
|
write!(f, "[ ")?;
|
|
for v in self.data.iter() {
|
|
match v {
|
|
List(_) => write!(f, "[ ... ] ")?,
|
|
AttrSet(_) => write!(f, "{{ ... }} ")?,
|
|
v => write!(f, "{v} ")?,
|
|
}
|
|
}
|
|
write!(f, "]")
|
|
}
|
|
}
|
|
}
|
|
|
|
impl List {
|
|
pub fn display_compat(&self) -> ListCompatDisplay<'_> {
|
|
ListCompatDisplay(self)
|
|
}
|
|
}
|
|
|
|
pub struct ListCompatDisplay<'a>(&'a List);
|
|
|
|
impl Display for ListCompatDisplay<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
write!(f, "[ ")?;
|
|
for v in self.0.data.iter() {
|
|
write!(f, "{} ", v.display_compat())?;
|
|
}
|
|
write!(f, "]")
|
|
}
|
|
}
|
|
|
|
/// Represents any possible Nix value that can be returned from an evaluation.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum Value {
|
|
/// An integer value.
|
|
Int(i64),
|
|
/// An floating-point value.
|
|
Float(f64),
|
|
/// An boolean value.
|
|
Bool(bool),
|
|
/// An null value.
|
|
Null,
|
|
/// A string value.
|
|
String(String),
|
|
/// A path value (absolute path string).
|
|
Path(String),
|
|
/// An attribute set.
|
|
AttrSet(AttrSet),
|
|
/// A list.
|
|
List(List),
|
|
/// 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,
|
|
}
|
|
|
|
/// Wrapper to format a float in Nix style (C printf `%g` with precision 6).
|
|
pub struct NixFloat(pub f64);
|
|
|
|
impl Display for NixFloat {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
fmt_nix_float(f, self.0)
|
|
}
|
|
}
|
|
|
|
/// Format a float matching C's `printf("%g", x)` with default precision 6.
|
|
fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult {
|
|
if !x.is_finite() {
|
|
return write!(f, "{x}");
|
|
}
|
|
if x == 0.0 {
|
|
return if x.is_sign_negative() {
|
|
write!(f, "-0")
|
|
} else {
|
|
write!(f, "0")
|
|
};
|
|
}
|
|
|
|
let precision: i32 = 6;
|
|
let exp = x.abs().log10().floor() as i32;
|
|
|
|
#[expect(
|
|
clippy::cast_sign_loss,
|
|
reason = "this branch runs only when exp < precision, so precision-1-exp and precision-1 are non-negative"
|
|
)]
|
|
let formatted = if exp >= -4 && exp < precision {
|
|
let decimal_places = (precision - 1 - exp) as usize;
|
|
format!("{x:.decimal_places$}")
|
|
} else {
|
|
let sig_digits = (precision - 1) as usize;
|
|
let s = format!("{x:.sig_digits$e}");
|
|
let (mantissa, exp_part) = s
|
|
.split_once('e')
|
|
.expect("scientific notation must contain 'e'");
|
|
let (sign, digits) = if let Some(d) = exp_part.strip_prefix('-') {
|
|
("-", d)
|
|
} else if let Some(d) = exp_part.strip_prefix('+') {
|
|
("+", d)
|
|
} else {
|
|
("+", exp_part)
|
|
};
|
|
if digits.len() < 2 {
|
|
format!("{mantissa}e{sign}0{digits}")
|
|
} else {
|
|
format!("{mantissa}e{sign}{digits}")
|
|
}
|
|
};
|
|
|
|
if formatted.contains('.') {
|
|
if let Some((head, tail)) = formatted.split_once('e') {
|
|
let trimmed = head.trim_end_matches('0').trim_end_matches('.');
|
|
write!(f, "{trimmed}e{tail}")
|
|
} else {
|
|
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
|
|
write!(f, "{trimmed}")
|
|
}
|
|
} else {
|
|
write!(f, "{formatted}")
|
|
}
|
|
}
|
|
|
|
impl Display for Value {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
use Value::*;
|
|
match self {
|
|
&Int(x) => write!(f, "{x}"),
|
|
&Float(x) => fmt_nix_float(f, x),
|
|
&Bool(x) => write!(f, "{x}"),
|
|
Null => write!(f, "null"),
|
|
String(x) => write!(f, "{}", escape_quote_string(x)),
|
|
Path(x) => write!(f, "{x}"),
|
|
AttrSet(x) => write!(f, "{x}"),
|
|
List(x) => write!(f, "{x}"),
|
|
Thunk => write!(f, "«code»"),
|
|
Func => write!(f, "«lambda»"),
|
|
PrimOp(name) => write!(f, "«primop {name}»"),
|
|
PrimOpApp(name) => write!(f, "«partially applied primop {name}»"),
|
|
Repeated => write!(f, "«repeated»"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Value {
|
|
pub fn display_compat(&self) -> ValueCompatDisplay<'_> {
|
|
ValueCompatDisplay(self)
|
|
}
|
|
}
|
|
|
|
pub struct ValueCompatDisplay<'a>(&'a Value);
|
|
|
|
impl Display for ValueCompatDisplay<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
use Value::*;
|
|
match self.0 {
|
|
&Int(x) => write!(f, "{x}"),
|
|
&Float(x) => fmt_nix_float(f, x),
|
|
&Bool(x) => write!(f, "{x}"),
|
|
Null => write!(f, "null"),
|
|
String(x) => write!(f, "{}", escape_quote_string(x)),
|
|
Path(x) => write!(f, "{x}"),
|
|
AttrSet(x) => write!(f, "{}", x.display_compat()),
|
|
List(x) => write!(f, "{}", x.display_compat()),
|
|
Thunk => write!(f, "«thunk»"),
|
|
Func => write!(f, "<LAMBDA>"),
|
|
PrimOp(_) => write!(f, "<PRIMOP>"),
|
|
PrimOpApp(_) => write!(f, "<PRIMOP-APP>"),
|
|
Repeated => write!(f, "«repeated»"),
|
|
}
|
|
}
|
|
}
|