Compare commits

..
1 Commits
Author SHA1 Message Date
imxyy1soope1 1083b8fdfa temp 2026-03-13 21:57:23 +08:00
519 changed files with 13709 additions and 3964 deletions

No files matched your search

+3
View File
@@ -16,6 +16,9 @@ vim.lsp.config("rust_analyzer", {
settings = {
["rust-analyzer"] = {
cargo = {
features = {
"inspector"
}
}
}
}
Generated
+969 -101
View File
File diff suppressed because it is too large. Load diff
+1 -2
View File
@@ -1,8 +1,7 @@
[workspace]
resolver = "3"
members = [
"fix",
"boxing",
"nix-js"
]
[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;
-471
View File
@@ -1,471 +0,0 @@
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 {
fn to_val(self, value: &mut Value);
fn from_val(value: &Value) -> Self;
}
impl RawStore for [u8; 6] {
#[inline]
fn to_val(self, value: &mut Value) {
value.set_data(self);
}
#[inline]
fn from_val(value: &Value) -> Self {
*value.data()
}
}
impl RawStore for bool {
#[inline]
fn to_val(self, value: &mut Value) {
value.set_data([u8::from(self)].truncate_to());
}
#[inline]
fn from_val(value: &Value) -> Self {
value.data()[0] == 1
}
}
macro_rules! int_store {
($ty:ty) => {
impl RawStore for $ty {
#[inline]
fn to_val(self, value: &mut Value) {
let bytes = self.to_ne_bytes();
value.set_data(bytes.truncate_to());
}
#[inline]
fn from_val(value: &Value) -> Self {
<$ty>::from_ne_bytes(value.data().truncate_to())
}
}
};
}
int_store!(u8);
int_store!(u16);
int_store!(u32);
int_store!(i8);
int_store!(i16);
int_store!(i32);
fn store_ptr<P: Strict + Copy>(value: &mut Value, ptr: P) {
#[cfg(target_pointer_width = "64")]
{
assert!(
ptr.addr() <= 0x0000_FFFF_FFFF_FFFF,
"Pointer too large to store in NaN box"
);
let val = (unsafe { value.whole_mut() } as *mut [u8; 8]).cast::<P>();
let ptr = Strict::map_addr(ptr, |addr| {
addr | (usize::from(value.header().into_raw()) << 48)
});
unsafe { val.write(ptr) };
}
#[cfg(target_pointer_width = "32")]
{
let _ = (value, ptr);
unimplemented!("32-bit pointer storage not supported");
}
}
fn load_ptr<P: Strict>(value: &Value) -> P {
#[cfg(target_pointer_width = "64")]
{
let val = (unsafe { value.whole() } as *const [u8; 8]).cast::<P>();
let ptr = unsafe { val.read() };
Strict::map_addr(ptr, |addr| addr & 0x0000_FFFF_FFFF_FFFF)
}
#[cfg(target_pointer_width = "32")]
{
let _ = value;
unimplemented!("32-bit pointer storage not supported");
}
}
impl<T> RawStore for *const T {
fn to_val(self, value: &mut Value) {
store_ptr::<*const T>(value, self);
}
fn from_val(value: &Value) -> Self {
load_ptr::<*const T>(value)
}
}
impl<T> RawStore for *mut T {
fn to_val(self, value: &mut Value) {
store_ptr::<*mut T>(value, self);
}
fn from_val(value: &Value) -> Self {
load_ptr::<*mut T>(value)
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum TagVal {
_P1,
_P2,
_P3,
_P4,
_P5,
_P6,
_P7,
_N1,
_N2,
_N3,
_N4,
_N5,
_N6,
_N7,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct RawTag(TagVal);
impl RawTag {
#[inline]
#[must_use]
pub 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> {
Some(RawTag(match (neg, val) {
(false, 1) => TagVal::_P1,
(false, 2) => TagVal::_P2,
(false, 3) => TagVal::_P3,
(false, 4) => TagVal::_P4,
(false, 5) => TagVal::_P5,
(false, 6) => TagVal::_P6,
(false, 7) => TagVal::_P7,
(true, 1) => TagVal::_N1,
(true, 2) => TagVal::_N2,
(true, 3) => TagVal::_N3,
(true, 4) => TagVal::_N4,
(true, 5) => TagVal::_N5,
(true, 6) => TagVal::_N6,
(true, 7) => TagVal::_N7,
_ => return None,
}))
}
/// # Safety
///
/// `val` must be in the range `1..8`
#[inline]
#[must_use]
pub unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag {
RawTag(match (neg, val) {
(false, 1) => TagVal::_P1,
(false, 2) => TagVal::_P2,
(false, 3) => TagVal::_P3,
(false, 4) => TagVal::_P4,
(false, 5) => TagVal::_P5,
(false, 6) => TagVal::_P6,
(false, 7) => TagVal::_P7,
(true, 1) => TagVal::_N1,
(true, 2) => TagVal::_N2,
(true, 3) => TagVal::_N3,
(true, 4) => TagVal::_N4,
(true, 5) => TagVal::_N5,
(true, 6) => TagVal::_N6,
(true, 7) => TagVal::_N7,
_ => unsafe { core::hint::unreachable_unchecked() },
})
}
#[inline]
#[must_use]
pub fn is_neg(self) -> bool {
matches!(self.0, |TagVal::_N1| TagVal::_N2
| TagVal::_N3
| TagVal::_N4
| TagVal::_N5
| TagVal::_N6
| TagVal::_N7)
}
#[inline]
#[must_use]
pub fn val(self) -> NonZeroU8 {
match self.0 {
TagVal::_P1 | TagVal::_N1 => NonZeroU8::MIN,
TagVal::_P2 | TagVal::_N2 => NonZeroU8::MIN.saturating_add(1),
TagVal::_P3 | TagVal::_N3 => NonZeroU8::MIN.saturating_add(2),
TagVal::_P4 | TagVal::_N4 => NonZeroU8::MIN.saturating_add(3),
TagVal::_P5 | TagVal::_N5 => NonZeroU8::MIN.saturating_add(4),
TagVal::_P6 | TagVal::_N6 => NonZeroU8::MIN.saturating_add(5),
TagVal::_P7 | TagVal::_N7 => NonZeroU8::MIN.saturating_add(6),
}
}
#[inline]
#[must_use]
pub fn neg_val(self) -> (bool, u8) {
match self.0 {
TagVal::_P1 => (false, 1),
TagVal::_P2 => (false, 2),
TagVal::_P3 => (false, 3),
TagVal::_P4 => (false, 4),
TagVal::_P5 => (false, 5),
TagVal::_P6 => (false, 6),
TagVal::_P7 => (false, 7),
TagVal::_N1 => (true, 1),
TagVal::_N2 => (true, 2),
TagVal::_N3 => (true, 3),
TagVal::_N4 => (true, 4),
TagVal::_N5 => (true, 5),
TagVal::_N6 => (true, 6),
TagVal::_N7 => (true, 7),
}
}
}
impl fmt::Debug for RawTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RawTag")
.field("neg", &self.is_neg())
.field("val", &self.val())
.finish()
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
#[repr(transparent)]
struct Header(u16);
impl Header {
#[inline]
fn new(tag: RawTag) -> Header {
let (neg, val) = tag.neg_val();
Header(0x7FF8 | (u16::from(neg) << 15) | u16::from(val))
}
#[inline]
fn tag(self) -> RawTag {
unsafe { RawTag::new_unchecked(self.get_sign(), self.get_tag()) }
}
#[inline]
fn get_sign(self) -> bool {
self.0 & 0x8000 != 0
}
#[inline]
fn get_tag(self) -> u8 {
(self.0 & 0x0007) as u8
}
#[inline]
fn into_raw(self) -> u16 {
self.0
}
}
#[derive(Clone, Debug, PartialEq)]
#[repr(C, align(8))]
pub struct Value {
#[cfg(target_endian = "big")]
header: Header,
data: [u8; 6],
#[cfg(target_endian = "little")]
header: Header,
}
impl Value {
#[inline]
pub fn new(tag: RawTag, data: [u8; 6]) -> Value {
Value {
header: Header::new(tag),
data,
}
}
#[inline]
pub fn empty(tag: RawTag) -> Value {
Value::new(tag, [0; 6])
}
pub 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 {
T::from_val(&self)
}
#[inline]
#[must_use]
pub fn tag(&self) -> RawTag {
self.header.tag()
}
#[inline]
fn header(&self) -> &Header {
&self.header
}
#[inline]
pub fn set_data(&mut self, val: [u8; 6]) {
self.data = val;
}
#[inline]
#[must_use]
pub fn data(&self) -> &[u8; 6] {
&self.data
}
#[inline]
#[must_use]
pub fn data_mut(&mut self) -> &mut [u8; 6] {
&mut self.data
}
#[inline]
#[must_use]
pub 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] {
let ptr = (self as *mut Value).cast::<[u8; 8]>();
unsafe { &mut *ptr }
}
}
#[repr(C)]
pub union RawBox {
float: f64,
value: ManuallyDrop<Value>,
bits: u64,
#[cfg(target_pointer_width = "64")]
ptr: *const (),
#[cfg(target_pointer_width = "32")]
ptr: (u32, *const ()),
}
impl RawBox {
#[inline]
#[must_use]
pub fn from_float(val: f64) -> RawBox {
match (val.is_nan(), val.is_sign_positive()) {
(true, true) => RawBox {
float: f64::from_bits(QUIET_NAN),
},
(true, false) => RawBox {
float: f64::from_bits(NEG_QUIET_NAN),
},
(false, _) => RawBox { float: val },
}
}
#[inline]
#[must_use]
pub fn from_value(value: Value) -> RawBox {
RawBox {
value: ManuallyDrop::new(value),
}
}
#[inline]
#[must_use]
pub fn tag(&self) -> Option<RawTag> {
if self.is_value() {
Some(unsafe { self.value.tag() })
} else {
None
}
}
#[inline]
#[must_use]
pub 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 {
(unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN })
}
#[inline]
#[must_use]
pub fn float(&self) -> Option<&f64> {
if self.is_float() {
Some(unsafe { &self.float })
} else {
None
}
}
#[inline]
#[must_use]
pub fn value(&self) -> Option<&Value> {
if self.is_value() {
Some(unsafe { &self.value })
} else {
None
}
}
#[inline]
pub 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() {
Some(val) => f.debug_tuple("RawBox::Float").field(val).finish(),
None => {
let val = self.value().expect("RawBox is neither float nor value");
f.debug_struct("RawBox::Data")
.field("tag", &val.tag())
.field("data", val.data())
.finish()
}
}
}
}
-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
}
}
-18
View File
@@ -1,18 +0,0 @@
#![allow(dead_code)]
use fix::error::{Result, Source};
use fix::runtime::Runtime;
use fix::value::Value;
pub fn eval(expr: &str) -> Value {
Runtime::new()
.unwrap()
.eval(Source::new_eval(expr.into()).unwrap())
.unwrap()
}
pub fn eval_result(expr: &str) -> Result<Value> {
Runtime::new()
.unwrap()
.eval(Source::new_eval(expr.into()).unwrap())
}
-531
View File
@@ -1,531 +0,0 @@
use std::hash::BuildHasher;
use bumpalo::Bump;
use gc_arena::{Arena, Rootable, arena::CollectionPhase};
use ghost_cell::{GhostCell, GhostToken};
use hashbrown::{DefaultHashBuilder, HashMap, HashSet, HashTable};
use rnix::TextRange;
use string_interner::DefaultStringInterner;
use crate::codegen::{BytecodeContext, InstructionPtr};
use crate::downgrade::{Downgrade as _, DowngradeContext};
use crate::error::{Error, Result, Source};
use crate::ir::{ArgId, Ir, IrKey, IrRef, RawIrRef, StringId, ThunkId, ir_content_eq};
use crate::runtime::builtins::new_builtins_env;
use crate::store::{DaemonStore, StoreConfig};
use crate::value::Symbol;
mod builtins;
mod stack;
mod value;
mod vm;
use vm::{Action, VM};
pub struct Runtime {
bytecode: Vec<u8>,
global_env: HashMap<StringId, Ir<'static, RawIrRef<'static>>>,
sources: Vec<Source>,
store: DaemonStore,
spans: Vec<(usize, TextRange)>,
thunk_count: usize,
strings: DefaultStringInterner,
arena: Arena<Rootable![VM<'_>]>,
}
impl Runtime {
const COLLECTOR_GRANULARITY: f64 = 1024.0;
pub fn new() -> Result<Self> {
let mut strings = DefaultStringInterner::new();
let global_env = new_builtins_env(&mut strings);
let config = StoreConfig::from_env();
let store = DaemonStore::connect(&config.daemon_socket)?;
Ok(Self {
global_env,
store,
strings,
thunk_count: 0,
bytecode: Vec::new(),
sources: Vec::new(),
spans: Vec::new(),
arena: Arena::new(|mc| VM::new(mc)),
})
}
pub fn eval(&mut self, source: Source) -> Result<crate::value::Value> {
let root = self.downgrade(source, None)?;
let ip = crate::codegen::compile_bytecode(root.as_ref(), self);
self.run(ip)
}
pub fn eval_shallow(&mut self, _source: Source) -> Result<crate::value::Value> {
todo!()
}
pub fn eval_deep(&mut self, source: Source) -> Result<crate::value::Value> {
// FIXME: deep
let root = self.downgrade(source, None)?;
let ip = crate::codegen::compile_bytecode(root.as_ref(), self);
self.run(ip)
}
pub fn eval_repl(
&mut self,
source: Source,
scope: &HashSet<StringId>,
) -> Result<crate::value::Value> {
// FIXME: shallow
let root = self.downgrade(source, Some(Scope::Repl(scope)))?;
let ip = crate::codegen::compile_bytecode(root.as_ref(), self);
self.run(ip)
}
pub fn add_binding(
&mut self,
_ident: &str,
_expr: &str,
_scope: &mut HashSet<StringId>,
) -> Result<crate::value::Value> {
todo!()
}
fn downgrade_ctx<'a, 'bump, 'id>(
&'a mut self,
bump: &'bump Bump,
token: GhostToken<'id>,
extra_scope: Option<Scope<'a>>,
) -> DowngradeCtx<'a, 'id, 'bump> {
let Runtime {
global_env,
sources,
thunk_count,
strings,
..
} = self;
DowngradeCtx {
bump,
token,
strings,
source: sources.last().unwrap().clone(),
scopes: [Scope::Global(global_env)].into_iter().chain(extra_scope.into_iter()).collect(),
with_scope_count: 0,
arg_count: 0,
thunk_count,
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
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());
let root = rnix::Root::parse(&source.src);
handle_parse_error(root.errors(), source).map_or(Ok(()), Err)?;
tracing::debug!("Downgrading Nix expression");
let expr = root
.tree()
.expr()
.ok_or_else(|| Error::parse_error("unexpected EOF".into()))?;
let bump = Bump::new();
GhostToken::new(|token| {
let ir = self
.downgrade_ctx(&bump, token, extra_scope)
.downgrade_toplevel(expr)?;
let ir = unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) };
Ok(OwnedIr { _bump: bump, ir })
})
}
fn run(&mut self, ip: InstructionPtr) -> Result<crate::value::Value> {
let mut pc = ip.0;
loop {
let Runtime {
bytecode,
strings,
arena,
..
} = self;
let action =
arena.mutate_root(|mc, root| root.run_batch(bytecode, &mut pc, mc, strings));
match action {
Action::NeedGc => {
if self.arena.collection_phase() == CollectionPhase::Sweeping {
self.arena.collect_debt();
} else if let Some(marked) = self.arena.mark_debt() {
marked.start_sweeping();
}
}
Action::Done(done) => {
break done;
}
Action::Continue => (),
Action::IoRequest(_) => todo!(),
}
}
}
}
fn parse_error_span(error: &rnix::ParseError) -> Option<rnix::TextRange> {
use rnix::ParseError::*;
match error {
Unexpected(range)
| UnexpectedExtra(range)
| UnexpectedWanted(_, range, _)
| UnexpectedDoubleBind(range)
| DuplicatedArgs(range, _) => Some(*range),
_ => None,
}
}
fn handle_parse_error<'a>(
errors: impl IntoIterator<Item = &'a rnix::ParseError>,
source: Source,
) -> Option<Box<Error>> {
for err in errors {
if let Some(span) = parse_error_span(err) {
return Some(
Error::parse_error(err.to_string())
.with_source(source)
.with_span(span),
);
}
}
None
}
struct DowngradeCtx<'ctx, 'id, 'ir> {
bump: &'ir Bump,
token: GhostToken<'id>,
strings: &'ctx mut DefaultStringInterner,
source: Source,
scopes: Vec<Scope<'ctx>>,
with_scope_count: u32,
arg_count: u32,
thunk_count: &'ctx mut usize,
thunk_scopes: Vec<ThunkScope<'id, 'ir>>,
}
fn should_thunk<'id>(ir: IrRef<'id, '_>, token: &GhostToken<'id>) -> bool {
!matches!(
ir.borrow(token),
Ir::Builtin(_)
| Ir::Builtins
| Ir::Int(_)
| Ir::Float(_)
| Ir::Bool(_)
| Ir::Null
| Ir::Str(_)
| Ir::Thunk(_)
)
}
impl<'ctx, 'id, 'ir> DowngradeCtx<'ctx, 'id, 'ir> {
fn new(
bump: &'ir Bump,
token: GhostToken<'id>,
symbols: &'ctx mut DefaultStringInterner,
global: &'ctx HashMap<StringId, Ir<'static, RawIrRef<'static>>>,
extra_scope: Option<Scope<'ctx>>,
thunk_count: &'ctx mut usize,
source: Source,
) -> Self {
Self {
bump,
token,
strings: symbols,
source,
scopes: std::iter::once(Scope::Global(global))
.chain(extra_scope)
.collect(),
thunk_count,
arg_count: 0,
with_scope_count: 0,
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
}
impl<'ctx: 'ir, 'id, 'ir> DowngradeContext<'id, 'ir> for DowngradeCtx<'ctx, 'id, 'ir> {
fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir> {
IrRef::new(self.bump.alloc(GhostCell::new(expr)))
}
fn new_arg(&mut self) -> ArgId {
self.arg_count += 1;
ArgId(self.arg_count - 1)
}
fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir> {
if !should_thunk(ir, &self.token) {
return ir;
}
let cached = self
.thunk_scopes
.last()
.expect("no active cache scope")
.lookup_cache(ir, &self.token);
if let Some(id) = cached {
return IrRef::alloc(self.bump, Ir::Thunk(id));
}
let id = ThunkId(*self.thunk_count);
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
self.thunk_scopes
.last_mut()
.expect("no active cache scope")
.add_binding(id, ir, &self.token);
IrRef::alloc(self.bump, Ir::Thunk(id))
}
fn new_sym(&mut self, sym: String) -> StringId {
StringId(self.strings.get_or_intern(sym))
}
fn get_sym(&self, id: StringId) -> Symbol<'_> {
self.strings.resolve(id.0).expect("no symbol found").into()
}
fn lookup(&self, sym: StringId, span: TextRange) -> Result<IrRef<'id, 'ir>> {
for scope in self.scopes.iter().rev() {
match scope {
&Scope::Global(global_scope) => {
if let Some(expr) = global_scope.get(&sym) {
let ir = match expr {
Ir::Builtins => Ir::Builtins,
Ir::Builtin(s) => Ir::Builtin(*s),
Ir::Bool(b) => Ir::Bool(*b),
Ir::Null => Ir::Null,
_ => unreachable!("globals should only contain leaf IR nodes"),
};
return Ok(self.new_expr(ir));
}
}
&Scope::Repl(repl_bindings) => {
if repl_bindings.contains(&sym) {
return Ok(self.new_expr(Ir::ReplBinding(sym)));
}
}
Scope::ScopedImport(scoped_bindings) => {
if scoped_bindings.contains(&sym) {
return Ok(self.new_expr(Ir::ScopedImportBinding(sym)));
}
}
Scope::Let(let_scope) => {
if let Some(&expr) = let_scope.get(&sym) {
return Ok(self.new_expr(Ir::Thunk(expr)));
}
}
&Scope::Param(param_sym, id) => {
if param_sym == sym {
return Ok(self.new_expr(Ir::Arg(id)));
}
}
}
}
if self.with_scope_count > 0 {
Ok(self.new_expr(Ir::WithLookup(sym)))
} else {
Err(Error::downgrade_error(
format!("'{}' not found", self.get_sym(sym)),
self.get_current_source(),
span,
))
}
}
fn get_current_source(&self) -> Source {
self.source.clone()
}
fn with_let_scope<F, R>(&mut self, keys: &[StringId], f: F) -> Result<R>
where
F: FnOnce(&mut Self) -> Result<(bumpalo::collections::Vec<'ir, IrRef<'id, 'ir>>, R)>,
{
let base = *self.thunk_count;
*self.thunk_count = self
.thunk_count
.checked_add(keys.len())
.expect("thunk id overflow");
let iter = keys.iter().enumerate().map(|(offset, &key)| {
(
key,
ThunkId(unsafe { base.checked_add(offset).unwrap_unchecked() }),
)
});
self.scopes.push(Scope::Let(iter.collect()));
let (vals, ret) = {
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())?
};
assert_eq!(keys.len(), vals.len());
let scope = self.thunk_scopes.last_mut().expect("no active thunk scope");
scope.extend_bindings((base..base + keys.len()).map(ThunkId).zip(vals));
Ok(ret)
}
fn with_param_scope<F, R>(&mut self, param: StringId, arg: ArgId, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.scopes.push(Scope::Param(param, arg));
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())
}
fn with_with_scope<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.with_scope_count += 1;
let ret = f(self);
self.with_scope_count -= 1;
ret
}
fn with_thunk_scope<F, R>(
&mut self,
f: F,
) -> (
R,
bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>,
)
where
F: FnOnce(&mut Self) -> R,
{
self.thunk_scopes.push(ThunkScope::new_in(self.bump));
let ret = f(self);
(
ret,
self.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings,
)
}
fn bump(&self) -> &'ir bumpalo::Bump {
self.bump
}
}
impl<'id, 'ir, 'ctx: 'ir> DowngradeCtx<'ctx, 'id, 'ir> {
fn downgrade_toplevel(mut self, root: rnix::ast::Expr) -> Result<RawIrRef<'ir>> {
let body = root.downgrade(&mut self)?;
let thunks = self
.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings;
let ir = IrRef::alloc(self.bump, Ir::TopLevel { body, thunks });
Ok(ir.freeze(self.token))
}
}
struct ThunkScope<'id, 'ir> {
bindings: bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>,
cache: HashTable<(IrRef<'id, 'ir>, ThunkId)>,
hasher: DefaultHashBuilder,
}
impl<'id, 'ir> ThunkScope<'id, 'ir> {
fn new_in(bump: &'ir Bump) -> Self {
Self {
bindings: bumpalo::collections::Vec::new_in(bump),
cache: HashTable::new(),
hasher: DefaultHashBuilder::default(),
}
}
fn lookup_cache(&self, key: IrRef<'id, 'ir>, token: &GhostToken<'id>) -> Option<ThunkId> {
let hash = self.hasher.hash_one(IrKey(key, token));
self.cache
.find(hash, |&(ir, _)| ir_content_eq(key, ir, token))
.map(|&(_, id)| id)
}
fn add_binding(&mut self, id: ThunkId, ir: IrRef<'id, 'ir>, token: &GhostToken<'id>) {
self.bindings.push((id, ir));
let hash = self.hasher.hash_one(IrKey(ir, token));
self.cache.insert_unique(hash, (ir, id), |&(ir, _)| {
self.hasher.hash_one(IrKey(ir, token))
});
}
fn extend_bindings(&mut self, iter: impl IntoIterator<Item = (ThunkId, IrRef<'id, 'ir>)>) {
self.bindings.extend(iter);
}
}
enum Scope<'ctx> {
Global(&'ctx HashMap<StringId, Ir<'static, RawIrRef<'static>>>),
Repl(&'ctx HashSet<StringId>),
ScopedImport(HashSet<StringId>),
Let(HashMap<StringId, ThunkId>),
Param(StringId, ArgId),
}
struct ScopeGuard<'a, 'ctx, 'id, 'ir> {
ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir>,
}
impl Drop for ScopeGuard<'_, '_, '_, '_> {
fn drop(&mut self) {
self.ctx.scopes.pop();
}
}
impl<'id, 'ir, 'ctx> ScopeGuard<'_, 'ctx, 'id, 'ir> {
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir> {
self.ctx
}
}
struct OwnedIr {
_bump: Bump,
ir: RawIrRef<'static>,
}
impl OwnedIr {
unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self {
Self {
_bump: bump,
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) }
}
}
fn as_ref(&self) -> RawIrRef<'_> {
self.ir
}
}
impl BytecodeContext for Runtime {
fn intern_string(&mut self, s: &str) -> StringId {
StringId(self.strings.get_or_intern(s))
}
fn register_span(&mut self, range: TextRange) -> u32 {
let id = self.spans.len();
let source_id = self
.sources
.len()
.checked_sub(1)
.expect("current_source not set");
self.spans.push((source_id, range));
id as u32
}
fn get_code(&self) -> &[u8] {
&self.bytecode
}
fn get_code_mut(&mut self) -> &mut Vec<u8> {
&mut self.bytecode
}
}
-51
View File
@@ -1,51 +0,0 @@
use hashbrown::HashMap;
use string_interner::DefaultStringInterner;
use crate::ir::{Ir, RawIrRef, StringId};
pub(super) fn new_builtins_env(
interner: &mut DefaultStringInterner,
) -> HashMap<StringId, Ir<'static, RawIrRef<'static>>> {
let mut builtins = HashMap::new();
let builtins_sym = StringId(interner.get_or_intern("builtins"));
builtins.insert(builtins_sym, Ir::Builtins);
let free_globals = [
"abort",
"baseNameOf",
"break",
"dirOf",
"derivation",
"derivationStrict",
"fetchGit",
"fetchMercurial",
"fetchTarball",
"fetchTree",
"fromTOML",
"import",
"isNull",
"map",
"placeholder",
"removeAttrs",
"scopedImport",
"throw",
"toString",
];
let consts = [
("true", Ir::Bool(true)),
("false", Ir::Bool(false)),
("null", Ir::Null),
];
for name in free_globals {
let name = StringId(interner.get_or_intern(name));
let value = Ir::Builtin(name);
builtins.insert(name, value);
}
for (name, value) in consts {
let name = StringId(interner.get_or_intern(name));
builtins.insert(name, value);
}
builtins
}
-89
View File
@@ -1,89 +0,0 @@
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;
fn trace<U: gc_arena::collect::Trace<'gc>>(&self, cc: &mut U) {
for item in self.inner[..self.len].iter() {
unsafe {
item.assume_init_ref().trace(cc);
}
}
}
}
impl<const N: usize, T> Default for Stack<N, T> {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize, T> Stack<N, T> {
pub(super) fn new() -> Self {
Self {
inner: Box::new([const { MaybeUninit::uninit() }; N]),
len: 0,
}
}
pub(super) fn push(&mut self, val: T) -> Result<(), T> {
if self.len == N {
return Err(val);
}
unsafe {
self.inner.get_unchecked_mut(self.len).write(val);
}
self.len += 1;
Ok(())
}
pub(super) fn pop(&mut self) -> Option<T> {
if self.len == 0 {
return None;
}
let ret = unsafe {
self.inner
.get_unchecked_mut(self.len - 1)
.assume_init_read()
};
self.len -= 1;
Some(ret)
}
pub(super) fn tos(&self) -> Option<&T> {
if self.len == 0 {
return None;
}
Some(unsafe { self.inner.get_unchecked(self.len - 1).assume_init_ref() })
}
pub(super) fn tos_mut(&mut self) -> Option<&mut T> {
if self.len == 0 {
return None;
}
Some(unsafe { self.inner.get_unchecked_mut(self.len - 1).assume_init_mut() })
}
pub(super) fn len(&self) -> usize {
self.len
}
pub(super) fn pop_n<const M: usize>(&mut self, n: usize) -> SmallVec<[T; M]> {
assert!(n <= self.len, "pop_n: not enough items on stack");
let mut result = SmallVec::new();
let start = self.len - n;
for i in start..self.len {
result.push(unsafe { self.inner.get_unchecked(i).assume_init_read() });
}
self.len = start;
result
}
}
-489
View File
@@ -1,489 +0,0 @@
use std::fmt;
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 sealed::sealed;
use smallvec::SmallVec;
use string_interner::{Symbol, symbol::SymbolU32};
use crate::ir::StringId;
#[sealed]
pub(crate) trait Storable {
const TAG: (bool, u8);
}
pub(crate) trait InlineStorable: Storable + RawStore {}
pub(crate) trait GcStorable: Storable {}
macro_rules! define_value_types {
(
inline { $($itype:ty => $itag:expr, $iname:literal;)* }
gc { $($gtype:ty => $gtag:expr, $gname:literal;)* }
) => {
$(
#[sealed]
impl Storable for $itype {
const TAG: (bool, u8) = $itag;
}
impl InlineStorable for $itype {}
)*
$(
#[sealed]
impl Storable for $gtype {
const TAG: (bool, u8) = $gtag;
}
impl GcStorable for $gtype {}
)*
const _: () = assert!(size_of::<Value<'static>>() == 8);
$(const _: () = assert!(size_of::<$itype>() <= 6);)*
$(const _: () = { let (_, val) = $itag; assert!(val >= 1 && val <= 7); };)*
$(const _: () = { let (_, val) = $gtag; assert!(val >= 1 && val <= 7); };)*
const _: () = {
let tags: &[(bool, u8)] = &[$($itag),*, $($gtag),*];
let mut mask_false: u8 = 0;
let mut mask_true: u8 = 0;
let mut i = 0;
while i < tags.len() {
let (neg, val) = tags[i];
let bit = 1 << val;
if neg {
assert!(mask_true & bit == 0, "duplicate true tag id");
mask_true |= bit;
} else {
assert!(mask_false & bit == 0, "duplicate false tag id");
mask_false |= bit;
}
i += 1;
}
};
unsafe impl<'gc> Collect<'gc> for Value<'gc> {
const NEEDS_TRACE: bool = true;
fn trace<T: Trace<'gc>>(&self, cc: &mut T) {
let Some(tag) = self.raw.tag() else { return };
match tag.neg_val() {
$(<$gtype as Storable>::TAG => unsafe {
self.load_gc::<$gtype>().trace(cc)
},)*
$(<$itype as Storable>::TAG => (),)*
(neg, val) => unreachable!("invalid tag: neg={neg}, val={val}"),
}
}
}
impl fmt::Debug for Value<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.tag() {
None => write!(f, "Float({:?})", unsafe {
self.raw.float().unwrap_unchecked()
}),
$(Some(<$itype as Storable>::TAG) => write!(f, "{}({:?})", $iname, unsafe {
self.as_inline::<$itype>().unwrap_unchecked()
}),)*
$(Some(<$gtype as Storable>::TAG) =>
write!(f, "{}({:?})", $gname, unsafe { self.as_gc::<$gtype>().unwrap_unchecked() }),)*
Some((neg, val)) => write!(f, "Unknown(neg={neg}, val={val})"),
}
}
}
};
}
define_value_types! {
inline {
i32 => (false, 1), "SmallInt";
bool => (false, 2), "Bool";
Null => (false, 3), "Null";
StringId => (false, 4), "SmallString";
PrimOp => (false, 5), "PrimOp";
}
gc {
i64 => (false, 6), "BigInt";
NixString => (false, 7), "String";
AttrSet<'_> => (true, 1), "AttrSet";
List<'_> => (true, 2), "List";
Thunk<'_> => (true, 3), "Thunk";
Closure<'_> => (true, 4), "Closure";
PrimOpApp<'_> => (true, 5), "PrimOpApp";
}
}
/// # Nix runtime value representation
///
/// NaN-boxed value fitting in 8 bytes.
#[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 {
Self::new_inline(Null)
}
}
impl<'gc> Value<'gc> {
#[inline(always)]
fn mk_tag(neg: bool, val: u8) -> RawTag {
// Safety: val is asserted to be in 1..=7.
unsafe { RawTag::new_unchecked(neg, val) }
}
#[inline(always)]
fn from_raw_value(rv: RawValue) -> Self {
Self {
raw: RawBox::from_value(rv),
_marker: PhantomData,
}
}
/// Load a GC pointer from a value with a negative tag.
///
/// # Safety
///
/// The value must actually store a `Gc<'gc, T>` with the matching type.
#[inline(always)]
unsafe fn load_gc<T: GcStorable>(&self) -> Gc<'gc, T> {
unsafe {
let rv = self.raw.value().unwrap_unchecked();
let ptr: *const T = <*const T as RawStore>::from_val(rv);
Gc::from_ptr(ptr)
}
}
/// Returns the `(negative, val)` tag, or `None` for a float.
#[inline(always)]
fn tag(&self) -> Option<(bool, u8)> {
self.raw.tag().map(|t| t.neg_val())
}
}
impl<'gc> Value<'gc> {
#[inline]
pub(crate) fn new_float(val: f64) -> Self {
Self {
raw: RawBox::from_float(val),
_marker: PhantomData,
}
}
#[inline]
pub(crate) fn new_inline<T: InlineStorable>(val: T) -> Self {
Self::from_raw_value(RawValue::store(Self::mk_tag(T::TAG.0, T::TAG.1), val))
}
#[inline]
pub(crate) fn new_gc<T: GcStorable>(gc: Gc<'gc, T>) -> Self {
let ptr = Gc::as_ptr(gc);
Self::from_raw_value(RawValue::store(Self::mk_tag(T::TAG.0, T::TAG.1), ptr))
}
}
impl<'gc> Value<'gc> {
#[inline]
pub(crate) fn is_float(&self) -> bool {
self.raw.is_float()
}
#[inline]
pub(crate) fn is<T: Storable>(&self) -> bool {
self.tag() == Some(T::TAG)
}
}
impl<'gc> Value<'gc> {
#[inline]
pub(crate) fn as_float(&self) -> Option<f64> {
self.raw.float().copied()
}
#[inline]
pub(crate) fn as_inline<T: InlineStorable>(&self) -> Option<T> {
if self.is::<T>() {
Some(unsafe {
let rv = self.raw.value().unwrap_unchecked();
T::from_val(rv)
})
} else {
None
}
}
#[inline]
pub(crate) fn as_gc<T: GcStorable>(&self) -> Option<Gc<'gc, T>> {
if self.is::<T>() {
Some(unsafe {
let rv = self.raw.value().unwrap_unchecked();
let ptr: *const T = <*const T as RawStore>::from_val(rv);
Gc::from_ptr(ptr)
})
} else {
None
}
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Null;
impl RawStore for Null {
fn to_val(self, value: &mut RawValue) {
value.set_data([0; 6]);
}
fn from_val(_: &RawValue) -> Self {
Self
}
}
impl RawStore for StringId {
fn to_val(self, value: &mut RawValue) {
(self.0.to_usize() as u32).to_val(value);
}
fn from_val(value: &RawValue) -> Self {
Self(
SymbolU32::try_from_usize(u32::from_val(value) as usize)
.expect("failed to read StringId from Value"),
)
}
}
/// Heap-allocated Nix string.
///
/// Stored on the GC heap via `Gc<'gc, NixString>`. The string data itself
/// lives in a standard `Box<str>` owned by this struct; the GC only manages
/// the outer allocation.
#[derive(Collect)]
#[collect(require_static)]
pub(crate) struct NixString {
data: Box<str>,
// TODO: string context for derivation dependency tracking
}
impl NixString {
pub(crate) fn new(s: impl Into<Box<str>>) -> Self {
Self { data: s.into() }
}
pub(crate) fn as_str(&self) -> &str {
&self.data
}
}
impl fmt::Debug for NixString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.data, f)
}
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) struct AttrSet<'gc> {
pub(crate) entries: SmallVec<[(StringId, Value<'gc>); 4]>,
}
impl<'gc> AttrSet<'gc> {
pub(crate) fn from_sorted(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self {
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
Self { entries }
}
pub(crate) fn lookup(&self, key: StringId) -> Option<Value<'gc>> {
self.entries
.binary_search_by_key(&key, |(k, _)| *k)
.ok()
.map(|i| self.entries[i].1.clone())
}
pub(crate) fn has(&self, key: StringId) -> bool {
self.entries
.binary_search_by_key(&key, |(k, _)| *k)
.is_ok()
}
pub(crate) fn merge(&self, other: &Self, mc: &Mutation<'gc>) -> Gc<'gc, Self> {
use std::cmp::Ordering::*;
debug_assert!(self.entries.is_sorted_by_key(|(key, _)| *key));
debug_assert!(other.entries.is_sorted_by_key(|(key, _)| *key));
let mut entries = SmallVec::new();
let mut i = 0;
let mut j = 0;
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());
i += 1;
}
Greater => {
entries.push(other.entries[j].clone());
j += 1;
}
Equal => {
entries.push(other.entries[j].clone());
i += 1;
j += 1;
}
}
}
entries.extend(other.entries[j..].iter().cloned());
entries.extend(self.entries[i..].iter().cloned());
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
Gc::new(mc, AttrSet { entries })
}
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) struct List<'gc> {
pub(crate) inner: SmallVec<[Value<'gc>; 4]>,
}
pub(crate) type Thunk<'gc> = RefLock<ThunkState<'gc>>;
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) enum ThunkState<'gc> {
Pending {
ip: u32,
env: Gc<'gc, RefLock<Env<'gc>>>,
},
Blackhole,
Evaluated(Value<'gc>),
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) struct Env<'gc> {
pub(crate) locals: SmallVec<[Value<'gc>; 4]>,
pub(crate) prev: Option<Gc<'gc, RefLock<Env<'gc>>>>,
}
impl<'gc> Env<'gc> {
pub(crate) fn empty() -> Self {
Env {
locals: SmallVec::new(),
prev: None,
}
}
pub(crate) fn with_arg(
arg: Value<'gc>,
n_locals: u32,
prev: Gc<'gc, RefLock<Env<'gc>>>,
) -> Self {
let mut locals = smallvec::smallvec![Value::default(); 1 + n_locals as usize];
locals[0] = arg;
Env {
locals,
prev: Some(prev),
}
}
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) struct Closure<'gc> {
pub(crate) ip: u32,
pub(crate) n_locals: u32,
pub(crate) env: Gc<'gc, RefLock<Env<'gc>>>,
pub(crate) pattern: Option<Gc<'gc, PatternInfo>>,
}
#[derive(Collect, Debug)]
#[collect(require_static)]
pub(crate) struct PatternInfo {
pub(crate) required: SmallVec<[StringId; 4]>,
pub(crate) optional: SmallVec<[StringId; 4]>,
pub(crate) ellipsis: bool,
pub(crate) param_spans: Box<[(StringId, u32)]>,
}
#[derive(Clone, Copy, Debug, Collect)]
#[collect(require_static)]
pub(crate) struct PrimOp {
pub(crate) id: u8,
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]);
}
fn from_val(value: &RawValue) -> Self {
let [.., id, arity] = *value.data();
Self { id, arity }
}
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) struct PrimOpApp<'gc> {
pub(crate) primop: PrimOp,
pub(crate) args: SmallVec<[Value<'gc>; 2]>,
}
#[repr(transparent)]
pub(crate) struct StrictValue<'gc>(Value<'gc>);
impl<'gc> StrictValue<'gc> {
#[inline]
pub(crate) fn try_from_forced(val: Value<'gc>) -> Option<Self> {
if !val.is::<Thunk<'gc>>() {
Some(Self(val))
} else {
None
}
}
#[inline]
pub(crate) fn into_relaxed(self) -> Value<'gc> {
self.0
}
}
impl<'gc> Deref for StrictValue<'gc> {
type Target = Value<'gc>;
#[inline]
fn deref(&self) -> &Value<'gc> {
&self.0
}
}
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)
}
}
unsafe impl<'gc> Collect<'gc> for StrictValue<'gc> {
const NEEDS_TRACE: bool = true;
fn trace<T: gc_arena::collect::Trace<'gc>>(&self, cc: &mut T) {
self.0.trace(cc);
}
}
File diff suppressed because it is too large. Load diff
Generated
+9 -9
View File
@@ -8,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1773471952,
"narHash": "sha256-kIRggXyT8RzijtfvyRIzj+zIDWM2fnCp8t0X4BkkTVc=",
"lastModified": 1770966612,
"narHash": "sha256-S6k14z/JsDwX6zZyLucDBTOe/9RsvxH9GTUxHn2o4vc=",
"owner": "nix-community",
"repo": "fenix",
"rev": "a1b770adbc3f6c27485d03b90462ec414d4e1ce5",
"rev": "e90d48dcfaebac7ea7a5687888a2d0733be26343",
"type": "github"
},
"original": {
@@ -37,11 +37,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1773282481,
"narHash": "sha256-b/GV2ysM8mKHhinse2wz+uP37epUrSE+sAKXy/xvBY4=",
"lastModified": 1770841267,
"narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "fe416aaedd397cacb33a610b33d60ff2b431b127",
"rev": "ec7c70d12ce2fc37cb92aff673dcdca89d187bae",
"type": "github"
},
"original": {
@@ -61,11 +61,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1773326183,
"narHash": "sha256-tj3piRd9RnnP36HwHmQD4O4XZeowsH/rvMeyp9Pmot0=",
"lastModified": 1770934477,
"narHash": "sha256-GX0cINHhhzUbQHyDYN2Mc+ovb6Sx/4yrF95VVou9aW4=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "6254616e97f358e67b70dfc0463687f5f7911c1a",
"rev": "931cd553be123b11db1435ac7ea5657e62e5e601",
"type": "github"
},
"original": {
+28 -29
View File
@@ -1,21 +1,13 @@
[package]
name = "fix"
name = "nix-js"
version = "0.1.0"
edition = "2024"
[dependencies]
mimalloc = "0.1"
tokio = { version = "1.41", features = [
"rt-multi-thread",
"sync",
"net",
"io-util",
] }
nix-compat = { git = "https://git.snix.dev/snix/snix.git", version = "0.1.0", features = [
"wire",
"async",
] }
tokio = { version = "1.41", features = ["rt-multi-thread", "sync", "net", "io-util"] }
nix-compat = { git = "https://git.snix.dev/snix/snix.git", version = "0.1.0", features = ["wire", "async"] }
# REPL
anyhow = "1.0"
@@ -34,18 +26,17 @@ miette = { version = "7.4", features = ["fancy"] }
hashbrown = "0.16"
string-interner = "0.19"
bumpalo = { version = "3.20", features = [
"allocator-api2",
"boxed",
"collections",
] }
bumpalo = { version = "3.20", features = ["allocator-api2", "boxed", "collections"] }
rust-embed = "8.11"
rust-embed="8.11"
itertools = "0.14"
regex = "1.11"
deno_core = "0.385"
deno_error = "0.7"
nix-nar = "0.3"
sha2 = "0.10"
sha1 = "0.10"
@@ -54,10 +45,7 @@ hex = "0.4"
base64 = "0.22"
reqwest = { version = "0.13", features = [
"blocking",
"rustls",
], default-features = false }
reqwest = { version = "0.13", features = ["blocking", "rustls"], default-features = false }
tar = "0.4"
flate2 = "1.0"
xz2 = "0.1"
@@ -77,17 +65,24 @@ ere = "0.2.4"
num_enum = "0.7.5"
tap = "1.0.1"
# Inspector (optional)
fastwebsockets = { version = "0.10", features = ["upgrade"], optional = true }
hyper = { version = "1", features = ["http1", "server"], optional = true }
hyper-util = { version = "0.1", features = ["tokio"], optional = true }
http-body-util = { version = "0.1", optional = true }
http = { version = "1", optional = true }
uuid = { version = "1", features = ["v4"], optional = true }
ghost-cell = "0.2"
colored = "3.1"
boxing = { path = "../boxing" }
sealed = "0.6"
small-map = "0.1"
smallvec = "1.15"
boxing = "0.1"
gc-arena = { version = "0.5.3", features = ["allocator-api2"] }
allocator-api2 = "0.4.0"
smallvec = "1.15.1"
[dependencies.gc-arena]
git = "https://github.com/kyren/gc-arena"
rev = "75671ae03f53718357b741ed4027560f14e90836"
features = ["allocator-api2", "hashbrown", "smallvec"]
[features]
inspector = ["dep:fastwebsockets", "dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:http", "dep:uuid"]
prof = []
[dev-dependencies]
criterion = { version = "0.8", features = ["html_reports"] }
@@ -104,3 +99,7 @@ harness = false
[[bench]]
name = "thunk_scope"
harness = false
[[bench]]
name = "compile_time"
harness = false
File renamed without changes.
File renamed without changes.
+141
View File
@@ -0,0 +1,141 @@
mod utils;
use std::hint::black_box;
use criterion::{Criterion, criterion_group, criterion_main};
use nix_js::context::Context;
use utils::compile;
fn bench_parse_and_downgrade(c: &mut Criterion) {
let mut group = c.benchmark_group("parse_and_downgrade");
group.bench_function("simple_expression", |b| {
b.iter(|| {
compile(black_box("1 + 1"));
})
});
group.bench_function("complex_function", |b| {
b.iter(|| {
compile(black_box(
"let fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); in fib",
));
})
});
group.bench_function("large_attrset", |b| {
b.iter(|| {
compile(black_box(
"{ a = 1; b = 2; c = 3; d = 4; e = 5; f = 6; g = 7; h = 8; i = 9; j = 10; k = 11; l = 12; m = 13; n = 14; o = 15; }",
));
})
});
group.bench_function("nested_let_bindings", |b| {
b.iter(|| {
compile(black_box(
"let a = 1; b = 2; c = 3; in let d = a + b; e = b + c; in let f = d + e; in f",
));
})
});
group.finish();
}
fn bench_codegen(c: &mut Criterion) {
let mut group = c.benchmark_group("codegen");
group.bench_function("arithmetic_expression", |b| {
b.iter(|| compile(black_box("(1 + 2) * (3 - 4) / 5")))
});
group.bench_function("function_with_closure", |b| {
b.iter(|| compile(black_box("let x = 10; f = y: x + y; in f 5")))
});
group.bench_function("recursive_attrset", |b| {
b.iter(|| {
compile(black_box(
"rec { a = 1; b = a + 1; c = b + 1; d = c + 1; e = d + 1; }",
))
})
});
group.finish();
}
fn bench_full_pipeline(c: &mut Criterion) {
let mut group = c.benchmark_group("full_pipeline");
group.bench_function("simple_eval", |b| b.iter(|| compile(black_box("1 + 1"))));
group.bench_function("fibonacci_10", |b| {
b.iter(|| {
compile(black_box(
"let fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); in fib 10",
))
})
});
group.bench_function("map_operation", |b| {
b.iter(|| compile(black_box("map (x: x * 2) [1 2 3 4 5 6 7 8 9 10]")))
});
group.bench_function("complex_attrset_access", |b| {
b.iter(|| {
compile(black_box(
"let attrs = { a.b.c = { d.e = 42; }; }; in attrs.a.b.c.d.e",
))
})
});
group.bench_function("with_expression", |b| {
b.iter(|| {
compile(black_box(
"let attrs = { x = 1; y = 2; z = 3; }; in with attrs; x + y + z",
))
})
});
group.finish();
}
fn bench_context_creation(c: &mut Criterion) {
c.bench_function("context_new", |b| {
b.iter(|| {
let _ = Context::new();
})
});
}
fn bench_symbol_interning(c: &mut Criterion) {
let mut group = c.benchmark_group("symbol_interning");
group.bench_function("many_unique_symbols", |b| {
b.iter(|| {
compile(black_box(
"let a1 = 1; a2 = 2; a3 = 3; a4 = 4; a5 = 5; a6 = 6; a7 = 7; a8 = 8; a9 = 9; a10 = 10; in a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10",
))
})
});
group.bench_function("repeated_symbols", |b| {
b.iter(|| {
compile(black_box(
"let x = 1; y = x; z = x; a = x; b = x; c = x; in x + y + z + a + b + c",
))
})
});
group.finish();
}
criterion_group!(
benches,
bench_parse_and_downgrade,
bench_codegen,
bench_full_pipeline,
bench_context_creation,
bench_symbol_interning
);
criterion_main!(benches);
File renamed without changes.
+25
View File
@@ -0,0 +1,25 @@
#![allow(dead_code)]
use nix_js::context::Context;
use nix_js::error::{Result, Source};
use nix_js::value::Value;
pub fn eval(expr: &str) -> Value {
Context::new()
.unwrap()
.eval(Source::new_eval(expr.into()).unwrap())
.unwrap()
}
pub fn eval_result(expr: &str) -> Result<Value> {
Context::new()
.unwrap()
.eval(Source::new_eval(expr.into()).unwrap())
}
pub fn compile(expr: &str) -> String {
Context::new()
.unwrap()
.compile(Source::new_eval(expr.into()).unwrap())
.unwrap()
}
+2
View File
@@ -0,0 +1,2 @@
/node_modules
/dist
+8
View File
@@ -0,0 +1,8 @@
import * as esbuild from "esbuild";
await esbuild.build({
entryPoints: ["src/index.ts"],
outfile: "dist/runtime.js",
bundle: true,
// minify: true,
});
+20
View File
@@ -0,0 +1,20 @@
import js from "@eslint/js";
import { defineConfig } from "eslint/config";
import globals from "globals";
import tseslint from "typescript-eslint";
export default defineConfig([
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
languageOptions: { globals: globals.es2022 },
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { varsIgnorePattern: "^_", argsIgnorePattern: "^_" }],
},
},
{
ignores: ["dist/**/*"],
},
]);
+1967
View File
File diff suppressed because it is too large. Load diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "nix-js-runtime",
"version": "0.1.0",
"private": true,
"scripts": {
"check": "tsc --noEmit && npx eslint && biome check",
"typecheck": "tsc --noEmit",
"build": "node build.mjs",
"dev": "npm run typecheck && npm run build"
},
"devDependencies": {
"esbuild": "^0.24.2",
"eslint": "^9.39.2",
"typescript": "^5.7.2",
"typescript-eslint": "^8.55.0",
"jiti": "^2.6.1"
},
"dependencies": {
"globals": "^17.3.0",
"js-sdsl": "^4.4.2"
}
}
@@ -0,0 +1,65 @@
import { op } from "../operators";
import { coerceNumeric, forceInt, forceNumeric } from "../type-assert";
import type { NixBool, NixInt, NixNumber, NixValue } from "../types";
export const add =
(a: NixValue) =>
(b: NixValue): bigint | number => {
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as never) + (bv as never);
};
export const sub =
(a: NixValue) =>
(b: NixValue): bigint | number => {
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as never) - (bv as never);
};
export const mul =
(a: NixValue) =>
(b: NixValue): bigint | number => {
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as never) * (bv as never);
};
export const div =
(a: NixValue) =>
(b: NixValue): NixNumber => {
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
if (bv === 0 || bv === 0n) {
throw new RangeError("Division by zero");
}
return (av as never) / (bv as never);
};
export const bitAnd =
(a: NixValue) =>
(b: NixValue): NixInt => {
const av = forceInt(a);
const bv = forceInt(b);
return av & bv;
};
export const bitOr =
(a: NixValue) =>
(b: NixValue): NixInt => {
const av = forceInt(a);
const bv = forceInt(b);
return av | bv;
};
export const bitXor =
(a: NixValue) =>
(b: NixValue): NixInt => {
const av = forceInt(a);
const bv = forceInt(b);
return av ^ bv;
};
export const lessThan =
(a: NixValue) =>
(b: NixValue): NixBool =>
op.lt(a, b);
+159
View File
@@ -0,0 +1,159 @@
import { mkPos, select } from "../helpers";
import { createThunk } from "../thunk";
import { forceAttrs, forceFunction, forceList, forceStringValue } from "../type-assert";
import { ATTR_POSITIONS, type NixAttrs, type NixList, type NixValue } from "../types";
export const attrNames = (set: NixValue): string[] => Array.from(forceAttrs(set).keys()).sort();
export const attrValues = (set: NixValue): NixValue[] =>
Array.from(forceAttrs(set).entries())
.sort(([a], [b]) => {
if (a < b) {
return -1;
} else if (a === b) {
return 0;
} else {
return 1;
}
})
.map(([_, val]) => val);
export const getAttr =
(s: NixValue) =>
(set: NixValue): NixValue =>
select(forceAttrs(set), [s]);
export const hasAttr =
(s: NixValue) =>
(set: NixValue): boolean =>
forceAttrs(set).has(forceStringValue(s));
export const mapAttrs =
(f: NixValue) =>
(attrs: NixValue): NixAttrs => {
const forcedAttrs = forceAttrs(attrs);
const forcedF = forceFunction(f);
const newAttrs: NixAttrs = new Map();
for (const [key, val] of forcedAttrs) {
newAttrs.set(
key,
createThunk(() => forceFunction(forcedF(key))(val), "created by mapAttrs"),
);
}
return newAttrs;
};
export const removeAttrs =
(attrs: NixValue) =>
(list: NixValue): NixAttrs => {
const newAttrs: NixAttrs = new Map(forceAttrs(attrs));
const forcedList = forceList(list);
for (const item of forcedList) {
newAttrs.delete(forceStringValue(item));
}
return newAttrs;
};
export const listToAttrs = (e: NixValue): NixAttrs => {
const attrs: NixAttrs = new Map();
const forcedE = [...forceList(e)].reverse();
for (const obj of forcedE) {
const item = forceAttrs(obj);
attrs.set(forceStringValue(select(item, ["name"])), select(item, ["value"]));
}
return attrs;
};
export const intersectAttrs =
(e1: NixValue) =>
(e2: NixValue): NixAttrs => {
const f1 = forceAttrs(e1);
const f2 = forceAttrs(e2);
const attrs: NixAttrs = new Map();
if (f1.size < f2.size) {
for (const [key] of f1) {
if (f2.has(key)) {
attrs.set(key, f2.get(key) as NixValue);
}
}
} else {
for (const [key] of f2) {
if (f1.has(key)) {
attrs.set(key, f2.get(key) as NixValue);
}
}
}
return attrs;
};
export const catAttrs =
(attr: NixValue) =>
(list: NixValue): NixList => {
const key = forceStringValue(attr);
return forceList(list)
.map((set) => forceAttrs(set).get(key))
.filter((val) => val !== undefined) as NixList;
};
export const groupBy =
(f: NixValue) =>
(list: NixValue): NixAttrs => {
const attrs: NixAttrs = new Map();
const forcedF = forceFunction(f);
const forcedList = forceList(list);
for (const elem of forcedList) {
const key = forceStringValue(forcedF(elem));
if (!attrs.has(key)) attrs.set(key, []);
(attrs.get(key) as NixList).push(elem);
}
return attrs;
};
export const zipAttrsWith =
(f: NixValue) =>
(list: NixValue): NixValue => {
const listForced = forceList(list);
const attrMap = new Map<string, NixValue[]>();
for (const item of listForced) {
const attrs = forceAttrs(item);
for (const [key, value] of attrs) {
if (!attrMap.has(key)) {
attrMap.set(key, []);
}
(attrMap.get(key) as NixValue[]).push(value);
}
}
const result: NixAttrs = new Map();
for (const [name, values] of attrMap.entries()) {
result.set(
name,
createThunk(() => forceFunction(forceFunction(f)(name))(values)),
);
}
return result;
};
export const unsafeGetAttrPos =
(attrName: NixValue) =>
(attrSet: NixValue): NixValue => {
const name = forceStringValue(attrName);
const attrs = forceAttrs(attrSet);
if (!attrs.has(name)) {
return null;
}
const positions = attrs[ATTR_POSITIONS];
if (!positions || !positions.has(name)) {
return null;
}
const span = positions.get(name) as number;
return mkPos(span);
};
+202
View File
@@ -0,0 +1,202 @@
import {
decodeContextElem,
getStringContext,
getStringValue,
mkStringWithContext,
type NixStringContext,
parseContextToInfoMap,
} from "../string-context";
import { force } from "../thunk";
import { forceAttrs, forceList, forceString, forceStringValue } from "../type-assert";
import type { NixAttrs, NixString, NixValue } from "../types";
import { isStringWithContext } from "../types";
/**
* builtins.hasContext - Check if string has context
*
* Returns true if the string has any store path references.
*/
export const hasContext = (value: NixValue): boolean => {
const s = forceString(value);
return isStringWithContext(s) && s.context.size > 0;
};
/**
* builtins.unsafeDiscardStringContext - Remove all context from string
*
* IMPORTANT: This discards string context, returning only the string value.
* Use with caution as it removes derivation dependencies.
*/
export const unsafeDiscardStringContext = (value: NixValue): string => {
const s = forceString(value);
return getStringValue(s);
};
/**
* builtins.unsafeDiscardOutputDependency - Convert DrvDeep to Opaque context
*
* IMPORTANT: Transforms "all outputs" references (=) to plain path references.
* Preserves other context types unchanged.
*/
export const unsafeDiscardOutputDependency = (value: NixValue): NixString => {
const s = forceString(value);
const strValue = getStringValue(s);
const context = getStringContext(s);
if (context.size === 0) {
return strValue;
}
const newContext: NixStringContext = new Set();
for (const encoded of context) {
const elem = decodeContextElem(encoded);
if (elem.type === "drvDeep") {
newContext.add(elem.drvPath);
} else {
newContext.add(encoded);
}
}
if (newContext.size === 0) {
return strValue;
}
return mkStringWithContext(strValue, newContext);
};
/**
* builtins.addDrvOutputDependencies - Convert Opaque to DrvDeep context
*
* IMPORTANT: Transforms plain derivation path references to "all outputs" references (=).
* The string must have exactly one context element which must be a .drv path.
*/
export const addDrvOutputDependencies = (value: NixValue): NixString => {
const s = forceString(value);
const strValue = getStringValue(s);
const context = getStringContext(s);
if (context.size !== 1) {
throw new Error(`context of string '${strValue}' must have exactly one element, but has ${context.size}`);
}
const [encoded] = context;
const elem = decodeContextElem(encoded);
if (elem.type === "drvDeep") {
return s;
}
if (elem.type === "built") {
throw new Error(
`\`addDrvOutputDependencies\` can only act on derivations, not on a derivation output such as '${elem.output}'`,
);
}
if (!elem.path.endsWith(".drv")) {
throw new Error(`path '${elem.path}' is not a derivation`);
}
const newContext: NixStringContext = new Set([`=${elem.path}`]);
return mkStringWithContext(strValue, newContext);
};
/**
* builtins.getContext - Extract context as structured attribute set
*
* Returns an attribute set mapping store paths to their context info:
* - path: true if it's a plain store path reference (opaque)
* - allOutputs: true if it references all derivation outputs (drvDeep, encoded as =path)
* - outputs: list of specific output names (built, encoded as !output!path)
*/
export const getContext = (value: NixValue): NixAttrs => {
const s = forceString(value);
const context = getStringContext(s);
const infoMap = parseContextToInfoMap(context);
const result: NixAttrs = new Map();
for (const [path, info] of infoMap) {
const attrs: NixAttrs = new Map();
if (info.path) {
attrs.set("path", true);
}
if (info.allOutputs) {
attrs.set("allOutputs", true);
}
if (info.outputs.length > 0) {
attrs.set("outputs", info.outputs);
}
result.set(path, attrs);
}
return result;
};
/**
* builtins.appendContext - Add context to a string
*
* IMPORTANT: Merges the provided context attribute set with any existing context
* from the input string. Used to manually construct strings with specific
* derivation dependencies.
*
* Context format matches getContext output:
* - path: boolean - add as opaque reference
* - allOutputs: boolean - add as drvDeep reference (=)
* - outputs: [string] - add as built references (!output!)
*/
export const appendContext =
(strValue: NixValue) =>
(ctxValue: NixValue): NixString => {
const s = forceString(strValue);
const strVal = getStringValue(s);
const existingContext = getStringContext(s);
const ctxAttrs = forceAttrs(ctxValue);
const newContext: NixStringContext = new Set(existingContext);
for (const [path, infoVal] of ctxAttrs) {
if (!path.startsWith("/nix/store/")) {
throw new Error(`context key '${path}' is not a store path`);
}
const info = forceAttrs(infoVal as NixValue);
if (info.has("path")) {
const pathVal = force(info.get("path") as NixValue);
if (pathVal === true) {
newContext.add(path);
}
}
if (info.has("allOutputs")) {
const allOutputs = force(info.get("allOutputs") as NixValue);
if (allOutputs === true) {
if (!path.endsWith(".drv")) {
throw new Error(
`tried to add all-outputs context of ${path}, which is not a derivation, to a string`,
);
}
newContext.add(`=${path}`);
}
}
if (info.has("outputs")) {
const outputs = forceList(info.get("outputs") as NixValue);
if (outputs.length > 0 && !path.endsWith(".drv")) {
throw new Error(
`tried to add derivation output context of ${path}, which is not a derivation, to a string`,
);
}
for (const output of outputs) {
const outputName = forceStringValue(output);
newContext.add(`!${outputName}!${path}`);
}
}
}
if (newContext.size === 0) {
return strVal;
}
return mkStringWithContext(strVal, newContext);
};
@@ -0,0 +1,371 @@
/**
* Conversion and serialization builtin functions
*/
import {
addBuiltContext,
mkStringWithContext,
type NixStringContext,
StringWithContext,
} from "../string-context";
import { force, isThunk } from "../thunk";
import { forceFunction, forceStringNoCtx } from "../type-assert";
import type { NixString, NixValue } from "../types";
import { isNixPath, isStringWithContext, NixPath } from "../types";
import { isAttrs, isPath, typeOf } from "./type-check";
export const fromJSON = (e: NixValue): NixValue => {
const str = force(e);
if (typeof str !== "string" && !isStringWithContext(str)) {
throw new TypeError(`builtins.fromJSON: expected a string, got ${typeOf(str)}`);
}
const jsonStr = isStringWithContext(str) ? str.value : str;
return Deno.core.ops.op_from_json(jsonStr) as NixValue;
};
export const fromTOML = (e: NixValue): NixValue => {
const toml = forceStringNoCtx(e);
return Deno.core.ops.op_from_toml(toml) as NixValue;
};
export const toJSON = (e: NixValue): NixString => {
const context: Set<string> = new Set();
const string = JSON.stringify(nixValueToJson(e, true, context, true));
if (context.size === 0) {
return string;
}
return mkStringWithContext(string, context);
};
export const toXML = (e: NixValue): NixString => {
const [xml, context] = Deno.core.ops.op_to_xml(force(e));
if (context.length === 0) {
return xml;
}
return mkStringWithContext(xml, new Set(context));
};
/**
* String coercion modes control which types can be coerced to strings
*
* - Base: Only strings are allowed (no coercion)
* - Interpolation: Used in string interpolation "${expr}" - allows strings and integers
* - ToString: Used in builtins.toString - allows all types (bools, floats, null, lists, etc.)
*/
export enum StringCoercionMode {
Base = 0,
Interpolation = 1,
ToString = 2,
}
export interface CoerceResult {
value: string;
context: NixStringContext;
}
/**
* Coerce a Nix value to a string according to the specified mode.
* This implements the same behavior as Lix's EvalState::coerceToString.
*
* IMPORTANT: String context preservation rules:
* - StringWithContext: Context is collected in outContext parameter
* - Derivations (with outPath): Built context is added for the drvPath/outputName
* - Lists (ToString mode): Context from all elements is merged
* - All other coercions: No context added
*
* @param value - The value to coerce
* @param mode - The coercion mode (controls which types are allowed)
* @param copyToStore - If true, paths should be copied to the Nix store (not implemented yet)
* @param outContext - Optional context set to collect string contexts
* @returns The string representation of the value
* @throws TypeError if the value cannot be coerced in the given mode
*
* Coercion rules by type:
* - String: Always returns as-is
* - Path: Returns the path string (copyToStore not implemented yet)
* - Integer: Only in Interpolation or ToString mode
* - Float: Only in ToString mode
* - Boolean: Only in ToString mode (true → "1", false → "")
* - Null: Only in ToString mode (→ "")
* - List: Only in ToString mode (recursively coerce elements, join with spaces)
* - Attrs: Check for __toString method or outPath attribute
* - Function: Never coercible (throws error)
*/
export const coerceToString = (
value: NixValue,
mode: StringCoercionMode,
copyToStore: boolean = false,
outContext: NixStringContext,
): string => {
const v = force(value);
// Strings are always returned as-is, regardless of mode
if (typeof v === "string") {
return v;
}
if (isStringWithContext(v)) {
for (const elem of v.context) {
outContext.add(elem);
}
return v.value;
}
// Paths coerce to their string value
if (isNixPath(v)) {
if (copyToStore) {
const pathStr = v.value;
const storePath = Deno.core.ops.op_copy_path_to_store(pathStr);
outContext.add(storePath);
return storePath;
}
return v.value;
}
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
if (v instanceof Map) {
if (v.has("__toString")) {
const toStringMethod = forceFunction(v.get("__toString") as NixValue);
const result = force(toStringMethod(v));
return coerceToString(result, mode, copyToStore, outContext);
}
if (v.has("outPath")) {
const outPath = coerceToString(v.get("outPath") as NixValue, mode, copyToStore, outContext);
if (v.has("type") && v.get("type") === "derivation" && v.has("drvPath") && outContext) {
const drvPathValue = force(v.get("drvPath") as NixValue);
const drvPathStr = isStringWithContext(drvPathValue)
? drvPathValue.value
: typeof drvPathValue === "string"
? drvPathValue
: null;
if (drvPathStr) {
const outputName = v.has("outputName") ? String(force(v.get("outputName") as NixValue)) : "out";
addBuiltContext(outContext, drvPathStr, outputName);
}
}
return outPath;
}
}
throw new TypeError(`cannot coerce ${typeOf(v)} to a string`);
}
// Integer coercion is allowed in Interpolation and ToString modes
// This enables string interpolation like "value: ${42}"
if (mode >= StringCoercionMode.Interpolation) {
if (typeof v === "bigint") {
return v.toString();
}
}
// The following types are only coercible in ToString mode (builtins.toString)
if (mode >= StringCoercionMode.ToString) {
// Booleans: true → "1", false → ""
// This is for shell scripting convenience (same as null)
if (typeof v === "boolean") {
return v ? "1" : "";
}
// Floats are converted using JavaScript's default toString
if (typeof v === "number") {
return v.toString();
}
// Null becomes empty string (for shell scripting convenience)
if (v === null) {
return "";
}
// Lists are recursively converted and joined with spaces
// We cannot use Array.join() directly because of special spacing rules:
// - Elements are recursively coerced to strings
// - Spaces are added between elements, BUT:
// * No space is added after an element if it's an empty list
// * The last element never gets a trailing space
//
// Examples:
// - [ 1 2 3 ] → "1 2 3"
// - [ 1 [ ] 2 ] → "1 2" (empty list doesn't add space)
// - [ 1 [ [ ] ] 2 ] → "1 2" (nested empty list is not itself empty, so adds space)
// - [ [ 1 2 ] [ 3 4 ] ] → "1 2 3 4" (nested lists flatten)
if (Array.isArray(v)) {
let result = "";
for (let i = 0; i < v.length; i++) {
const item = v[i];
// Recursively convert element to string
const str = coerceToString(item, mode, copyToStore, outContext);
result += str;
// Add space after this element if:
// 1. It's not the last element, AND
// 2. The element is not an empty list
//
// Note: We check if the ELEMENT is an empty list, not if its
// string representation is empty.
// For example, [[]] is not an empty list (length 1), so it gets
// a trailing space even though its toString is "".
if (i < v.length - 1) {
const forcedItem = force(item);
if (!Array.isArray(forcedItem) || forcedItem.length !== 0) {
result += " ";
}
}
}
return result;
}
}
throw new TypeError(`cannot coerce ${typeOf(v)} to a string`);
};
/**
* Coerce a Nix value to a string with context tracking
*/
export const coerceToStringWithContext = (
value: NixValue,
mode: StringCoercionMode = StringCoercionMode.ToString,
copyToStore: boolean = false,
): NixString => {
const context: NixStringContext = new Set();
const str = coerceToString(value, mode, copyToStore, context);
if (context.size === 0) {
return str;
}
return mkStringWithContext(str, context);
};
/**
* Coerce a Nix value to an absolute path string.
* This implements the same behavior as Lix's EvalState::coerceToPath.
*
* @param value - The value to coerce
* @param outContext - Optional context set to collect string contexts
* @returns The absolute path string
* @throws TypeError if the value cannot be coerced to a string
* @throws Error if the result is not an absolute path
*
* Semantics:
* - Coerces to string using Strict mode (same as coerceToString with Base mode)
* - Validates the result is non-empty and starts with '/'
* - Returns the path string (not a NixPath object)
* - Preserves string context if present
*/
export const coerceToPath = (value: NixValue, outContext: NixStringContext): string => {
const forced = force(value);
if (isPath(forced)) {
return forced.value;
}
if (isAttrs(forced) && forced.has("__toString")) {
const toStringFunc = forceFunction(forced.get("__toString") as NixValue);
return coerceToPath(toStringFunc(forced), outContext);
}
const pathStr = coerceToString(value, StringCoercionMode.Base, false, outContext);
if (pathStr === "") {
throw new Error("string doesn't represent an absolute path: empty string");
}
if (pathStr[0] !== "/") {
throw new Error(`string '${pathStr}' doesn't represent an absolute path`);
}
return pathStr;
};
/**
* builtins.toString - Convert a value to a string
*
* This is the public builtin function exposed to Nix code.
* It uses ToString mode, which allows coercing all types except functions.
*
* @param value - The value to convert to a string
* @returns The string representation
*/
export const toStringFunc = (value: NixValue): NixString => {
return coerceToStringWithContext(value, StringCoercionMode.ToString, false);
};
export type JsonValue = number | boolean | string | null | { [key: string]: JsonValue } | Array<JsonValue>;
export const nixValueToJson = (
value: NixValue,
strict: boolean,
outContext: NixStringContext,
copyToStore: boolean,
seen: Set<NixValue> = new Set(),
): JsonValue => {
const v = strict ? force(value) : value;
if (isThunk(v) || typeof v === "function")
throw new Error(`cannot convert ${isThunk(v) ? "thunk" : "lambda"} to JSON`);
if (v === null) return null;
if (typeof v === "bigint") {
const num = Number(v);
if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {
console.warn(`integer ${v} exceeds safe range, precision may be lost`);
}
return num;
}
if (typeof v === "number") return v;
if (typeof v === "boolean") return v;
if (typeof v === "string") return v;
if (v instanceof StringWithContext) {
for (const elem of v.context) {
outContext.add(elem);
}
return v.value;
}
if (v instanceof NixPath) {
if (copyToStore) {
const storePath = Deno.core.ops.op_copy_path_to_store(v.value);
outContext.add(storePath);
return storePath;
} else {
return v.value;
}
}
// FIXME: is this check necessary?
// if (seen.has(v)) {
// throw new Error("cycle detected in toJSON");
// } else {
// seen.add(v)
// }
if (Array.isArray(v)) {
return v.map((item) => nixValueToJson(item, strict, outContext, copyToStore, seen));
}
if (v instanceof Map) {
if (v.has("__toString") && typeof force(v.get("__toString") as NixValue) === "function") {
const toStringMethod = force(v.get("__toString") as NixValue) as (self: typeof v) => NixValue;
const result = force(toStringMethod(v));
if (typeof result === "string") {
return result;
}
if (isStringWithContext(result)) {
for (const elem of result.context) {
outContext.add(elem);
}
return result.value;
}
return nixValueToJson(result, strict, outContext, copyToStore, seen);
}
if (v.has("outPath")) {
return nixValueToJson(v.get("outPath") as NixValue, strict, outContext, copyToStore, seen);
}
const result: { [key: string]: JsonValue } = {};
const keys = Array.from(v.keys()).sort();
for (const key of keys) {
result[key] = nixValueToJson(v.get(key) as NixValue, strict, outContext, copyToStore, seen);
}
return result;
}
throw new Error(`cannot convert ${typeof v} to JSON`);
};
@@ -0,0 +1,408 @@
import {
addBuiltContext,
addDrvDeepContext,
mkStringWithContext,
type NixStringContext,
} from "../string-context";
import { force } from "../thunk";
import { forceAttrs, forceList, forceStringNoCtx, forceStringValue } from "../type-assert";
import type { NixAttrs, NixValue } from "../types";
import { coerceToString, type JsonValue, nixValueToJson, StringCoercionMode } from "./conversion";
export interface OutputInfo {
path: string;
hashAlgo: string;
hash: string;
}
export interface DerivationData {
name: string;
outputs: Map<string, OutputInfo>;
inputDrvs: Map<string, Set<string>>;
inputSrcs: Set<string>;
platform: string;
builder: string;
args: string[];
env: Map<string, string>;
}
export const escapeString = (s: string): string => {
let result = "";
for (const char of s) {
switch (char) {
case '"':
result += '\\"';
break;
case "\\":
result += "\\\\";
break;
case "\n":
result += "\\n";
break;
case "\r":
result += "\\r";
break;
case "\t":
result += "\\t";
break;
default:
result += char;
}
}
return `"${result}"`;
};
const quoteString = (s: string): string => `"${s}"`;
const cmpByKey = <T>(a: [string, T], b: [string, T]): number => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
export const generateAterm = (drv: DerivationData): string => {
const outputEntries: string[] = [];
const sortedOutputs = Array.from(drv.outputs.entries()).sort(cmpByKey);
for (const [name, info] of sortedOutputs) {
outputEntries.push(
`(${quoteString(name)},${quoteString(info.path)},${quoteString(info.hashAlgo)},${quoteString(info.hash)})`,
);
}
const outputs = outputEntries.join(",");
const inputDrvEntries: string[] = [];
const sortedInputDrvs = Array.from(drv.inputDrvs.entries()).sort(cmpByKey);
for (const [drvPath, outputs] of sortedInputDrvs) {
const sortedOuts = Array.from(outputs).sort();
const outList = `[${sortedOuts.map(quoteString).join(",")}]`;
inputDrvEntries.push(`(${quoteString(drvPath)},${outList})`);
}
const inputDrvs = inputDrvEntries.join(",");
const sortedInputSrcs = Array.from(drv.inputSrcs).sort();
const inputSrcs = sortedInputSrcs.map(quoteString).join(",");
const args = drv.args.map(escapeString).join(",");
const envs = Array.from(drv.env.entries())
.sort(cmpByKey)
.map(([k, v]) => `(${escapeString(k)},${escapeString(v)})`);
return `Derive([${outputs}],[${inputDrvs}],[${inputSrcs}],${quoteString(drv.platform)},${escapeString(drv.builder)},[${args}],[${envs}])`;
};
export const generateAtermModulo = (drv: DerivationData, inputDrvHashes: Map<string, string>): string => {
const outputEntries: string[] = [];
const sortedOutputs = Array.from(drv.outputs.entries()).sort(cmpByKey);
for (const [name, info] of sortedOutputs) {
outputEntries.push(
`(${quoteString(name)},${quoteString(info.path)},${quoteString(info.hashAlgo)},${quoteString(info.hash)})`,
);
}
const outputs = outputEntries.join(",");
const inputDrvEntries: string[] = [];
const sortedInputDrvHashes = Array.from(inputDrvHashes.entries()).sort(cmpByKey);
for (const [drvHash, outputs] of sortedInputDrvHashes) {
const sortedOuts = outputs.split(",").sort();
const outList = `[${sortedOuts.map(quoteString).join(",")}]`;
inputDrvEntries.push(`(${quoteString(drvHash)},${outList})`);
}
const inputDrvs = inputDrvEntries.join(",");
const sortedInputSrcs = Array.from(drv.inputSrcs).sort();
const inputSrcs = sortedInputSrcs.map(quoteString).join(",");
const args = drv.args.map(escapeString).join(",");
const envs = Array.from(drv.env.entries())
.sort(cmpByKey)
.map(([k, v]) => `(${escapeString(k)},${escapeString(v)})`);
return `Derive([${outputs}],[${inputDrvs}],[${inputSrcs}],${quoteString(drv.platform)},${escapeString(drv.builder)},[${args}],[${envs}])`;
};
const validateName = (attrs: NixAttrs): string => {
if (!attrs.has("name")) {
throw new Error("derivation: missing required attribute 'name'");
}
const name = forceStringValue(attrs.get("name") as NixValue);
if (!name) {
throw new Error("derivation: 'name' cannot be empty");
}
if (name.endsWith(".drv")) {
throw new Error(`derivation: invalid name '${name}' (cannot end with .drv)`);
}
return name;
};
const validateBuilder = (attrs: NixAttrs, outContext: NixStringContext): string => {
if (!attrs.has("builder")) {
throw new Error("derivation: missing required attribute 'builder'");
}
return coerceToString(attrs.get("builder") as NixValue, StringCoercionMode.ToString, true, outContext);
};
const validateSystem = (attrs: NixAttrs): string => {
if (!attrs.has("system")) {
throw new Error("derivation: missing required attribute 'system'");
}
return forceStringValue(attrs.get("system") as NixValue);
};
const validateOutputs = (outputs: string[]): void => {
if (outputs.length === 0) {
throw new Error("derivation: outputs list cannot be empty");
}
if (outputs.includes("drv")) {
throw new Error("derivation: invalid output name 'drv'");
}
const seen = new Set<string>();
for (const output of outputs) {
if (seen.has(output)) {
throw new Error(`derivation: duplicate output '${output}'`);
}
seen.add(output);
}
};
const extractOutputs = (attrs: NixAttrs, structuredAttrs: boolean): string[] => {
if (!attrs.has("outputs")) {
return ["out"];
}
let outputs: string[];
if (structuredAttrs) {
const outputsList = forceList(attrs.get("outputs") as NixValue);
outputs = outputsList.map((o) => forceStringValue(o));
} else {
const outputsStr = coerceToString(
attrs.get("outputs") as NixValue,
StringCoercionMode.ToString,
false,
new Set(),
);
outputs = outputsStr
.trim()
.split(/\s+/)
.filter((s) => s.length > 0);
}
validateOutputs(outputs);
return outputs;
};
const extractArgs = (attrs: NixAttrs, outContext: NixStringContext): string[] => {
if (!attrs.has("args")) {
return [];
}
const argsList = forceList(attrs.get("args") as NixValue);
return argsList.map((a) => coerceToString(a, StringCoercionMode.ToString, true, outContext));
};
const structuredAttrsExcludedKeys = new Set([
"__structuredAttrs",
"__ignoreNulls",
"__contentAddressed",
"__impure",
"args",
]);
const specialAttrs = new Set(["args", "__ignoreNulls", "__contentAddressed", "__impure"]);
const sortedJsonStringify = (obj: Record<string, JsonValue>): string => {
const sortedKeys = Object.keys(obj).sort();
const sortedObj: Record<string, JsonValue> = {};
for (const key of sortedKeys) {
sortedObj[key] = obj[key];
}
return JSON.stringify(sortedObj);
};
const extractEnv = (
attrs: NixAttrs,
structuredAttrs: boolean,
ignoreNulls: boolean,
outContext: NixStringContext,
drvName: string,
): Map<string, string> => {
const env = new Map<string, string>();
if (structuredAttrs) {
const jsonAttrs: Record<string, JsonValue> = {};
for (const [key, value] of attrs) {
if (!structuredAttrsExcludedKeys.has(key)) {
const forcedValue = force(value);
if (ignoreNulls && forcedValue === null) {
continue;
}
jsonAttrs[key] = nixValueToJson(value, true, outContext, true);
}
if (key === "allowedReferences") {
console.warn(
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
`the derivation attribute 'allowedReferences'; use ` +
`'outputChecks.<output>.allowedReferences' instead`,
);
}
if (key === "allowedRequisites") {
console.warn(
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
`the derivation attribute 'allowedRequisites'; use ` +
`'outputChecks.<output>.allowedRequisites' instead`,
);
}
if (key === "disallowedReferences") {
console.warn(
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
`the derivation attribute 'disallowedReferences'; use ` +
`'outputChecks.<output>.disallowedReferences' instead`,
);
}
if (key === "disallowedRequisites") {
console.warn(
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
`the derivation attribute 'disallowedRequisites'; use ` +
`'outputChecks.<output>.disallowedRequisites' instead`,
);
}
if (key === "maxSize") {
console.warn(
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
`the derivation attribute 'maxSize'; use ` +
`'outputChecks.<output>.maxSize' instead`,
);
}
if (key === "maxClosureSize") {
console.warn(
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
`the derivation attribute 'maxClosureSize'; use ` +
`'outputChecks.<output>.maxClosureSize' instead`,
);
}
}
env.set("__json", sortedJsonStringify(jsonAttrs));
} else {
for (const [key, value] of attrs) {
if (!specialAttrs.has(key)) {
const forcedValue = force(value as NixValue);
if (ignoreNulls && forcedValue === null) {
continue;
}
env.set(key, coerceToString(value as NixValue, StringCoercionMode.ToString, true, outContext));
}
}
}
return env;
};
interface FixedOutputInfo {
hash: string;
hashAlgo: string;
hashMode: string;
}
const extractFixedOutputInfo = (attrs: NixAttrs, ignoreNulls: boolean): FixedOutputInfo | null => {
if (!attrs.has("outputHash")) {
return null;
}
const hashValue = force(attrs.get("outputHash") as NixValue);
if (ignoreNulls && hashValue === null) {
return null;
}
const hashRaw = forceStringNoCtx(hashValue);
let hashAlgo = null;
if (attrs.has("outputHashAlgo")) {
const algoValue = force(attrs.get("outputHashAlgo") as NixValue);
if (!(ignoreNulls && algoValue === null)) {
hashAlgo = forceStringNoCtx(algoValue);
}
}
let hashMode = "flat";
if (attrs.has("outputHashMode")) {
const modeValue = force(attrs.get("outputHashMode") as NixValue);
if (!(ignoreNulls && modeValue === null)) {
hashMode = forceStringValue(modeValue);
}
}
if (hashMode !== "flat" && hashMode !== "recursive") {
throw new Error(`derivation: invalid outputHashMode '${hashMode}' (must be 'flat' or 'recursive')`);
}
const parsed = Deno.core.ops.op_parse_hash(hashRaw, hashAlgo);
return { hash: parsed.hex, hashAlgo: parsed.algo, hashMode };
};
const validateFixedOutputConstraints = (fixedOutput: FixedOutputInfo | null, outputs: string[]) => {
if (fixedOutput && (outputs.length !== 1 || outputs[0] !== "out")) {
throw new Error("derivation: fixed-output derivations must have exactly one 'out' output");
}
};
export const derivationStrict = (args: NixValue): NixAttrs => {
const attrs = forceAttrs(args);
const drvName = validateName(attrs);
const collectedContext: NixStringContext = new Set();
const builder = validateBuilder(attrs, collectedContext);
const platform = validateSystem(attrs);
const structuredAttrs = attrs.has("__structuredAttrs")
? force(attrs.get("__structuredAttrs") as NixValue) === true
: false;
const ignoreNulls = attrs.has("__ignoreNulls")
? force(attrs.get("__ignoreNulls") as NixValue) === true
: false;
const outputs = extractOutputs(attrs, structuredAttrs);
const fixedOutputInfo = extractFixedOutputInfo(attrs, ignoreNulls);
validateFixedOutputConstraints(fixedOutputInfo, outputs);
if (attrs.has("__contentAddressed") && force(attrs.get("__contentAddressed") as NixValue) === true) {
throw new Error("ca derivations are not supported");
}
if (attrs.has("__impure") && force(attrs.get("__impure") as NixValue) === true) {
throw new Error("impure derivations are not supported");
}
const drvArgs = extractArgs(attrs, collectedContext);
const env = extractEnv(attrs, structuredAttrs, ignoreNulls, collectedContext, drvName);
const envEntries: [string, string][] = Array.from(env.entries());
const contextArray: string[] = Array.from(collectedContext);
const rustResult: {
drvPath: string;
outputs: [string, string][];
} = Deno.core.ops.op_finalize_derivation(
drvName,
builder,
platform,
outputs,
drvArgs,
envEntries,
contextArray,
fixedOutputInfo,
);
const result: NixAttrs = new Map();
const drvPathContext = new Set<string>();
addDrvDeepContext(drvPathContext, rustResult.drvPath);
result.set("drvPath", mkStringWithContext(rustResult.drvPath, drvPathContext));
for (const [outputName, outputPath] of rustResult.outputs) {
const outputContext = new Set<string>();
addBuiltContext(outputContext, rustResult.drvPath, outputName);
result.set(outputName, mkStringWithContext(outputPath, outputContext));
}
return result;
};
export const derivationStub = (_: NixValue): NixAttrs => {
throw new Error("unreachable: stub derivation implementation called");
};
+17
View File
@@ -0,0 +1,17 @@
import type { NixValue } from "../types";
export const getFlake = (_attrs: NixValue): never => {
throw new Error("Not implemented: getFlake");
};
export const parseFlakeName = (_s: NixValue): never => {
throw new Error("Not implemented: parseFlakeName");
};
export const parseFlakeRef = (_s: NixValue): never => {
throw new Error("Not implemented: parseFlakeRef");
};
export const flakeRefToString = (_attrs: NixValue): never => {
throw new Error("Not implemented: flakeRefToString");
};
@@ -0,0 +1,61 @@
import { printValue } from "../print";
import { force } from "../thunk";
import { CatchableError, type NixValue } from "../types";
import { coerceToString, StringCoercionMode } from "./conversion";
import { isAttrs } from "./type-check";
export const seq =
(e1: NixValue) =>
(e2: NixValue): NixValue => {
force(e1); // Force evaluation of e1
return e2;
};
export const deepSeq =
(e1: NixValue) =>
(e2: NixValue): NixValue => {
const seen: Set<NixValue> = new Set();
const recurse = (e: NixValue) => {
if (!seen.has(e)) {
seen.add(e);
} else {
return;
}
const forced = force(e);
if (Array.isArray(forced)) {
for (const val of forced) {
recurse(val);
}
} else if (isAttrs(forced)) {
for (const [_, val] of forced.entries()) {
recurse(val);
}
}
};
recurse(e1);
return e2;
};
export const abort = (s: NixValue): never => {
throw new Error(`evaluation aborted with the following error message: '${force(s)}'`);
};
export const throwFunc = (s: NixValue): never => {
throw new CatchableError(coerceToString(s, StringCoercionMode.Base, false, new Set()));
};
export const trace =
(e1: NixValue) =>
(e2: NixValue): NixValue => {
console.error(`trace: ${printValue(force(e1))}`);
return e2;
};
export const warn =
(e1: NixValue) =>
(e2: NixValue): NixValue => {
console.log(`evaluation warning: ${force(e1)}`);
return e2;
};
export const breakFunc = (v: NixValue): NixValue => v;
+34
View File
@@ -0,0 +1,34 @@
import { select } from "../helpers";
import { forceAttrs, forceStringNoCtx, forceStringValue } from "../type-assert";
import type { NixValue } from "../types";
import { realisePath } from "./io";
export const hashFile =
(type: NixValue) =>
(p: NixValue): string => {
const algo = forceStringNoCtx(type);
const pathStr = realisePath(p);
return Deno.core.ops.op_hash_file(algo, pathStr);
};
export const hashString =
(type: NixValue) =>
(s: NixValue): string => {
const algo = forceStringNoCtx(type);
const data = forceStringValue(s);
return Deno.core.ops.op_hash_string(algo, data);
};
export const convertHash = (args: NixValue): string => {
const attrs = forceAttrs(args);
const hash = forceStringNoCtx(select(attrs, ["hash"]));
let hashAlgo: string | null = null;
if (attrs.has("hashAlgo")) {
hashAlgo = forceStringNoCtx(select(attrs, ["hashAlgo"]));
}
const toHashFormat = forceStringNoCtx(select(attrs, ["toHashFormat"]));
return Deno.core.ops.op_convert_hash(hash, hashAlgo, toHashFormat);
};
+224
View File
@@ -0,0 +1,224 @@
import { createThunk, force } from "../thunk";
import type { NixAttrs, NixFunction, NixValue } from "../types";
import * as arithmetic from "./arithmetic";
import * as attrs from "./attrs";
import * as conversion from "./conversion";
import * as derivation from "./derivation";
import * as flake from "./flake";
import * as functional from "./functional";
import * as hash from "./hash";
import * as io from "./io";
import * as list from "./list";
import * as math from "./math";
import * as misc from "./misc";
import * as pathOps from "./path";
import * as string from "./string";
import * as typeCheck from "./type-check";
export const PRIMOP_METADATA = Symbol("primop_metadata");
export interface PrimopMetadata {
name: string;
arity: number;
applied: number;
}
export const mkPrimop = (
func: NixFunction,
name: string,
arity: number,
applied: number = 0,
): ((...args: NixValue[]) => NixValue) => {
func[PRIMOP_METADATA] = {
name,
arity,
applied,
} satisfies PrimopMetadata;
if (applied < arity - 1) {
const wrappedFunc: NixFunction = ((arg: NixValue) => {
const result = func(arg);
if (typeof result === "function") {
return mkPrimop(result, name, arity, applied + 1);
}
return result;
});
wrappedFunc[PRIMOP_METADATA] = {
name,
arity,
applied,
};
return wrappedFunc;
}
return func;
};
export const isPrimop = (
value: NixValue,
): value is NixFunction & { [PRIMOP_METADATA]: PrimopMetadata } => {
return (
typeof value === "function" &&
PRIMOP_METADATA in value &&
typeof value[PRIMOP_METADATA] === "object" &&
value[PRIMOP_METADATA] !== null
);
};
export const getPrimopMetadata = (func: NixValue): PrimopMetadata | undefined => {
if (isPrimop(func)) {
return func[PRIMOP_METADATA];
}
return undefined;
};
export const builtins: NixAttrs = new Map<string, NixValue>(
Object.entries({
add: mkPrimop(arithmetic.add, "add", 2),
sub: mkPrimop(arithmetic.sub, "sub", 2),
mul: mkPrimop(arithmetic.mul, "mul", 2),
div: mkPrimop(arithmetic.div, "div", 2),
bitAnd: mkPrimop(arithmetic.bitAnd, "bitAnd", 2),
bitOr: mkPrimop(arithmetic.bitOr, "bitOr", 2),
bitXor: mkPrimop(arithmetic.bitXor, "bitXor", 2),
lessThan: mkPrimop(arithmetic.lessThan, "lessThan", 2),
ceil: mkPrimop(math.ceil, "ceil", 1),
floor: mkPrimop(math.floor, "floor", 1),
isAttrs: mkPrimop((e: NixValue) => typeCheck.isAttrs(force(e)), "isAttrs", 1),
isBool: mkPrimop((e: NixValue) => typeCheck.isBool(force(e)), "isBool", 1),
isFloat: mkPrimop((e: NixValue) => typeCheck.isFloat(force(e)), "isFloat", 1),
isFunction: mkPrimop((e: NixValue) => typeCheck.isFunction(force(e)), "isFunction", 1),
isInt: mkPrimop((e: NixValue) => typeCheck.isInt(force(e)), "isInt", 1),
isList: mkPrimop((e: NixValue) => typeCheck.isList(force(e)), "isList", 1),
isNull: mkPrimop((e: NixValue) => typeCheck.isNull(force(e)), "isNull", 1),
isPath: mkPrimop((e: NixValue) => typeCheck.isPath(force(e)), "isPath", 1),
isString: mkPrimop((e: NixValue) => typeCheck.isString(force(e)), "isString", 1),
typeOf: mkPrimop((e: NixValue) => typeCheck.typeOf(force(e)), "typeOf", 1),
map: mkPrimop(list.map, "map", 2),
filter: mkPrimop(list.filter, "filter", 2),
length: mkPrimop(list.length, "length", 1),
head: mkPrimop(list.head, "head", 1),
tail: mkPrimop(list.tail, "tail", 1),
elem: mkPrimop(list.elem, "elem", 2),
elemAt: mkPrimop(list.elemAt, "elemAt", 2),
concatLists: mkPrimop(list.concatLists, "concatLists", 1),
concatMap: mkPrimop(list.concatMap, "concatMap", 2),
"foldl'": mkPrimop(list.foldlPrime, "foldl'", 3),
sort: mkPrimop(list.sort, "sort", 2),
partition: mkPrimop(list.partition, "partition", 2),
genList: mkPrimop(list.genList, "genList", 2),
all: mkPrimop(list.all, "all", 2),
any: mkPrimop(list.any, "any", 2),
attrNames: mkPrimop(attrs.attrNames, "attrNames", 1),
attrValues: mkPrimop(attrs.attrValues, "attrValues", 1),
getAttr: mkPrimop(attrs.getAttr, "getAttr", 2),
hasAttr: mkPrimop(attrs.hasAttr, "hasAttr", 2),
mapAttrs: mkPrimop(attrs.mapAttrs, "mapAttrs", 2),
removeAttrs: mkPrimop(attrs.removeAttrs, "removeAttrs", 2),
listToAttrs: mkPrimop(attrs.listToAttrs, "listToAttrs", 1),
intersectAttrs: mkPrimop(attrs.intersectAttrs, "intersectAttrs", 2),
catAttrs: mkPrimop(attrs.catAttrs, "catAttrs", 2),
groupBy: mkPrimop(attrs.groupBy, "groupBy", 2),
zipAttrsWith: mkPrimop(attrs.zipAttrsWith, "zipAttrsWith", 2),
unsafeGetAttrPos: mkPrimop(attrs.unsafeGetAttrPos, "unsafeGetAttrPos", 2),
stringLength: mkPrimop(string.stringLength, "stringLength", 1),
substring: mkPrimop(string.substring, "substring", 3),
concatStringsSep: mkPrimop(string.concatStringsSep, "concatStringsSep", 2),
baseNameOf: mkPrimop(pathOps.baseNameOf, "baseNameOf", 1),
dirOf: mkPrimop(pathOps.dirOf, "dirOf", 1),
toPath: mkPrimop(pathOps.toPath, "toPath", 1),
match: mkPrimop(string.match, "match", 2),
split: mkPrimop(string.split, "split", 2),
seq: mkPrimop(functional.seq, "seq", 2),
deepSeq: mkPrimop(functional.deepSeq, "deepSeq", 2),
abort: mkPrimop(functional.abort, "abort", 1),
throw: mkPrimop(functional.throwFunc, "throw", 1),
trace: mkPrimop(functional.trace, "trace", 2),
warn: mkPrimop(functional.warn, "warn", 2),
break: mkPrimop(functional.breakFunc, "break", 1),
derivation: mkPrimop(derivation.derivationStub, "derivation", 1),
derivationStrict: mkPrimop(derivation.derivationStrict, "derivationStrict", 1),
import: mkPrimop(io.importFunc, "import", 1),
scopedImport: mkPrimop(io.scopedImport, "scopedImport", 2),
storePath: mkPrimop(io.storePath, "storePath", 1),
fetchClosure: mkPrimop(io.fetchClosure, "fetchClosure", 1),
fetchMercurial: mkPrimop(io.fetchMercurial, "fetchMercurial", 1),
fetchGit: mkPrimop(io.fetchGit, "fetchGit", 1),
fetchTarball: mkPrimop(io.fetchTarball, "fetchTarball", 1),
fetchTree: mkPrimop(io.fetchTree, "fetchTree", 1),
fetchurl: mkPrimop(io.fetchurl, "fetchurl", 1),
readDir: mkPrimop(io.readDir, "readDir", 1),
readFile: mkPrimop(io.readFile, "readFile", 1),
readFileType: mkPrimop(io.readFileType, "readFileType", 1),
pathExists: mkPrimop(io.pathExists, "pathExists", 1),
path: mkPrimop(io.path, "path", 1),
toFile: mkPrimop(io.toFile, "toFile", 2),
filterSource: mkPrimop(io.filterSource, "filterSource", 2),
findFile: mkPrimop(io.findFile, "findFile", 2),
getEnv: mkPrimop(io.getEnv, "getEnv", 1),
fromJSON: mkPrimop(conversion.fromJSON, "fromJSON", 1),
fromTOML: mkPrimop(conversion.fromTOML, "fromTOML", 1),
toJSON: mkPrimop(conversion.toJSON, "toJSON", 1),
toXML: mkPrimop(conversion.toXML, "toXML", 1),
toString: mkPrimop(conversion.toStringFunc, "toString", 1),
hashFile: mkPrimop(hash.hashFile, "hashFile", 2),
hashString: mkPrimop(hash.hashString, "hashString", 2),
convertHash: mkPrimop(hash.convertHash, "convertHash", 2),
flakeRefToString: mkPrimop(flake.flakeRefToString, "flakeRefToString", 1),
getFlake: mkPrimop(flake.getFlake, "getFlake", 1),
parseFlakeName: mkPrimop(flake.parseFlakeName, "parseFlakeName", 1),
parseFlakeRef: mkPrimop(flake.parseFlakeRef, "parseFlakeRef", 1),
addErrorContext: mkPrimop(misc.addErrorContext, "addErrorContext", 1),
appendContext: mkPrimop(misc.appendContext, "appendContext", 1),
getContext: mkPrimop(misc.getContext, "getContext", 1),
hasContext: mkPrimop(misc.hasContext, "hasContext", 1),
unsafeDiscardOutputDependency: mkPrimop(
misc.unsafeDiscardOutputDependency,
"unsafeDiscardOutputDependency",
1,
),
unsafeDiscardStringContext: mkPrimop(misc.unsafeDiscardStringContext, "unsafeDiscardStringContext", 1),
addDrvOutputDependencies: mkPrimop(misc.addDrvOutputDependencies, "addDrvOutputDependencies", 2),
compareVersions: mkPrimop(misc.compareVersions, "compareVersions", 2),
functionArgs: mkPrimop(misc.functionArgs, "functionArgs", 1),
genericClosure: mkPrimop(misc.genericClosure, "genericClosure", 1),
outputOf: mkPrimop(misc.outputOf, "outputOf", 2),
parseDrvName: mkPrimop(misc.parseDrvName, "parseDrvName", 1),
placeholder: mkPrimop(misc.placeholder, "placeholder", 1),
replaceStrings: mkPrimop(misc.replaceStrings, "replaceStrings", 3),
splitVersion: mkPrimop(misc.splitVersion, "splitVersion", 1),
traceVerbose: mkPrimop(misc.traceVerbose, "traceVerbose", 2),
tryEval: mkPrimop(misc.tryEval, "tryEval", 1),
builtins: createThunk(() => builtins, "builtins"),
currentSystem: createThunk(() => {
return "x86_64-linux";
}, "currentSystem"),
currentTime: createThunk(() => Date.now(), "currentTime"),
false: false,
true: true,
null: null,
langVersion: 6,
nixPath: [],
nixVersion: "2.31.2",
storeDir: createThunk(() => {
throw new Error("stub storeDir evaluated");
}),
}),
);
+486
View File
@@ -0,0 +1,486 @@
import { select } from "../helpers";
import { getPathValue } from "../path";
import type { NixStringContext, StringWithContext } from "../string-context";
import { addOpaqueContext, decodeContextElem, mkStringWithContext } from "../string-context";
import { force } from "../thunk";
import {
forceAttrs,
forceBool,
forceFunction,
forceList,
forceStringNoCtx,
forceStringValue,
} from "../type-assert";
import type { NixAttrs, NixString, NixValue } from "../types";
import { CatchableError, isNixPath, NixPath } from "../types";
import { coerceToPath, coerceToString, StringCoercionMode } from "./conversion";
import { baseNameOf } from "./path";
import { isAttrs, isPath, isString } from "./type-check";
import { execBytecode, execBytecodeScoped } from "../vm";
const importCache = new Map<string, NixValue>();
const realiseContext = (context: NixStringContext): void => {
for (const encoded of context) {
const elem = decodeContextElem(encoded);
if (elem.type === "built") {
throw new Error(
`cannot build derivation '${elem.drvPath}' during evaluation because import-from-derivation is not supported`,
);
}
}
};
export const realisePath = (value: NixValue): string => {
const context: NixStringContext = new Set();
const pathStr = coerceToPath(value, context);
if (context.size > 0) {
realiseContext(context);
}
return pathStr;
};
export const importFunc = (path: NixValue): NixValue => {
const pathStr = realisePath(path);
const cached = importCache.get(pathStr);
if (cached !== undefined) {
return cached;
}
const [code, currentDir] = Deno.core.ops.op_import(pathStr);
const result = execBytecode(code, currentDir);
importCache.set(pathStr, result);
return result;
};
export const scopedImport =
(scope: NixValue) =>
(path: NixValue): NixValue => {
const scopeAttrs = forceAttrs(scope);
const scopeKeys = Array.from(scopeAttrs.keys());
const pathStr = realisePath(path);
const [code, currentDir] = Deno.core.ops.op_scoped_import(pathStr, scopeKeys);
return execBytecodeScoped(code, currentDir, scopeAttrs);
};
export const storePath = (pathArg: NixValue): StringWithContext => {
const context: NixStringContext = new Set();
const pathStr = coerceToPath(pathArg, context);
const validatedPath: string = Deno.core.ops.op_store_path(pathStr);
context.add(validatedPath);
return mkStringWithContext(validatedPath, context);
};
export const fetchClosure = (_args: NixValue): never => {
throw new Error("Not implemented: fetchClosure");
};
export interface FetchUrlResult {
storePath: string;
hash: string;
}
export interface FetchTarballResult {
storePath: string;
narHash: string;
}
export interface FetchGitResult {
outPath: string;
rev: string;
shortRev: string;
revCount: number;
lastModified: number;
lastModifiedDate: string;
submodules: boolean;
narHash: string | null;
}
const normalizeUrlInput = (
args: NixValue,
): { url: string; hash?: string; name?: string; executable?: boolean } => {
const forced = force(args);
if (typeof forced === "string") {
return { url: forced };
}
const attrs = forceAttrs(args);
const url = forceStringValue(select(attrs, ["url"]));
const hash = attrs.has("sha256")
? forceStringValue(attrs.get("sha256") as NixValue)
: attrs.has("hash")
? forceStringValue(attrs.get("hash") as NixValue)
: undefined;
const name = attrs.has("name") ? forceStringValue(attrs.get("name") as NixValue) : undefined;
const executable = attrs.has("executable") ? forceBool(attrs.get("executable") as NixValue) : false;
return { url, hash, name, executable };
};
const normalizeTarballInput = (args: NixValue): { url: string; sha256?: string; name?: string } => {
const forced = force(args);
if (isAttrs(forced)) {
const url = resolvePseudoUrl(forceStringNoCtx(select(forced, ["url"])));
const sha256 = forced.has("sha256") ? forceStringNoCtx(forced.get("sha256") as NixValue) : undefined;
const nameRaw = forced.has("name") ? forceStringNoCtx(forced.get("name") as NixValue) : undefined;
const name = nameRaw === "" ? (baseNameOf(nameRaw) as string) : nameRaw;
return { url, sha256, name };
} else {
return { url: forceStringNoCtx(forced) };
}
};
const resolvePseudoUrl = (url: string) => {
if (url.startsWith("channel:")) {
return `https://channels.nixos.org/${url.substring(8)}/nixexprs.tar.xz`;
} else {
return url;
}
};
export const fetchurl = (args: NixValue): NixString => {
const { url, hash, name, executable } = normalizeUrlInput(args);
const result: FetchUrlResult = Deno.core.ops.op_fetch_url(
url,
hash ?? null,
name ?? null,
executable ?? false,
);
const context: NixStringContext = new Set();
addOpaqueContext(context, result.storePath);
return mkStringWithContext(result.storePath, context);
};
export const fetchTarball = (args: NixValue): NixString => {
const { url, name, sha256 } = normalizeTarballInput(args);
const result: FetchTarballResult = Deno.core.ops.op_fetch_tarball(url, name ?? null, sha256 ?? null);
const context: NixStringContext = new Set();
addOpaqueContext(context, result.storePath);
return mkStringWithContext(result.storePath, context);
};
export const fetchGit = (args: NixValue): NixAttrs => {
const forced = force(args);
const disposedContext: NixStringContext = new Set();
if (isString(forced) || isPath(forced)) {
const url = coerceToString(forced, StringCoercionMode.Base, false, disposedContext);
const result = Deno.core.ops.op_fetch_git(url, null, null, false, false, false, null);
const outContext: NixStringContext = new Set();
addOpaqueContext(outContext, result.outPath);
return new Map<string, NixValue>([
["outPath", mkStringWithContext(result.outPath, outContext)],
["rev", result.rev],
["shortRev", result.shortRev],
["revCount", BigInt(result.revCount)],
["lastModified", BigInt(result.lastModified)],
["lastModifiedDate", result.lastModifiedDate],
["submodules", result.submodules],
["narHash", result.narHash],
]);
}
const attrs = forceAttrs(args);
const url = forceStringValue(select(attrs, ["url"]));
const gitRef = attrs.has("ref") ? forceStringValue(attrs.get("ref") as NixValue) : null;
const rev = attrs.has("rev") ? forceStringValue(attrs.get("rev") as NixValue) : null;
const shallow = attrs.has("shallow") ? forceBool(attrs.get("shallow") as NixValue) : false;
const submodules = attrs.has("submodules") ? forceBool(attrs.get("submodules") as NixValue) : false;
const allRefs = attrs.has("allRefs") ? forceBool(attrs.get("allRefs") as NixValue) : false;
const name = attrs.has("name") ? forceStringValue(attrs.get("name") as NixValue) : null;
const result: FetchGitResult = Deno.core.ops.op_fetch_git(
url,
gitRef,
rev,
shallow,
submodules,
allRefs,
name,
);
const outContext: NixStringContext = new Set();
addOpaqueContext(outContext, result.outPath);
return new Map<string, NixValue>([
["outPath", mkStringWithContext(result.outPath, outContext)],
["rev", result.rev],
["shortRev", result.shortRev],
["revCount", BigInt(result.revCount)],
["lastModified", BigInt(result.lastModified)],
["lastModifiedDate", result.lastModifiedDate],
["submodules", result.submodules],
["narHash", result.narHash],
]);
};
export const fetchMercurial = (_args: NixValue): NixAttrs => {
throw new Error("Not implemented: fetchMercurial");
};
export const fetchTree = (args: NixValue): NixAttrs => {
const attrs = forceAttrs(args);
const type = attrs.has("type") ? forceStringValue(attrs.get("type") as NixValue) : "auto";
switch (type) {
case "git":
return fetchGit(args);
case "hg":
case "mercurial":
return fetchMercurial(args);
case "tarball":
return new Map<string, NixValue>([["outPath", fetchTarball(args)]]);
case "file":
return new Map<string, NixValue>([["outPath", fetchurl(args)]]);
case "path": {
const path = forceStringValue(select(attrs, ["path"]));
return new Map<string, NixValue>([["outPath", path]]);
}
case "github":
case "gitlab":
case "sourcehut":
return fetchGitForge(type, attrs);
default:
return autoDetectAndFetch(attrs);
}
};
const fetchGitForge = (forge: string, attrs: NixAttrs): NixAttrs => {
const owner = forceStringValue(select(forge, ["owner"]));
const repo = forceStringValue(select(forge, ["repo"]));
const rev = attrs.has("rev")
? forceStringValue(attrs.get("rev") as NixValue)
: attrs.has("ref")
? forceStringValue(attrs.get("ref") as NixValue)
: "HEAD";
const host = attrs.has("host") ? forceStringValue(attrs.get("host") as NixValue) : undefined;
let tarballUrl: string;
switch (forge) {
case "github": {
const apiHost = host || "github.com";
tarballUrl = `https://api.${apiHost}/repos/${owner}/${repo}/tarball/${rev}`;
break;
}
case "gitlab": {
const glHost = host || "gitlab.com";
tarballUrl = `https://${glHost}/api/v4/projects/${owner}%2F${repo}/repository/archive.tar.gz?sha=${rev}`;
break;
}
case "sourcehut": {
const shHost = host || "git.sr.ht";
tarballUrl = `https://${shHost}/${owner}/${repo}/archive/${rev}.tar.gz`;
break;
}
default:
throw new Error(`Unknown forge type: ${forge}`);
}
const outPath = fetchTarball(new Map<string, NixValue>([["url", tarballUrl], ...attrs]));
return new Map<string, NixValue>([
["outPath", outPath],
["rev", rev],
["shortRev", rev.substring(0, 7)],
]);
};
const autoDetectAndFetch = (attrs: NixAttrs): NixAttrs => {
const url = forceStringValue(select(attrs, ["url"]));
if (url.endsWith(".git") || url.includes("github.com") || url.includes("gitlab.com")) {
return fetchGit(attrs);
}
if (
url.endsWith(".tar.gz") ||
url.endsWith(".tar.xz") ||
url.endsWith(".tar.bz2") ||
url.endsWith(".tgz")
) {
return new Map<string, NixValue>([["outPath", fetchTarball(attrs)]]);
}
return new Map<string, NixValue>([["outPath", fetchurl(attrs)]]);
};
export const readDir = (path: NixValue): NixAttrs => {
const pathStr = realisePath(path);
return Deno.core.ops.op_read_dir(pathStr);
};
export const readFile = (path: NixValue): string => {
const pathStr = realisePath(path);
return Deno.core.ops.op_read_file(pathStr);
};
export const readFileType = (path: NixValue): string => {
const pathStr = realisePath(path);
return Deno.core.ops.op_read_file_type(pathStr);
};
export const pathExists = (path: NixValue): boolean => {
try {
const pathStr = realisePath(path);
return Deno.core.ops.op_path_exists(pathStr);
} catch {
return false;
}
};
/**
* builtins.path
* Add a path to the Nix store with fine-grained control
*
* Parameters (attribute set):
* - path (required): Path to add to the store
* - name (optional): Name to use in store path (defaults to basename)
* - filter (optional): Function (path, type) -> bool
* - recursive (optional): Boolean, default true (NAR vs flat hashing)
* - sha256 (optional): Expected SHA-256 hash (hex-encoded)
*
* Returns: Store path string
*/
export const path = (args: NixValue): NixString => {
const attrs = forceAttrs(args);
if (!attrs.has("path")) {
throw new TypeError("builtins.path: 'path' attribute is required");
}
const pathValue = force(attrs.get("path") as NixValue);
let pathStr: string;
if (isNixPath(pathValue)) {
pathStr = getPathValue(pathValue);
} else {
pathStr = forceStringValue(pathValue);
}
const name = attrs.has("name") ? forceStringValue(attrs.get("name") as NixValue) : null;
const recursive = attrs.has("recursive") ? forceBool(attrs.get("recursive") as NixValue) : true;
const sha256 = attrs.has("sha256") ? forceStringValue(attrs.get("sha256") as NixValue) : null;
let storePath: string;
if (attrs.has("filter")) {
const filterFn = forceFunction(attrs.get("filter") as NixValue);
const entries: [string, string][] = Deno.core.ops.op_walk_dir(pathStr);
const includePaths: string[] = [];
for (const [relPath, fileType] of entries) {
const fullPath = `${pathStr}/${relPath}`;
const innerFn = forceFunction(filterFn(fullPath));
const shouldInclude = force(innerFn(fileType));
if (shouldInclude === true) {
includePaths.push(relPath);
}
}
storePath = Deno.core.ops.op_add_filtered_path(pathStr, name, recursive, sha256, includePaths);
} else {
storePath = Deno.core.ops.op_add_path(pathStr, name, recursive, sha256);
}
const context: NixStringContext = new Set();
addOpaqueContext(context, storePath);
return mkStringWithContext(storePath, context);
};
export const toFile =
(nameArg: NixValue) =>
(contentsArg: NixValue): StringWithContext => {
const name = forceStringValue(nameArg);
if (name.includes("/")) {
throw new Error("builtins.toFile: name cannot contain '/'");
}
if (name === "." || name === "..") {
throw new Error("builtins.toFile: invalid name");
}
const context: NixStringContext = new Set();
const contents = coerceToString(contentsArg, StringCoercionMode.ToString, false, context);
const references: string[] = Array.from(context);
const storePath: string = Deno.core.ops.op_to_file(name, contents, references);
return mkStringWithContext(storePath, new Set([storePath]));
};
export const filterSource =
(_filter: NixValue) =>
(_path: NixValue): never => {
throw new Error("Not implemented: filterSource");
};
const suffixIfPotentialMatch = (prefix: string, path: string): string | null => {
const n = prefix.length;
const needSeparator = n > 0 && n < path.length;
if (needSeparator && path[n] !== "/") {
return null;
}
if (!path.startsWith(prefix)) {
return null;
}
return needSeparator ? path.substring(n + 1) : path.substring(n);
};
export const findFile =
(searchPath: NixValue) =>
(lookupPath: NixValue): NixPath => {
const forcedSearchPath = forceList(searchPath);
const lookupPathStr = forceStringNoCtx(lookupPath);
for (const item of forcedSearchPath) {
const attrs = forceAttrs(item);
const prefix = attrs.has("prefix") ? forceStringNoCtx(attrs.get("prefix") as NixValue) : "";
if (!attrs.has("path")) {
throw new Error("findFile: search path element is missing 'path' attribute");
}
const suffix = suffixIfPotentialMatch(prefix, lookupPathStr);
if (suffix === null) {
continue;
}
const context: NixStringContext = new Set();
const pathVal = coerceToString(
attrs.get("path") as NixValue,
StringCoercionMode.Interpolation,
false,
context,
);
if (context.size > 0) {
throw new Error("findFile: path with string context is not yet supported");
}
const resolvedPath = Deno.core.ops.op_resolve_path(pathVal, "");
const candidatePath =
suffix.length > 0 ? Deno.core.ops.op_resolve_path(suffix, resolvedPath) : resolvedPath;
if (Deno.core.ops.op_path_exists(candidatePath)) {
return new NixPath(candidatePath);
}
}
if (lookupPathStr.startsWith("nix/")) {
// FIXME: special path type
return new NixPath(`<${lookupPathStr}>`);
}
throw new CatchableError(`file '${lookupPathStr}' was not found in the Nix search path`);
};
export const getEnv = (s: NixValue): string => {
return Deno.core.ops.op_get_env(forceStringValue(s));
};
+145
View File
@@ -0,0 +1,145 @@
import { op } from "../operators";
import { force } from "../thunk";
import { forceBool, forceFunction, forceInt, forceList } from "../type-assert";
import type { NixAttrs, NixList, NixValue } from "../types";
export const map =
(f: NixValue) =>
(list: NixValue): NixList => {
const forcedList = forceList(list);
if (forcedList.length) {
const func = forceFunction(f);
return forcedList.map(func);
}
return [];
};
export const filter =
(f: NixValue) =>
(list: NixValue): NixList => {
const forcedList = forceList(list);
if (forcedList.length) {
const func = forceFunction(f);
return forcedList.filter((e) => forceBool(func(e)));
}
return [];
};
export const length = (e: NixValue): bigint => {
const forced = force(e);
if (typeof forced === "string") return BigInt(forced.length);
return BigInt(forceList(forced).length);
};
export const head = (list: NixValue): NixValue => forceList(list)[0];
export const tail = (list: NixValue): NixList => forceList(list).slice(1);
export const elem =
(x: NixValue) =>
(xs: NixValue): boolean =>
forceList(xs).find((e) => op.eq(x, e)) !== undefined;
export const elemAt =
(xs: NixValue) =>
(n: NixValue): NixValue => {
const list = forceList(xs);
const idx = Number(forceInt(n));
if (idx < 0 || idx >= list.length) {
throw new RangeError(`Index ${idx} out of bounds for list of length ${list.length}`);
}
return list[idx];
};
export const concatLists = (lists: NixValue): NixList => {
return forceList(lists).reduce((acc: NixList, cur: NixValue) => {
return acc.concat(forceList(cur));
}, []);
};
export const concatMap =
(f: NixValue) =>
(lists: NixValue): NixList => {
const fn = forceFunction(f);
return forceList(lists).reduce((acc: NixList, cur: NixValue) => {
return acc.concat(force(fn(cur)) as NixList);
}, []);
};
export const foldlPrime =
(opFn: NixValue) =>
(nul: NixValue) =>
(list: NixValue): NixValue => {
const forcedOp = forceFunction(opFn);
return forceList(list).reduce((acc: NixValue, cur: NixValue) => {
return forceFunction(forcedOp(acc))(cur);
}, nul);
};
export const sort =
(cmp: NixValue) =>
(list: NixValue): NixList => {
const forcedList = [...forceList(list)];
const forcedCmp = forceFunction(cmp);
return forcedList.sort((a, b) => {
if (force(forceFunction(forcedCmp(a))(b))) return -1;
if (force(forceFunction(forcedCmp(b))(a))) return 1;
return 0;
});
};
export const partition =
(pred: NixValue) =>
(list: NixValue): NixAttrs => {
const forcedList = forceList(list);
const forcedPred = forceFunction(pred);
const right: NixList = [];
const wrong: NixList = [];
for (const elem of forcedList) {
if (force(forcedPred(elem))) {
right.push(elem);
} else {
wrong.push(elem);
}
}
return new Map<string, NixValue>([
["right", right],
["wrong", wrong],
]);
};
export const genList =
(f: NixValue) =>
(len: NixValue): NixList => {
const func = forceFunction(f);
const length = forceInt(len);
if (length < 0) {
throw new TypeError(`genList length must be non-negative integer, got ${length}`);
}
return [...Array(Number(length)).keys()].map((i) => func(BigInt(i)));
};
export const all =
(pred: NixValue) =>
(list: NixValue): boolean => {
const forcedList = forceList(list);
if (forcedList.length) {
const f = forceFunction(pred);
return forcedList.every((e) => forceBool(f(e)));
}
return true;
};
export const any =
(pred: NixValue) =>
(list: NixValue): boolean => {
// CppNix forces `pred` eagerly
const f = forceFunction(pred);
const forcedList = forceList(list);
// `false` when no element
return forcedList.some((e) => forceBool(f(e)));
};
+14
View File
@@ -0,0 +1,14 @@
import { forceNumeric } from "../type-assert";
import type { NixValue } from "../types";
export const ceil = (x: NixValue): bigint => {
const val = forceNumeric(x);
if (typeof val === "bigint") return val; // Already an integer
return BigInt(Math.ceil(val)); // Convert to integer
};
export const floor = (x: NixValue): bigint => {
const val = forceNumeric(x);
if (typeof val === "bigint") return val; // Already an integer
return BigInt(Math.floor(val)); // Convert to integer
};
+344
View File
@@ -0,0 +1,344 @@
import { OrderedSet } from "js-sdsl";
import { select } from "../helpers";
import { compareValues } from "../operators";
import {
getStringContext,
getStringValue,
mkStringWithContext,
type NixStringContext,
} from "../string-context";
import { force } from "../thunk";
import {
forceAttrs,
forceFunction,
forceList,
forceString,
forceStringNoCtx,
forceStringValue,
} from "../type-assert";
import type { NixAttrs, NixStrictValue, NixValue } from "../types";
import { ATTR_POSITIONS, CatchableError } from "../types";
import * as context from "./context";
import { isBool, isFloat, isInt, isList, isString, typeOf } from "./type-check";
export const addErrorContext =
(_e1: NixValue) =>
(e2: NixValue): NixValue => {
// FIXME:
// console.log("[WARNING]: addErrorContext not implemented");
return e2;
};
export const appendContext = context.appendContext;
export const getContext = context.getContext;
export const hasContext = context.hasContext;
export const unsafeDiscardOutputDependency = context.unsafeDiscardOutputDependency;
export const unsafeDiscardStringContext = context.unsafeDiscardStringContext;
export const addDrvOutputDependencies = context.addDrvOutputDependencies;
export const compareVersions =
(s1: NixValue) =>
(s2: NixValue): NixValue => {
const str1 = forceStringValue(s1);
const str2 = forceStringValue(s2);
let i1 = 0;
let i2 = 0;
while (i1 < str1.length || i2 < str2.length) {
const c1 = nextComponent(str1, i1);
const c2 = nextComponent(str2, i2);
i1 = c1.nextIndex;
i2 = c2.nextIndex;
if (componentsLt(c1.component, c2.component)) {
return -1n;
} else if (componentsLt(c2.component, c1.component)) {
return 1n;
}
}
return 0n;
};
interface ComponentResult {
component: string;
nextIndex: number;
}
function nextComponent(s: string, startIdx: number): ComponentResult {
let p = startIdx;
// Skip any dots and dashes (component separators)
while (p < s.length && (s[p] === "." || s[p] === "-")) {
p++;
}
if (p >= s.length) {
return { component: "", nextIndex: p };
}
const start = p;
// If the first character is a digit, consume the longest sequence of digits
if (s[p] >= "0" && s[p] <= "9") {
while (p < s.length && s[p] >= "0" && s[p] <= "9") {
p++;
}
} else {
// Otherwise, consume the longest sequence of non-digit, non-separator characters
while (p < s.length && !(s[p] >= "0" && s[p] <= "9") && s[p] !== "." && s[p] !== "-") {
p++;
}
}
return { component: s.substring(start, p), nextIndex: p };
}
function componentsLt(c1: string, c2: string): boolean {
const n1 = c1.match(/^[0-9]+$/) ? BigInt(c1) : null;
const n2 = c2.match(/^[0-9]+$/) ? BigInt(c2) : null;
// Both are numbers: compare numerically
if (n1 !== null && n2 !== null) {
return n1 < n2;
}
// Empty string < number
if (c1 === "" && n2 !== null) {
return true;
}
// Special case: "pre" comes before everything except another "pre"
if (c1 === "pre" && c2 !== "pre") {
return true;
}
if (c2 === "pre") {
return false;
}
// Assume that `2.3a' < `2.3.1'
if (n2 !== null) {
return true;
}
if (n1 !== null) {
return false;
}
// Both are strings: compare lexicographically
return c1 < c2;
}
export const functionArgs = (f: NixValue): NixAttrs => {
const func = forceFunction(f);
if (func.args) {
const ret: NixAttrs = new Map();
for (const key of func.args.required) {
ret.set(key, false);
}
for (const key of func.args.optional) {
ret.set(key, true);
}
const positions = func.args.positions;
if (positions) {
ret[ATTR_POSITIONS] = positions;
}
return ret;
}
return new Map();
};
const checkComparable = (value: NixStrictValue): void => {
if (isString(value) || isInt(value) || isFloat(value) || isBool(value) || isList(value)) {
return;
}
throw new Error(`Unsupported key type for genericClosure: ${typeOf(value)}`);
};
export const genericClosure = (args: NixValue): NixValue => {
const forcedArgs = forceAttrs(args);
const startSet = select(forcedArgs, ["startSet"]);
const operator = select(forcedArgs, ["operator"]);
const initialList = forceList(startSet);
const opFunction = forceFunction(operator);
const resultSet = new OrderedSet<NixStrictValue>(undefined, compareValues);
const resultList: NixStrictValue[] = [];
const queue: NixStrictValue[] = [];
for (const item of initialList) {
const itemAttrs = forceAttrs(item);
const key = force(select(itemAttrs, ["key"]));
checkComparable(key);
if (resultSet.find(key).equals(resultSet.end())) {
resultSet.insert(key);
resultList.push(itemAttrs);
queue.push(itemAttrs);
}
}
let head = 0;
while (head < queue.length) {
const currentItem = queue[head++];
const newItems = forceList(opFunction(currentItem));
for (const newItem of newItems) {
const newItemAttrs = forceAttrs(newItem);
const key = force(select(newItemAttrs, ["key"]));
checkComparable(key);
if (resultSet.find(key).equals(resultSet.end())) {
resultSet.insert(key);
resultList.push(newItemAttrs);
queue.push(newItemAttrs);
}
}
}
return resultList;
};
export const outputOf =
(_drv: NixValue) =>
(_out: NixValue): never => {
throw new Error("Not implemented: outputOf (part of dynamic-derivation)");
};
export const parseDrvName = (s: NixValue): NixAttrs => {
const fullName = forceStringNoCtx(s);
let name = fullName;
let version = "";
for (let i = 0; i < fullName.length; ++i) {
if (fullName[i] === "-" && i + 1 < fullName.length && !/[a-zA-Z]/.test(fullName[i + 1])) {
name = fullName.substring(0, i);
version = fullName.substring(i + 1);
break;
}
}
return new Map<string, NixValue>([
["name", name],
["version", version],
]);
};
export const placeholder = (output: NixValue): NixValue => {
const outputStr = forceStringNoCtx(output);
return Deno.core.ops.op_make_placeholder(outputStr);
};
export const replaceStrings =
(from: NixValue) =>
(to: NixValue) =>
(s: NixValue): NixValue => {
const fromList = forceList(from);
const toList = forceList(to);
const inputStr = forceString(s);
const inputStrValue = getStringValue(inputStr);
const resultContext: NixStringContext = getStringContext(inputStr);
if (fromList.length !== toList.length) {
throw new Error("'from' and 'to' arguments passed to builtins.replaceStrings have different lengths");
}
const toCache = new Map<number, string>();
const toContextCache = new Map<number, NixStringContext>();
let result = "";
let pos = 0;
while (pos <= inputStrValue.length) {
let found = false;
for (let i = 0; i < fromList.length; i++) {
const pattern = forceStringValue(fromList[i]);
if (inputStrValue.substring(pos).startsWith(pattern)) {
found = true;
if (!toCache.has(i)) {
const replacementStr = forceString(toList[i]);
const replacementValue = getStringValue(replacementStr);
const replacementContext = getStringContext(replacementStr);
toCache.set(i, replacementValue);
toContextCache.set(i, replacementContext);
for (const elem of replacementContext) {
resultContext.add(elem);
}
}
const replacement = toCache.get(i) as string;
result += replacement;
if (pattern.length === 0) {
if (pos < inputStrValue.length) {
result += inputStrValue[pos];
}
pos++;
} else {
pos += pattern.length;
}
break;
}
}
if (!found) {
if (pos < inputStrValue.length) {
result += inputStrValue[pos];
}
pos++;
}
}
if (resultContext.size === 0) {
return result;
}
return mkStringWithContext(result, resultContext);
};
export const splitVersion = (s: NixValue): NixValue => {
const version = forceStringValue(s);
const components: string[] = [];
let idx = 0;
while (idx < version.length) {
const result = nextComponent(version, idx);
if (result.component === "") {
break;
}
components.push(result.component);
idx = result.nextIndex;
}
return components;
};
export const traceVerbose =
(_e1: NixValue) =>
(e2: NixValue): NixStrictValue => {
// TODO: implement traceVerbose
return force(e2);
};
export const tryEval = (e: NixValue): NixAttrs => {
try {
return new Map<string, NixValue>([
["success", true],
["value", force(e)],
]);
} catch (err) {
if (err instanceof CatchableError) {
return new Map<string, NixValue>([
["success", false],
["value", false],
]);
} else {
throw err;
}
}
};
+133
View File
@@ -0,0 +1,133 @@
import { mkPath } from "../path";
import { mkStringWithContext, type NixStringContext } from "../string-context";
import { force } from "../thunk";
import type { NixPath, NixString, NixValue } from "../types";
import { isNixPath } from "../types";
import { coerceToPath, coerceToString, StringCoercionMode } from "./conversion";
/**
* builtins.baseNameOf
* Get the last component of a path or string
* Always returns a string (coerces paths)
* Preserves string context if present
*
* Implements Nix's legacyBaseNameOf logic:
* - If string ends with '/', removes only the final slash
* - Then returns everything after the last remaining '/'
*
* Examples:
* - baseNameOf ./foo/bar → "bar"
* - baseNameOf "/foo/bar/" → "bar" (trailing slash removed first)
* - baseNameOf "foo" → "foo"
*/
export const baseNameOf = (s: NixValue): NixString => {
const context: NixStringContext = new Set();
const pathStr = coerceToString(s, StringCoercionMode.Base, false, context);
if (pathStr.length === 0) {
if (context.size === 0) {
return "";
}
return mkStringWithContext("", context);
}
let last = pathStr.length - 1;
if (pathStr[last] === "/" && last > 0) {
last -= 1;
}
let pos = last;
while (pos >= 0 && pathStr[pos] !== "/") {
pos -= 1;
}
if (pos === -1) {
pos = 0;
} else {
pos += 1;
}
const result = pathStr.substring(pos, last + 1);
// Preserve string context if present
if (context.size > 0) {
return mkStringWithContext(result, context);
}
return result;
};
/**
* builtins.dirOf
* Get the directory part of a path or string
* TYPE-PRESERVING: path → path, string → string
*
* Examples:
* - dirOf ./foo/bar → ./foo (path)
* - dirOf "/foo/bar" → "/foo" (string)
* - dirOf "/" → "/" (same type as input)
*/
export const dirOf = (s: NixValue): NixPath | NixString => {
const forced = force(s);
// Path input → path output
if (isNixPath(forced)) {
const pathStr = forced.value;
const lastSlash = pathStr.lastIndexOf("/");
if (lastSlash === -1) {
return mkPath(".");
}
if (lastSlash === 0) {
return mkPath("/");
}
return mkPath(pathStr.slice(0, lastSlash));
}
// String input → string output
const outContext: NixStringContext = new Set();
const pathStr = coerceToString(s, StringCoercionMode.Base, false, outContext);
const lastSlash = pathStr.lastIndexOf("/");
if (lastSlash === -1) {
return ".";
}
if (lastSlash === 0) {
return "/";
}
const result = pathStr.slice(0, lastSlash);
if (outContext.size > 0) {
return mkStringWithContext(result, outContext);
}
return result;
};
/**
* builtins.toPath
* Convert a value to an absolute path string.
* DEPRECATED: Use `/. + "/path"` to convert a string into an absolute path.
*
* This validates that the input can be coerced to an absolute path.
* Returns a **string** (not a NixPath), with context preserved.
*
* Examples:
* - toPath "/foo" → "/foo" (string)
* - toPath "/foo/bar" → "/foo/bar" (string)
* - toPath "foo" → ERROR (not absolute)
* - toPath "" → ERROR (empty)
*/
export const toPath = (s: NixValue): NixString => {
const context: NixStringContext = new Set();
const pathStr = coerceToPath(s, context);
if (context.size === 0) {
return pathStr;
}
return mkStringWithContext(pathStr, context);
};
+94
View File
@@ -0,0 +1,94 @@
import {
getStringContext,
getStringValue,
mkStringWithContext,
type NixStringContext,
} from "../string-context";
import { forceInt, forceList, forceString, forceStringValue } from "../type-assert";
import type { NixInt, NixString, NixValue } from "../types";
import { coerceToString, StringCoercionMode } from "./conversion";
export const stringLength = (e: NixValue): NixInt => BigInt(forceStringValue(e).length);
export const substring =
(start: NixValue) =>
(len: NixValue) =>
(s: NixValue): NixString => {
const startPos = Number(forceInt(start));
const length = Number(forceInt(len));
if (startPos < 0) {
throw new Error("negative start position in 'substring'");
}
const str = forceString(s);
const strValue = getStringValue(str);
const context = getStringContext(str);
if (length === 0) {
if (context.size === 0) {
return "";
}
return mkStringWithContext("", context);
}
const actualLength = length < 0 ? Number.MAX_SAFE_INTEGER : length;
const result = startPos >= strValue.length ? "" : strValue.substring(startPos, startPos + actualLength);
if (context.size === 0) {
return result;
}
return mkStringWithContext(result, context);
};
export const concatStringsSep =
(sep: NixValue) =>
(list: NixValue): NixString => {
const context: NixStringContext = new Set();
const separator = coerceToString(sep, StringCoercionMode.Interpolation, false, context);
const parts = forceList(list).map((elem) =>
coerceToString(elem, StringCoercionMode.Interpolation, false, context),
);
const result = parts.join(separator);
if (context.size === 0) {
return result;
}
return mkStringWithContext(result, context);
};
export const match =
(regex: NixValue) =>
(str: NixValue): NixValue => {
const regexStr = forceStringValue(regex);
const inputStr = forceStringValue(str);
const result = Deno.core.ops.op_match(regexStr, inputStr);
if (result === null) {
return null;
}
return result.map((g) => (g !== null ? g : null));
};
export const split =
(regex: NixValue) =>
(str: NixValue): NixValue => {
const regexStr = forceStringValue(regex);
const inputStr = forceString(str);
const inputStrValue = getStringValue(inputStr);
const result = Deno.core.ops.op_split(regexStr, inputStrValue);
if (result.length === 1 && typeof result[0] === "string") {
return [inputStr];
}
return result.map((item) => {
if (typeof item === "string") {
return item;
}
return item.map((g) => (g !== null ? g : null));
});
};
@@ -0,0 +1,63 @@
import {
isNixPath,
isStringWithContext,
type NixAttrs,
type NixBool,
type NixFloat,
type NixFunction,
type NixInt,
type NixList,
type NixNull,
type NixPath,
type NixStrictValue,
type NixString,
} from "../types";
export const isNixString = (v: NixStrictValue): v is NixString => {
return typeof v === "string" || isStringWithContext(v);
};
export const isAttrs = (e: NixStrictValue): e is NixAttrs => {
return e instanceof Map;
};
export const isBool = (e: NixStrictValue): e is NixBool => typeof e === "boolean";
export const isFloat = (e: NixStrictValue): e is NixFloat => {
const val = e;
return typeof val === "number"; // Only number is float
};
export const isFunction = (e: NixStrictValue): e is NixFunction => typeof e === "function";
export const isInt = (e: NixStrictValue): e is NixInt => {
const val = e;
return typeof val === "bigint"; // Only bigint is int
};
export const isList = (e: NixStrictValue): e is NixList => Array.isArray(e);
export const isNull = (e: NixStrictValue): e is NixNull => e === null;
export const isPath = (e: NixStrictValue): e is NixPath => {
const val = e;
return isNixPath(val);
};
export const isString = (e: NixStrictValue): e is NixString =>
typeof e === "string" || isStringWithContext(e);
export type NixType = "int" | "float" | "bool" | "string" | "path" | "null" | "list" | "lambda" | "set";
export const typeOf = (e: NixStrictValue): NixType => {
if (typeof e === "bigint") return "int";
if (typeof e === "number") return "float";
if (typeof e === "boolean") return "bool";
if (e === null) return "null";
if (isNixString(e)) return "string";
if (isNixPath(e)) return "path";
if (Array.isArray(e)) return "list";
if (e instanceof Map) return "set";
if (typeof e === "function") return "lambda";
throw new TypeError(`Unknown Nix type: ${typeof e}`);
};
+326
View File
@@ -0,0 +1,326 @@
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
import { isAttrs, typeOf } from "./builtins/type-check";
import { mkPath } from "./path";
import { isStringWithContext, mkStringWithContext, type NixStringContext } from "./string-context";
import { force } from "./thunk";
import { forceAttrs, forceBool, forceFunction, forceStringNoCtx, forceStringValue } from "./type-assert";
import type { NixAttrs, NixBool, NixPath, NixString, NixValue } from "./types";
import { CatchableError, isNixPath } from "./types";
interface StackFrame {
span: number;
message: string;
}
const callStack: StackFrame[] = [];
const MAX_STACK_DEPTH = 1000;
function enrichError(error: unknown): Error {
const err = error instanceof Error ? error : new Error(String(error));
if (callStack.length === 0) {
return err;
}
const nixStackLines = callStack.map((frame) => {
return `NIX_STACK_FRAME:${frame.span}:${frame.message}`;
});
// Prepend stack frames to error stack
err.stack = `${nixStackLines.join("\n")}\n${err.stack || ""}`;
return err;
}
const pushContext = (message: string, span: number): void => {
if (callStack.length >= MAX_STACK_DEPTH) {
callStack.shift();
}
callStack.push({ span, message });
};
const popContext = (): void => {
callStack.pop();
};
export const withContext = <T>(message: string, span: number, fn: () => T): T => {
pushContext(message, span);
try {
return fn();
} catch (error) {
throw enrichError(error);
} finally {
popContext();
}
};
/**
* Concatenate multiple values into a string or path with context
* This is used for string interpolation like "hello ${world}"
*
* IMPORTANT: String context handling:
* - All contexts from interpolated values are merged into the result
* - Path mode: Store contexts are forbidden (will throw error)
* - String mode: All contexts are preserved and merged
*
* @param parts - Array of values to concatenate
* @param forceString - If true, result is always a string (paths are copied to store)
* @returns String or Path with merged contexts from all parts
*/
export const concatStringsWithContext = (parts: NixValue[], forceString: boolean): NixString | NixPath => {
if (parts.length === 0) {
return "";
}
const forced = parts.map(force);
const firstIsPath = !forceString && isNixPath(forced[0]);
if (firstIsPath) {
let result = (forced[0] as NixPath).value;
for (let i = 1; i < forced.length; i++) {
const part = forced[i];
if (isNixPath(part)) {
result += part.value;
} else if (typeof part === "string") {
result += part;
} else if (isStringWithContext(part)) {
if (part.context.size > 0) {
throw new TypeError("a string that refers to a store path cannot be appended to a path");
}
result += part.value;
} else {
const tempContext: NixStringContext = new Set();
const coerced = coerceToString(part, StringCoercionMode.Interpolation, false, tempContext);
if (tempContext.size > 0) {
throw new TypeError("a string that refers to a store path cannot be appended to a path");
}
result += coerced;
}
}
return mkPath(result);
}
const context: NixStringContext = new Set();
const strParts: string[] = [];
for (const part of forced) {
if (isNixPath(part)) {
const str = coerceToString(part, StringCoercionMode.Interpolation, true, context);
strParts.push(str);
} else {
const str = coerceToString(part, StringCoercionMode.Interpolation, false, context);
strParts.push(str);
}
}
const value = strParts.join("");
if (context.size === 0) {
return value;
}
return mkStringWithContext(value, context);
};
export const resolvePath = (currentDir: string, path: NixValue): NixPath => {
const forced = force(path);
let pathStr: string;
if (isNixPath(forced)) {
pathStr = forced.value;
} else {
pathStr = forceStringValue(path);
}
const resolved = Deno.core.ops.op_resolve_path(currentDir, pathStr);
return mkPath(resolved);
};
export const select = (obj: NixValue, attrpath: NixValue[], span?: number): NixValue => {
if (span !== undefined) {
if (callStack.length >= MAX_STACK_DEPTH) {
callStack.shift();
}
const frame: StackFrame = { span, message: "while selecting attribute" };
callStack.push(frame);
try {
return selectImpl(obj, attrpath);
} catch (error) {
try {
const path = attrpath.map((a) => forceStringValue(a)).join(".");
if (path) frame.message = `while selecting attribute [${path}]`;
} catch {
throw enrichError(error);
}
throw enrichError(error);
} finally {
callStack.pop();
}
} else {
return selectImpl(obj, attrpath);
}
};
function selectImpl(obj: NixValue, attrpath: NixValue[]): NixValue {
let attrs = forceAttrs(obj);
for (let i = 0; i < attrpath.length - 1; i++) {
const key = forceStringValue(attrpath[i]);
if (!attrs.has(key)) {
throw new Error(`Attribute '${key}' not found`);
}
const cur = forceAttrs(attrs.get(key) as NixValue);
attrs = cur;
}
const last = forceStringValue(attrpath[attrpath.length - 1]);
if (!attrs.has(last)) {
throw new Error(`Attribute '${last}' not found`);
}
return attrs.get(last) as NixValue;
}
export const selectWithDefault = (
obj: NixValue,
attrpath: NixValue[],
defaultVal: NixValue,
span?: number,
): NixValue => {
if (span !== undefined) {
if (callStack.length >= MAX_STACK_DEPTH) {
callStack.shift();
}
const frame: StackFrame = { span, message: "while selecting attribute" };
callStack.push(frame);
try {
return selectWithDefaultImpl(obj, attrpath, defaultVal);
} catch (error) {
try {
const path = attrpath.map((a) => forceStringValue(a)).join(".");
if (path) frame.message = `while selecting attribute [${path}]`;
} catch {
throw enrichError(error);
}
throw enrichError(error);
} finally {
callStack.pop();
}
} else {
return selectWithDefaultImpl(obj, attrpath, defaultVal);
}
};
function selectWithDefaultImpl(obj: NixValue, attrpath: NixValue[], defaultVal: NixValue): NixValue {
let attrs = force(obj);
if (!isAttrs(attrs)) {
return defaultVal;
}
for (let i = 0; i < attrpath.length - 1; i++) {
const key = forceStringValue(attrpath[i]);
if (!attrs.has(key)) {
return defaultVal;
}
const cur = force(attrs.get(key) as NixValue);
if (!isAttrs(cur)) {
return defaultVal;
}
attrs = cur;
}
const last = forceStringValue(attrpath[attrpath.length - 1]);
if (attrs.has(last)) {
return attrs.get(last) as NixValue;
}
return defaultVal;
}
export const hasAttr = (obj: NixValue, attrpath: NixValue[]): NixBool => {
const forced = force(obj);
if (!isAttrs(forced)) {
return false;
}
let attrs = forced;
for (let i = 0; i < attrpath.length - 1; i++) {
const key = forceStringNoCtx(attrpath[i]);
if (!attrs.has(key)) {
return false;
}
const cur = force(attrs.get(key) as NixValue);
if (!isAttrs(cur)) {
return false;
}
attrs = cur;
}
return attrs.has(forceStringValue(attrpath[attrpath.length - 1]));
};
export const call = (func: NixValue, arg: NixValue, span?: number): NixValue => {
if (span !== undefined) {
if (callStack.length >= MAX_STACK_DEPTH) {
callStack.shift();
}
callStack.push({ span, message: "from call site" });
try {
return callImpl(func, arg);
} catch (error) {
throw enrichError(error);
} finally {
callStack.pop();
}
} else {
return callImpl(func, arg);
}
};
function callImpl(func: NixValue, arg: NixValue): NixValue {
const forced = force(func);
if (typeof forced === "function") {
forced.args?.check(arg);
return forced(arg);
}
if (forced instanceof Map && forced.has("__functor")) {
const functor = forceFunction(forced.get("__functor") as NixValue);
return call(callImpl(functor, forced), arg);
}
throw new Error(`attempt to call something which is not a function but ${typeOf(forced)}`);
}
export const assert = (assertion: NixValue, expr: NixValue, assertionRaw: string, span: number): NixValue => {
if (forceBool(assertion)) {
return expr;
}
withContext("while evaluating assertion", span, () => {
throw new CatchableError(`assertion '${assertionRaw}' failed`);
});
throw "unreachable";
};
export const mkPos = (span: number): NixAttrs => {
return Deno.core.ops.op_decode_span(span);
};
interface WithScope {
env: NixValue;
last: WithScope | null;
}
export const lookupWith = (name: string, withScope: WithScope): NixValue => {
let current: WithScope | null = withScope;
while (current !== null) {
const attrs = forceAttrs(current.env);
if (attrs.has(name)) {
return attrs.get(name) as NixValue;
}
current = current.last;
}
throw new Error(`undefined variable '${name}'`);
};
+98
View File
@@ -0,0 +1,98 @@
/**
* nix-js Runtime Entry Point
*
* All functionality is exported via the global `Nix` object
*/
import { builtins, PRIMOP_METADATA } from "./builtins";
import {
assert,
call,
concatStringsWithContext,
hasAttr,
lookupWith,
mkPos,
resolvePath,
select,
selectWithDefault,
} from "./helpers";
import { op } from "./operators";
import { HAS_CONTEXT } from "./string-context";
import { createThunk, DEBUG_THUNKS, force, forceDeep, forceShallow, IS_CYCLE, IS_THUNK } from "./thunk";
import { forceBool } from "./type-assert";
import { IS_PATH, mkAttrs, mkFunction, type NixValue } from "./types";
import { execBytecode, execBytecodeScoped, vmStrings, vmConstants } from "./vm";
export type NixRuntime = typeof Nix;
const replBindings: Map<string, NixValue> = new Map();
export const Nix = {
IS_THUNK,
IS_CYCLE,
HAS_CONTEXT,
IS_PATH,
PRIMOP_METADATA,
DEBUG_THUNKS,
createThunk,
force,
forceBool,
forceShallow,
forceDeep,
assert,
call,
hasAttr,
select,
selectWithDefault,
lookupWith,
resolvePath,
concatStringsWithContext,
mkAttrs,
mkFunction,
mkPos,
op,
builtins,
strings: vmStrings,
constants: vmConstants,
execBytecode,
execBytecodeScoped,
replBindings,
setReplBinding: (name: string, value: NixValue) => {
replBindings.set(name, value);
},
getReplBinding: (name: string) => replBindings.get(name),
};
globalThis.Nix = Nix;
globalThis.$t = createThunk;
globalThis.$f = force;
globalThis.$fb = forceBool;
globalThis.$a = assert;
globalThis.$c = call;
globalThis.$h = hasAttr;
globalThis.$s = select;
globalThis.$sd = selectWithDefault;
globalThis.$l = lookupWith;
globalThis.$r = resolvePath;
globalThis.$cs = concatStringsWithContext;
globalThis.$ma = mkAttrs;
globalThis.$mf = mkFunction;
globalThis.$mp = mkPos;
globalThis.$gb = Nix.getReplBinding;
globalThis.$oa = op.add;
globalThis.$os = op.sub;
globalThis.$om = op.mul;
globalThis.$od = op.div;
globalThis.$oe = op.eq;
globalThis.$ol = op.lt;
globalThis.$og = op.gt;
globalThis.$oc = op.concat;
globalThis.$ou = op.update;
globalThis.$b = builtins;
globalThis.$e = new Map();
+285
View File
@@ -0,0 +1,285 @@
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
import { isNixString, typeOf } from "./builtins/type-check";
import { mkPath } from "./path";
import {
getStringContext,
getStringValue,
mergeContexts,
mkStringWithContext,
type NixStringContext,
} from "./string-context";
import { force } from "./thunk";
import { coerceNumeric, forceAttrs, forceBool, forceList, forceNumeric } from "./type-assert";
import type { NixAttrs, NixList, NixPath, NixString, NixValue } from "./types";
import { isNixPath } from "./types";
const canCoerceToString = (v: NixValue): boolean => {
const forced = force(v);
if (isNixString(forced)) return true;
if (forced instanceof Map) {
if (forced.has("outPath") || forced.has("__toString")) return true;
}
return false;
};
export const compareValues = (a: NixValue, b: NixValue): -1 | 0 | 1 => {
const av = force(a);
const bv = force(b);
// Handle float/int mixed comparisons
if (typeof av === "number" && typeof bv === "bigint") {
const cmp = av - Number(bv);
return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
}
if (typeof av === "bigint" && typeof bv === "number") {
const cmp = Number(av) - bv;
return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
}
const typeA = typeOf(av);
const typeB = typeOf(bv);
// Types must match (except float/int which is handled above)
if (typeA !== typeB) {
throw new TypeError(`cannot compare ${typeOf(av)} with ${typeOf(bv)}`);
}
if (typeA === "int" || typeA === "float") {
return (av as never) < (bv as never) ? -1 : av === bv ? 0 : 1;
}
if (typeA === "string") {
const strA = getStringValue(av as NixString);
const strB = getStringValue(bv as NixString);
return strA < strB ? -1 : strA > strB ? 1 : 0;
}
if (typeA === "path") {
const aPath = av as NixPath;
const bPath = bv as NixPath;
return aPath.value < bPath.value ? -1 : aPath.value > bPath.value ? 1 : 0;
}
if (typeA === "list") {
const aList = av as NixList;
const bList = bv as NixList;
for (let i = 0; ; i++) {
// Equal if same length, else aList > bList
if (i === bList.length) {
return i === aList.length ? 0 : 1;
} else if (i === aList.length) {
return -1; // aList < bList
} else if (!op.eq(aList[i], bList[i])) {
return compareValues(aList[i], bList[i]);
}
}
}
// Other types are incomparable
throw new TypeError(
`cannot compare ${typeOf(av)} with ${typeOf(bv)}; values of that type are incomparable`,
);
};
export const op = {
add: (a: NixValue, b: NixValue): bigint | number | NixString | NixPath => {
const av = force(a);
const bv = force(b);
// Path concatenation: path + string = path
if (isNixPath(av)) {
if (isNixString(bv)) {
const strB = getStringValue(bv);
const ctxB = getStringContext(bv);
if (ctxB.size > 0) {
throw new TypeError("a string that refers to a store path cannot be appended to a path");
}
return mkPath(av.value + strB);
}
// FIXME: handle corepkgs
// path + path: concatenate
if (isNixPath(bv)) {
return mkPath(av.value + bv.value);
}
}
// String + path: result is string (path coerces to string)
if (isNixString(av) && isNixPath(bv)) {
const strA = getStringValue(av);
const ctxA = getStringContext(av);
const result = strA + bv.value;
if (ctxA.size === 0) {
return result;
}
return mkStringWithContext(result, ctxA);
}
// String concatenation
if (isNixString(av) && isNixString(bv)) {
// Merge string context
const strA = getStringValue(av);
const strB = getStringValue(bv);
const ctxA = getStringContext(av);
const ctxB = getStringContext(bv);
if (ctxA.size === 0 && ctxB.size === 0) {
return strA + strB;
}
return mkStringWithContext(strA + strB, mergeContexts(ctxA, ctxB));
}
// Auto-coerce to string if possible
if (canCoerceToString(a) && canCoerceToString(b)) {
const context: NixStringContext = new Set();
const strA = coerceToString(a, StringCoercionMode.Interpolation, false, context);
const strB = coerceToString(b, StringCoercionMode.Interpolation, false, context);
const result = strA + strB;
if (context.size === 0) {
return result;
}
return mkStringWithContext(result, context);
}
// Perform numeric addition otherwise
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (numA as never) + (numB as never);
},
sub: (a: NixValue, b: NixValue): bigint | number => {
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as never) - (bv as never);
},
mul: (a: NixValue, b: NixValue): bigint | number => {
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
return (av as never) * (bv as never);
},
div: (a: NixValue, b: NixValue): bigint | number => {
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
if (bv === 0 || bv === 0n) {
throw new RangeError("Division by zero");
}
return (av as never) / (bv as never);
},
eq: (a: NixValue, b: NixValue): boolean => {
const av = force(a);
const bv = force(b);
// Pointer equality
if (av === bv) return true;
// Special case: int == float type compatibility
if (typeof av === "bigint" && typeof bv === "number") {
return Number(av) === bv;
}
if (typeof av === "number" && typeof bv === "bigint") {
return av === Number(bv);
}
const typeA = typeOf(av);
const typeB = typeOf(bv);
// All other types must match exactly
if (typeA !== typeB) return false;
if (typeA === "int" || typeA === "float" || typeA === "bool" || typeA === "null") {
return av === bv;
}
// String comparison (handles both plain strings and StringWithContext)
if (typeA === "string") {
return getStringValue(av as NixString) === getStringValue(bv as NixString);
}
// Path comparison
if (typeA === "path") {
return (av as NixPath).value === (bv as NixPath).value;
}
if (typeA === "list") {
const aList = av as NixList;
const bList = bv as NixList;
if (aList.length !== bList.length) return false;
for (let i = 0; i < aList.length; i++) {
if (!op.eq(aList[i], bList[i])) return false;
}
return true;
}
if (typeA === "set") {
const attrsA = av as NixAttrs;
const attrsB = bv as NixAttrs;
if (attrsA.has("type") && attrsB.has("type")) {
const typeValA = force(attrsA.get("type") as NixValue);
const typeValB = force(attrsB.get("type") as NixValue);
if (typeValA === "derivation" && typeValB === "derivation") {
if (attrsA.has("outPath") && attrsB.has("outPath")) {
return op.eq(attrsA.get("outPath") as NixValue, attrsB.get("outPath") as NixValue);
}
}
}
const keysA = Array.from(attrsA.keys()).sort();
const keysB = Array.from(attrsB.keys()).sort();
if (keysA.length !== keysB.length) return false;
for (let i = 0; i < keysA.length; i++) {
if (keysA[i] !== keysB[i]) return false;
if (!op.eq(attrsA.get(keysA[i]) as NixValue, attrsB.get(keysB[i]) as NixValue)) return false;
}
return true;
}
// Other types are incomparable
return false;
},
neq: (a: NixValue, b: NixValue): boolean => {
return !op.eq(a, b);
},
lt: (a: NixValue, b: NixValue): boolean => {
return compareValues(a, b) < 0;
},
lte: (a: NixValue, b: NixValue): boolean => {
return compareValues(a, b) <= 0;
},
gt: (a: NixValue, b: NixValue): boolean => {
return compareValues(a, b) > 0;
},
gte: (a: NixValue, b: NixValue): boolean => {
return compareValues(a, b) >= 0;
},
bnot: (a: NixValue): boolean => !forceBool(a),
concat: (a: NixValue, b: NixValue): NixList => {
return forceList(a).concat(forceList(b));
},
update: (a: NixValue, b: NixValue): NixAttrs => {
const mapA = forceAttrs(a);
const mapB = forceAttrs(b);
if (mapA.size === 0) {
return mapB;
}
if (mapB.size === 0) {
return mapA;
}
const result: NixAttrs = new Map(mapA);
for (const [k, v] of mapB) {
result.set(k, v);
}
return result;
},
};
+38
View File
@@ -0,0 +1,38 @@
import { NixPath } from "./types";
const canonicalizePath = (path: string): string => {
const parts: string[] = [];
let i = 0;
const len = path.length;
while (i < len) {
while (i < len && path[i] === "/") i++;
if (i >= len) break;
let j = i;
while (j < len && path[j] !== "/") j++;
const component = path.slice(i, j);
i = j;
if (component === "..") {
if (parts.length > 0) {
parts.pop();
}
} else if (component !== ".") {
parts.push(component);
}
}
if (parts.length === 0) {
return "/";
}
return `/${parts.join("/")}`;
};
export const mkPath = (value: string): NixPath => {
return new NixPath(canonicalizePath(value));
};
export const getPathValue = (p: NixPath): string => {
return p.value;
};
+111
View File
@@ -0,0 +1,111 @@
import { getPrimopMetadata, isPrimop } from "./builtins/index";
import { isStringWithContext } from "./string-context";
import { IS_CYCLE, isThunk } from "./thunk";
import { isNixPath, type NixValue } from "./types";
export const printValue = (value: NixValue, seen: WeakSet<object> = new WeakSet()): string => {
if (isThunk(value)) {
return "«thunk»";
}
if (value === null) {
return "null";
}
if (typeof value === "boolean") {
return value ? "true" : "false";
}
if (typeof value === "bigint") {
return value.toString();
}
if (typeof value === "number") {
return value.toString();
}
if (typeof value === "string") {
return printString(value);
}
if (typeof value === "function") {
if (isPrimop(value)) {
const meta = getPrimopMetadata(value);
if (meta && meta.applied > 0) {
return "<PRIMOP-APP>";
}
return "<PRIMOP>";
}
return "<LAMBDA>";
}
if (IS_CYCLE in value) {
return "«repeated»";
}
if (isNixPath(value)) {
return value.value;
}
if (isStringWithContext(value)) {
return printString(value.value);
}
if (Array.isArray(value)) {
if (value.length > 0) {
if (seen.has(value)) {
return "«repeated»";
}
seen.add(value);
}
const items = value.map((v) => printValue(v, seen)).join(" ");
return `[ ${items} ]`;
}
if (seen.has(value)) {
return "«repeated»";
}
if (value.size > 0) {
seen.add(value);
}
const entries = [...value.entries()]
.map(([k, v]) => `${printSymbol(k)} = ${printValue(v, seen)};`)
.join(" ");
return `{${entries ? ` ${entries} ` : " "}}`;
};
const printString = (s: string): string => {
let result = '"';
for (const c of s) {
switch (c) {
case "\\":
result += "\\\\";
break;
case '"':
result += '\\"';
break;
case "\n":
result += "\\n";
break;
case "\r":
result += "\\r";
break;
case "\t":
result += "\\t";
break;
default:
result += c;
}
}
return `${result}"`;
};
const SYMBOL_REGEX = /^[a-zA-Z_][a-zA-Z0-9_'-]*$/;
const printSymbol = (s: string): string => {
if (SYMBOL_REGEX.test(s)) {
return s;
}
return printString(s);
};
+153
View File
@@ -0,0 +1,153 @@
import type { NixStrictValue } from "./types";
export const HAS_CONTEXT = Symbol("HAS_CONTEXT");
export interface StringContextOpaque {
type: "opaque";
path: string;
}
export interface StringContextDrvDeep {
type: "drvDeep";
drvPath: string;
}
export interface StringContextBuilt {
type: "built";
drvPath: string;
output: string;
}
export type StringContextElem = StringContextOpaque | StringContextDrvDeep | StringContextBuilt;
export type NixStringContext = Set<string>;
export class StringWithContext {
readonly [HAS_CONTEXT] = true as const;
value: string;
context: NixStringContext;
constructor(value: string, context: NixStringContext) {
this.value = value;
this.context = context;
}
}
export const isStringWithContext = (v: NixStrictValue): v is StringWithContext => {
return v instanceof StringWithContext;
};
export const mkStringWithContext = (value: string, context: NixStringContext): StringWithContext => {
return new StringWithContext(value, context);
};
export const mkPlainString = (value: string): string => value;
export const getStringValue = (s: string | StringWithContext): string => {
if (isStringWithContext(s)) {
return s.value;
}
return s;
};
const emptyContext: NixStringContext = new Set();
export const getStringContext = (s: string | StringWithContext): NixStringContext => {
if (isStringWithContext(s)) {
return s.context;
}
return emptyContext;
};
export const mergeContexts = (...contexts: NixStringContext[]): NixStringContext => {
const result = new Set<string>();
for (const ctx of contexts) {
for (const elem of ctx) {
result.add(elem);
}
}
return result;
};
export const encodeContextElem = (elem: StringContextElem): string => {
switch (elem.type) {
case "opaque":
return elem.path;
case "drvDeep":
return `=${elem.drvPath}`;
case "built":
return `!${elem.output}!${elem.drvPath}`;
}
};
export const decodeContextElem = (encoded: string): StringContextElem => {
if (encoded.startsWith("=")) {
return { type: "drvDeep", drvPath: encoded.slice(1) };
} else if (encoded.startsWith("!")) {
const secondBang = encoded.indexOf("!", 1);
if (secondBang === -1) {
throw new Error(`Invalid context element: ${encoded}`);
}
return {
type: "built",
output: encoded.slice(1, secondBang),
drvPath: encoded.slice(secondBang + 1),
};
} else {
return { type: "opaque", path: encoded };
}
};
export const addContextElem = (context: NixStringContext, elem: StringContextElem): void => {
context.add(encodeContextElem(elem));
};
export const addOpaqueContext = (context: NixStringContext, path: string): void => {
context.add(path);
};
export const addDrvDeepContext = (context: NixStringContext, drvPath: string): void => {
context.add(`=${drvPath}`);
};
export const addBuiltContext = (context: NixStringContext, drvPath: string, output: string): void => {
context.add(`!${output}!${drvPath}`);
};
export interface ParsedContextInfo {
path: boolean;
allOutputs: boolean;
outputs: string[];
}
export const parseContextToInfoMap = (context: NixStringContext): Map<string, ParsedContextInfo> => {
const result = new Map<string, ParsedContextInfo>();
const getOrCreate = (path: string): ParsedContextInfo => {
let info = result.get(path);
if (!info) {
info = { path: false, allOutputs: false, outputs: [] };
result.set(path, info);
}
return info;
};
for (const encoded of context) {
const elem = decodeContextElem(encoded);
switch (elem.type) {
case "opaque":
getOrCreate(elem.path).path = true;
break;
case "drvDeep":
getOrCreate(elem.drvPath).allOutputs = true;
break;
case "built":
getOrCreate(elem.drvPath).outputs.push(elem.output);
break;
}
}
for (const info of result.values()) {
info.outputs.sort();
}
return result;
};
+188
View File
@@ -0,0 +1,188 @@
import { isAttrs, isList } from "./builtins/type-check";
import { StringWithContext } from "./string-context";
import type { NixAttrs, NixStrictValue, NixValue } from "./types";
import { NixPath } from "./types";
export const IS_THUNK = Symbol("is_thunk");
const forceStack: NixThunk[] = [];
const MAX_FORCE_DEPTH = 1000;
export const DEBUG_THUNKS = { enabled: true };
/**
* NixThunk class - represents a lazy, unevaluated expression
*
* A thunk wraps a function that produces a value when called.
* Once evaluated, the result is cached to avoid recomputation.
*
* Thunk states:
* - Unevaluated: func is defined, result is undefined
* - Evaluating (blackhole): func is undefined, result is undefined
* - Evaluated: func is undefined, result is defined
*/
export class NixThunk {
readonly [IS_THUNK] = true as const;
func: (() => NixValue) | undefined;
result: NixStrictValue | undefined;
readonly label: string | undefined;
constructor(func: () => NixValue, label?: string) {
this.func = func;
this.result = undefined;
this.label = label;
}
toString(): string {
if (this.label) {
return `«thunk ${this.label}»`;
}
return `«thunk»`;
}
}
export const isThunk = (value: NixValue): value is NixThunk => {
return value instanceof NixThunk;
};
/**
* Force evaluation of a value
* If the value is a thunk, evaluate it and cache the result
* If already evaluated or not a thunk, return as-is
*
* Uses "blackhole" detection to catch infinite recursion:
* - Before evaluating, set func to undefined (entering blackhole state)
* - If we encounter a thunk with func=undefined and result=undefined, it's a blackhole
*
* @param value - Value to force (may be a thunk)
* @returns The forced/evaluated value
* @throws Error if infinite recursion is detected
*/
export const force = (value: NixValue): NixStrictValue => {
if (!isThunk(value)) {
return value;
}
if (value.func === undefined) {
if (value.result === undefined) {
const thunk = value as NixThunk;
let msg = `infinite recursion encountered at ${thunk}\n`;
if (DEBUG_THUNKS.enabled) {
msg += "Force chain (most recent first):\n";
for (let i = forceStack.length - 1; i >= 0; i--) {
const t = forceStack[i];
msg += ` ${i + 1}. ${t}`;
msg += "\n";
}
}
throw new Error(msg);
}
return value.result;
}
const thunk = value as NixThunk;
const func = thunk.func as () => NixValue;
thunk.func = undefined;
if (DEBUG_THUNKS.enabled) {
forceStack.push(thunk);
if (forceStack.length > MAX_FORCE_DEPTH) {
let msg = `force depth exceeded ${MAX_FORCE_DEPTH} at ${thunk}\n`;
msg += "Force chain (most recent first):\n";
for (let i = forceStack.length - 1; i >= Math.max(0, forceStack.length - 20); i--) {
const t = forceStack[i];
msg += ` ${i + 1}. ${t}`;
msg += "\n";
}
throw new Error(msg);
}
}
try {
const result = force(func());
thunk.result = result;
return result;
} catch (e) {
thunk.func = func;
throw e;
} finally {
if (DEBUG_THUNKS.enabled) {
forceStack.pop();
}
}
};
export const createThunk = (func: () => NixValue, label?: string): NixThunk => {
return new NixThunk(func, label);
};
export const IS_CYCLE = Symbol("is_cycle");
export const CYCLE_MARKER = { [IS_CYCLE]: true as const };
/**
* Deeply force a value, handling cycles by returning a special marker.
* Uses WeakSet to track seen objects and avoid infinite recursion.
* Returns a fully forced value where thunks are replaced with their results.
* Cyclic references are replaced with CYCLE_MARKER, preserving the container type.
*/
export const forceDeep = (value: NixValue, seen: WeakSet<object> = new WeakSet()): NixStrictValue => {
const forced = force(value);
if (forced === null || typeof forced !== "object") {
return forced;
}
if (seen.has(forced)) {
return CYCLE_MARKER;
}
if ((isAttrs(forced) && forced.size > 0) || (isList(forced) && forced.length > 0)) {
seen.add(forced);
}
if (forced instanceof StringWithContext || forced instanceof NixPath) {
return forced;
}
if (Array.isArray(forced)) {
return forced.map((item) => forceDeep(item, seen));
}
if (forced instanceof Map) {
const result: NixAttrs = new Map();
for (const [key, val] of forced) {
result.set(key, forceDeep(val, seen));
}
return result;
}
return forced;
};
export const forceShallow = (value: NixValue): NixStrictValue => {
const forced = force(value);
if (forced === null || typeof forced !== "object") {
return forced;
}
if (Array.isArray(forced)) {
return forced.map((item) => {
const forcedItem = force(item);
if (typeof forcedItem === "object" && forcedItem === forced) {
return CYCLE_MARKER;
} else {
return forcedItem;
}
});
}
if (isAttrs(forced)) {
const result: NixAttrs = new Map();
for (const [key, val] of forced) {
const forcedVal = force(val as NixValue);
result.set(key, forcedVal === forced ? CYCLE_MARKER : forcedVal);
}
return result;
}
return forced;
};
+127
View File
@@ -0,0 +1,127 @@
import { isAttrs, isFunction, typeOf } from "./builtins/type-check";
import { force } from "./thunk";
import type {
NixAttrs,
NixFloat,
NixFunction,
NixInt,
NixList,
NixNumber,
NixPath,
NixString,
NixValue,
} from "./types";
import { isNixPath, isStringWithContext } from "./types";
export const forceList = (value: NixValue): NixList => {
const forced = force(value);
if (!Array.isArray(forced)) {
throw new TypeError(`Expected list, got ${typeOf(forced)}`);
}
return forced;
};
export const forceFunction = (value: NixValue): NixFunction => {
const forced = force(value);
if (isFunction(forced)) {
return forced;
}
if (forced instanceof Map && forced.has("__functor")) {
const functorSet = forced as NixAttrs;
const functor = forceFunction(functorSet.get("__functor") as NixValue);
return (arg: NixValue) => forceFunction(functor(functorSet))(arg);
}
throw new TypeError(`Expected function, got ${typeOf(forced)}`);
};
export const forceAttrs = (value: NixValue): NixAttrs => {
const forced = force(value);
if (!isAttrs(forced)) {
throw new TypeError(`Expected attribute set, got ${typeOf(forced)}`);
}
return forced;
};
export const forceStringValue = (value: NixValue): string => {
const forced = force(value);
if (typeof forced === "string") {
return forced;
}
if (isStringWithContext(forced)) {
return forced.value;
}
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
};
export const forceString = (value: NixValue): NixString => {
const forced = force(value);
if (typeof forced === "string") {
return forced;
}
if (isStringWithContext(forced)) {
return forced;
}
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
};
export const forceStringNoCtx = (value: NixValue): string => {
const forced = force(value);
if (typeof forced === "string") {
return forced;
}
if (isStringWithContext(forced)) {
throw new TypeError(`the string '${forced.value}' is not allowed to refer to a store path`);
}
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
};
export const forceBool = (value: NixValue): boolean => {
const forced = force(value);
if (typeof forced !== "boolean") {
throw new TypeError(`Expected boolean, got ${typeOf(forced)}`);
}
return forced;
};
export const forceInt = (value: NixValue): NixInt => {
const forced = force(value);
if (typeof forced === "bigint") {
return forced;
}
throw new TypeError(`Expected int, got ${typeOf(forced)}`);
};
export const forceFloat = (value: NixValue): NixFloat => {
const forced = force(value);
if (typeof forced === "number") {
return forced;
}
throw new TypeError(`Expected float, got ${typeOf(forced)}`);
};
export const forceNumeric = (value: NixValue): NixNumber => {
const forced = force(value);
if (typeof forced === "bigint" || typeof forced === "number") {
return forced;
}
throw new TypeError(`Expected numeric type, got ${typeOf(forced)}`);
};
export const coerceNumeric = (a: NixNumber, b: NixNumber): [NixFloat, NixFloat] | [NixInt, NixInt] => {
const aIsInt = typeof a === "bigint";
const bIsInt = typeof b === "bigint";
if (!aIsInt || !bIsInt) {
return [Number(a), Number(b)];
}
return [a, b];
};
export const forceNixPath = (value: NixValue): NixPath => {
const forced = force(value);
if (isNixPath(forced)) {
return forced;
}
throw new TypeError(`Expected path, got ${typeOf(forced)}`);
};
+115
View File
@@ -0,0 +1,115 @@
import { PRIMOP_METADATA, type PrimopMetadata } from "./builtins";
import { HAS_CONTEXT, isStringWithContext, type StringWithContext } from "./string-context";
import { type CYCLE_MARKER, force, type NixThunk } from "./thunk";
import { forceAttrs, forceStringNoCtx } from "./type-assert";
export { HAS_CONTEXT, isStringWithContext };
export type { StringWithContext };
export const IS_PATH = Symbol("IS_PATH");
export class NixPath {
readonly [IS_PATH] = true as const;
value: string;
constructor(value: string) {
this.value = value;
}
}
export const isNixPath = (v: NixStrictValue): v is NixPath => {
return v instanceof NixPath;
};
export type NixInt = bigint;
export type NixFloat = number;
export type NixNumber = NixInt | NixFloat;
export type NixBool = boolean;
export type NixString = string | StringWithContext;
export type NixNull = null;
export const ATTR_POSITIONS = Symbol("attrPositions");
export type NixList = NixValue[];
export type NixAttrs = Map<string, NixValue> & { [ATTR_POSITIONS]?: Map<string, number> };
export type NixFunction = ((arg: NixValue) => NixValue) & {
args?: NixArgs;
[PRIMOP_METADATA]?: PrimopMetadata;
};
export class NixArgs {
required: string[];
optional: string[];
allowed: Set<string>;
ellipsis: boolean;
positions: Map<string, number>;
constructor(required: string[], optional: string[], positions: Map<string, number>, ellipsis: boolean) {
this.required = required;
this.optional = optional;
this.positions = positions;
this.ellipsis = ellipsis;
this.allowed = new Set(required.concat(optional));
}
check(arg: NixValue) {
const attrs = forceAttrs(arg);
for (const key of this.required) {
if (!attrs.has(key)) {
throw new Error(`Function called without required argument '${key}'`);
}
}
if (!this.ellipsis) {
for (const key of attrs.keys()) {
if (!this.allowed.has(key)) {
throw new Error(`Function called with unexpected argument '${key}'`);
}
}
}
}
}
export const mkFunction = (
f: (arg: NixValue) => NixValue,
required: string[],
optional: string[],
positions: Map<string, number>,
ellipsis: boolean,
): NixFunction => {
const func: NixFunction = f;
func.args = new NixArgs(required, optional, positions, ellipsis);
return func;
};
export const mkAttrs = (
attrs: NixAttrs,
positions: Map<string, number>,
dyns?: { dynKeys: NixValue[]; dynVals: NixValue[]; dynSpans: number[] },
): NixAttrs => {
if (dyns) {
const len = dyns.dynKeys.length;
for (let i = 0; i < len; i++) {
const key = force(dyns.dynKeys[i]);
if (key === null) {
continue;
}
const str = forceStringNoCtx(key);
attrs.set(str, dyns.dynVals[i]);
positions.set(str, dyns.dynSpans[i]);
}
}
if (positions.size > 0) {
attrs[ATTR_POSITIONS] = positions;
}
return attrs;
};
export type NixPrimitive = NixNull | NixBool | NixInt | NixFloat | NixString;
export type NixValue =
| NixPrimitive
| NixPath
| NixList
| NixAttrs
| NixFunction
| NixThunk
| typeof CYCLE_MARKER;
export type NixStrictValue = Exclude<NixValue, NixThunk>;
export class CatchableError extends Error {}
+134
View File
@@ -0,0 +1,134 @@
import type { NixRuntime } from "..";
import type { builtins } from "../builtins";
import type { FetchGitResult, FetchTarballResult, FetchUrlResult } from "../builtins/io";
import type {
assert,
call,
concatStringsWithContext,
hasAttr,
lookupWith,
mkPos,
resolvePath,
select,
selectWithDefault,
} from "../helpers";
import type { op } from "../operators";
import type { createThunk, force } from "../thunk";
import type { forceBool } from "../type-assert";
import type { mkAttrs, mkFunction, NixAttrs, NixStrictValue } from "../types";
declare global {
var Nix: NixRuntime;
var $t: typeof createThunk;
var $f: typeof force;
var $fb: typeof forceBool;
var $a: typeof assert;
var $c: typeof call;
var $h: typeof hasAttr;
var $s: typeof select;
var $sd: typeof selectWithDefault;
var $l: typeof lookupWith;
var $r: typeof resolvePath;
var $cs: typeof concatStringsWithContext;
var $ma: typeof mkAttrs;
var $mf: typeof mkFunction;
var $mp: typeof mkPos;
var $oa: typeof op.add;
var $os: typeof op.sub;
var $om: typeof op.mul;
var $od: typeof op.div;
var $oe: typeof op.eq;
var $ol: typeof op.lt;
var $og: typeof op.gt;
var $oc: typeof op.concat;
var $ou: typeof op.update;
var $b: typeof builtins;
var $e: NixAttrs;
var $gb: typeof Nix.getReplBinding;
namespace Deno {
namespace core {
namespace ops {
function op_import(path: string): [Uint8Array, string];
function op_scoped_import(path: string, scopeKeys: string[]): [Uint8Array, string];
function op_resolve_path(currentDir: string, path: string): string;
function op_read_file(path: string): string;
function op_read_file_type(path: string): string;
function op_read_dir(path: string): Map<string, string>;
function op_path_exists(path: string): boolean;
function op_walk_dir(path: string): [string, string][];
function op_make_placeholder(output: string): string;
function op_store_path(path: string): string;
function op_convert_hash(hash: string, hashAlgo: string | null, toHashFormat: string): string;
function op_hash_string(algo: string, data: string): string;
function op_hash_file(algo: string, path: string): string;
function op_parse_hash(hashStr: string, algo: string | null): { hex: string; algo: string };
function op_add_path(
path: string,
name: string | null,
recursive: boolean,
sha256: string | null,
): string;
function op_add_filtered_path(
path: string,
name: string | null,
recursive: boolean,
sha256: string | null,
includePaths: string[],
): string;
function op_decode_span(span: number): NixAttrs;
function op_to_file(name: string, contents: string, references: string[]): string;
function op_copy_path_to_store(path: string): string;
function op_get_env(key: string): string;
function op_match(regex: string, text: string): (string | null)[] | null;
function op_split(regex: string, text: string): (string | (string | null)[])[];
function op_from_json(json: string): NixStrictValue;
function op_from_toml(toml: string): NixStrictValue;
function op_to_xml(e: NixValue): [string, string[]];
function op_finalize_derivation(
name: string,
builder: string,
platform: string,
outputs: string[],
args: string[],
env: [string, string][],
context: string[],
fixedOutput: { hashAlgo: string; hash: string; hashMode: string } | null,
): { drvPath: string; outputs: [string, string][] };
function op_fetch_url(
url: string,
expectedHash: string | null,
name: string | null,
executable: boolean,
): FetchUrlResult;
function op_fetch_tarball(
url: string,
name: string | null,
sha256: string | null,
): FetchTarballResult;
function op_fetch_git(
url: string,
ref: string | null,
rev: string | null,
shallow: boolean,
submodules: boolean,
allRefs: boolean,
name: string | null,
): FetchGitResult;
}
}
}
}
+617
View File
@@ -0,0 +1,617 @@
import {
assert,
call,
concatStringsWithContext,
hasAttr,
lookupWith,
mkPos,
resolvePath,
select,
selectWithDefault,
} from "./helpers";
import { op } from "./operators";
import { NixThunk } from "./thunk";
import { forceBool } from "./type-assert";
import { mkAttrs, NixArgs, type NixAttrs, type NixFunction, type NixValue } from "./types";
import { builtins } from "./builtins";
enum Op {
PushConst = 0x01,
PushString = 0x02,
PushNull = 0x03,
PushTrue = 0x04,
PushFalse = 0x05,
LoadLocal = 0x06,
LoadOuter = 0x07,
StoreLocal = 0x08,
AllocLocals = 0x09,
MakeThunk = 0x0A,
MakeClosure = 0x0B,
MakePatternClosure = 0x0C,
Call = 0x0D,
CallNoSpan = 0x0E,
MakeAttrs = 0x0F,
MakeAttrsDyn = 0x10,
MakeEmptyAttrs = 0x11,
Select = 0x12,
SelectDefault = 0x13,
HasAttr = 0x14,
MakeList = 0x15,
OpAdd = 0x16,
OpSub = 0x17,
OpMul = 0x18,
OpDiv = 0x19,
OpEq = 0x20,
OpNeq = 0x21,
OpLt = 0x22,
OpGt = 0x23,
OpLeq = 0x24,
OpGeq = 0x25,
OpConcat = 0x26,
OpUpdate = 0x27,
OpNeg = 0x28,
OpNot = 0x29,
ForceBool = 0x30,
JumpIfFalse = 0x31,
JumpIfTrue = 0x32,
Jump = 0x33,
ConcatStrings = 0x34,
ResolvePath = 0x35,
Assert = 0x36,
PushWith = 0x37,
PopWith = 0x38,
WithLookup = 0x39,
LoadBuiltins = 0x40,
LoadBuiltin = 0x41,
MkPos = 0x43,
LoadReplBinding = 0x44,
LoadScopedBinding = 0x45,
Return = 0x46,
}
interface ScopeChain {
locals: NixValue[];
parent: ScopeChain | null;
}
interface WithScope {
env: NixValue;
last: WithScope | null;
}
const strings: string[] = [];
const constants: NixValue[] = [];
const $e: NixAttrs = new Map();
function readU16(code: Uint8Array, offset: number): number {
return code[offset] | (code[offset + 1] << 8);
}
function readU32(code: Uint8Array, offset: number): number {
return (
code[offset] |
(code[offset + 1] << 8) |
(code[offset + 2] << 16) |
(code[offset + 3] << 24)
) >>> 0;
}
function readI32(code: Uint8Array, offset: number): number {
return code[offset] | (code[offset + 1] << 8) | (code[offset + 2] << 16) | (code[offset + 3] << 24);
}
export function execBytecode(code: Uint8Array, currentDir: string): NixValue {
const chain: ScopeChain = { locals: [], parent: null };
return execFrame(code, 0, chain, currentDir, null, null);
}
export function execBytecodeScoped(
code: Uint8Array,
currentDir: string,
scopeMap: NixAttrs,
): NixValue {
const chain: ScopeChain = { locals: [], parent: null };
return execFrame(code, 0, chain, currentDir, null, scopeMap);
}
function execFrame(
code: Uint8Array,
startPc: number,
chain: ScopeChain,
currentDir: string,
withScope: WithScope | null,
scopeMap: NixAttrs | null,
): NixValue {
const locals = chain.locals;
const stack: NixValue[] = [];
let pc = startPc;
for (;;) {
const opcode = code[pc++];
switch (opcode) {
case Op.PushConst: {
const idx = readU32(code, pc);
pc += 4;
stack.push(constants[idx]);
break;
}
case Op.PushString: {
const idx = readU32(code, pc);
pc += 4;
stack.push(strings[idx]);
break;
}
case Op.PushNull:
stack.push(null);
break;
case Op.PushTrue:
stack.push(true);
break;
case Op.PushFalse:
stack.push(false);
break;
case Op.LoadLocal: {
const idx = readU32(code, pc);
pc += 4;
stack.push(locals[idx]);
break;
}
case Op.LoadOuter: {
const layer = code[pc++];
const idx = readU32(code, pc);
pc += 4;
let c: ScopeChain = chain;
for (let i = 0; i < layer; i++) c = c.parent!;
stack.push(c.locals[idx]);
break;
}
case Op.StoreLocal: {
const idx = readU32(code, pc);
pc += 4;
locals[idx] = stack.pop()!;
break;
}
case Op.AllocLocals: {
const n = readU32(code, pc);
pc += 4;
for (let i = 0; i < n; i++) locals.push(null);
break;
}
case Op.MakeThunk: {
const bodyPc = readU32(code, pc);
pc += 4;
const labelIdx = readU32(code, pc);
pc += 4;
const label = strings[labelIdx];
const scopeChain = chain;
const scopeCode = code;
const scopeDir = currentDir;
const scopeWith = withScope;
stack.push(
new NixThunk(
() => execFrame(scopeCode, bodyPc, scopeChain, scopeDir, scopeWith, null),
label,
),
);
break;
}
case Op.MakeClosure: {
const bodyPc = readU32(code, pc);
pc += 4;
const nSlots = readU32(code, pc);
pc += 4;
const closureChain = chain;
const closureCode = code;
const closureDir = currentDir;
const closureWith = withScope;
const func: NixFunction = (arg: NixValue) => {
const innerLocals = new Array<NixValue>(1 + nSlots).fill(null);
innerLocals[0] = arg;
const innerChain: ScopeChain = { locals: innerLocals, parent: closureChain };
return execFrame(closureCode, bodyPc, innerChain, closureDir, closureWith, null);
};
stack.push(func);
break;
}
case Op.MakePatternClosure: {
const bodyPc = readU32(code, pc);
pc += 4;
const nSlots = readU32(code, pc);
pc += 4;
const nRequired = readU16(code, pc);
pc += 2;
const nOptional = readU16(code, pc);
pc += 2;
const hasEllipsis = code[pc++] !== 0;
const required: string[] = [];
for (let i = 0; i < nRequired; i++) {
required.push(strings[readU32(code, pc)]);
pc += 4;
}
const optional: string[] = [];
for (let i = 0; i < nOptional; i++) {
optional.push(strings[readU32(code, pc)]);
pc += 4;
}
const positions = new Map<string, number>();
const nTotal = nRequired + nOptional;
for (let i = 0; i < nTotal; i++) {
const nameIdx = readU32(code, pc);
pc += 4;
const spanId = readU32(code, pc);
pc += 4;
positions.set(strings[nameIdx], spanId);
}
const closureChain = chain;
const closureCode = code;
const closureDir = currentDir;
const closureWith = withScope;
const func: NixFunction = (arg: NixValue) => {
const innerLocals = new Array<NixValue>(1 + nSlots).fill(null);
innerLocals[0] = arg;
const innerChain: ScopeChain = { locals: innerLocals, parent: closureChain };
return execFrame(closureCode, bodyPc, innerChain, closureDir, closureWith, null);
};
func.args = new NixArgs(required, optional, positions, hasEllipsis);
stack.push(func);
break;
}
case Op.Call: {
const spanId = readU32(code, pc);
pc += 4;
const arg = stack.pop()!;
const func = stack.pop()!;
stack.push(call(func, arg, spanId));
break;
}
case Op.CallNoSpan: {
const arg = stack.pop()!;
const func = stack.pop()!;
stack.push(call(func, arg));
break;
}
case Op.MakeAttrs: {
const n = readU32(code, pc);
pc += 4;
const spanValues: number[] = [];
for (let i = 0; i < n; i++) {
spanValues.push(stack.pop() as number);
}
spanValues.reverse();
const map: NixAttrs = new Map();
const posMap = new Map<string, number>();
const pairs: [string, NixValue][] = [];
for (let i = 0; i < n; i++) {
const val = stack.pop()!;
const key = stack.pop() as string;
pairs.push([key, val]);
}
pairs.reverse();
for (let i = 0; i < n; i++) {
map.set(pairs[i][0], pairs[i][1]);
posMap.set(pairs[i][0], spanValues[i]);
}
stack.push(mkAttrs(map, posMap));
break;
}
case Op.MakeAttrsDyn: {
const nStatic = readU32(code, pc);
pc += 4;
const nDyn = readU32(code, pc);
pc += 4;
const dynTriples: [NixValue, NixValue, number][] = [];
for (let i = 0; i < nDyn; i++) {
const dynSpan = stack.pop() as number;
const dynVal = stack.pop()!;
const dynKey = stack.pop()!;
dynTriples.push([dynKey, dynVal, dynSpan]);
}
dynTriples.reverse();
const spanValues: number[] = [];
for (let i = 0; i < nStatic; i++) {
spanValues.push(stack.pop() as number);
}
spanValues.reverse();
const map: NixAttrs = new Map();
const posMap = new Map<string, number>();
const pairs: [string, NixValue][] = [];
for (let i = 0; i < nStatic; i++) {
const val = stack.pop()!;
const key = stack.pop() as string;
pairs.push([key, val]);
}
pairs.reverse();
for (let i = 0; i < nStatic; i++) {
map.set(pairs[i][0], pairs[i][1]);
posMap.set(pairs[i][0], spanValues[i]);
}
const dynKeys: NixValue[] = [];
const dynVals: NixValue[] = [];
const dynSpans: number[] = [];
for (const [k, v, s] of dynTriples) {
dynKeys.push(k);
dynVals.push(v);
dynSpans.push(s);
}
stack.push(mkAttrs(map, posMap, { dynKeys, dynVals, dynSpans }));
break;
}
case Op.MakeEmptyAttrs:
stack.push($e);
break;
case Op.Select: {
const nKeys = readU16(code, pc);
pc += 2;
const spanId = readU32(code, pc);
pc += 4;
const keys: NixValue[] = [];
for (let i = 0; i < nKeys; i++) keys.push(stack.pop()!);
keys.reverse();
const obj = stack.pop()!;
stack.push(select(obj, keys, spanId));
break;
}
case Op.SelectDefault: {
const nKeys = readU16(code, pc);
pc += 2;
const spanId = readU32(code, pc);
pc += 4;
const defaultVal = stack.pop()!;
const keys: NixValue[] = [];
for (let i = 0; i < nKeys; i++) keys.push(stack.pop()!);
keys.reverse();
const obj = stack.pop()!;
stack.push(selectWithDefault(obj, keys, defaultVal, spanId));
break;
}
case Op.HasAttr: {
const nKeys = readU16(code, pc);
pc += 2;
const keys: NixValue[] = [];
for (let i = 0; i < nKeys; i++) keys.push(stack.pop()!);
keys.reverse();
const obj = stack.pop()!;
stack.push(hasAttr(obj, keys));
break;
}
case Op.MakeList: {
const count = readU32(code, pc);
pc += 4;
const items: NixValue[] = new Array(count);
for (let i = count - 1; i >= 0; i--) {
items[i] = stack.pop()!;
}
stack.push(items);
break;
}
case Op.OpAdd: {
const b = stack.pop()!;
const a = stack.pop()!;
stack.push(op.add(a, b));
break;
}
case Op.OpSub: {
const b = stack.pop()!;
const a = stack.pop()!;
stack.push(op.sub(a, b));
break;
}
case Op.OpMul: {
const b = stack.pop()!;
const a = stack.pop()!;
stack.push(op.mul(a, b));
break;
}
case Op.OpDiv: {
const b = stack.pop()!;
const a = stack.pop()!;
stack.push(op.div(a, b));
break;
}
case Op.OpEq: {
const b = stack.pop()!;
const a = stack.pop()!;
stack.push(op.eq(a, b));
break;
}
case Op.OpNeq: {
const b = stack.pop()!;
const a = stack.pop()!;
stack.push(!op.eq(a, b));
break;
}
case Op.OpLt: {
const b = stack.pop()!;
const a = stack.pop()!;
stack.push(op.lt(a, b));
break;
}
case Op.OpGt: {
const b = stack.pop()!;
const a = stack.pop()!;
stack.push(op.gt(a, b));
break;
}
case Op.OpLeq: {
const b = stack.pop()!;
const a = stack.pop()!;
stack.push(!op.gt(a, b));
break;
}
case Op.OpGeq: {
const b = stack.pop()!;
const a = stack.pop()!;
stack.push(!op.lt(a, b));
break;
}
case Op.OpConcat: {
const b = stack.pop()!;
const a = stack.pop()!;
stack.push(op.concat(a, b));
break;
}
case Op.OpUpdate: {
const b = stack.pop()!;
const a = stack.pop()!;
stack.push(op.update(a, b));
break;
}
case Op.OpNeg: {
const a = stack.pop()!;
stack.push(op.sub(0n, a));
break;
}
case Op.OpNot: {
const a = stack.pop()!;
stack.push(!forceBool(a));
break;
}
case Op.ForceBool: {
const val = stack.pop()!;
stack.push(forceBool(val));
break;
}
case Op.JumpIfFalse: {
const offset = readI32(code, pc);
pc += 4;
const val = stack.pop()!;
if (val === false) {
pc += offset;
}
break;
}
case Op.JumpIfTrue: {
const offset = readI32(code, pc);
pc += 4;
const val = stack.pop()!;
if (val === true) {
pc += offset;
}
break;
}
case Op.Jump: {
const offset = readI32(code, pc);
pc += 4;
pc += offset;
break;
}
case Op.ConcatStrings: {
const nParts = readU16(code, pc);
pc += 2;
const forceString = code[pc++] !== 0;
const parts: NixValue[] = new Array(nParts);
for (let i = nParts - 1; i >= 0; i--) {
parts[i] = stack.pop()!;
}
stack.push(concatStringsWithContext(parts, forceString));
break;
}
case Op.ResolvePath: {
const pathExpr = stack.pop()!;
stack.push(resolvePath(currentDir, pathExpr));
break;
}
case Op.Assert: {
const rawIdx = readU32(code, pc);
pc += 4;
const spanId = readU32(code, pc);
pc += 4;
const expr = stack.pop()!;
const assertion = stack.pop()!;
stack.push(assert(assertion, expr, strings[rawIdx], spanId));
break;
}
case Op.PushWith: {
const namespace = stack.pop()!;
withScope = { env: namespace, last: withScope };
break;
}
case Op.PopWith:
withScope = withScope!.last;
break;
case Op.WithLookup: {
const nameIdx = readU32(code, pc);
pc += 4;
stack.push(lookupWith(strings[nameIdx], withScope!));
break;
}
case Op.LoadBuiltins:
stack.push(builtins);
break;
case Op.LoadBuiltin: {
const idx = readU32(code, pc);
pc += 4;
stack.push(builtins.get(strings[idx])!);
break;
}
case Op.MkPos: {
const spanId = readU32(code, pc);
pc += 4;
stack.push(mkPos(spanId));
break;
}
case Op.LoadReplBinding: {
const idx = readU32(code, pc);
pc += 4;
stack.push(Nix.getReplBinding(strings[idx]));
break;
}
case Op.LoadScopedBinding: {
const idx = readU32(code, pc);
pc += 4;
stack.push(scopeMap!.get(strings[idx])!);
break;
}
case Op.Return:
return stack.pop()!;
default:
throw new Error(`Unknown bytecode opcode: ${opcode ? `0x${opcode.toString(16)}` : "undefined"} at pc=${pc - 1}`);
}
}
}
declare const Nix: {
getReplBinding: (name: string) => NixValue;
};
export { strings as vmStrings, constants as vmConstants };
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "DOM"],
"moduleResolution": "bundler",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"declaration": false,
"outDir": "./dist",
"rootDir": "./src",
"typeRoots": ["./src/types"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+193 -154
View File
@@ -1,101 +1,101 @@
use std::ops::Deref;
use std::path::Path;
use gc_arena::Collect;
use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use rnix::TextRange;
use string_interner::Symbol as _;
use crate::ir::{ArgId, Attr, BinOpKind, Ir, Param, RawIrRef, StringId, ThunkId, UnOpKind};
use crate::ir::{ArgId, Attr, BinOpKind, Ir, Param, RawIrRef, SymId, ThunkId, UnOpKind};
pub(crate) struct InstructionPtr(pub usize);
#[derive(Clone, Hash, Eq, PartialEq)]
pub(crate) enum Constant {
Int(i64),
Float(u64),
}
#[derive(Collect)]
#[collect(require_static)]
pub struct Bytecode {
pub code: Box<[u8]>,
pub current_dir: String,
}
pub(crate) trait BytecodeContext {
fn intern_string(&mut self, s: &str) -> StringId;
fn register_span(&mut self, range: TextRange) -> u32;
fn get_code(&self) -> &[u8];
fn get_code_mut(&mut self) -> &mut Vec<u8>;
fn intern_string(&mut self, s: &str) -> u32;
fn intern_constant(&mut self, c: Constant) -> u32;
fn register_span(&self, range: TextRange) -> u32;
fn get_sym(&self, id: SymId) -> &str;
fn get_current_dir(&self) -> &Path;
}
#[repr(u8)]
#[derive(Clone, Copy, TryFromPrimitive)]
#[allow(clippy::enum_variant_names)]
pub enum Op {
PushSmi,
PushBigInt,
PushFloat,
PushString,
PushNull,
PushTrue,
PushFalse,
PushConst = 0x01,
PushString = 0x02,
PushNull = 0x03,
PushTrue = 0x04,
PushFalse = 0x05,
LoadLocal,
LoadOuter,
StoreLocal,
AllocLocals,
LoadLocal = 0x06,
LoadOuter = 0x07,
StoreLocal = 0x08,
AllocLocals = 0x09,
MakeThunk,
MakeClosure,
MakePatternClosure,
MakeThunk = 0x0A,
MakeClosure = 0x0B,
MakePatternClosure = 0x0C,
Call,
CallNoSpan,
Call = 0x0D,
CallNoSpan = 0x0E,
MakeAttrs,
MakeAttrsDyn,
MakeEmptyAttrs,
Select,
SelectDefault,
HasAttr,
MakeAttrs = 0x0F,
MakeAttrsDyn = 0x10,
MakeEmptyAttrs = 0x11,
Select = 0x12,
SelectDefault = 0x13,
HasAttr = 0x14,
MakeList,
MakeList = 0x15,
OpAdd,
OpSub,
OpMul,
OpDiv,
OpEq,
OpNeq,
OpLt,
OpGt,
OpLeq,
OpGeq,
OpConcat,
OpUpdate,
OpAdd = 0x16,
OpSub = 0x17,
OpMul = 0x18,
OpDiv = 0x19,
OpEq = 0x20,
OpNeq = 0x21,
OpLt = 0x22,
OpGt = 0x23,
OpLeq = 0x24,
OpGeq = 0x25,
OpConcat = 0x26,
OpUpdate = 0x27,
OpNeg,
OpNot,
OpNeg = 0x28,
OpNot = 0x29,
ForceBool,
JumpIfFalse,
JumpIfTrue,
Jump,
ForceBool = 0x30,
JumpIfFalse = 0x31,
JumpIfTrue = 0x32,
Jump = 0x33,
ConcatStrings,
ResolvePath,
ConcatStrings = 0x34,
ResolvePath = 0x35,
Assert,
Assert = 0x36,
PushWith,
PopWith,
WithLookup,
PushWith = 0x37,
PopWith = 0x38,
WithLookup = 0x39,
LoadBuiltins,
LoadBuiltin,
LoadBuiltins = 0x40,
LoadBuiltin = 0x41,
MkPos,
MkPos = 0x43,
LoadReplBinding,
LoadScopedBinding,
LoadReplBinding = 0x44,
LoadScopedBinding = 0x45,
Return,
Return = 0x46,
}
struct ScopeInfo {
@@ -106,78 +106,71 @@ struct ScopeInfo {
struct BytecodeEmitter<'a, Ctx: BytecodeContext> {
ctx: &'a mut Ctx,
code: Vec<u8>,
scope_stack: Vec<ScopeInfo>,
}
pub(crate) fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr {
let ip = ctx.get_code().len();
pub(crate) fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> Bytecode {
let current_dir = ctx.get_current_dir().to_string_lossy().to_string();
let mut emitter = BytecodeEmitter::new(ctx);
emitter.emit_toplevel(ir);
InstructionPtr(ip)
Bytecode {
code: emitter.code.into_boxed_slice(),
current_dir,
}
}
pub(crate) fn compile_bytecode_scoped(
ir: RawIrRef<'_>,
ctx: &mut impl BytecodeContext,
) -> Bytecode {
let current_dir = ctx.get_current_dir().to_string_lossy().to_string();
let mut emitter = BytecodeEmitter::new(ctx);
emitter.emit_toplevel_scoped(ir);
Bytecode {
code: emitter.code.into_boxed_slice(),
current_dir,
}
}
impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
fn new(ctx: &'a mut Ctx) -> Self {
Self {
ctx,
code: Vec::with_capacity(4096),
scope_stack: Vec::with_capacity(32),
}
}
#[inline]
fn emit_op(&mut self, op: Op) {
self.ctx.get_code_mut().push(op as u8);
self.code.push(op as u8);
}
#[inline]
fn emit_u8(&mut self, val: u8) {
self.ctx.get_code_mut().push(val);
self.code.push(val);
}
#[inline]
fn emit_u16(&mut self, val: u16) {
self.ctx
.get_code_mut()
.extend_from_slice(&val.to_le_bytes());
self.code.extend_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_u32(&mut self, val: u32) {
self.ctx
.get_code_mut()
.extend_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_i32(&mut self, val: i32) {
self.ctx
.get_code_mut()
.extend_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_i64(&mut self, val: i64) {
self.ctx
.get_code_mut()
.extend_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_f64(&mut self, val: f64) {
self.ctx
.get_code_mut()
.extend_from_slice(&val.to_le_bytes());
self.code.extend_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_i32_placeholder(&mut self) -> usize {
let offset = self.ctx.get_code_mut().len();
self.ctx.get_code_mut().extend_from_slice(&[0u8; 4]);
let offset = self.code.len();
self.code.extend_from_slice(&[0u8; 4]);
offset
}
#[inline]
fn patch_i32(&mut self, offset: usize, val: i32) {
self.ctx.get_code_mut()[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
self.code[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
}
#[inline]
@@ -188,18 +181,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
#[inline]
fn patch_jump_target(&mut self, placeholder_offset: usize) {
let current_pos = self.ctx.get_code_mut().len();
let current_pos = self.code.len();
let relative_offset = (current_pos as i32) - (placeholder_offset as i32) - 4;
self.patch_i32(placeholder_offset, relative_offset);
}
#[inline]
fn emit_str_id(&mut self, id: StringId) {
self.ctx
.get_code_mut()
.extend_from_slice(&(id.0.to_usize() as u32).to_le_bytes());
}
fn current_depth(&self) -> u16 {
self.scope_stack.last().map_or(0, |s| s.depth)
}
@@ -411,19 +397,50 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
}
}
fn emit_toplevel_scoped(&mut self, ir: RawIrRef<'_>) {
match ir.deref() {
Ir::TopLevel { body, thunks } => {
let with_thunk_count = self.count_with_thunks(*body);
let total_slots = thunks.len() + with_thunk_count;
let all_thunks = self.collect_all_thunks(thunks, *body);
let thunk_ids: Vec<ThunkId> = all_thunks.iter().map(|&(id, _)| id).collect();
self.push_scope(false, None, &thunk_ids);
if total_slots > 0 {
self.emit_op(Op::AllocLocals);
self.emit_u32(total_slots as u32);
}
self.emit_scope_thunks(thunks);
self.emit_expr(*body);
self.emit_op(Op::Return);
self.pop_scope();
}
_ => {
self.push_scope(false, None, &[]);
self.emit_expr(ir);
self.emit_op(Op::Return);
self.pop_scope();
}
}
}
fn emit_scope_thunks(&mut self, thunks: &[(ThunkId, RawIrRef<'_>)]) {
for &(id, inner) in thunks {
let label = format!("e{}", id.0);
let label_idx = self.ctx.intern_string(&label);
let skip_patch = self.emit_jump_placeholder();
let entry_point = self.ctx.get_code_mut().len() as u32;
let entry_point = self.code.len() as u32;
self.emit_expr(inner);
self.emit_op(Op::Return);
self.patch_jump_target(skip_patch);
self.emit_op(Op::MakeThunk);
self.emit_u32(entry_point);
self.emit_str_id(label_idx);
self.emit_u32(label_idx);
let (_, local_idx) = self.resolve_thunk(id);
self.emit_op(Op::StoreLocal);
self.emit_u32(local_idx);
@@ -433,17 +450,14 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
fn emit_expr(&mut self, ir: RawIrRef<'_>) {
match ir.deref() {
&Ir::Int(x) => {
if x <= i32::MAX as i64 {
self.emit_op(Op::PushSmi);
self.emit_i32(x as i32);
} else {
self.emit_op(Op::PushBigInt);
self.emit_i64(x);
}
let idx = self.ctx.intern_constant(Constant::Int(x));
self.emit_op(Op::PushConst);
self.emit_u32(idx);
}
&Ir::Float(x) => {
self.emit_op(Op::PushFloat);
self.emit_f64(x);
let idx = self.ctx.intern_constant(Constant::Float(x.to_bits()));
self.emit_op(Op::PushConst);
self.emit_u32(idx);
}
&Ir::Bool(true) => self.emit_op(Op::PushTrue),
&Ir::Bool(false) => self.emit_op(Op::PushFalse),
@@ -451,7 +465,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
Ir::Str(s) => {
let idx = self.ctx.intern_string(s.deref());
self.emit_op(Op::PushString);
self.emit_str_id(idx);
self.emit_u32(idx);
}
&Ir::Path(p) => {
self.emit_expr(p);
@@ -463,20 +477,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_op(Op::JumpIfFalse);
let else_placeholder = self.emit_i32_placeholder();
let after_jif = self.ctx.get_code_mut().len();
let after_jif = self.code.len();
self.emit_expr(consq);
self.emit_op(Op::Jump);
let end_placeholder = self.emit_i32_placeholder();
let after_jump = self.ctx.get_code_mut().len();
let after_jump = self.code.len();
let else_offset = (after_jump as i32) - (after_jif as i32);
self.patch_i32(else_placeholder, else_offset);
self.emit_expr(alter);
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
let end_offset = (self.code.len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset);
}
&Ir::BinOp { lhs, rhs, kind } => {
@@ -540,8 +554,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_op(Op::LoadBuiltins);
}
&Ir::Builtin(name) => {
let sym = self.ctx.get_sym(name).to_string();
let idx = self.ctx.intern_string(&sym);
self.emit_op(Op::LoadBuiltin);
self.emit_u32(name.0.to_usize() as u32);
self.emit_u32(idx);
}
&Ir::ConcatStrings {
ref parts,
@@ -568,7 +584,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_expr(*assertion);
self.emit_expr(*expr);
self.emit_op(Op::Assert);
self.emit_str_id(raw_idx);
self.emit_u32(raw_idx);
self.emit_u32(span_id);
}
&Ir::CurPos(span) => {
@@ -577,12 +593,16 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_u32(span_id);
}
&Ir::ReplBinding(name) => {
let sym = self.ctx.get_sym(name).to_string();
let idx = self.ctx.intern_string(&sym);
self.emit_op(Op::LoadReplBinding);
self.emit_str_id(name);
self.emit_u32(idx);
}
&Ir::ScopedImportBinding(name) => {
let sym = self.ctx.get_sym(name).to_string();
let idx = self.ctx.intern_string(&sym);
self.emit_op(Op::LoadScopedBinding);
self.emit_str_id(name);
self.emit_u32(idx);
}
&Ir::With {
namespace,
@@ -592,8 +612,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_with(namespace, body, thunks);
}
&Ir::WithLookup(name) => {
let sym = self.ctx.get_sym(name).to_string();
let idx = self.ctx.intern_string(&sym);
self.emit_op(Op::WithLookup);
self.emit_str_id(name);
self.emit_u32(idx);
}
}
}
@@ -606,20 +628,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_op(Op::ForceBool);
self.emit_op(Op::JumpIfFalse);
let skip_placeholder = self.emit_i32_placeholder();
let after_jif = self.ctx.get_code_mut().len();
let after_jif = self.code.len();
self.emit_expr(rhs);
self.emit_op(Op::ForceBool);
self.emit_op(Op::Jump);
let end_placeholder = self.emit_i32_placeholder();
let after_jump = self.ctx.get_code_mut().len();
let after_jump = self.code.len();
let false_offset = (after_jump as i32) - (after_jif as i32);
self.patch_i32(skip_placeholder, false_offset);
self.emit_op(Op::PushFalse);
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
let end_offset = (self.code.len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset);
}
Or => {
@@ -627,20 +649,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_op(Op::ForceBool);
self.emit_op(Op::JumpIfTrue);
let skip_placeholder = self.emit_i32_placeholder();
let after_jit = self.ctx.get_code_mut().len();
let after_jit = self.code.len();
self.emit_expr(rhs);
self.emit_op(Op::ForceBool);
self.emit_op(Op::Jump);
let end_placeholder = self.emit_i32_placeholder();
let after_jump = self.ctx.get_code_mut().len();
let after_jump = self.code.len();
let true_offset = (after_jump as i32) - (after_jit as i32);
self.patch_i32(skip_placeholder, true_offset);
self.emit_op(Op::PushTrue);
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
let end_offset = (self.code.len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset);
}
Impl => {
@@ -648,20 +670,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_op(Op::ForceBool);
self.emit_op(Op::JumpIfFalse);
let skip_placeholder = self.emit_i32_placeholder();
let after_jif = self.ctx.get_code_mut().len();
let after_jif = self.code.len();
self.emit_expr(rhs);
self.emit_op(Op::ForceBool);
self.emit_op(Op::Jump);
let end_placeholder = self.emit_i32_placeholder();
let after_jump = self.ctx.get_code_mut().len();
let after_jump = self.code.len();
let true_offset = (after_jump as i32) - (after_jif as i32);
self.patch_i32(skip_placeholder, true_offset);
self.emit_op(Op::PushTrue);
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
let end_offset = (self.code.len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset);
}
PipeL => {
@@ -710,7 +732,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
let thunk_ids: Vec<ThunkId> = all_thunks.iter().map(|&(id, _)| id).collect();
let skip_patch = self.emit_jump_placeholder();
let entry_point = self.ctx.get_code().len() as u32;
let entry_point = self.code.len() as u32;
self.push_scope(true, Some(arg), &thunk_ids);
self.emit_scope_thunks(thunks);
self.emit_expr(body);
@@ -732,14 +754,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_u8(if *ellipsis { 1 } else { 0 });
for &(sym, _) in required.iter() {
self.emit_str_id(sym);
let name = self.ctx.get_sym(sym).to_string();
let idx = self.ctx.intern_string(&name);
self.emit_u32(idx);
}
for &(sym, _) in optional.iter() {
self.emit_str_id(sym);
let name = self.ctx.get_sym(sym).to_string();
let idx = self.ctx.intern_string(&name);
self.emit_u32(idx);
}
for &(sym, span) in required.iter().chain(optional.iter()) {
let name = self.ctx.get_sym(sym).to_string();
let name_idx = self.ctx.intern_string(&name);
let span_id = self.ctx.register_span(span);
self.emit_str_id(sym);
self.emit_u32(name_idx);
self.emit_u32(span_id);
}
} else {
@@ -751,7 +779,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
fn emit_attrset(
&mut self,
stcs: &crate::ir::HashMap<'_, StringId, (RawIrRef<'_>, TextRange)>,
stcs: &crate::ir::HashMap<'_, SymId, (RawIrRef<'_>, TextRange)>,
dyns: &[(RawIrRef<'_>, RawIrRef<'_>, TextRange)],
) {
if stcs.is_empty() && dyns.is_empty() {
@@ -761,35 +789,42 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
if !dyns.is_empty() {
for (&sym, &(val, _)) in stcs.iter() {
let key = self.ctx.get_sym(sym).to_string();
let idx = self.ctx.intern_string(&key);
self.emit_op(Op::PushString);
self.emit_str_id(sym);
self.emit_u32(idx);
self.emit_expr(val);
}
for (_, &(_, span)) in stcs.iter() {
let span_id = self.ctx.register_span(span);
self.emit_op(Op::PushSmi);
self.emit_u32(span_id);
let idx = self.ctx.intern_constant(Constant::Int(span_id as i64));
self.emit_op(Op::PushConst);
self.emit_u32(idx);
}
for &(key, val, span) in dyns.iter() {
self.emit_expr(key);
self.emit_expr(val);
let span_id = self.ctx.register_span(span);
self.emit_op(Op::PushSmi);
self.emit_u32(span_id);
let idx = self.ctx.intern_constant(Constant::Int(span_id as i64));
self.emit_op(Op::PushConst);
self.emit_u32(idx);
}
self.emit_op(Op::MakeAttrsDyn);
self.emit_u32(stcs.len() as u32);
self.emit_u32(dyns.len() as u32);
} else {
for (&sym, &(val, _)) in stcs.iter() {
let key = self.ctx.get_sym(sym).to_string();
let idx = self.ctx.intern_string(&key);
self.emit_op(Op::PushString);
self.emit_str_id(sym);
self.emit_u32(idx);
self.emit_expr(val);
}
for (_, &(_, span)) in stcs.iter() {
let span_id = self.ctx.register_span(span);
self.emit_op(Op::PushSmi);
self.emit_u32(span_id);
let idx = self.ctx.intern_constant(Constant::Int(span_id as i64));
self.emit_op(Op::PushConst);
self.emit_u32(idx);
}
self.emit_op(Op::MakeAttrs);
self.emit_u32(stcs.len() as u32);
@@ -805,13 +840,15 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
) {
self.emit_expr(expr);
for attr in attrpath.iter() {
match *attr {
match attr {
Attr::Str(sym, _) => {
let key = self.ctx.get_sym(*sym).to_string();
let idx = self.ctx.intern_string(&key);
self.emit_op(Op::PushString);
self.emit_str_id(sym);
self.emit_u32(idx);
}
Attr::Dynamic(expr, _) => {
self.emit_expr(expr);
self.emit_expr(*expr);
}
}
}
@@ -833,13 +870,15 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
fn emit_has_attr(&mut self, lhs: RawIrRef<'_>, rhs: &[Attr<RawIrRef<'_>>]) {
self.emit_expr(lhs);
for attr in rhs.iter() {
match *attr {
match attr {
Attr::Str(sym, _) => {
let key = self.ctx.get_sym(*sym).to_string();
let idx = self.ctx.intern_string(&key);
self.emit_op(Op::PushString);
self.emit_str_id(sym);
self.emit_u32(idx);
}
Attr::Dynamic(expr, _) => {
self.emit_expr(expr);
self.emit_expr(*expr);
}
}
}
+618
View File
@@ -0,0 +1,618 @@
use std::fmt::{self, Write as _};
use std::ops::Deref;
use std::path::Path;
use rnix::TextRange;
use crate::ir::*;
use crate::value::Symbol;
macro_rules! code {
($buf:expr, $ctx:expr; $($item:expr)*) => {{
$(
($item).compile($ctx, $buf);
)*
}};
($buf:expr, $ctx:expr; $($item:expr)*) => {{
$(
($item).compile($ctx, $buf);
)*
}};
($buf:expr, $fmt:literal, $($arg:tt)*) => {
write!($buf, $fmt, $($arg)*).unwrap()
};
($buf:expr, $fmt:literal) => {
write!($buf, $fmt).unwrap()
};
}
pub(crate) fn compile<const SCOPED: bool>(expr: RawIrRef<'_>, ctx: &impl CodegenContext) -> String {
let mut buf = CodeBuffer::with_capacity(8192);
code!(
&mut buf, ctx;
"((" { if SCOPED { "_s" } else { "" } } ")=>{"
"const _d="
quoted(&ctx.get_current_dir().display().to_string())
",_w=null;"
"return " expr
"})" { if SCOPED { "" } else { "()" } }
);
buf.into_string()
}
struct CodeBuffer {
buf: String,
}
impl fmt::Write for CodeBuffer {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.buf.push_str(s);
Ok(())
}
}
impl CodeBuffer {
#[inline]
fn with_capacity(capacity: usize) -> Self {
Self {
buf: String::with_capacity(capacity),
}
}
#[inline]
fn push_str(&mut self, s: &str) {
self.buf.push_str(s);
}
#[inline]
fn into_string(self) -> String {
self.buf
}
}
struct Quoted<'a>(&'a str);
#[inline]
fn quoted(s: &str) -> Quoted<'_> {
Quoted(s)
}
struct Escaped<'a>(&'a str);
impl<Ctx: CodegenContext> Compile<Ctx> for Escaped<'_> {
fn compile(&self, _ctx: &Ctx, buf: &mut CodeBuffer) {
for c in self.0.chars() {
let _ = match c {
'\\' => buf.write_str("\\\\"),
'"' => buf.write_str("\\\""),
'\n' => buf.write_str("\\n"),
'\r' => buf.write_str("\\r"),
'\t' => buf.write_str("\\t"),
_ => buf.write_char(c),
};
}
}
}
#[inline]
fn escaped(s: &str) -> Escaped<'_> {
Escaped(s)
}
struct Joined<I, F> {
items: I,
sep: &'static str,
write_fn: F,
}
#[inline]
fn joined<Ctx: CodegenContext, I: Iterator, F: Fn(&Ctx, &mut CodeBuffer, I::Item)>(
items: I,
sep: &'static str,
write_fn: F,
) -> Joined<I, F> {
Joined {
items,
sep,
write_fn,
}
}
trait Compile<Ctx: CodegenContext> {
fn compile(&self, ctx: &Ctx, buf: &mut CodeBuffer);
}
impl<Ctx: CodegenContext> Compile<Ctx> for str {
fn compile(&self, _ctx: &Ctx, buf: &mut CodeBuffer) {
buf.push_str(self);
}
}
impl<Ctx: CodegenContext> Compile<Ctx> for usize {
fn compile(&self, _ctx: &Ctx, buf: &mut CodeBuffer) {
let _ = write!(buf, "{self}");
}
}
impl<Ctx: CodegenContext> Compile<Ctx> for bool {
fn compile(&self, _ctx: &Ctx, buf: &mut CodeBuffer) {
let _ = write!(buf, "{self}");
}
}
impl<Ctx: CodegenContext> Compile<Ctx> for Quoted<'_> {
fn compile(&self, ctx: &Ctx, buf: &mut CodeBuffer) {
code!(buf, ctx; "\"" escaped(self.0) "\"")
}
}
impl<Ctx: CodegenContext, I, F> Compile<Ctx> for Joined<I, F>
where
I: IntoIterator + Clone,
F: Fn(&Ctx, &mut CodeBuffer, I::Item) + Clone,
{
fn compile(&self, ctx: &Ctx, buf: &mut CodeBuffer) {
let mut iter = self.items.clone().into_iter();
if let Some(first) = iter.next() {
(self.write_fn)(ctx, buf, first);
for item in iter {
buf.push_str(self.sep);
(self.write_fn)(ctx, buf, item);
}
}
}
}
impl<Ctx: CodegenContext> Compile<Ctx> for rnix::TextRange {
fn compile(&self, ctx: &Ctx, buf: &mut CodeBuffer) {
code!(buf, "{}", ctx.register_span(*self));
}
}
pub(crate) trait CodegenContext {
fn get_sym(&self, id: SymId) -> Symbol<'_>;
fn get_current_dir(&self) -> &Path;
fn get_store_dir(&self) -> &str;
fn get_current_source_id(&self) -> usize;
fn register_span(&self, range: rnix::TextRange) -> usize;
}
impl<Ctx: CodegenContext> Compile<Ctx> for Symbol<'_> {
fn compile(&self, ctx: &Ctx, buf: &mut CodeBuffer) {
quoted(self).compile(ctx, buf);
}
}
impl<Ctx: CodegenContext> Compile<Ctx> for RawIrRef<'_> {
fn compile(&self, ctx: &Ctx, buf: &mut CodeBuffer) {
match self.deref() {
Ir::Int(int) => {
code!(buf, "{}n", int);
}
Ir::Float(float) => {
code!(buf, "{}", float);
}
Ir::Bool(bool) => {
code!(buf, "{}", bool);
}
Ir::Null => {
code!(buf, ctx; "null");
}
Ir::Str(s) => {
code!(buf, ctx; quoted(s));
}
Ir::Path(p) => {
// Nix.resolvePath
code!(buf, ctx; "$r(_d," p ")");
}
Ir::If { cond, consq, alter } => {
code!(buf, ctx; "$fb(" cond ")?(" consq "):(" alter ")");
}
&Ir::BinOp { lhs, rhs, kind } => compile_binop(lhs, rhs, kind, ctx, buf),
&Ir::UnOp { rhs, kind } => compile_unop(rhs, kind, ctx, buf),
&Ir::Func {
body,
ref param,
arg,
ref thunks,
} => compile_func(arg, thunks, param, body, ctx, buf),
Ir::AttrSet { stcs, dyns } => compile_attrset(stcs, dyns, ctx, buf),
Ir::List { items } => compile_list(items, ctx, buf),
Ir::Call { func, arg, span } => {
code!(buf, ctx;
"$c("
func
","
arg
","
span
")"
);
}
Ir::Arg(x) => {
code!(buf, "a{}", x.0);
}
&Ir::TopLevel { body, ref thunks } => compile_toplevel(body, thunks, ctx, buf),
&Ir::Select {
expr,
ref attrpath,
default,
span,
} => compile_select(expr, attrpath, default, span, ctx, buf),
Ir::Thunk(ThunkId(id)) => {
code!(buf, "e{}", id);
}
Ir::Builtins => {
// Nix.builtins
code!(buf, ctx; "$b");
}
&Ir::Builtin(name) => {
// Nix.builtins
code!(buf, ctx; "$b.get(" ctx.get_sym(name) ")");
}
&Ir::ConcatStrings {
ref parts,
force_string,
} => compile_concat_strings(parts, force_string, ctx, buf),
&Ir::HasAttr { lhs, ref rhs } => compile_has_attr(lhs, rhs, ctx, buf),
Ir::Assert {
assertion,
expr,
assertion_raw,
span: assert_span,
} => {
// Nix.assert
code!(buf, ctx;
"$a("
assertion
","
expr
","
quoted(assertion_raw)
","
assert_span
")"
);
}
Ir::CurPos(span) => {
// Nix.mkPos
code!(buf, ctx; "$mp(" span ")");
}
&Ir::ReplBinding(name) => {
// Nix.getReplBinding
code!(buf, ctx; "$gb(" ctx.get_sym(name) ")");
}
&Ir::ScopedImportBinding(name) => {
code!(buf, ctx; "_s.get(" ctx.get_sym(name) ")");
}
&Ir::With {
namespace,
body,
ref thunks,
} => compile_with(namespace, body, thunks, ctx, buf),
&Ir::WithLookup(name) => {
// Nix.lookupWith
code!(buf, ctx; "$l(" ctx.get_sym(name) ",_w)");
}
}
}
}
fn compile_binop<'ir>(
lhs: RawIrRef<'ir>,
rhs: RawIrRef<'ir>,
kind: BinOpKind,
ctx: &impl CodegenContext,
buf: &mut CodeBuffer,
) {
use BinOpKind::*;
match kind {
Add | Sub | Mul | Div | Eq | Neq | Lt | Gt | Leq | Geq | Con | Upd => {
let op_func = match kind {
Add => "$oa",
Sub => "$os",
Mul => "$om",
Div => "$od",
Eq => "$oe",
Neq => "!$oe",
Lt => "$ol",
Gt => "$og",
Leq => "!$og",
Geq => "!$ol",
Con => "$oc",
Upd => "$ou",
_ => unreachable!(),
};
code!(
buf, ctx;
op_func "(" lhs "," rhs ")"
);
}
And => {
code!(
buf, ctx;
"$fb(" lhs ")" "&&" "$fb(" rhs ")"
);
}
Or => {
code!(
buf, ctx;
"$fb(" lhs ")" "||" "$fb(" rhs ")"
);
}
Impl => {
code!(
buf, ctx;
"!$fb(" lhs ")" "||" "$fb(" rhs ")"
);
}
PipeL => {
code!(buf, ctx; "$c(" rhs "," lhs ")");
}
PipeR => {
code!(buf, ctx; "$c(" lhs "," rhs ")");
}
}
}
fn compile_unop(
rhs: RawIrRef<'_>,
kind: UnOpKind,
ctx: &impl CodegenContext,
buf: &mut CodeBuffer,
) {
use UnOpKind::*;
match kind {
Neg => {
// 0 - rhs
code!(buf, ctx; "$os(0n," rhs ")");
}
Not => {
code!(buf, ctx; "!$fb(" rhs ")");
}
}
}
fn compile_func<'ir, Ctx: CodegenContext>(
ArgId(id): ArgId,
thunks: &[(ThunkId, RawIrRef<'ir>)],
param: &Option<Param<'ir>>,
body: RawIrRef<'ir>,
ctx: &Ctx,
buf: &mut CodeBuffer,
) {
let has_thunks = !thunks.is_empty();
if let Some(Param {
required,
optional,
ellipsis,
}) = &param
{
code!(buf, "$mf(a{}=>", id);
if has_thunks {
code!(buf, ctx; "{" thunks "return " body "}");
} else {
code!(buf, ctx; "(" body ")");
}
code!(buf, ctx;
",["
joined(required.iter(), ",", |ctx: &Ctx, buf, &(sym, _)| {
code!(buf, ctx; ctx.get_sym(sym));
})
"],["
joined(optional.iter(), ",", |ctx: &Ctx, buf, &(sym, _)| {
code!(buf, ctx; ctx.get_sym(sym));
})
"],new Map(["
joined(required.iter().chain(optional.iter()), ",", |ctx: &Ctx, buf, &(sym, span)| {
code!(buf, ctx; "[" ctx.get_sym(sym) "," span "]");
})
"]),"
ellipsis
")"
);
} else {
code!(buf, "a{}=>", id);
if has_thunks {
code!(buf, ctx; "{" thunks "return " body "}");
} else {
code!(buf, ctx; "(" body ")");
}
}
}
impl<'ir, Ctx: CodegenContext> Compile<Ctx> for [(ThunkId, RawIrRef<'ir>)] {
fn compile(&self, ctx: &Ctx, buf: &mut CodeBuffer) {
if self.is_empty() {
return;
}
code!(
buf, ctx;
"const "
joined(self.iter(), ",", |ctx: &Ctx, buf, &(slot, inner)| {
code!(buf, ctx; "e" slot.0 "=$t(()=>(" inner ")," "'e" slot.0 "')");
})
";"
);
}
}
fn compile_toplevel<'ir, Ctx: CodegenContext>(
body: RawIrRef<'ir>,
thunks: &[(ThunkId, RawIrRef<'ir>)],
ctx: &Ctx,
buf: &mut CodeBuffer,
) {
if thunks.is_empty() {
body.compile(ctx, buf);
} else {
code!(buf, ctx; "(()=>{" thunks "return " body "})()");
}
}
fn compile_with<'ir>(
namespace: RawIrRef<'ir>,
body: RawIrRef<'ir>,
thunks: &[(ThunkId, RawIrRef<'ir>)],
ctx: &impl CodegenContext,
buf: &mut CodeBuffer,
) {
let has_thunks = !thunks.is_empty();
if has_thunks {
code!(buf, ctx; "((_w)=>{" thunks "return " body "})({env:" namespace ",last:_w})");
} else {
code!(buf, ctx; "((_w)=>(" body "))({env:" namespace ",last:_w})");
}
}
fn compile_select<'ir, Ctx: CodegenContext>(
expr: RawIrRef<'ir>,
attrpath: &[Attr<RawIrRef<'ir>>],
default: Option<RawIrRef<'ir>>,
span: TextRange,
ctx: &Ctx,
buf: &mut CodeBuffer,
) {
if let Some(default) = default {
code!(buf, ctx;
"$sd("
expr
",["
joined(attrpath.iter(), ",", |ctx: &Ctx, buf, attr| {
match attr {
Attr::Str(sym, _) => code!(buf, ctx; ctx.get_sym(*sym)),
Attr::Dynamic(expr_id, _) => code!(buf, ctx; *expr_id),
}
})
"],"
default
","
span
")"
);
} else {
code!(buf, ctx;
"$s("
expr
",["
joined(attrpath.iter(), ",", |ctx: &Ctx, buf, attr| {
match attr {
Attr::Str(sym, _) => code!(buf, ctx; ctx.get_sym(*sym)),
Attr::Dynamic(expr, _) => code!(buf, ctx; expr),
}
})
"],"
span
")"
);
}
}
fn compile_attrset<'ir, Ctx: CodegenContext>(
stcs: &HashMap<'ir, SymId, (RawIrRef<'ir>, TextRange)>,
dyns: &[(RawIrRef<'ir>, RawIrRef<'ir>, TextRange)],
ctx: &Ctx,
buf: &mut CodeBuffer,
) {
if !dyns.is_empty() {
code!(buf, ctx;
"$ma(new Map(["
joined(stcs.iter(), ",", |ctx: &Ctx, buf, (&sym, &(val, _))| {
let key = ctx.get_sym(sym);
code!(
buf, ctx;
"[" key "," val "]"
);
})
"]),new Map(["
joined(stcs.iter(), ",", |ctx: &Ctx, buf, (&sym, &(_, span))| {
code!(buf, ctx; "[" ctx.get_sym(sym) "," span "]");
})
"]),{dynKeys:["
joined(dyns.iter(), ",", |ctx: &Ctx, buf, (key, _, _)| {
code!(buf, ctx; key);
})
"],dynVals:["
joined(dyns.iter(), ",", |ctx: &Ctx, buf, (_, val, _)| {
code!(buf, ctx; val);
})
"],dynSpans:["
joined(dyns.iter(), ",", |ctx: &Ctx, buf, (_, _, attr_span)| {
code!(buf, ctx; attr_span);
})
"]})"
);
} else if !stcs.is_empty() {
code!(buf, ctx;
"$ma(new Map(["
joined(stcs.iter(), ",", |ctx: &Ctx, buf, (&sym, &(val, _))| {
let key = ctx.get_sym(sym);
code!(
buf, ctx;
"[" key "," val "]"
);
})
"]),new Map(["
joined(stcs.iter(), ",", |ctx: &Ctx, buf, (&sym, &(_, span))| {
code!(buf, ctx; "[" ctx.get_sym(sym) "," span "]");
})
"]))"
);
} else {
code!(buf, ctx; "$e");
}
}
fn compile_list<Ctx: CodegenContext>(items: &[RawIrRef<'_>], ctx: &Ctx, buf: &mut CodeBuffer) {
code!(buf, ctx;
"["
joined(items.iter(), ",", |ctx: &Ctx, buf, item| {
code!(buf, ctx; item);
})
"]"
);
}
fn compile_concat_strings<Ctx: CodegenContext>(
parts: &[RawIrRef<'_>],
force_string: bool,
ctx: &Ctx,
buf: &mut CodeBuffer,
) {
code!(buf, ctx;
"$cs(["
joined(parts.iter(), ",", |ctx: &Ctx, buf, part| {
code!(buf, ctx; part);
})
"]," force_string ")"
);
}
fn compile_has_attr<'ir, Ctx: CodegenContext>(
lhs: RawIrRef<'ir>,
rhs: &[Attr<RawIrRef<'ir>>],
ctx: &Ctx,
buf: &mut CodeBuffer,
) {
code!(buf, ctx;
"$h("
lhs
",["
joined(rhs.iter(), ",", |ctx: &Ctx, buf, attr| {
match attr {
Attr::Str(sym, _) => code!(buf, ctx; ctx.get_sym(*sym)),
Attr::Dynamic(expr, _) => code!(buf, ctx; expr),
}
})
"])"
);
}
+801
View File
@@ -0,0 +1,801 @@
use std::cell::UnsafeCell;
use std::hash::BuildHasher;
use std::path::Path;
use bumpalo::Bump;
use ghost_cell::{GhostCell, GhostToken};
use hashbrown::{DefaultHashBuilder, HashMap, HashSet, HashTable};
use rnix::TextRange;
use string_interner::DefaultStringInterner;
use crate::bytecode::{self, Bytecode, BytecodeContext, Constant};
use crate::codegen::{CodegenContext, compile};
use crate::disassembler::{Disassembler, DisassemblerContext};
use crate::downgrade::*;
use crate::error::{Error, Result, Source};
use crate::ir::{ArgId, Ir, IrKey, IrRef, RawIrRef, SymId, ThunkId, ir_content_eq};
#[cfg(feature = "inspector")]
use crate::runtime::inspector::InspectorServer;
use crate::runtime::{ForceMode, Runtime, RuntimeContext};
use crate::store::{DaemonStore, Store, StoreConfig};
use crate::value::{Symbol, Value};
fn parse_error_span(error: &rnix::ParseError) -> Option<rnix::TextRange> {
use rnix::ParseError::*;
match error {
Unexpected(range)
| UnexpectedExtra(range)
| UnexpectedWanted(_, range, _)
| UnexpectedDoubleBind(range)
| DuplicatedArgs(range, _) => Some(*range),
_ => None,
}
}
fn handle_parse_error<'a>(
errors: impl IntoIterator<Item = &'a rnix::ParseError>,
source: Source,
) -> Option<Box<Error>> {
for err in errors {
if let Some(span) = parse_error_span(err) {
return Some(
Error::parse_error(err.to_string())
.with_source(source)
.with_span(span),
);
}
}
None
}
pub struct Context {
ctx: Ctx,
runtime: Runtime<Ctx>,
#[cfg(feature = "inspector")]
_inspector_server: Option<InspectorServer>,
}
macro_rules! eval_bc {
($name:ident, $mode:expr) => {
pub fn $name(&mut self, source: Source) -> Result<Value> {
tracing::info!("Starting evaluation");
tracing::debug!("Compiling bytecode");
let bytecode = self.ctx.compile_bytecode(source)?;
tracing::debug!("Executing bytecode");
self.runtime.eval_bytecode(bytecode, &mut self.ctx, $mode)
}
};
}
impl Context {
pub fn new() -> Result<Self> {
let ctx = Ctx::new()?;
#[cfg(feature = "inspector")]
let runtime = Runtime::new(Default::default())?;
#[cfg(not(feature = "inspector"))]
let runtime = Runtime::new()?;
let mut context = Self {
ctx,
runtime,
#[cfg(feature = "inspector")]
_inspector_server: None,
};
context.init()?;
Ok(context)
}
#[cfg(feature = "inspector")]
pub fn new_with_inspector(addr: std::net::SocketAddr, wait_for_session: bool) -> Result<Self> {
use crate::runtime::InspectorOptions;
let ctx = Ctx::new()?;
let runtime = Runtime::new(InspectorOptions {
enable: true,
wait: wait_for_session,
})?;
let server = crate::runtime::inspector::InspectorServer::new(addr, "nix-js")
.map_err(|e| Error::internal(e.to_string()))?;
server.register_inspector("nix-js".to_string(), runtime.inspector(), wait_for_session);
let mut context = Self {
ctx,
runtime,
_inspector_server: Some(server),
};
context.init()?;
Ok(context)
}
#[cfg(feature = "inspector")]
pub fn wait_for_inspector_disconnect(&mut self) {
self.runtime.wait_for_inspector_disconnect();
}
fn init(&mut self) -> Result<()> {
const DERIVATION_NIX: &str = include_str!("runtime/corepkgs/derivation.nix");
let source = Source::new_virtual(
"<nix/derivation-internal.nix>".into(),
DERIVATION_NIX.to_string(),
);
let code = self.ctx.compile(source, None)?;
self.runtime.eval(
format!(
"Nix.builtins.set('derivation',({}));Nix.builtins.set('storeDir','{}');{}0n",
code,
self.get_store_dir(),
if std::env::var("NIX_JS_DEBUG_THUNKS").is_ok() {
"Nix.DEBUG_THUNKS.enabled=true;"
} else {
""
}
),
&mut self.ctx,
)?;
Ok(())
}
eval_bc!(eval, ForceMode::Force);
eval_bc!(eval_shallow, ForceMode::ForceShallow);
eval_bc!(eval_deep, ForceMode::ForceDeep);
pub fn eval_repl<'a>(&'a mut self, source: Source, scope: &'a HashSet<SymId>) -> Result<Value> {
tracing::info!("Starting evaluation");
tracing::debug!("Compiling code");
let code = self.ctx.compile(source, Some(Scope::Repl(scope)))?;
tracing::debug!("Executing JavaScript");
self.runtime
.eval(format!("Nix.forceShallow({})", code), &mut self.ctx)
}
pub fn compile(&mut self, source: Source) -> Result<String> {
self.ctx.compile(source, None)
}
pub fn compile_bytecode(&mut self, source: Source) -> Result<Bytecode> {
self.ctx.compile_bytecode(source)
}
pub fn disassemble(&self, bytecode: &Bytecode) -> String {
Disassembler::new(bytecode, &self.ctx).disassemble()
}
pub fn disassemble_colored(&self, bytecode: &Bytecode) -> String {
Disassembler::new(bytecode, &self.ctx).disassemble_colored()
}
pub fn get_store_dir(&self) -> &str {
self.ctx.get_store_dir()
}
pub fn add_binding<'a>(
&'a mut self,
name: &str,
expr: &str,
scope: &'a mut HashSet<SymId>,
) -> Result<Value> {
let source = Source::new_repl(expr.to_string())?;
let code = self.ctx.compile(source, Some(Scope::Repl(scope)))?;
let sym = self.ctx.symbols.get_or_intern(name);
let eval_and_store = format!(
"(()=>{{const __v=Nix.forceShallow({});Nix.setReplBinding(\"{}\",__v);return __v}})()",
code, name
);
scope.insert(sym);
self.runtime.eval(eval_and_store, &mut self.ctx)
}
}
struct Ctx {
symbols: DefaultStringInterner,
global: HashMap<SymId, Ir<'static, RawIrRef<'static>>>,
sources: Vec<Source>,
store: DaemonStore,
spans: UnsafeCell<Vec<(usize, TextRange)>>,
thunk_count: usize,
global_strings: Vec<String>,
global_string_map: HashMap<String, u32>,
global_constants: Vec<Constant>,
global_constant_map: HashMap<Constant, u32>,
synced_strings: usize,
synced_constants: usize,
}
/// Owns the bump allocator and a read-only reference into it.
///
/// # Safety
/// The `ir` field points into `_bump`'s storage. We use `'static` as a sentinel
/// lifetime because the struct owns the backing memory. The `as_ref` method
/// re-binds the lifetime to `&self`, preventing use-after-free.
struct OwnedIr {
_bump: Bump,
ir: RawIrRef<'static>,
}
impl OwnedIr {
fn as_ref(&self) -> RawIrRef<'_> {
self.ir
}
}
impl Ctx {
fn new() -> Result<Self> {
let mut symbols = DefaultStringInterner::new();
let mut global = HashMap::new();
let builtins_sym = symbols.get_or_intern("builtins");
global.insert(builtins_sym, Ir::Builtins);
let free_globals = [
"abort",
"baseNameOf",
"break",
"dirOf",
"derivation",
"derivationStrict",
"fetchGit",
"fetchMercurial",
"fetchTarball",
"fetchTree",
"fromTOML",
"import",
"isNull",
"map",
"placeholder",
"removeAttrs",
"scopedImport",
"throw",
"toString",
];
let consts = [
("true", Ir::Bool(true)),
("false", Ir::Bool(false)),
("null", Ir::Null),
];
for name in free_globals {
let name = symbols.get_or_intern(name);
let value = Ir::Builtin(name);
global.insert(name, value);
}
for (name, value) in consts {
let name = symbols.get_or_intern(name);
global.insert(name, value);
}
let config = StoreConfig::from_env();
let store = DaemonStore::connect(&config.daemon_socket)?;
Ok(Self {
symbols,
global,
sources: Vec::new(),
store,
spans: UnsafeCell::new(Vec::new()),
thunk_count: 0,
global_strings: Vec::new(),
global_string_map: HashMap::new(),
global_constants: Vec::new(),
global_constant_map: HashMap::new(),
synced_strings: 0,
synced_constants: 0,
})
}
fn downgrade_ctx<'ctx, 'id, 'ir>(
&'ctx mut self,
bump: &'ir Bump,
token: GhostToken<'id>,
extra_scope: Option<Scope<'ctx>>,
) -> DowngradeCtx<'ctx, 'id, 'ir> {
let source = self.get_current_source();
DowngradeCtx::new(
bump,
token,
&mut self.symbols,
&self.global,
extra_scope,
&mut self.thunk_count,
source,
)
}
fn get_current_dir(&self) -> &Path {
self.sources
.last()
.as_ref()
.expect("current_source is not set")
.get_dir()
}
fn get_current_source(&self) -> Source {
self.sources
.last()
.expect("current_source is not set")
.clone()
}
fn downgrade<'ctx>(
&'ctx mut self,
source: Source,
extra_scope: Option<Scope<'ctx>>,
) -> Result<OwnedIr> {
tracing::debug!("Parsing Nix expression");
self.sources.push(source.clone());
let root = rnix::Root::parse(&source.src);
handle_parse_error(root.errors(), source).map_or(Ok(()), Err)?;
tracing::debug!("Downgrading Nix expression");
let expr = root
.tree()
.expr()
.ok_or_else(|| Error::parse_error("unexpected EOF".into()))?;
let bump = Bump::new();
GhostToken::new(|token| {
let ir = self
.downgrade_ctx(&bump, token, extra_scope)
.downgrade_toplevel(expr)?;
let ir = unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) };
Ok(OwnedIr { _bump: bump, ir })
})
}
fn compile<'ctx>(
&'ctx mut self,
source: Source,
extra_scope: Option<Scope<'ctx>>,
) -> Result<String> {
let root = self.downgrade(source, extra_scope)?;
tracing::debug!("Generating JavaScript code");
let code = compile::<false>(root.as_ref(), self);
tracing::debug!("Generated code: {}", &code);
Ok(code)
}
fn compile_scoped(&mut self, source: Source, scope: Vec<String>) -> Result<String> {
let scope = Scope::ScopedImport(
scope
.into_iter()
.map(|k| self.symbols.get_or_intern(k))
.collect(),
);
let root = self.downgrade(source, Some(scope))?;
tracing::debug!("Generating JavaScript code for scoped import");
let code = compile::<true>(root.as_ref(), self);
tracing::debug!("Generated scoped code: {}", &code);
Ok(code)
}
fn compile_bytecode(&mut self, source: Source) -> Result<Bytecode> {
let root = self.downgrade(source, None)?;
tracing::debug!("Generating bytecode");
let bytecode = bytecode::compile_bytecode(root.as_ref(), self);
tracing::debug!("Compiled bytecode: {:#04X?}", bytecode.code);
Ok(bytecode)
}
fn compile_bytecode_scoped(&mut self, source: Source, scope: Vec<String>) -> Result<Bytecode> {
let scope = Scope::ScopedImport(
scope
.into_iter()
.map(|k| self.symbols.get_or_intern(k))
.collect(),
);
let root = self.downgrade(source, Some(scope))?;
tracing::debug!("Generating bytecode for scoped import");
Ok(bytecode::compile_bytecode_scoped(root.as_ref(), self))
}
}
impl CodegenContext for Ctx {
fn get_sym(&self, id: SymId) -> Symbol<'_> {
self.symbols
.resolve(id)
.expect("SymId out of bounds")
.into()
}
fn get_current_dir(&self) -> &std::path::Path {
self.get_current_dir()
}
fn get_current_source_id(&self) -> usize {
self.sources
.len()
.checked_sub(1)
.expect("current_source not set")
}
fn get_store_dir(&self) -> &str {
self.store.get_store_dir()
}
fn register_span(&self, range: rnix::TextRange) -> usize {
let spans = unsafe { &mut *self.spans.get() };
let id = spans.len();
spans.push((self.get_current_source_id(), range));
id
}
}
impl BytecodeContext for Ctx {
fn intern_string(&mut self, s: &str) -> u32 {
if let Some(&idx) = self.global_string_map.get(s) {
return idx;
}
let idx = self.global_strings.len() as u32;
self.global_strings.push(s.to_string());
self.global_string_map.insert(s.to_string(), idx);
idx
}
fn intern_constant(&mut self, c: Constant) -> u32 {
if let Some(&idx) = self.global_constant_map.get(&c) {
return idx;
}
let idx = self.global_constants.len() as u32;
self.global_constants.push(c.clone());
self.global_constant_map.insert(c, idx);
idx
}
fn register_span(&self, range: TextRange) -> u32 {
CodegenContext::register_span(self, range) as u32
}
fn get_sym(&self, id: SymId) -> &str {
self.symbols.resolve(id).expect("SymId out of bounds")
}
fn get_current_dir(&self) -> &Path {
Ctx::get_current_dir(self)
}
}
impl RuntimeContext for Ctx {
fn get_current_dir(&self) -> &Path {
self.get_current_dir()
}
fn add_source(&mut self, source: Source) {
self.sources.push(source);
}
fn compile(&mut self, source: Source) -> Result<String> {
self.compile(source, None)
}
fn compile_scoped(&mut self, source: Source, scope: Vec<String>) -> Result<String> {
self.compile_scoped(source, scope)
}
fn compile_bytecode(&mut self, source: Source) -> Result<Bytecode> {
self.compile_bytecode(source)
}
fn compile_bytecode_scoped(&mut self, source: Source, scope: Vec<String>) -> Result<Bytecode> {
self.compile_bytecode_scoped(source, scope)
}
fn get_source(&self, id: usize) -> Source {
self.sources.get(id).expect("source not found").clone()
}
fn get_store(&self) -> &DaemonStore {
&self.store
}
fn get_span(&self, id: usize) -> (usize, TextRange) {
let spans = unsafe { &*self.spans.get() };
spans[id]
}
fn get_unsynced(&mut self) -> (&[String], &[Constant], usize, usize) {
let strings_base = self.synced_strings;
let constants_base = self.synced_constants;
let new_strings = &self.global_strings[strings_base..];
let new_constants = &self.global_constants[constants_base..];
self.synced_strings = self.global_strings.len();
self.synced_constants = self.global_constants.len();
(new_strings, new_constants, strings_base, constants_base)
}
}
impl DisassemblerContext for Ctx {
fn lookup_string(&self, id: u32) -> &str {
self.global_strings
.get(id as usize)
.expect("string not found")
}
fn lookup_constant(&self, id: u32) -> &Constant {
self.global_constants
.get(id as usize)
.expect("constant not found")
}
}
enum Scope<'ctx> {
Global(&'ctx HashMap<SymId, Ir<'static, RawIrRef<'static>>>),
Repl(&'ctx HashSet<SymId>),
ScopedImport(HashSet<SymId>),
Let(HashMap<SymId, ThunkId>),
Param(SymId, ArgId),
}
struct ScopeGuard<'a, 'ctx, 'id, 'ir> {
ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir>,
}
impl Drop for ScopeGuard<'_, '_, '_, '_> {
fn drop(&mut self) {
self.ctx.scopes.pop();
}
}
impl<'id, 'ir, 'ctx> ScopeGuard<'_, 'ctx, 'id, 'ir> {
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir> {
self.ctx
}
}
struct ThunkScope<'id, 'ir> {
bindings: bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>,
cache: HashTable<(IrRef<'id, 'ir>, ThunkId)>,
hasher: DefaultHashBuilder,
}
impl<'id, 'ir> ThunkScope<'id, 'ir> {
fn new_in(bump: &'ir Bump) -> Self {
Self {
bindings: bumpalo::collections::Vec::new_in(bump),
cache: HashTable::new(),
hasher: DefaultHashBuilder::default(),
}
}
fn lookup_cache(&self, key: IrRef<'id, 'ir>, token: &GhostToken<'id>) -> Option<ThunkId> {
let hash = self.hasher.hash_one(IrKey(key, token));
self.cache
.find(hash, |&(ir, _)| ir_content_eq(key, ir, token))
.map(|&(_, id)| id)
}
fn add_binding(&mut self, id: ThunkId, ir: IrRef<'id, 'ir>, token: &GhostToken<'id>) {
self.bindings.push((id, ir));
let hash = self.hasher.hash_one(IrKey(ir, token));
self.cache.insert_unique(hash, (ir, id), |&(ir, _)| {
self.hasher.hash_one(IrKey(ir, token))
});
}
fn extend_bindings(&mut self, iter: impl IntoIterator<Item = (ThunkId, IrRef<'id, 'ir>)>) {
self.bindings.extend(iter);
}
}
struct DowngradeCtx<'ctx, 'id, 'ir> {
bump: &'ir Bump,
token: GhostToken<'id>,
symbols: &'ctx mut DefaultStringInterner,
source: Source,
scopes: Vec<Scope<'ctx>>,
with_scope_count: usize,
arg_count: usize,
thunk_count: &'ctx mut usize,
thunk_scopes: Vec<ThunkScope<'id, 'ir>>,
}
fn should_thunk<'id>(ir: IrRef<'id, '_>, token: &GhostToken<'id>) -> bool {
!matches!(
ir.borrow(token),
Ir::Builtin(_)
| Ir::Builtins
| Ir::Int(_)
| Ir::Float(_)
| Ir::Bool(_)
| Ir::Null
| Ir::Str(_)
| Ir::Thunk(_)
)
}
impl<'ctx, 'id, 'ir> DowngradeCtx<'ctx, 'id, 'ir> {
fn new(
bump: &'ir Bump,
token: GhostToken<'id>,
symbols: &'ctx mut DefaultStringInterner,
global: &'ctx HashMap<SymId, Ir<'static, RawIrRef<'static>>>,
extra_scope: Option<Scope<'ctx>>,
thunk_count: &'ctx mut usize,
source: Source,
) -> Self {
Self {
bump,
token,
symbols,
source,
scopes: std::iter::once(Scope::Global(global))
.chain(extra_scope)
.collect(),
thunk_count,
arg_count: 0,
with_scope_count: 0,
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
}
impl<'ctx: 'ir, 'id, 'ir> DowngradeContext<'id, 'ir> for DowngradeCtx<'ctx, 'id, 'ir> {
fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir> {
IrRef::new(self.bump.alloc(GhostCell::new(expr)))
}
fn new_arg(&mut self) -> ArgId {
self.arg_count += 1;
ArgId(self.arg_count - 1)
}
fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir> {
if !should_thunk(ir, &self.token) {
return ir;
}
let cached = self
.thunk_scopes
.last()
.expect("no active cache scope")
.lookup_cache(ir, &self.token);
if let Some(id) = cached {
return IrRef::alloc(self.bump, Ir::Thunk(id));
}
let id = ThunkId(*self.thunk_count);
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
self.thunk_scopes
.last_mut()
.expect("no active cache scope")
.add_binding(id, ir, &self.token);
IrRef::alloc(self.bump, Ir::Thunk(id))
}
fn new_sym(&mut self, sym: String) -> SymId {
self.symbols.get_or_intern(sym)
}
fn get_sym(&self, id: SymId) -> Symbol<'_> {
self.symbols.resolve(id).expect("no symbol found").into()
}
fn lookup(&self, sym: SymId, span: TextRange) -> Result<IrRef<'id, 'ir>> {
for scope in self.scopes.iter().rev() {
match scope {
&Scope::Global(global_scope) => {
if let Some(expr) = global_scope.get(&sym) {
let ir = match expr {
Ir::Builtins => Ir::Builtins,
Ir::Builtin(s) => Ir::Builtin(*s),
Ir::Bool(b) => Ir::Bool(*b),
Ir::Null => Ir::Null,
_ => unreachable!("globals should only contain leaf IR nodes"),
};
return Ok(self.new_expr(ir));
}
}
&Scope::Repl(repl_bindings) => {
if repl_bindings.contains(&sym) {
return Ok(self.new_expr(Ir::ReplBinding(sym)));
}
}
Scope::ScopedImport(scoped_bindings) => {
if scoped_bindings.contains(&sym) {
return Ok(self.new_expr(Ir::ScopedImportBinding(sym)));
}
}
Scope::Let(let_scope) => {
if let Some(&expr) = let_scope.get(&sym) {
return Ok(self.new_expr(Ir::Thunk(expr)));
}
}
&Scope::Param(param_sym, id) => {
if param_sym == sym {
return Ok(self.new_expr(Ir::Arg(id)));
}
}
}
}
if self.with_scope_count > 0 {
Ok(self.new_expr(Ir::WithLookup(sym)))
} else {
Err(Error::downgrade_error(
format!("'{}' not found", self.get_sym(sym)),
self.get_current_source(),
span,
))
}
}
fn get_current_source(&self) -> Source {
self.source.clone()
}
fn with_let_scope<F, R>(&mut self, keys: &[SymId], f: F) -> Result<R>
where
F: FnOnce(&mut Self) -> Result<(bumpalo::collections::Vec<'ir, IrRef<'id, 'ir>>, R)>,
{
let base = *self.thunk_count;
*self.thunk_count = self
.thunk_count
.checked_add(keys.len())
.expect("thunk id overflow");
let iter = keys.iter().enumerate().map(|(offset, &key)| {
(
key,
ThunkId(unsafe { base.checked_add(offset).unwrap_unchecked() }),
)
});
self.scopes.push(Scope::Let(iter.collect()));
let (vals, ret) = {
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())?
};
assert_eq!(keys.len(), vals.len());
let scope = self.thunk_scopes.last_mut().expect("no active thunk scope");
scope.extend_bindings((base..base + keys.len()).map(ThunkId).zip(vals));
Ok(ret)
}
fn with_param_scope<F, R>(&mut self, param: SymId, arg: ArgId, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.scopes.push(Scope::Param(param, arg));
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())
}
fn with_with_scope<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.with_scope_count += 1;
let ret = f(self);
self.with_scope_count -= 1;
ret
}
fn with_thunk_scope<F, R>(
&mut self,
f: F,
) -> (
R,
bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>,
)
where
F: FnOnce(&mut Self) -> R,
{
self.thunk_scopes.push(ThunkScope::new_in(self.bump));
let ret = f(self);
(
ret,
self.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings,
)
}
fn bump(&self) -> &'ir bumpalo::Bump {
self.bump
}
}
impl<'id, 'ir, 'ctx: 'ir> DowngradeCtx<'ctx, 'id, 'ir> {
fn downgrade_toplevel(mut self, root: rnix::ast::Expr) -> Result<RawIrRef<'ir>> {
let body = root.downgrade(&mut self)?;
let thunks = self
.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings;
let ir = IrRef::alloc(self.bump, Ir::TopLevel { body, thunks });
Ok(ir.freeze(self.token))
}
}
File renamed without changes.
@@ -3,10 +3,11 @@ use std::fmt::Write;
use colored::Colorize;
use num_enum::TryFromPrimitive;
use crate::codegen::{Bytecode, Op};
use crate::bytecode::{Bytecode, Constant, Op};
pub(crate) trait DisassemblerContext {
fn lookup_string(&self, id: u32) -> &str;
fn lookup_constant(&self, id: u32) -> &Constant;
}
pub(crate) struct Disassembler<'a, Ctx> {
@@ -54,22 +55,6 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
i32::from_le_bytes(bytes)
}
fn read_i64(&mut self) -> i64 {
let bytes = self.code[self.pos..self.pos + 8]
.try_into()
.expect("no enough bytes");
self.pos += 8;
i64::from_le_bytes(bytes)
}
fn read_f64(&mut self) -> f64 {
let bytes = self.code[self.pos..self.pos + 8]
.try_into()
.expect("no enough bytes");
self.pos += 8;
f64::from_le_bytes(bytes)
}
pub fn disassemble(&mut self) -> String {
self.disassemble_impl(false)
}
@@ -159,17 +144,14 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
let op = Op::try_from_primitive(op_byte).expect("invalid op code");
match op {
Op::PushSmi => {
let val = self.read_i32();
("PushSmi", format!("{}", val))
}
Op::PushBigInt => {
let val = self.read_i64();
("PushBigInt", format!("{}", val))
}
Op::PushFloat => {
let val = self.read_f64();
("PushFloat", format!("{}", val))
Op::PushConst => {
let idx = self.read_u32();
let val = self.ctx.lookup_constant(idx);
let val_str = match val {
Constant::Int(i) => format!("Int({})", i),
Constant::Float(f) => format!("Float(bits: {})", f),
};
("PushConst", format!("@{} ({})", idx, val_str))
}
Op::PushString => {
let idx = self.read_u32();
@@ -50,16 +50,16 @@ pub trait DowngradeContext<'id: 'ir, 'ir> {
fn new_arg(&mut self) -> ArgId;
fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir>;
fn new_sym(&mut self, sym: String) -> StringId;
fn get_sym(&self, id: StringId) -> Symbol<'_>;
fn lookup(&self, sym: StringId, span: TextRange) -> Result<IrRef<'id, 'ir>>;
fn new_sym(&mut self, sym: String) -> SymId;
fn get_sym(&self, id: SymId) -> Symbol<'_>;
fn lookup(&self, sym: SymId, span: TextRange) -> Result<IrRef<'id, 'ir>>;
fn get_current_source(&self) -> Source;
fn with_param_scope<F, R>(&mut self, param: StringId, arg: ArgId, f: F) -> R
fn with_param_scope<F, R>(&mut self, param: SymId, arg: ArgId, f: F) -> R
where
F: FnOnce(&mut Self) -> R;
fn with_let_scope<F, R>(&mut self, bindings: &[StringId], f: F) -> Result<R>
fn with_let_scope<F, R>(&mut self, bindings: &[SymId], f: F) -> Result<R>
where
F: FnOnce(&mut Self) -> Result<(Vec<'ir, IrRef<'id, 'ir>>, R)>;
fn with_with_scope<F, R>(&mut self, f: F) -> R
@@ -456,8 +456,8 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
enum PendingValue<'ir> {
Expr(ast::Expr),
InheritFrom(ast::Expr, StringId, TextRange),
InheritScope(StringId, TextRange),
InheritFrom(ast::Expr, SymId, TextRange),
InheritScope(SymId, TextRange),
Set(PendingAttrSet<'ir>),
ExtendedRecAttrSet {
base: ast::AttrSet,
@@ -466,7 +466,7 @@ enum PendingValue<'ir> {
}
struct PendingAttrSet<'ir> {
stcs: HashMap<'ir, StringId, (PendingValue<'ir>, TextRange)>,
stcs: HashMap<'ir, SymId, (PendingValue<'ir>, TextRange)>,
dyns: Vec<'ir, (ast::Attr, PendingValue<'ir>, TextRange)>,
}
@@ -510,7 +510,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
fn insert_static(
&mut self,
sym: StringId,
sym: SymId,
span: TextRange,
path: &[ast::Attr],
value: ast::Expr,
@@ -846,7 +846,7 @@ fn make_attrpath_value_entry<'ir>(path: Vec<'ir, ast::Attr>, value: ast::Expr) -
}
struct FinalizedAttrSet<'id, 'ir> {
stcs: HashMap<'ir, StringId, (IrRef<'id, 'ir>, TextRange)>,
stcs: HashMap<'ir, SymId, (IrRef<'id, 'ir>, TextRange)>,
dyns: Vec<'ir, (IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)>,
}
@@ -930,23 +930,23 @@ fn downgrade_attrpath<'id, 'ir>(
struct PatternBindings<'id, 'ir> {
body: IrRef<'id, 'ir>,
required: Vec<'ir, (StringId, TextRange)>,
optional: Vec<'ir, (StringId, TextRange)>,
required: Vec<'ir, (SymId, TextRange)>,
optional: Vec<'ir, (SymId, TextRange)>,
}
fn downgrade_pattern_bindings<'id, 'ir, Ctx>(
pat_entries: impl Iterator<Item = ast::PatEntry>,
alias: Option<StringId>,
alias: Option<SymId>,
arg: ArgId,
ctx: &mut Ctx,
body_fn: impl FnOnce(&mut Ctx, &[StringId]) -> Result<IrRef<'id, 'ir>>,
body_fn: impl FnOnce(&mut Ctx, &[SymId]) -> Result<IrRef<'id, 'ir>>,
) -> Result<PatternBindings<'id, 'ir>>
where
Ctx: DowngradeContext<'id, 'ir>,
{
let arg = ctx.new_expr(Ir::Arg(arg));
struct Param {
sym: StringId,
sym: SymId,
sym_span: TextRange,
default: Option<ast::Expr>,
span: TextRange,
@@ -1045,7 +1045,7 @@ fn downgrade_let_bindings<'id, 'ir, Ctx, F>(
) -> Result<IrRef<'id, 'ir>>
where
Ctx: DowngradeContext<'id, 'ir>,
F: FnOnce(&mut Ctx, &[StringId]) -> Result<IrRef<'id, 'ir>>,
F: FnOnce(&mut Ctx, &[SymId]) -> Result<IrRef<'id, 'ir>>,
{
downgrade_rec_attrs_impl::<_, _, false>(entries, ctx, |ctx, binding_keys, _dyns| {
body_fn(ctx, binding_keys)
@@ -1081,7 +1081,7 @@ where
Ctx: DowngradeContext<'id, 'ir>,
F: FnOnce(
&mut Ctx,
&[StringId],
&[SymId],
&[(IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)],
) -> Result<IrRef<'id, 'ir>>,
{
@@ -1106,7 +1106,7 @@ where
fn collect_inherit_lookups<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>>(
entries: &[ast::Entry],
ctx: &mut Ctx,
) -> Result<HashMap<'ir, StringId, (IrRef<'id, 'ir>, TextRange)>> {
) -> Result<HashMap<'ir, SymId, (IrRef<'id, 'ir>, TextRange)>> {
let mut inherit_lookups = HashMap::new_in(ctx.bump());
for entry in entries {
if let ast::Entry::Inherit(inherit) = entry
@@ -1128,7 +1128,7 @@ fn collect_inherit_lookups<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>>(
fn collect_binding_syms<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>(
pending: &PendingAttrSet,
ctx: &mut Ctx,
) -> Result<HashSet<StringId>> {
) -> Result<HashSet<SymId>> {
let mut binding_syms = HashSet::new();
for (sym, (_, span)) in &pending.stcs {
@@ -1146,7 +1146,7 @@ fn collect_binding_syms<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, const AL
fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>(
pending: PendingAttrSet,
inherit_lookups: &HashMap<StringId, (IrRef<'id, 'ir>, TextRange)>,
inherit_lookups: &HashMap<SymId, (IrRef<'id, 'ir>, TextRange)>,
ctx: &mut Ctx,
) -> Result<FinalizedAttrSet<'id, 'ir>> {
let mut stcs = HashMap::new_in(ctx.bump());
@@ -1176,7 +1176,7 @@ fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_D
fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>(
value: PendingValue,
inherit_lookups: &HashMap<StringId, (IrRef<'id, 'ir>, TextRange)>,
inherit_lookups: &HashMap<SymId, (IrRef<'id, 'ir>, TextRange)>,
ctx: &mut Ctx,
) -> Result<IrRef<'id, 'ir>> {
match value {
+129
View File
@@ -1,9 +1,14 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use deno_core::error::JsError;
use deno_error::JsErrorClass as _;
use itertools::Itertools as _;
use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error;
use crate::runtime::RuntimeContext;
pub type Result<T> = core::result::Result<T, Box<Error>>;
#[derive(Clone, Debug)]
@@ -224,3 +229,127 @@ pub struct StackFrame {
#[source_code]
pub src: NamedSource<Arc<str>>,
}
const MAX_STACK_FRAMES: usize = 20;
const FRAMES_AT_START: usize = 15;
const FRAMES_AT_END: usize = 5;
pub(crate) fn parse_js_error(error: Box<JsError>, ctx: &impl RuntimeContext) -> Error {
let (span, src, frames) = if let Some(stack) = &error.stack {
let mut frames = parse_frames(stack, ctx);
if let Some(last_frame) = frames.pop() {
(
Some(text_range_to_source_span(last_frame.span)),
Some(last_frame.src.into()),
frames,
)
} else {
(None, None, frames)
}
} else {
(None, None, Vec::new())
};
let stack_trace = if std::env::var("NIX_JS_STACK_TRACE").is_ok() {
truncate_stack_trace(frames)
} else {
Vec::new()
};
let message = error.get_message().to_string();
let js_backtrace = error.stack.map(|stack| {
stack
.lines()
.filter(|line| !line.starts_with("NIX_STACK_FRAME:"))
.join("\n")
});
Error::EvalError {
src,
span,
message,
js_backtrace,
stack_trace,
}
}
struct NixStackFrame {
span: rnix::TextRange,
message: String,
src: Source,
}
impl From<NixStackFrame> for StackFrame {
fn from(NixStackFrame { span, message, src }: NixStackFrame) -> Self {
StackFrame {
span: text_range_to_source_span(span),
message,
src: src.into(),
}
}
}
fn parse_frames(stack: &str, ctx: &impl RuntimeContext) -> Vec<NixStackFrame> {
let mut frames = Vec::new();
for line in stack.lines() {
// Format: NIX_STACK_FRAME:span_id:message
let Some(rest) = line.strip_prefix("NIX_STACK_FRAME:") else {
continue;
};
let parts: Vec<&str> = rest.splitn(2, ':').collect();
if parts.is_empty() {
continue;
}
let span_id: usize = match parts[0].parse() {
Ok(id) => id,
Err(_) => continue,
};
let (source_id, span) = ctx.get_span(span_id);
let src = ctx.get_source(source_id);
let message = if parts.len() == 2 {
parts[1].to_string()
} else {
String::new()
};
frames.push(NixStackFrame { span, message, src });
}
frames.dedup_by(|a, b| a.span == b.span && a.message == b.message);
frames
}
fn truncate_stack_trace(frames: Vec<NixStackFrame>) -> Vec<StackFrame> {
let reversed: Vec<_> = frames.into_iter().rev().collect();
let total = reversed.len();
if total <= MAX_STACK_FRAMES {
return reversed.into_iter().map(Into::into).collect();
}
let omitted_count = total - FRAMES_AT_START - FRAMES_AT_END;
reversed
.into_iter()
.enumerate()
.filter_map(|(i, frame)| {
if i < FRAMES_AT_START {
Some(frame.into())
} else if i == FRAMES_AT_START {
Some(StackFrame {
span: text_range_to_source_span(frame.span),
message: format!("... ({} more frames omitted)", omitted_count),
src: frame.src.into(),
})
} else if i >= total - FRAMES_AT_END {
Some(frame.into())
} else {
None
}
})
.collect()
}
@@ -5,6 +5,8 @@ use nix_compat::nixhash::HashAlgo;
use nix_compat::nixhash::NixHash;
use tracing::{debug, info, warn};
use crate::runtime::OpStateExt;
use crate::runtime::RuntimeContext;
use crate::store::Store as _;
mod archive;
@@ -18,6 +20,7 @@ pub use download::Downloader;
pub use metadata_cache::MetadataCache;
use crate::nar;
use crate::runtime::NixRuntimeError;
#[derive(ToV8)]
pub struct FetchUrlResult {
@@ -303,3 +306,11 @@ fn normalize_hash(hash: &str) -> String {
}
hash.to_string()
}
pub fn register_ops<Ctx: RuntimeContext>() -> Vec<deno_core::OpDecl> {
vec![
op_fetch_url::<Ctx>(),
op_fetch_tarball::<Ctx>(),
op_fetch_git::<Ctx>(),
]
}
File renamed without changes.
@@ -10,7 +10,7 @@ impl FetcherCache {
pub fn new() -> Result<Self, std::io::Error> {
let base_dir = dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("fix")
.join("nix-js")
.join("fetchers");
fs::create_dir_all(&base_dir)?;
File renamed without changes.
File renamed without changes.
File renamed without changes.
+17 -22
View File
@@ -4,7 +4,6 @@ use std::{
};
use bumpalo::{Bump, boxed::Box, collections::Vec};
use gc_arena::Collect;
use ghost_cell::{GhostCell, GhostToken};
use rnix::{TextRange, ast};
use string_interner::symbol::SymbolU32;
@@ -24,10 +23,6 @@ impl<'id, 'ir> IrRef<'id, 'ir> {
Self(bump.alloc(GhostCell::new(ir)))
}
pub fn borrow<'a>(&'a self, token: &'a GhostToken<'id>) -> &'a Ir<'ir, Self> {
self.0.borrow(token)
}
/// Freeze a mutable IR reference into a read-only one, consuming the
/// `GhostToken` to prevent any further mutation.
///
@@ -47,6 +42,13 @@ impl<'id, 'ir> IrRef<'id, 'ir> {
}
}
impl<'id, 'ir> Deref for IrRef<'id, 'ir> {
type Target = GhostCell<'id, Ir<'ir, IrRef<'id, 'ir>>>;
fn deref(&self) -> &Self::Target {
self.0
}
}
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct RawIrRef<'ir>(&'ir Ir<'ir, Self>);
@@ -66,7 +68,7 @@ pub enum Ir<'ir, Ref> {
Null,
Str(Box<'ir, String>),
AttrSet {
stcs: HashMap<'ir, StringId, (Ref, TextRange)>,
stcs: HashMap<'ir, SymId, (Ref, TextRange)>,
dyns: Vec<'ir, (Ref, Ref, TextRange)>,
},
List {
@@ -117,7 +119,7 @@ pub enum Ir<'ir, Ref> {
body: Ref,
thunks: Vec<'ir, (ThunkId, Ref)>,
},
WithLookup(StringId),
WithLookup(SymId),
// Function related
Func {
@@ -135,7 +137,7 @@ pub enum Ir<'ir, Ref> {
// Builtins
Builtins,
Builtin(StringId),
Builtin(SymId),
// Misc
TopLevel {
@@ -144,26 +146,19 @@ pub enum Ir<'ir, Ref> {
},
Thunk(ThunkId),
CurPos(TextRange),
ReplBinding(StringId),
ScopedImportBinding(StringId),
ReplBinding(SymId),
ScopedImportBinding(SymId),
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ThunkId(pub usize);
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)]
#[collect(require_static)]
pub struct StringId(pub SymbolU32);
pub type SymId = SymbolU32;
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ArgId(pub u32);
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SpanId(pub u32);
pub struct ArgId(pub usize);
/// Represents a key in an attribute path.
#[allow(unused)]
@@ -174,7 +169,7 @@ pub enum Attr<Ref> {
Dynamic(Ref, TextRange),
/// A static attribute key.
/// Example: `attrs.key`
Str(StringId, TextRange),
Str(SymId, TextRange),
}
/// The kinds of binary operations supported in Nix.
@@ -253,8 +248,8 @@ impl From<ast::UnaryOpKind> for UnOpKind {
/// Describes the parameters of a function.
#[derive(Debug)]
pub struct Param<'ir> {
pub required: Vec<'ir, (StringId, TextRange)>,
pub optional: Vec<'ir, (StringId, TextRange)>,
pub required: Vec<'ir, (SymId, TextRange)>,
pub optional: Vec<'ir, (SymId, TextRange)>,
pub ellipsis: bool,
}
+4 -3
View File
@@ -1,19 +1,20 @@
#![warn(clippy::unwrap_used)]
#![allow(dead_code)]
pub mod context;
pub mod error;
pub mod logging;
pub mod runtime;
pub mod value;
mod bytecode;
mod codegen;
mod derivation;
mod disassembler;
mod downgrade;
// mod fetcher;
mod fetcher;
mod ir;
mod nar;
mod nix_utils;
mod runtime;
mod store;
mod string_context;
File renamed without changes.
+53 -18
View File
@@ -3,15 +3,23 @@ use std::process::exit;
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use fix::error::Source;
use fix::runtime::Runtime;
use hashbrown::HashSet;
use nix_js::context::Context;
use nix_js::error::Source;
use rustyline::DefaultEditor;
use rustyline::error::ReadlineError;
#[derive(Parser)]
#[command(name = "nix-js", about = "Nix expression evaluator")]
struct Cli {
#[cfg(feature = "inspector")]
#[arg(long, value_name = "HOST:PORT", num_args = 0..=1, default_missing_value = "127.0.0.1:9229")]
inspect: Option<String>,
#[cfg(feature = "inspector")]
#[arg(long, value_name = "HOST:PORT", num_args = 0..=1, default_missing_value = "127.0.0.1:9229")]
inspect_brk: Option<String>,
#[command(subcommand)]
command: Command,
}
@@ -40,7 +48,28 @@ struct ExprSource {
file: Option<PathBuf>,
}
fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<()> {
fn create_context(#[cfg(feature = "inspector")] cli: &Cli) -> Result<Context> {
#[cfg(feature = "inspector")]
{
let (addr_str, wait) = if let Some(ref addr) = cli.inspect_brk {
(Some(addr.as_str()), true)
} else if let Some(ref addr) = cli.inspect {
(Some(addr.as_str()), false)
} else {
(None, false)
};
if let Some(addr_str) = addr_str {
let addr: std::net::SocketAddr = addr_str
.parse()
.map_err(|e| anyhow::anyhow!("invalid inspector address '{}': {}", addr_str, e))?;
return Ok(Context::new_with_inspector(addr, wait)?);
}
}
Ok(Context::new()?)
}
fn run_compile(context: &mut Context, src: ExprSource, silent: bool) -> Result<()> {
let src = if let Some(expr) = src.expr {
Source::new_eval(expr)?
} else if let Some(file) = src.file {
@@ -48,11 +77,10 @@ fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<(
} else {
unreachable!()
};
todo!()
/* match runtime.compile_bytecode(src) {
match context.compile_bytecode(src) {
Ok(compiled) => {
if !silent {
println!("{}", runtime.disassemble_colored(&compiled));
println!("{}", context.disassemble_colored(&compiled));
}
}
Err(err) => {
@@ -60,10 +88,12 @@ fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<(
exit(1);
}
};
Ok(()) */
#[cfg(feature = "inspector")]
context.wait_for_inspector_disconnect();
Ok(())
}
fn run_eval(runtime: &mut Runtime, src: ExprSource) -> Result<()> {
fn run_eval(context: &mut Context, src: ExprSource) -> Result<()> {
let src = if let Some(expr) = src.expr {
Source::new_eval(expr)?
} else if let Some(file) = src.file {
@@ -71,7 +101,7 @@ fn run_eval(runtime: &mut Runtime, src: ExprSource) -> Result<()> {
} else {
unreachable!()
};
match runtime.eval_deep(src) {
match context.eval_deep(src) {
Ok(value) => {
println!("{}", value.display_compat());
}
@@ -80,10 +110,12 @@ fn run_eval(runtime: &mut Runtime, src: ExprSource) -> Result<()> {
exit(1);
}
};
#[cfg(feature = "inspector")]
context.wait_for_inspector_disconnect();
Ok(())
}
fn run_repl(runtime: &mut Runtime) -> Result<()> {
fn run_repl(context: &mut Context) -> Result<()> {
let mut rl = DefaultEditor::new()?;
let mut scope = HashSet::new();
const RE: ere::Regex<3> = ere::compile_regex!("^[ \t]*([a-zA-Z_][a-zA-Z0-9_'-]*)[ \t]*(.*)$");
@@ -102,20 +134,20 @@ fn run_repl(runtime: &mut Runtime) -> Result<()> {
eprintln!("Error: missing expression after '='");
continue;
}
match runtime.add_binding(ident, expr, &mut scope) {
match context.add_binding(ident, expr, &mut scope) {
Ok(value) => println!("{} = {}", ident, value),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
}
} else {
let src = Source::new_repl(line)?;
match runtime.eval_repl(src, &scope) {
match context.eval_repl(src, &scope) {
Ok(value) => println!("{value}"),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
}
}
} else {
let src = Source::new_repl(line)?;
match runtime.eval_repl(src, &scope) {
match context.eval_shallow(src) {
Ok(value) => println!("{value}"),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
}
@@ -138,15 +170,18 @@ fn run_repl(runtime: &mut Runtime) -> Result<()> {
}
fn main() -> Result<()> {
fix::logging::init_logging();
nix_js::logging::init_logging();
let cli = Cli::parse();
let mut runtime = Runtime::new()?;
let mut context = create_context(
#[cfg(feature = "inspector")]
&cli,
)?;
match cli.command {
Command::Compile { source, silent } => run_compile(&mut runtime, source, silent),
Command::Eval { source } => run_eval(&mut runtime, source),
Command::Repl => run_repl(&mut runtime),
Command::Compile { source, silent } => run_compile(&mut context, source, silent),
Command::Eval { source } => run_eval(&mut context, source),
Command::Repl => run_repl(&mut context),
}
}
File renamed without changes.
File renamed without changes.
+688
View File
@@ -0,0 +1,688 @@
use std::borrow::Cow;
use std::marker::PhantomData;
use std::path::Path;
#[cfg(feature = "inspector")]
use deno_core::PollEventLoopOptions;
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8};
use crate::bytecode::{Bytecode, Constant};
use crate::error::{Error, Result, Source};
use crate::store::DaemonStore;
use crate::value::{AttrSet, List, Symbol, Value};
#[cfg(feature = "inspector")]
pub(crate) mod inspector;
mod ops;
use ops::*;
mod value;
type ScopeRef<'p, 's> = v8::PinnedRef<'p, v8::HandleScope<'s>>;
type LocalValue<'a> = v8::Local<'a, v8::Value>;
type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>;
pub(crate) trait RuntimeContext: 'static {
fn get_current_dir(&self) -> &Path;
fn add_source(&mut self, path: Source);
fn compile(&mut self, source: Source) -> Result<String>;
fn compile_scoped(&mut self, source: Source, scope: Vec<String>) -> Result<String>;
fn compile_bytecode(&mut self, source: Source) -> Result<Bytecode>;
fn compile_bytecode_scoped(&mut self, source: Source, scope: Vec<String>) -> Result<Bytecode>;
fn get_source(&self, id: usize) -> Source;
fn get_store(&self) -> &DaemonStore;
fn get_span(&self, id: usize) -> (usize, rnix::TextRange);
fn get_unsynced(&mut self) -> (&[String], &[Constant], usize, usize);
}
pub(crate) trait OpStateExt<Ctx: RuntimeContext> {
fn get_ctx(&self) -> &Ctx;
fn get_ctx_mut(&mut self) -> &mut Ctx;
}
impl<Ctx: RuntimeContext> OpStateExt<Ctx> for OpState {
fn get_ctx(&self) -> &Ctx {
self.try_borrow::<&'static mut Ctx>()
.expect("RuntimeContext not set")
}
fn get_ctx_mut(&mut self) -> &mut Ctx {
self.try_borrow_mut::<&'static mut Ctx>()
.expect("RuntimeContext not set")
}
}
fn runtime_extension<Ctx: RuntimeContext>() -> Extension {
const ESM: &[ExtensionFileSource] =
&deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js");
let mut ops = vec![
op_import::<Ctx>(),
op_scoped_import::<Ctx>(),
op_resolve_path(),
op_read_file(),
op_read_file_type(),
op_read_dir(),
op_path_exists(),
op_walk_dir(),
op_make_placeholder(),
op_store_path::<Ctx>(),
op_convert_hash(),
op_hash_string(),
op_hash_file(),
op_parse_hash(),
op_add_path::<Ctx>(),
op_add_filtered_path::<Ctx>(),
op_decode_span::<Ctx>(),
op_to_file::<Ctx>(),
op_copy_path_to_store::<Ctx>(),
op_get_env(),
op_match(),
op_split(),
op_from_json(),
op_from_toml(),
op_finalize_derivation::<Ctx>(),
op_to_xml(),
];
ops.extend(crate::fetcher::register_ops::<Ctx>());
Extension {
name: "nix_runtime",
esm_files: Cow::Borrowed(ESM),
esm_entry_point: Some("ext:nix_runtime/runtime.js"),
ops: Cow::Owned(ops),
enabled: true,
..Default::default()
}
}
mod private {
use deno_error::js_error_wrapper;
#[derive(Debug)]
pub struct SimpleErrorWrapper(String);
impl std::fmt::Display for SimpleErrorWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl std::error::Error for SimpleErrorWrapper {}
js_error_wrapper!(SimpleErrorWrapper, NixRuntimeError, "Error");
impl From<String> for NixRuntimeError {
fn from(value: String) -> Self {
NixRuntimeError(SimpleErrorWrapper(value))
}
}
impl From<&str> for NixRuntimeError {
fn from(value: &str) -> Self {
NixRuntimeError(SimpleErrorWrapper(value.to_string()))
}
}
}
pub(crate) use private::NixRuntimeError;
pub(crate) struct Runtime<Ctx: RuntimeContext> {
js_runtime: JsRuntime,
#[cfg(feature = "inspector")]
rt: tokio::runtime::Runtime,
#[cfg(feature = "inspector")]
wait_for_inspector: bool,
symbols: GlobalSymbols,
cached_fns: CachedFunctions,
_marker: PhantomData<Ctx>,
}
#[cfg(feature = "inspector")]
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct InspectorOptions {
pub(crate) enable: bool,
pub(crate) wait: bool,
}
impl<Ctx: RuntimeContext> Runtime<Ctx> {
pub(crate) fn new(
#[cfg(feature = "inspector")] inspector_options: InspectorOptions,
) -> Result<Self> {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
assert_eq!(
deno_core::v8_set_flags(vec![
"".into(),
format!("--stack-size={}", 8 * 1024),
#[cfg(feature = "prof")]
("--prof".into())
]),
[""]
);
JsRuntime::init_platform(Some(v8::new_default_platform(0, false).make_shared()));
});
let mut js_runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![runtime_extension::<Ctx>()],
#[cfg(feature = "inspector")]
inspector: inspector_options.enable,
is_main: true,
..Default::default()
});
js_runtime.op_state().borrow_mut().put(RegexCache::new());
js_runtime.op_state().borrow_mut().put(DrvHashCache::new());
let (symbols, cached_fns) = {
deno_core::scope!(scope, &mut js_runtime);
let symbols = Self::get_symbols(scope)?;
let cached_fns = Self::get_cached_functions(scope)?;
(symbols, cached_fns)
};
Ok(Self {
js_runtime,
#[cfg(feature = "inspector")]
rt: tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to build tokio runtime"),
#[cfg(feature = "inspector")]
wait_for_inspector: inspector_options.wait,
symbols,
cached_fns,
_marker: PhantomData,
})
}
#[cfg(feature = "inspector")]
pub(crate) fn inspector(&self) -> std::rc::Rc<deno_core::JsRuntimeInspector> {
self.js_runtime.inspector()
}
#[cfg(feature = "inspector")]
pub(crate) fn wait_for_inspector_disconnect(&mut self) {
let _ = self
.rt
.block_on(self.js_runtime.run_event_loop(PollEventLoopOptions {
wait_for_inspector: true,
..Default::default()
}));
}
pub(crate) fn eval(&mut self, script: String, ctx: &mut Ctx) -> Result<Value> {
let ctx: &'static mut Ctx = unsafe { &mut *(ctx as *mut Ctx) };
self.js_runtime.op_state().borrow_mut().put(ctx);
#[cfg(feature = "inspector")]
if self.wait_for_inspector {
self.js_runtime
.inspector()
.wait_for_session_and_break_on_next_statement();
} else {
self.js_runtime.inspector().wait_for_session();
}
let global_value = self
.js_runtime
.execute_script("<eval>", script)
.map_err(|error| {
let op_state = self.js_runtime.op_state();
let op_state_borrow = op_state.borrow();
let ctx: &Ctx = op_state_borrow.get_ctx();
crate::error::parse_js_error(error, ctx)
})?;
// Retrieve scope from JsRuntime
deno_core::scope!(scope, self.js_runtime);
let local_value = v8::Local::new(scope, &global_value);
let symbols = &self.symbols.local(scope);
Ok(to_value(local_value, scope, symbols))
}
pub(crate) fn eval_bytecode(
&mut self,
result: Bytecode,
ctx: &mut Ctx,
force_mode: ForceMode,
) -> Result<Value> {
let ctx: &'static mut Ctx = unsafe { &mut *(ctx as *mut Ctx) };
{
deno_core::scope!(scope, self.js_runtime);
sync_global_tables(scope, &self.cached_fns, ctx);
}
let op_state = self.js_runtime.op_state();
op_state.borrow_mut().put(ctx);
#[cfg(feature = "inspector")]
if self.wait_for_inspector {
self.js_runtime
.inspector()
.wait_for_session_and_break_on_next_statement();
} else {
self.js_runtime.inspector().wait_for_session();
}
deno_core::scope!(scope, self.js_runtime);
let store = v8::ArrayBuffer::new_backing_store_from_boxed_slice(result.code);
let ab = v8::ArrayBuffer::with_backing_store(scope, &store.make_shared());
let u8a = v8::Uint8Array::new(scope, ab, 0, ab.byte_length())
.ok_or_else(|| Error::internal("failed to create Uint8Array".into()))?;
let dir = v8::String::new(scope, &result.current_dir)
.ok_or_else(|| Error::internal("failed to create dir string".into()))?;
let undef = v8::undefined(scope);
let tc = std::pin::pin!(v8::TryCatch::new(scope));
let scope = &mut tc.init();
let exec_bytecode = v8::Local::new(scope, &self.cached_fns.exec_bytecode);
let raw_result = exec_bytecode
.call(scope, undef.into(), &[u8a.into(), dir.into()])
.ok_or_else(|| {
scope
.exception()
.map(|e| {
let op_state_borrow = op_state.borrow();
let ctx: &Ctx = op_state_borrow.get_ctx();
Box::new(crate::error::parse_js_error(
deno_core::error::JsError::from_v8_exception(scope, e),
ctx,
))
})
.unwrap_or_else(|| Error::internal("bytecode execution failed".into()))
})?;
let force_fn = match force_mode {
ForceMode::Force => &self.cached_fns.force_fn,
ForceMode::ForceShallow => &self.cached_fns.force_shallow_fn,
ForceMode::ForceDeep => &self.cached_fns.force_deep_fn,
};
let force_fn = v8::Local::new(scope, force_fn);
let forced = force_fn
.call(scope, undef.into(), &[raw_result])
.ok_or_else(|| {
scope
.exception()
.map(|e| {
let op_state_borrow = op_state.borrow();
let ctx: &Ctx = op_state_borrow.get_ctx();
Box::new(crate::error::parse_js_error(
deno_core::error::JsError::from_v8_exception(scope, e),
ctx,
))
})
.unwrap_or_else(|| Error::internal("force failed".into()))
})?;
let symbols = &self.symbols.local(scope);
Ok(to_value(forced, scope, symbols))
}
fn get_symbols(scope: &ScopeRef) -> Result<GlobalSymbols> {
let global = scope.get_current_context().global(scope);
let nix_key = v8::String::new(scope, "Nix")
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let nix_obj = global
.get(scope, nix_key.into())
.ok_or_else(|| Error::internal("failed to get global Nix object".into()))?
.to_object(scope)
.ok_or_else(|| {
Error::internal("failed to convert global Nix Value to object".into())
})?;
let get_symbol = |symbol| {
let key = v8::String::new(scope, symbol)
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let val = nix_obj
.get(scope, key.into())
.ok_or_else(|| Error::internal(format!("failed to get {symbol} Symbol")))?;
let sym = val.try_cast::<v8::Symbol>().map_err(|err| {
Error::internal(format!(
"failed to convert {symbol} Value to Symbol ({err})"
))
})?;
Result::Ok(v8::Global::new(scope, sym))
};
let is_thunk = get_symbol("IS_THUNK")?;
let primop_metadata = get_symbol("PRIMOP_METADATA")?;
let has_context = get_symbol("HAS_CONTEXT")?;
let is_path = get_symbol("IS_PATH")?;
let is_cycle = get_symbol("IS_CYCLE")?;
Ok(GlobalSymbols {
is_thunk,
primop_metadata,
has_context,
is_path,
is_cycle,
})
}
fn get_cached_functions(scope: &ScopeRef) -> Result<CachedFunctions> {
let global = scope.get_current_context().global(scope);
let nix_key = v8::String::new(scope, "Nix")
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let nix_obj = global
.get(scope, nix_key.into())
.ok_or_else(|| Error::internal("failed to get global Nix object".into()))?
.to_object(scope)
.ok_or_else(|| {
Error::internal("failed to convert global Nix Value to object".into())
})?;
let get_fn = |name: &str| -> Result<v8::Global<v8::Function>> {
let key = v8::String::new(scope, name)
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let val = nix_obj
.get(scope, key.into())
.ok_or_else(|| Error::internal(format!("failed to get Nix.{name}")))?;
let func = val
.try_cast::<v8::Function>()
.map_err(|err| Error::internal(format!("Nix.{name} is not a function ({err})")))?;
Ok(v8::Global::new(scope, func))
};
let exec_bytecode = get_fn("execBytecode")?;
let force_fn = get_fn("force")?;
let force_shallow_fn = get_fn("forceShallow")?;
let force_deep_fn = get_fn("forceDeep")?;
let strings_key = v8::String::new(scope, "strings")
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let strings_array = nix_obj
.get(scope, strings_key.into())
.ok_or_else(|| Error::internal("failed to get Nix.strings".into()))?
.try_cast::<v8::Array>()
.map_err(|err| Error::internal(format!("Nix.strings is not an array ({err})")))?;
let constants_key = v8::String::new(scope, "constants")
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let constants_array = nix_obj
.get(scope, constants_key.into())
.ok_or_else(|| Error::internal("failed to get Nix.constants".into()))?
.try_cast::<v8::Array>()
.map_err(|err| Error::internal(format!("Nix.constants is not an array ({err})")))?;
Ok(CachedFunctions {
exec_bytecode,
force_fn,
force_shallow_fn,
force_deep_fn,
strings_array: v8::Global::new(scope, strings_array),
constants_array: v8::Global::new(scope, constants_array),
})
}
}
struct GlobalSymbols {
is_thunk: v8::Global<v8::Symbol>,
primop_metadata: v8::Global<v8::Symbol>,
has_context: v8::Global<v8::Symbol>,
is_path: v8::Global<v8::Symbol>,
is_cycle: v8::Global<v8::Symbol>,
}
impl GlobalSymbols {
fn local<'a>(&self, scope: &ScopeRef<'a, '_>) -> LocalSymbols<'a> {
LocalSymbols {
is_thunk: v8::Local::new(scope, &self.is_thunk),
primop_metadata: v8::Local::new(scope, &self.primop_metadata),
has_context: v8::Local::new(scope, &self.has_context),
is_path: v8::Local::new(scope, &self.is_path),
is_cycle: v8::Local::new(scope, &self.is_cycle),
}
}
}
struct LocalSymbols<'a> {
is_thunk: v8::Local<'a, v8::Symbol>,
primop_metadata: v8::Local<'a, v8::Symbol>,
has_context: v8::Local<'a, v8::Symbol>,
is_path: v8::Local<'a, v8::Symbol>,
is_cycle: v8::Local<'a, v8::Symbol>,
}
struct CachedFunctions {
exec_bytecode: v8::Global<v8::Function>,
force_fn: v8::Global<v8::Function>,
force_shallow_fn: v8::Global<v8::Function>,
force_deep_fn: v8::Global<v8::Function>,
strings_array: v8::Global<v8::Array>,
constants_array: v8::Global<v8::Array>,
}
pub(crate) enum ForceMode {
Force,
ForceShallow,
ForceDeep,
}
fn sync_global_tables<Ctx: RuntimeContext>(
scope: &ScopeRef,
cached: &CachedFunctions,
ctx: &mut Ctx,
) {
let (new_strings, new_constants, strings_base, constants_base) = ctx.get_unsynced();
if !new_strings.is_empty() {
let s_array = v8::Local::new(scope, &cached.strings_array);
for (i, s) in new_strings.iter().enumerate() {
let idx = (strings_base + i) as u32;
#[allow(clippy::unwrap_used)]
let val = v8::String::new(scope, s).unwrap();
s_array.set_index(scope, idx, val.into());
}
}
if !new_constants.is_empty() {
let k_array = v8::Local::new(scope, &cached.constants_array);
for (i, c) in new_constants.iter().enumerate() {
let idx = (constants_base + i) as u32;
let val: v8::Local<v8::Value> = match c {
Constant::Int(n) => v8::BigInt::new_from_i64(scope, *n).into(),
Constant::Float(bits) => v8::Number::new(scope, f64::from_bits(*bits)).into(),
};
k_array.set_index(scope, idx, val);
}
}
}
fn to_value<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,
symbols: &LocalSymbols<'a>,
) -> Value {
match () {
_ if val.is_big_int() => {
let (val, lossless) = val
.to_big_int(scope)
.expect("infallible conversion")
.i64_value();
if !lossless {
panic!("BigInt value out of i64 range: conversion lost precision");
}
Value::Int(val)
}
_ if val.is_number() => {
let val = val.to_number(scope).expect("infallible conversion").value();
Value::Float(val)
}
_ if val.is_true() => Value::Bool(true),
_ if val.is_false() => Value::Bool(false),
_ if val.is_null() => Value::Null,
_ if val.is_string() => {
let val = val.to_string(scope).expect("infallible conversion");
Value::String(val.to_rust_string_lossy(scope))
}
_ if val.is_array() => {
let val = val.try_cast::<v8::Array>().expect("infallible conversion");
let len = val.length();
let list = (0..len)
.map(|i| {
let val = val.get_index(scope, i).expect("infallible index operation");
to_value(val, scope, symbols)
})
.collect();
Value::List(List::new(list))
}
_ if val.is_function() => {
if let Some(primop) = to_primop(val, scope, symbols.primop_metadata) {
primop
} else {
Value::Func
}
}
_ if val.is_map() => {
let val = val.try_cast::<v8::Map>().expect("infallible conversion");
let size = val.size() as u32;
let array = val.as_array(scope);
let attrs = (0..size)
.map(|i| {
let key = array
.get_index(scope, i * 2)
.expect("infallible index operation");
let key = key.to_rust_string_lossy(scope);
let val = array
.get_index(scope, i * 2 + 1)
.expect("infallible index operation");
let val = to_value(val, scope, symbols);
(Symbol::new(Cow::Owned(key)), val)
})
.collect();
Value::AttrSet(AttrSet::new(attrs))
}
_ if val.is_object() => {
if is_thunk(val, scope, symbols.is_thunk) {
return Value::Thunk;
}
if is_cycle(val, scope, symbols.is_cycle) {
return Value::Repeated;
}
if let Some(path_val) = extract_path(val, scope, symbols.is_path) {
return Value::Path(path_val);
}
if let Some(string_val) = extract_string_with_context(val, scope, symbols.has_context) {
return Value::String(string_val);
}
let val = val.to_object(scope).expect("infallible conversion");
let keys = val
.get_own_property_names(scope, v8::GetPropertyNamesArgsBuilder::new().build())
.expect("infallible operation");
let len = keys.length();
let attrs = (0..len)
.map(|i| {
let key = keys
.get_index(scope, i)
.expect("infallible index operation");
let val = val.get(scope, key).expect("infallible operation");
let key = key.to_rust_string_lossy(scope);
(Symbol::from(key), to_value(val, scope, symbols))
})
.collect();
Value::AttrSet(AttrSet::new(attrs))
}
_ => unimplemented!("can not convert {} to NixValue", val.type_repr()),
}
}
fn is_thunk<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymbol<'a>) -> bool {
if !val.is_object() {
return false;
}
let obj = val.to_object(scope).expect("infallible conversion");
matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true())
}
fn is_cycle<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymbol<'a>) -> bool {
if !val.is_object() {
return false;
}
let obj = val.to_object(scope).expect("infallible conversion");
matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true())
}
fn extract_string_with_context<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,
symbol: LocalSymbol<'a>,
) -> Option<String> {
if !val.is_object() {
return None;
}
let obj = val.to_object(scope).expect("infallible conversion");
let has_context = obj.get(scope, symbol.into())?;
if !has_context.is_true() {
return None;
}
let value_key = v8::String::new(scope, "value")?;
let value = obj.get(scope, value_key.into())?;
if value.is_string() {
Some(value.to_rust_string_lossy(scope))
} else {
None
}
}
fn extract_path<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,
symbol: LocalSymbol<'a>,
) -> Option<String> {
if !val.is_object() {
return None;
}
let obj = val.to_object(scope).expect("infallible conversion");
let is_path = obj.get(scope, symbol.into())?;
if !is_path.is_true() {
return None;
}
let value_key = v8::String::new(scope, "value")?;
let value = obj.get(scope, value_key.into())?;
if value.is_string() {
Some(value.to_rust_string_lossy(scope))
} else {
None
}
}
fn to_primop<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,
symbol: LocalSymbol<'a>,
) -> Option<Value> {
if !val.is_function() {
return None;
}
let obj = val.to_object(scope).expect("infallible conversion");
let metadata = obj.get(scope, symbol.into())?.to_object(scope)?;
let name_key = v8::String::new(scope, "name")?;
let name = metadata
.get(scope, name_key.into())?
.to_rust_string_lossy(scope);
let applied_key = v8::String::new(scope, "applied")?;
let applied_val = metadata.get(scope, applied_key.into())?;
let applied = applied_val.to_number(scope)?.value();
if applied == 0.0 {
Some(Value::PrimOp(name))
} else {
Some(Value::PrimOpApp(name))
}
}
+493
View File
@@ -0,0 +1,493 @@
// Copyright 2018-2025 the Deno authors. MIT license.
// Alias for the future `!` type.
use core::convert::Infallible as Never;
use std::cell::RefCell;
use std::net::SocketAddr;
use std::pin::pin;
use std::process;
use std::rc::Rc;
use std::task::Poll;
use std::thread;
use deno_core::InspectorMsg;
use deno_core::InspectorSessionChannels;
use deno_core::InspectorSessionKind;
use deno_core::InspectorSessionProxy;
use deno_core::JsRuntimeInspector;
use deno_core::anyhow::Context;
use deno_core::futures::channel::mpsc;
use deno_core::futures::channel::mpsc::UnboundedReceiver;
use deno_core::futures::channel::mpsc::UnboundedSender;
use deno_core::futures::channel::oneshot;
use deno_core::futures::prelude::*;
use deno_core::futures::stream::StreamExt;
use deno_core::serde_json::Value;
use deno_core::serde_json::json;
use deno_core::unsync::spawn;
use deno_core::url::Url;
use fastwebsockets::Frame;
use fastwebsockets::OpCode;
use fastwebsockets::WebSocket;
use hashbrown::HashMap;
use hyper::body::Bytes;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use tokio::sync::broadcast;
use uuid::Uuid;
/// Websocket server that is used to proxy connections from
/// devtools to the inspector.
pub struct InspectorServer {
pub host: SocketAddr,
register_inspector_tx: UnboundedSender<InspectorInfo>,
shutdown_server_tx: Option<broadcast::Sender<()>>,
thread_handle: Option<thread::JoinHandle<()>>,
}
impl InspectorServer {
pub fn new(host: SocketAddr, name: &'static str) -> Result<Self, anyhow::Error> {
let (register_inspector_tx, register_inspector_rx) = mpsc::unbounded::<InspectorInfo>();
let (shutdown_server_tx, shutdown_server_rx) = broadcast::channel(1);
let tcp_listener = std::net::TcpListener::bind(host)
.with_context(|| format!("Failed to bind inspector server socket at {}", host))?;
tcp_listener.set_nonblocking(true)?;
let thread_handle = thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to build tokio runtime");
let local = tokio::task::LocalSet::new();
local.block_on(
&rt,
server(
tcp_listener,
register_inspector_rx,
shutdown_server_rx,
name,
),
)
});
Ok(Self {
host,
register_inspector_tx,
shutdown_server_tx: Some(shutdown_server_tx),
thread_handle: Some(thread_handle),
})
}
pub fn register_inspector(
&self,
module_url: String,
inspector: Rc<JsRuntimeInspector>,
wait_for_session: bool,
) {
let session_sender = inspector.get_session_sender();
let deregister_rx = inspector.add_deregister_handler();
let info = InspectorInfo::new(
self.host,
session_sender,
deregister_rx,
module_url,
wait_for_session,
);
self.register_inspector_tx
.unbounded_send(info)
.expect("unreachable");
}
}
impl Drop for InspectorServer {
fn drop(&mut self) {
if let Some(shutdown_server_tx) = self.shutdown_server_tx.take() {
shutdown_server_tx
.send(())
.expect("unable to send shutdown signal");
}
if let Some(thread_handle) = self.thread_handle.take() {
thread_handle.join().expect("unable to join thread");
}
}
}
fn handle_ws_request(
req: http::Request<hyper::body::Incoming>,
inspector_map_rc: Rc<RefCell<HashMap<Uuid, InspectorInfo>>>,
) -> http::Result<http::Response<Box<http_body_util::Full<Bytes>>>> {
let (parts, body) = req.into_parts();
let req = http::Request::from_parts(parts, ());
let maybe_uuid = req
.uri()
.path()
.strip_prefix("/ws/")
.and_then(|s| Uuid::parse_str(s).ok());
let Some(uuid) = maybe_uuid else {
return http::Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.body(Box::new(Bytes::from("Malformed inspector UUID").into()));
};
// run in a block to not hold borrow to `inspector_map` for too long
let new_session_tx = {
let inspector_map = inspector_map_rc.borrow();
let maybe_inspector_info = inspector_map.get(&uuid);
let Some(info) = maybe_inspector_info else {
return http::Response::builder()
.status(http::StatusCode::NOT_FOUND)
.body(Box::new(Bytes::from("Invalid inspector UUID").into()));
};
info.new_session_tx.clone()
};
let (parts, _) = req.into_parts();
let mut req = http::Request::from_parts(parts, body);
let Ok((resp, upgrade_fut)) = fastwebsockets::upgrade::upgrade(&mut req) else {
return http::Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.body(Box::new(
Bytes::from("Not a valid Websocket Request").into(),
));
};
// spawn a task that will wait for websocket connection and then pump messages between
// the socket and inspector proxy
spawn(async move {
let websocket = match upgrade_fut.await {
Ok(w) => w,
Err(err) => {
eprintln!(
"Inspector server failed to upgrade to WS connection: {:?}",
err
);
return;
}
};
// The 'outbound' channel carries messages sent to the websocket.
let (outbound_tx, outbound_rx) = mpsc::unbounded();
// The 'inbound' channel carries messages received from the websocket.
let (inbound_tx, inbound_rx) = mpsc::unbounded();
let inspector_session_proxy = InspectorSessionProxy {
channels: InspectorSessionChannels::Regular {
tx: outbound_tx,
rx: inbound_rx,
},
kind: InspectorSessionKind::NonBlocking {
wait_for_disconnect: true,
},
};
eprintln!("Debugger session started.");
let _ = new_session_tx.unbounded_send(inspector_session_proxy);
pump_websocket_messages(websocket, inbound_tx, outbound_rx).await;
});
let (parts, _body) = resp.into_parts();
let resp = http::Response::from_parts(parts, Box::new(http_body_util::Full::new(Bytes::new())));
Ok(resp)
}
fn handle_json_request(
inspector_map: Rc<RefCell<HashMap<Uuid, InspectorInfo>>>,
host: Option<String>,
) -> http::Result<http::Response<Box<http_body_util::Full<Bytes>>>> {
let data = inspector_map
.borrow()
.values()
.map(move |info| info.get_json_metadata(&host))
.collect::<Vec<_>>();
let body: http_body_util::Full<Bytes> =
Bytes::from(serde_json::to_string(&data).expect("unreachable")).into();
http::Response::builder()
.status(http::StatusCode::OK)
.header(http::header::CONTENT_TYPE, "application/json")
.body(Box::new(body))
}
fn handle_json_version_request(
version_response: Value,
) -> http::Result<http::Response<Box<http_body_util::Full<Bytes>>>> {
let body = Box::new(http_body_util::Full::from(
serde_json::to_string(&version_response).expect("unreachable"),
));
http::Response::builder()
.status(http::StatusCode::OK)
.header(http::header::CONTENT_TYPE, "application/json")
.body(body)
}
async fn server(
listener: std::net::TcpListener,
register_inspector_rx: UnboundedReceiver<InspectorInfo>,
shutdown_server_rx: broadcast::Receiver<()>,
name: &str,
) {
let inspector_map_ = Rc::new(RefCell::new(HashMap::<Uuid, InspectorInfo>::new()));
let inspector_map = Rc::clone(&inspector_map_);
let register_inspector_handler =
listen_for_new_inspectors(register_inspector_rx, inspector_map.clone()).boxed_local();
let inspector_map = Rc::clone(&inspector_map_);
let deregister_inspector_handler = future::poll_fn(|cx| {
inspector_map
.borrow_mut()
.retain(|_, info| info.deregister_rx.poll_unpin(cx) == Poll::Pending);
Poll::<Never>::Pending
})
.boxed_local();
let json_version_response = json!({
"Browser": name,
"Protocol-Version": "1.3",
"V8-Version": deno_core::v8::VERSION_STRING,
});
// Create the server manually so it can use the Local Executor
let listener = match TcpListener::from_std(listener) {
Ok(l) => l,
Err(err) => {
eprintln!("Cannot create async listener from std listener: {:?}", err);
return;
}
};
let server_handler = async move {
loop {
let mut rx = shutdown_server_rx.resubscribe();
let mut shutdown_rx = pin!(rx.recv());
let mut accept = pin!(listener.accept());
let stream = tokio::select! {
accept_result =
&mut accept => {
match accept_result {
Ok((s, _)) => s,
Err(err) => {
eprintln!("Failed to accept inspector connection: {:?}", err);
continue;
}
}
},
_ = &mut shutdown_rx => {
break;
}
};
let io = TokioIo::new(stream);
let inspector_map = Rc::clone(&inspector_map_);
let json_version_response = json_version_response.clone();
let mut shutdown_server_rx = shutdown_server_rx.resubscribe();
let service =
hyper::service::service_fn(move |req: http::Request<hyper::body::Incoming>| {
future::ready({
// If the host header can make a valid URL, use it
let host = req
.headers()
.get("host")
.and_then(|host| host.to_str().ok())
.and_then(|host| Url::parse(&format!("http://{host}")).ok())
.and_then(|url| match (url.host(), url.port()) {
(Some(host), Some(port)) => Some(format!("{host}:{port}")),
(Some(host), None) => Some(format!("{host}")),
_ => None,
});
match (req.method(), req.uri().path()) {
(&http::Method::GET, path) if path.starts_with("/ws/") => {
handle_ws_request(req, Rc::clone(&inspector_map))
}
(&http::Method::GET, "/json/version") => {
handle_json_version_request(json_version_response.clone())
}
(&http::Method::GET, "/json") => {
handle_json_request(Rc::clone(&inspector_map), host)
}
(&http::Method::GET, "/json/list") => {
handle_json_request(Rc::clone(&inspector_map), host)
}
_ => http::Response::builder()
.status(http::StatusCode::NOT_FOUND)
.body(Box::new(http_body_util::Full::new(Bytes::from(
"Not Found",
)))),
}
})
});
deno_core::unsync::spawn(async move {
let server = hyper::server::conn::http1::Builder::new();
let mut conn = pin!(server.serve_connection(io, service).with_upgrades());
let mut shutdown_rx = pin!(shutdown_server_rx.recv());
tokio::select! {
result = conn.as_mut() => {
if let Err(err) = result {
eprintln!("Failed to serve connection: {:?}", err);
}
},
_ = &mut shutdown_rx => {
conn.as_mut().graceful_shutdown();
let _ = conn.await;
}
}
});
}
}
.boxed_local();
tokio::select! {
_ = register_inspector_handler => {},
_ = deregister_inspector_handler => unreachable!(),
_ = server_handler => {},
}
}
async fn listen_for_new_inspectors(
mut register_inspector_rx: UnboundedReceiver<InspectorInfo>,
inspector_map: Rc<RefCell<HashMap<Uuid, InspectorInfo>>>,
) {
while let Some(info) = register_inspector_rx.next().await {
eprintln!(
"Debugger listening on {}",
info.get_websocket_debugger_url(&info.host.to_string())
);
eprintln!("Visit chrome://inspect to connect to the debugger.");
if info.wait_for_session {
eprintln!("nix-js is waiting for debugger to connect.");
}
if inspector_map.borrow_mut().insert(info.uuid, info).is_some() {
panic!("Inspector UUID already in map");
}
}
}
/// The pump future takes care of forwarding messages between the websocket
/// and channels. It resolves when either side disconnects, ignoring any
/// errors.
///
/// The future proxies messages sent and received on a WebSocket
/// to a UnboundedSender/UnboundedReceiver pair. We need these "unbounded" channel ends to sidestep
/// Tokio's task budget, which causes issues when JsRuntimeInspector::poll_sessions()
/// needs to block the thread because JavaScript execution is paused.
///
/// This works because UnboundedSender/UnboundedReceiver are implemented in the
/// 'futures' crate, therefore they can't participate in Tokio's cooperative
/// task yielding.
async fn pump_websocket_messages(
mut websocket: WebSocket<TokioIo<hyper::upgrade::Upgraded>>,
inbound_tx: UnboundedSender<String>,
mut outbound_rx: UnboundedReceiver<InspectorMsg>,
) {
'pump: loop {
tokio::select! {
Some(msg) = outbound_rx.next() => {
let msg = Frame::text(msg.content.into_bytes().into());
let _ = websocket.write_frame(msg).await;
}
Ok(msg) = websocket.read_frame() => {
match msg.opcode {
OpCode::Text => {
if let Ok(s) = String::from_utf8(msg.payload.to_vec()) {
let _ = inbound_tx.unbounded_send(s);
}
}
OpCode::Close => {
// Users don't care if there was an error coming from debugger,
// just about the fact that debugger did disconnect.
eprintln!("Debugger session ended");
break 'pump;
}
_ => {
// Ignore other messages.
}
}
}
else => {
break 'pump;
}
}
}
}
/// Inspector information that is sent from the isolate thread to the server
/// thread when a new inspector is created.
pub struct InspectorInfo {
pub host: SocketAddr,
pub uuid: Uuid,
pub thread_name: Option<String>,
pub new_session_tx: UnboundedSender<InspectorSessionProxy>,
pub deregister_rx: oneshot::Receiver<()>,
pub url: String,
pub wait_for_session: bool,
}
impl InspectorInfo {
pub fn new(
host: SocketAddr,
new_session_tx: mpsc::UnboundedSender<InspectorSessionProxy>,
deregister_rx: oneshot::Receiver<()>,
url: String,
wait_for_session: bool,
) -> Self {
Self {
host,
uuid: Uuid::new_v4(),
thread_name: thread::current().name().map(|n| n.to_owned()),
new_session_tx,
deregister_rx,
url,
wait_for_session,
}
}
fn get_json_metadata(&self, host: &Option<String>) -> Value {
let host_listen = format!("{}", self.host);
let host = host.as_ref().unwrap_or(&host_listen);
json!({
"description": "nix-js",
"devtoolsFrontendUrl": self.get_frontend_url(host),
"faviconUrl": "https://deno.land/favicon.ico",
"id": self.uuid.to_string(),
"title": self.get_title(),
"type": "node",
"url": self.url.to_string(),
"webSocketDebuggerUrl": self.get_websocket_debugger_url(host),
})
}
pub fn get_websocket_debugger_url(&self, host: &str) -> String {
format!("ws://{}/ws/{}", host, &self.uuid)
}
fn get_frontend_url(&self, host: &str) -> String {
format!(
"devtools://devtools/bundled/js_app.html?ws={}/ws/{}&experiments=true&v8only=true",
host, &self.uuid
)
}
fn get_title(&self) -> String {
format!(
"nix-js{} [pid: {}]",
self.thread_name
.as_ref()
.map(|n| format!(" - {n}"))
.unwrap_or_default(),
process::id(),
)
}
}
File diff suppressed because it is too large. Load diff
+500
View File
@@ -0,0 +1,500 @@
use std::fmt;
use std::marker::PhantomData;
use boxing::nan::raw::{RawBox, RawStore, RawTag, Value as RawValue};
use gc_arena::{Collect, Gc};
use hashbrown::HashTable;
// ---------------------------------------------------------------------------
// Tag layout
// ---------------------------------------------------------------------------
//
// Positive tags (sign=false) — inline data in 6 bytes:
// 1: SmallInt — i32
// 2: Bool — u8 (0 or 1)
// 3: Null — no payload
// 4: SmallString — SmallStringId (u32)
//
// Negative tags (sign=true) — GC heap pointer (48-bit address):
// 1: BigInt → Gc<'gc, i64>
// 2: String → Gc<'gc, NixString>
// 3: SmallAttrSet → Gc<'gc, SmallAttrSet<'gc>>
// 4: AttrSet → Gc<'gc, AttrSet<'gc>>
// 5: List → Gc<'gc, Box<[Value<'gc>]>>
//
// Floats are stored directly as f64 (no tag).
const TAG_SMI: (bool, u8) = (false, 1);
const TAG_BOOL: (bool, u8) = (false, 2);
const TAG_NULL: (bool, u8) = (false, 3);
const TAG_SMALL_STRING: (bool, u8) = (false, 4);
const TAG_BIG_INT: (bool, u8) = (true, 1);
const TAG_STRING: (bool, u8) = (true, 2);
const TAG_SMALL_ATTRS: (bool, u8) = (true, 3);
const TAG_ATTRS: (bool, u8) = (true, 4);
const TAG_LIST: (bool, u8) = (true, 5);
/// # Nix runtime value representation
///
/// NaN-boxed value fitting in 8 bytes. Morally equivalent to:
/// ```ignore
/// enum NixValue<'gc> {
/// Float(SingleNaNF64),
/// SmallInt(i32),
/// BigInt(Gc<'gc, i64>),
/// Bool(bool),
/// Null,
/// SmallString(SmallStringId),
/// String(Gc<'gc, NixString>),
/// SmallAttrSet(Gc<'gc, SmallAttrSet<'gc>>),
/// AttrSet(Gc<'gc, AttrSet<'gc>>),
/// List(Gc<'gc, Box<[Value<'gc>]>>),
/// }
/// ```
#[repr(transparent)]
pub(crate) struct Value<'gc> {
raw: RawBox,
_marker: PhantomData<Gc<'gc, ()>>,
}
impl<'gc> Clone for Value<'gc> {
#[inline]
fn clone(&self) -> Self {
Self {
raw: self.raw.clone(),
_marker: PhantomData,
}
}
}
unsafe impl<'gc> Collect for Value<'gc> {
fn trace(&self, cc: &gc_arena::Collection) {
let Some(tag) = self.raw.tag() else { return };
let (neg, val) = tag.neg_val();
if !neg {
return; // inline values hold no GC pointers
}
// Negative tags are heap pointers — reconstruct the Gc and trace it.
unsafe {
match val {
1 => self.load_gc::<i64>().trace(cc),
2 => self.load_gc::<NixString>().trace(cc),
3 => self.load_gc::<SmallAttrSet<'gc>>().trace(cc),
4 => self.load_gc::<AttrSet<'gc>>().trace(cc),
5 => self.load_gc::<Box<[Value<'gc>]>>().trace(cc),
_ => debug_assert!(false, "invalid negative tag value: {val}"),
}
}
}
fn needs_trace() -> bool
where
Self: Sized,
{
true
}
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
impl<'gc> Value<'gc> {
#[inline(always)]
fn mk_tag(neg: bool, val: u8) -> RawTag {
debug_assert!((1..=7).contains(&val));
// Safety: val is asserted to be in 1..=7.
unsafe { RawTag::new_unchecked(neg, val) }
}
#[inline(always)]
fn from_raw_value(rv: RawValue) -> Self {
Self {
raw: RawBox::from_value(rv),
_marker: PhantomData,
}
}
/// Store a GC pointer with the given (negative) tag value.
#[inline(always)]
fn store_gc<T>(tag_val: u8, gc: Gc<'gc, T>) -> Self {
let ptr = Gc::as_ptr(gc);
Self::from_raw_value(RawValue::store(Self::mk_tag(true, tag_val), ptr))
}
/// Load a GC pointer from a value with a negative tag.
///
/// # Safety
///
/// The value must actually store a `Gc<'gc, T>` with the matching type.
#[inline(always)]
unsafe fn load_gc<T>(&self) -> Gc<'gc, T> {
unsafe {
let rv = self.raw.value().unwrap_unchecked();
let ptr: *const T = <*const T as RawStore>::from_val(rv);
Gc::from_ptr(ptr)
}
}
/// Returns the `(negative, val)` tag, or `None` for a float.
#[inline(always)]
fn tag_nv(&self) -> Option<(bool, u8)> {
self.raw.tag().map(|t| t.neg_val())
}
}
// ---------------------------------------------------------------------------
// Constructors
// ---------------------------------------------------------------------------
impl<'gc> Value<'gc> {
#[inline]
pub(crate) fn new_float(val: f64) -> Self {
Self {
raw: RawBox::from_float(val),
_marker: PhantomData,
}
}
#[inline]
pub(crate) fn new_smi(val: i32) -> Self {
Self::from_raw_value(RawValue::store(Self::mk_tag(TAG_SMI.0, TAG_SMI.1), val))
}
#[inline]
pub(crate) fn new_int(gc: Gc<'gc, i64>) -> Self {
Self::store_gc(TAG_BIG_INT.1, gc)
}
#[inline]
pub(crate) fn new_bool(val: bool) -> Self {
Self::from_raw_value(RawValue::store(
Self::mk_tag(TAG_BOOL.0, TAG_BOOL.1),
val,
))
}
#[inline]
pub(crate) fn new_null() -> Self {
Self::from_raw_value(RawValue::empty(Self::mk_tag(TAG_NULL.0, TAG_NULL.1)))
}
#[inline]
pub(crate) fn new_small_string(id: SmallStringId) -> Self {
Self::from_raw_value(RawValue::store(
Self::mk_tag(TAG_SMALL_STRING.0, TAG_SMALL_STRING.1),
id.0,
))
}
#[inline]
pub(crate) fn new_string(gc: Gc<'gc, NixString>) -> Self {
Self::store_gc(TAG_STRING.1, gc)
}
#[inline]
pub(crate) fn new_small_attrs(gc: Gc<'gc, SmallAttrSet<'gc>>) -> Self {
Self::store_gc(TAG_SMALL_ATTRS.1, gc)
}
#[inline]
pub(crate) fn new_attrs(gc: Gc<'gc, AttrSet<'gc>>) -> Self {
Self::store_gc(TAG_ATTRS.1, gc)
}
#[inline]
pub(crate) fn new_list(gc: Gc<'gc, Box<[Value<'gc>]>>) -> Self {
Self::store_gc(TAG_LIST.1, gc)
}
}
// ---------------------------------------------------------------------------
// Type checks
// ---------------------------------------------------------------------------
impl<'gc> Value<'gc> {
#[inline]
pub(crate) fn is_float(&self) -> bool {
self.raw.is_float()
}
#[inline]
pub(crate) fn is_smi(&self) -> bool {
self.tag_nv() == Some(TAG_SMI)
}
#[inline]
pub(crate) fn is_big_int(&self) -> bool {
self.tag_nv() == Some(TAG_BIG_INT)
}
/// True for float, small int, or big int.
#[inline]
pub(crate) fn is_number(&self) -> bool {
match self.tag_nv() {
None => true,
Some(TAG_SMI) | Some(TAG_BIG_INT) => true,
_ => false,
}
}
#[inline]
pub(crate) fn is_bool(&self) -> bool {
self.tag_nv() == Some(TAG_BOOL)
}
#[inline]
pub(crate) fn is_null(&self) -> bool {
self.tag_nv() == Some(TAG_NULL)
}
#[inline]
pub(crate) fn is_small_string(&self) -> bool {
self.tag_nv() == Some(TAG_SMALL_STRING)
}
#[inline]
pub(crate) fn is_heap_string(&self) -> bool {
self.tag_nv() == Some(TAG_STRING)
}
/// True for small string or heap string.
#[inline]
pub(crate) fn is_string(&self) -> bool {
matches!(self.tag_nv(), Some(TAG_SMALL_STRING | TAG_STRING))
}
#[inline]
pub(crate) fn is_small_attrs(&self) -> bool {
self.tag_nv() == Some(TAG_SMALL_ATTRS)
}
#[inline]
pub(crate) fn is_heap_attrs(&self) -> bool {
self.tag_nv() == Some(TAG_ATTRS)
}
/// True for small or heap attr set.
#[inline]
pub(crate) fn is_attrs(&self) -> bool {
matches!(self.tag_nv(), Some(TAG_SMALL_ATTRS | TAG_ATTRS))
}
#[inline]
pub(crate) fn is_list(&self) -> bool {
self.tag_nv() == Some(TAG_LIST)
}
}
// ---------------------------------------------------------------------------
// Accessors
// ---------------------------------------------------------------------------
impl<'gc> Value<'gc> {
#[inline]
pub(crate) fn as_float(&self) -> Option<f64> {
self.raw.float().copied()
}
#[inline]
pub(crate) fn as_smi(&self) -> Option<i32> {
if self.is_smi() {
Some(unsafe {
let rv = self.raw.value().unwrap_unchecked();
<i32 as RawStore>::from_val(rv)
})
} else {
None
}
}
#[inline]
pub(crate) fn as_big_int(&self) -> Option<Gc<'gc, i64>> {
if self.is_big_int() {
Some(unsafe { self.load_gc() })
} else {
None
}
}
/// Read the integer value as `i64` regardless of smi/big-int representation.
#[inline]
pub(crate) fn as_i64(&self) -> Option<i64> {
match self.tag_nv() {
Some(TAG_SMI) => Some(unsafe {
let rv = self.raw.value().unwrap_unchecked();
<i32 as RawStore>::from_val(rv) as i64
}),
Some(TAG_BIG_INT) => Some(unsafe { *self.load_gc::<i64>() }),
_ => None,
}
}
#[inline]
pub(crate) fn as_bool(&self) -> Option<bool> {
if self.is_bool() {
Some(unsafe {
let rv = self.raw.value().unwrap_unchecked();
<bool as RawStore>::from_val(rv)
})
} else {
None
}
}
#[inline]
pub(crate) fn as_small_string(&self) -> Option<SmallStringId> {
if self.is_small_string() {
Some(SmallStringId(unsafe {
let rv = self.raw.value().unwrap_unchecked();
<u32 as RawStore>::from_val(rv)
}))
} else {
None
}
}
#[inline]
pub(crate) fn as_heap_string(&self) -> Option<Gc<'gc, NixString>> {
if self.is_heap_string() {
Some(unsafe { self.load_gc() })
} else {
None
}
}
#[inline]
pub(crate) fn as_small_attr_set(&self) -> Option<Gc<'gc, SmallAttrSet<'gc>>> {
if self.is_small_attrs() {
Some(unsafe { self.load_gc() })
} else {
None
}
}
#[inline]
pub(crate) fn as_heap_attr_set(&self) -> Option<Gc<'gc, AttrSet<'gc>>> {
if self.is_heap_attrs() {
Some(unsafe { self.load_gc() })
} else {
None
}
}
#[inline]
pub(crate) fn as_list(&self) -> Option<Gc<'gc, Box<[Value<'gc>]>>> {
if self.is_list() {
Some(unsafe { self.load_gc() })
} else {
None
}
}
}
// ---------------------------------------------------------------------------
// Debug
// ---------------------------------------------------------------------------
impl fmt::Debug for Value<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.tag_nv() {
None => {
let v = self.raw.float().unwrap();
write!(f, "Float({v:?})")
}
Some(TAG_SMI) => write!(f, "SmallInt({:?})", self.as_smi().unwrap()),
Some(TAG_BOOL) => write!(f, "Bool({:?})", self.as_bool().unwrap()),
Some(TAG_NULL) => write!(f, "Null"),
Some(TAG_SMALL_STRING) => {
write!(f, "SmallString({:?})", self.as_small_string().unwrap())
}
Some(TAG_BIG_INT) => write!(f, "BigInt(Gc<..>)"),
Some(TAG_STRING) => write!(f, "String(Gc<..>)"),
Some(TAG_SMALL_ATTRS) => write!(f, "SmallAttrSet(Gc<..>)"),
Some(TAG_ATTRS) => write!(f, "AttrSet(Gc<..>)"),
Some(TAG_LIST) => write!(f, "List(Gc<..>)"),
Some((neg, val)) => write!(f, "Unknown(neg={neg}, val={val})"),
}
}
}
// ===========================================================================
// Supporting types
// ===========================================================================
// TODO: size?
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Collect)]
#[collect(require_static)]
pub(crate) struct SmallStringId(u32);
/// Heap-allocated Nix string.
///
/// Stored on the GC heap via `Gc<'gc, NixString>`. The string data itself
/// lives in a standard `Box<str>` owned by this struct; the GC only manages
/// the outer allocation.
#[derive(Collect)]
#[collect(require_static)]
pub(crate) struct NixString {
data: Box<str>,
// TODO: string context for derivation dependency tracking
}
impl NixString {
pub(crate) fn new(s: impl Into<Box<str>>) -> Self {
Self { data: s.into() }
}
pub(crate) fn as_str(&self) -> &str {
&self.data
}
}
impl fmt::Debug for NixString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.data, f)
}
}
/// Fixed-size attribute set (up to 8 entries).
#[derive(Collect)]
#[collect(no_drop)]
pub(crate) struct SmallAttrSet<'gc> {
// TODO: proper key storage, length tracking, and lookup
inner: [Value<'gc>; 8],
}
/// Hash-table-backed attribute set.
pub(crate) struct AttrSet<'gc> {
inner: HashTable<AttrSetEntry<'gc>>,
}
unsafe impl<'gc> Collect for AttrSet<'gc> {
fn trace(&self, cc: &gc_arena::Collection) {
for entry in self.inner.iter() {
Collect::trace(&entry.key, cc);
Collect::trace(&entry.value, cc);
}
}
fn needs_trace() -> bool
where
Self: Sized,
{
true
}
}
#[derive(Collect)]
#[collect(no_drop)]
struct AttrSetEntry<'gc> {
key: AttrKey<'gc>,
value: Value<'gc>,
}
#[derive(Collect)]
#[collect(no_drop)]
pub(crate) enum AttrKey<'gc> {
Small(SmallStringId),
Large(Gc<'gc, str>),
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,4 @@
use fix::value::Value;
use nix_js::value::Value;
use crate::utils::{eval, eval_result};
@@ -1,6 +1,6 @@
use std::collections::BTreeMap;
use fix::value::{AttrSet, List, Value};
use nix_js::value::{AttrSet, List, Value};
use crate::utils::eval;
@@ -1,4 +1,4 @@
use fix::value::Value;
use nix_js::value::Value;
use crate::utils::eval_result;
@@ -1,4 +1,4 @@
use fix::value::Value;
use nix_js::value::Value;
use crate::utils::{eval_deep, eval_deep_result};
File renamed without changes.
@@ -1,4 +1,4 @@
use fix::value::{List, Value};
use nix_js::value::{List, Value};
use crate::utils::{eval, eval_result};
@@ -1,4 +1,4 @@
use fix::value::Value;
use nix_js::value::Value;
use crate::utils::{eval, eval_result};
@@ -1,6 +1,6 @@
use fix::error::Source;
use fix::runtime::Runtime;
use fix::value::Value;
use nix_js::context::Context;
use nix_js::error::Source;
use nix_js::value::Value;
use crate::utils::{eval, eval_result};
@@ -97,7 +97,7 @@ fn import_with_complex_dependency_graph() {
#[test_log::test]
fn path_with_file() {
let mut ctx = Runtime::new().unwrap();
let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let test_file = temp_dir.path().join("test.txt");
std::fs::write(&test_file, "Hello, World!").unwrap();
@@ -107,7 +107,7 @@ fn path_with_file() {
// Should return a store path string
if let Value::String(store_path) = result {
assert!(store_path.starts_with("/nix/store"));
assert!(store_path.starts_with(ctx.get_store_dir()));
assert!(store_path.contains("test.txt"));
} else {
panic!("Expected string, got {:?}", result);
@@ -136,7 +136,7 @@ fn path_with_custom_name() {
#[test_log::test]
fn path_with_directory_recursive() {
let mut ctx = Runtime::new().unwrap();
let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let test_dir = temp_dir.path().join("mydir");
std::fs::create_dir_all(&test_dir).unwrap();
@@ -150,7 +150,7 @@ fn path_with_directory_recursive() {
let result = ctx.eval(Source::new_eval(expr).unwrap()).unwrap();
if let Value::String(store_path) = result {
assert!(store_path.starts_with("/nix/store"));
assert!(store_path.starts_with(ctx.get_store_dir()));
assert!(store_path.contains("mydir"));
} else {
panic!("Expected string, got {:?}", result);
@@ -159,7 +159,7 @@ fn path_with_directory_recursive() {
#[test_log::test]
fn path_flat_with_file() {
let mut ctx = Runtime::new().unwrap();
let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let test_file = temp_dir.path().join("flat.txt");
std::fs::write(&test_file, "Flat content").unwrap();
@@ -171,7 +171,7 @@ fn path_flat_with_file() {
let result = ctx.eval(Source::new_eval(expr).unwrap()).unwrap();
if let Value::String(store_path) = result {
assert!(store_path.starts_with("/nix/store"));
assert!(store_path.starts_with(ctx.get_store_dir()));
} else {
panic!("Expected string, got {:?}", result);
}
@@ -2,9 +2,9 @@
use std::path::PathBuf;
use fix::error::Source;
use fix::runtime::Runtime;
use fix::value::Value;
use nix_js::context::Context;
use nix_js::error::Source;
use nix_js::value::Value;
fn get_lang_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/tests/lang")
@@ -16,9 +16,9 @@ fn eval_file(name: &str) -> Result<(Value, Source), String> {
let expr = format!(r#"import "{}""#, nix_path.display());
let mut ctx = Runtime::new().map_err(|e| e.to_string())?;
let mut ctx = Context::new().map_err(|e| e.to_string())?;
let source = Source {
ty: fix::error::SourceType::File(nix_path.into()),
ty: nix_js::error::SourceType::File(nix_path.into()),
src: expr.into(),
};
ctx.eval_deep(source.clone())
@@ -247,7 +247,7 @@ eval_fail_test!(fail_abort);
eval_fail_test!(fail_addDrvOutputDependencies_empty_context);
eval_fail_test!(fail_addDrvOutputDependencies_multi_elem_context);
eval_fail_test!(fail_addDrvOutputDependencies_wrong_element_kind);
eval_fail_test!(fail_addErrorRuntime_example);
eval_fail_test!(fail_addErrorContext_example);
eval_fail_test!(fail_assert);
eval_fail_test!(fail_assert_equal_attrs_names);
eval_fail_test!(fail_assert_equal_attrs_names_2);
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 519 files, more files were not shown because too many files have changed in this diff. Show more