Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1083b8fdfa
|
@@ -16,6 +16,9 @@ vim.lsp.config("rust_analyzer", {
|
|||||||
settings = {
|
settings = {
|
||||||
["rust-analyzer"] = {
|
["rust-analyzer"] = {
|
||||||
cargo = {
|
cargo = {
|
||||||
|
features = {
|
||||||
|
"inspector"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+969
-101
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -1,8 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = [
|
members = [
|
||||||
"fix",
|
"nix-js"
|
||||||
"boxing",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.profiling]
|
[profile.profiling]
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pub mod nan;
|
|
||||||
mod utils;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
pub trait ArrayExt<const LEN: usize> {
|
|
||||||
type Elem;
|
|
||||||
|
|
||||||
fn truncate_to<const M: usize>(self) -> [Self::Elem; M];
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Default + Copy, const N: usize> ArrayExt<N> for [T; N] {
|
|
||||||
type Elem = T;
|
|
||||||
|
|
||||||
fn truncate_to<const M: usize>(self) -> [Self::Elem; M] {
|
|
||||||
let copy_len = usize::min(N, M);
|
|
||||||
let mut out = [T::default(); M];
|
|
||||||
out[0..copy_len].copy_from_slice(&self[0..copy_len]);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,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())
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -8,11 +8,11 @@
|
|||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1773471952,
|
"lastModified": 1770966612,
|
||||||
"narHash": "sha256-kIRggXyT8RzijtfvyRIzj+zIDWM2fnCp8t0X4BkkTVc=",
|
"narHash": "sha256-S6k14z/JsDwX6zZyLucDBTOe/9RsvxH9GTUxHn2o4vc=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "a1b770adbc3f6c27485d03b90462ec414d4e1ce5",
|
"rev": "e90d48dcfaebac7ea7a5687888a2d0733be26343",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -37,11 +37,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1773282481,
|
"lastModified": 1770841267,
|
||||||
"narHash": "sha256-b/GV2ysM8mKHhinse2wz+uP37epUrSE+sAKXy/xvBY4=",
|
"narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "fe416aaedd397cacb33a610b33d60ff2b431b127",
|
"rev": "ec7c70d12ce2fc37cb92aff673dcdca89d187bae",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -61,11 +61,11 @@
|
|||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1773326183,
|
"lastModified": 1770934477,
|
||||||
"narHash": "sha256-tj3piRd9RnnP36HwHmQD4O4XZeowsH/rvMeyp9Pmot0=",
|
"narHash": "sha256-GX0cINHhhzUbQHyDYN2Mc+ovb6Sx/4yrF95VVou9aW4=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "6254616e97f358e67b70dfc0463687f5f7911c1a",
|
"rev": "931cd553be123b11db1435ac7ea5657e62e5e601",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fix"
|
name = "nix-js"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mimalloc = "0.1"
|
mimalloc = "0.1"
|
||||||
|
|
||||||
tokio = { version = "1.41", features = [
|
tokio = { version = "1.41", features = ["rt-multi-thread", "sync", "net", "io-util"] }
|
||||||
"rt-multi-thread",
|
nix-compat = { git = "https://git.snix.dev/snix/snix.git", version = "0.1.0", features = ["wire", "async"] }
|
||||||
"sync",
|
|
||||||
"net",
|
|
||||||
"io-util",
|
|
||||||
] }
|
|
||||||
nix-compat = { git = "https://git.snix.dev/snix/snix.git", version = "0.1.0", features = [
|
|
||||||
"wire",
|
|
||||||
"async",
|
|
||||||
] }
|
|
||||||
|
|
||||||
# REPL
|
# REPL
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
@@ -34,18 +26,17 @@ miette = { version = "7.4", features = ["fancy"] }
|
|||||||
|
|
||||||
hashbrown = "0.16"
|
hashbrown = "0.16"
|
||||||
string-interner = "0.19"
|
string-interner = "0.19"
|
||||||
bumpalo = { version = "3.20", features = [
|
bumpalo = { version = "3.20", features = ["allocator-api2", "boxed", "collections"] }
|
||||||
"allocator-api2",
|
|
||||||
"boxed",
|
|
||||||
"collections",
|
|
||||||
] }
|
|
||||||
|
|
||||||
rust-embed = "8.11"
|
rust-embed="8.11"
|
||||||
|
|
||||||
itertools = "0.14"
|
itertools = "0.14"
|
||||||
|
|
||||||
regex = "1.11"
|
regex = "1.11"
|
||||||
|
|
||||||
|
deno_core = "0.385"
|
||||||
|
deno_error = "0.7"
|
||||||
|
|
||||||
nix-nar = "0.3"
|
nix-nar = "0.3"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
sha1 = "0.10"
|
sha1 = "0.10"
|
||||||
@@ -54,10 +45,7 @@ hex = "0.4"
|
|||||||
|
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
|
||||||
reqwest = { version = "0.13", features = [
|
reqwest = { version = "0.13", features = ["blocking", "rustls"], default-features = false }
|
||||||
"blocking",
|
|
||||||
"rustls",
|
|
||||||
], default-features = false }
|
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
flate2 = "1.0"
|
flate2 = "1.0"
|
||||||
xz2 = "0.1"
|
xz2 = "0.1"
|
||||||
@@ -77,17 +65,24 @@ ere = "0.2.4"
|
|||||||
num_enum = "0.7.5"
|
num_enum = "0.7.5"
|
||||||
tap = "1.0.1"
|
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"
|
ghost-cell = "0.2"
|
||||||
colored = "3.1"
|
colored = "3.1"
|
||||||
boxing = { path = "../boxing" }
|
boxing = "0.1"
|
||||||
sealed = "0.6"
|
gc-arena = { version = "0.5.3", features = ["allocator-api2"] }
|
||||||
small-map = "0.1"
|
allocator-api2 = "0.4.0"
|
||||||
smallvec = "1.15"
|
smallvec = "1.15.1"
|
||||||
|
|
||||||
[dependencies.gc-arena]
|
[features]
|
||||||
git = "https://github.com/kyren/gc-arena"
|
inspector = ["dep:fastwebsockets", "dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:http", "dep:uuid"]
|
||||||
rev = "75671ae03f53718357b741ed4027560f14e90836"
|
prof = []
|
||||||
features = ["allocator-api2", "hashbrown", "smallvec"]
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.8", features = ["html_reports"] }
|
criterion = { version = "0.8", features = ["html_reports"] }
|
||||||
@@ -104,3 +99,7 @@ harness = false
|
|||||||
[[bench]]
|
[[bench]]
|
||||||
name = "thunk_scope"
|
name = "thunk_scope"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "compile_time"
|
||||||
|
harness = false
|
||||||
@@ -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);
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/dist
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import * as esbuild from "esbuild";
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: ["src/index.ts"],
|
||||||
|
outfile: "dist/runtime.js",
|
||||||
|
bundle: true,
|
||||||
|
// minify: true,
|
||||||
|
});
|
||||||
@@ -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/**/*"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
Generated
+1967
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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");
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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");
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -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));
|
||||||
|
};
|
||||||
@@ -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)));
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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}`);
|
||||||
|
};
|
||||||
@@ -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}'`);
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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)}`);
|
||||||
|
};
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -1,101 +1,101 @@
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use gc_arena::Collect;
|
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
use rnix::TextRange;
|
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 struct Bytecode {
|
||||||
pub code: Box<[u8]>,
|
pub code: Box<[u8]>,
|
||||||
pub current_dir: String,
|
pub current_dir: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait BytecodeContext {
|
pub(crate) trait BytecodeContext {
|
||||||
fn intern_string(&mut self, s: &str) -> StringId;
|
fn intern_string(&mut self, s: &str) -> u32;
|
||||||
fn register_span(&mut self, range: TextRange) -> u32;
|
fn intern_constant(&mut self, c: Constant) -> u32;
|
||||||
fn get_code(&self) -> &[u8];
|
fn register_span(&self, range: TextRange) -> u32;
|
||||||
fn get_code_mut(&mut self) -> &mut Vec<u8>;
|
fn get_sym(&self, id: SymId) -> &str;
|
||||||
|
fn get_current_dir(&self) -> &Path;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Clone, Copy, TryFromPrimitive)]
|
#[derive(Clone, Copy, TryFromPrimitive)]
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[allow(clippy::enum_variant_names)]
|
||||||
pub enum Op {
|
pub enum Op {
|
||||||
PushSmi,
|
PushConst = 0x01,
|
||||||
PushBigInt,
|
PushString = 0x02,
|
||||||
PushFloat,
|
PushNull = 0x03,
|
||||||
PushString,
|
PushTrue = 0x04,
|
||||||
PushNull,
|
PushFalse = 0x05,
|
||||||
PushTrue,
|
|
||||||
PushFalse,
|
|
||||||
|
|
||||||
LoadLocal,
|
LoadLocal = 0x06,
|
||||||
LoadOuter,
|
LoadOuter = 0x07,
|
||||||
StoreLocal,
|
StoreLocal = 0x08,
|
||||||
AllocLocals,
|
AllocLocals = 0x09,
|
||||||
|
|
||||||
MakeThunk,
|
MakeThunk = 0x0A,
|
||||||
MakeClosure,
|
MakeClosure = 0x0B,
|
||||||
MakePatternClosure,
|
MakePatternClosure = 0x0C,
|
||||||
|
|
||||||
Call,
|
Call = 0x0D,
|
||||||
CallNoSpan,
|
CallNoSpan = 0x0E,
|
||||||
|
|
||||||
MakeAttrs,
|
MakeAttrs = 0x0F,
|
||||||
MakeAttrsDyn,
|
MakeAttrsDyn = 0x10,
|
||||||
MakeEmptyAttrs,
|
MakeEmptyAttrs = 0x11,
|
||||||
Select,
|
Select = 0x12,
|
||||||
SelectDefault,
|
SelectDefault = 0x13,
|
||||||
HasAttr,
|
HasAttr = 0x14,
|
||||||
|
|
||||||
MakeList,
|
MakeList = 0x15,
|
||||||
|
|
||||||
OpAdd,
|
OpAdd = 0x16,
|
||||||
OpSub,
|
OpSub = 0x17,
|
||||||
OpMul,
|
OpMul = 0x18,
|
||||||
OpDiv,
|
OpDiv = 0x19,
|
||||||
OpEq,
|
OpEq = 0x20,
|
||||||
OpNeq,
|
OpNeq = 0x21,
|
||||||
OpLt,
|
OpLt = 0x22,
|
||||||
OpGt,
|
OpGt = 0x23,
|
||||||
OpLeq,
|
OpLeq = 0x24,
|
||||||
OpGeq,
|
OpGeq = 0x25,
|
||||||
OpConcat,
|
OpConcat = 0x26,
|
||||||
OpUpdate,
|
OpUpdate = 0x27,
|
||||||
|
|
||||||
OpNeg,
|
OpNeg = 0x28,
|
||||||
OpNot,
|
OpNot = 0x29,
|
||||||
|
|
||||||
ForceBool,
|
ForceBool = 0x30,
|
||||||
JumpIfFalse,
|
JumpIfFalse = 0x31,
|
||||||
JumpIfTrue,
|
JumpIfTrue = 0x32,
|
||||||
Jump,
|
Jump = 0x33,
|
||||||
|
|
||||||
ConcatStrings,
|
ConcatStrings = 0x34,
|
||||||
ResolvePath,
|
ResolvePath = 0x35,
|
||||||
|
|
||||||
Assert,
|
Assert = 0x36,
|
||||||
|
|
||||||
PushWith,
|
PushWith = 0x37,
|
||||||
PopWith,
|
PopWith = 0x38,
|
||||||
WithLookup,
|
WithLookup = 0x39,
|
||||||
|
|
||||||
LoadBuiltins,
|
LoadBuiltins = 0x40,
|
||||||
LoadBuiltin,
|
LoadBuiltin = 0x41,
|
||||||
|
|
||||||
MkPos,
|
MkPos = 0x43,
|
||||||
|
|
||||||
LoadReplBinding,
|
LoadReplBinding = 0x44,
|
||||||
LoadScopedBinding,
|
LoadScopedBinding = 0x45,
|
||||||
|
|
||||||
Return,
|
Return = 0x46,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ScopeInfo {
|
struct ScopeInfo {
|
||||||
@@ -106,78 +106,71 @@ struct ScopeInfo {
|
|||||||
|
|
||||||
struct BytecodeEmitter<'a, Ctx: BytecodeContext> {
|
struct BytecodeEmitter<'a, Ctx: BytecodeContext> {
|
||||||
ctx: &'a mut Ctx,
|
ctx: &'a mut Ctx,
|
||||||
|
code: Vec<u8>,
|
||||||
scope_stack: Vec<ScopeInfo>,
|
scope_stack: Vec<ScopeInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr {
|
pub(crate) fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> Bytecode {
|
||||||
let ip = ctx.get_code().len();
|
let current_dir = ctx.get_current_dir().to_string_lossy().to_string();
|
||||||
let mut emitter = BytecodeEmitter::new(ctx);
|
let mut emitter = BytecodeEmitter::new(ctx);
|
||||||
emitter.emit_toplevel(ir);
|
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> {
|
impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||||
fn new(ctx: &'a mut Ctx) -> Self {
|
fn new(ctx: &'a mut Ctx) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ctx,
|
ctx,
|
||||||
|
code: Vec::with_capacity(4096),
|
||||||
scope_stack: Vec::with_capacity(32),
|
scope_stack: Vec::with_capacity(32),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn emit_op(&mut self, op: Op) {
|
fn emit_op(&mut self, op: Op) {
|
||||||
self.ctx.get_code_mut().push(op as u8);
|
self.code.push(op as u8);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn emit_u8(&mut self, val: u8) {
|
fn emit_u8(&mut self, val: u8) {
|
||||||
self.ctx.get_code_mut().push(val);
|
self.code.push(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn emit_u16(&mut self, val: u16) {
|
fn emit_u16(&mut self, val: u16) {
|
||||||
self.ctx
|
self.code.extend_from_slice(&val.to_le_bytes());
|
||||||
.get_code_mut()
|
|
||||||
.extend_from_slice(&val.to_le_bytes());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn emit_u32(&mut self, val: u32) {
|
fn emit_u32(&mut self, val: u32) {
|
||||||
self.ctx
|
self.code.extend_from_slice(&val.to_le_bytes());
|
||||||
.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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn emit_i32_placeholder(&mut self) -> usize {
|
fn emit_i32_placeholder(&mut self) -> usize {
|
||||||
let offset = self.ctx.get_code_mut().len();
|
let offset = self.code.len();
|
||||||
self.ctx.get_code_mut().extend_from_slice(&[0u8; 4]);
|
self.code.extend_from_slice(&[0u8; 4]);
|
||||||
offset
|
offset
|
||||||
}
|
}
|
||||||
#[inline]
|
#[inline]
|
||||||
fn patch_i32(&mut self, offset: usize, val: i32) {
|
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]
|
#[inline]
|
||||||
@@ -188,18 +181,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn patch_jump_target(&mut self, placeholder_offset: usize) {
|
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;
|
let relative_offset = (current_pos as i32) - (placeholder_offset as i32) - 4;
|
||||||
self.patch_i32(placeholder_offset, relative_offset);
|
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 {
|
fn current_depth(&self) -> u16 {
|
||||||
self.scope_stack.last().map_or(0, |s| s.depth)
|
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<'_>)]) {
|
fn emit_scope_thunks(&mut self, thunks: &[(ThunkId, RawIrRef<'_>)]) {
|
||||||
for &(id, inner) in thunks {
|
for &(id, inner) in thunks {
|
||||||
let label = format!("e{}", id.0);
|
let label = format!("e{}", id.0);
|
||||||
let label_idx = self.ctx.intern_string(&label);
|
let label_idx = self.ctx.intern_string(&label);
|
||||||
|
|
||||||
let skip_patch = self.emit_jump_placeholder();
|
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_expr(inner);
|
||||||
self.emit_op(Op::Return);
|
self.emit_op(Op::Return);
|
||||||
self.patch_jump_target(skip_patch);
|
self.patch_jump_target(skip_patch);
|
||||||
self.emit_op(Op::MakeThunk);
|
self.emit_op(Op::MakeThunk);
|
||||||
self.emit_u32(entry_point);
|
self.emit_u32(entry_point);
|
||||||
self.emit_str_id(label_idx);
|
self.emit_u32(label_idx);
|
||||||
let (_, local_idx) = self.resolve_thunk(id);
|
let (_, local_idx) = self.resolve_thunk(id);
|
||||||
self.emit_op(Op::StoreLocal);
|
self.emit_op(Op::StoreLocal);
|
||||||
self.emit_u32(local_idx);
|
self.emit_u32(local_idx);
|
||||||
@@ -433,17 +450,14 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
fn emit_expr(&mut self, ir: RawIrRef<'_>) {
|
fn emit_expr(&mut self, ir: RawIrRef<'_>) {
|
||||||
match ir.deref() {
|
match ir.deref() {
|
||||||
&Ir::Int(x) => {
|
&Ir::Int(x) => {
|
||||||
if x <= i32::MAX as i64 {
|
let idx = self.ctx.intern_constant(Constant::Int(x));
|
||||||
self.emit_op(Op::PushSmi);
|
self.emit_op(Op::PushConst);
|
||||||
self.emit_i32(x as i32);
|
self.emit_u32(idx);
|
||||||
} else {
|
|
||||||
self.emit_op(Op::PushBigInt);
|
|
||||||
self.emit_i64(x);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&Ir::Float(x) => {
|
&Ir::Float(x) => {
|
||||||
self.emit_op(Op::PushFloat);
|
let idx = self.ctx.intern_constant(Constant::Float(x.to_bits()));
|
||||||
self.emit_f64(x);
|
self.emit_op(Op::PushConst);
|
||||||
|
self.emit_u32(idx);
|
||||||
}
|
}
|
||||||
&Ir::Bool(true) => self.emit_op(Op::PushTrue),
|
&Ir::Bool(true) => self.emit_op(Op::PushTrue),
|
||||||
&Ir::Bool(false) => self.emit_op(Op::PushFalse),
|
&Ir::Bool(false) => self.emit_op(Op::PushFalse),
|
||||||
@@ -451,7 +465,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
Ir::Str(s) => {
|
Ir::Str(s) => {
|
||||||
let idx = self.ctx.intern_string(s.deref());
|
let idx = self.ctx.intern_string(s.deref());
|
||||||
self.emit_op(Op::PushString);
|
self.emit_op(Op::PushString);
|
||||||
self.emit_str_id(idx);
|
self.emit_u32(idx);
|
||||||
}
|
}
|
||||||
&Ir::Path(p) => {
|
&Ir::Path(p) => {
|
||||||
self.emit_expr(p);
|
self.emit_expr(p);
|
||||||
@@ -463,20 +477,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
|
|
||||||
self.emit_op(Op::JumpIfFalse);
|
self.emit_op(Op::JumpIfFalse);
|
||||||
let else_placeholder = self.emit_i32_placeholder();
|
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_expr(consq);
|
||||||
|
|
||||||
self.emit_op(Op::Jump);
|
self.emit_op(Op::Jump);
|
||||||
let end_placeholder = self.emit_i32_placeholder();
|
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);
|
let else_offset = (after_jump as i32) - (after_jif as i32);
|
||||||
self.patch_i32(else_placeholder, else_offset);
|
self.patch_i32(else_placeholder, else_offset);
|
||||||
|
|
||||||
self.emit_expr(alter);
|
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);
|
self.patch_i32(end_placeholder, end_offset);
|
||||||
}
|
}
|
||||||
&Ir::BinOp { lhs, rhs, kind } => {
|
&Ir::BinOp { lhs, rhs, kind } => {
|
||||||
@@ -540,8 +554,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
self.emit_op(Op::LoadBuiltins);
|
self.emit_op(Op::LoadBuiltins);
|
||||||
}
|
}
|
||||||
&Ir::Builtin(name) => {
|
&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_op(Op::LoadBuiltin);
|
||||||
self.emit_u32(name.0.to_usize() as u32);
|
self.emit_u32(idx);
|
||||||
}
|
}
|
||||||
&Ir::ConcatStrings {
|
&Ir::ConcatStrings {
|
||||||
ref parts,
|
ref parts,
|
||||||
@@ -568,7 +584,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
self.emit_expr(*assertion);
|
self.emit_expr(*assertion);
|
||||||
self.emit_expr(*expr);
|
self.emit_expr(*expr);
|
||||||
self.emit_op(Op::Assert);
|
self.emit_op(Op::Assert);
|
||||||
self.emit_str_id(raw_idx);
|
self.emit_u32(raw_idx);
|
||||||
self.emit_u32(span_id);
|
self.emit_u32(span_id);
|
||||||
}
|
}
|
||||||
&Ir::CurPos(span) => {
|
&Ir::CurPos(span) => {
|
||||||
@@ -577,12 +593,16 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
self.emit_u32(span_id);
|
self.emit_u32(span_id);
|
||||||
}
|
}
|
||||||
&Ir::ReplBinding(name) => {
|
&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_op(Op::LoadReplBinding);
|
||||||
self.emit_str_id(name);
|
self.emit_u32(idx);
|
||||||
}
|
}
|
||||||
&Ir::ScopedImportBinding(name) => {
|
&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_op(Op::LoadScopedBinding);
|
||||||
self.emit_str_id(name);
|
self.emit_u32(idx);
|
||||||
}
|
}
|
||||||
&Ir::With {
|
&Ir::With {
|
||||||
namespace,
|
namespace,
|
||||||
@@ -592,8 +612,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
self.emit_with(namespace, body, thunks);
|
self.emit_with(namespace, body, thunks);
|
||||||
}
|
}
|
||||||
&Ir::WithLookup(name) => {
|
&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_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::ForceBool);
|
||||||
self.emit_op(Op::JumpIfFalse);
|
self.emit_op(Op::JumpIfFalse);
|
||||||
let skip_placeholder = self.emit_i32_placeholder();
|
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_expr(rhs);
|
||||||
self.emit_op(Op::ForceBool);
|
self.emit_op(Op::ForceBool);
|
||||||
self.emit_op(Op::Jump);
|
self.emit_op(Op::Jump);
|
||||||
let end_placeholder = self.emit_i32_placeholder();
|
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);
|
let false_offset = (after_jump as i32) - (after_jif as i32);
|
||||||
self.patch_i32(skip_placeholder, false_offset);
|
self.patch_i32(skip_placeholder, false_offset);
|
||||||
|
|
||||||
self.emit_op(Op::PushFalse);
|
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);
|
self.patch_i32(end_placeholder, end_offset);
|
||||||
}
|
}
|
||||||
Or => {
|
Or => {
|
||||||
@@ -627,20 +649,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
self.emit_op(Op::ForceBool);
|
self.emit_op(Op::ForceBool);
|
||||||
self.emit_op(Op::JumpIfTrue);
|
self.emit_op(Op::JumpIfTrue);
|
||||||
let skip_placeholder = self.emit_i32_placeholder();
|
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_expr(rhs);
|
||||||
self.emit_op(Op::ForceBool);
|
self.emit_op(Op::ForceBool);
|
||||||
self.emit_op(Op::Jump);
|
self.emit_op(Op::Jump);
|
||||||
let end_placeholder = self.emit_i32_placeholder();
|
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);
|
let true_offset = (after_jump as i32) - (after_jit as i32);
|
||||||
self.patch_i32(skip_placeholder, true_offset);
|
self.patch_i32(skip_placeholder, true_offset);
|
||||||
|
|
||||||
self.emit_op(Op::PushTrue);
|
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);
|
self.patch_i32(end_placeholder, end_offset);
|
||||||
}
|
}
|
||||||
Impl => {
|
Impl => {
|
||||||
@@ -648,20 +670,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
self.emit_op(Op::ForceBool);
|
self.emit_op(Op::ForceBool);
|
||||||
self.emit_op(Op::JumpIfFalse);
|
self.emit_op(Op::JumpIfFalse);
|
||||||
let skip_placeholder = self.emit_i32_placeholder();
|
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_expr(rhs);
|
||||||
self.emit_op(Op::ForceBool);
|
self.emit_op(Op::ForceBool);
|
||||||
self.emit_op(Op::Jump);
|
self.emit_op(Op::Jump);
|
||||||
let end_placeholder = self.emit_i32_placeholder();
|
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);
|
let true_offset = (after_jump as i32) - (after_jif as i32);
|
||||||
self.patch_i32(skip_placeholder, true_offset);
|
self.patch_i32(skip_placeholder, true_offset);
|
||||||
|
|
||||||
self.emit_op(Op::PushTrue);
|
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);
|
self.patch_i32(end_placeholder, end_offset);
|
||||||
}
|
}
|
||||||
PipeL => {
|
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 thunk_ids: Vec<ThunkId> = all_thunks.iter().map(|&(id, _)| id).collect();
|
||||||
|
|
||||||
let skip_patch = self.emit_jump_placeholder();
|
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.push_scope(true, Some(arg), &thunk_ids);
|
||||||
self.emit_scope_thunks(thunks);
|
self.emit_scope_thunks(thunks);
|
||||||
self.emit_expr(body);
|
self.emit_expr(body);
|
||||||
@@ -732,14 +754,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
self.emit_u8(if *ellipsis { 1 } else { 0 });
|
self.emit_u8(if *ellipsis { 1 } else { 0 });
|
||||||
|
|
||||||
for &(sym, _) in required.iter() {
|
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() {
|
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()) {
|
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);
|
let span_id = self.ctx.register_span(span);
|
||||||
self.emit_str_id(sym);
|
self.emit_u32(name_idx);
|
||||||
self.emit_u32(span_id);
|
self.emit_u32(span_id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -751,7 +779,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
|
|
||||||
fn emit_attrset(
|
fn emit_attrset(
|
||||||
&mut self,
|
&mut self,
|
||||||
stcs: &crate::ir::HashMap<'_, StringId, (RawIrRef<'_>, TextRange)>,
|
stcs: &crate::ir::HashMap<'_, SymId, (RawIrRef<'_>, TextRange)>,
|
||||||
dyns: &[(RawIrRef<'_>, RawIrRef<'_>, TextRange)],
|
dyns: &[(RawIrRef<'_>, RawIrRef<'_>, TextRange)],
|
||||||
) {
|
) {
|
||||||
if stcs.is_empty() && dyns.is_empty() {
|
if stcs.is_empty() && dyns.is_empty() {
|
||||||
@@ -761,35 +789,42 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
|
|
||||||
if !dyns.is_empty() {
|
if !dyns.is_empty() {
|
||||||
for (&sym, &(val, _)) in stcs.iter() {
|
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_op(Op::PushString);
|
||||||
self.emit_str_id(sym);
|
self.emit_u32(idx);
|
||||||
self.emit_expr(val);
|
self.emit_expr(val);
|
||||||
}
|
}
|
||||||
for (_, &(_, span)) in stcs.iter() {
|
for (_, &(_, span)) in stcs.iter() {
|
||||||
let span_id = self.ctx.register_span(span);
|
let span_id = self.ctx.register_span(span);
|
||||||
self.emit_op(Op::PushSmi);
|
let idx = self.ctx.intern_constant(Constant::Int(span_id as i64));
|
||||||
self.emit_u32(span_id);
|
self.emit_op(Op::PushConst);
|
||||||
|
self.emit_u32(idx);
|
||||||
}
|
}
|
||||||
for &(key, val, span) in dyns.iter() {
|
for &(key, val, span) in dyns.iter() {
|
||||||
self.emit_expr(key);
|
self.emit_expr(key);
|
||||||
self.emit_expr(val);
|
self.emit_expr(val);
|
||||||
let span_id = self.ctx.register_span(span);
|
let span_id = self.ctx.register_span(span);
|
||||||
self.emit_op(Op::PushSmi);
|
let idx = self.ctx.intern_constant(Constant::Int(span_id as i64));
|
||||||
self.emit_u32(span_id);
|
self.emit_op(Op::PushConst);
|
||||||
|
self.emit_u32(idx);
|
||||||
}
|
}
|
||||||
self.emit_op(Op::MakeAttrsDyn);
|
self.emit_op(Op::MakeAttrsDyn);
|
||||||
self.emit_u32(stcs.len() as u32);
|
self.emit_u32(stcs.len() as u32);
|
||||||
self.emit_u32(dyns.len() as u32);
|
self.emit_u32(dyns.len() as u32);
|
||||||
} else {
|
} else {
|
||||||
for (&sym, &(val, _)) in stcs.iter() {
|
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_op(Op::PushString);
|
||||||
self.emit_str_id(sym);
|
self.emit_u32(idx);
|
||||||
self.emit_expr(val);
|
self.emit_expr(val);
|
||||||
}
|
}
|
||||||
for (_, &(_, span)) in stcs.iter() {
|
for (_, &(_, span)) in stcs.iter() {
|
||||||
let span_id = self.ctx.register_span(span);
|
let span_id = self.ctx.register_span(span);
|
||||||
self.emit_op(Op::PushSmi);
|
let idx = self.ctx.intern_constant(Constant::Int(span_id as i64));
|
||||||
self.emit_u32(span_id);
|
self.emit_op(Op::PushConst);
|
||||||
|
self.emit_u32(idx);
|
||||||
}
|
}
|
||||||
self.emit_op(Op::MakeAttrs);
|
self.emit_op(Op::MakeAttrs);
|
||||||
self.emit_u32(stcs.len() as u32);
|
self.emit_u32(stcs.len() as u32);
|
||||||
@@ -805,13 +840,15 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|||||||
) {
|
) {
|
||||||
self.emit_expr(expr);
|
self.emit_expr(expr);
|
||||||
for attr in attrpath.iter() {
|
for attr in attrpath.iter() {
|
||||||
match *attr {
|
match attr {
|
||||||
Attr::Str(sym, _) => {
|
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_op(Op::PushString);
|
||||||
self.emit_str_id(sym);
|
self.emit_u32(idx);
|
||||||
}
|
}
|
||||||
Attr::Dynamic(expr, _) => {
|
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<'_>>]) {
|
fn emit_has_attr(&mut self, lhs: RawIrRef<'_>, rhs: &[Attr<RawIrRef<'_>>]) {
|
||||||
self.emit_expr(lhs);
|
self.emit_expr(lhs);
|
||||||
for attr in rhs.iter() {
|
for attr in rhs.iter() {
|
||||||
match *attr {
|
match attr {
|
||||||
Attr::Str(sym, _) => {
|
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_op(Op::PushString);
|
||||||
self.emit_str_id(sym);
|
self.emit_u32(idx);
|
||||||
}
|
}
|
||||||
Attr::Dynamic(expr, _) => {
|
Attr::Dynamic(expr, _) => {
|
||||||
self.emit_expr(expr);
|
self.emit_expr(*expr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
}) = ¶m
|
||||||
|
{
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
"])"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,11 @@ use std::fmt::Write;
|
|||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
|
|
||||||
use crate::codegen::{Bytecode, Op};
|
use crate::bytecode::{Bytecode, Constant, Op};
|
||||||
|
|
||||||
pub(crate) trait DisassemblerContext {
|
pub(crate) trait DisassemblerContext {
|
||||||
fn lookup_string(&self, id: u32) -> &str;
|
fn lookup_string(&self, id: u32) -> &str;
|
||||||
|
fn lookup_constant(&self, id: u32) -> &Constant;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Disassembler<'a, Ctx> {
|
pub(crate) struct Disassembler<'a, Ctx> {
|
||||||
@@ -54,22 +55,6 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
|
|||||||
i32::from_le_bytes(bytes)
|
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 {
|
pub fn disassemble(&mut self) -> String {
|
||||||
self.disassemble_impl(false)
|
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");
|
let op = Op::try_from_primitive(op_byte).expect("invalid op code");
|
||||||
|
|
||||||
match op {
|
match op {
|
||||||
Op::PushSmi => {
|
Op::PushConst => {
|
||||||
let val = self.read_i32();
|
let idx = self.read_u32();
|
||||||
("PushSmi", format!("{}", val))
|
let val = self.ctx.lookup_constant(idx);
|
||||||
}
|
let val_str = match val {
|
||||||
Op::PushBigInt => {
|
Constant::Int(i) => format!("Int({})", i),
|
||||||
let val = self.read_i64();
|
Constant::Float(f) => format!("Float(bits: {})", f),
|
||||||
("PushBigInt", format!("{}", val))
|
};
|
||||||
}
|
("PushConst", format!("@{} ({})", idx, val_str))
|
||||||
Op::PushFloat => {
|
|
||||||
let val = self.read_f64();
|
|
||||||
("PushFloat", format!("{}", val))
|
|
||||||
}
|
}
|
||||||
Op::PushString => {
|
Op::PushString => {
|
||||||
let idx = self.read_u32();
|
let idx = self.read_u32();
|
||||||
@@ -50,16 +50,16 @@ pub trait DowngradeContext<'id: 'ir, 'ir> {
|
|||||||
fn new_arg(&mut self) -> ArgId;
|
fn new_arg(&mut self) -> ArgId;
|
||||||
fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir>;
|
fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir>;
|
||||||
|
|
||||||
fn new_sym(&mut self, sym: String) -> StringId;
|
fn new_sym(&mut self, sym: String) -> SymId;
|
||||||
fn get_sym(&self, id: StringId) -> Symbol<'_>;
|
fn get_sym(&self, id: SymId) -> Symbol<'_>;
|
||||||
fn lookup(&self, sym: StringId, span: TextRange) -> Result<IrRef<'id, 'ir>>;
|
fn lookup(&self, sym: SymId, span: TextRange) -> Result<IrRef<'id, 'ir>>;
|
||||||
|
|
||||||
fn get_current_source(&self) -> Source;
|
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
|
where
|
||||||
F: FnOnce(&mut Self) -> R;
|
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
|
where
|
||||||
F: FnOnce(&mut Self) -> Result<(Vec<'ir, IrRef<'id, 'ir>>, R)>;
|
F: FnOnce(&mut Self) -> Result<(Vec<'ir, IrRef<'id, 'ir>>, R)>;
|
||||||
fn with_with_scope<F, R>(&mut self, f: F) -> 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> {
|
enum PendingValue<'ir> {
|
||||||
Expr(ast::Expr),
|
Expr(ast::Expr),
|
||||||
InheritFrom(ast::Expr, StringId, TextRange),
|
InheritFrom(ast::Expr, SymId, TextRange),
|
||||||
InheritScope(StringId, TextRange),
|
InheritScope(SymId, TextRange),
|
||||||
Set(PendingAttrSet<'ir>),
|
Set(PendingAttrSet<'ir>),
|
||||||
ExtendedRecAttrSet {
|
ExtendedRecAttrSet {
|
||||||
base: ast::AttrSet,
|
base: ast::AttrSet,
|
||||||
@@ -466,7 +466,7 @@ enum PendingValue<'ir> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct PendingAttrSet<'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)>,
|
dyns: Vec<'ir, (ast::Attr, PendingValue<'ir>, TextRange)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,7 +510,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
|
|||||||
|
|
||||||
fn insert_static(
|
fn insert_static(
|
||||||
&mut self,
|
&mut self,
|
||||||
sym: StringId,
|
sym: SymId,
|
||||||
span: TextRange,
|
span: TextRange,
|
||||||
path: &[ast::Attr],
|
path: &[ast::Attr],
|
||||||
value: ast::Expr,
|
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> {
|
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)>,
|
dyns: Vec<'ir, (IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -930,23 +930,23 @@ fn downgrade_attrpath<'id, 'ir>(
|
|||||||
|
|
||||||
struct PatternBindings<'id, 'ir> {
|
struct PatternBindings<'id, 'ir> {
|
||||||
body: IrRef<'id, 'ir>,
|
body: IrRef<'id, 'ir>,
|
||||||
required: Vec<'ir, (StringId, TextRange)>,
|
required: Vec<'ir, (SymId, TextRange)>,
|
||||||
optional: Vec<'ir, (StringId, TextRange)>,
|
optional: Vec<'ir, (SymId, TextRange)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn downgrade_pattern_bindings<'id, 'ir, Ctx>(
|
fn downgrade_pattern_bindings<'id, 'ir, Ctx>(
|
||||||
pat_entries: impl Iterator<Item = ast::PatEntry>,
|
pat_entries: impl Iterator<Item = ast::PatEntry>,
|
||||||
alias: Option<StringId>,
|
alias: Option<SymId>,
|
||||||
arg: ArgId,
|
arg: ArgId,
|
||||||
ctx: &mut Ctx,
|
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>>
|
) -> Result<PatternBindings<'id, 'ir>>
|
||||||
where
|
where
|
||||||
Ctx: DowngradeContext<'id, 'ir>,
|
Ctx: DowngradeContext<'id, 'ir>,
|
||||||
{
|
{
|
||||||
let arg = ctx.new_expr(Ir::Arg(arg));
|
let arg = ctx.new_expr(Ir::Arg(arg));
|
||||||
struct Param {
|
struct Param {
|
||||||
sym: StringId,
|
sym: SymId,
|
||||||
sym_span: TextRange,
|
sym_span: TextRange,
|
||||||
default: Option<ast::Expr>,
|
default: Option<ast::Expr>,
|
||||||
span: TextRange,
|
span: TextRange,
|
||||||
@@ -1045,7 +1045,7 @@ fn downgrade_let_bindings<'id, 'ir, Ctx, F>(
|
|||||||
) -> Result<IrRef<'id, 'ir>>
|
) -> Result<IrRef<'id, 'ir>>
|
||||||
where
|
where
|
||||||
Ctx: DowngradeContext<'id, 'ir>,
|
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| {
|
downgrade_rec_attrs_impl::<_, _, false>(entries, ctx, |ctx, binding_keys, _dyns| {
|
||||||
body_fn(ctx, binding_keys)
|
body_fn(ctx, binding_keys)
|
||||||
@@ -1081,7 +1081,7 @@ where
|
|||||||
Ctx: DowngradeContext<'id, 'ir>,
|
Ctx: DowngradeContext<'id, 'ir>,
|
||||||
F: FnOnce(
|
F: FnOnce(
|
||||||
&mut Ctx,
|
&mut Ctx,
|
||||||
&[StringId],
|
&[SymId],
|
||||||
&[(IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)],
|
&[(IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)],
|
||||||
) -> Result<IrRef<'id, 'ir>>,
|
) -> Result<IrRef<'id, 'ir>>,
|
||||||
{
|
{
|
||||||
@@ -1106,7 +1106,7 @@ where
|
|||||||
fn collect_inherit_lookups<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>>(
|
fn collect_inherit_lookups<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>>(
|
||||||
entries: &[ast::Entry],
|
entries: &[ast::Entry],
|
||||||
ctx: &mut Ctx,
|
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());
|
let mut inherit_lookups = HashMap::new_in(ctx.bump());
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
if let ast::Entry::Inherit(inherit) = entry
|
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>(
|
fn collect_binding_syms<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>(
|
||||||
pending: &PendingAttrSet,
|
pending: &PendingAttrSet,
|
||||||
ctx: &mut Ctx,
|
ctx: &mut Ctx,
|
||||||
) -> Result<HashSet<StringId>> {
|
) -> Result<HashSet<SymId>> {
|
||||||
let mut binding_syms = HashSet::new();
|
let mut binding_syms = HashSet::new();
|
||||||
|
|
||||||
for (sym, (_, span)) in &pending.stcs {
|
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>(
|
fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>(
|
||||||
pending: PendingAttrSet,
|
pending: PendingAttrSet,
|
||||||
inherit_lookups: &HashMap<StringId, (IrRef<'id, 'ir>, TextRange)>,
|
inherit_lookups: &HashMap<SymId, (IrRef<'id, 'ir>, TextRange)>,
|
||||||
ctx: &mut Ctx,
|
ctx: &mut Ctx,
|
||||||
) -> Result<FinalizedAttrSet<'id, 'ir>> {
|
) -> Result<FinalizedAttrSet<'id, 'ir>> {
|
||||||
let mut stcs = HashMap::new_in(ctx.bump());
|
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>(
|
fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>(
|
||||||
value: PendingValue,
|
value: PendingValue,
|
||||||
inherit_lookups: &HashMap<StringId, (IrRef<'id, 'ir>, TextRange)>,
|
inherit_lookups: &HashMap<SymId, (IrRef<'id, 'ir>, TextRange)>,
|
||||||
ctx: &mut Ctx,
|
ctx: &mut Ctx,
|
||||||
) -> Result<IrRef<'id, 'ir>> {
|
) -> Result<IrRef<'id, 'ir>> {
|
||||||
match value {
|
match value {
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use deno_core::error::JsError;
|
||||||
|
use deno_error::JsErrorClass as _;
|
||||||
|
use itertools::Itertools as _;
|
||||||
use miette::{Diagnostic, NamedSource, SourceSpan};
|
use miette::{Diagnostic, NamedSource, SourceSpan};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::runtime::RuntimeContext;
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Box<Error>>;
|
pub type Result<T> = core::result::Result<T, Box<Error>>;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -224,3 +229,127 @@ pub struct StackFrame {
|
|||||||
#[source_code]
|
#[source_code]
|
||||||
pub src: NamedSource<Arc<str>>,
|
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 nix_compat::nixhash::NixHash;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
use crate::runtime::OpStateExt;
|
||||||
|
use crate::runtime::RuntimeContext;
|
||||||
use crate::store::Store as _;
|
use crate::store::Store as _;
|
||||||
|
|
||||||
mod archive;
|
mod archive;
|
||||||
@@ -18,6 +20,7 @@ pub use download::Downloader;
|
|||||||
pub use metadata_cache::MetadataCache;
|
pub use metadata_cache::MetadataCache;
|
||||||
|
|
||||||
use crate::nar;
|
use crate::nar;
|
||||||
|
use crate::runtime::NixRuntimeError;
|
||||||
|
|
||||||
#[derive(ToV8)]
|
#[derive(ToV8)]
|
||||||
pub struct FetchUrlResult {
|
pub struct FetchUrlResult {
|
||||||
@@ -303,3 +306,11 @@ fn normalize_hash(hash: &str) -> String {
|
|||||||
}
|
}
|
||||||
hash.to_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>(),
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ impl FetcherCache {
|
|||||||
pub fn new() -> Result<Self, std::io::Error> {
|
pub fn new() -> Result<Self, std::io::Error> {
|
||||||
let base_dir = dirs::cache_dir()
|
let base_dir = dirs::cache_dir()
|
||||||
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
||||||
.join("fix")
|
.join("nix-js")
|
||||||
.join("fetchers");
|
.join("fetchers");
|
||||||
|
|
||||||
fs::create_dir_all(&base_dir)?;
|
fs::create_dir_all(&base_dir)?;
|
||||||
@@ -4,7 +4,6 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use bumpalo::{Bump, boxed::Box, collections::Vec};
|
use bumpalo::{Bump, boxed::Box, collections::Vec};
|
||||||
use gc_arena::Collect;
|
|
||||||
use ghost_cell::{GhostCell, GhostToken};
|
use ghost_cell::{GhostCell, GhostToken};
|
||||||
use rnix::{TextRange, ast};
|
use rnix::{TextRange, ast};
|
||||||
use string_interner::symbol::SymbolU32;
|
use string_interner::symbol::SymbolU32;
|
||||||
@@ -24,10 +23,6 @@ impl<'id, 'ir> IrRef<'id, 'ir> {
|
|||||||
Self(bump.alloc(GhostCell::new(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
|
/// Freeze a mutable IR reference into a read-only one, consuming the
|
||||||
/// `GhostToken` to prevent any further mutation.
|
/// `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)]
|
#[repr(transparent)]
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct RawIrRef<'ir>(&'ir Ir<'ir, Self>);
|
pub struct RawIrRef<'ir>(&'ir Ir<'ir, Self>);
|
||||||
@@ -66,7 +68,7 @@ pub enum Ir<'ir, Ref> {
|
|||||||
Null,
|
Null,
|
||||||
Str(Box<'ir, String>),
|
Str(Box<'ir, String>),
|
||||||
AttrSet {
|
AttrSet {
|
||||||
stcs: HashMap<'ir, StringId, (Ref, TextRange)>,
|
stcs: HashMap<'ir, SymId, (Ref, TextRange)>,
|
||||||
dyns: Vec<'ir, (Ref, Ref, TextRange)>,
|
dyns: Vec<'ir, (Ref, Ref, TextRange)>,
|
||||||
},
|
},
|
||||||
List {
|
List {
|
||||||
@@ -117,7 +119,7 @@ pub enum Ir<'ir, Ref> {
|
|||||||
body: Ref,
|
body: Ref,
|
||||||
thunks: Vec<'ir, (ThunkId, Ref)>,
|
thunks: Vec<'ir, (ThunkId, Ref)>,
|
||||||
},
|
},
|
||||||
WithLookup(StringId),
|
WithLookup(SymId),
|
||||||
|
|
||||||
// Function related
|
// Function related
|
||||||
Func {
|
Func {
|
||||||
@@ -135,7 +137,7 @@ pub enum Ir<'ir, Ref> {
|
|||||||
|
|
||||||
// Builtins
|
// Builtins
|
||||||
Builtins,
|
Builtins,
|
||||||
Builtin(StringId),
|
Builtin(SymId),
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
TopLevel {
|
TopLevel {
|
||||||
@@ -144,26 +146,19 @@ pub enum Ir<'ir, Ref> {
|
|||||||
},
|
},
|
||||||
Thunk(ThunkId),
|
Thunk(ThunkId),
|
||||||
CurPos(TextRange),
|
CurPos(TextRange),
|
||||||
ReplBinding(StringId),
|
ReplBinding(SymId),
|
||||||
ScopedImportBinding(StringId),
|
ScopedImportBinding(SymId),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct ThunkId(pub usize);
|
pub struct ThunkId(pub usize);
|
||||||
|
|
||||||
#[repr(transparent)]
|
pub type SymId = SymbolU32;
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)]
|
|
||||||
#[collect(require_static)]
|
|
||||||
pub struct StringId(pub SymbolU32);
|
|
||||||
|
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct ArgId(pub u32);
|
pub struct ArgId(pub usize);
|
||||||
|
|
||||||
#[repr(transparent)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
||||||
pub struct SpanId(pub u32);
|
|
||||||
|
|
||||||
/// Represents a key in an attribute path.
|
/// Represents a key in an attribute path.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
@@ -174,7 +169,7 @@ pub enum Attr<Ref> {
|
|||||||
Dynamic(Ref, TextRange),
|
Dynamic(Ref, TextRange),
|
||||||
/// A static attribute key.
|
/// A static attribute key.
|
||||||
/// Example: `attrs.key`
|
/// Example: `attrs.key`
|
||||||
Str(StringId, TextRange),
|
Str(SymId, TextRange),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The kinds of binary operations supported in Nix.
|
/// The kinds of binary operations supported in Nix.
|
||||||
@@ -253,8 +248,8 @@ impl From<ast::UnaryOpKind> for UnOpKind {
|
|||||||
/// Describes the parameters of a function.
|
/// Describes the parameters of a function.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Param<'ir> {
|
pub struct Param<'ir> {
|
||||||
pub required: Vec<'ir, (StringId, TextRange)>,
|
pub required: Vec<'ir, (SymId, TextRange)>,
|
||||||
pub optional: Vec<'ir, (StringId, TextRange)>,
|
pub optional: Vec<'ir, (SymId, TextRange)>,
|
||||||
pub ellipsis: bool,
|
pub ellipsis: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
#![warn(clippy::unwrap_used)]
|
#![warn(clippy::unwrap_used)]
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
|
pub mod context;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
pub mod runtime;
|
|
||||||
pub mod value;
|
pub mod value;
|
||||||
|
|
||||||
|
mod bytecode;
|
||||||
mod codegen;
|
mod codegen;
|
||||||
mod derivation;
|
mod derivation;
|
||||||
mod disassembler;
|
mod disassembler;
|
||||||
mod downgrade;
|
mod downgrade;
|
||||||
// mod fetcher;
|
mod fetcher;
|
||||||
mod ir;
|
mod ir;
|
||||||
mod nar;
|
mod nar;
|
||||||
mod nix_utils;
|
mod nix_utils;
|
||||||
|
mod runtime;
|
||||||
mod store;
|
mod store;
|
||||||
mod string_context;
|
mod string_context;
|
||||||
|
|
||||||
@@ -3,15 +3,23 @@ use std::process::exit;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
use fix::error::Source;
|
|
||||||
use fix::runtime::Runtime;
|
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
|
use nix_js::context::Context;
|
||||||
|
use nix_js::error::Source;
|
||||||
use rustyline::DefaultEditor;
|
use rustyline::DefaultEditor;
|
||||||
use rustyline::error::ReadlineError;
|
use rustyline::error::ReadlineError;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "nix-js", about = "Nix expression evaluator")]
|
#[command(name = "nix-js", about = "Nix expression evaluator")]
|
||||||
struct Cli {
|
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(subcommand)]
|
||||||
command: Command,
|
command: Command,
|
||||||
}
|
}
|
||||||
@@ -40,7 +48,28 @@ struct ExprSource {
|
|||||||
file: Option<PathBuf>,
|
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 {
|
let src = if let Some(expr) = src.expr {
|
||||||
Source::new_eval(expr)?
|
Source::new_eval(expr)?
|
||||||
} else if let Some(file) = src.file {
|
} else if let Some(file) = src.file {
|
||||||
@@ -48,11 +77,10 @@ fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<(
|
|||||||
} else {
|
} else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
todo!()
|
match context.compile_bytecode(src) {
|
||||||
/* match runtime.compile_bytecode(src) {
|
|
||||||
Ok(compiled) => {
|
Ok(compiled) => {
|
||||||
if !silent {
|
if !silent {
|
||||||
println!("{}", runtime.disassemble_colored(&compiled));
|
println!("{}", context.disassemble_colored(&compiled));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -60,10 +88,12 @@ fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<(
|
|||||||
exit(1);
|
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 {
|
let src = if let Some(expr) = src.expr {
|
||||||
Source::new_eval(expr)?
|
Source::new_eval(expr)?
|
||||||
} else if let Some(file) = src.file {
|
} else if let Some(file) = src.file {
|
||||||
@@ -71,7 +101,7 @@ fn run_eval(runtime: &mut Runtime, src: ExprSource) -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
match runtime.eval_deep(src) {
|
match context.eval_deep(src) {
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
println!("{}", value.display_compat());
|
println!("{}", value.display_compat());
|
||||||
}
|
}
|
||||||
@@ -80,10 +110,12 @@ fn run_eval(runtime: &mut Runtime, src: ExprSource) -> Result<()> {
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
context.wait_for_inspector_disconnect();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_repl(runtime: &mut Runtime) -> Result<()> {
|
fn run_repl(context: &mut Context) -> Result<()> {
|
||||||
let mut rl = DefaultEditor::new()?;
|
let mut rl = DefaultEditor::new()?;
|
||||||
let mut scope = HashSet::new();
|
let mut scope = HashSet::new();
|
||||||
const RE: ere::Regex<3> = ere::compile_regex!("^[ \t]*([a-zA-Z_][a-zA-Z0-9_'-]*)[ \t]*(.*)$");
|
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 '='");
|
eprintln!("Error: missing expression after '='");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match runtime.add_binding(ident, expr, &mut scope) {
|
match context.add_binding(ident, expr, &mut scope) {
|
||||||
Ok(value) => println!("{} = {}", ident, value),
|
Ok(value) => println!("{} = {}", ident, value),
|
||||||
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let src = Source::new_repl(line)?;
|
let src = Source::new_repl(line)?;
|
||||||
match runtime.eval_repl(src, &scope) {
|
match context.eval_repl(src, &scope) {
|
||||||
Ok(value) => println!("{value}"),
|
Ok(value) => println!("{value}"),
|
||||||
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let src = Source::new_repl(line)?;
|
let src = Source::new_repl(line)?;
|
||||||
match runtime.eval_repl(src, &scope) {
|
match context.eval_shallow(src) {
|
||||||
Ok(value) => println!("{value}"),
|
Ok(value) => println!("{value}"),
|
||||||
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
||||||
}
|
}
|
||||||
@@ -138,15 +170,18 @@ fn run_repl(runtime: &mut Runtime) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
fix::logging::init_logging();
|
nix_js::logging::init_logging();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
let mut runtime = Runtime::new()?;
|
let mut context = create_context(
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
&cli,
|
||||||
|
)?;
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Compile { source, silent } => run_compile(&mut runtime, source, silent),
|
Command::Compile { source, silent } => run_compile(&mut context, source, silent),
|
||||||
Command::Eval { source } => run_eval(&mut runtime, source),
|
Command::Eval { source } => run_eval(&mut context, source),
|
||||||
Command::Repl => run_repl(&mut runtime),
|
Command::Repl => run_repl(&mut context),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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>),
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use fix::value::Value;
|
use nix_js::value::Value;
|
||||||
|
|
||||||
use crate::utils::{eval, eval_result};
|
use crate::utils::{eval, eval_result};
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use fix::value::{AttrSet, List, Value};
|
use nix_js::value::{AttrSet, List, Value};
|
||||||
|
|
||||||
use crate::utils::eval;
|
use crate::utils::eval;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use fix::value::Value;
|
use nix_js::value::Value;
|
||||||
|
|
||||||
use crate::utils::eval_result;
|
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};
|
use crate::utils::{eval_deep, eval_deep_result};
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use fix::value::{List, Value};
|
use nix_js::value::{List, Value};
|
||||||
|
|
||||||
use crate::utils::{eval, eval_result};
|
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};
|
use crate::utils::{eval, eval_result};
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use fix::error::Source;
|
use nix_js::context::Context;
|
||||||
use fix::runtime::Runtime;
|
use nix_js::error::Source;
|
||||||
use fix::value::Value;
|
use nix_js::value::Value;
|
||||||
|
|
||||||
use crate::utils::{eval, eval_result};
|
use crate::utils::{eval, eval_result};
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ fn import_with_complex_dependency_graph() {
|
|||||||
|
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
fn path_with_file() {
|
fn path_with_file() {
|
||||||
let mut ctx = Runtime::new().unwrap();
|
let mut ctx = Context::new().unwrap();
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_file = temp_dir.path().join("test.txt");
|
let test_file = temp_dir.path().join("test.txt");
|
||||||
std::fs::write(&test_file, "Hello, World!").unwrap();
|
std::fs::write(&test_file, "Hello, World!").unwrap();
|
||||||
@@ -107,7 +107,7 @@ fn path_with_file() {
|
|||||||
|
|
||||||
// Should return a store path string
|
// Should return a store path string
|
||||||
if let Value::String(store_path) = result {
|
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"));
|
assert!(store_path.contains("test.txt"));
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected string, got {:?}", result);
|
panic!("Expected string, got {:?}", result);
|
||||||
@@ -136,7 +136,7 @@ fn path_with_custom_name() {
|
|||||||
|
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
fn path_with_directory_recursive() {
|
fn path_with_directory_recursive() {
|
||||||
let mut ctx = Runtime::new().unwrap();
|
let mut ctx = Context::new().unwrap();
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_dir = temp_dir.path().join("mydir");
|
let test_dir = temp_dir.path().join("mydir");
|
||||||
std::fs::create_dir_all(&test_dir).unwrap();
|
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();
|
let result = ctx.eval(Source::new_eval(expr).unwrap()).unwrap();
|
||||||
|
|
||||||
if let Value::String(store_path) = result {
|
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"));
|
assert!(store_path.contains("mydir"));
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected string, got {:?}", result);
|
panic!("Expected string, got {:?}", result);
|
||||||
@@ -159,7 +159,7 @@ fn path_with_directory_recursive() {
|
|||||||
|
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
fn path_flat_with_file() {
|
fn path_flat_with_file() {
|
||||||
let mut ctx = Runtime::new().unwrap();
|
let mut ctx = Context::new().unwrap();
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_file = temp_dir.path().join("flat.txt");
|
let test_file = temp_dir.path().join("flat.txt");
|
||||||
std::fs::write(&test_file, "Flat content").unwrap();
|
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();
|
let result = ctx.eval(Source::new_eval(expr).unwrap()).unwrap();
|
||||||
|
|
||||||
if let Value::String(store_path) = result {
|
if let Value::String(store_path) = result {
|
||||||
assert!(store_path.starts_with("/nix/store"));
|
assert!(store_path.starts_with(ctx.get_store_dir()));
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected string, got {:?}", result);
|
panic!("Expected string, got {:?}", result);
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use fix::error::Source;
|
use nix_js::context::Context;
|
||||||
use fix::runtime::Runtime;
|
use nix_js::error::Source;
|
||||||
use fix::value::Value;
|
use nix_js::value::Value;
|
||||||
|
|
||||||
fn get_lang_dir() -> PathBuf {
|
fn get_lang_dir() -> PathBuf {
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/tests/lang")
|
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 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 {
|
let source = Source {
|
||||||
ty: fix::error::SourceType::File(nix_path.into()),
|
ty: nix_js::error::SourceType::File(nix_path.into()),
|
||||||
src: expr.into(),
|
src: expr.into(),
|
||||||
};
|
};
|
||||||
ctx.eval_deep(source.clone())
|
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_empty_context);
|
||||||
eval_fail_test!(fail_addDrvOutputDependencies_multi_elem_context);
|
eval_fail_test!(fail_addDrvOutputDependencies_multi_elem_context);
|
||||||
eval_fail_test!(fail_addDrvOutputDependencies_wrong_element_kind);
|
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);
|
||||||
eval_fail_test!(fail_assert_equal_attrs_names);
|
eval_fail_test!(fail_assert_equal_attrs_names);
|
||||||
eval_fail_test!(fail_assert_equal_attrs_names_2);
|
eval_fail_test!(fail_assert_equal_attrs_names_2);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user