init builtins

This commit is contained in:
2026-03-16 18:03:40 +08:00
parent 198d847151
commit 1950d4de6c
22 changed files with 2197 additions and 665 deletions
Generated
+1 -8
View File
@@ -188,13 +188,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "boxing"
version = "0.1.3"
dependencies = [
"sptr",
]
[[package]]
name = "bstr"
version = "1.12.1"
@@ -798,7 +791,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"boxing",
"bumpalo",
"bzip2",
"clap",
@@ -833,6 +825,7 @@ dependencies = [
"sha2",
"small-map",
"smallvec",
"sptr",
"string-interner",
"tap",
"tar",
-1
View File
@@ -2,7 +2,6 @@
resolver = "3"
members = [
"fix",
"boxing",
]
[profile.profiling]
-8
View File
@@ -1,8 +0,0 @@
[package]
name = "boxing"
version = "0.1.3"
edition = "2021"
description = "NaN-boxing primitives (local fork with bool fix)"
[dependencies]
sptr = "0.3"
-2
View File
@@ -1,2 +0,0 @@
pub mod nan;
mod utils;
-7
View File
@@ -1,7 +0,0 @@
pub mod raw;
pub use raw::RawBox;
const SIGN_MASK: u64 = 0x7FFF_FFFF_FFFF_FFFF;
const QUIET_NAN: u64 = 0x7FF8_0000_0000_0000;
const NEG_QUIET_NAN: u64 = 0xFFF8_0000_0000_0000;
-16
View File
@@ -1,16 +0,0 @@
pub trait ArrayExt<const LEN: usize> {
type Elem;
fn truncate_to<const M: usize>(self) -> [Self::Elem; M];
}
impl<T: Default + Copy, const N: usize> ArrayExt<N> for [T; N] {
type Elem = T;
fn truncate_to<const M: usize>(self) -> [Self::Elem; M] {
let copy_len = usize::min(N, M);
let mut out = [T::default(); M];
out[0..copy_len].copy_from_slice(&self[0..copy_len]);
out
}
}
+1 -1
View File
@@ -79,7 +79,7 @@ tap = "1.0.1"
ghost-cell = "0.2"
colored = "3.1"
boxing = { path = "../boxing" }
sptr = "0.3"
sealed = "0.6"
small-map = "0.1"
smallvec = "1.15"
+53 -46
View File
@@ -1,11 +1,28 @@
use super::{NEG_QUIET_NAN, QUIET_NAN, SIGN_MASK};
use crate::utils::ArrayExt;
use sptr::Strict;
use std::fmt;
use std::mem::ManuallyDrop;
use std::num::NonZeroU8;
pub trait RawStore: Sized {
use sptr::Strict;
const SIGN_MASK: u64 = 0x7FFF_FFFF_FFFF_FFFF;
const QUIET_NAN: u64 = 0x7FF8_0000_0000_0000;
const NEG_QUIET_NAN: u64 = 0xFFF8_0000_0000_0000;
pub(crate) trait ArrayExt<const LEN: usize> {
type Elem;
fn truncate_to<const M: usize>(self) -> [Self::Elem; M];
}
impl<T: Default + Copy, const N: usize> ArrayExt<N> for [T; N] {
type Elem = T;
fn truncate_to<const M: usize>(self) -> [Self::Elem; M] {
let copy_len = usize::min(N, M);
let mut out = [T::default(); M];
out[0..copy_len].copy_from_slice(&self[0..copy_len]);
out
}
}
pub(crate) trait RawStore: Sized {
fn to_val(self, value: &mut Value);
fn from_val(value: &Value) -> Self;
}
@@ -138,18 +155,18 @@ enum TagVal {
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct RawTag(TagVal);
pub(crate) struct RawTag(TagVal);
impl RawTag {
#[inline]
#[must_use]
pub fn new(neg: bool, val: NonZeroU8) -> RawTag {
pub(crate) fn new(neg: bool, val: NonZeroU8) -> RawTag {
unsafe { Self::new_unchecked(neg, val.get() & 0x07) }
}
#[inline]
#[must_use]
pub fn new_checked(neg: bool, val: u8) -> Option<RawTag> {
pub(crate) fn new_checked(neg: bool, val: u8) -> Option<RawTag> {
Some(RawTag(match (neg, val) {
(false, 1) => TagVal::_P1,
(false, 2) => TagVal::_P2,
@@ -176,7 +193,7 @@ impl RawTag {
/// `val` must be in the range `1..8`
#[inline]
#[must_use]
pub unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag {
pub(crate) unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag {
RawTag(match (neg, val) {
(false, 1) => TagVal::_P1,
(false, 2) => TagVal::_P2,
@@ -200,7 +217,7 @@ impl RawTag {
#[inline]
#[must_use]
pub fn is_neg(self) -> bool {
pub(crate) fn is_neg(self) -> bool {
matches!(self.0, |TagVal::_N1| TagVal::_N2
| TagVal::_N3
| TagVal::_N4
@@ -211,7 +228,7 @@ impl RawTag {
#[inline]
#[must_use]
pub fn val(self) -> NonZeroU8 {
pub(crate) fn val(self) -> NonZeroU8 {
match self.0 {
TagVal::_P1 | TagVal::_N1 => NonZeroU8::MIN,
TagVal::_P2 | TagVal::_N2 => NonZeroU8::MIN.saturating_add(1),
@@ -225,7 +242,7 @@ impl RawTag {
#[inline]
#[must_use]
pub fn neg_val(self) -> (bool, u8) {
pub(crate) fn neg_val(self) -> (bool, u8) {
match self.0 {
TagVal::_P1 => (false, 1),
TagVal::_P2 => (false, 2),
@@ -286,9 +303,9 @@ impl Header {
}
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Copy, Clone, Debug, PartialEq)]
#[repr(C, align(8))]
pub struct Value {
pub(crate) struct Value {
#[cfg(target_endian = "big")]
header: Header,
data: [u8; 6],
@@ -298,7 +315,7 @@ pub struct Value {
impl Value {
#[inline]
pub fn new(tag: RawTag, data: [u8; 6]) -> Value {
pub(crate) fn new(tag: RawTag, data: [u8; 6]) -> Value {
Value {
header: Header::new(tag),
data,
@@ -306,23 +323,23 @@ impl Value {
}
#[inline]
pub fn empty(tag: RawTag) -> Value {
pub(crate) fn empty(tag: RawTag) -> Value {
Value::new(tag, [0; 6])
}
pub fn store<T: RawStore>(tag: RawTag, val: T) -> Value {
pub(crate) fn store<T: RawStore>(tag: RawTag, val: T) -> Value {
let mut v = Value::new(tag, [0; 6]);
T::to_val(val, &mut v);
v
}
pub fn load<T: RawStore>(self) -> T {
pub(crate) fn load<T: RawStore>(self) -> T {
T::from_val(&self)
}
#[inline]
#[must_use]
pub fn tag(&self) -> RawTag {
pub(crate) fn tag(&self) -> RawTag {
self.header.tag()
}
@@ -332,41 +349,42 @@ impl Value {
}
#[inline]
pub fn set_data(&mut self, val: [u8; 6]) {
pub(crate) fn set_data(&mut self, val: [u8; 6]) {
self.data = val;
}
#[inline]
#[must_use]
pub fn data(&self) -> &[u8; 6] {
pub(crate) fn data(&self) -> &[u8; 6] {
&self.data
}
#[inline]
#[must_use]
pub fn data_mut(&mut self) -> &mut [u8; 6] {
pub(crate) fn data_mut(&mut self) -> &mut [u8; 6] {
&mut self.data
}
#[inline]
#[must_use]
pub unsafe fn whole(&self) -> &[u8; 8] {
unsafe fn whole(&self) -> &[u8; 8] {
let ptr = (self as *const Value).cast::<[u8; 8]>();
unsafe { &*ptr }
}
#[inline]
#[must_use]
pub unsafe fn whole_mut(&mut self) -> &mut [u8; 8] {
unsafe fn whole_mut(&mut self) -> &mut [u8; 8] {
let ptr = (self as *mut Value).cast::<[u8; 8]>();
unsafe { &mut *ptr }
}
}
#[repr(C)]
pub union RawBox {
#[derive(Copy, Clone)]
pub(crate) union RawBox {
float: f64,
value: ManuallyDrop<Value>,
value: Value,
bits: u64,
#[cfg(target_pointer_width = "64")]
ptr: *const (),
@@ -377,7 +395,7 @@ pub union RawBox {
impl RawBox {
#[inline]
#[must_use]
pub fn from_float(val: f64) -> RawBox {
pub(crate) fn from_float(val: f64) -> RawBox {
match (val.is_nan(), val.is_sign_positive()) {
(true, true) => RawBox {
float: f64::from_bits(QUIET_NAN),
@@ -391,15 +409,13 @@ impl RawBox {
#[inline]
#[must_use]
pub fn from_value(value: Value) -> RawBox {
RawBox {
value: ManuallyDrop::new(value),
}
pub(crate) fn from_value(value: Value) -> RawBox {
RawBox { value }
}
#[inline]
#[must_use]
pub fn tag(&self) -> Option<RawTag> {
pub(crate) fn tag(&self) -> Option<RawTag> {
if self.is_value() {
Some(unsafe { self.value.tag() })
} else {
@@ -409,19 +425,19 @@ impl RawBox {
#[inline]
#[must_use]
pub fn is_float(&self) -> bool {
pub(crate) fn is_float(&self) -> bool {
(unsafe { !self.float.is_nan() } || unsafe { self.bits & SIGN_MASK == QUIET_NAN })
}
#[inline]
#[must_use]
pub fn is_value(&self) -> bool {
pub(crate) fn is_value(&self) -> bool {
(unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN })
}
#[inline]
#[must_use]
pub fn float(&self) -> Option<&f64> {
pub(crate) fn float(&self) -> Option<&f64> {
if self.is_float() {
Some(unsafe { &self.float })
} else {
@@ -431,7 +447,7 @@ impl RawBox {
#[inline]
#[must_use]
pub fn value(&self) -> Option<&Value> {
pub(crate) fn value(&self) -> Option<&Value> {
if self.is_value() {
Some(unsafe { &self.value })
} else {
@@ -440,20 +456,11 @@ impl RawBox {
}
#[inline]
pub fn into_float_unchecked(self) -> f64 {
pub(crate) fn into_float_unchecked(self) -> f64 {
unsafe { self.float }
}
}
impl Clone for RawBox {
#[inline]
fn clone(&self) -> Self {
RawBox {
ptr: unsafe { self.ptr },
}
}
}
impl fmt::Debug for RawBox {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.float() {
+2 -5
View File
@@ -125,8 +125,6 @@ pub enum Error {
#[label("error occurred here")]
span: Option<SourceSpan>,
message: String,
#[help]
js_backtrace: Option<String>,
#[related]
stack_trace: Vec<StackFrame>,
},
@@ -163,12 +161,11 @@ impl Error {
.into()
}
pub fn eval_error(msg: String, backtrace: Option<String>) -> Box<Self> {
pub fn eval_error(msg: impl Into<String>) -> Box<Self> {
Error::EvalError {
src: None,
span: None,
message: msg,
js_backtrace: backtrace,
message: msg.into(),
stack_trace: Vec::new(),
}
.into()
+1
View File
@@ -1,6 +1,7 @@
#![warn(clippy::unwrap_used)]
#![allow(dead_code)]
mod boxing;
pub mod error;
pub mod logging;
pub mod runtime;
+3 -3
View File
@@ -10,7 +10,7 @@ use rustyline::DefaultEditor;
use rustyline::error::ReadlineError;
#[derive(Parser)]
#[command(name = "nix-js", about = "Nix expression evaluator")]
#[command(name = "fix", about = "Nix expression evaluator")]
struct Cli {
#[command(subcommand)]
command: Command,
@@ -40,8 +40,8 @@ struct ExprSource {
file: Option<PathBuf>,
}
fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<()> {
let src = if let Some(expr) = src.expr {
fn run_compile(_runtime: &mut Runtime, src: ExprSource, _silent: bool) -> Result<()> {
let _src = if let Some(expr) = src.expr {
Source::new_eval(expr)?
} else if let Some(file) = src.file {
Source::new_file(file)?
+13 -5
View File
@@ -16,6 +16,7 @@ use crate::store::{DaemonStore, StoreConfig};
use crate::value::Symbol;
mod builtins;
mod primops;
mod stack;
mod value;
mod vm;
@@ -43,6 +44,7 @@ impl Runtime {
let store = DaemonStore::connect(&config.daemon_socket)?;
Ok(Self {
arena: Arena::new(|mc| VM::new(mc, &mut strings)),
global_env,
store,
strings,
@@ -50,7 +52,6 @@ impl Runtime {
bytecode: Vec::new(),
sources: Vec::new(),
spans: Vec::new(),
arena: Arena::new(|mc| VM::new(mc)),
})
}
@@ -108,8 +109,11 @@ impl Runtime {
bump,
token,
strings,
source: sources.last().unwrap().clone(),
scopes: [Scope::Global(global_env)].into_iter().chain(extra_scope.into_iter()).collect(),
source: sources.last().expect("no current source").clone(),
scopes: [Scope::Global(global_env)]
.into_iter()
.chain(extra_scope)
.collect(),
with_scope_count: 0,
arg_count: 0,
thunk_count,
@@ -117,7 +121,11 @@ impl Runtime {
}
}
fn downgrade<'a>(&'a mut self, source: Source, extra_scope: Option<Scope<'a>>) -> Result<OwnedIr> {
fn downgrade<'a>(
&'a mut self,
source: Source,
extra_scope: Option<Scope<'a>>,
) -> Result<OwnedIr> {
tracing::debug!("Parsing Nix expression");
self.sources.push(source.clone());
@@ -496,7 +504,7 @@ impl OwnedIr {
unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self {
Self {
_bump: bump,
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) }
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) },
}
}
+196
View File
@@ -1,11 +1,204 @@
use gc_arena::Collect;
use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use string_interner::DefaultStringInterner;
use super::value::*;
use crate::ir::{Ir, RawIrRef, StringId};
/// Generates both the BUILTINS const table and the BuiltinId enum
/// from a single source of truth, preventing index desync.
macro_rules! define_builtins {
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
/// Builtin function registry.
/// Array index IS the PrimOp id. (name, arity) pairs.
pub(super) const BUILTINS: &[(&str, u8)] = &[
$(($name, $arity),)*
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive, Collect)]
#[repr(u8)]
#[collect(require_static)]
pub(super) enum BuiltinId {
$($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),
("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),
("null", Null, 0), // constant, not a function
("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),
("unsafeGetAttrPos", UnsafeGetAttrPos, 2),
("warn", Warn, 2),
("zipAttrsWith", ZipAttrsWith, 2),
("break", Break, 1),
}
/// Names that need to be pre-interned for builtin implementations.
const EXTRA_INTERN_NAMES: &[&str] = &[
"builtins",
"currentSystem",
"langVersion",
"nixVersion",
"storeDir",
"nixPath",
"true",
"false",
// typeOf return values
"int",
"float",
"bool",
"string",
"path",
"null",
"set",
"list",
"lambda",
// attrset keys used by builtins
"name",
"value",
"success",
"right",
"wrong",
"key",
"operator",
"startSet",
"__toString",
"outPath",
"__functor",
"drvPath",
"type",
"derivation",
"version",
];
/// Returns true if this builtin has lazy argument semantics
/// (not all args should be forced before dispatch).
pub(super) fn is_lazy_builtin(id: BuiltinId) -> bool {
matches!(
id,
BuiltinId::Seq
| BuiltinId::DeepSeq
| BuiltinId::Trace
| BuiltinId::Warn
| BuiltinId::TryEval
| BuiltinId::AddErrorContext
| BuiltinId::Break
)
}
/// Intern all builtin names and extra names needed at runtime.
fn intern_all_builtins(interner: &mut DefaultStringInterner) {
for &(name, _) in BUILTINS {
interner.get_or_intern(name);
}
for &name in EXTRA_INTERN_NAMES {
interner.get_or_intern(name);
}
}
pub(super) fn new_builtins_env(
interner: &mut DefaultStringInterner,
) -> HashMap<StringId, Ir<'static, RawIrRef<'static>>> {
intern_all_builtins(interner);
let mut builtins = HashMap::new();
let builtins_sym = StringId(interner.get_or_intern("builtins"));
builtins.insert(builtins_sym, Ir::Builtins);
@@ -49,3 +242,6 @@ pub(super) fn new_builtins_env(
builtins
}
pub(super) type PrimOpArgs<'gc> = [Value<'gc>; 3];
pub(super) type PrimOpStrictArgs<'gc> = [StrictValue<'gc>; 3];
+929
View File
@@ -0,0 +1,929 @@
use gc_arena::{Collect, Gc, Mutation, RefLock};
use smallvec::SmallVec;
use string_interner::DefaultStringInterner;
use super::builtins::{BuiltinId, PrimOpArgs, PrimOpStrictArgs};
use super::value::*;
use super::vm::{ForceResult, VM, VmError};
use crate::ir::StringId;
pub(super) enum BuiltinResult<'gc> {
Done(Value<'gc>),
Force(BuiltinState<'gc>, Value<'gc>),
Call(BuiltinState<'gc>, StrictValue<'gc>, Value<'gc>),
CallAndForce(BuiltinState<'gc>, StrictValue<'gc>, Value<'gc>),
Error(VmError),
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(super) enum BuiltinState<'gc> {
FoldlStrict(FoldlStrict<'gc>),
// future: Filter, GenericClosure, Sort, All, Any, ConcatMap, ...
}
impl<'gc> BuiltinState<'gc> {
pub(super) fn resume(
self,
val: StrictValue<'gc>,
ctx: &PrimOpCtx<'_, 'gc>,
) -> BuiltinResult<'gc> {
match self {
BuiltinState::FoldlStrict(s) => s.resume(val, ctx),
}
}
}
pub(super) struct PrimOpCtx<'a, 'gc> {
pub(super) vm: &'a VM<'gc>,
pub(super) mc: &'a Mutation<'gc>,
pub(super) strings: &'a DefaultStringInterner,
}
macro_rules! force_inline_or_err {
($ctx:expr, $val:expr) => {{
let val = $val;
match $ctx.vm.force_inline(val) {
Ok(ForceResult::Ready(v)) => v,
Ok(_) => {
return BuiltinResult::Error(VM::err(
"value requires evaluation in non-stateful builtin context",
));
}
Err(e) => return BuiltinResult::Error(e),
}
}};
}
macro_rules! force {
($ctx:expr, $state:expr, $val:expr) => {{
let val = $val;
match $ctx.vm.force_inline(val) {
Ok(ForceResult::Ready(v)) => v,
Ok(ForceResult::NeedEval { .. } | ForceResult::NeedApply(_)) => {
return BuiltinResult::Force($state, val);
}
Err(e) => return BuiltinResult::Error(e),
}
}};
}
macro_rules! call {
($ctx:expr, $state:expr, $func:expr, $arg:expr) => {{
let func = $func;
let arg = $arg;
return BuiltinResult::Call($state, func, arg);
}};
}
macro_rules! call_and_force {
($ctx:expr, $state:expr, $func:expr, $arg:expr) => {{
let func = $func;
let arg = $arg;
return BuiltinResult::CallAndForce($state, func, arg);
}};
}
pub(super) fn dispatch_strict_builtin<'gc>(
id: BuiltinId,
args: PrimOpStrictArgs<'gc>,
_arity: u8,
ctx: &PrimOpCtx<'_, 'gc>,
) -> BuiltinResult<'gc> {
match id {
BuiltinId::TypeOf => {
let val = args[0];
let name = if val.as_inline::<i32>().is_some() || val.as_gc::<i64>().is_some() {
"int"
} else if val.as_float().is_some() {
"float"
} else if val.as_inline::<bool>().is_some() {
"bool"
} else if VM::get_string(val, ctx.strings).is_some() {
"string"
} else if val.is::<Null>() {
"null"
} else if val.as_gc::<AttrSet<'gc>>().is_some() {
"set"
} else if val.as_gc::<List<'gc>>().is_some() {
"list"
} else if val.as_gc::<Closure<'gc>>().is_some()
|| val.as_inline::<PrimOp>().is_some()
|| val.as_gc::<PrimOpApp<'gc>>().is_some()
{
"lambda"
} else {
return BuiltinResult::Error(VM::err("typeOf: unknown type"));
};
let sid = ctx.strings.get(name).expect("typeOf string not interned");
BuiltinResult::Done(Value::new_inline(StringId(sid)))
}
BuiltinId::IsNull => BuiltinResult::Done(Value::new_inline(args[0].is::<Null>())),
BuiltinId::IsAttrs => {
BuiltinResult::Done(Value::new_inline(args[0].as_gc::<AttrSet<'gc>>().is_some()))
}
BuiltinId::IsBool => {
BuiltinResult::Done(Value::new_inline(args[0].as_inline::<bool>().is_some()))
}
BuiltinId::IsFloat => BuiltinResult::Done(Value::new_inline(args[0].as_float().is_some())),
BuiltinId::IsFunction => {
let v = args[0];
let is_func = v.as_gc::<Closure<'gc>>().is_some()
|| v.as_inline::<PrimOp>().is_some()
|| v.as_gc::<PrimOpApp<'gc>>().is_some();
BuiltinResult::Done(Value::new_inline(is_func))
}
BuiltinId::IsInt => {
let v = args[0];
let is_int = v.as_inline::<i32>().is_some() || v.as_gc::<i64>().is_some();
BuiltinResult::Done(Value::new_inline(is_int))
}
BuiltinId::IsList => {
BuiltinResult::Done(Value::new_inline(args[0].as_gc::<List<'gc>>().is_some()))
}
BuiltinId::IsString => {
let v = args[0];
let is_str = v.as_inline::<StringId>().is_some() || v.as_gc::<NixString>().is_some();
BuiltinResult::Done(Value::new_inline(is_str))
}
BuiltinId::IsPath => BuiltinResult::Done(Value::new_inline(false)),
BuiltinId::Length => {
let Some(list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.length: not a list"));
};
BuiltinResult::Done(VM::make_int(list.inner.len() as i64, ctx.mc))
}
BuiltinId::Head => {
let Some(list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.head: not a list"));
};
if list.inner.is_empty() {
return BuiltinResult::Error(VM::err("builtins.head: empty list"));
}
BuiltinResult::Done(list.inner[0])
}
BuiltinId::Tail => {
let Some(list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.tail: not a list"));
};
if list.inner.is_empty() {
return BuiltinResult::Error(VM::err("builtins.tail: empty list"));
}
let tail = List {
inner: SmallVec::from_slice(&list.inner[1..]),
};
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, tail)))
}
BuiltinId::AttrNames => {
let Some(attrs) = args[0].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.attrNames: not a set"));
};
let items: SmallVec<[Value<'gc>; 4]> =
attrs.iter().map(|(k, _)| Value::new_inline(*k)).collect();
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: items })))
}
BuiltinId::AttrValues => {
let Some(attrs) = args[0].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.attrValues: not a set"));
};
let items: SmallVec<[Value<'gc>; 4]> = attrs.iter().map(|(_, v)| *v).collect();
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: items })))
}
BuiltinId::Map => {
let f = args[0];
let Some(list) = args[1].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.map: second argument is not a list",
));
};
if list.inner.is_empty() {
return BuiltinResult::Done(Value::new_gc(Gc::new(
ctx.mc,
List {
inner: SmallVec::new(),
},
)));
}
let new_elems: SmallVec<[Value<'gc>; 4]> = list
.inner
.iter()
.map(|elem| {
let thunk: Gc<'gc, Thunk<'gc>> = Gc::new(
ctx.mc,
RefLock::new(ThunkState::Apply {
func: f.relax(),
arg: *elem,
}),
);
Value::new_gc(thunk)
})
.collect();
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: new_elems })))
}
BuiltinId::GenList => {
let f = args[0];
let len_val = args[1];
let Some(len) = VM::as_num(len_val) else {
return BuiltinResult::Error(VM::err(
"builtins.genList: second argument is not a number",
));
};
let super::vm::NixNum::Int(len) = len else {
return BuiltinResult::Error(VM::err(
"builtins.genList: second argument is not an integer",
));
};
if len < 0 {
return BuiltinResult::Error(VM::err("builtins.genList: negative length"));
}
let items: SmallVec<[Value<'gc>; 4]> = (0..len)
.map(|i| {
let arg = VM::make_int(i, ctx.mc);
let thunk: Gc<'gc, Thunk<'gc>> = Gc::new(
ctx.mc,
RefLock::new(ThunkState::Apply {
func: f.relax(),
arg,
}),
);
Value::new_gc(thunk)
})
.collect();
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: items })))
}
BuiltinId::ElemAt => {
let Some(list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.elemAt: not a list"));
};
let Some(idx) = VM::as_num(args[1]) else {
return BuiltinResult::Error(VM::err("builtins.elemAt: index is not a number"));
};
let super::vm::NixNum::Int(idx) = idx else {
return BuiltinResult::Error(VM::err("builtins.elemAt: index is not an integer"));
};
if idx < 0 || idx as usize >= list.inner.len() {
return BuiltinResult::Error(VM::err(format!(
"builtins.elemAt: index {} out of bounds for list of length {}",
idx,
list.inner.len()
)));
}
BuiltinResult::Done(list.inner[idx as usize])
}
BuiltinId::GetAttr => {
let Some(name) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.getAttr: first argument is not a string",
));
};
let Some(sid) = ctx.strings.get(name) else {
return BuiltinResult::Error(VM::err(format!(
"builtins.getAttr: attribute '{}' not found",
name
)));
};
let Some(attrs) = args[1].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.getAttr: second argument is not a set",
));
};
match attrs.lookup(StringId(sid)) {
Some(v) => BuiltinResult::Done(v),
None => BuiltinResult::Error(VM::err(format!("attribute '{}' missing", name))),
}
}
BuiltinId::HasAttr => {
let Some(name) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.hasAttr: first argument is not a string",
));
};
let Some(attrs) = args[1].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.hasAttr: second argument is not a set",
));
};
let has = ctx
.strings
.get(name)
.map(|sid| attrs.has(StringId(sid)))
.unwrap_or(false);
BuiltinResult::Done(Value::new_inline(has))
}
BuiltinId::RemoveAttrs => {
let Some(attrs) = args[0].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.removeAttrs: first argument is not a set",
));
};
let Some(remove_list) = args[1].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.removeAttrs: second argument is not a list",
));
};
let mut to_remove = Vec::new();
for item in remove_list.inner.iter() {
let sv = force_inline_or_err!(ctx, *item);
if let Some(s) = VM::get_string(sv, ctx.strings)
&& let Some(sid) = ctx.strings.get(s)
{
to_remove.push(StringId(sid));
}
}
let entries: SmallVec<[(StringId, Value<'gc>); 4]> = attrs
.iter()
.filter(|(k, _)| !to_remove.contains(k))
.cloned()
.collect();
let new_attrs = Gc::new(ctx.mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
BuiltinResult::Done(Value::new_gc(new_attrs))
}
BuiltinId::IntersectAttrs => {
let Some(a) = args[0].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.intersectAttrs: first argument is not a set",
));
};
let Some(b) = args[1].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.intersectAttrs: second argument is not a set",
));
};
let entries: SmallVec<[(StringId, Value<'gc>); 4]> =
b.iter().filter(|(k, _)| a.has(*k)).cloned().collect();
let new_attrs = Gc::new(ctx.mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
BuiltinResult::Done(Value::new_gc(new_attrs))
}
BuiltinId::ListToAttrs => {
let Some(list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.listToAttrs: not a list"));
};
let name_sid = ctx.strings.get("name").expect("'name' not interned");
let value_sid = ctx.strings.get("value").expect("'value' not interned");
let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new();
for item in list.inner.iter() {
let sv = force_inline_or_err!(ctx, *item);
let Some(attr_set) = sv.as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.listToAttrs: element is not a set",
));
};
let Some(name_val) = attr_set.lookup(StringId(name_sid)) else {
return BuiltinResult::Error(VM::err(
"builtins.listToAttrs: element missing 'name'",
));
};
let name_sv = force_inline_or_err!(ctx, name_val);
let Some(name_str) = VM::get_string(name_sv, ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.listToAttrs: 'name' is not a string",
));
};
let Some(value_val) = attr_set.lookup(StringId(value_sid)) else {
return BuiltinResult::Error(VM::err(
"builtins.listToAttrs: element missing 'value'",
));
};
let Some(key_sym) = ctx.strings.get(name_str) else {
return BuiltinResult::Error(VM::err(
"builtins.listToAttrs: name not interned",
));
};
entries.push((StringId(key_sym), value_val));
}
entries.sort_by_key(|(k, _)| *k);
entries.dedup_by_key(|(k, _)| *k);
let new_attrs = Gc::new(ctx.mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
BuiltinResult::Done(Value::new_gc(new_attrs))
}
BuiltinId::ConcatLists => {
let Some(list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.concatLists: not a list"));
};
let mut result = SmallVec::<[Value<'gc>; 4]>::new();
for item in list.inner.iter() {
let sv = force_inline_or_err!(ctx, *item);
let Some(inner) = sv.as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.concatLists: element is not a list",
));
};
result.extend(inner.inner.iter().cloned());
}
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: result })))
}
BuiltinId::LessThan => {
match ctx
.vm
.compare_values(args[0], args[1], ctx.strings, |o| o.is_lt())
{
Ok(v) => BuiltinResult::Done(v),
Err(e) => BuiltinResult::Error(e),
}
}
BuiltinId::Add => {
match ctx.vm.compute_binop(
super::vm::BinOpTag::Add,
args[0],
args[1],
ctx.mc,
ctx.strings,
) {
Ok(v) => BuiltinResult::Done(v),
Err(e) => BuiltinResult::Error(e),
}
}
BuiltinId::Sub => {
match ctx
.vm
.numeric_binop(args[0], args[1], ctx.mc, i64::wrapping_sub, |a, b| a - b)
{
Ok(v) => BuiltinResult::Done(v),
Err(e) => BuiltinResult::Error(e),
}
}
BuiltinId::Mul => {
match ctx
.vm
.numeric_binop(args[0], args[1], ctx.mc, i64::wrapping_mul, |a, b| a * b)
{
Ok(v) => BuiltinResult::Done(v),
Err(e) => BuiltinResult::Error(e),
}
}
BuiltinId::Div => {
match ctx.vm.compute_binop(
super::vm::BinOpTag::Div,
args[0],
args[1],
ctx.mc,
ctx.strings,
) {
Ok(v) => BuiltinResult::Done(v),
Err(e) => BuiltinResult::Error(e),
}
}
BuiltinId::BitAnd => {
let (Some(super::vm::NixNum::Int(a)), Some(super::vm::NixNum::Int(b))) =
(VM::as_num(args[0]), VM::as_num(args[1]))
else {
return BuiltinResult::Error(VM::err(
"builtins.bitAnd: arguments must be integers",
));
};
BuiltinResult::Done(VM::make_int(a & b, ctx.mc))
}
BuiltinId::BitOr => {
let (Some(super::vm::NixNum::Int(a)), Some(super::vm::NixNum::Int(b))) =
(VM::as_num(args[0]), VM::as_num(args[1]))
else {
return BuiltinResult::Error(VM::err("builtins.bitOr: arguments must be integers"));
};
BuiltinResult::Done(VM::make_int(a | b, ctx.mc))
}
BuiltinId::BitXor => {
let (Some(super::vm::NixNum::Int(a)), Some(super::vm::NixNum::Int(b))) =
(VM::as_num(args[0]), VM::as_num(args[1]))
else {
return BuiltinResult::Error(VM::err(
"builtins.bitXor: arguments must be integers",
));
};
BuiltinResult::Done(VM::make_int(a ^ b, ctx.mc))
}
BuiltinId::Ceil => {
if let Some(f) = args[0].as_float() {
BuiltinResult::Done(VM::make_int(f.ceil() as i64, ctx.mc))
} else if VM::as_num(args[0]).is_some() {
BuiltinResult::Done(args[0].relax())
} else {
BuiltinResult::Error(VM::err("builtins.ceil: not a number"))
}
}
BuiltinId::Floor => {
if let Some(f) = args[0].as_float() {
BuiltinResult::Done(VM::make_int(f.floor() as i64, ctx.mc))
} else if VM::as_num(args[0]).is_some() {
BuiltinResult::Done(args[0].relax())
} else {
BuiltinResult::Error(VM::err("builtins.floor: not a number"))
}
}
BuiltinId::StringLength => {
let Some(s) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err("builtins.stringLength: not a string"));
};
BuiltinResult::Done(VM::make_int(s.len() as i64, ctx.mc))
}
BuiltinId::Substring => {
let Some(super::vm::NixNum::Int(start)) = VM::as_num(args[0]) else {
return BuiltinResult::Error(VM::err(
"builtins.substring: start is not an integer",
));
};
let Some(super::vm::NixNum::Int(len)) = VM::as_num(args[1]) else {
return BuiltinResult::Error(VM::err(
"builtins.substring: length is not an integer",
));
};
let Some(s) = VM::get_string(args[2], ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.substring: third argument is not a string",
));
};
let start = start.max(0) as usize;
if start >= s.len() {
let ns = Gc::new(ctx.mc, NixString::new(""));
return BuiltinResult::Done(Value::new_gc(ns));
}
let end = if len < 0 {
s.len()
} else {
(start + len as usize).min(s.len())
};
let result = &s[start..end];
let ns = Gc::new(ctx.mc, NixString::new(result));
BuiltinResult::Done(Value::new_gc(ns))
}
BuiltinId::ToString => {
let v = args[0];
if let Some(s) = VM::get_string(v, ctx.strings) {
let ns = Gc::new(ctx.mc, NixString::new(s));
BuiltinResult::Done(Value::new_gc(ns))
} else if let Some(b) = v.as_inline::<bool>() {
let s = if b { "1" } else { "" };
let ns = Gc::new(ctx.mc, NixString::new(s));
BuiltinResult::Done(Value::new_gc(ns))
} else if v.is::<Null>() {
let ns = Gc::new(ctx.mc, NixString::new(""));
BuiltinResult::Done(Value::new_gc(ns))
} else if let Some(n) = VM::as_num(v) {
let s = match n {
super::vm::NixNum::Int(i) => i.to_string(),
super::vm::NixNum::Float(f) => format!("{f}"),
};
let ns = Gc::new(ctx.mc, NixString::new(s));
BuiltinResult::Done(Value::new_gc(ns))
} else {
BuiltinResult::Error(VM::err("builtins.toString: cannot coerce to string"))
}
}
BuiltinId::Abort => {
let Some(msg) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err("builtins.abort: argument is not a string"));
};
BuiltinResult::Error(VmError::Uncatchable(crate::error::Error::eval_error(
format!("evaluation aborted with the following error message: '{msg}'"),
)))
}
BuiltinId::Throw => {
let Some(msg) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err("builtins.throw: argument is not a string"));
};
BuiltinResult::Error(VmError::Catchable(msg.to_owned()))
}
BuiltinId::FunctionArgs => {
let v = args[0];
if let Some(closure) = v.as_gc::<Closure<'gc>>() {
if let Some(ref pattern) = closure.pattern {
let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new();
for &name in &pattern.required {
entries.push((name, Value::new_inline(false)));
}
for &name in &pattern.optional {
entries.push((name, Value::new_inline(true)));
}
entries.sort_by_key(|(k, _)| *k);
let attrs = Gc::new(ctx.mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
BuiltinResult::Done(Value::new_gc(attrs))
} else {
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, AttrSet::default())))
}
} else {
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, AttrSet::default())))
}
}
BuiltinId::FoldlStrict => FoldlStrict::call(args, ctx),
BuiltinId::Elem => {
let Some(list) = args[1].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.elem: second argument is not a list",
));
};
let needle = args[0];
for item in list.inner.iter() {
let sv = force_inline_or_err!(ctx, *item);
if ctx.vm.values_equal(needle, sv, ctx.strings) {
return BuiltinResult::Done(Value::new_inline(true));
}
}
BuiltinResult::Done(Value::new_inline(false))
}
BuiltinId::ReplaceStrings => {
let Some(from_list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.replaceStrings: first argument is not a list",
));
};
let Some(to_list) = args[1].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.replaceStrings: second argument is not a list",
));
};
let Some(s) = VM::get_string(args[2], ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.replaceStrings: third argument is not a string",
));
};
if from_list.inner.len() != to_list.inner.len() {
return BuiltinResult::Error(VM::err(
"builtins.replaceStrings: lists must have same length",
));
}
let mut from_strs = Vec::new();
let mut to_strs = Vec::new();
for (f, t) in from_list.inner.iter().zip(to_list.inner.iter()) {
let fv = force_inline_or_err!(ctx, *f);
let tv = force_inline_or_err!(ctx, *t);
let Some(fs) = VM::get_string(fv, ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.replaceStrings: from element is not a string",
));
};
let Some(ts) = VM::get_string(tv, ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.replaceStrings: to element is not a string",
));
};
from_strs.push(fs.to_owned());
to_strs.push(ts.to_owned());
}
let s = s.to_owned();
let mut result = String::new();
let mut i = 0;
while i < s.len() {
let mut found = false;
for (j, from) in from_strs.iter().enumerate() {
if from.is_empty() {
result.push_str(&to_strs[j]);
result.push(s.as_bytes()[i] as char);
i += 1;
found = true;
break;
}
if s[i..].starts_with(from.as_str()) {
result.push_str(&to_strs[j]);
i += from.len();
found = true;
break;
}
}
if !found {
result.push(s.as_bytes()[i] as char);
i += 1;
}
}
if from_strs.iter().any(|f| f.is_empty()) {
let j = from_strs
.iter()
.position(|f| f.is_empty())
.expect("just checked");
result.push_str(&to_strs[j]);
}
let ns = Gc::new(ctx.mc, NixString::new(result));
BuiltinResult::Done(Value::new_gc(ns))
}
BuiltinId::ConcatStringsSep => {
let Some(sep) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.concatStringsSep: first argument is not a string",
));
};
let sep = sep.to_owned();
let Some(list) = args[1].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.concatStringsSep: second argument is not a list",
));
};
let mut result = String::new();
for (i, item) in list.inner.iter().enumerate() {
if i > 0 {
result.push_str(&sep);
}
let sv = force_inline_or_err!(ctx, *item);
let Some(s) = VM::get_string(sv, ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.concatStringsSep: element is not a string",
));
};
result.push_str(s);
}
let ns = Gc::new(ctx.mc, NixString::new(result));
BuiltinResult::Done(Value::new_gc(ns))
}
BuiltinId::FromJSON => {
let Some(s) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err("builtins.fromJSON: not a string"));
};
match serde_json::from_str::<serde_json::Value>(s) {
Ok(json) => {
let v = json_to_nix(&json, ctx.mc, ctx.strings);
BuiltinResult::Done(v)
}
Err(e) => BuiltinResult::Error(VM::err(format!("builtins.fromJSON: {e}"))),
}
}
BuiltinId::ToJSON => BuiltinResult::Error(VM::err("builtins.toJSON: not yet implemented")),
BuiltinId::Null => BuiltinResult::Done(Value::new_inline(Null)),
_ => BuiltinResult::Error(VM::err(format!("builtin {:?} not yet implemented", id))),
}
}
pub(super) fn dispatch_lazy_builtin<'gc>(
id: BuiltinId,
args: &PrimOpArgs<'gc>,
_arity: u8,
ctx: &PrimOpCtx<'_, 'gc>,
) -> BuiltinResult<'gc> {
match id {
BuiltinId::Seq => {
let _ = force_inline_or_err!(ctx, args[0]);
BuiltinResult::Done(args[1])
}
BuiltinId::DeepSeq => {
// TODO: deep force
let _ = force_inline_or_err!(ctx, args[0]);
BuiltinResult::Done(args[1])
}
BuiltinId::Trace => {
let sv = force_inline_or_err!(ctx, args[0]);
if let Some(s) = VM::get_string(sv, ctx.strings) {
eprintln!("trace: {s}");
} else {
eprintln!("trace: <non-string>");
}
BuiltinResult::Done(args[1])
}
BuiltinId::Warn => {
let sv = force_inline_or_err!(ctx, args[0]);
if let Some(s) = VM::get_string(sv, ctx.strings) {
eprintln!("warning: {s}");
} else {
eprintln!("warning: <non-string>");
}
BuiltinResult::Done(args[1])
}
BuiltinId::TryEval => BuiltinResult::Error(VM::err(
"builtins.tryEval: requires catch frame support (TODO)",
)),
BuiltinId::AddErrorContext => BuiltinResult::Done(args[1]),
BuiltinId::Break => BuiltinResult::Done(args[0]),
_ => BuiltinResult::Error(VM::err(format!(
"lazy builtin {:?} not yet implemented",
id
))),
}
}
fn json_to_nix<'gc>(
json: &serde_json::Value,
mc: &Mutation<'gc>,
strings: &DefaultStringInterner,
) -> Value<'gc> {
match json {
serde_json::Value::Null => Value::new_inline(Null),
serde_json::Value::Bool(b) => Value::new_inline(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
VM::make_int(i, mc)
} else if let Some(f) = n.as_f64() {
Value::new_float(f)
} else {
Value::new_inline(Null)
}
}
serde_json::Value::String(s) => {
let ns = Gc::new(mc, NixString::new(s.as_str()));
Value::new_gc(ns)
}
serde_json::Value::Array(arr) => {
let items: SmallVec<[Value<'gc>; 4]> =
arr.iter().map(|v| json_to_nix(v, mc, strings)).collect();
Value::new_gc(Gc::new(mc, List { inner: items }))
}
serde_json::Value::Object(obj) => {
let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new();
for (k, v) in obj {
if let Some(sym) = strings.get(k.as_str()) {
entries.push((StringId(sym), json_to_nix(v, mc, strings)));
}
}
entries.sort_by_key(|(k, _)| *k);
let attrs = Gc::new(mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
Value::new_gc(attrs)
}
}
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(super) struct FoldlStrict<'gc> {
op: StrictValue<'gc>,
list: Gc<'gc, List<'gc>>,
acc: StrictValue<'gc>,
index: usize,
phase: FoldlPhase<'gc>,
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
enum FoldlPhase<'gc> {
CallOp,
CallPartial(StrictValue<'gc>),
}
impl<'gc> FoldlStrict<'gc> {
fn call(args: PrimOpStrictArgs<'gc>, ctx: &PrimOpCtx<'_, 'gc>) -> BuiltinResult<'gc> {
let op = args[0];
let nul = args[1];
let Some(list) = args[2].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.foldl': third argument is not a list"));
};
if list.inner.is_empty() {
return BuiltinResult::Done(nul.relax());
}
let state = FoldlStrict {
op,
list,
acc: nul,
index: 0,
phase: FoldlPhase::CallOp,
};
state.step(ctx)
}
fn step(self, _ctx: &PrimOpCtx<'_, 'gc>) -> BuiltinResult<'gc> {
let state = BuiltinState::FoldlStrict(FoldlStrict {
op: self.op,
list: self.list,
acc: self.acc,
index: self.index,
phase: FoldlPhase::CallOp,
});
call!(ctx, state, self.op, self.acc.relax())
}
fn resume(mut self, val: StrictValue<'gc>, ctx: &PrimOpCtx<'_, 'gc>) -> BuiltinResult<'gc> {
match self.phase {
FoldlPhase::CallOp => {
let partial = val;
let elem = self.list.inner[self.index];
self.phase = FoldlPhase::CallPartial(partial);
let state = BuiltinState::FoldlStrict(self);
call_and_force!(ctx, state, partial, elem)
}
FoldlPhase::CallPartial(_) => {
self.acc = val;
self.index += 1;
self.phase = FoldlPhase::CallOp;
if self.index >= self.list.inner.len() {
return BuiltinResult::Done(self.acc.relax());
}
self.step(ctx)
}
}
}
}
+8 -2
View File
@@ -3,14 +3,13 @@ use std::mem::MaybeUninit;
use gc_arena::Collect;
use smallvec::SmallVec;
// FIXME: Drop???
pub(super) struct Stack<const N: usize, T> {
inner: Box<[MaybeUninit<T>; N]>,
len: usize,
}
unsafe impl<'gc, const N: usize, T: Collect<'gc> + 'gc> Collect<'gc> for Stack<N, T> {
const NEEDS_TRACE: bool = true;
const NEEDS_TRACE: bool = T::NEEDS_TRACE;
fn trace<U: gc_arena::collect::Trace<'gc>>(&self, cc: &mut U) {
for item in self.inner[..self.len].iter() {
unsafe {
@@ -34,6 +33,13 @@ impl<const N: usize, T> Stack<N, T> {
}
}
pub(super) unsafe fn push_unchecked(&mut self, val: T) {
unsafe {
self.inner.get_unchecked_mut(self.len).write(val);
}
self.len += 1;
}
pub(super) fn push(&mut self, val: T) -> Result<(), T> {
if self.len == N {
return Err(val);
+39 -37
View File
@@ -3,16 +3,19 @@ use std::marker::PhantomData;
use std::mem::size_of;
use std::ops::Deref;
use boxing::nan::raw::{RawBox, RawStore, RawTag, Value as RawValue};
use gc_arena::{Collect, Gc, Mutation, RefLock, collect::Trace};
use num_enum::TryFromPrimitive;
use sealed::sealed;
use smallvec::SmallVec;
use string_interner::{Symbol, symbol::SymbolU32};
use crate::ir::StringId;
use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue};
use crate::{ir::StringId, runtime::builtins::BuiltinId};
#[sealed]
pub(crate) trait Storable {
/// # Safety
/// TAG must be unique among all implementors.
pub(crate) unsafe trait Storable {
const TAG: (bool, u8);
}
pub(crate) trait InlineStorable: Storable + RawStore {}
@@ -25,14 +28,14 @@ macro_rules! define_value_types {
) => {
$(
#[sealed]
impl Storable for $itype {
unsafe impl Storable for $itype {
const TAG: (bool, u8) = $itag;
}
impl InlineStorable for $itype {}
)*
$(
#[sealed]
impl Storable for $gtype {
unsafe impl Storable for $gtype {
const TAG: (bool, u8) = $gtag;
}
impl GcStorable for $gtype {}
@@ -116,22 +119,13 @@ define_value_types! {
/// # Nix runtime value representation
///
/// NaN-boxed value fitting in 8 bytes.
#[derive(Copy, Clone)]
#[repr(transparent)]
pub(crate) struct Value<'gc> {
raw: RawBox,
_marker: PhantomData<Gc<'gc, ()>>,
}
impl Clone for Value<'_> {
#[inline]
fn clone(&self) -> Self {
Self {
raw: self.raw.clone(),
_marker: PhantomData,
}
}
}
impl Default for Value<'_> {
#[inline(always)]
fn default() -> Self {
@@ -291,14 +285,23 @@ impl fmt::Debug for NixString {
}
}
#[derive(Collect, Debug)]
#[derive(Collect, Debug, Default)]
#[collect(no_drop)]
pub(crate) struct AttrSet<'gc> {
pub(crate) entries: SmallVec<[(StringId, Value<'gc>); 4]>,
entries: SmallVec<[(StringId, Value<'gc>); 4]>,
}
impl<'gc> Deref for AttrSet<'gc> {
type Target = [(StringId, Value<'gc>)];
fn deref(&self) -> &Self::Target {
&self.entries
}
}
impl<'gc> AttrSet<'gc> {
pub(crate) fn from_sorted(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self {
pub(crate) unsafe fn from_sorted_unchecked(
entries: SmallVec<[(StringId, Value<'gc>); 4]>,
) -> Self {
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
Self { entries }
}
@@ -307,13 +310,11 @@ impl<'gc> AttrSet<'gc> {
self.entries
.binary_search_by_key(&key, |(k, _)| *k)
.ok()
.map(|i| self.entries[i].1.clone())
.map(|i| self.entries[i].1)
}
pub(crate) fn has(&self, key: StringId) -> bool {
self.entries
.binary_search_by_key(&key, |(k, _)| *k)
.is_ok()
self.entries.binary_search_by_key(&key, |(k, _)| *k).is_ok()
}
pub(crate) fn merge(&self, other: &Self, mc: &Mutation<'gc>) -> Gc<'gc, Self> {
@@ -328,15 +329,15 @@ impl<'gc> AttrSet<'gc> {
while i < self.entries.len() && j < other.entries.len() {
match self.entries[i].0.cmp(&other.entries[j].0) {
Less => {
entries.push(self.entries[i].clone());
entries.push(self.entries[i]);
i += 1;
}
Greater => {
entries.push(other.entries[j].clone());
entries.push(other.entries[j]);
j += 1;
}
Equal => {
entries.push(other.entries[j].clone());
entries.push(other.entries[j]);
i += 1;
j += 1;
}
@@ -351,7 +352,7 @@ impl<'gc> AttrSet<'gc> {
}
}
#[derive(Collect, Debug)]
#[derive(Collect, Debug, Default)]
#[collect(no_drop)]
pub(crate) struct List<'gc> {
pub(crate) inner: SmallVec<[Value<'gc>; 4]>,
@@ -366,6 +367,10 @@ pub(crate) enum ThunkState<'gc> {
ip: u32,
env: Gc<'gc, RefLock<Env<'gc>>>,
},
Apply {
func: Value<'gc>,
arg: Value<'gc>,
},
Blackhole,
Evaluated(Value<'gc>),
}
@@ -420,17 +425,20 @@ pub(crate) struct PatternInfo {
#[derive(Clone, Copy, Debug, Collect)]
#[collect(require_static)]
pub(crate) struct PrimOp {
pub(crate) id: u8,
pub(crate) id: BuiltinId,
pub(crate) arity: u8,
}
impl RawStore for PrimOp {
fn to_val(self, value: &mut RawValue) {
value.set_data([0, 0, 0, 0, self.id, self.arity]);
value.set_data([0, 0, 0, 0, self.id as u8, self.arity]);
}
fn from_val(value: &RawValue) -> Self {
let [.., id, arity] = *value.data();
Self { id, arity }
Self {
id: BuiltinId::try_from_primitive(id).expect("invalid BuiltinId"),
arity,
}
}
}
@@ -441,6 +449,7 @@ pub(crate) struct PrimOpApp<'gc> {
pub(crate) args: SmallVec<[Value<'gc>; 2]>,
}
#[derive(Copy, Clone, Default)]
#[repr(transparent)]
pub(crate) struct StrictValue<'gc>(Value<'gc>);
@@ -455,7 +464,7 @@ impl<'gc> StrictValue<'gc> {
}
#[inline]
pub(crate) fn into_relaxed(self) -> Value<'gc> {
pub(crate) fn relax(self) -> Value<'gc> {
self.0
}
}
@@ -468,13 +477,6 @@ impl<'gc> Deref for StrictValue<'gc> {
}
}
impl Clone for StrictValue<'_> {
#[inline]
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl fmt::Debug for StrictValue<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
+847 -422
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -7,7 +7,6 @@ mod validation;
pub use config::StoreConfig;
pub use daemon::DaemonStore;
pub use validation::validate_store_path;
pub trait Store: Send + Sync {
fn get_store_dir(&self) -> &str;
+2 -5
View File
@@ -69,13 +69,10 @@ impl Store for DaemonStore {
fn ensure_path(&self, path: &str) -> Result<()> {
self.block_on(async {
self.connection.ensure_path(path).await.map_err(|e| {
Error::eval_error(
format!(
Error::eval_error(format!(
"builtins.storePath: path '{}' is not valid in nix store: {}",
path, e
),
None,
)
))
})
})
}
+31 -34
View File
@@ -2,75 +2,72 @@ use crate::error::{Error, Result};
pub fn validate_store_path(store_dir: &str, path: &str) -> Result<()> {
if !path.starts_with(store_dir) {
return Err(Error::eval_error(
format!("path '{}' is not in the Nix store", path),
None,
));
return Err(Error::eval_error(format!(
"path '{}' is not in the Nix store",
path
)));
}
let relative = path
.strip_prefix(store_dir)
.and_then(|s| s.strip_prefix('/'))
.ok_or_else(|| Error::eval_error(format!("invalid store path format: {}", path), None))?;
.ok_or_else(|| Error::eval_error(format!("invalid store path format: {}", path)))?;
if relative.is_empty() {
return Err(Error::eval_error(
format!("store path cannot be store directory itself: {}", path),
None,
));
return Err(Error::eval_error(format!(
"store path cannot be store directory itself: {}",
path
)));
}
let parts: Vec<&str> = relative.splitn(2, '-').collect();
if parts.len() != 2 {
return Err(Error::eval_error(
format!("invalid store path format (missing name): {}", path),
None,
));
return Err(Error::eval_error(format!(
"invalid store path format (missing name): {}",
path
)));
}
let hash = parts[0];
let name = parts[1];
if hash.len() != 32 {
return Err(Error::eval_error(
format!(
return Err(Error::eval_error(format!(
"invalid store path hash length (expected 32, got {}): {}",
hash.len(),
hash
),
None,
));
)));
}
for ch in hash.chars() {
if !matches!(ch, '0'..='9' | 'a'..='d' | 'f'..='n' | 'p'..='s' | 'v'..='z') {
return Err(Error::eval_error(
format!("invalid character '{}' in store path hash: {}", ch, hash),
None,
));
return Err(Error::eval_error(format!(
"invalid character '{}' in store path hash: {}",
ch, hash
)));
}
}
if name.is_empty() {
return Err(Error::eval_error(
format!("store path has empty name: {}", path),
None,
));
return Err(Error::eval_error(format!(
"store path has empty name: {}",
path
)));
}
if name.starts_with('.') {
return Err(Error::eval_error(
format!("store path name cannot start with '.': {}", name),
None,
));
return Err(Error::eval_error(format!(
"store path name cannot start with '.': {}",
name
)));
}
for ch in name.chars() {
if !matches!(ch, '0'..='9' | 'a'..='z' | 'A'..='Z' | '+' | '-' | '.' | '_' | '?' | '=') {
return Err(Error::eval_error(
format!("invalid character '{}' in store path name: {}", ch, name),
None,
));
return Err(Error::eval_error(format!(
"invalid character '{}' in store path name: {}",
ch, name
)));
}
}
+9
View File
@@ -267,6 +267,15 @@ fn escape_quote_string(s: &str) -> String {
ret
}
/// Wrapper to format a float in Nix style (C printf `%g` with precision 6).
pub(crate) 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() {
Generated
+9 -9
View File
@@ -8,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1773471952,
"narHash": "sha256-kIRggXyT8RzijtfvyRIzj+zIDWM2fnCp8t0X4BkkTVc=",
"lastModified": 1774076307,
"narHash": "sha256-v8axK9HGgVERw9oG3SKdsuE+ri0GPUZDyRBN4GLqQ1c=",
"owner": "nix-community",
"repo": "fenix",
"rev": "a1b770adbc3f6c27485d03b90462ec414d4e1ce5",
"rev": "556198cc6c69c0a13228a15e33b2360f333b0092",
"type": "github"
},
"original": {
@@ -37,11 +37,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1773282481,
"narHash": "sha256-b/GV2ysM8mKHhinse2wz+uP37epUrSE+sAKXy/xvBY4=",
"lastModified": 1773821835,
"narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "fe416aaedd397cacb33a610b33d60ff2b431b127",
"rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0",
"type": "github"
},
"original": {
@@ -61,11 +61,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1773326183,
"narHash": "sha256-tj3piRd9RnnP36HwHmQD4O4XZeowsH/rvMeyp9Pmot0=",
"lastModified": 1774036669,
"narHash": "sha256-EWhsBSh/h1VcyLKXuTEyH8lNVB2ktuKVkqx8dkQ6hxk=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "6254616e97f358e67b70dfc0463687f5f7911c1a",
"rev": "0cf3e8a07f0e29825f5db78840e646a4bb519742",
"type": "github"
},
"original": {