Compare commits

...
60 Commits
Author SHA1 Message Date
imxyy1soope1 29fab93cd1 refactor: abstract VM 2026-05-16 19:51:38 +08:00
imxyy1soope1 21899f7380 implement Path type 2026-05-16 19:51:00 +08:00
imxyy1soope1 3d07f89afe implement foldl' 2026-05-08 21:09:54 +08:00
imxyy1soope1 cfd2df5d0e implement import & scopedImport (WIP, ResolvePath resolves to string) 2026-05-08 18:52:18 +08:00
imxyy1soope1 49392f66f8 fix: null dynamic attrs 2026-05-08 17:46:23 +08:00
imxyy1soope1 4aff27142c implement __functor 2026-05-08 17:46:23 +08:00
imxyy1soope1 62d65b2e5f refactor primops 2026-05-08 17:46:23 +08:00
imxyy1soope1 b3e6591809 refactor with 2026-05-08 17:46:23 +08:00
imxyy1soope1 fca00b04ba implement Assert 2026-05-08 17:46:23 +08:00
imxyy1soope1 88a205f419 document trying mechanism 2026-05-08 17:46:23 +08:00
imxyy1soope1 7401f1ba5e chore: update flake.lock 2026-05-08 17:46:23 +08:00
imxyy1soope1 9d10fa7da3 ConcatStrings 2026-05-08 17:46:23 +08:00
imxyy1soope1 1550868e90 implement dynamic key; implement __curPos; other small changes 2026-05-08 17:46:23 +08:00
imxyy1soope1 035ebd3808 implement |> and <| 2026-05-08 17:46:23 +08:00
imxyy1soope1 06e73fc9be avoid thunking trivial values 2026-05-08 17:46:23 +08:00
imxyy1soope1 47b1344ebe refactor: use GAT in enum Ir 2026-05-08 17:46:23 +08:00
imxyy1soope1 6659b22dce layer: usize -> u8 2026-05-08 17:46:22 +08:00
imxyy1soope1 045f0bd6de temp 2026-05-08 17:46:22 +08:00
imxyy1soope1 fe96f6d9c5 implement pattern calling 2026-05-08 17:46:22 +08:00
imxyy1soope1 a28dfada30 implement unary operations 2026-05-08 17:46:22 +08:00
imxyy1soope1 103928779f ForceMode 2026-05-08 17:46:22 +08:00
imxyy1soope1 c08e0b81c4 implement __seq and __deepSeq 2026-05-08 17:46:22 +08:00
imxyy1soope1 26717a8184 implement primop (filter) 2026-05-08 17:46:22 +08:00
imxyy1soope1 4f3cd0ef4c refactor: split VmContext 2026-04-26 15:24:40 +08:00
imxyy1soope1 468269c20d chore: update flake.lock 2026-04-26 15:24:33 +08:00
imxyy1soope1 21036aba46 implement Select and HasAttr 2026-04-24 21:09:12 +08:00
imxyy1soope1 e469d1b819 better force eval ergonomic 2026-04-21 22:56:57 +08:00
imxyy1soope1 b31c2a4906 Step -> ControlFlow (use ?) 2026-04-21 11:31:11 +08:00
imxyy1soope1 11b0b8a78e better type assertion ergonomic 2026-04-21 11:30:51 +08:00
imxyy1soope1 581c333070 minor changes 2026-04-20 15:36:11 +08:00
imxyy1soope1 520bb7d75e optimize: remove {StepResult,TailResult}::ForceThunk 2026-04-20 15:11:53 +08:00
imxyy1soope1 98b07f00e4 feat: add experimental tailcall vm backend 2026-04-20 08:51:19 +08:00
imxyy1soope1 800249cb1e optimize: avoid generating drop glue for StepResult 2026-04-19 19:18:00 +08:00
imxyy1soope1 e527d31450 refactor vm 2026-04-19 17:55:54 +08:00
imxyy1soope1 ca7f7a5ec8 fix disassembler 2026-04-19 16:59:25 +08:00
imxyy1soope1 f66752afa5 split SelectDefault -> SelectStatic & Jump... 2026-04-19 16:59:25 +08:00
imxyy1soope1 74866ec1d3 refactor: merge GcRoot & Vm 2026-04-19 13:07:49 +08:00
imxyy1soope1 df9664f5c4 LookupWith: retry 2026-04-19 01:25:31 +08:00
imxyy1soope1 f372ebcb8e chore: callgrind 2026-04-18 18:45:39 +08:00
imxyy1soope1 291e3a9a13 chore: update flake 2026-04-18 15:41:55 +08:00
imxyy1soope1 8ad4cf7c7a refactor(downgrade): MaybeThunk 2026-04-12 17:44:31 +08:00
imxyy1soope1 e34cfc7add minor changes 2026-04-12 17:05:44 +08:00
imxyy1soope1 9983458b31 feat(vm): threaded VM 2026-04-12 17:05:26 +08:00
imxyy1soope1 8e67f9f636 chore: update flake.lock 2026-04-10 22:01:55 +08:00
imxyy1soope1 d90fd8d681 minor changes 2026-04-10 21:26:56 +08:00
imxyy1soope1 b1b886229b refactor 2026-04-06 17:53:27 +08:00
imxyy1soope1 ee54ab8895 implement WithLookup 2026-04-04 10:05:10 +08:00
imxyy1soope1 88dc8539b5 refactor test 2026-04-04 10:05:10 +08:00
imxyy1soope1 0c517e3c18 fix: nested return 2026-04-04 10:05:10 +08:00
imxyy1soope1 310f4acc89 remove ArgId and Ir caching mechanism 2026-04-04 10:05:10 +08:00
imxyy1soope1 95eea517e4 fix force 2026-04-04 10:05:10 +08:00
imxyy1soope1 e78e62795b small changes 2026-04-04 10:05:10 +08:00
imxyy1soope1 dc96e63b7c implement ForceMode 2026-04-04 10:04:58 +08:00
imxyy1soope1 e82369695c implement Select & SelectDefault 2026-04-04 10:04:15 +08:00
imxyy1soope1 6567ed4058 rewrite VM to support reentry (WIP) 2026-03-29 17:51:42 +08:00
imxyy1soope1 b3f1f4f6ff remove redundant tests 2026-03-22 17:01:20 +08:00
imxyy1soope1 1950d4de6c init builtins 2026-03-22 16:50:08 +08:00
imxyy1soope1 198d847151 feat: init Rust VM 2026-03-15 17:53:19 +08:00
imxyy1soope1 40d00a6c47 feat: value 2026-03-14 10:28:43 +08:00
imxyy1soope1 0c9a391618 clean up 2026-03-14 10:28:43 +08:00
563 changed files with 10029 additions and 20074 deletions

No files matched your search

+1
View File
@@ -10,3 +10,4 @@ prof.json
*.cpuprofile *.cpuprofile
*.cpuprofile.gz *.cpuprofile.gz
*v8.log* *v8.log*
callgrind.*
+1 -1
View File
@@ -17,7 +17,7 @@ vim.lsp.config("rust_analyzer", {
["rust-analyzer"] = { ["rust-analyzer"] = {
cargo = { cargo = {
features = { features = {
"inspector" "tailcall"
} }
} }
} }
Generated
+272 -3050
View File
File diff suppressed because it is too large. Load diff
+34 -2
View File
@@ -1,10 +1,42 @@
[workspace] [workspace]
resolver = "3" resolver = "3"
members = [ members = [
"nix-js", "fix",
"nix-js-macros" "fix-abstract-vm",
"fix-builtins",
"fix-codegen",
"fix-common",
"fix-error",
"fix-ir",
"fix-primops",
"fix-vm",
] ]
[profile.profiling] [profile.profiling]
inherits = "release" inherits = "release"
debug = true debug = true
[profile.lto]
inherits = "release"
lto = true
[workspace.dependencies]
bumpalo = { version = "3.20", features = [
"allocator-api2",
"boxed",
"collections",
] }
ghost-cell = "0.2"
hashbrown = "0.16"
num_enum = "0.7.5"
smallvec = "1.15"
ere = "0.2"
string-interner = "0.19"
rnix = "0.14"
rowan = "0.16"
likely_stable = "0.1"
[workspace.dependencies.gc-arena]
git = "https://github.com/kyren/gc-arena"
rev = "75671ae03f53718357b741ed4027560f14e90836"
features = ["allocator-api2", "hashbrown", "smallvec"]
+5
View File
@@ -29,3 +29,8 @@
[no-exit-message] [no-exit-message]
@evalp expr: @evalp expr:
cargo run --release --features prof -- eval --expr '{{expr}}' cargo run --release --features prof -- eval --expr '{{expr}}'
[no-exit-message]
[positional-arguments]
@cg *args='':
valgrind --tool=callgrind --dump-instr=yes --collect-jumps=yes "$@"
-70
View File
@@ -1,70 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!!**/dist"]
},
"formatter": {
"enabled": true,
"formatWithErrors": true,
"attributePosition": "auto",
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 110,
"lineEnding": "lf"
},
"linter": {
"rules": {
"style": {
"useNamingConvention": {
"level": "warn",
"options": {
"strictCase": false,
"conventions": [
{
"selector": { "kind": "objectLiteralProperty" },
"formats": ["camelCase", "PascalCase", "CONSTANT_CASE"]
},
{
"selector": { "kind": "typeProperty" },
"formats": ["camelCase", "snake_case"]
}
]
}
}
}
}
},
"overrides": [
{
"includes": ["**/global.d.ts"],
"linter": {
"rules": {
"style": {
"useNamingConvention": "off"
}
}
}
}
],
"javascript": {
"formatter": {
"arrowParentheses": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"semicolons": "always",
"trailingCommas": "all"
}
},
"json": {
"formatter": {
"trailingCommas": "none"
}
}
}
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "fix-abstract-vm"
version = "0.1.0"
edition = "2024"
[dependencies]
gc-arena = { workspace = true }
hashbrown = { workspace = true }
num_enum = { workspace = true }
smallvec = { workspace = true }
string-interner = { workspace = true }
likely_stable = { workspace = true }
sptr = "0.3"
fix-builtins = { path = "../fix-builtins" }
fix-codegen = { path = "../fix-codegen" }
fix-common = { path = "../fix-common" }
fix-error = { path = "../fix-error" }
+502
View File
@@ -0,0 +1,502 @@
#![allow(dead_code)]
use std::fmt;
use std::num::NonZeroU8;
use sptr::Strict;
const SIGN_MASK: u64 = 0x7FFF_FFFF_FFFF_FFFF;
const QUIET_NAN: u64 = 0x7FF8_0000_0000_0000;
const NEG_QUIET_NAN: u64 = 0xFFF8_0000_0000_0000;
pub(crate) trait ArrayExt<const LEN: usize> {
type Elem;
fn truncate_to<const M: usize>(self) -> [Self::Elem; M];
}
impl<T: Default + Copy, const N: usize> ArrayExt<N> for [T; N] {
type Elem = T;
fn truncate_to<const M: usize>(self) -> [Self::Elem; M] {
let copy_len = usize::min(N, M);
let mut out = [T::default(); M];
out[0..copy_len].copy_from_slice(&self[0..copy_len]);
out
}
}
pub 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 {
pub const P1: RawTag = RawTag(TagVal::_P1);
pub const P2: RawTag = RawTag(TagVal::_P2);
pub const P3: RawTag = RawTag(TagVal::_P3);
pub const P4: RawTag = RawTag(TagVal::_P4);
pub const P5: RawTag = RawTag(TagVal::_P5);
pub const P6: RawTag = RawTag(TagVal::_P6);
pub const P7: RawTag = RawTag(TagVal::_P7);
pub const N1: RawTag = RawTag(TagVal::_N1);
pub const N2: RawTag = RawTag(TagVal::_N2);
pub const N3: RawTag = RawTag(TagVal::_N3);
pub const N4: RawTag = RawTag(TagVal::_N4);
pub const N5: RawTag = RawTag(TagVal::_N5);
pub const N6: RawTag = RawTag(TagVal::_N6);
pub const N7: RawTag = RawTag(TagVal::_N7);
#[inline]
#[must_use]
pub(crate) fn new(neg: bool, val: NonZeroU8) -> RawTag {
unsafe { Self::new_unchecked(neg, val.get() & 0x07) }
}
#[inline]
#[must_use]
pub(crate) 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(crate) const 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(crate) 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(crate) 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 const 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]
const fn tag(self) -> RawTag {
unsafe { RawTag::new_unchecked(self.get_sign(), self.get_tag()) }
}
#[inline]
const fn get_sign(self) -> bool {
self.0 & 0x8000 != 0
}
#[inline]
const fn get_tag(self) -> u8 {
(self.0 & 0x0007) as u8
}
#[inline]
fn into_raw(self) -> u16 {
self.0
}
}
#[derive(Copy, 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(crate) fn new(tag: RawTag, data: [u8; 6]) -> Value {
Value {
header: Header::new(tag),
data,
}
}
#[inline]
pub(crate) fn empty(tag: RawTag) -> Value {
Value::new(tag, [0; 6])
}
pub(crate) fn store<T: RawStore>(tag: RawTag, val: T) -> Value {
let mut v = Value::new(tag, [0; 6]);
T::to_val(val, &mut v);
v
}
pub(crate) fn load<T: RawStore>(self) -> T {
T::from_val(&self)
}
#[inline]
#[must_use]
pub(crate) const fn tag(&self) -> RawTag {
self.header.tag()
}
#[inline]
fn header(&self) -> &Header {
&self.header
}
#[inline]
pub(crate) 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(crate) fn data_mut(&mut self) -> &mut [u8; 6] {
&mut self.data
}
#[inline]
#[must_use]
unsafe fn whole(&self) -> &[u8; 8] {
let ptr = (self as *const Value).cast::<[u8; 8]>();
unsafe { &*ptr }
}
#[inline]
#[must_use]
unsafe fn whole_mut(&mut self) -> &mut [u8; 8] {
let ptr = (self as *mut Value).cast::<[u8; 8]>();
unsafe { &mut *ptr }
}
}
#[repr(C)]
#[derive(Copy, Clone)]
pub(crate) union RawBox {
float: f64,
value: Value,
bits: u64,
#[cfg(target_pointer_width = "64")]
ptr: *const (),
#[cfg(target_pointer_width = "32")]
ptr: (u32, *const ()),
}
impl RawBox {
#[inline]
#[must_use]
pub(crate) fn from_float(val: f64) -> RawBox {
match (val.is_nan(), val.is_sign_positive()) {
(true, true) => RawBox {
float: f64::from_bits(QUIET_NAN),
},
(true, false) => RawBox {
float: f64::from_bits(NEG_QUIET_NAN),
},
(false, _) => RawBox { float: val },
}
}
#[inline]
#[must_use]
pub(crate) fn from_value(value: Value) -> RawBox {
RawBox { value }
}
#[inline]
#[must_use]
pub(crate) const fn tag(&self) -> Option<RawTag> {
if self.is_value() {
Some(unsafe { self.value.tag() })
} else {
None
}
}
#[inline]
#[must_use]
pub(crate) fn is_float(&self) -> bool {
(unsafe { !self.float.is_nan() } || unsafe { self.bits & SIGN_MASK == QUIET_NAN })
}
#[inline]
#[must_use]
pub(crate) const fn is_value(&self) -> bool {
(unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN })
}
#[inline]
#[must_use]
pub(crate) fn float(&self) -> Option<&f64> {
if self.is_float() {
Some(unsafe { &self.float })
} else {
None
}
}
#[inline]
#[must_use]
pub(crate) fn value(&self) -> Option<&Value> {
if self.is_value() {
Some(unsafe { &self.value })
} else {
None
}
}
#[inline]
pub(crate) fn into_float_unchecked(self) -> f64 {
unsafe { self.float }
}
#[inline]
#[must_use]
pub(crate) fn to_bits(self) -> u64 {
unsafe { self.bits }
}
}
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()
}
}
}
}
+142
View File
@@ -0,0 +1,142 @@
#![allow(dead_code)]
use fix_codegen::OperandType;
use fix_common::StringId;
use num_enum::TryFromPrimitive;
use string_interner::Symbol as _;
use crate::{OperandData, VmRuntimeCtx};
pub struct BytecodeReader<'a> {
bytecode: &'a [u8],
pc: usize,
inst_start_pc: usize,
}
impl<'a> BytecodeReader<'a> {
pub fn new(bytecode: &'a [u8], pc: usize) -> Self {
Self {
bytecode,
pc,
inst_start_pc: pc,
}
}
#[inline(always)]
pub fn from_after_op(bytecode: &'a [u8], inst_start_pc: usize) -> Self {
Self {
bytecode,
pc: inst_start_pc + 1,
inst_start_pc,
}
}
#[inline(always)]
#[cfg_attr(debug_assertions, track_caller)]
fn read_array<const N: usize>(&mut self) -> [u8; N] {
let ret = self.bytecode[self.pc..self.pc + N]
.try_into()
.expect("read_array failed");
self.pc += N;
ret
}
#[inline(always)]
pub fn read_op(&mut self) -> fix_codegen::Op {
use fix_codegen::Op;
self.inst_start_pc = self.pc;
let byte = self.bytecode[self.pc];
if !likely_stable::likely((0..Op::Illegal as u8).contains(&byte)) {
panic!("unknown opcode: {byte:#04x}")
}
self.pc += 1;
unsafe { std::mem::transmute::<u8, Op>(byte) }
}
#[inline(always)]
pub fn read_u8(&mut self) -> u8 {
let val = self.bytecode[self.pc];
self.pc += 1;
val
}
#[inline(always)]
pub fn read_u16(&mut self) -> u16 {
u16::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_u32(&mut self) -> u32 {
u32::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_i32(&mut self) -> i32 {
i32::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_i64(&mut self) -> i64 {
i64::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_f64(&mut self) -> f64 {
f64::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_string_id(&mut self) -> StringId {
let raw = self.read_u32();
#[allow(clippy::unwrap_used)]
StringId(string_interner::symbol::SymbolU32::try_from_usize(raw as usize).unwrap())
}
#[inline(always)]
pub fn read_operand_data<C: VmRuntimeCtx>(&mut self, ctx: &C) -> OperandData {
let tag = self.read_u8();
let Ok(ty) = OperandType::try_from_primitive(tag)
.map_err(|err| panic!("unknown operand tag: {:#04x}", err.number));
match ty {
OperandType::Const => {
let id = self.read_u32();
OperandData::Const(ctx.get_const(id))
}
OperandType::BigInt => {
let val = self.read_i64();
OperandData::BigInt(val)
}
OperandType::Local => {
let layer = self.read_u8();
let idx = self.read_u32();
OperandData::Local { layer, idx }
}
OperandType::BuiltinConst => {
let id = self.read_string_id();
OperandData::BuiltinConst(id)
}
OperandType::Builtins => OperandData::Builtins,
OperandType::ReplBinding => {
let id = self.read_string_id();
OperandData::ReplBinding(id)
}
OperandType::ScopedImportBinding => {
let slot_id = self.read_u32();
let name = self.read_string_id();
OperandData::ScopedImportBinding { slot_id, name }
}
}
}
pub fn pc(&self) -> usize {
self.pc
}
pub fn set_pc(&mut self, pc: usize) {
self.pc = pc;
}
pub fn inst_start_pc(&self) -> usize {
self.inst_start_pc
}
}
+272
View File
@@ -0,0 +1,272 @@
use fix_common::StringId;
use gc_arena::{Gc, Mutation};
use crate::{
AttrSet, Break, BytecodeReader, Closure, List, Machine, NixNum, NixString, NixType, Null,
PrimOp, PrimOpApp, Step, StrictValue,
};
pub trait Forced<'gc>: Sized {
const WIDTH: usize;
/// Force and type-check the `WIDTH` slots starting at `base_depth` from
/// TOS, deepest-first. If a slot holds a thunk, enter it and return
/// `Break::Force`. If a slot holds a value of the wrong type, call
/// `finish_type_err` and return `Break::Done`.
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base_depth: usize,
resume_pc: usize,
) -> Step;
/// After `force_and_check` returned `Continue`, pop `WIDTH` slots
/// (TOS first) and convert. Type assertions are infallible because
/// `force_and_check` already validated every slot.
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self;
}
impl<'gc> Forced<'gc> for StrictValue<'gc> {
const WIDTH: usize = 1;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base_depth: usize,
resume_pc: usize,
) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced()
}
}
macro_rules! impl_forced_inline {
($($ty:ty => $nix_ty:expr),* $(,)?) => {
$(
impl<'gc> Forced<'gc> for $ty {
const WIDTH: usize = 1;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base_depth: usize,
resume_pc: usize,
) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = m.peek_forced(base_depth);
if v.as_inline::<$ty>().is_none() {
let _: Step = m.finish_type_err($nix_ty, v.ty());
return Step::Break(Break::Done);
}
Step::Continue(())
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced()
.as_inline::<$ty>()
.expect("type checked in force_and_check")
}
}
)*
};
}
macro_rules! impl_forced_gc {
($($ty:ty => $nix_ty:expr),* $(,)?) => {
$(
impl<'gc> Forced<'gc> for Gc<'gc, $ty> {
const WIDTH: usize = 1;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base_depth: usize,
resume_pc: usize,
) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = m.peek_forced(base_depth);
if v.as_gc::<$ty>().is_none() {
let _: Step = m.finish_type_err($nix_ty, v.ty());
return Step::Break(Break::Done);
}
Step::Continue(())
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced()
.as_gc::<$ty>()
.expect("type checked in force_and_check")
}
}
)*
};
}
impl_forced_inline! {
i32 => NixType::Int,
bool => NixType::Bool,
Null => NixType::Null,
StringId => NixType::String,
PrimOp => NixType::PrimOp,
}
impl_forced_gc! {
i64 => NixType::Int,
NixString => NixType::String,
AttrSet<'gc> => NixType::AttrSet,
List<'gc> => NixType::List,
Closure<'gc> => NixType::Closure,
PrimOpApp<'gc> => NixType::PrimOpApp,
}
impl<'gc> Forced<'gc> for NixNum {
const WIDTH: usize = 1;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base_depth: usize,
resume_pc: usize,
) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = m.peek_forced(base_depth);
if v.as_num().is_none() {
let _: Step = m.finish_type_err(NixType::Int, v.ty());
return Step::Break(Break::Done);
}
Step::Continue(())
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced()
.as_num()
.expect("type checked in force_and_check")
}
}
impl<'gc> Forced<'gc> for f64 {
const WIDTH: usize = 1;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base_depth: usize,
resume_pc: usize,
) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = m.peek_forced(base_depth);
if v.as_float().is_none() {
let _: Step = m.finish_type_err(NixType::Float, v.ty());
return Step::Break(Break::Done);
}
Step::Continue(())
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced()
.as_float()
.expect("type checked in force_and_check")
}
}
impl<'gc, A: Forced<'gc>, B: Forced<'gc>> Forced<'gc> for (A, B) {
const WIDTH: usize = A::WIDTH + B::WIDTH;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base: usize,
resume_pc: usize,
) -> Step {
A::force_and_check(m, reader, mc, base + B::WIDTH, resume_pc)?;
B::force_and_check(m, reader, mc, base, resume_pc)
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
let b = B::pop_converted(m);
let a = A::pop_converted(m);
(a, b)
}
}
impl<'gc, A: Forced<'gc>, B: Forced<'gc>, C: Forced<'gc>> Forced<'gc> for (A, B, C) {
const WIDTH: usize = A::WIDTH + B::WIDTH + C::WIDTH;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base: usize,
resume_pc: usize,
) -> Step {
A::force_and_check(m, reader, mc, base + B::WIDTH + C::WIDTH, resume_pc)?;
B::force_and_check(m, reader, mc, base + C::WIDTH, resume_pc)?;
C::force_and_check(m, reader, mc, base, resume_pc)
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
let c = C::pop_converted(m);
let b = B::pop_converted(m);
let a = A::pop_converted(m);
(a, b, c)
}
}
impl<'gc, A: Forced<'gc>, B: Forced<'gc>, C: Forced<'gc>, D: Forced<'gc>> Forced<'gc>
for (A, B, C, D)
{
const WIDTH: usize = A::WIDTH + B::WIDTH + C::WIDTH + D::WIDTH;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base: usize,
resume_pc: usize,
) -> Step {
A::force_and_check(
m,
reader,
mc,
base + B::WIDTH + C::WIDTH + D::WIDTH,
resume_pc,
)?;
B::force_and_check(m, reader, mc, base + C::WIDTH + D::WIDTH, resume_pc)?;
C::force_and_check(m, reader, mc, base + D::WIDTH, resume_pc)?;
D::force_and_check(m, reader, mc, base, resume_pc)
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
let d = D::pop_converted(m);
let c = C::pop_converted(m);
let b = B::pop_converted(m);
let a = A::pop_converted(m);
(a, b, c, d)
}
}
+154
View File
@@ -0,0 +1,154 @@
use fix_codegen::InstructionPtr;
use fix_common::StringId;
use fix_error::Source;
use hashbrown::HashSet;
use crate::{
AttrSet, Closure, ExtraScope, List, NixString, NixType, Null, Path, PrimOp, PrimOpApp,
StaticValue, StrictValue, Thunk, ThunkState, Value,
};
pub trait VmContext {
fn split(&mut self) -> (&mut impl VmCode, &mut impl VmRuntimeCtx);
}
pub trait VmRuntimeCtx {
fn intern_string(&mut self, s: impl AsRef<str>) -> StringId;
fn resolve_string(&self, id: StringId) -> &str;
fn get_const(&self, id: u32) -> StaticValue;
fn add_const(&mut self, val: StaticValue) -> u32;
}
pub trait VmCode {
fn bytecode(&self) -> &[u8];
fn compile_with_scope(
&mut self,
source: Source,
extra_scope: Option<ExtraScope>,
ctx: &mut impl VmRuntimeCtx,
) -> fix_error::Result<InstructionPtr>;
}
pub trait VmRuntimeCtxExt: VmRuntimeCtx {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>;
fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>;
fn get_string_id<'a, 'gc: 'a>(
&'a mut self,
val: StrictValue<'gc>,
) -> std::result::Result<StringId, NixType>;
fn convert_value(&self, val: Value) -> fix_common::Value;
}
impl<T: VmRuntimeCtx> VmRuntimeCtxExt for T {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(sid) = val.as_inline::<StringId>() {
Some(self.resolve_string(sid))
} else {
val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str())
}
}
/// Like `get_string`, but also accepts `Path` values (returning their
/// underlying canonical-path string). Use this in places where Nix
/// would coerce a path to a string (string interpolation, file IO
/// builtins, etc.).
fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(p) = val.as_inline::<Path>() {
Some(self.resolve_string(p.0))
} else {
self.get_string(val)
}
}
fn get_string_id<'a, 'gc: 'a>(
&'a mut self,
val: StrictValue<'gc>,
) -> std::result::Result<StringId, NixType> {
if let Some(sid) = val.as_inline::<StringId>() {
Ok(sid)
} else if let Some(s) = val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str()) {
Ok(self.intern_string(s))
} else {
Err(val.ty())
}
}
fn convert_value(&self, val: Value) -> fix_common::Value {
self.convert_value_with_seen(val, &mut HashSet::new())
}
}
pub(crate) trait ConvertValueWithSeen: VmRuntimeCtx {
fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_common::Value;
}
impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_common::Value {
use fix_common::Value;
if let Some(i) = val.as_inline::<i32>() {
Value::Int(i as i64)
} else if let Some(gc_i) = val.as_gc::<i64>() {
Value::Int(*gc_i)
} else if let Some(f) = val.as_float() {
Value::Float(f)
} else if let Some(b) = val.as_inline::<bool>() {
Value::Bool(b)
} else if val.is::<Null>() {
Value::Null
} else if let Some(sid) = val.as_inline::<StringId>() {
let s = self.resolve_string(sid).to_owned();
Value::String(s)
} else if let Some(ns) = val.as_gc::<NixString>() {
Value::String(ns.as_str().to_owned())
} else if let Some(p) = val.as_inline::<Path>() {
Value::Path(self.resolve_string(p.0).to_owned())
} else if let Some(attrs) = val.as_gc::<AttrSet>() {
let bits = val.to_bits();
if attrs.entries.is_empty() {
return Value::AttrSet(Default::default());
}
if !seen.insert(bits) {
return Value::Repeated;
}
let mut map = std::collections::BTreeMap::new();
for &(key, val) in attrs.entries.iter() {
let key = self.resolve_string(key).to_owned();
let converted = self.convert_value_with_seen(val, seen);
map.insert(fix_common::Symbol::from(key), converted);
}
Value::AttrSet(fix_common::AttrSet::new(map))
} else if let Some(list) = val.as_gc::<List>() {
let bits = val.to_bits();
if list.inner.borrow().is_empty() {
return Value::List(Default::default());
}
if !seen.insert(bits) {
return Value::Repeated;
}
let items: Vec<_> = list
.inner
.borrow()
.iter()
.copied()
.map(|v| self.convert_value_with_seen(v, seen))
.collect();
Value::List(fix_common::List::new(items))
} else if val.is::<Closure>() {
Value::Func
} else if let Some(thunk) = val.as_gc::<Thunk>() {
if let ThunkState::Evaluated(v) = *thunk.borrow() {
self.convert_value_with_seen(v.relax(), seen)
} else {
Value::Thunk
}
} else if let Some(primop) = val.as_inline::<PrimOp>() {
let name = fix_builtins::BUILTINS[primop.id as usize].0;
Value::PrimOp(name.strip_prefix("__").unwrap_or(name))
} else if let Some(app) = val.as_gc::<PrimOpApp>() {
let name = fix_builtins::BUILTINS[app.primop.id as usize].0;
Value::PrimOpApp(name.strip_prefix("__").unwrap_or(name))
} else {
Value::Null
}
}
}
+18
View File
@@ -0,0 +1,18 @@
mod boxing;
mod bytecode_reader;
mod forced;
mod host;
mod machine;
mod path_util;
mod resolve;
mod state;
mod value;
pub use bytecode_reader::*;
pub use forced::*;
pub use host::*;
pub use machine::*;
pub use path_util::*;
pub use resolve::*;
pub use state::*;
pub use value::*;
+174
View File
@@ -0,0 +1,174 @@
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use fix_common::StringId;
use fix_error::Error;
use gc_arena::Mutation;
use crate::{
Break, BytecodeReader, CallFrame, ForceMode, Forced, GcEnv, NixType, PendingLoad, Step,
StrictValue, Value, VmError,
};
/// Abstract VM-side operations consumed by instruction handlers and primops.
///
/// Implementors maintain a value stack, a call stack, an environment chain,
/// pending result/error state, and a set of GC-allocated globals. Methods
/// fall into a few groups:
///
/// - Stack ops (`push` / `pop` / `peek` / `replace` / `pop_forced` / ...)
/// - Forcing primitives (`force_slot` / `force_slot_to_pc`)
/// - Calling (`call` / `return_from_primop`)
/// - Call-frame management (`push_call_frame` / `pop_call_frame` / call-depth)
/// - Environment access (`env` / `set_env` / `local`)
/// - Result finalization (`finish_ok` / `finish_err` / ...)
/// - Global lookup (`builtins` / `empty_list` / `empty_attrs` / ...)
/// - Imports and scope slots (`import_cache_*` / `scope_slot*` / `set_pending_load`)
pub trait Machine<'gc> {
fn push(&mut self, val: Value<'gc>);
fn pop(&mut self) -> Value<'gc>;
fn peek(&self, depth: usize) -> Value<'gc>;
fn peek_forced(&self, depth: usize) -> StrictValue<'gc>;
fn pop_forced(&mut self) -> StrictValue<'gc>;
fn replace(&mut self, depth: usize, val: Value<'gc>);
fn stack_len(&self) -> usize;
fn force_slot_to_pc(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
resume_pc: usize,
) -> Step;
#[inline(always)]
fn force_slot(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let pc = reader.inst_start_pc();
self.force_slot_to_pc(depth, reader, mc, pc)
}
fn call(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
arg: Value<'gc>,
resume_pc: usize,
) -> Step;
#[inline(always)]
fn return_from_primop(&mut self, val: Value<'gc>, reader: &mut BytecodeReader<'_>) -> Step {
self.push(val);
let Some(CallFrame {
pc: ret_pc,
thunk: _,
env,
}) = self.pop_call_frame()
else {
unreachable!()
};
reader.set_pc(ret_pc);
self.dec_call_depth();
self.set_env(env);
Step::Continue(())
}
fn push_call_frame(&mut self, frame: CallFrame<'gc>);
fn pop_call_frame(&mut self) -> Option<CallFrame<'gc>>;
fn call_depth(&self) -> usize;
fn inc_call_depth(&mut self);
fn dec_call_depth(&mut self);
fn env(&self) -> GcEnv<'gc>;
fn set_env(&mut self, env: GcEnv<'gc>);
#[inline(always)]
fn local(&self, layer: u8, idx: u32) -> Value<'gc> {
let mut cur = self.env();
for _ in 0..layer {
let prev = cur.borrow().prev.expect("env chain too short");
cur = prev;
}
cur.borrow().locals[idx as usize]
}
fn finish_ok(&mut self, val: fix_common::Value) -> Step;
fn finish_err(&mut self, err: Box<Error>) -> Step;
fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step;
#[inline(always)]
fn finish_vm_err(&mut self, err: VmError) -> Step {
self.finish_err(err.into_error())
}
fn builtins(&self) -> Value<'gc>;
fn functor_sym(&self) -> StringId;
fn empty_list(&self) -> Value<'gc>;
fn empty_attrs(&self) -> Value<'gc>;
fn force_mode(&self) -> ForceMode;
fn import_cache_get(&self, path: &Path) -> Option<Value<'gc>>;
fn import_cache_insert(&mut self, path: PathBuf, val: Value<'gc>);
fn scope_slot(&self, idx: u32) -> Value<'gc>;
fn scope_slots_push(&mut self, val: Value<'gc>) -> u32;
fn set_pending_load(&mut self, load: PendingLoad);
}
/// Extension trait with convenience helpers built on top of [`Machine`].
///
/// Auto-implemented for every `Machine<'gc>` so callers just need to bring
/// `MachineExt` (or `Machine`) into scope.
pub trait MachineExt<'gc>: Machine<'gc> {
/// Force the top `T::WIDTH` stack slots and return them as `T`.
///
/// If any slot holds a pending thunk, this method pushes a call frame
/// whose resume PC is the **start of the current instruction**
/// (`reader.inst_start_pc()`), enters the thunk, and returns
/// `Break::Force`. When the thunk eventually returns, the VM will
/// **re-execute the entire opcode handler from the beginning**.
///
/// # Invariants
///
/// * **Do not call this method more than once in a single handler.**
/// If you need to force multiple values, use a tuple type such as
/// `(StrictValue, StrictValue)` so they are forced and popped in one
/// atomic operation.
/// * The stack layout at the call site must be **identical** every time
/// the handler is re-entered.
/// * Propagate the return value with `?` so `Break::Force` correctly
/// unwinds to the dispatch loop.
#[inline(always)]
fn force_and_retry<T: Forced<'gc>>(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break, T>
where
Self: Sized,
{
let pc = reader.inst_start_pc();
self.force_and_retry_pc(reader, mc, pc)
}
/// Same as [`force_and_retry`](Self::force_and_retry) but allows
/// specifying a custom resume PC.
#[inline(always)]
fn force_and_retry_pc<T: Forced<'gc>>(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
resume_pc: usize,
) -> ControlFlow<Break, T>
where
Self: Sized,
{
T::force_and_check(self, reader, mc, 0, resume_pc)?;
ControlFlow::Continue(T::pop_converted(self))
}
}
impl<'gc, M: Machine<'gc>> MachineExt<'gc> for M {}
+18
View File
@@ -0,0 +1,18 @@
use std::path::{Component, PathBuf};
pub fn canon_path_str(path: impl AsRef<std::path::Path>) -> String {
let p = path.as_ref();
let mut normalized = PathBuf::new();
for component in p.components() {
match component {
Component::Prefix(p) => normalized.push(p.as_os_str()),
Component::RootDir => normalized.push("/"),
Component::CurDir => {}
Component::ParentDir => {
normalized.pop();
}
Component::Normal(c) => normalized.push(c),
}
}
normalized.to_string_lossy().into_owned()
}
+33
View File
@@ -0,0 +1,33 @@
use gc_arena::{Gc, Mutation};
use crate::{AttrSet, Machine, OperandData, Value};
/// Resolve a decoded operand into a runtime [`Value`].
///
/// The operand decoder ([`crate::BytecodeReader::read_operand_data`])
/// produces a static enum; this function materializes it against the
/// running [`Machine`] (env chain, builtins, scope slots, ...).
#[inline]
pub fn resolve_operand<'gc, M: Machine<'gc>>(
op: &OperandData,
mc: &Mutation<'gc>,
m: &M,
) -> Value<'gc> {
use OperandData::*;
match *op {
Const(sv) => sv.into(),
BigInt(val) => Value::new_gc(Gc::new(mc, val)),
Local { layer, idx } => m.local(layer, idx),
#[allow(clippy::unwrap_used)]
BuiltinConst(id) => m.builtins().as_gc::<AttrSet>().unwrap().lookup(id).unwrap(),
Builtins => m.builtins(),
ReplBinding(_id) => todo!(),
ScopedImportBinding { slot_id, name } => {
let scope = m.scope_slot(slot_id);
#[allow(clippy::unwrap_used)]
let attrs = scope.as_gc::<AttrSet>().expect("scope must be attrset");
#[allow(clippy::unwrap_used)]
attrs.lookup(name).expect("scoped binding not found")
}
}
}
+99
View File
@@ -0,0 +1,99 @@
use std::ops::ControlFlow;
use std::path::PathBuf;
use fix_common::StringId;
use fix_error::Error;
use gc_arena::{Collect, Gc};
use hashbrown::HashSet;
use crate::{GcEnv, StaticValue, Thunk};
#[allow(dead_code)]
pub enum VmError {
Catchable(String),
Uncatchable(Box<Error>),
}
impl From<Box<Error>> for VmError {
fn from(e: Box<Error>) -> Self {
VmError::Uncatchable(e)
}
}
impl VmError {
pub fn into_error(self) -> Box<Error> {
match self {
VmError::Catchable(_) => todo!("Check for tryEval catch frames"),
VmError::Uncatchable(e) => e,
}
}
}
pub fn vm_err(msg: impl Into<String>) -> VmError {
VmError::Uncatchable(Error::eval_error(msg.into()))
}
#[derive(Collect, Clone, Copy, Debug, PartialEq, Eq, Default)]
#[collect(require_static)]
pub enum ForceMode {
#[default]
AsIs,
Shallow,
Deep,
}
#[repr(u8)]
pub enum Break {
Force,
Done,
LoadFile,
}
pub type Step = ControlFlow<Break>;
#[allow(dead_code)]
pub struct ErrorFrame {
pub span_id: u32,
pub message: Option<String>,
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub struct CallFrame<'gc> {
pub pc: usize,
pub thunk: Option<Gc<'gc, Thunk<'gc>>>,
pub env: GcEnv<'gc>,
}
#[derive(Debug)]
pub struct PendingLoad {
pub path: PathBuf,
pub scope: Option<PendingScope>,
}
#[derive(Debug)]
pub struct PendingScope {
pub keys: HashSet<StringId>,
pub slot_id: u32,
}
/// Extra scope passed to a re-entrant compile from inside a running VM.
///
/// Currently only `ScopedImport` is produced (by the `scopedImport` builtin),
/// but the variant is kept open so REPL bindings could later land here too.
pub enum ExtraScope {
ScopedImport {
keys: HashSet<StringId>,
slot_id: u32,
},
}
pub enum OperandData {
Const(StaticValue),
BigInt(i64),
Local { layer: u8, idx: u32 },
BuiltinConst(StringId),
Builtins,
ReplBinding(StringId),
ScopedImportBinding { slot_id: u32, name: StringId },
}
+712
View File
@@ -0,0 +1,712 @@
#![allow(dead_code)]
use std::cell::RefCell;
use std::fmt;
use std::marker::PhantomData;
use std::mem::size_of;
use std::ops::Deref;
use fix_builtins::BuiltinId;
use fix_common::*;
use gc_arena::barrier::Unlock;
use gc_arena::collect::Trace;
use gc_arena::{Collect, Gc, GcRefLock, Mutation, RefLock};
use num_enum::TryFromPrimitive;
use smallvec::SmallVec;
use string_interner::Symbol;
use string_interner::symbol::SymbolU32;
use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue};
mod private {
pub trait Cealed {}
}
/// # Safety
///
/// TAG must be unique among all implementors.
#[allow(private_interfaces)]
pub unsafe trait Storable: private::Cealed {
const TAG: RawTag;
}
#[allow(private_bounds)]
pub trait InlineStorable: Storable + RawStore {}
pub trait GcStorable: Storable {}
macro_rules! define_value_types {
(
inline { $($itype:ty => $itag:expr, $iname:literal;)* }
gc { $($gtype:ty => $gtag:expr, $gname:literal;)* }
) => {
$(
#[allow(private_interfaces)]
unsafe impl Storable for $itype {
const TAG: RawTag = $itag;
}
impl InlineStorable for $itype {}
impl private::Cealed for $itype {}
)*
$(
#[allow(private_interfaces)]
unsafe impl Storable for $gtype {
const TAG: RawTag = $gtag;
}
impl GcStorable for $gtype {}
impl private::Cealed for $gtype {}
)*
const _: () = assert!(size_of::<Value<'static>>() == 8);
$(const _: () = assert!(size_of::<$itype>() <= 6);)*
const _: () = {
let tags: &[(bool, u8)] = &[$(RawTag::neg_val($itag)),*, $(RawTag::neg_val($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 {
$(<$gtype as Storable>::TAG => unsafe {
self.load_gc::<$gtype>().trace(cc)
},)*
$(<$itype as Storable>::TAG => (),)*
_ => unreachable!("invalid value tag"),
}
}
}
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),)*
_ => unreachable!("invalid value tag"),
}
}
}
};
}
define_value_types! {
inline {
i32 => RawTag::P1, "SmallInt";
bool => RawTag::P2, "Bool";
Null => RawTag::P3, "Null";
StringId => RawTag::P4, "SmallString";
PrimOp => RawTag::P5, "PrimOp";
Path => RawTag::N6, "Path";
}
gc {
i64 => RawTag::P6, "BigInt";
NixString => RawTag::P7, "String";
AttrSet<'_> => RawTag::N1, "AttrSet";
List<'_> => RawTag::N2, "List";
Thunk<'_> => RawTag::N3, "Thunk";
Closure<'_> => RawTag::N4, "Closure";
PrimOpApp<'_> => RawTag::N5, "PrimOpApp";
}
}
/// # Nix runtime value representation
///
/// NaN-boxed value fitting in 8 bytes.
#[derive(Copy, Clone)]
#[repr(transparent)]
pub struct Value<'gc> {
raw: RawBox,
_marker: PhantomData<Gc<'gc, ()>>,
}
impl Default for Value<'_> {
#[inline(always)]
fn default() -> Self {
Self::new_inline(Null)
}
}
impl<'gc> Value<'gc> {
#[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)
}
}
#[inline(always)]
const fn tag(self) -> Option<RawTag> {
self.raw.tag()
}
}
impl<'gc> Value<'gc> {
#[inline]
pub fn new_float(val: f64) -> Self {
Self {
raw: RawBox::from_float(val),
_marker: PhantomData,
}
}
#[inline]
pub fn new_inline<T: InlineStorable>(val: T) -> Self {
Self::from_raw_value(RawValue::store(T::TAG, val))
}
#[inline]
pub fn new_gc<T: GcStorable>(gc: Gc<'gc, T>) -> Self {
let ptr = Gc::as_ptr(gc);
Self::from_raw_value(RawValue::store(T::TAG, ptr))
}
#[inline]
pub fn make_int(val: i64, mc: &Mutation<'gc>) -> Self {
if val >= i32::MIN as i64 && val <= i32::MAX as i64 {
Value::new_inline(val as i32)
} else {
Value::new_gc(Gc::new(mc, val))
}
}
}
impl<'gc> Value<'gc> {
#[inline]
pub fn is_float(self) -> bool {
self.raw.is_float()
}
#[inline]
pub fn is<T: Storable>(self) -> bool {
self.tag() == Some(T::TAG)
}
}
impl<'gc> Value<'gc> {
#[inline]
pub fn as_float(self) -> Option<f64> {
self.raw.float().copied()
}
#[inline]
pub 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 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
}
}
#[inline]
pub fn to_bits(self) -> u64 {
self.raw.to_bits()
}
#[inline]
pub fn as_num(self) -> Option<NixNum> {
if let Some(i) = self.as_inline::<i32>() {
Some(NixNum::Int(i as i64))
} else if let Some(gc_i) = self.as_gc::<i64>() {
Some(NixNum::Int(*gc_i))
} else {
self.as_float().map(NixNum::Float)
}
}
#[inline]
pub fn restrict(self) -> Result<StrictValue<'gc>, Gc<'gc, Thunk<'gc>>> {
if let Some(thunk) = self.as_gc::<Thunk<'gc>>() {
Err(thunk)
} else {
Ok(StrictValue(self))
}
}
#[inline]
pub fn ty(self) -> NixType {
if self.is_float() {
NixType::Float
} else if self.is::<i32>() || self.is::<i64>() {
NixType::Int
} else if self.is::<bool>() {
NixType::Bool
} else if self.is::<Null>() {
NixType::Null
} else if self.is::<StringId>() {
NixType::String
} else if self.is::<PrimOp>() {
NixType::PrimOp
} else if self.is::<NixString>() {
NixType::String
} else if self.is::<Path>() {
NixType::Path
} else if self.is::<AttrSet>() {
NixType::AttrSet
} else if self.is::<List>() {
NixType::List
} else if self.is::<Thunk>() {
NixType::Thunk
} else if self.is::<Closure>() {
NixType::Closure
} else if self.is::<PrimOpApp>() {
NixType::PrimOpApp
} else {
unreachable!("value has no recognized type tag")
}
}
#[inline]
pub fn expect_inline<T: InlineStorable>(self) -> Result<T, NixType> {
self.as_inline::<T>().ok_or_else(|| self.ty())
}
#[inline]
pub fn expect_gc<T: GcStorable>(self) -> Result<Gc<'gc, T>, NixType> {
self.as_gc::<T>().ok_or_else(|| self.ty())
}
#[inline]
pub fn expect_num(self) -> Result<NixNum, NixType> {
self.as_num().ok_or_else(|| self.ty())
}
#[inline]
pub fn expect_bool(self) -> Result<bool, NixType> {
self.as_inline::<bool>().ok_or_else(|| self.ty())
}
#[inline]
pub fn expect_float(self) -> Result<f64, NixType> {
self.as_float().ok_or_else(|| self.ty())
}
}
#[derive(Copy, Clone, Default)]
#[repr(transparent)]
pub struct StaticValue(Value<'static>);
impl<'gc> From<StaticValue> for Value<'gc> {
#[inline]
fn from(value: StaticValue) -> Self {
// SAFETY: StaticValue is guaranteed to not contain any `Gc`.
unsafe { std::mem::transmute::<Value<'static>, Value<'gc>>(value.0) }
}
}
impl StaticValue {
#[inline]
pub fn new_float(val: f64) -> Self {
Self(Value::new_float(val))
}
#[inline]
pub fn new_inline<T: InlineStorable>(val: T) -> Self {
Self(Value::new_inline(val))
}
#[inline]
pub fn new_primop(id: BuiltinId, arity: u8, dispatch_ip: u32) -> Self {
Self(Value::new_inline(PrimOp {
id,
arity,
dispatch_ip,
}))
}
#[inline]
pub fn is_float(self) -> bool {
self.0.is_float()
}
#[inline]
pub fn is<T: InlineStorable>(self) -> bool {
self.0.is::<T>()
}
#[inline]
pub fn as_float(self) -> Option<f64> {
self.0.as_float()
}
#[inline]
pub fn as_inline<T: InlineStorable>(self) -> Option<T> {
self.0.as_inline::<T>()
}
#[inline]
pub fn to_bits(self) -> u64 {
self.0.raw.to_bits()
}
}
#[derive(Clone, Copy, Debug)]
pub 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"),
)
}
}
/// A canonicalized absolute path. Inline value carrying an interned
/// `StringId` whose contents are the path's absolute, dot-resolved form.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Path(pub StringId);
impl RawStore for Path {
fn to_val(self, value: &mut RawValue) {
self.0.to_val(value);
}
fn from_val(value: &RawValue) -> Self {
Self(StringId::from_val(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 struct NixString {
data: Box<str>,
// TODO: string context for derivation dependency tracking
}
impl NixString {
pub fn new(s: impl Into<Box<str>>) -> Self {
Self { data: s.into() }
}
pub 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, Default)]
#[collect(no_drop)]
pub struct AttrSet<'gc> {
pub entries: SmallVec<[(StringId, Value<'gc>); 4]>,
}
impl<'gc> AttrSet<'gc> {
pub fn from_sorted_unchecked(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self {
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
Self { entries }
}
pub fn lookup(&self, key: StringId) -> Option<Value<'gc>> {
self.entries
.binary_search_by_key(&key, |(k, _)| *k)
.ok()
.map(|i| self.entries[i].1)
}
pub fn has(&self, key: StringId) -> bool {
self.entries.binary_search_by_key(&key, |(k, _)| *k).is_ok()
}
pub 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]);
i += 1;
}
Greater => {
entries.push(other.entries[j]);
j += 1;
}
Equal => {
entries.push(other.entries[j]);
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, Default)]
#[repr(transparent)]
#[collect(no_drop)]
pub struct List<'gc> {
pub inner: RefLock<SmallVec<[Value<'gc>; 4]>>,
}
impl<'gc> List<'gc> {
pub fn new(mc: &Mutation<'gc>, data: SmallVec<[Value<'gc>; 4]>) -> Gc<'gc, Self> {
Gc::new(
mc,
Self {
inner: RefLock::new(data),
},
)
}
pub fn new_gc(mc: &Mutation<'gc>) -> Gc<'gc, Self> {
Gc::new(mc, Self::default())
}
}
impl<'gc> Unlock for List<'gc> {
type Unlocked = RefCell<SmallVec<[Value<'gc>; 4]>>;
unsafe fn unlock_unchecked(&self) -> &Self::Unlocked {
unsafe { self.inner.unlock_unchecked() }
}
}
pub type Thunk<'gc> = RefLock<ThunkState<'gc>>;
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub enum ThunkState<'gc> {
Pending { ip: usize, env: GcEnv<'gc> },
Apply { func: Value<'gc>, arg: Value<'gc> },
Blackhole,
Evaluated(StrictValue<'gc>),
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub struct Env<'gc> {
pub locals: SmallVec<[Value<'gc>; 4]>,
pub prev: Option<GcEnv<'gc>>,
}
pub type GcEnv<'gc> = GcRefLock<'gc, Env<'gc>>;
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub struct WithEnv<'gc> {
pub env: Value<'gc>,
pub prev: Option<GcWithEnv<'gc>>,
}
pub type GcWithEnv<'gc> = Gc<'gc, WithEnv<'gc>>;
impl<'gc> Env<'gc> {
pub fn empty() -> Self {
Env {
locals: SmallVec::new(),
prev: None,
}
}
pub 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 struct Closure<'gc> {
pub ip: u32,
pub n_locals: u32,
pub env: Gc<'gc, RefLock<Env<'gc>>>,
pub pattern: Option<Gc<'gc, PatternInfo>>,
}
#[derive(Collect, Debug)]
#[collect(require_static)]
pub struct PatternInfo {
pub required: SmallVec<[StringId; 4]>,
pub optional: SmallVec<[StringId; 4]>,
pub ellipsis: bool,
pub param_spans: Box<[(StringId, u32)]>,
}
#[repr(packed, Rust)]
#[derive(Clone, Copy, Debug, Collect)]
#[collect(require_static)]
pub struct PrimOp {
pub id: BuiltinId,
pub arity: u8,
pub dispatch_ip: u32,
}
impl RawStore for PrimOp {
fn to_val(self, value: &mut RawValue) {
let bytes = self.dispatch_ip.to_le_bytes();
value.set_data([
self.id as u8,
self.arity,
bytes[0],
bytes[1],
bytes[2],
bytes[3],
]);
}
fn from_val(value: &RawValue) -> Self {
let [id, arity, bytes @ ..] = *value.data();
Self {
id: BuiltinId::try_from_primitive(id).expect("invalid BuiltinId"),
arity,
dispatch_ip: u32::from_le_bytes(bytes),
}
}
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub struct PrimOpApp<'gc> {
pub primop: PrimOp,
pub arity: u8,
pub args: [Value<'gc>; 3],
}
#[derive(Copy, Clone, Default, Collect)]
#[repr(transparent)]
#[collect(no_drop)]
pub struct StrictValue<'gc>(Value<'gc>);
impl<'gc> StrictValue<'gc> {
#[inline]
pub fn relax(self) -> Value<'gc> {
self.0
}
}
impl<'gc> Deref for StrictValue<'gc> {
type Target = Value<'gc>;
#[inline]
fn deref(&self) -> &Value<'gc> {
&self.0
}
}
impl fmt::Debug for StrictValue<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Collect)]
#[collect(require_static)]
pub enum NixType {
Int,
Float,
Bool,
Null,
String,
Path,
AttrSet,
List,
Thunk,
Closure,
PrimOp,
PrimOpApp,
}
impl NixType {
pub fn display(self) -> &'static str {
use NixType::*;
match self {
Int => "an integer",
Float => "a float",
Bool => "a boolean",
Null => "null",
String => "a string",
Path => "a path",
AttrSet => "a set",
List => "a list",
Thunk => "a thunk",
Closure => "a function",
PrimOp => "a built-in function",
PrimOpApp => "a partially applied built-in function",
}
}
}
impl std::fmt::Display for NixType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.display())
}
}
pub enum NixNum {
Int(i64),
Float(f64),
}
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "fix-builtins"
version = "0.1.0"
edition = "2024"
[dependencies]
num_enum = { workspace = true }
gc-arena = { workspace = true }
+386
View File
@@ -0,0 +1,386 @@
use gc_arena::Collect;
use num_enum::TryFromPrimitive;
macro_rules! define_builtins {
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
/// Builtin function registry.
/// Array index IS the PrimOp id. (name, arity) pairs.
pub const BUILTINS: &[(&str, u8)] = &[
$(($name, $arity),)*
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, Collect)]
#[repr(u8)]
#[collect(require_static)]
pub enum BuiltinId {
$($variant,)*
}
};
}
define_builtins! {
("abort", Abort, 1),
("__add", Add, 2),
("__addErrorContext", AddErrorContext, 2),
("__all", All, 2),
("__any", Any, 2),
("__appendContext", AppendContext, 2),
("__attrNames", AttrNames, 1),
("__attrValues", AttrValues, 1),
("baseNameOf", BaseNameOf, 1),
("__bitAnd", BitAnd, 2),
("__bitOr", BitOr, 2),
("__bitXor", BitXor, 2),
("break", Break, 1),
("__catAttrs", CatAttrs, 2),
("__ceil", Ceil, 1),
("__compareVersions", CompareVersions, 2),
("__concatLists", ConcatLists, 1),
("__concatMap", ConcatMap, 2),
("__concatStringsSep", ConcatStringsSep, 2),
("__convertHash", ConvertHash, 1),
("__deepSeq", DeepSeq, 2),
("derivation", Derivation, 1),
("derivationStrict", DerivationStrict, 1),
("dirOf", DirOf, 1),
("__div", Div, 2),
("__elem", Elem, 2),
("__elemAt", ElemAt, 2),
("fetchGit", FetchGit, 1),
("fetchMercurial", FetchMercurial, 1),
("fetchTarball", FetchTarball, 1),
("fetchTree", FetchTree, 1),
("__fetchurl", FetchUrl, 1),
("__filter", Filter, 2),
("__filterSource", FilterSource, 2),
("__findFile", FindFile, 2),
("__floor", Floor, 1),
("__foldl'", FoldlStrict, 3),
("__fromJSON", FromJSON, 1),
("fromTOML", FromTOML, 1),
("__functionArgs", FunctionArgs, 1),
("__genList", GenList, 2),
("__genericClosure", GenericClosure, 1),
("__getAttr", GetAttr, 2),
("__getContext", GetContext, 1),
("__getEnv", GetEnv, 1),
("__groupBy", GroupBy, 2),
("__hasAttr", HasAttr, 2),
("__hasContext", HasContext, 1),
("__hashFile", HashFile, 2),
("__hashString", HashString, 2),
("__head", Head, 1),
("import", Import, 1),
("__intersectAttrs", IntersectAttrs, 2),
("__isAttrs", IsAttrs, 1),
("__isBool", IsBool, 1),
("__isFloat", IsFloat, 1),
("__isFunction", IsFunction, 1),
("__isInt", IsInt, 1),
("__isList", IsList, 1),
("isNull", IsNull, 1),
("__isPath", IsPath, 1),
("__isString", IsString, 1),
("__length", Length, 1),
("__lessThan", LessThan, 2),
("__listToAttrs", ListToAttrs, 1),
("map", Map, 2),
("__mapAttrs", MapAttrs, 2),
("__match", Match, 2),
("__mul", Mul, 2),
("__parseDrvName", ParseDrvName, 1),
("__partition", Partition, 2),
("__path", Path, 1),
("__pathExists", PathExists, 1),
("placeholder", Placeholder, 1),
("__readDir", ReadDir, 1),
("__readFile", ReadFile, 1),
("__readFileType", ReadFileType, 1),
("removeAttrs", RemoveAttrs, 2),
("__replaceStrings", ReplaceStrings, 3),
("scopedImport", ScopedImport, 2),
("__seq", Seq, 2),
("__sort", Sort, 2),
("__split", Split, 2),
("__splitVersion", SplitVersion, 1),
("__storePath", StorePath, 1),
("__stringLength", StringLength, 1),
("__sub", Sub, 2),
("__substring", Substring, 3),
("__tail", Tail, 1),
("throw", Throw, 1),
("__toFile", ToFile, 2),
("__toJSON", ToJSON, 1),
("__toPath", ToPath, 1),
("toString", ToString, 1),
("__toXML", ToXML, 1),
("__trace", Trace, 2),
("__tryEval", TryEval, 1),
("__typeOf", TypeOf, 1),
("__unsafeDiscardStringContext", UnsafeDiscardStringContext, 1),
("__unsafeGetAttrPos", UnsafeGetAttrPos, 2),
("__warn", Warn, 2),
("__zipAttrsWith", ZipAttrsWith, 2),
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum PrimOpPhase {
Abort,
Add,
AddErrorContext,
All,
Any,
AppendContext,
AttrNames,
AttrValues,
BaseNameOf,
BitAnd,
BitOr,
BitXor,
Break,
CatAttrs,
Ceil,
CompareVersions,
ConcatLists,
ConcatMap,
ConcatStringsSep,
ConvertHash,
DeepSeq,
DeepSeqPush,
DeepSeqLoop,
Derivation,
DerivationStrict,
DirOf,
Div,
Elem,
ElemAt,
FetchGit,
FetchMercurial,
FetchTarball,
FetchTree,
FetchUrl,
FilterForceList,
FilterCallPred,
FilterCheck,
FilterSource,
FindFile,
Floor,
FoldlStrict,
FoldlStrictEmpty,
FoldlStrictCall1,
FoldlStrictCall2,
FoldlStrictUpdate,
FromJSON,
FromTOML,
FunctionArgs,
GenList,
GenericClosure,
GetAttr,
GetContext,
GetEnv,
GroupBy,
HasAttr,
HasContext,
HashFile,
HashString,
Head,
Import,
IntersectAttrs,
IsAttrs,
IsBool,
IsFloat,
IsFunction,
IsInt,
IsList,
IsNull,
IsPath,
IsString,
Length,
LessThan,
ListToAttrs,
Map,
MapAttrs,
Match,
Mul,
ParseDrvName,
Partition,
Path,
PathExists,
Placeholder,
ReadDir,
ReadFile,
ReadFileType,
RemoveAttrs,
ReplaceStrings,
ScopedImport,
Seq,
Sort,
Split,
SplitVersion,
StorePath,
StringLength,
Sub,
Substring,
Tail,
Throw,
ToFile,
ToJSON,
ToPath,
ToString,
ToXML,
Trace,
TryEval,
TypeOf,
UnsafeDiscardStringContext,
UnsafeGetAttrPos,
Warn,
ZipAttrsWith,
ForceResultShallow,
ForceResultShallowPush,
ForceResultShallowLoop,
ForceResultDeepFinish,
// TODO: split into separate enums
CallPattern,
CallFunctor1,
CallFunctor2,
ImportFinalize,
ScopedImportFinalize,
Illegal,
}
impl TryFrom<u8> for PrimOpPhase {
type Error = u8;
fn try_from(value: u8) -> Result<Self, Self::Error> {
if (0..Self::Illegal as u8).contains(&value) {
Ok(unsafe { std::mem::transmute::<u8, Self>(value) })
} else {
Err(value)
}
}
}
impl BuiltinId {
#[inline(always)]
pub fn entry_phase(self) -> PrimOpPhase {
use BuiltinId::*;
match self {
Abort => PrimOpPhase::Abort,
Add => PrimOpPhase::Add,
AddErrorContext => PrimOpPhase::AddErrorContext,
All => PrimOpPhase::All,
Any => PrimOpPhase::Any,
AppendContext => PrimOpPhase::AppendContext,
AttrNames => PrimOpPhase::AttrNames,
AttrValues => PrimOpPhase::AttrValues,
BaseNameOf => PrimOpPhase::BaseNameOf,
BitAnd => PrimOpPhase::BitAnd,
BitOr => PrimOpPhase::BitOr,
BitXor => PrimOpPhase::BitXor,
Break => PrimOpPhase::Break,
CatAttrs => PrimOpPhase::CatAttrs,
Ceil => PrimOpPhase::Ceil,
CompareVersions => PrimOpPhase::CompareVersions,
ConcatLists => PrimOpPhase::ConcatLists,
ConcatMap => PrimOpPhase::ConcatMap,
ConcatStringsSep => PrimOpPhase::ConcatStringsSep,
ConvertHash => PrimOpPhase::ConvertHash,
DeepSeq => PrimOpPhase::DeepSeq,
Derivation => PrimOpPhase::Derivation,
DerivationStrict => PrimOpPhase::DerivationStrict,
DirOf => PrimOpPhase::DirOf,
Div => PrimOpPhase::Div,
Elem => PrimOpPhase::Elem,
ElemAt => PrimOpPhase::ElemAt,
FetchGit => PrimOpPhase::FetchGit,
FetchMercurial => PrimOpPhase::FetchMercurial,
FetchTarball => PrimOpPhase::FetchTarball,
FetchTree => PrimOpPhase::FetchTree,
FetchUrl => PrimOpPhase::FetchUrl,
Filter => PrimOpPhase::FilterForceList,
FilterSource => PrimOpPhase::FilterSource,
FindFile => PrimOpPhase::FindFile,
Floor => PrimOpPhase::Floor,
FoldlStrict => PrimOpPhase::FoldlStrict,
FromJSON => PrimOpPhase::FromJSON,
FromTOML => PrimOpPhase::FromTOML,
FunctionArgs => PrimOpPhase::FunctionArgs,
GenList => PrimOpPhase::GenList,
GenericClosure => PrimOpPhase::GenericClosure,
GetAttr => PrimOpPhase::GetAttr,
GetContext => PrimOpPhase::GetContext,
GetEnv => PrimOpPhase::GetEnv,
GroupBy => PrimOpPhase::GroupBy,
HasAttr => PrimOpPhase::HasAttr,
HasContext => PrimOpPhase::HasContext,
HashFile => PrimOpPhase::HashFile,
HashString => PrimOpPhase::HashString,
Head => PrimOpPhase::Head,
Import => PrimOpPhase::Import,
IntersectAttrs => PrimOpPhase::IntersectAttrs,
IsAttrs => PrimOpPhase::IsAttrs,
IsBool => PrimOpPhase::IsBool,
IsFloat => PrimOpPhase::IsFloat,
IsFunction => PrimOpPhase::IsFunction,
IsInt => PrimOpPhase::IsInt,
IsList => PrimOpPhase::IsList,
IsNull => PrimOpPhase::IsNull,
IsPath => PrimOpPhase::IsPath,
IsString => PrimOpPhase::IsString,
Length => PrimOpPhase::Length,
LessThan => PrimOpPhase::LessThan,
ListToAttrs => PrimOpPhase::ListToAttrs,
Map => PrimOpPhase::Map,
MapAttrs => PrimOpPhase::MapAttrs,
Match => PrimOpPhase::Match,
Mul => PrimOpPhase::Mul,
ParseDrvName => PrimOpPhase::ParseDrvName,
Partition => PrimOpPhase::Partition,
Path => PrimOpPhase::Path,
PathExists => PrimOpPhase::PathExists,
Placeholder => PrimOpPhase::Placeholder,
ReadDir => PrimOpPhase::ReadDir,
ReadFile => PrimOpPhase::ReadFile,
ReadFileType => PrimOpPhase::ReadFileType,
RemoveAttrs => PrimOpPhase::RemoveAttrs,
ReplaceStrings => PrimOpPhase::ReplaceStrings,
ScopedImport => PrimOpPhase::ScopedImport,
Seq => PrimOpPhase::Seq,
Sort => PrimOpPhase::Sort,
Split => PrimOpPhase::Split,
SplitVersion => PrimOpPhase::SplitVersion,
StorePath => PrimOpPhase::StorePath,
StringLength => PrimOpPhase::StringLength,
Sub => PrimOpPhase::Sub,
Substring => PrimOpPhase::Substring,
Tail => PrimOpPhase::Tail,
Throw => PrimOpPhase::Throw,
ToFile => PrimOpPhase::ToFile,
ToJSON => PrimOpPhase::ToJSON,
ToPath => PrimOpPhase::ToPath,
ToString => PrimOpPhase::ToString,
ToXML => PrimOpPhase::ToXML,
Trace => PrimOpPhase::Trace,
TryEval => PrimOpPhase::TryEval,
TypeOf => PrimOpPhase::TypeOf,
UnsafeDiscardStringContext => PrimOpPhase::UnsafeDiscardStringContext,
UnsafeGetAttrPos => PrimOpPhase::UnsafeGetAttrPos,
Warn => PrimOpPhase::Warn,
ZipAttrsWith => PrimOpPhase::ZipAttrsWith,
}
}
}
impl PrimOpPhase {
pub fn ip(self) -> u32 {
self as u32 * 2
}
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "fix-codegen"
version = "0.1.0"
edition = "2024"
[dependencies]
hashbrown = { workspace = true }
num_enum = { workspace = true }
rnix = { workspace = true }
string-interner = { workspace = true }
colored = "3.1.1"
fix-builtins = { path = "../fix-builtins" }
fix-common = { path = "../fix-common" }
fix-ir = { path = "../fix-ir" }
+467
View File
@@ -0,0 +1,467 @@
use std::fmt::Write;
use colored::Colorize;
use fix_builtins::BuiltinId;
use num_enum::TryFromPrimitive;
use crate::{InstructionPtr, Op, OperandType};
pub trait DisassemblerContext {
fn resolve_string(&self, id: u32) -> &str;
fn get_code(&self) -> &[u8];
}
pub struct Disassembler<'a, Ctx> {
code: &'a [u8],
ctx: &'a Ctx,
pc: usize,
}
impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
pub fn new(ip: InstructionPtr, ctx: &'a Ctx) -> Self {
Self {
code: ctx.get_code(),
ctx,
pc: ip.0,
}
}
#[inline(always)]
fn read_u8(&mut self) -> u8 {
let b = self.code[self.pc];
self.pc += 1;
b
}
#[inline(always)]
fn read_u16(&mut self) -> u16 {
let bytes = self.code[self.pc..self.pc + 2]
.try_into()
.expect("no enough bytes");
self.pc += 2;
u16::from_le_bytes(bytes)
}
#[inline(always)]
fn read_u32(&mut self) -> u32 {
let bytes = self.code[self.pc..self.pc + 4]
.try_into()
.expect("no enough bytes");
self.pc += 4;
u32::from_le_bytes(bytes)
}
#[inline(always)]
fn read_i32(&mut self) -> i32 {
let bytes = self.code[self.pc..self.pc + 4]
.try_into()
.expect("no enough bytes");
self.pc += 4;
i32::from_le_bytes(bytes)
}
#[inline(always)]
fn read_i64(&mut self) -> i64 {
let bytes = self.code[self.pc..self.pc + 8]
.try_into()
.expect("no enough bytes");
self.pc += 8;
i64::from_le_bytes(bytes)
}
#[inline(always)]
fn read_f64(&mut self) -> f64 {
let bytes = self.code[self.pc..self.pc + 8]
.try_into()
.expect("no enough bytes");
self.pc += 8;
f64::from_le_bytes(bytes)
}
#[inline(always)]
fn read_operand_data(&mut self) {
use OperandType::*;
let tag = self.read_u8();
let ty = OperandType::try_from_primitive(tag).expect("invalid operand type");
match ty {
Const => {
self.read_u32();
}
BigInt => {
self.read_i64();
}
Local => {
self.read_u8();
self.read_u32();
}
BuiltinConst => {
self.read_u32();
}
Builtins => {}
ReplBinding => {
self.read_u32();
}
ScopedImportBinding => {
self.read_u32();
self.read_u32();
}
}
}
pub fn disassemble(&mut self) -> String {
self.disassemble_impl(false)
}
pub fn disassemble_colored(&mut self) -> String {
self.disassemble_impl(true)
}
fn disassemble_impl(&mut self, color: bool) -> String {
let mut out = String::new();
if color {
let _ = writeln!(out, "{}", "=== Bytecode Disassembly ===".bold().white());
let _ = writeln!(
out,
"{} {}",
"Length:".white(),
format!("{} bytes", self.code.len()).cyan()
);
} else {
let _ = writeln!(out, "=== Bytecode Disassembly ===");
let _ = writeln!(out, "Length: {} bytes", self.code.len());
}
while self.pc < self.code.len() {
let start_pos = self.pc;
let op_byte = self.read_u8();
let (mnemonic, args) = self.decode_instruction(op_byte, start_pos);
let bytes_slice = &self.code[start_pos + 1..self.pc];
let mut chunks = bytes_slice.chunks(4);
let first_chunk = chunks.next().unwrap_or(&[]);
let bytes_str = {
let mut temp = format!("{:02x}", self.code[start_pos]);
for b in first_chunk {
let _ = write!(&mut temp, " {:02x}", b);
}
temp
};
if color {
let sep = if args.is_empty() { "" } else { " " };
let _ = writeln!(
out,
"{} {:<14} | {}{}{}",
format!("{:04x}", start_pos).dimmed(),
bytes_str.green(),
mnemonic.yellow().bold(),
sep,
args.cyan()
);
} else {
let op_str = if args.is_empty() {
mnemonic.to_string()
} else {
format!("{} {}", mnemonic, args)
};
let _ = writeln!(out, "{:04x} {:<14} | {}", start_pos, bytes_str, op_str);
}
for chunk in chunks {
let bytes_str = {
let mut temp = String::from(" ");
for b in chunk {
let _ = write!(&mut temp, " {:02x}", b);
}
temp
};
let extra_width = if start_pos > 0 {
start_pos.ilog2() >> 4
} else {
0
};
if color {
let _ = write!(out, " ");
for _ in 0..extra_width {
let _ = write!(out, " ");
}
let _ = writeln!(out, " {:<14} |", bytes_str.green());
} else {
let _ = write!(out, " ");
for _ in 0..extra_width {
let _ = write!(out, " ");
}
let _ = writeln!(out, " {:<14} |", bytes_str);
}
}
}
out
}
fn decode_instruction(&mut self, op_byte: u8, current_pc: usize) -> (&'static str, String) {
let op = Op::try_from_primitive(op_byte).expect("invalid op code");
match op {
Op::PushSmi => {
let val = self.read_i32();
("PushSmi", format!("{}", val))
}
Op::PushBigInt => {
let val = self.read_i64();
("PushBigInt", format!("{}", val))
}
Op::PushFloat => {
let val = self.read_f64();
("PushFloat", format!("{}", val))
}
Op::PushString => {
let idx = self.read_u32();
let s = self.ctx.resolve_string(idx);
let len = s.len();
let mut s_fmt = format!("{:?}", s);
if s_fmt.len() > 60 {
s_fmt.truncate(57);
#[allow(clippy::unwrap_used)]
write!(s_fmt, "...\" (total {len} bytes)").unwrap();
}
("PushString", format!("@{} {}", idx, s_fmt))
}
Op::PushNull => ("PushNull", String::new()),
Op::PushTrue => ("PushTrue", String::new()),
Op::PushFalse => ("PushFalse", String::new()),
Op::LoadLocal => {
let idx = self.read_u32();
("LoadLocal", format!("[{}]", idx))
}
Op::LoadOuter => {
let depth = self.read_u8();
let idx = self.read_u32();
("LoadOuter", format!("depth={} [{}]", depth, idx))
}
Op::StoreLocal => {
let idx = self.read_u32();
("StoreLocal", format!("[{}]", idx))
}
Op::AllocLocals => {
let count = self.read_u32();
("AllocLocals", format!("count={}", count))
}
Op::MakeThunk => {
let offset = self.read_u32();
("MakeThunk", format!("-> {:04x}", offset))
}
Op::MakeClosure => {
let offset = self.read_u32();
let slots = self.read_u32();
("MakeClosure", format!("-> {:04x} slots={}", offset, slots))
}
Op::MakePatternClosure => {
let offset = self.read_u32();
let slots = self.read_u32();
let req_count = self.read_u16();
let opt_count = self.read_u16();
let ellipsis = self.read_u8() != 0;
let mut arg_str = format!(
"-> {:04x} slots={} req={} opt={} ...={})",
offset, slots, req_count, opt_count, ellipsis
);
arg_str.push_str(" Args=[");
for _ in 0..req_count {
let idx = self.read_u32();
arg_str.push_str(&format!("Req({}) ", self.ctx.resolve_string(idx)));
}
for _ in 0..opt_count {
let idx = self.read_u32();
arg_str.push_str(&format!("Opt({}) ", self.ctx.resolve_string(idx)));
}
let total_args = req_count + opt_count;
for _ in 0..total_args {
let _name_idx = self.read_u32();
let _span_id = self.read_u32();
}
arg_str.push(']');
("MakePatternClosure", arg_str)
}
Op::Call => {
self.read_operand_data();
("Call", "arg=?".into())
}
Op::DispatchPrimOp => {
let id = BuiltinId::try_from_primitive(self.read_u8()).expect("invalid builtin id");
("DispatchPrimOp", format!("id={id:?}"))
}
Op::MakeAttrs => {
let static_count = self.read_u32();
let dynamic_count = self.read_u32();
let mut args = format!("static={} dynamic={}", static_count, dynamic_count);
for _ in 0..static_count {
let key_id = self.read_u32();
let _ = write!(args, " [{}={}", self.ctx.resolve_string(key_id), key_id);
self.read_operand_data();
let _span_id = self.read_u32();
args.push(']');
}
for _ in 0..dynamic_count {
let _ = write!(args, " [dyn");
self.read_operand_data();
let _span_id = self.read_u32();
args.push(']');
}
("MakeAttrs", args)
}
Op::MakeEmptyAttrs => ("MakeEmptyAttrs", String::new()),
Op::SelectStatic => {
let span_id = self.read_u32();
let key_id = self.read_u32();
(
"SelectStatic",
format!("key={} span={}", self.ctx.resolve_string(key_id), span_id),
)
}
Op::SelectDynamic => {
let span_id = self.read_u32();
("SelectDynamic", format!("span={}", span_id))
}
Op::HasAttrPathStatic => {
let span_id = self.read_u32();
let key_id = self.read_u32();
(
"HasAttrPathStatic",
format!("key={} span={}", self.ctx.resolve_string(key_id), span_id),
)
}
Op::HasAttrPathDynamic => {
let span_id = self.read_u32();
("HasAttrPathDynamic", format!("span={}", span_id))
}
Op::HasAttrStatic => {
let key_id = self.read_u32();
(
"HasAttrStatic",
format!("key={}", self.ctx.resolve_string(key_id)),
)
}
Op::HasAttrDynamic => ("HasAttrDynamic", String::new()),
Op::HasAttrResolve => ("HasAttrResolve", String::new()),
Op::JumpIfSelectSucceeded => {
let offset = self.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
(
"JumpIfSelectSucceeded",
format!("-> {:04x} offset={}", target, offset),
)
}
Op::JumpIfSelectFailed => {
let offset = self.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
(
"JumpIfSelectFailed",
format!("-> {:04x} offset={}", target, offset),
)
}
Op::MakeList => {
let count = self.read_u32();
for _ in 0..count {
self.read_operand_data();
}
("MakeList", format!("size={}", count))
}
Op::MakeEmptyList => ("MakeEmptyList", String::new()),
Op::OpAdd => ("OpAdd", String::new()),
Op::OpSub => ("OpSub", String::new()),
Op::OpMul => ("OpMul", String::new()),
Op::OpDiv => ("OpDiv", String::new()),
Op::OpEq => ("OpEq", String::new()),
Op::OpNeq => ("OpNeq", String::new()),
Op::OpLt => ("OpLt", String::new()),
Op::OpGt => ("OpGt", String::new()),
Op::OpLeq => ("OpLeq", String::new()),
Op::OpGeq => ("OpGeq", String::new()),
Op::OpConcat => ("OpConcat", String::new()),
Op::OpUpdate => ("OpUpdate", String::new()),
Op::OpNeg => ("OpNeg", String::new()),
Op::OpNot => ("OpNot", String::new()),
Op::JumpIfFalse => {
let offset = self.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
(
"JumpIfFalse",
format!("-> {:04x} offset={}", target, offset),
)
}
Op::JumpIfTrue => {
let offset = self.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
("JumpIfTrue", format!("-> {:04x} offset={}", target, offset))
}
Op::Jump => {
let offset = self.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
("Jump", format!("-> {:04x} offset={}", target, offset))
}
Op::ConcatStrings => {
let count = self.read_u16();
let force = self.read_u8();
("ConcatStrings", format!("count={} force={}", count, force))
}
Op::CoerceToString => ("CoerceToString", String::new()),
Op::ResolvePath => {
let dir_id = self.read_u32();
let dir = self.ctx.resolve_string(dir_id);
("ResolvePath", format!("dir={:?}", dir))
}
Op::Assert => {
let raw_idx = self.read_u32();
let span_id = self.read_u32();
("Assert", format!("text_id={} span={}", raw_idx, span_id))
}
Op::LookupWith => {
let idx = self.read_u32();
let name = self.ctx.resolve_string(idx);
let n = self.read_u8();
for _ in 0..n {
self.read_operand_data();
}
("LookupWith", format!("sym={:?} n={}", name, n))
}
Op::LoadBuiltins => ("LoadBuiltins", String::new()),
Op::LoadBuiltin => {
let id = self.read_u8();
("LoadBuiltin", format!("id={}", id))
}
Op::LoadReplBinding => {
let idx = self.read_u32();
let name = self.ctx.resolve_string(idx);
("LoadReplBinding", format!("{:?}", name))
}
Op::LoadScopedBinding => {
let slot = self.read_u32();
let idx = self.read_u32();
let name = self.ctx.resolve_string(idx);
("LoadScopedBinding", format!("slot={} {:?}", slot, name))
}
Op::Return => ("Return", String::new()),
Op::Illegal => ("Illegal", String::new()),
}
}
}
+903
View File
@@ -0,0 +1,903 @@
use fix_builtins::{BUILTINS, BuiltinId};
use fix_common::StringId;
use fix_ir::{Attr, BinOpKind, Ir, MaybeThunk, Param, RawIrRef, ThunkId, UnOpKind};
use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use rnix::TextRange;
use string_interner::Symbol as _;
pub mod disassembler;
pub struct InstructionPtr(pub usize);
pub trait BytecodeContext {
fn intern_string(&mut self, s: &str) -> StringId;
fn register_span(&mut self, range: TextRange) -> u32;
fn get_code(&self) -> &[u8];
fn get_code_mut(&mut self) -> &mut Vec<u8>;
fn add_constant(&mut self, val: Const) -> u32;
fn current_source_dir(&mut self) -> StringId;
fn current_scope_slot(&self) -> Option<u32>;
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
#[allow(clippy::enum_variant_names)]
pub enum Op {
PushSmi,
PushBigInt,
PushFloat,
PushString,
PushNull,
PushTrue,
PushFalse,
LoadLocal,
LoadOuter,
StoreLocal,
AllocLocals,
MakeThunk,
MakeClosure,
MakePatternClosure,
Call,
DispatchPrimOp,
MakeAttrs,
MakeEmptyAttrs,
SelectStatic,
SelectDynamic,
HasAttrPathStatic,
HasAttrPathDynamic,
HasAttrStatic,
HasAttrDynamic,
HasAttrResolve,
JumpIfSelectSucceeded,
JumpIfSelectFailed,
MakeList,
MakeEmptyList,
OpAdd,
OpSub,
OpMul,
OpDiv,
OpEq,
OpNeq,
OpLt,
OpGt,
OpLeq,
OpGeq,
OpConcat,
OpUpdate,
OpNeg,
OpNot,
JumpIfFalse,
JumpIfTrue,
Jump,
CoerceToString,
ConcatStrings,
ResolvePath,
Assert,
LookupWith,
LoadBuiltins,
LoadBuiltin,
LoadReplBinding,
LoadScopedBinding,
Return,
Illegal,
}
struct ScopeInfo {
depth: u8,
thunk_map: HashMap<ThunkId, u32>,
}
struct BytecodeEmitter<'a, Ctx: BytecodeContext> {
ctx: &'a mut Ctx,
scope_stack: Vec<ScopeInfo>,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum OperandType {
Const,
BigInt,
Local,
BuiltinConst,
Builtins,
ReplBinding,
ScopedImportBinding,
}
pub enum Const {
Smi(i32),
Float(f64),
Bool(bool),
String(StringId),
Path(StringId),
PrimOp {
id: BuiltinId,
arity: u8,
dispatch_ip: u32,
},
Null,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum AttrKeyType {
Static,
Dynamic,
}
pub enum InlineOperand {
Const(Const),
BigInt(i64),
Local { layer: u8, local: u32 },
BuiltinConst(StringId),
Builtins,
ReplBinding(StringId),
ScopedImportBinding(StringId),
}
pub fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr {
let ip = ctx.get_code().len();
let mut emitter = BytecodeEmitter::new(ctx);
emitter.emit_toplevel(ir);
InstructionPtr(ip)
}
impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
fn new(ctx: &'a mut Ctx) -> Self {
Self {
ctx,
scope_stack: Vec::with_capacity(32),
}
}
#[must_use]
fn inline_maybe_thunk(&self, val: &MaybeThunk) -> InlineOperand {
use MaybeThunk::*;
match *val {
Int(x) => {
if let Ok(x) = x.try_into() {
InlineOperand::Const(Const::Smi(x))
} else {
InlineOperand::BigInt(x)
}
}
Float(x) => InlineOperand::Const(Const::Float(x)),
Bool(b) => InlineOperand::Const(Const::Bool(b)),
Null => InlineOperand::Const(Const::Null),
Str(id) => InlineOperand::Const(Const::String(id)),
Path(id) => InlineOperand::Const(Const::String(id)),
Thunk(id) => {
let (layer, local) = self.resolve_thunk(id);
InlineOperand::Local { layer, local }
}
Arg { layer } => InlineOperand::Local { layer, local: 0 },
Builtin(id) => {
let (_, arity) = BUILTINS[id as usize];
InlineOperand::Const(Const::PrimOp {
id,
arity,
dispatch_ip: id.entry_phase().ip(),
})
}
BuiltinConst(id) => InlineOperand::BuiltinConst(id),
Builtins => InlineOperand::Builtins,
ReplBinding(id) => InlineOperand::ReplBinding(id),
ScopedImportBinding(id) => InlineOperand::ScopedImportBinding(id),
}
}
fn emit_maybe_thunk(&mut self, val: &MaybeThunk) {
use InlineOperand::*;
let operand = self.inline_maybe_thunk(val);
match operand {
Const(val) => {
let idx = self.ctx.add_constant(val);
self.emit_u8(OperandType::Const as u8);
self.emit_u32(idx);
}
BigInt(val) => {
self.emit_u8(OperandType::BigInt as u8);
self.emit_i64(val);
}
Local { layer, local } => {
self.emit_u8(OperandType::Local as u8);
self.emit_u8(layer);
self.emit_u32(local);
}
BuiltinConst(id) => {
self.emit_u8(OperandType::BuiltinConst as u8);
self.emit_str_id(id);
}
Builtins => {
self.emit_u8(OperandType::Builtins as u8);
}
ReplBinding(id) => {
self.emit_u8(OperandType::ReplBinding as u8);
self.emit_str_id(id);
}
ScopedImportBinding(id) => {
self.emit_u8(OperandType::ScopedImportBinding as u8);
let slot = self
.ctx
.current_scope_slot()
.expect("ScopedImportBinding outside scoped compilation");
self.emit_u32(slot);
self.emit_str_id(id);
}
}
}
#[inline]
fn emit_op(&mut self, op: Op) {
self.ctx.get_code_mut().push(op as u8);
}
#[inline]
fn emit_bool(&mut self, val: bool) {
self.emit_u8(u8::from(val));
}
#[inline]
fn emit_u8(&mut self, val: u8) {
self.ctx.get_code_mut().push(val);
}
#[inline]
fn emit_u16(&mut self, val: u16) {
self.ctx
.get_code_mut()
.extend_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_u32(&mut self, val: u32) {
self.ctx
.get_code_mut()
.extend_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_i32(&mut self, val: i32) {
self.ctx
.get_code_mut()
.extend_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_i64(&mut self, val: i64) {
self.ctx
.get_code_mut()
.extend_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_f64(&mut self, val: f64) {
self.ctx
.get_code_mut()
.extend_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_i32_placeholder(&mut self) -> usize {
let offset = self.ctx.get_code_mut().len();
self.ctx.get_code_mut().extend_from_slice(&[0u8; 4]);
offset
}
#[inline]
fn patch_i32(&mut self, offset: usize, val: i32) {
self.ctx.get_code_mut()[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
}
#[inline]
fn emit_jump_placeholder(&mut self) -> usize {
self.emit_op(Op::Jump);
self.emit_i32_placeholder()
}
#[inline]
fn patch_jump_target(&mut self, placeholder_offset: usize) {
let current_pos = self.ctx.get_code_mut().len();
let relative_offset = (current_pos as i32) - (placeholder_offset as i32) - 4;
self.patch_i32(placeholder_offset, relative_offset);
}
#[inline]
fn emit_str_id(&mut self, id: StringId) {
self.ctx
.get_code_mut()
.extend_from_slice(&(id.0.to_usize() as u32).to_le_bytes());
}
fn current_depth(&self) -> u8 {
self.scope_stack.last().map_or(0, |s| s.depth)
}
fn resolve_thunk(&self, id: ThunkId) -> (u8, u32) {
for scope in self.scope_stack.iter().rev() {
if let Some(&local_idx) = scope.thunk_map.get(&id) {
let layer = self.current_depth() - scope.depth;
return (layer, local_idx);
}
}
panic!("ThunkId {:?} not found in any scope", id);
}
fn emit_load(&mut self, layer: u8, local: u32) {
if layer == 0 {
self.emit_op(Op::LoadLocal);
self.emit_u32(local);
} else {
self.emit_op(Op::LoadOuter);
self.emit_u8(layer);
self.emit_u32(local);
}
}
fn push_scope(&mut self, has_arg: bool, thunk_ids: &[ThunkId]) {
let depth = self.scope_stack.len().try_into().expect("scope too deep!");
let thunk_base = if has_arg { 1u32 } else { 0u32 };
let thunk_map = thunk_ids
.iter()
.enumerate()
.map(|(i, &id)| (id, thunk_base + i as u32))
.collect();
self.scope_stack.push(ScopeInfo { depth, thunk_map });
}
fn pop_scope(&mut self) {
self.scope_stack.pop();
}
fn emit_toplevel(&mut self, ir: RawIrRef<'_>) {
match ir {
&Ir::TopLevel { body, ref thunks } => {
let thunk_ids: Vec<ThunkId> = thunks.iter().map(|&(id, _)| id).collect();
self.push_scope(false, &thunk_ids);
if !thunks.is_empty() {
self.emit_op(Op::AllocLocals);
self.emit_u32(thunks.len().try_into().expect("too many thunks"));
}
self.emit_scope_thunks(thunks);
self.emit_expr(body);
self.emit_op(Op::Return);
self.pop_scope();
}
_ => {
self.push_scope(false, &[]);
self.emit_expr(ir);
self.emit_op(Op::Return);
self.pop_scope();
}
}
}
fn emit_scope_thunks(&mut self, thunks: &[(ThunkId, RawIrRef<'_>)]) {
for &(id, inner) in thunks {
let skip_patch = self.emit_jump_placeholder();
let entry_point = self.ctx.get_code_mut().len() as u32;
self.emit_expr(inner);
self.emit_op(Op::Return);
self.patch_jump_target(skip_patch);
self.emit_op(Op::MakeThunk);
self.emit_u32(entry_point);
let (_, local_idx) = self.resolve_thunk(id);
self.emit_op(Op::StoreLocal);
self.emit_u32(local_idx);
}
}
fn emit_expr(&mut self, ir: RawIrRef<'_>) {
match ir {
&Ir::Int(x) => {
if let Ok(x) = x.try_into() {
self.emit_op(Op::PushSmi);
self.emit_i32(x);
} else {
self.emit_op(Op::PushBigInt);
self.emit_i64(x);
}
}
&Ir::Float(x) => {
self.emit_op(Op::PushFloat);
self.emit_f64(x);
}
&Ir::Bool(true) => self.emit_op(Op::PushTrue),
&Ir::Bool(false) => self.emit_op(Op::PushFalse),
Ir::Null => self.emit_op(Op::PushNull),
&Ir::Str(id) => {
self.emit_op(Op::PushString);
self.emit_str_id(id);
}
&Ir::Path(p) => {
self.emit_expr(p);
self.emit_op(Op::ResolvePath);
let dir_id = self.ctx.current_source_dir();
self.emit_str_id(dir_id);
}
&Ir::If { cond, consq, alter } => {
self.emit_expr(cond);
self.emit_op(Op::JumpIfFalse);
let else_placeholder = self.emit_i32_placeholder();
let after_jif = self.ctx.get_code_mut().len();
self.emit_expr(consq);
self.emit_op(Op::Jump);
let end_placeholder = self.emit_i32_placeholder();
let after_jump = self.ctx.get_code_mut().len();
let else_offset = (after_jump as i32) - (after_jif as i32);
self.patch_i32(else_placeholder, else_offset);
self.emit_expr(alter);
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset);
}
&Ir::BinOp { lhs, rhs, kind } => {
self.emit_binop(lhs, rhs, kind);
}
&Ir::UnOp { rhs, kind } => match kind {
UnOpKind::Neg => {
self.emit_expr(rhs);
self.emit_op(Op::OpNeg);
}
UnOpKind::Not => {
self.emit_expr(rhs);
self.emit_op(Op::OpNot);
}
},
&Ir::Func {
body,
ref param,
ref thunks,
} => {
self.emit_func(thunks, param, body);
}
Ir::AttrSet { stcs, dyns } => {
self.emit_attrset(stcs, dyns);
}
Ir::List { items } => {
if items.is_empty() {
self.emit_op(Op::MakeEmptyList);
} else {
self.emit_op(Op::MakeList);
self.emit_u32(items.len() as u32);
for &item in items.iter() {
self.emit_maybe_thunk(item);
}
}
}
&Ir::Call { func, arg, .. } => {
self.emit_expr(func);
self.emit_op(Op::Call);
self.emit_maybe_thunk(arg);
}
&Ir::Arg { layer } => {
self.emit_load(layer, 0);
}
&Ir::TopLevel { body, ref thunks } => {
self.emit_toplevel_inner(body, thunks);
}
&Ir::Select {
expr,
ref attrpath,
default,
span,
} => {
self.emit_select(expr, attrpath, default, span);
}
Ir::Builtins => {
self.emit_op(Op::LoadBuiltins);
}
&Ir::Builtin(id) => {
self.emit_op(Op::LoadBuiltin);
self.emit_u8(id as u8);
}
&Ir::BuiltinConst(id) => {
self.emit_select(
&Ir::Builtins,
&[Attr::Str(id, TextRange::default())],
None,
TextRange::default(),
);
}
&Ir::ConcatStrings {
ref parts,
force_string,
} => {
for &part in parts.iter() {
self.emit_expr(part);
self.emit_op(Op::CoerceToString);
}
self.emit_op(Op::ConcatStrings);
self.emit_u16(parts.len() as u16);
self.emit_bool(force_string);
}
&Ir::HasAttr { lhs, ref rhs } => {
self.emit_has_attr(lhs, rhs);
}
Ir::Assert {
assertion,
expr,
assertion_raw,
span,
} => {
let raw_idx = self.ctx.intern_string(assertion_raw);
let span_id = self.ctx.register_span(*span);
self.emit_expr(*assertion);
self.emit_op(Op::Assert);
self.emit_str_id(raw_idx);
self.emit_u32(span_id);
self.emit_expr(*expr);
}
&Ir::ReplBinding(name) => {
self.emit_op(Op::LoadReplBinding);
self.emit_str_id(name);
}
&Ir::ScopedImportBinding(name) => {
self.emit_op(Op::LoadScopedBinding);
let slot = self
.ctx
.current_scope_slot()
.expect("ScopedImportBinding outside scoped compilation");
self.emit_u32(slot);
self.emit_str_id(name);
}
Ir::WithLookup { sym, namespaces } => {
// counter
self.emit_expr(&Ir::Int(0));
self.emit_op(Op::LookupWith);
self.emit_str_id(*sym);
self.emit_u8(
namespaces
.len()
.try_into()
.expect("too many `with` namespaces"),
);
for namespace in namespaces {
self.emit_maybe_thunk(namespace);
}
}
&Ir::MaybeThunk(thunk) => {
use MaybeThunk::*;
match *thunk {
Int(x) => {
if let Ok(x) = x.try_into() {
self.emit_op(Op::PushSmi);
self.emit_i32(x);
} else {
self.emit_op(Op::PushBigInt);
self.emit_i64(x);
}
}
Float(x) => {
self.emit_op(Op::PushFloat);
self.emit_f64(x);
}
Bool(true) => self.emit_op(Op::PushTrue),
Bool(false) => self.emit_op(Op::PushFalse),
Null => self.emit_op(Op::PushNull),
Str(id) => {
self.emit_op(Op::PushString);
self.emit_str_id(id);
}
Path(id) => {
self.emit_op(Op::PushString);
self.emit_str_id(id);
self.emit_op(Op::ResolvePath);
let dir_id = self.ctx.current_source_dir();
self.emit_str_id(dir_id);
}
Thunk(id) => {
let (layer, local) = self.resolve_thunk(id);
self.emit_load(layer, local);
}
Arg { layer } => self.emit_load(layer, 0),
Builtin(id) => {
self.emit_op(Op::LoadBuiltin);
self.emit_u8(id as u8);
}
BuiltinConst(id) => self.emit_select(
&Ir::Builtins,
&[Attr::Str(id, TextRange::default())],
None,
TextRange::default(),
),
Builtins => self.emit_op(Op::LoadBuiltins),
ReplBinding(name) => {
self.emit_op(Op::LoadReplBinding);
self.emit_str_id(name);
}
ScopedImportBinding(name) => {
self.emit_op(Op::LoadScopedBinding);
let slot = self
.ctx
.current_scope_slot()
.expect("ScopedImportBinding outside scoped compilation");
self.emit_u32(slot);
self.emit_str_id(name);
}
}
}
}
}
fn emit_binop(&mut self, lhs: RawIrRef<'_>, rhs: RawIrRef<'_>, kind: BinOpKind) {
use BinOpKind::*;
match kind {
And => {
self.emit_expr(lhs);
self.emit_op(Op::JumpIfFalse);
let skip_placeholder = self.emit_i32_placeholder();
let after_jif = self.ctx.get_code_mut().len();
self.emit_expr(rhs);
self.emit_op(Op::Jump);
let end_placeholder = self.emit_i32_placeholder();
let after_jump = self.ctx.get_code_mut().len();
let false_offset = (after_jump as i32) - (after_jif as i32);
self.patch_i32(skip_placeholder, false_offset);
self.emit_op(Op::PushFalse);
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset);
}
Or => {
self.emit_expr(lhs);
self.emit_op(Op::JumpIfTrue);
let skip_placeholder = self.emit_i32_placeholder();
let after_jit = self.ctx.get_code_mut().len();
self.emit_expr(rhs);
self.emit_op(Op::Jump);
let end_placeholder = self.emit_i32_placeholder();
let after_jump = self.ctx.get_code_mut().len();
let true_offset = (after_jump as i32) - (after_jit as i32);
self.patch_i32(skip_placeholder, true_offset);
self.emit_op(Op::PushTrue);
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset);
}
Impl => {
self.emit_expr(lhs);
self.emit_op(Op::JumpIfFalse);
let skip_placeholder = self.emit_i32_placeholder();
let after_jif = self.ctx.get_code_mut().len();
self.emit_expr(rhs);
self.emit_op(Op::Jump);
let end_placeholder = self.emit_i32_placeholder();
let after_jump = self.ctx.get_code_mut().len();
let true_offset = (after_jump as i32) - (after_jif as i32);
self.patch_i32(skip_placeholder, true_offset);
self.emit_op(Op::PushTrue);
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset);
}
_ => {
self.emit_expr(lhs);
self.emit_expr(rhs);
self.emit_op(match kind {
Add => Op::OpAdd,
Sub => Op::OpSub,
Mul => Op::OpMul,
Div => Op::OpDiv,
Eq => Op::OpEq,
Neq => Op::OpNeq,
Lt => Op::OpLt,
Gt => Op::OpGt,
Leq => Op::OpLeq,
Geq => Op::OpGeq,
Con => Op::OpConcat,
Upd => Op::OpUpdate,
_ => unreachable!(),
});
}
}
}
fn emit_func<'ir>(
&mut self,
thunks: &[(ThunkId, RawIrRef<'ir>)],
param: &Option<Param<'ir>>,
body: RawIrRef<'ir>,
) {
let thunk_ids: Vec<ThunkId> = thunks.iter().map(|&(id, _)| id).collect();
let skip_patch = self.emit_jump_placeholder();
let entry_point = self.ctx.get_code().len() as u32;
self.push_scope(true, &thunk_ids);
self.emit_scope_thunks(thunks);
self.emit_expr(body);
self.emit_op(Op::Return);
self.pop_scope();
self.patch_jump_target(skip_patch);
if let Some(Param {
required,
optional,
ellipsis,
}) = param
{
self.emit_op(Op::MakePatternClosure);
self.emit_u32(entry_point);
self.emit_u32(thunks.len().try_into().expect("too many thunks"));
self.emit_u16(required.len() as u16);
self.emit_u16(optional.len() as u16);
self.emit_bool(*ellipsis);
for &(sym, _) in required.iter() {
self.emit_str_id(sym);
}
for &(sym, _) in optional.iter() {
self.emit_str_id(sym);
}
for &(sym, span) in required.iter().chain(optional.iter()) {
let span_id = self.ctx.register_span(span);
self.emit_str_id(sym);
self.emit_u32(span_id);
}
} else {
self.emit_op(Op::MakeClosure);
self.emit_u32(entry_point);
self.emit_u32(thunks.len().try_into().expect("too many thunks"));
}
}
fn emit_attrset(
&mut self,
stcs: &fix_ir::HashMap<'_, StringId, (&MaybeThunk, TextRange)>,
dyns: &[(RawIrRef<'_>, &MaybeThunk, TextRange)],
) {
if stcs.is_empty() && dyns.is_empty() {
self.emit_op(Op::MakeEmptyAttrs);
return;
}
for &(key_expr, _val, _span) in dyns.iter() {
self.emit_expr(key_expr);
}
self.emit_op(Op::MakeAttrs);
self.emit_u32(stcs.len() as u32);
self.emit_u32(dyns.len() as u32);
for (&sym, &(val, span)) in stcs.iter() {
self.emit_str_id(sym);
self.emit_maybe_thunk(val);
let span_id = self.ctx.register_span(span);
self.emit_u32(span_id);
}
for &(_key, val, span) in dyns.iter() {
self.emit_maybe_thunk(val);
let span_id = self.ctx.register_span(span);
self.emit_u32(span_id);
}
}
fn emit_select(
&mut self,
expr: RawIrRef<'_>,
attrpath: &[Attr<RawIrRef<'_>>],
default: Option<RawIrRef<'_>>,
span: TextRange,
) {
self.emit_expr(expr);
let mut dynamic_patches = Vec::new();
for attr in attrpath.iter() {
match *attr {
Attr::Str(sym, _) => {
let span_id = self.ctx.register_span(span);
self.emit_op(Op::SelectStatic);
self.emit_u32(span_id);
self.emit_str_id(sym);
}
Attr::Dynamic(key_expr, _) => {
self.emit_op(Op::JumpIfSelectFailed);
dynamic_patches.push(self.emit_i32_placeholder());
self.emit_expr(key_expr);
let span_id = self.ctx.register_span(span);
self.emit_op(Op::SelectDynamic);
self.emit_u32(span_id);
}
}
}
if let Some(default) = default {
let before: i32 = self.ctx.get_code().len().try_into().unwrap();
for patch in dynamic_patches {
self.patch_jump_target(patch);
}
self.emit_op(Op::JumpIfSelectSucceeded);
let placeholder = self.emit_i32_placeholder();
self.emit_expr(default);
let after: i32 = self.ctx.get_code().len().try_into().unwrap();
// Offset is relative to after the placeholder, so subtract the
// size of JumpIfSelectSucceeded (1) + placeholder (4).
self.patch_i32(placeholder, after - before - 5);
} else {
for patch in dynamic_patches {
self.patch_jump_target(patch);
}
}
}
fn emit_has_attr(&mut self, lhs: RawIrRef<'_>, rhs: &[Attr<RawIrRef<'_>>]) {
self.emit_expr(lhs);
let mut dynamic_patches = Vec::new();
let [attrs @ .., last] = rhs else {
panic!("attrpath is empty");
};
for attr in attrs {
match *attr {
Attr::Str(sym, span) => {
let span_id = self.ctx.register_span(span);
self.emit_op(Op::HasAttrPathStatic);
self.emit_u32(span_id);
self.emit_str_id(sym);
}
Attr::Dynamic(key_expr, span) => {
self.emit_op(Op::JumpIfSelectFailed);
dynamic_patches.push(self.emit_i32_placeholder());
self.emit_expr(key_expr);
let span_id = self.ctx.register_span(span);
self.emit_op(Op::HasAttrPathDynamic);
self.emit_u32(span_id);
}
}
}
match *last {
Attr::Str(sym, _) => {
self.emit_op(Op::HasAttrStatic);
self.emit_str_id(sym);
}
Attr::Dynamic(key_expr, _) => {
self.emit_op(Op::JumpIfSelectFailed);
dynamic_patches.push(self.emit_i32_placeholder());
self.emit_expr(key_expr);
self.emit_op(Op::HasAttrDynamic);
}
}
for patch in dynamic_patches {
self.patch_jump_target(patch);
}
self.emit_op(Op::HasAttrResolve);
}
fn emit_toplevel_inner(&mut self, body: RawIrRef<'_>, thunks: &[(ThunkId, RawIrRef<'_>)]) {
self.emit_scope_thunks(thunks);
self.emit_expr(body);
}
}
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "fix-common"
version = "0.1.0"
edition = "2024"
[dependencies]
gc-arena = { workspace = true }
string-interner = { workspace = true }
ere = { workspace = true }
+49 -27
View File
@@ -1,14 +1,17 @@
use core::fmt::{Debug, Display, Formatter, Result as FmtResult};
use core::hash::Hash;
use core::ops::Deref;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::DerefMut; use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use std::ops::{Deref, DerefMut};
use derive_more::{Constructor, IsVariant, Unwrap}; use gc_arena::Collect;
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)]
#[collect(require_static)]
pub struct StringId(pub string_interner::symbol::SymbolU32);
/// Represents a Nix symbol, which is used as a key in attribute sets. /// Represents a Nix symbol, which is used as a key in attribute sets.
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Constructor)] #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Symbol<'a>(Cow<'a, str>); pub struct Symbol<'a>(Cow<'a, str>);
pub type StaticSymbol = Symbol<'static>; pub type StaticSymbol = Symbol<'static>;
@@ -60,13 +63,36 @@ impl Deref for Symbol<'_> {
} }
} }
fn escape_quote_string(s: &str) -> String {
let mut ret = String::with_capacity(s.len() + 2);
ret.push('"');
let mut iter = s.chars().peekable();
while let Some(c) = iter.next() {
match c {
'\\' => ret.push_str("\\\\"),
'"' => ret.push_str("\\\""),
'\n' => ret.push_str("\\n"),
'\r' => ret.push_str("\\r"),
'\t' => ret.push_str("\\t"),
'$' if iter.peek() == Some(&'{') => ret.push_str("\\$"),
c => ret.push(c),
}
}
ret.push('"');
ret
}
/// Represents a Nix attribute set, which is a map from symbols to values. /// Represents a Nix attribute set, which is a map from symbols to values.
#[derive(Constructor, Default, Clone, PartialEq)] #[derive(Default, Clone, PartialEq)]
pub struct AttrSet { pub struct AttrSet {
data: BTreeMap<StaticSymbol, Value>, data: BTreeMap<StaticSymbol, Value>,
} }
impl AttrSet { impl AttrSet {
pub fn new(data: BTreeMap<StaticSymbol, Value>) -> Self {
Self { data }
}
/// Gets a value by key (string or Symbol). /// Gets a value by key (string or Symbol).
pub fn get<'a, 'sym: 'a>(&'a self, key: impl Into<Symbol<'sym>>) -> Option<&'a Value> { pub fn get<'a, 'sym: 'a>(&'a self, key: impl Into<Symbol<'sym>>) -> Option<&'a Value> {
self.data.get(&key.into()) self.data.get(&key.into())
@@ -154,11 +180,17 @@ impl Display for AttrSetCompatDisplay<'_> {
} }
/// Represents a Nix list, which is a vector of values. /// Represents a Nix list, which is a vector of values.
#[derive(Constructor, Default, Clone, Debug, PartialEq)] #[derive(Default, Clone, Debug, PartialEq)]
pub struct List { pub struct List {
data: Vec<Value>, data: Vec<Value>,
} }
impl List {
pub fn new(data: Vec<Value>) -> Self {
Self { data }
}
}
impl Deref for List { impl Deref for List {
type Target = Vec<Value>; type Target = Vec<Value>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
@@ -217,7 +249,7 @@ impl Display for ListCompatDisplay<'_> {
} }
/// Represents any possible Nix value that can be returned from an evaluation. /// Represents any possible Nix value that can be returned from an evaluation.
#[derive(IsVariant, Unwrap, Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Value { pub enum Value {
/// An integer value. /// An integer value.
Int(i64), Int(i64),
@@ -240,32 +272,22 @@ pub enum Value {
/// A function (lambda). /// A function (lambda).
Func, Func,
/// A primitive (built-in) operation. /// A primitive (built-in) operation.
PrimOp(String), PrimOp(&'static str),
/// A partially applied primitive operation. /// A partially applied primitive operation.
PrimOpApp(String), PrimOpApp(&'static str),
/// A marker for a value that has been seen before during serialization, to break cycles. /// A marker for a value that has been seen before during serialization, to break cycles.
/// This is used to prevent infinite recursion when printing or serializing cyclic data structures. /// This is used to prevent infinite recursion when printing or serializing cyclic data structures.
Repeated, Repeated,
} }
fn escape_quote_string(s: &str) -> String { /// Wrapper to format a float in Nix style (C printf `%g` with precision 6).
let mut ret = String::with_capacity(s.len() + 2); pub struct NixFloat(pub f64);
ret.push('"');
let mut iter = s.chars().peekable(); impl Display for NixFloat {
while let Some(c) = iter.next() { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match c { fmt_nix_float(f, self.0)
'\\' => ret.push_str("\\\\"),
'"' => ret.push_str("\\\""),
'\n' => ret.push_str("\\n"),
'\r' => ret.push_str("\\r"),
'\t' => ret.push_str("\\t"),
'$' if iter.peek() == Some(&'{') => ret.push_str("\\$"),
c => ret.push(c),
} }
} }
ret.push('"');
ret
}
/// Format a float matching C's `printf("%g", x)` with default precision 6. /// Format a float matching C's `printf("%g", x)` with default precision 6.
fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult { fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult {
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "fix-error"
version = "0.1.0"
edition = "2024"
[dependencies]
miette = { version = "7.6", features = ["fancy"] }
thiserror = "2.0"
rnix = { workspace = true }
+223
View File
@@ -0,0 +1,223 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error;
pub type Result<T> = core::result::Result<T, Box<Error>>;
#[derive(Clone, Debug)]
pub enum SourceType {
/// dir
Eval(Arc<PathBuf>),
/// dir
Repl(Arc<PathBuf>),
/// file
File(Arc<PathBuf>),
/// virtual (name, no path)
Virtual(Arc<str>),
}
#[derive(Clone, Debug)]
pub struct Source {
pub ty: SourceType,
pub src: Arc<str>,
}
impl TryFrom<&str> for Source {
type Error = Box<Error>;
fn try_from(value: &str) -> Result<Self> {
Source::new_eval(value.into())
}
}
impl From<Source> for NamedSource<Arc<str>> {
fn from(value: Source) -> Self {
let name = value.get_name();
NamedSource::new(name, value.src.clone())
}
}
impl Source {
pub fn new_file(path: PathBuf) -> std::io::Result<Self> {
Ok(Source {
src: std::fs::read_to_string(&path)?.into(),
ty: SourceType::File(Arc::new(path)),
})
}
pub fn new_eval(src: String) -> Result<Self> {
Ok(Self {
ty: std::env::current_dir()
.map_err(|err| Error::internal(format!("Failed to get current working dir: {err}")))
.map(Arc::new)
.map(SourceType::Eval)?,
src: src.into(),
})
}
pub fn new_repl(src: String) -> Result<Self> {
Ok(Self {
ty: std::env::current_dir()
.map_err(|err| Error::internal(format!("Failed to get current working dir: {err}")))
.map(Arc::new)
.map(SourceType::Repl)?,
src: src.into(),
})
}
pub fn new_virtual(name: Arc<str>, src: String) -> Self {
Self {
ty: SourceType::Virtual(name),
src: src.into(),
}
}
pub fn get_dir(&self) -> &Path {
use SourceType::*;
match &self.ty {
Eval(dir) | Repl(dir) => dir.as_ref(),
File(file) => file
.as_path()
.parent()
.expect("source file must have a parent dir"),
Virtual(_) => Path::new("/"),
}
}
pub fn get_name(&self) -> String {
match &self.ty {
SourceType::Eval(_) => "«eval»".into(),
SourceType::Repl(_) => "«repl»".into(),
SourceType::File(path) => path.as_os_str().to_string_lossy().to_string(),
SourceType::Virtual(name) => name.to_string(),
}
}
}
#[derive(Error, Debug, Diagnostic)]
pub enum Error {
#[error("Parse error: {message}")]
#[diagnostic(code(nix::parse))]
ParseError {
#[source_code]
src: Option<NamedSource<Arc<str>>>,
#[label("error occurred here")]
span: Option<SourceSpan>,
message: String,
},
#[error("Downgrade error: {message}")]
#[diagnostic(code(nix::downgrade))]
DowngradeError {
#[source_code]
src: Option<NamedSource<Arc<str>>>,
#[label("{message}")]
span: Option<SourceSpan>,
message: String,
},
#[error("Evaluation error: {message}")]
#[diagnostic(code(nix::eval))]
EvalError {
#[source_code]
src: Option<NamedSource<Arc<str>>>,
#[label("error occurred here")]
span: Option<SourceSpan>,
message: String,
#[related]
stack_trace: Vec<StackFrame>,
},
#[error("Internal error: {message}")]
#[diagnostic(code(nix::internal))]
InternalError { message: String },
#[error("{message}")]
#[diagnostic(code(nix::catchable))]
Catchable { message: String },
#[error("Unknown error")]
#[diagnostic(code(nix::unknown))]
Unknown,
}
impl Error {
pub fn parse_error(msg: String) -> Box<Self> {
Error::ParseError {
src: None,
span: None,
message: msg,
}
.into()
}
pub fn downgrade_error(msg: String, src: Source, span: rnix::TextRange) -> Box<Self> {
Error::DowngradeError {
src: Some(src.into()),
span: Some(text_range_to_source_span(span)),
message: msg,
}
.into()
}
pub fn eval_error(msg: impl Into<String>) -> Box<Self> {
Error::EvalError {
src: None,
span: None,
message: msg.into(),
stack_trace: Vec::new(),
}
.into()
}
pub fn internal(msg: String) -> Box<Self> {
Error::InternalError { message: msg }.into()
}
pub fn catchable(msg: String) -> Box<Self> {
Error::Catchable { message: msg }.into()
}
pub fn with_span(mut self: Box<Self>, span: rnix::TextRange) -> Box<Self> {
use Error::*;
let source_span = Some(text_range_to_source_span(span));
let (ParseError { span, .. } | DowngradeError { span, .. } | EvalError { span, .. }) =
self.as_mut()
else {
return self;
};
*span = source_span;
self
}
pub fn with_source(mut self: Box<Self>, source: Source) -> Box<Self> {
use Error::*;
let new_src = Some(source.into());
let (ParseError { src, .. } | DowngradeError { src, .. } | EvalError { src, .. }) =
self.as_mut()
else {
return self;
};
*src = new_src;
self
}
}
pub fn text_range_to_source_span(range: rnix::TextRange) -> SourceSpan {
let start = usize::from(range.start());
let len = usize::from(range.end()) - start;
SourceSpan::new(start.into(), len)
}
/// Stack frame types from Nix evaluation
#[derive(Debug, Clone, Error, Diagnostic)]
#[error("{message}")]
pub struct StackFrame {
#[label]
pub span: SourceSpan,
#[help]
pub message: String,
#[source_code]
pub src: NamedSource<Arc<str>>,
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "fix-ir"
version = "0.1.0"
edition = "2024"
[dependencies]
bumpalo = { workspace = true }
ghost-cell = { workspace = true }
rnix = { workspace = true }
rowan = { workspace = true }
string-interner = { workspace = true }
hashbrown = { workspace = true }
num_enum = { workspace = true }
fix-builtins = { path = "../fix-builtins" }
fix-common = { path = "../fix-common" }
fix-error = { path = "../fix-error" }
@@ -1,14 +1,14 @@
use bumpalo::boxed::Box;
use bumpalo::collections::{CollectIn, Vec}; use bumpalo::collections::{CollectIn, Vec};
use fix_builtins::BuiltinId;
use fix_common::Symbol;
use fix_error::{Error, Result, Source};
use hashbrown::HashSet; use hashbrown::HashSet;
use hashbrown::hash_map::Entry; use hashbrown::hash_map::Entry;
use rnix::TextRange; use rnix::TextRange;
use rnix::ast::{self, Expr, HasEntry}; use rnix::ast::{self, AstToken, Expr, HasEntry};
use rowan::ast::AstNode; use rowan::ast::AstNode;
use crate::error::{Error, Result, Source}; use super::*;
use crate::ir::*;
use crate::value::Symbol;
trait Require<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, T> { trait Require<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, T> {
fn require(self, ctx: &Ctx, span: TextRange) -> Result<T>; fn require(self, ctx: &Ctx, span: TextRange) -> Result<T>;
@@ -38,34 +38,26 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, T, E: std::fmt::Display>
} }
} }
trait BoxIn: Sized {
fn box_in(self, bump: &bumpalo::Bump) -> Box<'_, Self> {
Box::new_in(self, bump)
}
}
impl<T: Sized> BoxIn for T {}
pub trait DowngradeContext<'id: 'ir, 'ir> { pub trait DowngradeContext<'id: 'ir, 'ir> {
fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir>; fn new_expr(&self, expr: Ir<'ir, GhostRoRef<'id, 'ir>>) -> GhostRoIrRef<'id, 'ir>;
fn new_arg(&mut self) -> ArgId; fn maybe_thunk(&mut self, ir: GhostRoIrRef<'id, 'ir>) -> GhostRoMaybeThunkRef<'id, 'ir>;
fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir>;
fn new_sym(&mut self, sym: String) -> SymId; fn intern_string(&mut self, sym: impl AsRef<str>) -> StringId;
fn get_sym(&self, id: SymId) -> Symbol<'_>; fn resolve_sym(&self, id: StringId) -> Symbol<'_>;
fn lookup(&self, sym: SymId, span: TextRange) -> Result<IrRef<'id, 'ir>>; fn lookup(&mut self, sym: StringId, span: TextRange) -> Result<GhostRoMaybeThunkRef<'id, 'ir>>;
fn get_current_source(&self) -> Source; fn get_current_source(&self) -> Source;
fn with_param_scope<F, R>(&mut self, param: SymId, arg: ArgId, f: F) -> R fn with_param_scope<F, R>(&mut self, param: StringId, f: F) -> R
where where
F: FnOnce(&mut Self) -> R; F: FnOnce(&mut Self) -> R;
fn with_let_scope<F, R>(&mut self, bindings: &[SymId], f: F) -> Result<R> fn with_let_scope<F, R>(&mut self, bindings: &[StringId], f: F) -> Result<R>
where where
F: FnOnce(&mut Self) -> Result<(Vec<'ir, IrRef<'id, 'ir>>, R)>; F: FnOnce(&mut Self) -> Result<(Vec<'ir, GhostRoMaybeThunkRef<'id, 'ir>>, R)>;
fn with_with_scope<F, R>(&mut self, f: F) -> R fn with_with_scope<F, R>(&mut self, namespace: GhostRoMaybeThunkRef<'id, 'ir>, f: F) -> R
where where
F: FnOnce(&mut Self) -> R; F: FnOnce(&mut Self) -> R;
fn with_thunk_scope<F, R>(&mut self, f: F) -> (R, Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>) fn with_thunk_scope<F, R>(&mut self, f: F) -> (R, Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>)
where where
F: FnOnce(&mut Self) -> R; F: FnOnce(&mut Self) -> R;
@@ -73,11 +65,11 @@ pub trait DowngradeContext<'id: 'ir, 'ir> {
} }
pub trait Downgrade<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> { pub trait Downgrade<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>>; fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>>;
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for Expr { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for Expr {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
use Expr::*; use Expr::*;
match self { match self {
Apply(apply) => apply.downgrade(ctx), Apply(apply) => apply.downgrade(ctx),
@@ -106,7 +98,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
AttrSet(attrs) => attrs.downgrade(ctx), AttrSet(attrs) => attrs.downgrade(ctx),
UnaryOp(op) => op.downgrade(ctx), UnaryOp(op) => op.downgrade(ctx),
Ident(ident) => ident.downgrade(ctx), Ident(ident) => ident.downgrade(ctx),
CurPos(curpos) => Ok(ctx.new_expr(Ir::CurPos(curpos.syntax().text_range()))), CurPos(curpos) => curpos.downgrade(ctx),
With(with) => with.downgrade(ctx), With(with) => with.downgrade(ctx),
HasAttr(has) => has.downgrade(ctx), HasAttr(has) => has.downgrade(ctx),
Paren(paren) => paren Paren(paren) => paren
@@ -122,7 +114,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Assert { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Assert {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let assertion = self.condition().require(ctx, span)?; let assertion = self.condition().require(ctx, span)?;
let assertion_raw = assertion.to_string(); let assertion_raw = assertion.to_string();
@@ -138,7 +130,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::IfElse { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::IfElse {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let cond = self.condition().require(ctx, span)?.downgrade(ctx)?; let cond = self.condition().require(ctx, span)?.downgrade(ctx)?;
let consq = self.body().require(ctx, span)?.downgrade(ctx)?; let consq = self.body().require(ctx, span)?.downgrade(ctx)?;
@@ -150,9 +142,8 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
macro_rules! path { macro_rules! path {
($ty:ident) => { ($ty:ident) => {
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::$ty { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::$ty {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); downgrade_path(self.parts(), ctx)
downgrade_path(self.parts(), span, ctx)
} }
} }
}; };
@@ -161,19 +152,20 @@ path!(PathAbs);
path!(PathRel); path!(PathRel);
path!(PathHome); path!(PathHome);
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::PathSearch { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::PathSearch {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let path = { let path = {
let temp = self.content().require(ctx, span)?; let temp = self.content().require(ctx, span)?;
let text = temp.text(); let text = temp.text();
ctx.new_expr(Ir::Str( let id = ctx.intern_string(&text[1..text.len() - 1]);
text[1..text.len() - 1].to_string().box_in(ctx.bump()), let expr = ctx.new_expr(Ir::Str(id));
)) ctx.maybe_thunk(expr)
}; };
let sym = ctx.new_sym("findFile".into()); // HACK: disgusting eww
let find_file = ctx.new_expr(Ir::Builtin(sym)); let find_file = ctx.new_expr(Ir::Builtin(BuiltinId::FindFile));
let sym = ctx.new_sym("nixPath".into()); let sym = ctx.intern_string("nixPath");
let nix_path = ctx.new_expr(Ir::Builtin(sym)); let nix_path = ctx.new_expr(Ir::BuiltinConst(sym));
let nix_path = ctx.maybe_thunk(nix_path);
let call = ctx.new_expr(Ir::Call { let call = ctx.new_expr(Ir::Call {
func: find_file, func: find_file,
arg: nix_path, arg: nix_path,
@@ -188,29 +180,27 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Str { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Str {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let normalized = self.normalized_parts(); let normalized = self.normalized_parts();
let is_single_literal = normalized.len() == 1 let is_single_literal = normalized.len() == 1
&& matches!(normalized.first(), Some(ast::InterpolPart::Literal(_))); && matches!(normalized.first(), Some(ast::InterpolPart::Literal(_)));
let bump = ctx.bump(); let bump = ctx.bump();
let parts = normalized let mut parts = normalized.into_iter().map(|part| match part {
.into_iter() ast::InterpolPart::Literal(lit) => {
.map(|part| match part { let id = ctx.intern_string(lit);
ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr(Ir::Str(lit.box_in(bump)))), Ok(ctx.new_expr(Ir::Str(id)))
ast::InterpolPart::Interpolation(interpol) => { }
let inner = interpol ast::InterpolPart::Interpolation(interpol) => interpol
.expr() .expr()
.require(ctx, interpol.syntax().text_range())? .require(ctx, interpol.syntax().text_range())?
.downgrade(ctx)?; .downgrade(ctx),
Ok(ctx.maybe_thunk(inner)) });
}
})
.collect_in::<Result<Vec<'ir, _>>>(bump)?;
Ok(if is_single_literal { Ok(if is_single_literal {
parts.into_iter().next().expect("is_single_literal checked") parts.next().expect("is_single_literal checked")?
} else { } else {
let parts = parts.collect_in::<Result<_>>(bump)?;
ctx.new_expr(Ir::ConcatStrings { ctx.new_expr(Ir::ConcatStrings {
parts, parts,
force_string: true, force_string: true,
@@ -220,27 +210,88 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Literal { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Literal {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
Ok(ctx.new_expr(match self.kind() { let expr = match self.kind() {
ast::LiteralKind::Integer(int) => Ir::Int(int.value().require(ctx, span)?), ast::LiteralKind::Integer(int) => Ir::Int(int.value().require(ctx, span)?),
ast::LiteralKind::Float(float) => Ir::Float(float.value().require(ctx, span)?), ast::LiteralKind::Float(float) => Ir::Float(float.value().require(ctx, span)?),
ast::LiteralKind::Uri(uri) => Ir::Str(uri.to_string().box_in(ctx.bump())), ast::LiteralKind::Uri(uri) => {
})) let id = ctx.intern_string(uri.syntax().text());
Ir::Str(id)
}
};
Ok(ctx.new_expr(expr))
} }
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Ident { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Ident {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let text = self.ident_token().require(ctx, span)?.to_string(); let text = self.ident_token().require(ctx, span)?.to_string();
let sym = ctx.new_sym(text); let sym = ctx.intern_string(text);
ctx.lookup(sym, span) ctx.lookup(sym, span)
.map(|thunk| ctx.new_expr(Ir::MaybeThunk(thunk)))
}
}
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::CurPos {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
fn byte_offset_to_line_col(content: &str, offset: usize) -> (u32, u32) {
let mut line = 1u32;
let mut col = 1u32;
for (idx, ch) in content.char_indices() {
if idx >= offset {
break;
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
let span = self.syntax().text_range();
let source = ctx.get_current_source();
let (line, column) = byte_offset_to_line_col(&source.src, span.start().into());
let file_sym = ctx.intern_string("file");
let line_sym = ctx.intern_string("line");
let column_sym = ctx.intern_string("column");
let file: GhostRoMaybeThunkRef = ctx
.bump()
.alloc(GhostCell::new(MaybeThunk::Str(ctx.intern_string(source.get_name()))).into());
let line = ctx
.bump()
.alloc(GhostCell::new(MaybeThunk::Int(i64::from(line))).into());
let column = ctx
.bump()
.alloc(GhostCell::new(MaybeThunk::Int(i64::from(column))).into());
let map = {
let mut map = HashMap::new_in(ctx.bump());
map.insert(file_sym, (file, TextRange::default()));
map.insert(line_sym, (line, TextRange::default()));
map.insert(column_sym, (column, TextRange::default()));
map
};
Ok(ctx.new_expr(Ir::AttrSet {
stcs: map,
dyns: Vec::new_in(ctx.bump()),
}))
} }
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::AttrSet { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::AttrSet {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let rec = self.rec_token().is_some(); let rec = self.rec_token().is_some();
if !rec { if !rec {
let attrs = downgrade_attrs(self, ctx)?; let attrs = downgrade_attrs(self, ctx)?;
@@ -256,7 +307,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::List { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::List {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let bump = ctx.bump(); let bump = ctx.bump();
let items = self let items = self
.items() .items()
@@ -270,17 +321,52 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::BinOp { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::BinOp {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
use BinOpKind::*;
use ast::BinOpKind as Kind;
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let lhs = self.lhs().require(ctx, span)?.downgrade(ctx)?; let lhs = self.lhs().require(ctx, span)?.downgrade(ctx)?;
let rhs = self.rhs().require(ctx, span)?.downgrade(ctx)?; let rhs = self.rhs().require(ctx, span)?.downgrade(ctx)?;
let kind = self.operator().require(ctx, span)?.into(); let kind = match self.operator().require(ctx, span)? {
Kind::Concat => Con,
Kind::Update => Upd,
Kind::Add => Add,
Kind::Sub => Sub,
Kind::Mul => Mul,
Kind::Div => Div,
Kind::And => And,
Kind::Equal => Eq,
Kind::Implication => Impl,
Kind::Less => Lt,
Kind::LessOrEq => Leq,
Kind::More => Gt,
Kind::MoreOrEq => Geq,
Kind::NotEqual => Neq,
Kind::Or => Or,
Kind::PipeLeft => {
let arg = ctx.maybe_thunk(rhs);
return Ok(ctx.new_expr(Ir::Call {
func: lhs,
arg,
span,
}));
}
Kind::PipeRight => {
let arg = ctx.maybe_thunk(lhs);
return Ok(ctx.new_expr(Ir::Call {
func: rhs,
arg,
span,
}));
}
};
Ok(ctx.new_expr(Ir::BinOp { lhs, rhs, kind })) Ok(ctx.new_expr(Ir::BinOp { lhs, rhs, kind }))
} }
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::HasAttr { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::HasAttr {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let lhs = self.expr().require(ctx, span)?.downgrade(ctx)?; let lhs = self.expr().require(ctx, span)?.downgrade(ctx)?;
let rhs = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?; let rhs = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?;
@@ -289,7 +375,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::UnaryOp { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::UnaryOp {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let rhs = self.expr().require(ctx, span)?.downgrade(ctx)?; let rhs = self.expr().require(ctx, span)?.downgrade(ctx)?;
let kind = self.operator().require(ctx, span)?.into(); let kind = self.operator().require(ctx, span)?.into();
@@ -298,16 +384,14 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Select { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Select {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let expr = self.expr().require(ctx, span)?.downgrade(ctx)?; let expr = self.expr().require(ctx, span)?.downgrade(ctx)?;
let attrpath = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?; let attrpath = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?;
let default = if let Some(default) = self.default_expr() { let default = self
let default_expr = default.downgrade(ctx)?; .default_expr()
Some(ctx.maybe_thunk(default_expr)) .map(|expr| expr.downgrade(ctx))
} else { .transpose()?;
None
};
let span = self.syntax().text_range(); let span = self.syntax().text_range();
Ok(ctx.new_expr(Ir::Select { Ok(ctx.new_expr(Ir::Select {
expr, expr,
@@ -319,7 +403,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::LegacyLet { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::LegacyLet {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump()); let entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump());
let attrset_expr = downgrade_let_bindings(entries, ctx, |ctx, binding_keys| { let attrset_expr = downgrade_let_bindings(entries, ctx, |ctx, binding_keys| {
@@ -334,7 +418,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
Ok(ctx.new_expr(Ir::AttrSet { stcs, dyns })) Ok(ctx.new_expr(Ir::AttrSet { stcs, dyns }))
})?; })?;
let body_sym = ctx.new_sym("body".to_string()); let body_sym = ctx.intern_string("body");
Ok(ctx.new_expr(Ir::Select { Ok(ctx.new_expr(Ir::Select {
expr: attrset_expr, expr: attrset_expr,
attrpath: Vec::from_iter_in( attrpath: Vec::from_iter_in(
@@ -348,7 +432,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::LetIn { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::LetIn {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump()); let entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump());
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let body_expr = self.body().require(ctx, span)?; let body_expr = self.body().require(ctx, span)?;
@@ -358,34 +442,25 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::With { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::With {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let namespace = self.namespace().require(ctx, span)?.downgrade(ctx)?; let namespace = self.namespace().require(ctx, span)?.downgrade(ctx)?;
let namespace = ctx.maybe_thunk(namespace); let namespace = ctx.maybe_thunk(namespace);
let body_expr = self.body().require(ctx, span)?; let body_expr = self.body().require(ctx, span)?;
let (body, thunks) = ctx.with_with_scope(namespace, |ctx| body_expr.downgrade(ctx))
ctx.with_thunk_scope(|ctx| ctx.with_with_scope(|ctx| body_expr.downgrade(ctx)));
let body = body?;
Ok(ctx.new_expr(Ir::With {
namespace,
body,
thunks,
}))
} }
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Lambda { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Lambda {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let raw_param = self.param().require(ctx, span)?; let raw_param = self.param().require(ctx, span)?;
let body_ast = self.body().require(ctx, span)?; let body_ast = self.body().require(ctx, span)?;
let arg = ctx.new_arg();
struct Ret<'id, 'ir> { struct Ret<'id, 'ir> {
param: Option<Param<'ir>>, param: Option<Param<'ir>>,
body: IrRef<'id, 'ir>, body: GhostRoIrRef<'id, 'ir>,
} }
let (ret, thunks) = ctx.with_thunk_scope(|ctx| { let (ret, thunks) = ctx.with_thunk_scope(|ctx| {
@@ -394,18 +469,17 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
match raw_param { match raw_param {
ast::Param::IdentParam(id) => { ast::Param::IdentParam(id) => {
let param_sym = ctx.new_sym(id.to_string()); let param_sym = ctx.intern_string(id.to_string());
param = None; param = None;
body = ctx body = ctx.with_param_scope(param_sym, |ctx| body_ast.downgrade(ctx))?;
.with_param_scope(param_sym, arg, |ctx| body_ast.clone().downgrade(ctx))?;
} }
ast::Param::Pattern(pattern) => { ast::Param::Pattern(pattern) => {
let alias = pattern let alias = pattern
.pat_bind() .pat_bind()
.map(|alias| { .map(|alias| {
let ident = alias.ident().require(ctx, alias.syntax().text_range())?; let ident = alias.ident().require(ctx, alias.syntax().text_range())?;
Ok::<_, std::boxed::Box<Error>>(ctx.new_sym(ident.to_string())) Ok::<_, std::boxed::Box<Error>>(ctx.intern_string(ident.to_string()))
}) })
.transpose()?; .transpose()?;
@@ -416,7 +490,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
body: inner_body, body: inner_body,
required, required,
optional, optional,
} = downgrade_pattern_bindings(pat_entries, alias, arg, ctx, |ctx, _| { } = downgrade_pattern_bindings(pat_entries, alias, ctx, |ctx, _| {
body_ast.clone().downgrade(ctx) body_ast.clone().downgrade(ctx)
})?; })?;
@@ -437,14 +511,13 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
Ok(ctx.new_expr(Ir::Func { Ok(ctx.new_expr(Ir::Func {
body, body,
param, param,
arg,
thunks, thunks,
})) }))
} }
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Apply { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Apply {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let func = self.lambda().require(ctx, span)?.downgrade(ctx)?; let func = self.lambda().require(ctx, span)?.downgrade(ctx)?;
let arg = self.argument().require(ctx, span)?.downgrade(ctx)?; let arg = self.argument().require(ctx, span)?.downgrade(ctx)?;
@@ -456,8 +529,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, SymId, TextRange), InheritFrom(ast::Expr, StringId, TextRange),
InheritScope(SymId, TextRange), InheritScope(StringId, TextRange),
Set(PendingAttrSet<'ir>), Set(PendingAttrSet<'ir>),
ExtendedRecAttrSet { ExtendedRecAttrSet {
base: ast::AttrSet, base: ast::AttrSet,
@@ -466,7 +539,7 @@ enum PendingValue<'ir> {
} }
struct PendingAttrSet<'ir> { struct PendingAttrSet<'ir> {
stcs: HashMap<'ir, SymId, (PendingValue<'ir>, TextRange)>, stcs: HashMap<'ir, StringId, (PendingValue<'ir>, TextRange)>,
dyns: Vec<'ir, (ast::Attr, PendingValue<'ir>, TextRange)>, dyns: Vec<'ir, (ast::Attr, PendingValue<'ir>, TextRange)>,
} }
@@ -490,7 +563,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
match first { match first {
ast::Attr::Ident(ident) => { ast::Attr::Ident(ident) => {
let sym = ctx.new_sym(ident.to_string()); let sym = ctx.intern_string(ident.to_string());
self.insert_static(sym, span, rest, value, ctx) self.insert_static(sym, span, rest, value, ctx)
} }
ast::Attr::Str(string) => { ast::Attr::Str(string) => {
@@ -499,7 +572,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
&& let ast::InterpolPart::Literal(lit) = && let ast::InterpolPart::Literal(lit) =
parts.into_iter().next().expect("len checked") parts.into_iter().next().expect("len checked")
{ {
let sym = ctx.new_sym(lit); let sym = ctx.intern_string(lit);
return self.insert_static(sym, span, rest, value, ctx); return self.insert_static(sym, span, rest, value, ctx);
} }
self.insert_dynamic(first.clone(), span, rest, value, ctx) self.insert_dynamic(first.clone(), span, rest, value, ctx)
@@ -510,7 +583,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
fn insert_static( fn insert_static(
&mut self, &mut self,
sym: SymId, sym: StringId,
span: TextRange, span: TextRange,
path: &[ast::Attr], path: &[ast::Attr],
value: ast::Expr, value: ast::Expr,
@@ -742,14 +815,14 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
for attr in inherit.attrs() { for attr in inherit.attrs() {
let span = attr.syntax().text_range(); let span = attr.syntax().text_range();
let sym = match &attr { let sym = match &attr {
ast::Attr::Ident(ident) => ctx.new_sym(ident.to_string()), ast::Attr::Ident(ident) => ctx.intern_string(ident.to_string()),
ast::Attr::Str(s) => { ast::Attr::Str(s) => {
let parts = s.normalized_parts(); let parts = s.normalized_parts();
if parts.len() == 1 if parts.len() == 1
&& let ast::InterpolPart::Literal(lit) = && let ast::InterpolPart::Literal(lit) =
parts.into_iter().next().expect("len checked") parts.into_iter().next().expect("len checked")
{ {
ctx.new_sym(lit) ctx.intern_string(lit)
} else { } else {
return Err(Error::downgrade_error( return Err(Error::downgrade_error(
"dynamic attributes not allowed in inherit".to_string(), "dynamic attributes not allowed in inherit".to_string(),
@@ -769,7 +842,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
if self.stcs.contains_key(&sym) { if self.stcs.contains_key(&sym) {
return Err(Error::downgrade_error( return Err(Error::downgrade_error(
format!("attribute '{}' already defined", ctx.get_sym(sym)), format!("attribute '{}' already defined", ctx.resolve_sym(sym)),
ctx.get_current_source(), ctx.get_current_source(),
span, span,
)); ));
@@ -846,8 +919,15 @@ 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, SymId, (IrRef<'id, 'ir>, TextRange)>, stcs: HashMap<'ir, StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>,
dyns: Vec<'ir, (IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)>, dyns: Vec<
'ir,
(
GhostRoIrRef<'id, 'ir>,
GhostRoMaybeThunkRef<'id, 'ir>,
TextRange,
),
>,
} }
fn downgrade_attrs<'id, 'ir>( fn downgrade_attrs<'id, 'ir>(
@@ -862,22 +942,22 @@ fn downgrade_attrs<'id, 'ir>(
fn downgrade_attr<'id, 'ir>( fn downgrade_attr<'id, 'ir>(
attr: ast::Attr, attr: ast::Attr,
ctx: &mut impl DowngradeContext<'id, 'ir>, ctx: &mut impl DowngradeContext<'id, 'ir>,
) -> Result<Attr<IrRef<'id, 'ir>>> { ) -> Result<Attr<GhostRoIrRef<'id, 'ir>>> {
use ast::Attr::*; use ast::Attr::*;
use ast::InterpolPart::*; use ast::InterpolPart::*;
match attr { match attr {
Ident(ident) => Ok(Attr::Str( Ident(ident) => Ok(Attr::Str(
ctx.new_sym(ident.to_string()), ctx.intern_string(ident.to_string()),
ident.syntax().text_range(), ident.syntax().text_range(),
)), )),
Str(string) => { Str(string) => {
let parts = string.normalized_parts(); let parts = string.normalized_parts();
let span = string.syntax().text_range(); let span = string.syntax().text_range();
if parts.is_empty() { if parts.is_empty() {
Ok(Attr::Str(ctx.new_sym("".to_string()), span)) Ok(Attr::Str(ctx.intern_string(""), span))
} else if parts.len() == 1 { } else if parts.len() == 1 {
match parts.into_iter().next().expect("len checked") { match parts.into_iter().next().expect("len checked") {
Literal(ident) => Ok(Attr::Str(ctx.new_sym(ident), span)), Literal(ident) => Ok(Attr::Str(ctx.intern_string(ident), span)),
Interpolation(interpol) => Ok(Attr::Dynamic( Interpolation(interpol) => Ok(Attr::Dynamic(
interpol interpol
.expr() .expr()
@@ -891,7 +971,10 @@ fn downgrade_attr<'id, 'ir>(
let parts = parts let parts = parts
.into_iter() .into_iter()
.map(|part| match part { .map(|part| match part {
Literal(lit) => Ok(ctx.new_expr(Ir::Str(lit.box_in(bump)))), Literal(lit) => {
let id = ctx.intern_string(lit);
Ok(ctx.new_expr(Ir::Str(id)))
}
Interpolation(interpol) => interpol Interpolation(interpol) => interpol
.expr() .expr()
.require(ctx, interpol.syntax().text_range())? .require(ctx, interpol.syntax().text_range())?
@@ -920,7 +1003,7 @@ fn downgrade_attr<'id, 'ir>(
fn downgrade_attrpath<'id, 'ir>( fn downgrade_attrpath<'id, 'ir>(
attrpath: ast::Attrpath, attrpath: ast::Attrpath,
ctx: &mut impl DowngradeContext<'id, 'ir>, ctx: &mut impl DowngradeContext<'id, 'ir>,
) -> Result<Vec<'ir, Attr<IrRef<'id, 'ir>>>> { ) -> Result<Vec<'ir, Attr<GhostRoIrRef<'id, 'ir>>>> {
let bump = ctx.bump(); let bump = ctx.bump();
attrpath attrpath
.attrs() .attrs()
@@ -929,24 +1012,22 @@ fn downgrade_attrpath<'id, 'ir>(
} }
struct PatternBindings<'id, 'ir> { struct PatternBindings<'id, 'ir> {
body: IrRef<'id, 'ir>, body: GhostRoIrRef<'id, 'ir>,
required: Vec<'ir, (SymId, TextRange)>, required: Vec<'ir, (StringId, TextRange)>,
optional: Vec<'ir, (SymId, TextRange)>, optional: Vec<'ir, (StringId, 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<SymId>, alias: Option<StringId>,
arg: ArgId,
ctx: &mut Ctx, ctx: &mut Ctx,
body_fn: impl FnOnce(&mut Ctx, &[SymId]) -> Result<IrRef<'id, 'ir>>, body_fn: impl FnOnce(&mut Ctx, &[StringId]) -> Result<GhostRoIrRef<'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));
struct Param { struct Param {
sym: SymId, sym: StringId,
sym_span: TextRange, sym_span: TextRange,
default: Option<ast::Expr>, default: Option<ast::Expr>,
span: TextRange, span: TextRange,
@@ -958,13 +1039,13 @@ where
for entry in pat_entries { for entry in pat_entries {
let ident = entry.ident().require(ctx, entry.syntax().text_range())?; let ident = entry.ident().require(ctx, entry.syntax().text_range())?;
let sym_span = ident.syntax().text_range(); let sym_span = ident.syntax().text_range();
let sym = ctx.new_sym(ident.syntax().text().to_string()); let sym = ctx.intern_string(ident.syntax().text().to_string());
let default = entry.default(); let default = entry.default();
let span = entry.syntax().text_range(); let span = entry.syntax().text_range();
if !seen_params.insert(sym) { if !seen_params.insert(sym) {
return Err(Error::downgrade_error( return Err(Error::downgrade_error(
format!("duplicate parameter '{}'", ctx.get_sym(sym)), format!("duplicate parameter '{}'", ctx.resolve_sym(sym)),
ctx.get_current_source(), ctx.get_current_source(),
span, span,
)); ));
@@ -998,6 +1079,8 @@ where
} }
} }
let arg = ctx.new_expr(Ir::Arg { layer: 0 });
let arg_thunk = ctx.maybe_thunk(arg);
ctx.with_let_scope(&keys, |ctx| { ctx.with_let_scope(&keys, |ctx| {
let vals = params let vals = params
.into_iter() .into_iter()
@@ -1008,21 +1091,17 @@ where
default, default,
span, span,
} = param; } = param;
let default = if let Some(default) = default { let default = default.map(|default| default.downgrade(ctx)).transpose()?;
let default = default.clone().downgrade(ctx)?;
Some(ctx.maybe_thunk(default))
} else {
None
};
Ok(ctx.new_expr(Ir::Select { let expr = ctx.new_expr(Ir::Select {
expr: arg, expr: arg,
attrpath: Vec::from_iter_in([Attr::Str(sym, sym_span)], bump), attrpath: Vec::from_iter_in([Attr::Str(sym, sym_span)], bump),
default, default,
span, span,
})) });
Ok(ctx.maybe_thunk(expr))
}) })
.chain(alias.into_iter().map(|_| Ok(arg))) .chain(alias.into_iter().map(|_| Ok(arg_thunk)))
.collect_in::<Result<_>>(bump)?; .collect_in::<Result<_>>(bump)?;
let body = body_fn(ctx, &keys)?; let body = body_fn(ctx, &keys)?;
@@ -1042,10 +1121,10 @@ fn downgrade_let_bindings<'id, 'ir, Ctx, F>(
entries: Vec<'ir, ast::Entry>, entries: Vec<'ir, ast::Entry>,
ctx: &mut Ctx, ctx: &mut Ctx,
body_fn: F, body_fn: F,
) -> Result<IrRef<'id, 'ir>> ) -> Result<GhostRoIrRef<'id, 'ir>>
where where
Ctx: DowngradeContext<'id, 'ir>, Ctx: DowngradeContext<'id, 'ir>,
F: FnOnce(&mut Ctx, &[SymId]) -> Result<IrRef<'id, 'ir>>, F: FnOnce(&mut Ctx, &[StringId]) -> Result<GhostRoIrRef<'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)
@@ -1055,7 +1134,7 @@ where
fn downgrade_rec_bindings<'id, 'ir, Ctx>( fn downgrade_rec_bindings<'id, 'ir, Ctx>(
entries: Vec<'ir, ast::Entry>, entries: Vec<'ir, ast::Entry>,
ctx: &mut Ctx, ctx: &mut Ctx,
) -> Result<IrRef<'id, 'ir>> ) -> Result<GhostRoIrRef<'id, 'ir>>
where where
Ctx: DowngradeContext<'id, 'ir>, Ctx: DowngradeContext<'id, 'ir>,
{ {
@@ -1076,14 +1155,18 @@ fn downgrade_rec_attrs_impl<'id, 'ir, Ctx, F, const ALLOW_DYN: bool>(
entries: Vec<'ir, ast::Entry>, entries: Vec<'ir, ast::Entry>,
ctx: &mut Ctx, ctx: &mut Ctx,
body_fn: F, body_fn: F,
) -> Result<IrRef<'id, 'ir>> ) -> Result<GhostRoIrRef<'id, 'ir>>
where where
Ctx: DowngradeContext<'id, 'ir>, Ctx: DowngradeContext<'id, 'ir>,
F: FnOnce( F: FnOnce(
&mut Ctx, &mut Ctx,
&[SymId], &[StringId],
&[(IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)], &[(
) -> Result<IrRef<'id, 'ir>>, GhostRoIrRef<'id, 'ir>,
GhostRoMaybeThunkRef<'id, 'ir>,
TextRange,
)],
) -> Result<GhostRoIrRef<'id, 'ir>>,
{ {
let mut pending = PendingAttrSet::new_in(ctx.bump()); let mut pending = PendingAttrSet::new_in(ctx.bump());
pending.collect_entries(entries.iter().cloned(), ctx)?; pending.collect_entries(entries.iter().cloned(), ctx)?;
@@ -1095,18 +1178,21 @@ where
ctx.with_let_scope(&keys, |ctx| { ctx.with_let_scope(&keys, |ctx| {
let finalized = finalize_pending_set::<_, ALLOW_DYN>(pending, &inherit_lookups, ctx)?; let finalized = finalize_pending_set::<_, ALLOW_DYN>(pending, &inherit_lookups, ctx)?;
let vals = keys let vals = {
.iter() let mut temp = Vec::with_capacity_in(keys.len(), ctx.bump());
.map(|sym| finalized.stcs.get(sym).expect("WTF").0) for sym in &keys {
.collect_in(ctx.bump()); temp.push(finalized.stcs.get(sym).expect("WTF").0);
}
temp
};
body_fn(ctx, &keys, &finalized.dyns).map(|body| (vals, body)) body_fn(ctx, &keys, &finalized.dyns).map(|body| (vals, body))
}) })
} }
fn collect_inherit_lookups<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>>( fn collect_inherit_lookups<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>>(
entries: &[ast::Entry], entries: &[ast::Entry],
ctx: &mut Ctx, ctx: &mut Ctx,
) -> Result<HashMap<'ir, SymId, (IrRef<'id, 'ir>, TextRange)>> { ) -> Result<HashMap<'ir, StringId, (GhostRoMaybeThunkRef<'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
@@ -1115,7 +1201,7 @@ fn collect_inherit_lookups<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>>(
for attr in inherit.attrs() { for attr in inherit.attrs() {
if let ast::Attr::Ident(ident) = attr { if let ast::Attr::Ident(ident) = attr {
let attr_span = ident.syntax().text_range(); let attr_span = ident.syntax().text_range();
let sym = ctx.new_sym(ident.to_string()); let sym = ctx.intern_string(ident.to_string());
let expr = ctx.lookup(sym, attr_span)?; let expr = ctx.lookup(sym, attr_span)?;
inherit_lookups.insert(sym, (expr, attr_span)); inherit_lookups.insert(sym, (expr, attr_span));
} }
@@ -1128,13 +1214,13 @@ 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<SymId>> { ) -> Result<HashSet<StringId>> {
let mut binding_syms = HashSet::new(); let mut binding_syms = HashSet::new();
for (sym, (_, span)) in &pending.stcs { for (sym, (_, span)) in &pending.stcs {
if !binding_syms.insert(*sym) { if !binding_syms.insert(*sym) {
return Err(Error::downgrade_error( return Err(Error::downgrade_error(
format!("attribute '{}' already defined", ctx.get_sym(*sym)), format!("attribute '{}' already defined", ctx.resolve_sym(*sym)),
ctx.get_current_source(), ctx.get_current_source(),
*span, *span,
)); ));
@@ -1146,15 +1232,15 @@ 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<SymId, (IrRef<'id, 'ir>, TextRange)>, inherit_lookups: &HashMap<StringId, (GhostRoMaybeThunkRef<'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());
let mut dyns = Vec::new_in(ctx.bump()); let mut dyns = Vec::new_in(ctx.bump());
for (sym, (value, value_span)) in pending.stcs { for (sym, (value, value_span)) in pending.stcs {
let expr_id = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?; let expr = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?;
stcs.insert(sym, (expr_id, value_span)); stcs.insert(sym, (ctx.maybe_thunk(expr), value_span));
} }
if ALLOW_DYN { if ALLOW_DYN {
@@ -1162,12 +1248,10 @@ fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_D
let key_id = downgrade_attr(attr, ctx)?; let key_id = downgrade_attr(attr, ctx)?;
let key_expr = match key_id { let key_expr = match key_id {
Attr::Dynamic(id, _) => id, Attr::Dynamic(id, _) => id,
Attr::Str(sym, _attr_span) => { Attr::Str(sym, _attr_span) => ctx.new_expr(Ir::Str(sym)),
ctx.new_expr(Ir::Str(ctx.get_sym(sym).to_string().box_in(ctx.bump())))
}
}; };
let value_id = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?; let value = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?;
dyns.push((key_expr, value_id, value_span)); dyns.push((key_expr, ctx.maybe_thunk(value), value_span));
} }
} }
@@ -1176,30 +1260,26 @@ 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<SymId, (IrRef<'id, 'ir>, TextRange)>, inherit_lookups: &HashMap<StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>,
ctx: &mut Ctx, ctx: &mut Ctx,
) -> Result<IrRef<'id, 'ir>> { ) -> Result<GhostRoIrRef<'id, 'ir>> {
match value { match value {
PendingValue::Expr(expr) => { PendingValue::Expr(expr) => expr.downgrade(ctx),
let id = Downgrade::downgrade(expr, ctx)?;
Ok(ctx.maybe_thunk(id))
}
PendingValue::InheritFrom(from_expr, sym, span) => { PendingValue::InheritFrom(from_expr, sym, span) => {
let from_id = Downgrade::downgrade(from_expr, ctx)?; let from = Downgrade::downgrade(from_expr, ctx)?;
let select_id = ctx.new_expr(Ir::Select { Ok(ctx.new_expr(Ir::Select {
expr: from_id, expr: from,
attrpath: Vec::from_iter_in([Attr::Str(sym, span)], ctx.bump()), attrpath: Vec::from_iter_in([Attr::Str(sym, span)], ctx.bump()),
default: None, default: None,
span, span,
}); }))
Ok(ctx.maybe_thunk(select_id))
} }
PendingValue::InheritScope(sym, span) => { PendingValue::InheritScope(sym, span) => {
if let Some(&(expr, _)) = inherit_lookups.get(&sym) { if let Some(&(expr, _)) = inherit_lookups.get(&sym) {
Ok(ctx.maybe_thunk(expr)) Ok(ctx.new_expr(Ir::MaybeThunk(expr)))
} else { } else {
let lookup_id = ctx.lookup(sym, span)?; ctx.lookup(sym, span)
Ok(ctx.maybe_thunk(lookup_id)) .map(|val| ctx.new_expr(Ir::MaybeThunk(val)))
} }
} }
PendingValue::Set(set) => { PendingValue::Set(set) => {
@@ -1221,15 +1301,15 @@ fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW
fn downgrade_path<'id, 'ir>( fn downgrade_path<'id, 'ir>(
parts: impl IntoIterator<Item = ast::InterpolPart<ast::PathContent>>, parts: impl IntoIterator<Item = ast::InterpolPart<ast::PathContent>>,
_span: rnix::TextRange,
ctx: &mut impl DowngradeContext<'id, 'ir>, ctx: &mut impl DowngradeContext<'id, 'ir>,
) -> Result<IrRef<'id, 'ir>> { ) -> Result<GhostRoIrRef<'id, 'ir>> {
let bump = ctx.bump(); let bump = ctx.bump();
let parts = parts let parts = parts
.into_iter() .into_iter()
.map(|part| match part { .map(|part| match part {
ast::InterpolPart::Literal(lit) => { ast::InterpolPart::Literal(lit) => {
Ok(ctx.new_expr(Ir::Str(lit.text().to_string().box_in(ctx.bump())))) let id = ctx.intern_string(lit.text());
Ok(ctx.new_expr(Ir::Str(id)))
} }
ast::InterpolPart::Interpolation(interpol) => interpol ast::InterpolPart::Interpolation(interpol) => interpol
.expr() .expr()
+332
View File
@@ -0,0 +1,332 @@
use std::hash::Hash;
use std::marker::PhantomData;
use bumpalo::Bump;
use bumpalo::collections::Vec;
use fix_builtins::{BUILTINS, BuiltinId};
use fix_common::StringId;
use ghost_cell::{GhostCell, GhostToken};
use num_enum::TryFromPrimitive as _;
use rnix::{TextRange, ast};
use string_interner::DefaultStringInterner;
pub mod downgrade;
pub type HashMap<'ir, K, V> = hashbrown::HashMap<K, V, hashbrown::DefaultHashBuilder, &'ir Bump>;
pub type GhostIrRef<'id, 'ir> = <GhostRef<'id, 'ir> as RefExt<'ir>>::IrRef;
pub type GhostRoIrRef<'id, 'ir> = <GhostRoRef<'id, 'ir> as RefExt<'ir>>::IrRef;
pub type RawIrRef<'ir> = <RawRef<'ir> as RefExt<'ir>>::IrRef;
pub type GhostMaybeThunkRef<'id, 'ir> = <GhostRef<'id, 'ir> as RefExt<'ir>>::MaybeThunkRef;
pub type GhostRoMaybeThunkRef<'id, 'ir> = <GhostRoRef<'id, 'ir> as RefExt<'ir>>::MaybeThunkRef;
impl<'id, 'ir> Ir<'ir, GhostRoRef<'id, 'ir>> {
/// Freeze a mutable IR reference into a read-only one, consuming the
/// `GhostToken` to prevent any further mutation.
pub fn freeze(this: GhostRoIrRef<'id, 'ir>, _: GhostToken<'id>) -> RawIrRef<'ir> {
// SAFETY: The transmute is sound because:
// - `GhostCell<'id, T>` is `#[repr(transparent)]` over `T`, so
// `&'ir GhostCell<'id, T>` and `&'ir T` have identical layout.
// - `Ir<'ir, R>` is `#[repr(C)]`, and for every field that depends on
// `R`, instantiating `R = GhostRef<'id, 'ir>` vs `R = RawRef<'ir>`
// produces types of identical layout:
// - `R::IrRef` becomes `&'ir GhostCell<'id, Ir<...>>` vs `&'ir Ir<...>`
// - `R::MaybeThunkRef` becomes `&'ir GhostCell<'id, MaybeThunk>`
// vs `&'ir MaybeThunk`
// - `R::Ref<Ir<'ir, R>>` (used in `ConcatStrings::parts`) reduces
// to the same case as `R::IrRef`
// - Therefore `IrRef<'id, 'ir>` and `RawIrRef<'ir>` are both
// pointer-sized references with the same layout.
//
// Consuming the `GhostToken` guarantees no `borrow_mut` calls can
// occur afterwards, so the shared `&Ir` references reachable from a
// `RawIrRef<'ir>` can never alias with mutable references.
unsafe { std::mem::transmute::<GhostRoIrRef<'id, 'ir>, RawIrRef<'ir>>(this) }
}
}
#[repr(transparent)]
pub struct GhostRoCell<'id, T: ?Sized>(GhostCell<'id, T>);
impl<'id, T> From<GhostCell<'id, T>> for GhostRoCell<'id, T> {
fn from(value: GhostCell<'id, T>) -> Self {
Self(value)
}
}
impl<'id, T: ?Sized> From<&GhostCell<'id, T>> for &GhostRoCell<'id, T> {
fn from(value: &GhostCell<'id, T>) -> Self {
// SAFETY: `GhostRoCell` is `#[repr(transparent)]` over `GhostCell`
// TODO: document mutability
unsafe { std::mem::transmute(value) }
}
}
impl<'id, T: ?Sized> From<&T> for &GhostRoCell<'id, T> {
fn from(value: &T) -> Self {
// SAFETY: `GhostRoCell` is `#[repr(transparent)]` over `GhostCell`,
// which is `#[repr(transparent)]` over `T`
// TODO: document mutability
unsafe { std::mem::transmute(value) }
}
}
impl<'id, T: ?Sized> GhostRoCell<'id, T> {
pub fn borrow<'a>(&'a self, token: &'a GhostToken<'id>) -> &'a T {
self.0.borrow(token)
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum MaybeThunk {
Int(i64),
Float(f64),
Bool(bool),
Null,
Str(StringId),
Path(StringId),
Thunk(ThunkId),
Arg { layer: u8 },
Builtin(BuiltinId),
BuiltinConst(StringId),
Builtins,
ReplBinding(StringId),
ScopedImportBinding(StringId),
}
pub trait Ref<'ir> {
type Ref<T>
where
T: 'ir;
}
pub trait RefExt<'ir>: Ref<'ir> {
type Ir;
type IrRef;
type MaybeThunkRef;
}
impl<'ir, T: Ref<'ir> + 'ir> RefExt<'ir> for T {
type Ir = Ir<'ir, Self>;
type IrRef = Self::Ref<Self::Ir>;
type MaybeThunkRef = Self::Ref<MaybeThunk>;
}
pub struct GhostRef<'id, 'ir>(PhantomData<&'ir GhostCell<'id, ()>>);
pub struct GhostRoRef<'id, 'ir>(PhantomData<&'ir GhostRoCell<'id, ()>>);
pub struct RawRef<'ir>(PhantomData<&'ir ()>);
impl<'id, 'ir> Ref<'ir> for GhostRef<'id, 'ir> {
type Ref<T: 'ir> = &'ir GhostCell<'id, T>;
}
impl<'id, 'ir> Ref<'ir> for GhostRoRef<'id, 'ir> {
type Ref<T: 'ir> = &'ir GhostRoCell<'id, T>;
}
impl<'ir> Ref<'ir> for RawRef<'ir> {
type Ref<T: 'ir> = &'ir T;
}
#[repr(C)]
#[derive(Debug)]
pub enum Ir<'ir, R: RefExt<'ir> + ?Sized + 'ir> {
Int(i64),
Float(f64),
Bool(bool),
Null,
Str(StringId),
Path(R::IrRef),
AttrSet {
stcs: HashMap<'ir, StringId, (R::MaybeThunkRef, TextRange)>,
dyns: Vec<'ir, (R::IrRef, R::MaybeThunkRef, TextRange)>,
},
List {
items: Vec<'ir, R::MaybeThunkRef>,
},
ConcatStrings {
parts: Vec<'ir, R::Ref<Ir<'ir, R>>>,
force_string: bool,
},
// OPs
UnOp {
rhs: R::IrRef,
kind: UnOpKind,
},
BinOp {
lhs: R::IrRef,
rhs: R::IrRef,
kind: BinOpKind,
},
HasAttr {
lhs: R::IrRef,
rhs: Vec<'ir, Attr<R::IrRef>>,
},
Select {
expr: R::IrRef,
attrpath: Vec<'ir, Attr<R::IrRef>>,
default: Option<R::IrRef>,
span: TextRange,
},
// Conditionals
If {
cond: R::IrRef,
consq: R::IrRef,
alter: R::IrRef,
},
Assert {
assertion: R::IrRef,
expr: R::IrRef,
assertion_raw: String,
span: TextRange,
},
WithLookup {
sym: StringId,
namespaces: Vec<'ir, R::MaybeThunkRef>,
},
// Function related
Func {
body: R::IrRef,
param: Option<Param<'ir>>,
thunks: Vec<'ir, (ThunkId, R::IrRef)>,
},
Arg {
layer: u8,
},
Call {
func: R::IrRef,
arg: R::MaybeThunkRef,
span: TextRange,
},
// Builtins
Builtins,
Builtin(BuiltinId),
BuiltinConst(StringId),
// Misc
TopLevel {
body: R::IrRef,
thunks: Vec<'ir, (ThunkId, R::IrRef)>,
},
MaybeThunk(R::MaybeThunkRef),
ReplBinding(StringId),
ScopedImportBinding(StringId),
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ThunkId(pub usize);
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SpanId(pub u32);
/// Represents a key in an attribute path.
#[allow(unused)]
#[derive(Debug)]
pub enum Attr<Ref> {
/// A dynamic attribute key, which is an expression that must evaluate to a string.
/// Example: `attrs.${key}`
Dynamic(Ref, TextRange),
/// A static attribute key.
/// Example: `attrs.key`
Str(StringId, TextRange),
}
/// The kinds of binary operations supported in Nix.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum BinOpKind {
// Arithmetic
Add,
Sub,
Div,
Mul,
// Comparison
Eq,
Neq,
Lt,
Gt,
Leq,
Geq,
// Logical
And,
Or,
Impl,
// Set/String/Path operations
Con, // List concatenation (`++`)
Upd, // AttrSet update (`//`)
}
/// The kinds of unary operations.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum UnOpKind {
Neg, // Negation (`-`)
Not, // Logical not (`!`)
}
impl From<ast::UnaryOpKind> for UnOpKind {
fn from(value: ast::UnaryOpKind) -> Self {
match value {
ast::UnaryOpKind::Invert => UnOpKind::Not,
ast::UnaryOpKind::Negate => UnOpKind::Neg,
}
}
}
/// Describes the parameters of a function.
#[derive(Debug)]
pub struct Param<'ir> {
pub required: Vec<'ir, (StringId, TextRange)>,
pub optional: Vec<'ir, (StringId, TextRange)>,
pub ellipsis: bool,
}
pub fn new_global_env(
strings: &mut DefaultStringInterner,
) -> hashbrown::HashMap<StringId, MaybeThunk> {
let mut global_env = hashbrown::HashMap::new();
let builtins_sym = StringId(strings.get_or_intern("builtins"));
global_env.insert(builtins_sym, MaybeThunk::Builtins);
for (idx, &(name, _)) in BUILTINS.iter().enumerate() {
let id = BuiltinId::try_from_primitive(idx as u8).expect("infallible");
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, MaybeThunk::Builtin(id));
}
let consts = [
(
"__currentSystem",
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("currentSystem"))),
),
("__langVersion", MaybeThunk::Int(6)),
(
"__nixVersion",
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("nixVersion"))),
),
(
"__storeDir",
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("storeDir"))),
),
(
"__nixPath",
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("nixPath"))),
),
("null", MaybeThunk::Null),
("true", MaybeThunk::Bool(true)),
("false", MaybeThunk::Bool(false)),
];
for (name, ir) in consts {
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, ir);
}
global_env
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "fix-primops"
version = "0.1.0"
edition = "2024"
[dependencies]
gc-arena = { workspace = true }
hashbrown = { workspace = true }
num_enum = { workspace = true }
smallvec = { workspace = true }
string-interner = { workspace = true }
fix-abstract-vm = { path = "../fix-abstract-vm" }
fix-builtins = { path = "../fix-builtins" }
fix-codegen = { path = "../fix-codegen" }
fix-common = { path = "../fix-common" }
fix-error = { path = "../fix-error" }
+362
View File
@@ -0,0 +1,362 @@
use fix_abstract_vm::{
AttrSet, BytecodeReader, Closure, Env, List, Machine, MachineExt, Step, StrictValue, Value,
VmRuntimeCtx, VmRuntimeCtxExt,
};
use fix_builtins::PrimOpPhase;
use fix_error::Error;
use gc_arena::{Gc, Mutation, RefLock};
use smallvec::SmallVec;
pub fn seq<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [e1, e2] - force e1, return e2
m.force_slot(1, reader, mc)?;
let e2 = m.pop();
let _ = m.pop();
m.return_from_primop(e2, reader)
}
pub fn abort<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [msg] - force msg, then abort with it
m.force_slot(0, reader, mc)?;
let msg_val = m.peek_forced(0);
let msg = ctx.get_string(msg_val).unwrap_or("<non-string-value>");
m.finish_err(Error::eval_error(format!(
"evaluation aborted with the following error message: '{msg}'"
)))
}
pub fn deep_seq_force_top<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [e1, e2] - force e1, return e2
m.force_slot(1, reader, mc)?;
let e1 = m.peek_forced(1);
let children: SmallVec<_> = if let Some(attrs) = e1.as_gc::<AttrSet>() {
let attrs = &attrs.entries;
if attrs.is_empty() {
SmallVec::new()
} else {
attrs.iter().map(|&(_, v)| v).collect()
}
} else if let Some(list) = e1.as_gc::<List<'gc>>() {
let inner = list.inner.borrow();
if inner.is_empty() {
SmallVec::new()
} else {
inner.iter().copied().collect()
}
} else {
SmallVec::new()
};
if children.is_empty() {
let e2 = m.pop();
let _ = m.pop();
return m.return_from_primop(e2, reader);
}
let count = children.len() as i32;
let seen: Gc<'gc, List<'gc>> = Gc::new(mc, List::default());
let worklist: Gc<'gc, List<'gc>> = List::new(mc, children);
let e2 = m.pop();
let _ = m.pop();
m.push(e2);
m.push(Value::new_gc(seen));
m.push(Value::new_gc(worklist));
m.push(Value::new_inline(count));
reader.set_pc(PrimOpPhase::DeepSeqPush.ip() as usize);
Step::Continue(())
}
pub fn deep_seq_push<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [e2, seen, worklist, counter]
#[allow(clippy::unwrap_used)]
let counter = m.peek(0).as_inline::<i32>().unwrap();
if counter == 0 {
let _ = m.pop(); // counter
let _ = m.pop(); // worklist
let _ = m.pop(); // seen
let val = m.pop();
return m.return_from_primop(val, reader);
}
#[allow(clippy::unwrap_used)]
let worklist = m.peek_forced(1).as_gc::<List<'gc>>().unwrap();
#[allow(clippy::unwrap_used)]
let item = worklist.unlock(mc).borrow_mut().pop().unwrap();
m.replace(0, Value::new_inline(counter - 1));
m.push(item);
// force item at TOS, resume at DeepSeqLoop after force
m.force_slot_to_pc(0, reader, mc, PrimOpPhase::DeepSeqLoop.ip() as usize)?;
reader.set_pc(PrimOpPhase::DeepSeqLoop.ip() as usize);
Step::Continue(())
}
pub fn deep_seq_loop<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack after pop: [e2, seen, worklist, counter]
let item = m.pop();
#[allow(clippy::unwrap_used)]
let counter = m.peek(0).as_inline::<i32>().unwrap();
let mut added: usize = 0;
if let Some(attrs) = item.as_gc::<AttrSet>() {
let attrs = &attrs.entries;
#[allow(clippy::unwrap_used)]
let seen = m.peek_forced(2).as_gc::<List<'gc>>().unwrap();
if !is_value_in_seen(seen, item) {
add_value_to_seen(seen, mc, item);
#[allow(clippy::unwrap_used)]
let worklist = m.peek_forced(1).as_gc::<List<'gc>>().unwrap();
{
let mut wl = worklist.unlock(mc).borrow_mut();
for &(_, v) in attrs.iter() {
wl.push(v);
}
added = attrs.len();
}
}
} else if let Some(list) = item.as_gc::<List<'gc>>() {
#[allow(clippy::unwrap_used)]
let seen = m.peek_forced(2).as_gc::<List<'gc>>().unwrap();
if !is_value_in_seen(seen, item) {
add_value_to_seen(seen, mc, item);
#[allow(clippy::unwrap_used)]
let worklist = m.peek_forced(1).as_gc::<List<'gc>>().unwrap();
{
let inner = list.inner.borrow();
let mut wl = worklist.unlock(mc).borrow_mut();
for &v in inner.iter() {
wl.push(v);
}
added = inner.len();
}
}
}
m.replace(0, Value::new_inline(counter + added as i32));
reader.set_pc(PrimOpPhase::DeepSeqPush.ip() as usize);
Step::Continue(())
}
pub fn force_result_shallow<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let val = m.peek_forced(0);
let (count, has_children) = if let Some(attrs) = val.as_gc::<AttrSet>() {
let len = attrs.entries.len();
(len, len > 0)
} else if let Some(list) = val.as_gc::<List<'gc>>() {
let len = list.inner.borrow().len();
(len, len > 0)
} else {
(0, false)
};
if !has_children {
let val = m.pop();
return m.finish_ok(ctx.convert_value(val));
}
m.push(Value::new_inline(0i32));
m.push(Value::new_inline(count as i32));
reader.set_pc(PrimOpPhase::ForceResultShallowPush.ip() as usize);
Step::Continue(())
}
pub fn force_result_shallow_push<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let len = m.peek(0).as_inline::<i32>().unwrap();
if idx == len {
let _ = m.pop(); // len
let _ = m.pop(); // idx
let val = m.pop();
return m.finish_ok(ctx.convert_value(val));
}
let val = m.peek_forced(2);
let child = if let Some(attrs) = val.as_gc::<AttrSet>() {
attrs.entries.get(idx as usize).map(|&(_, v)| v)
} else if let Some(list) = val.as_gc::<List<'gc>>() {
list.inner.borrow().get(idx as usize).copied()
} else {
None
};
if let Some(child) = child {
m.replace(1, Value::new_inline(idx + 1));
m.push(child);
m.force_slot_to_pc(
0,
reader,
mc,
PrimOpPhase::ForceResultShallowLoop.ip() as usize,
)?;
reader.set_pc(PrimOpPhase::ForceResultShallowLoop.ip() as usize);
}
Step::Continue(())
}
pub fn force_result_shallow_loop<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> Step {
let _ = m.pop(); // forced child
reader.set_pc(PrimOpPhase::ForceResultShallowPush.ip() as usize);
Step::Continue(())
}
pub fn force_result_deep_finish<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
m.finish_ok(ctx.convert_value(val.relax()))
}
fn is_value_in_seen<'gc>(seen: Gc<'gc, List<'gc>>, val: Value<'gc>) -> bool {
if !is_container(val) {
return false;
}
let target = val.to_bits();
for &v in seen.inner.borrow().iter() {
if v.to_bits() == target {
return true;
}
}
false
}
fn add_value_to_seen<'gc>(seen: Gc<'gc, List<'gc>>, mc: &Mutation<'gc>, val: Value<'gc>) {
if is_container(val) {
seen.unlock(mc).borrow_mut().push(val);
}
}
pub fn call_functor_1<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// Stack invariant on every (re-)entry: [..., orig_arg, self, functor]
// where `functor` is TOS. Retries during force land back here safely.
let functor = m.force_and_retry::<StrictValue>(reader, mc)?;
// Stack now: [..., orig_arg, self]
let self_val = m.pop();
m.push(functor.relax());
// Stack: [..., orig_arg, functor]
// Call 1: functor(self). Resume into CallFunctor2 once it returns.
m.call(
reader,
mc,
self_val,
PrimOpPhase::CallFunctor2.ip() as usize,
)
}
pub fn call_functor_2<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// Stack on entry: [..., orig_arg, intermediate]
// call_stack top: synthetic frame with caller's resume_pc.
let intermediate = m.pop();
let orig_arg = m.pop();
let saved = m.pop_call_frame().expect("functor outer frame missing");
m.set_env(saved.env);
m.push(intermediate);
// Call 2: intermediate(orig_arg). Resume to caller.
m.call(reader, mc, orig_arg, saved.pc)
}
pub fn call_pattern<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (func, attrset) = m.force_and_retry::<(Gc<Closure>, Gc<AttrSet>)>(reader, mc)?;
let Closure {
ip,
n_locals,
env,
pattern,
} = *func;
let Some(pattern) = pattern else {
unreachable!()
};
// TODO: get function name
// TODO: param spans
if !pattern.ellipsis {
for key in pattern.required.iter().copied() {
if attrset.lookup(key).is_none() {
let name = ctx.resolve_string(key);
return m.finish_err(Error::eval_error(format!(
"function 'anonymous lambda' called without required argument '{name}'"
)));
}
}
for &(key, _) in attrset.entries.iter() {
let is_expected = pattern.required.contains(&key) || pattern.optional.contains(&key);
if !is_expected {
let name = ctx.resolve_string(key);
return m.finish_err(Error::eval_error(format!(
"function 'anonymous lambda' called with unexpected argument '{name}'"
)));
}
}
}
let new_env = Gc::new(
mc,
RefLock::new(Env::with_arg(Value::new_gc(attrset), n_locals, env)),
);
reader.set_pc(ip as usize);
m.set_env(new_env);
Step::Continue(())
}
fn is_container(val: Value<'_>) -> bool {
val.is::<AttrSet>() || val.is::<List<'_>>()
}
+51
View File
@@ -0,0 +1,51 @@
use fix_abstract_vm::{
BytecodeReader, Machine, MachineExt, NixString, NixType, Path, Step, StrictValue, Value,
VmRuntimeCtx,
};
use fix_common::StringId;
use fix_error::Error;
use gc_arena::Mutation;
pub fn to_string<'gc, M: Machine<'gc>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if val.is::<StringId>() || val.is::<NixString>() {
return m.return_from_primop(val.relax(), reader);
}
if let Some(p) = val.as_inline::<Path>() {
return m.return_from_primop(Value::new_inline(p.0), reader);
}
// TODO: derivations / `__toString` / `outPath`,
// numbers, lists.
m.finish_err(Error::eval_error(format!(
"cannot coerce {} to a string",
val.ty()
)))
}
pub fn type_of<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
let name: &str = match val.ty() {
NixType::Int => "int",
NixType::Float => "float",
NixType::Bool => "bool",
NixType::Null => "null",
NixType::String => "string",
NixType::Path => "path",
NixType::AttrSet => "set",
NixType::List => "list",
NixType::Closure | NixType::PrimOp | NixType::PrimOpApp => "lambda",
NixType::Thunk => unreachable!("forced"),
};
let sid = ctx.intern_string(name);
m.return_from_primop(Value::new_inline(sid), reader)
}
+190
View File
@@ -0,0 +1,190 @@
use std::path::PathBuf;
use fix_abstract_vm::{
AttrSet, Break, BytecodeReader, CallFrame, Machine, MachineExt, Path, PendingLoad,
PendingScope, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt, canon_path_str,
};
use fix_builtins::PrimOpPhase;
use fix_common::StringId;
use fix_error::Error;
use gc_arena::{Gc, Mutation};
use hashbrown::HashSet;
pub fn import<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [path]
let path_val = m.force_and_retry::<StrictValue>(reader, mc)?;
let path_str = match ctx.get_string_or_path(path_val) {
Some(s) => s.to_owned(),
None => {
return m.finish_err(Error::eval_error(format!(
"expected a path or string, got {}",
path_val.ty()
)));
}
};
let abs = match resolve_import_target(&path_str) {
Ok(p) => p,
Err(e) => return m.finish_err(e),
};
if let Some(cached) = m.import_cache_get(&abs) {
return m.return_from_primop(cached, reader);
}
// Stash the resolved path on the stack as a string-id so the
// finalizer can use it as the cache key. The slot we pop here was
// freed by `force_and_retry`, so we simply push.
let path_sid = ctx.intern_string(abs.to_string_lossy());
m.push(Value::new_inline(path_sid));
let env = m.env();
m.push_call_frame(CallFrame {
pc: PrimOpPhase::ImportFinalize.ip() as usize,
thunk: None,
env,
});
m.set_pending_load(PendingLoad {
path: abs,
scope: None,
});
Step::Break(Break::LoadFile)
}
pub fn import_finalize<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
) -> Step {
// stack: [path_sid, return_value]
let val = m.pop();
#[allow(clippy::unwrap_used)]
let path_sid = m.pop().as_inline::<StringId>().unwrap();
// The cache key is keyed by the absolute path string we interned in
// `import`. Resolve it back to the host PathBuf.
let path_str = ctx.resolve_string(path_sid).to_owned();
m.import_cache_insert(PathBuf::from(path_str), val);
m.push(val);
let Some(CallFrame {
pc: ret_pc,
thunk: _,
env,
}) = m.pop_call_frame()
else {
unreachable!()
};
reader.set_pc(ret_pc);
// FIXME:
// m.dec_call_depth();
m.set_env(env);
Step::Continue(())
}
pub fn scoped_import<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [scope, path]
let (scope_attrs, path_val) = m.force_and_retry::<(Gc<AttrSet>, StrictValue)>(reader, mc)?;
let path_str = match ctx.get_string_or_path(path_val) {
Some(s) => s.to_owned(),
None => {
return m.finish_err(Error::eval_error(format!(
"expected a path or string, got {}",
path_val.ty()
)));
}
};
let abs = match resolve_import_target(&path_str) {
Ok(p) => p,
Err(e) => return m.finish_err(e),
};
let keys: HashSet<StringId> = scope_attrs.entries.iter().map(|&(k, _)| k).collect();
let slot_id = m.scope_slots_push(Value::new_gc(scope_attrs));
let env = m.env();
m.push_call_frame(CallFrame {
pc: PrimOpPhase::ScopedImportFinalize.ip() as usize,
thunk: None,
env,
});
m.set_pending_load(PendingLoad {
path: abs,
scope: Some(PendingScope { keys, slot_id }),
});
Step::Break(Break::LoadFile)
}
pub fn scoped_import_finalize<'gc, M: Machine<'gc>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> Step {
// stack: [return_value]
// We intentionally do NOT pop the slot from `scope_slots` so that
// closures or thunks created inside the imported file can still
// resolve their scope after `scopedImport` returns.
let val = m.pop();
m.return_from_primop(val, reader)
}
pub fn path_exists<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let path_val = m.force_and_retry::<StrictValue>(reader, mc)?;
// pathExists requires an absolute path. A `Path` value is
// always absolute; a string is accepted only if it starts with `/`.
let (path, is_path_value) = if let Some(p) = path_val.as_inline::<Path>() {
(ctx.resolve_string(p.0).to_owned(), true)
} else if let Some(s) = ctx.get_string(path_val) {
(s.to_owned(), false)
} else {
return m.finish_err(Error::eval_error(format!(
"expected a path or string, got {}",
path_val.ty()
)));
};
if !is_path_value && !path.starts_with('/') {
return m.finish_err(Error::eval_error(format!(
"string '{path}' doesn't represent an absolute path"
)));
}
// CppNix collapses consecutive slashes and resolves `.` / `..` lexically
// before checking. Trailing-slash / trailing-dot mean "must be a directory".
let must_be_dir = path.ends_with('/') || path.ends_with("/.");
let canon = canon_path_str(&path);
let p = std::path::Path::new(&canon);
let exists = if must_be_dir {
std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false)
} else {
std::fs::symlink_metadata(p).is_ok()
};
m.return_from_primop(Value::new_inline(exists), reader)
}
/// Convert the user-supplied path string into an absolute, dotted-segment
/// resolved `PathBuf` and append `default.nix` if the target is a directory.
fn resolve_import_target(path: &str) -> Result<PathBuf, Box<Error>> {
let mut abs = PathBuf::from(path);
if !abs.is_absolute() {
return Err(Error::eval_error(format!(
"import: expected an absolute path, got '{path}'"
)));
}
if abs.is_dir() {
abs.push("default.nix");
}
Ok(abs)
}
+69
View File
@@ -0,0 +1,69 @@
mod control;
mod conv;
mod io;
mod list;
mod path;
pub use control::*;
pub use conv::*;
use fix_abstract_vm::{BytecodeReader, Machine, Step, VmRuntimeCtx};
use fix_builtins::PrimOpPhase;
use fix_error::Error;
use gc_arena::Mutation;
pub use io::*;
pub use list::*;
pub use path::*;
#[allow(clippy::too_many_lines)]
pub fn dispatch_primop<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
use PrimOpPhase::*;
let phase_disc = reader.read_u8();
let Ok(phase) = PrimOpPhase::try_from(phase_disc) else {
return m.finish_err(Error::eval_error("invalid primop phase"));
};
match phase {
Abort => abort(m, ctx, reader, mc),
DeepSeq => deep_seq_force_top(m, reader, mc),
DeepSeqPush => deep_seq_push(m, reader, mc),
DeepSeqLoop => deep_seq_loop(m, reader, mc),
Seq => seq(m, reader, mc),
FilterForceList => filter_force_list(m, reader, mc),
FilterCallPred => filter_call_pred(m, reader, mc),
FilterCheck => filter_check(m, reader, mc),
FoldlStrict => foldl_strict_entry(m, reader, mc),
FoldlStrictEmpty => foldl_strict_empty(m, reader, mc),
FoldlStrictCall1 => foldl_strict_call1(m, reader, mc),
FoldlStrictCall2 => foldl_strict_call2(m, reader, mc),
FoldlStrictUpdate => foldl_strict_update(m, reader, mc),
ForceResultShallow => force_result_shallow(m, ctx, reader, mc),
ForceResultShallowPush => force_result_shallow_push(m, ctx, reader, mc),
ForceResultShallowLoop => force_result_shallow_loop(m, reader, mc),
ForceResultDeepFinish => force_result_deep_finish(m, ctx, reader, mc),
CallPattern => call_pattern(m, ctx, reader, mc),
CallFunctor1 => call_functor_1(m, reader, mc),
CallFunctor2 => call_functor_2(m, reader, mc),
Import => import(m, ctx, reader, mc),
ImportFinalize => import_finalize(m, ctx, reader),
ScopedImport => scoped_import(m, ctx, reader, mc),
ScopedImportFinalize => scoped_import_finalize(m, ctx, reader, mc),
PathExists => path_exists(m, ctx, reader, mc),
ToPath => to_path(m, ctx, reader, mc),
IsPath => is_path(m, reader, mc),
ToString => to_string(m, ctx, reader, mc),
TypeOf => type_of(m, ctx, reader, mc),
phase => todo!("primop phase {phase:?}"),
}
}
+166
View File
@@ -0,0 +1,166 @@
use fix_abstract_vm::{
BytecodeReader, List, Machine, MachineExt, NixType, Step, StrictValue, Value,
};
use fix_builtins::PrimOpPhase;
use gc_arena::Mutation;
pub fn filter_force_list<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let list = match m.peek_forced(0).expect_gc::<List>() {
Ok(list) => list,
Err(got) => return m.finish_type_err(NixType::List, got),
};
if list.inner.borrow().is_empty() {
let val = m.pop();
return m.return_from_primop(val, reader);
}
// prepare stack layout: [ pred list idx acc ]
m.push(Value::new_inline(0));
m.push(Value::new_gc(List::new_gc(mc)));
reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize);
Step::Continue(())
}
pub fn filter_call_pred<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(3, reader, mc)?;
let pred = m.peek_forced(3);
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let elem = m.peek_forced(2).as_gc::<List>().unwrap().inner.borrow()[idx as usize];
m.push(pred.relax());
m.call(reader, mc, elem, PrimOpPhase::FilterCheck.ip() as usize)
}
pub fn filter_check<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let ret = m.force_and_retry::<bool>(reader, mc)?;
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(2).as_gc::<List>().unwrap();
let list = list.inner.borrow();
#[allow(clippy::unwrap_used)]
let acc = m.peek_forced(0).as_gc::<List>().unwrap();
if ret {
let mut acc = acc.unlock(mc).borrow_mut();
acc.push(list[idx as usize]);
}
if idx as usize == list.len() - 1 {
let acc = m.pop();
let _ = m.pop(); // idx
let _ = m.pop(); // list
let _ = m.pop(); // pred
return m.return_from_primop(acc, reader);
}
m.replace(1, Value::new_inline(idx + 1));
reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize);
Step::Continue(())
}
// foldl' op nul list
//
// Stack layouts across phases:
// Entry: [op, nul, list]
// Empty: [op, nul]
// Call1: [op, list, idx, acc]
// Call2: [op, list, idx, acc, intermediate]
// Update: [op, list, idx, acc, result]
pub fn foldl_strict_entry<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let list_val = m.peek_forced(0);
let Some(list) = list_val.as_gc::<List>() else {
return m.finish_type_err(NixType::List, list_val.ty());
};
if list.inner.borrow().is_empty() {
let _ = m.pop(); // list
reader.set_pc(PrimOpPhase::FoldlStrictEmpty.ip() as usize);
return Step::Continue(());
}
let list_val = m.pop();
let nul_val = m.pop();
m.push(list_val);
m.push(Value::new_inline(0i32));
m.push(nul_val);
reader.set_pc(PrimOpPhase::FoldlStrictCall1.ip() as usize);
Step::Continue(())
}
pub fn foldl_strict_empty<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let nul = m.force_and_retry::<StrictValue>(reader, mc)?;
let _ = m.pop(); // op
m.return_from_primop(nul.relax(), reader)
}
pub fn foldl_strict_call1<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(3, reader, mc)?;
let op = m.peek_forced(3);
let acc = m.peek(0);
m.push(op.relax());
m.call(reader, mc, acc, PrimOpPhase::FoldlStrictCall2.ip() as usize)
}
pub fn foldl_strict_call2<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
#[allow(clippy::unwrap_used)]
let idx = m.peek(2).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(3).as_gc::<List>().unwrap();
let elem = list.inner.borrow()[idx as usize];
m.call(
reader,
mc,
elem,
PrimOpPhase::FoldlStrictUpdate.ip() as usize,
)
}
pub fn foldl_strict_update<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> Step {
let result = m.pop();
m.replace(0, result);
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(2).as_gc::<List>().unwrap();
let len = list.inner.borrow().len();
if (idx as usize) + 1 == len {
let acc = m.pop();
let _ = m.pop(); // idx
let _ = m.pop(); // list
let _ = m.pop(); // op
return m.return_from_primop(acc, reader);
}
m.replace(1, Value::new_inline(idx + 1));
reader.set_pc(PrimOpPhase::FoldlStrictCall1.ip() as usize);
Step::Continue(())
}
+43
View File
@@ -0,0 +1,43 @@
use fix_abstract_vm::{
BytecodeReader, Machine, MachineExt, Path, Step, StrictValue, Value, VmRuntimeCtx,
VmRuntimeCtxExt, canon_path_str,
};
use fix_error::Error;
use gc_arena::Mutation;
pub fn to_path<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// coerce to path THEN TO STRING
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(Path(s)) = val.as_inline::<Path>() {
return m.return_from_primop(Value::new_inline(s), reader);
}
let Some(s) = ctx.get_string(val) else {
return m.finish_err(Error::eval_error(format!(
"cannot coerce {} to a path",
val.ty()
)));
};
if !s.starts_with('/') {
return m.finish_err(Error::eval_error(format!(
"string '{s}' doesn't represent an absolute path"
)));
}
let canon = canon_path_str(s);
let sid = ctx.intern_string(canon);
m.return_from_primop(Value::new_inline(sid), reader)
}
pub fn is_path<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
let is_path = val.is::<Path>();
m.return_from_primop(Value::new_inline(is_path), reader)
}
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "fix-vm"
version = "0.1.0"
edition = "2024"
[features]
tailcall = []
[dependencies]
gc-arena = { workspace = true }
hashbrown = { workspace = true }
num_enum = { workspace = true }
smallvec = { workspace = true }
string-interner = { workspace = true }
likely_stable = { workspace = true }
sptr = "0.3"
fix-builtins = { path = "../fix-builtins" }
fix-codegen = { path = "../fix-codegen" }
fix-common = { path = "../fix-common" }
fix-error = { path = "../fix-error" }
fix-abstract-vm = { path = "../fix-abstract-vm" }
fix-primops = { path = "../fix-primops" }
+304
View File
@@ -0,0 +1,304 @@
#![cfg(feature = "tailcall")]
use gc_arena::Mutation;
use crate::{Break, BytecodeReader, Step, Vm, VmRuntimeCtx};
pub(crate) enum TailResult {
YieldFuel(u32),
Done,
LoadFile,
}
pub(crate) type OpFn<'gc, C> = extern "rust-preserve-none" fn(
&mut Vm<'gc>,
&Mutation<'gc>,
&mut C,
&[u8],
&DispatchTable<'gc, C>,
u32,
u32,
) -> TailResult;
pub(crate) struct DispatchTable<'gc, C: VmRuntimeCtx>(pub(crate) [OpFn<'gc, C>; 256]);
extern "rust-preserve-none" fn op_illegal<'gc, C: VmRuntimeCtx>(
_vm: &mut Vm<'gc>,
_mc: &Mutation<'gc>,
_ctx: &mut C,
_bc: &[u8],
_table: &DispatchTable<'gc, C>,
pc: u32,
_fuel: u32,
) -> TailResult {
panic!("illegal opcode at pc = {pc}");
}
macro_rules! tail_dispatch_after {
($result:expr, $new_pc:expr, $vm:ident, $mc:ident, $ctx:ident, $bc:ident, $table:ident, $fuel:ident) => {{
match $result {
Step::Continue(()) | Step::Break(Break::Force) => {}
Step::Break(Break::LoadFile) => return TailResult::LoadFile,
Step::Break(Break::Done) => return TailResult::Done,
}
let new_pc: u32 = $new_pc;
if $fuel == 0 {
return TailResult::YieldFuel(new_pc);
}
let next_op = $bc[new_pc as usize] as usize;
become $table.0[next_op]($vm, $mc, $ctx, $bc, $table, new_pc, $fuel - 1)
}};
}
macro_rules! tail_fn {
($name:ident, ()) => {
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult {
let result = vm.$name();
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (reader)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = vm.$name(&mut reader);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (reader, mc)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = vm.$name(&mut reader, mc);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (ctx, reader, mc)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = vm.$name(ctx, &mut reader, mc);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (ctx)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult {
let result = vm.$name(ctx);
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
}
};
}
tail_fn!(op_push_smi, (reader));
tail_fn!(op_push_bigint, (reader, mc));
tail_fn!(op_push_float, (reader));
tail_fn!(op_push_string, (reader));
tail_fn!(op_push_null, ());
tail_fn!(op_push_true, ());
tail_fn!(op_push_false, ());
tail_fn!(op_load_local, (reader));
tail_fn!(op_load_outer, (reader));
tail_fn!(op_store_local, (reader, mc));
tail_fn!(op_alloc_locals, (reader, mc));
tail_fn!(op_make_thunk, (reader, mc));
tail_fn!(op_make_closure, (reader, mc));
tail_fn!(op_make_pattern_closure, (reader, mc));
tail_fn!(op_call, (ctx, reader, mc));
tail_fn!(op_dispatch_primop, (ctx, reader, mc));
tail_fn!(op_return, (ctx, reader, mc));
tail_fn!(op_make_attrs, (ctx, reader, mc));
tail_fn!(op_make_empty_attrs, ());
tail_fn!(op_select_static, (ctx, reader, mc));
tail_fn!(op_select_dynamic, (ctx, reader, mc));
tail_fn!(op_has_attr_path_static, (ctx, reader, mc));
tail_fn!(op_has_attr_path_dynamic, (ctx, reader, mc));
tail_fn!(op_jump_if_select_failed, (reader));
tail_fn!(op_jump_if_select_succeeded, (reader));
tail_fn!(op_has_attr_static, (reader, mc));
tail_fn!(op_has_attr_dynamic, (ctx, reader, mc));
tail_fn!(op_has_attr_resolve, ());
tail_fn!(op_make_list, (ctx, reader, mc));
tail_fn!(op_make_empty_list, ());
tail_fn!(op_add, (ctx, reader, mc));
tail_fn!(op_sub, (reader, mc));
tail_fn!(op_mul, (reader, mc));
tail_fn!(op_div, (reader, mc));
tail_fn!(op_eq, (ctx, reader, mc));
tail_fn!(op_neq, (ctx, reader, mc));
tail_fn!(op_lt, (ctx, reader, mc));
tail_fn!(op_gt, (ctx, reader, mc));
tail_fn!(op_leq, (ctx, reader, mc));
tail_fn!(op_geq, (ctx, reader, mc));
tail_fn!(op_concat, (reader, mc));
tail_fn!(op_update, (reader, mc));
tail_fn!(op_neg, (reader, mc));
tail_fn!(op_not, (reader, mc));
tail_fn!(op_jump_if_false, (reader, mc));
tail_fn!(op_jump_if_true, (reader, mc));
tail_fn!(op_jump, (reader));
tail_fn!(op_coerce_to_string, (reader, mc));
tail_fn!(op_concat_strings, (ctx, reader, mc));
tail_fn!(op_resolve_path, (ctx, reader, mc));
tail_fn!(op_assert, (ctx, reader, mc));
tail_fn!(op_lookup_with, (ctx, reader, mc));
tail_fn!(op_load_builtins, ());
tail_fn!(op_load_builtin, (reader));
tail_fn!(op_load_repl_binding, (reader));
tail_fn!(op_load_scoped_binding, (ctx, reader, mc));
macro_rules! table {
($($variant:ident => $fn:ident),* $(,)?) => {
impl<'gc, C: VmRuntimeCtx> DispatchTable<'gc, C> {
pub(crate) const NEW: Self = {
let mut arr: [OpFn<'gc, C>; 256] = [op_illegal; 256];
$( arr[fix_codegen::Op::$variant as usize] = $fn; )*
DispatchTable(arr)
};
}
// Exhaustiveness check: fails to compile if `fix_codegen::Op` gains,
// loses, or renames a variant that isn't wired up above.
#[allow(dead_code)]
const _: fn(fix_codegen::Op) = |op| match op {
$( fix_codegen::Op::$variant => (), )*
};
};
}
table! {
PushSmi => op_push_smi,
PushBigInt => op_push_bigint,
PushFloat => op_push_float,
PushString => op_push_string,
PushNull => op_push_null,
PushTrue => op_push_true,
PushFalse => op_push_false,
LoadLocal => op_load_local,
LoadOuter => op_load_outer,
StoreLocal => op_store_local,
AllocLocals => op_alloc_locals,
MakeThunk => op_make_thunk,
MakeClosure => op_make_closure,
MakePatternClosure => op_make_pattern_closure,
Call => op_call,
DispatchPrimOp => op_dispatch_primop,
Return => op_return,
MakeAttrs => op_make_attrs,
MakeEmptyAttrs => op_make_empty_attrs,
SelectStatic => op_select_static,
SelectDynamic => op_select_dynamic,
HasAttrPathStatic => op_has_attr_path_static,
HasAttrPathDynamic => op_has_attr_path_dynamic,
HasAttrStatic => op_has_attr_static,
HasAttrDynamic => op_has_attr_dynamic,
HasAttrResolve => op_has_attr_resolve,
JumpIfSelectSucceeded => op_jump_if_select_succeeded,
JumpIfSelectFailed => op_jump_if_select_failed,
MakeList => op_make_list,
MakeEmptyList => op_make_empty_list,
OpAdd => op_add,
OpSub => op_sub,
OpMul => op_mul,
OpDiv => op_div,
OpEq => op_eq,
OpNeq => op_neq,
OpLt => op_lt,
OpGt => op_gt,
OpLeq => op_leq,
OpGeq => op_geq,
OpConcat => op_concat,
OpUpdate => op_update,
OpNeg => op_neg,
OpNot => op_not,
JumpIfFalse => op_jump_if_false,
JumpIfTrue => op_jump_if_true,
Jump => op_jump,
CoerceToString => op_coerce_to_string,
ConcatStrings => op_concat_strings,
ResolvePath => op_resolve_path,
Assert => op_assert,
LookupWith => op_lookup_with,
LoadBuiltins => op_load_builtins,
LoadBuiltin => op_load_builtin,
LoadReplBinding => op_load_repl_binding,
LoadScopedBinding => op_load_scoped_binding,
Illegal => op_illegal,
}
pub(crate) fn run_tailcall<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
pc: u32,
) -> TailResult {
let table = &DispatchTable::<'gc, C>::NEW;
let op = bc[pc as usize] as usize;
table.0[op](vm, mc, ctx, bc, table, pc, Vm::DEFAULT_FUEL_AMOUNT)
}
+362
View File
@@ -0,0 +1,362 @@
use std::cmp::Ordering;
use fix_abstract_vm::*;
use gc_arena::{Gc, Mutation, RefLock};
use crate::{BytecodeReader, NixNum, Step, VmError, VmRuntimeCtx};
impl<'gc> crate::Vm<'gc> {
#[inline(always)]
pub(crate) fn op_add(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
// if the LHS is a path, the result is a path obtained by
// canonicalizing the concatenated string. RHS may be a path or a
// string. (A `string + path` keeps the string-typed result, handled
// by the next branch.)
if lhs.is::<Path>() {
let (Some(ls), Some(rs)) = (ctx.get_string_or_path(lhs), ctx.get_string_or_path(rhs))
else {
return self.finish_err(fix_error::Error::eval_error(format!(
"cannot append {} to a path",
rhs.ty()
)));
};
let combined = format!("{ls}{rs}");
let canon = canon_path_str(&combined);
let sid = ctx.intern_string(canon);
self.push(Value::new_inline(fix_abstract_vm::Path(sid)));
return Step::Continue(());
}
if let (Some(ls), Some(rs)) = (ctx.get_string(lhs), ctx.get_string_or_path(rhs)) {
let ns = Gc::new(mc, crate::NixString::new(format!("{ls}{rs}")));
self.push(Value::new_gc(ns));
return Step::Continue(());
}
let res = numeric_binop(lhs, rhs, mc, i64::wrapping_add, |a, b| a + b);
match res {
Ok(val) => {
self.push(val);
Step::Continue(())
}
Err(e) => self.finish_vm_err(e),
}
}
#[inline(always)]
pub(crate) fn op_sub(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step {
self.op_arith(reader, mc, i64::wrapping_sub, |a, b| a - b)
}
#[inline(always)]
pub(crate) fn op_mul(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step {
self.op_arith(reader, mc, i64::wrapping_mul, |a, b| a * b)
}
#[inline(always)]
fn op_arith(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
int_op: fn(i64, i64) -> i64,
float_op: fn(f64, f64) -> f64,
) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let res = numeric_binop(lhs, rhs, mc, int_op, float_op);
match res {
Ok(val) => {
self.push(val);
Step::Continue(())
}
Err(e) => self.finish_vm_err(e),
}
}
#[inline(always)]
pub(crate) fn op_div(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
match (get_num(lhs), get_num(rhs)) {
(_, Some(NixNum::Int(0))) | (_, Some(NixNum::Float(0.))) => {
return self.finish_vm_err(VmError::Uncatchable(fix_error::Error::eval_error(
"division by zero",
)));
}
_ => {}
}
let res = numeric_binop(lhs, rhs, mc, |a, b| a / b, |a, b| a / b);
match res {
Ok(val) => {
self.push(val);
Step::Continue(())
}
Err(e) => self.finish_vm_err(e),
}
}
#[inline(always)]
pub(crate) fn op_eq(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let eq = match self.values_equal(ctx, lhs, rhs) {
Ok(eq) => eq,
Err(e) => return self.finish_vm_err(e),
};
self.push(Value::new_inline(eq));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_neq(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let eq = match self.values_equal(ctx, lhs, rhs) {
Ok(eq) => eq,
Err(e) => return self.finish_vm_err(e),
};
self.push(Value::new_inline(!eq));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_lt(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
self.compare_values(ctx, reader, mc, Ordering::is_lt)
}
#[inline(always)]
pub(crate) fn op_gt(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
self.compare_values(ctx, reader, mc, Ordering::is_gt)
}
#[inline(always)]
pub(crate) fn op_leq(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
self.compare_values(ctx, reader, mc, Ordering::is_le)
}
#[inline(always)]
pub(crate) fn op_geq(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
self.compare_values(ctx, reader, mc, Ordering::is_ge)
}
fn compare_values(
&mut self,
ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
pred: fn(Ordering) -> bool,
) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
match self.compare_values_inner(ctx, pred, lhs, rhs) {
Ok(()) => Step::Continue(()),
Err(e) => self.finish_vm_err(e),
}
}
#[inline(always)]
pub(crate) fn op_concat(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (l, r) = self.force_and_retry::<(Gc<List>, Gc<List>)>(reader, mc)?;
let mut items = smallvec::SmallVec::new();
items.extend_from_slice(&l.inner.borrow());
items.extend_from_slice(&r.inner.borrow());
self.push(Value::new_gc(Gc::new(
mc,
crate::List {
inner: RefLock::new(items),
},
)));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_update(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (l, r) = self.force_and_retry::<(Gc<AttrSet>, Gc<AttrSet>)>(reader, mc)?;
self.push(Value::new_gc(l.merge(&r, mc)));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_neg(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step {
let rhs = self.force_and_retry::<NixNum>(reader, mc)?;
match rhs {
NixNum::Int(int) => self.push(Value::make_int(-int, mc)),
NixNum::Float(float) => self.push(Value::new_float(-float)),
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_not(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step {
let rhs = self.force_and_retry::<bool>(reader, mc)?;
self.push(Value::new_inline(!rhs));
Step::Continue(())
}
pub(crate) fn values_equal(
&mut self,
ctx: &impl VmRuntimeCtx,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
) -> crate::VmResult<bool> {
if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) {
return Ok(match (a, b) {
(NixNum::Int(a), NixNum::Int(b)) => a == b,
(NixNum::Float(a), NixNum::Float(b)) => a == b,
(NixNum::Int(a), NixNum::Float(b)) => a as f64 == b,
(NixNum::Float(a), NixNum::Int(b)) => a == b as f64,
});
}
if let (Some(a), Some(b)) = (lhs.as_inline::<bool>(), rhs.as_inline::<bool>()) {
return Ok(a == b);
}
if lhs.is::<crate::Null>() && rhs.is::<crate::Null>() {
return Ok(true);
}
// Paths only equal paths (not strings, even if their text matches).
if let (Some(a), Some(b)) = (lhs.as_inline::<Path>(), rhs.as_inline::<Path>()) {
return Ok(a.0 == b.0);
}
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
return Ok(a == b);
}
if let (Some(a), Some(b)) = (lhs.as_gc::<crate::List>(), rhs.as_gc::<crate::List>()) {
if a.inner.borrow().len() != b.inner.borrow().len() {
return Ok(false);
}
for (x, y) in a.inner.borrow().iter().zip(b.inner.borrow().iter()) {
let lx = x.restrict().expect("forced");
let ly = y.restrict().expect("forced");
if !self.values_equal(ctx, lx, ly)? {
return Ok(false);
}
}
return Ok(true);
}
if let (Some(a), Some(b)) = (lhs.as_gc::<crate::AttrSet>(), rhs.as_gc::<crate::AttrSet>()) {
let a = &a.entries;
let b = &b.entries;
if a.len() != b.len() {
return Ok(false);
}
for ((k1, v1), (k2, v2)) in a.iter().zip(b.iter()) {
if k1 != k2 {
return Ok(false);
}
let lv1 = v1.restrict().expect("forced");
let lv2 = v2.restrict().expect("forced");
if !self.values_equal(ctx, lv1, lv2)? {
return Ok(false);
}
}
return Ok(true);
}
Ok(false)
}
fn compare_values_inner(
&mut self,
ctx: &impl VmRuntimeCtx,
pred: fn(Ordering) -> bool,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
) -> crate::VmResult<()> {
if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) {
let ord = match (a, b) {
(NixNum::Int(a), NixNum::Int(b)) => a.cmp(&b),
(NixNum::Float(a), NixNum::Float(b)) => a.partial_cmp(&b).unwrap_or(Ordering::Less),
(NixNum::Int(a), NixNum::Float(b)) => {
(a as f64).partial_cmp(&b).unwrap_or(Ordering::Less)
}
(NixNum::Float(a), NixNum::Int(b)) => {
a.partial_cmp(&(b as f64)).unwrap_or(Ordering::Less)
}
};
self.push(Value::new_inline(pred(ord)));
return Ok(());
}
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
self.push(Value::new_inline(pred(a.cmp(b))));
return Ok(());
}
if let (Some(a), Some(b)) = (lhs.as_inline::<Path>(), rhs.as_inline::<Path>()) {
let a = ctx.resolve_string(a.0);
let b = ctx.resolve_string(b.0);
self.push(Value::new_inline(pred(a.cmp(b))));
return Ok(());
}
// TODO: compare other types
Err(crate::vm_err("cannot compare these types"))
}
}
pub(crate) fn get_num(val: StrictValue<'_>) -> Option<NixNum> {
if let Some(i) = val.as_inline::<i32>() {
Some(NixNum::Int(i as i64))
} else if let Some(gc_i) = val.as_gc::<i64>() {
Some(NixNum::Int(*gc_i))
} else {
val.as_float().map(NixNum::Float)
}
}
#[inline]
fn numeric_binop<'gc>(
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
mc: &Mutation<'gc>,
int_op: fn(i64, i64) -> i64,
float_op: fn(f64, f64) -> f64,
) -> crate::VmResult<Value<'gc>> {
match (get_num(lhs), get_num(rhs)) {
(Some(NixNum::Int(a)), Some(NixNum::Int(b))) => Ok(Value::make_int(int_op(a, b), mc)),
(Some(NixNum::Float(a)), Some(NixNum::Float(b))) => Ok(Value::new_float(float_op(a, b))),
(Some(NixNum::Int(a)), Some(NixNum::Float(b))) => {
Ok(Value::new_float(float_op(a as f64, b)))
}
(Some(NixNum::Float(a)), Some(NixNum::Int(b))) => {
Ok(Value::new_float(float_op(a, b as f64)))
}
_ => Err(crate::vm_err(format!(
"cannot perform arithmetic on non-numbers: {:?}",
(lhs.ty(), rhs.ty())
))),
}
}
+183
View File
@@ -0,0 +1,183 @@
use fix_abstract_vm::{resolve_operand, *};
use fix_builtins::PrimOpPhase;
use fix_error::Error;
use gc_arena::{Gc, Mutation, RefLock};
use crate::{
BytecodeReader, CallFrame, Closure, Env, ForceMode, Step, ThunkState, VmRuntimeCtx,
VmRuntimeCtxExt,
};
impl<'gc> crate::Vm<'gc> {
#[inline(always)]
pub(crate) fn call(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
arg: Value<'gc>,
resume_pc: usize,
) -> Step {
let func = self.force_and_retry::<StrictValue>(reader, mc)?;
if self.call_depth > 10000 {
return self.finish_err(Error::eval_error("stack overflow; max-call-depth exceeded"));
}
self.call_depth += 1;
if let Some(closure) = func.as_gc::<Closure>() {
if closure.pattern.is_some() {
// FIXME: better DX...
self.push(func.relax());
self.push(arg);
self.call_stack.push(CallFrame {
pc: resume_pc,
thunk: None,
env: self.env,
});
reader.set_pc(PrimOpPhase::CallPattern.ip() as usize);
return Step::Continue(());
}
let ip = closure.ip;
let n_locals = closure.n_locals;
let env = closure.env;
let new_env = Gc::new(mc, RefLock::new(Env::with_arg(arg, n_locals, env)));
self.call_stack.push(CallFrame {
pc: resume_pc,
thunk: None,
env: self.env,
});
reader.set_pc(ip as usize);
self.env = new_env;
} else if let Some(primop) = func.as_inline::<PrimOp>() {
if primop.arity == 1 {
self.push(arg);
self.call_stack.push(CallFrame {
pc: resume_pc,
thunk: None,
env: self.env,
});
reader.set_pc(primop.dispatch_ip as usize)
} else {
let app = PrimOpApp {
primop,
arity: primop.arity - 1,
args: [arg, Value::default(), Value::default()],
};
self.push(Value::new_gc(Gc::new(mc, app)));
}
} else if let Some(app) = func.as_gc::<PrimOpApp>() {
if app.arity == 1 {
for i in 0..app.primop.arity - 1 {
self.push(app.args[i as usize]);
}
self.push(arg);
self.call_stack.push(CallFrame {
pc: resume_pc,
thunk: None,
env: self.env,
});
reader.set_pc(app.primop.dispatch_ip as usize)
} else {
let position = (app.primop.arity - app.arity) as usize;
let mut new_app = PrimOpApp {
arity: app.arity - 1,
..*app
};
new_app.args[position] = arg;
self.push(Value::new_gc(Gc::new(mc, new_app)))
}
} else if let Some(attrs) = func.as_gc::<AttrSet>()
&& let Some(functor) = attrs.lookup(self.functor_sym)
{
// f arg => (f.__functor f) arg
//
// Stage the work for `CallFunctor1` so retries during force are
// safe: the stack invariant `[..., orig_arg, self, functor]`
// holds every time control re-enters phase 1.
self.call_depth -= 1;
self.call_stack.push(CallFrame {
pc: resume_pc,
thunk: None,
env: self.env,
});
self.push(arg);
self.push(func.relax());
self.push(functor);
reader.set_pc(PrimOpPhase::CallFunctor1.ip() as usize);
return Step::Continue(());
} else {
return self.finish_err(Error::eval_error(format!(
"attempt to call something which is not a function but {}",
func.ty()
)));
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_call(
&mut self,
ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let arg = resolve_operand(&reader.read_operand_data(ctx), mc, self);
let pc = reader.pc();
self.call(reader, mc, arg, pc)
}
#[inline(always)]
pub(crate) fn op_return(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = self.force_and_retry::<StrictValue>(reader, mc)?;
let Some(CallFrame {
pc: ret_pc,
thunk,
env,
}) = self.call_stack.pop()
else {
match self.force_mode {
ForceMode::AsIs => return self.finish_ok(ctx.convert_value(val.relax())),
ForceMode::Shallow => {
self.push(val.relax());
reader.set_pc(PrimOpPhase::ForceResultShallow.ip() as usize);
return Step::Continue(());
}
ForceMode::Deep => {
self.push(val.relax());
self.push(val.relax());
self.call_stack.push(CallFrame {
pc: PrimOpPhase::ForceResultDeepFinish.ip() as usize,
thunk: None,
env: self.env,
});
self.call_depth += 1;
reader.set_pc(PrimOpPhase::DeepSeq.ip() as usize);
return Step::Continue(());
}
}
};
reader.set_pc(ret_pc);
if let Some(outer_thunk) = thunk {
*outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val);
} else {
self.call_depth -= 1;
self.push(val.relax())
}
self.env = env;
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_dispatch_primop(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
fix_primops::dispatch_primop(self, ctx, reader, mc)
}
}
+94
View File
@@ -0,0 +1,94 @@
use gc_arena::{Gc, Mutation, RefLock};
use crate::{BytecodeReader, Step, ThunkState, Value};
impl<'gc> crate::Vm<'gc> {
#[inline(always)]
pub(crate) fn op_make_thunk(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let entry_point = reader.read_u32();
let thunk = Gc::new(
mc,
RefLock::new(ThunkState::Pending {
ip: entry_point as usize,
env: self.env,
}),
);
self.push(Value::new_gc(thunk));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_closure(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let entry_point = reader.read_u32();
let n_locals = reader.read_u32();
let closure = Gc::new(
mc,
crate::Closure {
ip: entry_point,
n_locals,
env: self.env,
pattern: None,
},
);
self.push(Value::new_gc(closure));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_pattern_closure(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let entry_point = reader.read_u32();
let n_locals = reader.read_u32();
let req_count = reader.read_u16() as usize;
let opt_count = reader.read_u16() as usize;
let has_ellipsis = reader.read_u8() != 0;
let mut required = smallvec::SmallVec::new();
for _ in 0..req_count {
required.push(reader.read_string_id());
}
let mut optional = smallvec::SmallVec::new();
for _ in 0..opt_count {
optional.push(reader.read_string_id());
}
let total = req_count + opt_count;
let mut param_spans = Vec::with_capacity(total);
for _ in 0..total {
let name = reader.read_string_id();
let span_id = reader.read_u32();
param_spans.push((name, span_id));
}
let pattern = Gc::new(
mc,
crate::PatternInfo {
required,
optional,
ellipsis: has_ellipsis,
param_spans: param_spans.into_boxed_slice(),
},
);
let closure = Gc::new(
mc,
crate::Closure {
ip: entry_point,
n_locals,
env: self.env,
pattern: Some(pattern),
},
);
self.push(Value::new_gc(closure));
Step::Continue(())
}
}
+334
View File
@@ -0,0 +1,334 @@
use fix_abstract_vm::{NixType, resolve_operand};
use fix_common::StringId;
use fix_error::Error;
use gc_arena::{Gc, RefLock};
use smallvec::SmallVec;
use crate::{
AttrSet, BytecodeReader, List, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt,
};
impl<'gc> crate::Vm<'gc> {
#[inline(always)]
pub(crate) fn op_make_attrs(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let static_count = reader.read_u32() as usize;
let dynamic_count = reader.read_u32() as usize;
for i in 0..dynamic_count {
let depth = dynamic_count - 1 - i;
self.force_slot_to_pc(depth, reader, mc, reader.inst_start_pc())?;
}
let mut dyn_keys: SmallVec<[_; 2]> = SmallVec::with_capacity(dynamic_count);
for i in 0..dynamic_count {
let depth = dynamic_count - 1 - i;
let key_val = self.peek_forced(depth);
let key_sid = match ctx.get_string_id(key_val) {
Ok(id) => Some(id),
Err(NixType::Null) => None,
Err(got) => return self.finish_type_err(NixType::String, got),
};
dyn_keys.push(key_sid);
}
self.stack.truncate(self.stack.len() - dynamic_count);
let mut kv: SmallVec<[(crate::StringId, Value); 4]> =
SmallVec::with_capacity(static_count + dynamic_count);
for _ in 0..static_count {
let key = reader.read_string_id();
let val = resolve_operand(&reader.read_operand_data(ctx), mc, self);
let _span_id = reader.read_u32();
kv.push((key, val));
}
for key in dyn_keys {
let val = resolve_operand(&reader.read_operand_data(ctx), mc, self);
let _span_id = reader.read_u32();
if let Some(key) = key {
kv.push((key, val))
}
}
kv.sort_by_key(|(k, _)| *k);
let attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(kv));
self.push(Value::new_gc(attrs));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_empty_attrs(&mut self) -> Step {
self.push(self.empty_attrs);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_select_static(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let _span_id = reader.read_u32();
let key = reader.read_string_id();
let attrset = self.force_and_retry::<Gc<AttrSet>>(reader, mc)?;
match attrset.lookup(key) {
Some(v) => {
self.push(v);
}
None => return self.select_skip(key, ctx, reader),
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_select_dynamic(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let _span_id = reader.read_u32();
let (attrset, key_val) = self.force_and_retry::<(Gc<AttrSet>, StrictValue)>(reader, mc)?;
let key_sid = match ctx.get_string_id(key_val) {
Ok(id) => id,
Err(got) => return self.finish_type_err(NixType::String, got),
};
match attrset.lookup(key_sid) {
Some(v) => {
self.push(v);
}
None => return self.select_skip(key_sid, ctx, reader),
}
Step::Continue(())
}
/// Skip the rest of a **Select** attrpath after a missing attribute.
/// Only recognises Select opcodes and jumps; encountering any other
/// opcode means we've reached the end of the select sequence and
/// should report the missing-attribute error.
fn select_skip(
&mut self,
key: StringId,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
) -> Step {
use fix_codegen::Op::*;
loop {
match reader.read_op() {
SelectStatic => {
reader.set_pc(reader.pc() + 4 + 4);
}
SelectDynamic => {
reader.set_pc(reader.pc() + 4);
}
JumpIfSelectSucceeded => {
reader.set_pc(reader.pc() + 4);
break Step::Continue(());
}
JumpIfSelectFailed => {
let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
}
_ => {
let name = ctx.resolve_string(key);
return self
.finish_err(Error::eval_error(format!("attribute '{name}' missing")));
}
}
}
}
/// Skip the rest of a **HasAttr** attrpath after an intermediate
/// lookup failed. Only recognises HasAttr opcodes and jumps.
fn has_attr_skip(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
use fix_codegen::Op::*;
loop {
match reader.read_op() {
HasAttrPathStatic => {
reader.set_pc(reader.pc() + 4 + 4);
}
HasAttrPathDynamic => {
reader.set_pc(reader.pc() + 4);
}
HasAttrStatic => {
reader.set_pc(reader.pc() + 4);
break Step::Continue(());
}
JumpIfSelectFailed => {
let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
}
HasAttrDynamic => {
break Step::Continue(());
}
HasAttrResolve => {
reader.set_pc(reader.pc() - 1);
break Step::Continue(());
}
other => {
unreachable!("unexpected opcode {:?} in has_attr_skip", other as u8)
}
}
}
}
#[inline(always)]
pub(crate) fn op_has_attr_path_static(
&mut self,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let _span_id = reader.read_u32();
let key = reader.read_string_id();
let current = self.force_and_retry::<StrictValue>(reader, mc)?;
match current
.as_gc::<AttrSet>()
.and_then(|attrs| attrs.lookup(key))
{
Some(v) => {
self.push(v);
}
None => return self.has_attr_skip(reader),
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_has_attr_path_dynamic(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let _span_id = reader.read_u32();
let (current, key_val) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let key_sid = match ctx.get_string_id(key_val) {
Ok(id) => id,
Err(got) => return self.finish_type_err(NixType::String, got),
};
match current
.as_gc::<AttrSet>()
.and_then(|attrs| attrs.lookup(key_sid))
{
Some(v) => {
self.push(v);
}
None => return self.has_attr_skip(reader),
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_jump_if_select_failed(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
// No-op
let _offset = reader.read_i32();
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_jump_if_select_succeeded(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_has_attr_static(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let key = reader.read_string_id();
let current = self.force_and_retry::<StrictValue>(reader, mc)?;
self.push(Value::new_inline(
current
.as_gc::<AttrSet>()
.and_then(|attrs| attrs.lookup(key))
.is_some(),
));
// Skip HasAttrResolve
reader.set_pc(reader.pc() + 1);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_has_attr_dynamic(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let (current, dyn_key) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let key_sid = match ctx.get_string_id(dyn_key) {
Ok(id) => id,
Err(got) => return self.finish_type_err(NixType::String, got),
};
self.push(Value::new_inline(
current
.as_gc::<AttrSet>()
.and_then(|attrs| attrs.lookup(key_sid))
.is_some(),
));
// Skip HasAttrResolve
reader.set_pc(reader.pc() + 1);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_has_attr_resolve(&mut self) -> Step {
// If we reach here, has_attr check has failed, push false (AttrSet is already popped)
self.push(Value::new_inline(false));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_list(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let count = reader.read_u32() as usize;
let mut items: SmallVec<[Value; 4]> = SmallVec::with_capacity(count);
for _ in 0..count {
items.push(resolve_operand(&reader.read_operand_data(ctx), mc, self));
}
let list = Gc::new(
mc,
List {
inner: RefLock::new(items),
},
);
self.push(Value::new_gc(list));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_empty_list(&mut self) -> Step {
self.push(self.empty_list);
Step::Continue(())
}
}
+60
View File
@@ -0,0 +1,60 @@
use fix_abstract_vm::*;
use fix_error::Error;
use gc_arena::Mutation;
use crate::{BytecodeReader, Step, VmRuntimeCtx};
impl<'gc> crate::Vm<'gc> {
#[inline(always)]
pub(crate) fn op_jump_if_false(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let offset = reader.read_i32();
let cond = self.force_and_retry::<StrictValue>(reader, mc)?;
if cond.as_inline::<bool>() == Some(false) {
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_jump_if_true(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let offset = reader.read_i32();
let cond = self.force_and_retry::<StrictValue>(reader, mc)?;
if cond.as_inline::<bool>() == Some(true) {
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_jump(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_assert(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let raw_id = reader.read_string_id();
let raw = ctx.resolve_string(raw_id);
let _span_id = reader.read_u32();
let assertion = self.force_and_retry::<bool>(reader, mc)?;
if !assertion {
// FIXME: use catchable error
return self.finish_err(Error::eval_error(format!("assertion '{raw}' failed")));
}
Step::Continue(())
}
}
+55
View File
@@ -0,0 +1,55 @@
use gc_arena::{Gc, Mutation};
use crate::{BytecodeReader, Step, Value};
impl<'gc> crate::Vm<'gc> {
#[inline(always)]
pub(crate) fn op_push_smi(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let val = reader.read_i32();
self.push(Value::new_inline(val));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_bigint(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = reader.read_i64();
self.push(Value::new_gc(Gc::new(mc, val)));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_float(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let val = reader.read_f64();
self.push(Value::new_float(val));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_string(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let sid = reader.read_string_id();
self.push(Value::new_inline(sid));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_null(&mut self) -> Step {
self.push(Value::new_inline(crate::Null));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_true(&mut self) -> Step {
self.push(Value::new_inline(true));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_false(&mut self) -> Step {
self.push(Value::new_inline(false));
Step::Continue(())
}
}
+165
View File
@@ -0,0 +1,165 @@
use std::path::PathBuf;
use fix_abstract_vm::{AttrSet, NixString, Path, StrictValue, canon_path_str};
use fix_builtins::BuiltinId;
use fix_common::StringId;
use fix_error::Error;
use num_enum::TryFromPrimitive;
use crate::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx, VmRuntimeCtxExt};
impl<'gc> crate::Vm<'gc> {
#[inline(always)]
pub(crate) fn op_load_builtins(&mut self) -> Step {
self.push(self.builtins);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_load_builtin(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let Ok(id) = BuiltinId::try_from_primitive(reader.read_u8())
.map_err(|err| panic!("unknown builtin id: {}", err.number));
self.push(Value::new_inline(PrimOp {
id,
arity: fix_builtins::BUILTINS[id as usize].1,
dispatch_ip: id.entry_phase().ip(),
}));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_load_repl_binding(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let _name = reader.read_string_id();
todo!("LoadReplBinding");
}
#[inline(always)]
pub(crate) fn op_load_scoped_binding(
&mut self,
ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
_mc: &gc_arena::Mutation<'gc>,
) -> Step {
let slot_id = reader.read_u32();
let name = reader.read_string_id();
let scope = match self.scope_slots.get(slot_id as usize).copied() {
Some(s) => s,
None => {
return self.finish_err(Error::eval_error(format!(
"internal: invalid scope slot {slot_id}"
)));
}
};
let Some(attrs) = scope.as_gc::<AttrSet>() else {
return self.finish_err(Error::eval_error("internal: scope slot is not an attrset"));
};
match attrs.lookup(name) {
Some(val) => {
self.push(val);
Step::Continue(())
}
None => self.finish_err(Error::eval_error(format!(
"scoped binding '{}' not found",
ctx.resolve_string(name)
))),
}
}
#[inline(always)]
pub(crate) fn op_coerce_to_string(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let val = self.force_and_retry::<StrictValue>(reader, mc)?;
if val.is::<StringId>() || val.is::<NixString>() {
self.push(val.relax());
} else if let Some(p) = val.as_inline::<Path>() {
// Coercing a path to a string yields the canonical path text.
// FIXME: copy to store
self.push(Value::new_inline(p.0));
} else {
todo!("coerce other types to string: {:?}", val.ty());
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_concat_strings(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
_mc: &gc_arena::Mutation<'gc>,
) -> Step {
let count = reader.read_u16() as usize;
let _force_string = reader.read_u8() != 0;
let mut total_len = 0;
for i in 0..count {
let val = self.peek_forced(count - 1 - i);
let s = ctx.get_string(val).expect("coerced");
total_len += s.len();
}
let mut result = String::with_capacity(total_len);
for i in 0..count {
let val = self.peek_forced(count - 1 - i);
let s = ctx.get_string(val).expect("coerced");
result.push_str(s);
}
self.stack.truncate(self.stack.len() - count);
let sid = ctx.intern_string(result);
self.push(Value::new_inline(sid));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_resolve_path(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let path_val = self.force_and_retry::<StrictValue>(reader, mc)?;
let dir_id = reader.read_string_id();
// Already a path: keep as-is. ResolvePath is idempotent on paths.
if let Some(p) = path_val.as_inline::<Path>() {
self.push(Value::new_inline(p));
return Step::Continue(());
}
let path = match ctx.get_string(path_val) {
Some(s) => s.to_owned(),
None => {
return self.finish_err(Error::eval_error(format!(
"expected a string for path, got {}",
path_val.ty()
)));
}
};
let resolved = match resolve_path_str(ctx.resolve_string(dir_id), &path) {
Ok(s) => s,
Err(e) => return self.finish_err(e),
};
let sid = ctx.intern_string(resolved);
self.push(Value::new_inline(Path(sid)));
Step::Continue(())
}
}
fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>> {
let raw = if path.starts_with('/') {
return Ok(canon_path_str(path));
} else if let Some(rest) = path.strip_prefix("~/") {
let mut dir =
std::env::home_dir().ok_or_else(|| Error::eval_error("home dir not defined"))?;
dir.push(rest);
dir
} else {
let mut dir = PathBuf::from(current_dir);
dir.push(path);
dir
};
Ok(canon_path_str(&raw))
}
+9
View File
@@ -0,0 +1,9 @@
pub(crate) mod arithmetic;
pub(crate) mod calls;
pub(crate) mod closures;
pub(crate) mod collections;
pub(crate) mod control;
pub(crate) mod literals;
pub(crate) mod misc;
pub(crate) mod variables;
pub(crate) mod with_scope;
+50
View File
@@ -0,0 +1,50 @@
use crate::{BytecodeReader, Mutation, Step, Value};
impl<'gc> crate::Vm<'gc> {
#[inline(always)]
pub(crate) fn op_load_local(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let idx = reader.read_u32() as usize;
self.push(self.env.borrow().locals[idx]);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_load_outer(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let layer = reader.read_u8();
let idx = reader.read_u32() as usize;
let mut cur = self.env;
for _ in 0..layer {
let prev = cur.borrow().prev.expect("LoadOuter: env chain too short");
cur = prev;
}
let val = cur.borrow().locals[idx];
self.push(val);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_store_local(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let idx = reader.read_u32() as usize;
let val = self.pop();
self.env.borrow_mut(mc).locals[idx] = val;
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_alloc_locals(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let count = reader.read_u32() as usize;
self.env
.borrow_mut(mc)
.locals
.extend(std::iter::repeat_n(Value::default(), count));
Step::Continue(())
}
}
+78
View File
@@ -0,0 +1,78 @@
use fix_abstract_vm::{resolve_operand, *};
use fix_common::Symbol;
use fix_error::Error;
use smallvec::SmallVec;
use crate::{Break, BytecodeReader, CallFrame, Step, VmRuntimeCtx};
impl<'gc> crate::Vm<'gc> {
#[inline(always)]
pub(crate) fn op_lookup_with(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
#[allow(clippy::unwrap_used)]
let counter = self.peek_forced(0).as_inline::<i32>().unwrap();
let name = reader.read_string_id();
let n = reader.read_u8();
let mut namespaces = SmallVec::<[_; 2]>::new();
for _ in 0..n {
namespaces.push(resolve_operand(&reader.read_operand_data(ctx), mc, self));
}
let resume_pc = reader.inst_start_pc();
let namespace = match namespaces[counter as usize].restrict() {
Ok(val) => val,
Err(thunk) => {
let mut state = thunk.borrow_mut(mc);
match *state {
ThunkState::Pending { ip, env } => {
*state = ThunkState::Blackhole;
self.call_stack.push(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: self.env,
});
self.env = env;
reader.set_pc(ip);
return Step::Break(Break::Force);
}
ThunkState::Evaluated(v) => v,
ThunkState::Apply { func, arg } => {
self.call_stack.push(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: self.env,
});
self.push(func);
return self.call(reader, mc, arg, resume_pc);
}
ThunkState::Blackhole => {
return self
.finish_err(Error::eval_error("infinite recursion encountered"));
}
}
}
};
if let Some(val) = namespace
.as_gc::<AttrSet>()
.and_then(|attrs| attrs.lookup(name))
{
self.replace(0, val);
} else if counter + 1 == n as i32 {
return self.finish_err(Error::eval_error(format!(
"undefined variable '{}'",
Symbol::from(ctx.resolve_string(name))
)));
} else {
self.replace(0, Value::new_inline(counter + 1));
reader.set_pc(resume_pc);
}
Step::Continue(())
}
}
+682
View File
@@ -0,0 +1,682 @@
#![warn(clippy::unwrap_used)]
#![cfg_attr(feature = "tailcall", expect(incomplete_features))]
#![cfg_attr(
feature = "tailcall",
feature(explicit_tail_calls, rust_preserve_none_cc)
)]
use std::path::PathBuf;
use fix_builtins::{BUILTINS, BuiltinId};
use fix_codegen::InstructionPtr;
use fix_common::StringId;
use fix_error::{Error, Result, Source};
use gc_arena::arena::CollectionPhase;
use gc_arena::{Arena, Collect, Gc, Mutation, RefLock, Rootable};
use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use smallvec::SmallVec;
#[cfg(feature = "tailcall")]
mod dispatch_tailcall;
pub use fix_abstract_vm::*;
mod instructions;
type VmResult<T> = std::result::Result<T, VmError>;
#[derive(Collect)]
#[collect(no_drop)]
pub struct Vm<'gc> {
stack: Vec<Value<'gc>>,
call_stack: Vec<CallFrame<'gc>>,
call_depth: usize,
#[allow(dead_code)]
#[collect(require_static)]
error_context: Vec<ErrorFrame>,
env: GcEnv<'gc>,
import_cache: HashMap<PathBuf, Value<'gc>>,
scope_slots: Vec<Value<'gc>>,
builtins: Value<'gc>,
empty_list: Value<'gc>,
empty_attrs: Value<'gc>,
force_mode: ForceMode,
#[collect(require_static)]
result: Option<Result<fix_common::Value>>,
#[collect(require_static)]
pending_load: Option<PendingLoad>,
functor_sym: StringId,
}
fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Value<'gc> {
let mut entries = SmallVec::with_capacity(BUILTINS.len());
for (idx, &(name, arity)) in BUILTINS.iter().enumerate() {
let id = BuiltinId::try_from_primitive(idx as u8).expect("infallible");
let name = name.strip_prefix("__").unwrap_or(name);
let name = ctx.intern_string(name);
let dispatch_ip = id.entry_phase().ip();
entries.push((
name,
Value::new_inline(PrimOp {
id,
arity,
dispatch_ip,
}),
));
}
let consts = [
(
"__currentSystem",
Value::new_inline(ctx.intern_string("x86_64-linux")),
),
("__langVersion", Value::new_inline(6i32)),
(
"__nixVersion",
Value::new_inline(ctx.intern_string("2.24.0")),
),
(
"__storeDir",
Value::new_inline(ctx.intern_string("/nix/store")),
),
("__nixPath", Value::new_gc(Gc::new(mc, List::default()))),
("null", Value::new_inline(Null)),
("true", Value::new_inline(true)),
("false", Value::new_inline(false)),
];
for (name, val) in consts {
let name = name.strip_prefix("__").unwrap_or(name);
let name = ctx.intern_string(name);
entries.push((name, val));
}
let self_ref_thunk = Gc::new(mc, RefLock::new(ThunkState::Blackhole));
let sym = ctx.intern_string("builtins");
entries.push((sym, Value::new_gc(self_ref_thunk)));
entries.sort_by_key(|(k, _)| *k);
let builtins_set = Gc::new(mc, AttrSet::from_sorted_unchecked(entries));
let builtins_value = Value::new_gc(builtins_set);
*self_ref_thunk.borrow_mut(mc) =
ThunkState::Evaluated(builtins_value.restrict().expect("builtins is not a thunk"));
builtins_value
}
impl<'gc> Vm<'gc> {
fn new(force_mode: ForceMode, mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Self {
let builtins = init_builtins(mc, ctx);
Vm {
stack: Vec::with_capacity(8192),
call_stack: Vec::with_capacity(1024),
call_depth: 0,
error_context: Vec::with_capacity(1024),
env: Gc::new(mc, RefLock::new(Env::empty())),
import_cache: HashMap::new(),
scope_slots: Vec::new(),
builtins,
empty_list: Value::new_gc(Gc::new(mc, List::default())),
empty_attrs: Value::new_gc(Gc::new(mc, AttrSet::default())),
force_mode,
result: None,
pending_load: None,
functor_sym: ctx.intern_string("__functor"),
}
}
#[inline(always)]
fn finish_ok(&mut self, val: fix_common::Value) -> Step {
self.result = Some(Ok(val));
Step::Break(Break::Done)
}
#[inline(always)]
fn finish_err(&mut self, err: Box<Error>) -> Step {
self.result = Some(Err(err));
Step::Break(Break::Done)
}
#[inline(always)]
fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step {
self.result = Some(Err(Error::eval_error(format!(
"expected {expected}, got {got}"
))));
Step::Break(Break::Done)
}
#[inline(always)]
fn finish_vm_err(&mut self, err: VmError) -> Step {
self.finish_err(err.into_error())
}
#[inline(always)]
fn push(&mut self, val: Value<'gc>) {
self.stack.push(val);
}
#[inline(always)]
#[must_use]
fn pop(&mut self) -> Value<'gc> {
self.stack.pop().expect("stack underflow")
}
#[inline(always)]
#[must_use]
fn peek(&self, depth: usize) -> Value<'gc> {
*self
.stack
.get(self.stack.len() - depth - 1)
.expect("stack underflow")
}
#[inline(always)]
#[must_use]
fn peek_forced(&self, depth: usize) -> StrictValue<'gc> {
self.stack
.get(self.stack.len() - depth - 1)
.expect("stack underflow")
.restrict()
.expect("forced")
}
#[inline(always)]
fn replace(&mut self, depth: usize, val: Value<'gc>) {
let len = self.stack.len();
*self
.stack
.get_mut(len - depth - 1)
.expect("stack underflow") = val;
}
#[inline(always)]
#[cfg_attr(debug_assertions, track_caller)]
fn pop_forced(&mut self) -> StrictValue<'gc> {
self.stack
.pop()
.expect("stack underflow")
.restrict()
.expect("forced")
}
/// Force the top `T::WIDTH` stack slots and return them as `T`.
///
/// If any slot holds a pending thunk, this method pushes a call frame
/// whose resume PC is the **start of the current instruction**
/// (`reader.inst_start_pc()`), enters the thunk, and returns
/// `Break::Force`. When the thunk eventually returns, the VM will
/// **re-execute the entire opcode handler from the beginning**.
///
/// # Invariants
///
/// * **Do not call this method more than once in a single handler.**
/// If you need to force multiple values, use a tuple type such as
/// `(StrictValue, StrictValue)` so they are forced and popped in one
/// atomic operation. Calling `force_and_retry` twice (or more)
/// means the handler will be re-run from the top after each retry;
/// any stack modifications between the two calls would be duplicated
/// and corrupt the stack layout.
///
/// * The caller must ensure that the stack layout at the point of
/// invocation is **identical** every time the handler is re-entered.
/// In practice this means no pushes, pops, or local mutations may
/// happen before the call, and the call must be the first thing
/// that consumes the instruction's operand values.
///
/// * The return value must be propagated with `?` so that
/// `Break::Force` correctly unwinds to the dispatch loop.
#[inline(always)]
fn force_and_retry<T: Forced<'gc>>(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> std::ops::ControlFlow<Break, T> {
self.force_and_retry_pc(reader, mc, reader.inst_start_pc())
}
/// Same as [`force_and_retry`](Self::force_and_retry) but allows
/// specifying a custom resume PC.
#[inline(always)]
fn force_and_retry_pc<T: Forced<'gc>>(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
resume_pc: usize,
) -> std::ops::ControlFlow<Break, T> {
T::force_and_check(self, reader, mc, 0, resume_pc)?;
std::ops::ControlFlow::Continue(T::pop_converted(self))
}
#[inline(always)]
#[allow(unused)]
fn force_slot(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
self.force_slot_to_pc(depth, reader, mc, reader.inst_start_pc())
}
#[inline(always)]
fn force_slot_to_pc(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
resume_pc: usize,
) -> Step {
let Some(thunk) = self.peek(depth).as_gc::<Thunk>() else {
return Step::Continue(());
};
let mut state = thunk.borrow_mut(mc);
match *state {
ThunkState::Pending { ip, env } => {
*state = ThunkState::Blackhole;
self.call_stack.push(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: self.env,
});
self.env = env;
reader.set_pc(ip);
Step::Break(Break::Force)
}
ThunkState::Evaluated(v) => {
self.replace(depth, v.relax());
Step::Continue(())
}
ThunkState::Apply { func, arg } => {
self.call_stack.push(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: self.env,
});
self.push(func);
self.call(reader, mc, arg, resume_pc)
}
ThunkState::Blackhole => {
self.finish_err(Error::eval_error("infinite recursion encountered"))
}
}
}
}
impl<'gc> Machine<'gc> for Vm<'gc> {
#[inline(always)]
fn push(&mut self, val: Value<'gc>) {
self.push(val);
}
#[inline(always)]
fn pop(&mut self) -> Value<'gc> {
self.pop()
}
#[inline(always)]
fn peek(&self, depth: usize) -> Value<'gc> {
Vm::peek(self, depth)
}
#[inline(always)]
fn peek_forced(&self, depth: usize) -> StrictValue<'gc> {
Vm::peek_forced(self, depth)
}
#[inline(always)]
fn pop_forced(&mut self) -> StrictValue<'gc> {
self.pop_forced()
}
#[inline(always)]
fn replace(&mut self, depth: usize, val: Value<'gc>) {
self.replace(depth, val);
}
#[inline(always)]
fn stack_len(&self) -> usize {
self.stack.len()
}
#[inline(always)]
fn force_slot_to_pc(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
resume_pc: usize,
) -> Step {
self.force_slot_to_pc(depth, reader, mc, resume_pc)
}
#[inline(always)]
fn call(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
arg: Value<'gc>,
resume_pc: usize,
) -> Step {
self.call(reader, mc, arg, resume_pc)
}
#[inline(always)]
fn push_call_frame(&mut self, frame: CallFrame<'gc>) {
self.call_stack.push(frame);
}
#[inline(always)]
fn pop_call_frame(&mut self) -> Option<CallFrame<'gc>> {
self.call_stack.pop()
}
#[inline(always)]
fn call_depth(&self) -> usize {
self.call_depth
}
#[inline(always)]
fn inc_call_depth(&mut self) {
self.call_depth += 1;
}
#[inline(always)]
fn dec_call_depth(&mut self) {
self.call_depth -= 1;
}
#[inline(always)]
fn env(&self) -> GcEnv<'gc> {
self.env
}
#[inline(always)]
fn set_env(&mut self, env: GcEnv<'gc>) {
self.env = env;
}
#[inline(always)]
fn finish_ok(&mut self, val: fix_common::Value) -> Step {
self.finish_ok(val)
}
#[inline(always)]
fn finish_err(&mut self, err: Box<Error>) -> Step {
self.finish_err(err)
}
#[inline(always)]
fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step {
self.finish_type_err(expected, got)
}
#[inline(always)]
fn builtins(&self) -> Value<'gc> {
self.builtins
}
#[inline(always)]
fn functor_sym(&self) -> StringId {
self.functor_sym
}
#[inline(always)]
fn empty_list(&self) -> Value<'gc> {
self.empty_list
}
#[inline(always)]
fn empty_attrs(&self) -> Value<'gc> {
self.empty_attrs
}
#[inline(always)]
fn force_mode(&self) -> ForceMode {
self.force_mode
}
#[inline(always)]
fn import_cache_get(&self, path: &std::path::Path) -> Option<Value<'gc>> {
self.import_cache.get(path).copied()
}
#[inline(always)]
fn import_cache_insert(&mut self, path: PathBuf, val: Value<'gc>) {
self.import_cache.insert(path, val);
}
#[inline(always)]
fn scope_slot(&self, idx: u32) -> Value<'gc> {
*self
.scope_slots
.get(idx as usize)
.expect("invalid scope slot")
}
#[inline(always)]
fn scope_slots_push(&mut self, val: Value<'gc>) -> u32 {
let idx = self.scope_slots.len() as u32;
self.scope_slots.push(val);
idx
}
#[inline(always)]
fn set_pending_load(&mut self, load: PendingLoad) {
self.pending_load = Some(load);
}
}
enum Action {
Continue { pc: usize },
Done(Result<fix_common::Value>),
LoadFile(PendingLoad),
}
impl Vm<'_> {
pub fn run<C: VmContext>(
ctx: &mut C,
ip: InstructionPtr,
force_mode: ForceMode,
) -> Result<fix_common::Value> {
let (code, runtime) = ctx.split();
let mut arena: Arena<Rootable![Vm<'_>]> = Arena::new(|mc| Vm::new(force_mode, mc, runtime));
const COLLECTOR_GRANULARITY: f64 = 1024.0;
let mut pc = ip.0;
loop {
let bytecode = code.bytecode();
match arena.mutate_root(|mc, root| root.dispatch_batch(bytecode, runtime, pc, mc)) {
Action::Continue { pc: new_pc } => {
pc = new_pc;
if arena.metrics().allocation_debt() > COLLECTOR_GRANULARITY {
if arena.collection_phase() == CollectionPhase::Sweeping {
arena.collect_debt();
} else if let Some(marked) = arena.mark_debt() {
marked.start_sweeping();
}
}
}
Action::LoadFile(load) => {
let source = match Source::new_file(load.path) {
Ok(src) => src,
Err(err) => break Err(Error::eval_error(format!("import failed: {err}"))),
};
let extra_scope = load.scope.map(|s| ExtraScope::ScopedImport {
keys: s.keys,
slot_id: s.slot_id,
});
let new_ip = match code.compile_with_scope(source, extra_scope, runtime) {
Ok(ip) => ip,
Err(err) => break Err(err),
};
pc = new_ip.0;
arena.mutate_root(|mc, root| {
root.env = Gc::new(mc, RefLock::new(Env::empty()));
});
}
Action::Done(done) => break done,
}
}
}
}
impl<'gc> Vm<'gc> {
const DEFAULT_FUEL_AMOUNT: u32 = 1024;
#[inline(always)]
fn dispatch_batch<C: VmRuntimeCtx>(
&mut self,
bytecode: &[u8],
ctx: &mut C,
pc: usize,
mc: &Mutation<'gc>,
) -> Action {
#[cfg(not(feature = "tailcall"))]
{
self.execute_batch(bytecode, ctx, pc, mc)
}
#[cfg(feature = "tailcall")]
{
use crate::dispatch_tailcall::{TailResult, run_tailcall};
match run_tailcall(self, mc, ctx, bytecode, pc as u32) {
TailResult::YieldFuel(new_pc) => Action::Continue {
pc: new_pc as usize,
},
TailResult::Done => {
Action::Done(self.result.take().expect("TailResult::Done without result"))
}
TailResult::LoadFile => Action::LoadFile(
self.pending_load
.take()
.expect("TailResult::LoadFile without pending_load"),
),
}
}
}
#[inline(always)]
#[cfg(not(feature = "tailcall"))]
fn execute_batch(
&mut self,
bytecode: &[u8],
ctx: &mut impl VmRuntimeCtx,
pc: usize,
mc: &Mutation<'gc>,
) -> Action {
use fix_codegen::Op::*;
let mut reader = BytecodeReader::new(bytecode, pc);
let mut fuel = Self::DEFAULT_FUEL_AMOUNT;
loop {
if fuel == 0 {
return Action::Continue { pc: reader.pc() };
}
fuel -= 1;
let op = reader.read_op();
let result = match op {
PushSmi => self.op_push_smi(&mut reader),
PushBigInt => self.op_push_bigint(&mut reader, mc),
PushFloat => self.op_push_float(&mut reader),
PushString => self.op_push_string(&mut reader),
PushNull => self.op_push_null(),
PushTrue => self.op_push_true(),
PushFalse => self.op_push_false(),
LoadLocal => self.op_load_local(&mut reader),
LoadOuter => self.op_load_outer(&mut reader),
StoreLocal => self.op_store_local(&mut reader, mc),
AllocLocals => self.op_alloc_locals(&mut reader, mc),
MakeThunk => self.op_make_thunk(&mut reader, mc),
MakeClosure => self.op_make_closure(&mut reader, mc),
MakePatternClosure => self.op_make_pattern_closure(&mut reader, mc),
Call => self.op_call(ctx, &mut reader, mc),
DispatchPrimOp => self.op_dispatch_primop(ctx, &mut reader, mc),
Return => self.op_return(ctx, &mut reader, mc),
MakeAttrs => self.op_make_attrs(ctx, &mut reader, mc),
MakeEmptyAttrs => self.op_make_empty_attrs(),
SelectStatic => self.op_select_static(ctx, &mut reader, mc),
SelectDynamic => self.op_select_dynamic(ctx, &mut reader, mc),
HasAttrPathStatic => self.op_has_attr_path_static(ctx, &mut reader, mc),
HasAttrPathDynamic => self.op_has_attr_path_dynamic(ctx, &mut reader, mc),
HasAttrStatic => self.op_has_attr_static(&mut reader, mc),
HasAttrDynamic => self.op_has_attr_dynamic(ctx, &mut reader, mc),
HasAttrResolve => self.op_has_attr_resolve(),
JumpIfSelectFailed => self.op_jump_if_select_failed(&mut reader),
JumpIfSelectSucceeded => self.op_jump_if_select_succeeded(&mut reader),
MakeList => self.op_make_list(ctx, &mut reader, mc),
MakeEmptyList => self.op_make_empty_list(),
OpAdd => self.op_add(ctx, &mut reader, mc),
OpSub => self.op_sub(&mut reader, mc),
OpMul => self.op_mul(&mut reader, mc),
OpDiv => self.op_div(&mut reader, mc),
OpEq => self.op_eq(ctx, &mut reader, mc),
OpNeq => self.op_neq(ctx, &mut reader, mc),
OpLt => self.op_lt(ctx, &mut reader, mc),
OpGt => self.op_gt(ctx, &mut reader, mc),
OpLeq => self.op_leq(ctx, &mut reader, mc),
OpGeq => self.op_geq(ctx, &mut reader, mc),
OpConcat => self.op_concat(&mut reader, mc),
OpUpdate => self.op_update(&mut reader, mc),
OpNeg => self.op_neg(&mut reader, mc),
OpNot => self.op_not(&mut reader, mc),
JumpIfFalse => self.op_jump_if_false(&mut reader, mc),
JumpIfTrue => self.op_jump_if_true(&mut reader, mc),
Jump => self.op_jump(&mut reader),
ConcatStrings => self.op_concat_strings(ctx, &mut reader, mc),
CoerceToString => self.op_coerce_to_string(&mut reader, mc),
ResolvePath => self.op_resolve_path(ctx, &mut reader, mc),
Assert => self.op_assert(ctx, &mut reader, mc),
LookupWith => self.op_lookup_with(ctx, &mut reader, mc),
LoadBuiltins => self.op_load_builtins(),
LoadBuiltin => self.op_load_builtin(&mut reader),
LoadReplBinding => self.op_load_repl_binding(&mut reader),
LoadScopedBinding => self.op_load_scoped_binding(ctx, &mut reader, mc),
Illegal => unreachable!(),
};
match result {
Step::Continue(()) | Step::Break(Break::Force) => {}
Step::Break(Break::Done) => {
return Action::Done(self.result.take().expect("Break::Done without result"));
}
Step::Break(Break::LoadFile) => {
return Action::LoadFile(
self.pending_load
.take()
.expect("Break::LoadFile without pending_load"),
);
}
}
}
}
}
+59
View File
@@ -0,0 +1,59 @@
[package]
name = "fix"
version = "0.1.0"
edition = "2024"
[dependencies]
mimalloc = "0.1"
# REPL
anyhow = "1.0"
rustyline = "18.0"
# CLI
clap = { version = "4", features = ["derive"] }
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Error Reporting
thiserror = "2"
miette = { version = "7.4", features = ["fancy"] }
# Data Structure
hashbrown = { workspace = true }
string-interner = { workspace = true }
# Memory Management
bumpalo = { workspace = true }
rnix = { workspace = true }
ere = { workspace = true }
ghost-cell = { workspace = true }
fix-abstract-vm = { path = "../fix-abstract-vm" }
fix-builtins = { path = "../fix-builtins" }
fix-common = { path = "../fix-common" }
fix-codegen = { path = "../fix-codegen" }
fix-error = { path = "../fix-error" }
fix-ir = { path = "../fix-ir" }
fix-vm = { path = "../fix-vm" }
[dev-dependencies]
criterion = { version = "0.8", features = ["html_reports"] }
tempfile = "3.24"
test-log = { version = "0.2", features = ["trace"] }
[[bench]]
name = "basic_ops"
harness = false
[[bench]]
name = "builtins"
harness = false
[[bench]]
name = "thunk_scope"
harness = false
File renamed without changes.
File renamed without changes.
File renamed without changes.
+15
View File
@@ -0,0 +1,15 @@
#![allow(dead_code)]
use fix::Evaluator;
use fix_common::Value;
use fix_error::{Result, Source};
pub fn eval(expr: &str) -> Value {
Evaluator::new()
.eval(Source::new_eval(expr.into()).unwrap())
.unwrap()
}
pub fn eval_result(expr: &str) -> Result<Value> {
Evaluator::new().eval(Source::new_eval(expr.into()).unwrap())
}
File renamed without changes.
+2 -134
View File
@@ -1,14 +1,9 @@
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)]
@@ -130,8 +125,6 @@ pub enum Error {
#[label("error occurred here")] #[label("error occurred here")]
span: Option<SourceSpan>, span: Option<SourceSpan>,
message: String, message: String,
#[help]
js_backtrace: Option<String>,
#[related] #[related]
stack_trace: Vec<StackFrame>, stack_trace: Vec<StackFrame>,
}, },
@@ -168,12 +161,11 @@ impl Error {
.into() .into()
} }
pub fn eval_error(msg: String, backtrace: Option<String>) -> Box<Self> { pub fn eval_error(msg: impl Into<String>) -> Box<Self> {
Error::EvalError { Error::EvalError {
src: None, src: None,
span: None, span: None,
message: msg, message: msg.into(),
js_backtrace: backtrace,
stack_trace: Vec::new(), stack_trace: Vec::new(),
} }
.into() .into()
@@ -229,127 +221,3 @@ 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,8 +5,6 @@ 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;
@@ -20,7 +18,6 @@ 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 {
@@ -306,11 +303,3 @@ 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>(),
]
}
File renamed without changes.
@@ -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("nix-js") .join("fix")
.join("fetchers"); .join("fetchers");
fs::create_dir_all(&base_dir)?; fs::create_dir_all(&base_dir)?;
File renamed without changes.
File renamed without changes.
File renamed without changes.
+724
View File
@@ -0,0 +1,724 @@
#![warn(clippy::unwrap_used)]
#![allow(dead_code)]
use bumpalo::Bump;
use fix_abstract_vm::{ForceMode, StaticValue, VmCode, VmContext, VmRuntimeCtx};
use fix_builtins::PrimOpPhase;
use fix_codegen::disassembler::{Disassembler, DisassemblerContext};
use fix_codegen::{BytecodeContext, InstructionPtr, Op};
use fix_common::{StringId, Symbol};
use fix_error::{Error, Result, Source};
use fix_ir::downgrade::{Downgrade as _, DowngradeContext};
use fix_ir::{
GhostMaybeThunkRef, GhostRoIrRef, GhostRoMaybeThunkRef, GhostRoRef, Ir, MaybeThunk, RawIrRef,
ThunkId,
};
use fix_vm::Vm;
use ghost_cell::{GhostCell, GhostToken};
use hashbrown::{HashMap, HashSet};
use string_interner::{DefaultStringInterner, Symbol as _};
mod derivation;
pub mod logging;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
pub struct RuntimeState {
pub strings: DefaultStringInterner,
pub constants: Constants,
}
pub struct CodeState {
pub bytecode: Vec<u8>,
pub sources: Vec<Source>,
pub spans: Vec<(usize, rnix::TextRange)>,
pub thunk_count: usize,
pub global_env: HashMap<StringId, MaybeThunk>,
pub current_scope_slot: Option<u32>,
}
pub struct Evaluator {
pub runtime: RuntimeState,
pub code: CodeState,
}
impl Default for Evaluator {
fn default() -> Self {
Self::new()
}
}
impl Evaluator {
pub fn new() -> Self {
let mut strings = DefaultStringInterner::new();
let global_env = fix_ir::new_global_env(&mut strings);
let mut bytecode = Vec::with_capacity(PrimOpPhase::Illegal as usize * 2);
for phase in 0..=PrimOpPhase::Illegal as u8 {
bytecode.push(Op::DispatchPrimOp as u8);
bytecode.push(phase);
}
Self {
runtime: RuntimeState {
strings,
constants: Constants::default(),
},
code: CodeState {
sources: Vec::new(),
spans: Vec::new(),
thunk_count: 0,
bytecode,
global_env,
current_scope_slot: None,
},
}
}
pub fn eval(&mut self, source: Source) -> Result<fix_common::Value> {
self.do_eval(source, None, ForceMode::AsIs)
}
pub fn eval_shallow(&mut self, source: Source) -> Result<fix_common::Value> {
self.do_eval(source, None, ForceMode::Shallow)
}
pub fn eval_deep(&mut self, source: Source) -> Result<fix_common::Value> {
self.do_eval(source, None, ForceMode::Deep)
}
pub fn eval_repl(
&mut self,
source: Source,
scope: &HashSet<StringId>,
) -> Result<fix_common::Value> {
self.do_eval(source, Some(ExtraScope::Repl(scope)), ForceMode::Shallow)
}
fn do_eval<'ctx>(
&'ctx mut self,
source: Source,
extra_scope: Option<ExtraScope<'ctx>>,
force_mode: ForceMode,
) -> Result<fix_common::Value> {
let ip = {
let mut compiler = CompilerCtx {
code: &mut self.code,
runtime: &mut self.runtime,
};
compiler.compile_bytecode(source, extra_scope)?
};
Vm::run(self, ip, force_mode)
}
pub fn add_binding(
&mut self,
_ident: &str,
_expr: &str,
_scope: &mut HashSet<StringId>,
) -> Result<fix_common::Value> {
todo!("add_binding")
}
pub fn compile_bytecode(&mut self, source: Source) -> Result<InstructionPtr> {
let mut compiler = CompilerCtx {
code: &mut self.code,
runtime: &mut self.runtime,
};
compiler.compile_bytecode(source, None)
}
pub fn disassemble_colored(&self, ip: InstructionPtr) -> String {
Disassembler::new(ip, self).disassemble_colored()
}
}
impl VmRuntimeCtx for RuntimeState {
fn intern_string(&mut self, s: impl AsRef<str>) -> StringId {
StringId(self.strings.get_or_intern(s))
}
fn resolve_string(&self, id: StringId) -> &str {
#[allow(clippy::unwrap_used)]
self.strings.resolve(id.0).unwrap()
}
fn get_const(&self, id: u32) -> StaticValue {
#[allow(clippy::unwrap_used)]
self.constants.get(id).unwrap()
}
fn add_const(&mut self, val: StaticValue) -> u32 {
self.constants.insert(val)
}
}
impl VmCode for CodeState {
fn bytecode(&self) -> &[u8] {
&self.bytecode
}
fn compile_with_scope(
&mut self,
source: Source,
extra_scope: Option<fix_abstract_vm::ExtraScope>,
runtime: &mut impl VmRuntimeCtx,
) -> Result<InstructionPtr> {
let mut compiler = CompilerCtx {
code: self,
runtime,
};
let extra = extra_scope.map(|s| match s {
fix_abstract_vm::ExtraScope::ScopedImport { keys, slot_id } => {
ExtraScope::ScopedImport { keys, slot_id }
}
});
compiler.compile_bytecode(source, extra)
}
}
impl VmContext for Evaluator {
fn split(&mut self) -> (&mut impl VmCode, &mut impl VmRuntimeCtx) {
(&mut self.code, &mut self.runtime)
}
}
struct CompilerCtx<'a, R: VmRuntimeCtx> {
code: &'a mut CodeState,
runtime: &'a mut R,
}
impl<'a, R: VmRuntimeCtx> CompilerCtx<'a, R> {
fn compile_bytecode(
&mut self,
source: Source,
extra_scope: Option<ExtraScope>,
) -> Result<InstructionPtr> {
let prev_scope_slot = self.code.current_scope_slot;
self.code.current_scope_slot = match &extra_scope {
Some(ExtraScope::ScopedImport { slot_id, .. }) => Some(*slot_id),
_ => None,
};
let result = (|| -> Result<InstructionPtr> {
let root = self.downgrade(source, extra_scope)?;
let ip = fix_codegen::compile_bytecode(root.as_ref(), self);
Ok(ip)
})();
self.code.current_scope_slot = prev_scope_slot;
result
}
fn downgrade(&mut self, source: Source, extra_scope: Option<ExtraScope>) -> Result<OwnedIr> {
tracing::debug!("Parsing Nix expression");
self.code.sources.push(source.clone());
let root = rnix::Root::parse(&source.src);
handle_parse_error(root.errors(), source.clone()).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 downgrade_ctx = DowngradeCtx::new(
&bump,
token,
self.runtime,
&self.code.global_env,
extra_scope.map(Into::into),
&mut self.code.thunk_count,
source,
);
let ir = downgrade_ctx.downgrade_toplevel(expr)?;
let ir = unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) };
Ok(OwnedIr { _bump: bump, ir })
})
}
}
impl<'a, R: VmRuntimeCtx> BytecodeContext for CompilerCtx<'a, R> {
fn intern_string(&mut self, s: &str) -> StringId {
self.runtime.intern_string(s)
}
fn register_span(&mut self, range: rnix::TextRange) -> u32 {
let id = self.code.spans.len();
let source_id = self
.code
.sources
.len()
.checked_sub(1)
.expect("current_source not set");
self.code.spans.push((source_id, range));
id as u32
}
fn get_code(&self) -> &[u8] {
&self.code.bytecode
}
fn get_code_mut(&mut self) -> &mut Vec<u8> {
&mut self.code.bytecode
}
fn add_constant(&mut self, val: fix_codegen::Const) -> u32 {
use fix_codegen::Const::*;
let val = match val {
Smi(x) => StaticValue::new_inline(x),
Float(x) => StaticValue::new_float(x),
Bool(x) => StaticValue::new_inline(x),
String(x) => StaticValue::new_inline(x),
Path(_) => todo!("path value type"),
PrimOp {
id,
arity,
dispatch_ip,
} => StaticValue::new_primop(id, arity, dispatch_ip),
Null => StaticValue::default(),
};
self.runtime.add_const(val)
}
fn current_source_dir(&mut self) -> StringId {
let dir = self
.code
.sources
.last()
.expect("current_source not set")
.get_dir()
.to_string_lossy()
.into_owned();
self.runtime.intern_string(dir)
}
fn current_scope_slot(&self) -> Option<u32> {
self.code.current_scope_slot
}
}
#[derive(Default)]
pub struct Constants {
data: Vec<StaticValue>,
dedup: HashMap<u64, u32>,
}
impl Constants {
fn insert(&mut self, val: StaticValue) -> u32 {
let bits = val.to_bits();
*self.dedup.entry(bits).or_insert_with(|| {
let idx = self.data.len() as u32;
self.data.push(val);
idx
})
}
fn get(&self, id: u32) -> Option<StaticValue> {
self.data.get(id as usize).copied()
}
}
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, R: VmRuntimeCtx> {
bump: &'ir Bump,
token: GhostToken<'id>,
runtime: &'ctx mut R,
source: Source,
scopes: Vec<Scope<'ctx, 'id, 'ir>>,
with_stack: Vec<GhostRoMaybeThunkRef<'id, 'ir>>,
arg_count: u32,
thunk_count: &'ctx mut usize,
thunk_scopes: Vec<ThunkScope<'id, 'ir>>,
}
impl<'ctx, 'id, 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> {
fn new(
bump: &'ir Bump,
token: GhostToken<'id>,
runtime: &'ctx mut R,
global: &'ctx HashMap<StringId, MaybeThunk>,
extra_scope: Option<Scope<'ctx, 'id, 'ir>>,
thunk_count: &'ctx mut usize,
source: Source,
) -> Self {
Self {
bump,
token,
runtime,
source,
scopes: std::iter::once(Scope::Global(global))
.chain(extra_scope)
.collect(),
thunk_count,
arg_count: 0,
with_stack: Vec::new(),
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
}
impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir>
for DowngradeCtx<'ctx, 'id, 'ir, R>
{
fn new_expr(&self, expr: Ir<'ir, GhostRoRef<'id, 'ir>>) -> GhostRoIrRef<'id, 'ir> {
self.bump.alloc(GhostCell::new(expr).into())
}
fn maybe_thunk(&mut self, ir: GhostRoIrRef<'id, 'ir>) -> GhostRoMaybeThunkRef<'id, 'ir> {
use MaybeThunk::*;
let expr = (|| {
let expr = match *ir.borrow(&self.token) {
Ir::Builtin(x) => Builtin(x),
Ir::Int(x) => Int(x),
Ir::Float(x) => Float(x),
Ir::Bool(x) => Bool(x),
Ir::Str(x) => Str(x),
Ir::Arg { layer } => Arg { layer },
Ir::Builtins => Builtins,
Ir::Null => Null,
Ir::MaybeThunk(thunk) => return Some(thunk),
_ => return None,
};
Some(self.bump.alloc(GhostCell::new(expr).into()))
})();
if let Some(thunk) = expr {
return thunk;
}
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.bump.alloc(GhostCell::new(Thunk(id)).into())
}
fn intern_string(&mut self, sym: impl AsRef<str>) -> StringId {
self.runtime.intern_string(sym)
}
fn resolve_sym(&self, id: StringId) -> Symbol<'_> {
self.runtime.resolve_string(id).into()
}
fn lookup(
&mut self,
sym: StringId,
span: rnix::TextRange,
) -> Result<GhostRoMaybeThunkRef<'id, 'ir>> {
for scope in self.scopes.iter().rev() {
match scope {
&Scope::Global(global_scope) => {
if let Some(expr) = global_scope.get(&sym) {
return Ok(expr.into());
}
}
&Scope::Repl(repl_bindings) => {
if repl_bindings.contains(&sym) {
return Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::ReplBinding(sym)).into()));
}
}
Scope::ScopedImport { keys, .. } => {
if keys.contains(&sym) {
return Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::ScopedImportBinding(sym)).into()));
}
}
Scope::Let(let_scope) => {
if let Some(&expr) = let_scope.get(&sym) {
return Ok(expr.into());
}
}
&Scope::Param {
sym: param_sym,
abs_layer,
} => {
if param_sym == sym {
let layers: u8 =
self.thunk_scopes.len().try_into().expect("scope too deep!");
let layer = layers - abs_layer;
return Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::Arg { layer }).into()));
}
}
}
}
if !self.with_stack.is_empty() {
let id = ThunkId(*self.thunk_count);
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
let mut namespaces =
bumpalo::collections::Vec::with_capacity_in(self.with_stack.len(), self.bump);
namespaces.extend(self.with_stack.iter().rev().copied());
let body = self
.bump
.alloc(GhostCell::new(Ir::WithLookup { sym, namespaces }).into());
self.thunk_scopes
.last_mut()
.expect("no active thunk scope")
.add_binding(id, body);
Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::Thunk(id)).into()))
} else {
Err(Error::downgrade_error(
format!("'{}' not found", self.resolve_sym(sym)),
self.get_current_source(),
span,
))
}
}
fn get_current_source(&self) -> Source {
self.source.clone()
}
fn with_let_scope<F, Ret>(&mut self, keys: &[StringId], f: F) -> Result<Ret>
where
F: FnOnce(
&mut Self,
) -> Result<(
bumpalo::collections::Vec<'ir, GhostRoMaybeThunkRef<'id, 'ir>>,
Ret,
)>,
{
let base = *self.thunk_count;
*self.thunk_count = self
.thunk_count
.checked_add(keys.len())
.expect("thunk id overflow");
let handles = (base..base + keys.len())
.map(|id| {
&*self
.bump
.alloc(GhostCell::new(MaybeThunk::Thunk(ThunkId(id))))
})
.collect::<Vec<_>>();
let scope = keys.iter().copied().zip(handles.iter().copied()).collect();
self.scopes.push(Scope::Let(scope));
let (vals, ret) = { f(self)? };
self.scopes.pop();
assert_eq!(keys.len(), vals.len());
let scope = self.thunk_scopes.last_mut().expect("no active thunk scope");
for (i, (val, handle)) in vals.into_iter().zip(handles).enumerate() {
let thunk = *val.borrow(&self.token);
*handle.borrow_mut(&mut self.token) = thunk;
let id = ThunkId(base + i);
let ir_ref = self
.bump
.alloc(GhostCell::new(Ir::MaybeThunk(handle.into())).into());
scope.add_binding(id, ir_ref);
}
Ok(ret)
}
fn with_param_scope<F, Ret>(&mut self, sym: StringId, f: F) -> Ret
where
F: FnOnce(&mut Self) -> Ret,
{
self.scopes.push(Scope::Param {
sym,
abs_layer: self.thunk_scopes.len().try_into().expect("scope too deep!"),
});
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())
}
fn with_with_scope<F, Ret>(&mut self, namespace: GhostRoMaybeThunkRef<'id, 'ir>, f: F) -> Ret
where
F: FnOnce(&mut Self) -> Ret,
{
self.with_stack.push(namespace);
let ret = f(self);
self.with_stack.pop();
ret
}
fn with_thunk_scope<F, Ret>(
&mut self,
f: F,
) -> (
Ret,
bumpalo::collections::Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>,
)
where
F: FnOnce(&mut Self) -> Ret,
{
if self.thunk_scopes.len() == u8::MAX as usize {
panic!("scope too deep!");
}
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, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> {
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;
Ok(Ir::freeze(
self.new_expr(Ir::TopLevel { body, thunks }),
self.token,
))
}
}
struct ThunkScope<'id, 'ir> {
bindings: bumpalo::collections::Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>,
}
impl<'id, 'ir> ThunkScope<'id, 'ir> {
fn new_in(bump: &'ir Bump) -> Self {
Self {
bindings: bumpalo::collections::Vec::new_in(bump),
}
}
fn add_binding(&mut self, id: ThunkId, ir: GhostRoIrRef<'id, 'ir>) {
self.bindings.push((id, ir));
}
fn extend_bindings(
&mut self,
iter: impl IntoIterator<Item = (ThunkId, GhostRoIrRef<'id, 'ir>)>,
) {
self.bindings.extend(iter);
}
}
enum Scope<'ctx, 'id, 'ir> {
Global(&'ctx HashMap<StringId, MaybeThunk>),
Repl(&'ctx HashSet<StringId>),
ScopedImport {
keys: HashSet<StringId>,
slot_id: u32,
},
Let(HashMap<StringId, GhostMaybeThunkRef<'id, 'ir>>),
Param {
sym: StringId,
abs_layer: u8,
},
}
pub enum ExtraScope<'ctx> {
Repl(&'ctx HashSet<StringId>),
ScopedImport {
keys: HashSet<StringId>,
slot_id: u32,
},
}
impl<'ctx> From<ExtraScope<'ctx>> for Scope<'ctx, '_, '_> {
fn from(value: ExtraScope<'ctx>) -> Self {
use ExtraScope::*;
match value {
ScopedImport { keys, slot_id } => Scope::ScopedImport { keys, slot_id },
Repl(scope) => Scope::Repl(scope),
}
}
}
struct ScopeGuard<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> {
ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir, R>,
}
impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> Drop for ScopeGuard<'a, 'ctx, 'id, 'ir, R> {
fn drop(&mut self) {
self.ctx.scopes.pop();
}
}
impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> ScopeGuard<'a, 'ctx, 'id, 'ir, R> {
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir, R> {
self.ctx
}
}
struct OwnedIr {
_bump: Bump,
ir: RawIrRef<'static>,
}
impl OwnedIr {
/// # Safety
/// `ir` must be an allocation backed by `bump`. The reference's
/// lifetime is extended to `'static` as a placeholder; the stored IR
/// must only be re-borrowed via [`OwnedIr::as_ref`], which narrows
/// the lifetime back to that of the `&self` borrow. Moving `bump`
/// into the struct keeps the underlying allocation live for the
/// lifetime of the `OwnedIr`.
unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self {
Self {
_bump: bump,
// SAFETY: see function docs - caller guarantees `ir` is in `bump`,
// and the `'static` lifetime is a placeholder narrowed by `as_ref`.
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) },
}
}
fn as_ref<'ir>(&'ir self) -> RawIrRef<'ir> {
// SAFETY: narrows the placeholder `'static` lifetime stored in
// `self.ir` down to `'ir = &'ir self`. Lifetime shortening is
// logically sound for covariant positions; the transmute is only
// needed because `RawRef<'ir>` carries `'ir` through a GAT
// (`Ref::Ref<T>`), which prevents the compiler from inferring
// covariance automatically. The bump arena that backs the IR is
// owned by `self._bump`, so the data is live for at least `'ir`.
unsafe { std::mem::transmute::<RawIrRef<'static>, RawIrRef<'ir>>(self.ir) }
}
}
impl DisassemblerContext for Evaluator {
fn get_code(&self) -> &[u8] {
&self.code.bytecode
}
#[allow(clippy::unwrap_used)]
fn resolve_string(&self, id: u32) -> &str {
let id = string_interner::symbol::SymbolU32::try_from_usize(id as usize).unwrap();
self.runtime.strings.resolve(id).unwrap()
}
}
+3 -1
View File
@@ -1,7 +1,9 @@
use std::env; use std::env;
use std::io::IsTerminal; use std::io::IsTerminal;
use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, Layer, fmt};
pub fn init_logging() { pub fn init_logging() {
let is_terminal = std::io::stderr().is_terminal(); let is_terminal = std::io::stderr().is_terminal();
+19 -55
View File
@@ -3,23 +3,15 @@ use std::process::exit;
use anyhow::Result; use anyhow::Result;
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use fix::Evaluator;
use fix_error::Source;
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 = "fix", 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,
} }
@@ -48,28 +40,7 @@ struct ExprSource {
file: Option<PathBuf>, file: Option<PathBuf>,
} }
fn create_context(#[cfg(feature = "inspector")] cli: &Cli) -> Result<Context> { fn run_compile(eval: &mut Evaluator, src: ExprSource, silent: bool) -> Result<()> {
#[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 {
@@ -77,10 +48,10 @@ fn run_compile(context: &mut Context, src: ExprSource, silent: bool) -> Result<(
} else { } else {
unreachable!() unreachable!()
}; };
match context.compile_bytecode(src) { match eval.compile_bytecode(src) {
Ok(compiled) => { Ok(ip) => {
if !silent { if !silent {
println!("{}", context.disassemble_colored(&compiled)); println!("{}", eval.disassemble_colored(ip));
} }
} }
Err(err) => { Err(err) => {
@@ -88,12 +59,10 @@ fn run_compile(context: &mut Context, src: ExprSource, silent: bool) -> Result<(
exit(1); exit(1);
} }
}; };
#[cfg(feature = "inspector")]
context.wait_for_inspector_disconnect();
Ok(()) Ok(())
} }
fn run_eval(context: &mut Context, src: ExprSource) -> Result<()> { fn run_eval(eval: &mut Evaluator, 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 {
@@ -101,7 +70,7 @@ fn run_eval(context: &mut Context, src: ExprSource) -> Result<()> {
} else { } else {
unreachable!() unreachable!()
}; };
match context.eval_deep(src) { match eval.eval_deep(src) {
Ok(value) => { Ok(value) => {
println!("{}", value.display_compat()); println!("{}", value.display_compat());
} }
@@ -110,17 +79,15 @@ fn run_eval(context: &mut Context, src: ExprSource) -> Result<()> {
exit(1); exit(1);
} }
}; };
#[cfg(feature = "inspector")]
context.wait_for_inspector_disconnect();
Ok(()) Ok(())
} }
fn run_repl(context: &mut Context) -> Result<()> { fn run_repl(eval: &mut Evaluator) -> 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]*(.*)$");
loop { loop {
let readline = rl.readline("nix-js-repl> "); let readline = rl.readline("fix-repl> ");
match readline { match readline {
Ok(line) => { Ok(line) => {
if line.trim().is_empty() { if line.trim().is_empty() {
@@ -134,20 +101,20 @@ fn run_repl(context: &mut Context) -> Result<()> {
eprintln!("Error: missing expression after '='"); eprintln!("Error: missing expression after '='");
continue; continue;
} }
match context.add_binding(ident, expr, &mut scope) { match eval.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 context.eval_repl(src, &scope) { match eval.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 context.eval_shallow(src) { match eval.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)),
} }
@@ -170,18 +137,15 @@ fn run_repl(context: &mut Context) -> Result<()> {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
nix_js::logging::init_logging(); fix::logging::init_logging();
let cli = Cli::parse(); let cli = Cli::parse();
let mut context = create_context( let mut eval = Evaluator::new();
#[cfg(feature = "inspector")]
&cli,
)?;
match cli.command { match cli.command {
Command::Compile { source, silent } => run_compile(&mut context, source, silent), Command::Compile { source, silent } => run_compile(&mut eval, source, silent),
Command::Eval { source } => run_eval(&mut context, source), Command::Eval { source } => run_eval(&mut eval, source),
Command::Repl => run_repl(&mut context), Command::Repl => run_repl(&mut eval),
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ use std::path::Path;
use nix_nar::Encoder; use nix_nar::Encoder;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use crate::error::{Error, Result}; use fix_error::{Error, Result};
pub fn compute_nar_hash(path: &Path) -> Result<[u8; 32]> { pub fn compute_nar_hash(path: &Path) -> Result<[u8; 32]> {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
File renamed without changes.
-1
View File
@@ -7,7 +7,6 @@ mod validation;
pub use config::StoreConfig; pub use config::StoreConfig;
pub use daemon::DaemonStore; pub use daemon::DaemonStore;
pub use validation::validate_store_path;
pub trait Store: Send + Sync { pub trait Store: Send + Sync {
fn get_store_dir(&self) -> &str; fn get_store_dir(&self) -> &str;
File renamed without changes.
@@ -69,13 +69,10 @@ impl Store for DaemonStore {
fn ensure_path(&self, path: &str) -> Result<()> { fn ensure_path(&self, path: &str) -> Result<()> {
self.block_on(async { self.block_on(async {
self.connection.ensure_path(path).await.map_err(|e| { self.connection.ensure_path(path).await.map_err(|e| {
Error::eval_error( Error::eval_error(format!(
format!(
"builtins.storePath: path '{}' is not valid in nix store: {}", "builtins.storePath: path '{}' is not valid in nix store: {}",
path, e path, e
), ))
None,
)
}) })
}) })
} }
File renamed without changes.
@@ -2,75 +2,72 @@ use crate::error::{Error, Result};
pub fn validate_store_path(store_dir: &str, path: &str) -> Result<()> { pub fn validate_store_path(store_dir: &str, path: &str) -> Result<()> {
if !path.starts_with(store_dir) { if !path.starts_with(store_dir) {
return Err(Error::eval_error( return Err(Error::eval_error(format!(
format!("path '{}' is not in the Nix store", path), "path '{}' is not in the Nix store",
None, path
)); )));
} }
let relative = path let relative = path
.strip_prefix(store_dir) .strip_prefix(store_dir)
.and_then(|s| s.strip_prefix('/')) .and_then(|s| s.strip_prefix('/'))
.ok_or_else(|| Error::eval_error(format!("invalid store path format: {}", path), None))?; .ok_or_else(|| Error::eval_error(format!("invalid store path format: {}", path)))?;
if relative.is_empty() { if relative.is_empty() {
return Err(Error::eval_error( return Err(Error::eval_error(format!(
format!("store path cannot be store directory itself: {}", path), "store path cannot be store directory itself: {}",
None, path
)); )));
} }
let parts: Vec<&str> = relative.splitn(2, '-').collect(); let parts: Vec<&str> = relative.splitn(2, '-').collect();
if parts.len() != 2 { if parts.len() != 2 {
return Err(Error::eval_error( return Err(Error::eval_error(format!(
format!("invalid store path format (missing name): {}", path), "invalid store path format (missing name): {}",
None, path
)); )));
} }
let hash = parts[0]; let hash = parts[0];
let name = parts[1]; let name = parts[1];
if hash.len() != 32 { if hash.len() != 32 {
return Err(Error::eval_error( return Err(Error::eval_error(format!(
format!(
"invalid store path hash length (expected 32, got {}): {}", "invalid store path hash length (expected 32, got {}): {}",
hash.len(), hash.len(),
hash hash
), )));
None,
));
} }
for ch in hash.chars() { for ch in hash.chars() {
if !matches!(ch, '0'..='9' | 'a'..='d' | 'f'..='n' | 'p'..='s' | 'v'..='z') { if !matches!(ch, '0'..='9' | 'a'..='d' | 'f'..='n' | 'p'..='s' | 'v'..='z') {
return Err(Error::eval_error( return Err(Error::eval_error(format!(
format!("invalid character '{}' in store path hash: {}", ch, hash), "invalid character '{}' in store path hash: {}",
None, ch, hash
)); )));
} }
} }
if name.is_empty() { if name.is_empty() {
return Err(Error::eval_error( return Err(Error::eval_error(format!(
format!("store path has empty name: {}", path), "store path has empty name: {}",
None, path
)); )));
} }
if name.starts_with('.') { if name.starts_with('.') {
return Err(Error::eval_error( return Err(Error::eval_error(format!(
format!("store path name cannot start with '.': {}", name), "store path name cannot start with '.': {}",
None, name
)); )));
} }
for ch in name.chars() { for ch in name.chars() {
if !matches!(ch, '0'..='9' | 'a'..='z' | 'A'..='Z' | '+' | '-' | '.' | '_' | '?' | '=') { if !matches!(ch, '0'..='9' | 'a'..='z' | 'A'..='Z' | '+' | '-' | '.' | '_' | '?' | '=') {
return Err(Error::eval_error( return Err(Error::eval_error(format!(
format!("invalid character '{}' in store path name: {}", ch, name), "invalid character '{}' in store path name: {}",
None, ch, name
)); )));
} }
} }
File renamed without changes.
@@ -1,4 +1,4 @@
use nix_js::value::Value; use fix_common::Value;
use crate::utils::{eval_deep, eval_deep_result}; use crate::utils::{eval_deep, eval_deep_result};
File renamed without changes.
@@ -1,6 +1,6 @@
use nix_js::context::Context; use fix::Evaluator;
use nix_js::error::Source; use fix_common::Value;
use nix_js::value::Value; use fix_error::Source;
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 = Context::new().unwrap(); let mut ctx = Evaluator::new();
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(ctx.get_store_dir())); assert!(store_path.starts_with("/nix/store"));
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 = Context::new().unwrap(); let mut ctx = Evaluator::new();
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(ctx.get_store_dir())); assert!(store_path.starts_with("/nix/store"));
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 = Context::new().unwrap(); let mut ctx = Evaluator::new();
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(ctx.get_store_dir())); assert!(store_path.starts_with("/nix/store"));
} else { } else {
panic!("Expected string, got {:?}", result); panic!("Expected string, got {:?}", result);
} }
@@ -366,3 +366,193 @@ fn read_dir_on_file_fails() {
let err_msg = result.unwrap_err().to_string(); let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not a directory")); assert!(err_msg.contains("not a directory"));
} }
#[test_log::test]
fn to_file_simple() {
let result =
eval_result(r#"builtins.toFile "hello.txt" "Hello, World!""#).expect("Failed to evaluate");
match result {
Value::String(path) => {
assert!(path.contains("-hello.txt"));
assert!(std::path::Path::new(&path).exists());
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert_eq!(contents, "Hello, World!");
}
_ => panic!("Expected string, got {:?}", result),
}
}
#[test_log::test]
fn to_file_with_references() {
let result = eval_result(
r#"
let
dep = builtins.toFile "dep.txt" "dependency";
in
builtins.toFile "main.txt" "Reference: ${dep}"
"#,
)
.expect("Failed to evaluate");
match result {
Value::String(path) => {
assert!(path.contains("-main.txt"));
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert!(contents.contains("Reference: "));
assert!(contents.contains("-dep.txt"));
}
_ => panic!("Expected string"),
}
}
#[test_log::test]
fn to_file_invalid_name_with_slash() {
let result = eval_result(r#"builtins.toFile "foo/bar.txt" "content""#);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("name cannot contain '/'")
);
}
#[test_log::test]
fn to_file_invalid_name_dot() {
let result = eval_result(r#"builtins.toFile "." "content""#);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid name"));
}
#[test_log::test]
fn to_file_invalid_name_dotdot() {
let result = eval_result(r#"builtins.toFile ".." "content""#);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid name"));
}
#[test_log::test]
fn store_path_validation_not_in_store() {
let result = eval_result(r#"builtins.storePath "/tmp/foo""#);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("not in the Nix store")
);
}
#[test_log::test]
fn store_path_validation_malformed_hash() {
let dummy_file_result = eval_result(r#"builtins.toFile "dummy.txt" "content""#)
.expect("Failed to create dummy file");
let dummy_path = match dummy_file_result {
Value::String(ref p) => p.clone(),
_ => panic!("Expected string"),
};
let store_dir = std::path::Path::new(&dummy_path)
.parent()
.expect("Failed to get parent dir")
.to_str()
.expect("Failed to convert to string");
let test_path = format!("{}/invalid-hash-hello", store_dir);
let result = eval_result(&format!(r#"builtins.storePath "{}""#, test_path));
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("invalid") || err_str.contains("hash"),
"Expected hash validation error, got: {}",
err_str
);
}
#[test_log::test]
fn store_path_validation_missing_name() {
let dummy_file_result = eval_result(r#"builtins.toFile "dummy.txt" "content""#)
.expect("Failed to create dummy file");
let dummy_path = match dummy_file_result {
Value::String(ref p) => p.clone(),
_ => panic!("Expected string"),
};
let store_dir = std::path::Path::new(&dummy_path)
.parent()
.expect("Failed to get parent dir")
.to_str()
.expect("Failed to convert to string");
let test_path = format!("{}/abcd1234abcd1234abcd1234abcd1234", store_dir);
let result = eval_result(&format!(r#"builtins.storePath "{}""#, test_path));
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("missing name") || err_str.contains("format"),
"Expected missing name error, got: {}",
err_str
);
}
#[test_log::test]
fn to_file_curried_application() {
let result = eval_result(
r#"
let
makeFile = builtins.toFile "test.txt";
in
makeFile "test content"
"#,
)
.expect("Failed to evaluate");
match result {
Value::String(path) => {
assert!(path.contains("-test.txt"));
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert_eq!(contents, "test content");
}
_ => panic!("Expected string"),
}
}
#[test_log::test]
fn to_file_number_conversion() {
let result = eval_result(r#"builtins.toFile "number.txt" (builtins.toString 42)"#)
.expect("Failed to evaluate");
match result {
Value::String(path) => {
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert_eq!(contents, "42");
}
_ => panic!("Expected string"),
}
}
#[test_log::test]
fn to_file_list_conversion() {
let result = eval_result(
r#"builtins.toFile "list.txt" (builtins.concatStringsSep "\n" ["line1" "line2" "line3"])"#,
)
.expect("Failed to evaluate");
match result {
Value::String(path) => {
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert_eq!(contents, "line1\nline2\nline3");
}
_ => panic!("Expected string"),
}
}
+326
View File
@@ -0,0 +1,326 @@
#![allow(non_snake_case)]
use std::path::PathBuf;
use fix::Evaluator;
use fix_common::Value;
use fix_error::{Source, SourceType};
fn get_lang_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/tests/lang")
}
fn eval_file(name: &str) -> Result<(Value, Source), String> {
let lang_dir = get_lang_dir();
let nix_path = lang_dir.join(format!("{name}.nix"));
let mut ctx = Evaluator::new();
let content = std::fs::read_to_string(&nix_path).unwrap();
let source = Source {
ty: SourceType::File(nix_path.into()),
src: content.into(),
};
ctx.eval_deep(source.clone())
.map(|val| (val, source))
.map_err(|e| e.to_string())
}
fn read_expected(name: &str) -> String {
let lang_dir = get_lang_dir();
let exp_path = lang_dir.join(format!("{name}.exp"));
std::fs::read_to_string(exp_path)
.expect("expected file should exist")
.trim_end()
.to_string()
}
fn format_value(value: &Value) -> String {
value.display_compat().to_string()
}
macro_rules! eval_okay_test {
($(#[$attr:meta])* $name:ident$(, $pre:expr)?) => {
$(#[$attr])*
#[test_log::test]
fn $name() {
$(($pre)();)?
let test_name = concat!("eval-okay-", stringify!($name))
.replace("_", "-")
.replace("r#", "");
let result = eval_file(&test_name);
match result {
Ok((value, source)) => {
let actual = format_value(&value);
let actual = actual.replace(
source
.get_dir()
.parent()
.unwrap()
.to_string_lossy()
.as_ref(),
"/pwd",
);
let expected = read_expected(&test_name);
assert_eq!(actual, expected, "Output mismatch for {}", test_name);
}
Err(e) => {
panic!("Test {} failed to evaluate: {}", test_name, e);
}
}
}
};
}
macro_rules! eval_fail_test {
($name:ident) => {
#[test_log::test]
fn $name() {
let test_name = concat!("eval-fail-", stringify!($name))
.replace("_", "-")
.replace("r#", "");
let result = eval_file(&test_name);
assert!(
result.is_err(),
"Test {} should have failed but succeeded with: {:?}",
test_name,
result
);
}
};
}
mod okay {
use super::*;
eval_okay_test!(any_all);
eval_okay_test!(arithmetic);
eval_okay_test!(attrnames);
eval_okay_test!(attrs);
eval_okay_test!(attrs2);
eval_okay_test!(attrs3);
eval_okay_test!(attrs4);
eval_okay_test!(attrs5);
eval_okay_test!(
#[ignore = "__overrides is not supported"]
attrs6
);
eval_okay_test!(
#[ignore = "requires --arg/--argstr CLI flags"]
autoargs
);
eval_okay_test!(backslash_newline_1);
eval_okay_test!(backslash_newline_2);
eval_okay_test!(baseNameOf);
eval_okay_test!(builtins);
eval_okay_test!(builtins_add);
eval_okay_test!(callable_attrs);
eval_okay_test!(catattrs);
eval_okay_test!(closure);
eval_okay_test!(comments);
eval_okay_test!(concat);
eval_okay_test!(concatmap);
eval_okay_test!(concatstringssep);
eval_okay_test!(context);
eval_okay_test!(context_introspection);
eval_okay_test!(convertHash);
eval_okay_test!(curpos);
eval_okay_test!(deepseq);
eval_okay_test!(delayed_with);
eval_okay_test!(delayed_with_inherit);
eval_okay_test!(deprecate_cursed_or);
eval_okay_test!(derivation_legacy);
eval_okay_test!(dynamic_attrs);
eval_okay_test!(dynamic_attrs_2);
eval_okay_test!(dynamic_attrs_bare);
eval_okay_test!(elem);
eval_okay_test!(empty_args);
eval_okay_test!(eq);
eval_okay_test!(eq_derivations);
eval_okay_test!(filter);
eval_okay_test!(
#[ignore = "not implemented: flakeRefToString"]
flake_ref_to_string
);
eval_okay_test!(flatten);
eval_okay_test!(float);
eval_okay_test!(floor_ceil);
eval_okay_test!(foldlStrict);
eval_okay_test!(foldlStrict_lazy_elements);
eval_okay_test!(foldlStrict_lazy_initial_accumulator);
eval_okay_test!(fromjson);
eval_okay_test!(fromjson_escapes);
eval_okay_test!(fromTOML);
eval_okay_test!(
#[ignore = "timestamps are not supported"]
fromTOML_timestamps
);
eval_okay_test!(functionargs);
eval_okay_test!(hashfile);
eval_okay_test!(hashstring);
eval_okay_test!(getattrpos);
eval_okay_test!(getattrpos_functionargs);
eval_okay_test!(getattrpos_undefined);
eval_okay_test!(getenv, || {
unsafe { std::env::set_var("TEST_VAR", "foo") };
});
eval_okay_test!(groupBy);
eval_okay_test!(r#if);
eval_okay_test!(ind_string);
eval_okay_test!(import);
eval_okay_test!(inherit_attr_pos);
eval_okay_test!(
#[ignore = "__overrides is not supported"]
inherit_from
);
eval_okay_test!(intersectAttrs);
eval_okay_test!(r#let);
eval_okay_test!(list);
eval_okay_test!(listtoattrs);
eval_okay_test!(logic);
eval_okay_test!(map);
eval_okay_test!(mapattrs);
eval_okay_test!(merge_dynamic_attrs);
eval_okay_test!(nested_with);
eval_okay_test!(new_let);
eval_okay_test!(null_dynamic_attrs);
eval_okay_test!(
#[ignore = "__overrides is not supported"]
overrides
);
eval_okay_test!(
#[ignore = "not implemented: parseFlakeRef"]
parse_flake_ref
);
eval_okay_test!(partition);
eval_okay_test!(path);
eval_okay_test!(pathexists);
eval_okay_test!(path_string_interpolation, || {
unsafe {
std::env::set_var("HOME", "/fake-home");
}
});
eval_okay_test!(patterns);
eval_okay_test!(print);
eval_okay_test!(readDir);
eval_okay_test!(readfile);
eval_okay_test!(readFileType);
eval_okay_test!(redefine_builtin);
eval_okay_test!(regex_match);
eval_okay_test!(regex_split);
eval_okay_test!(regression_20220122);
eval_okay_test!(regression_20220125);
eval_okay_test!(regrettable_rec_attrset_merge);
eval_okay_test!(remove);
eval_okay_test!(repeated_empty_attrs);
eval_okay_test!(repeated_empty_list);
eval_okay_test!(replacestrings);
eval_okay_test!(
#[ignore = "requires -I CLI flags"]
search_path
);
eval_okay_test!(scope_1);
eval_okay_test!(scope_2);
eval_okay_test!(scope_3);
eval_okay_test!(scope_4);
eval_okay_test!(scope_6);
eval_okay_test!(scope_7);
eval_okay_test!(seq);
eval_okay_test!(sort);
eval_okay_test!(splitversion);
eval_okay_test!(string);
eval_okay_test!(strings_as_attrs_names);
eval_okay_test!(substring);
eval_okay_test!(substring_context);
eval_okay_test!(symlink_resolution);
eval_okay_test!(
#[ignore = "TCO not implemented, also disabled in CppNix"]
tail_call_1
);
eval_okay_test!(tojson);
eval_okay_test!(toxml);
eval_okay_test!(toxml2);
eval_okay_test!(tryeval);
eval_okay_test!(types);
eval_okay_test!(versions);
eval_okay_test!(with);
eval_okay_test!(zipAttrsWith);
}
mod fail {
use super::*;
eval_fail_test!(abort);
eval_fail_test!(addDrvOutputDependencies_empty_context);
eval_fail_test!(addDrvOutputDependencies_multi_elem_context);
eval_fail_test!(addDrvOutputDependencies_wrong_element_kind);
eval_fail_test!(addErrorRuntime_example);
eval_fail_test!(assert);
eval_fail_test!(assert_equal_attrs_names);
eval_fail_test!(assert_equal_attrs_names_2);
eval_fail_test!(assert_equal_derivations);
eval_fail_test!(assert_equal_derivations_extra);
eval_fail_test!(assert_equal_floats);
eval_fail_test!(assert_equal_function_direct);
eval_fail_test!(assert_equal_int_float);
eval_fail_test!(assert_equal_ints);
eval_fail_test!(assert_equal_list_length);
eval_fail_test!(assert_equal_paths);
eval_fail_test!(assert_equal_type);
eval_fail_test!(assert_equal_type_nested);
eval_fail_test!(assert_nested_bool);
eval_fail_test!(attr_name_type);
eval_fail_test!(attrset_merge_drops_later_rec);
eval_fail_test!(bad_string_interpolation_1);
eval_fail_test!(bad_string_interpolation_2);
eval_fail_test!(bad_string_interpolation_3);
eval_fail_test!(bad_string_interpolation_4);
eval_fail_test!(blackhole);
eval_fail_test!(call_primop);
eval_fail_test!(deepseq);
eval_fail_test!(derivation_name);
eval_fail_test!(dup_dynamic_attrs);
eval_fail_test!(duplicate_traces);
eval_fail_test!(eol_1);
eval_fail_test!(eol_2);
eval_fail_test!(eol_3);
eval_fail_test!(fetchTree_negative);
eval_fail_test!(fetchurl_baseName);
eval_fail_test!(fetchurl_baseName_attrs);
eval_fail_test!(fetchurl_baseName_attrs_name);
eval_fail_test!(flake_ref_to_string_negative_integer);
eval_fail_test!(foldlStrict_strict_op_application);
eval_fail_test!(fromJSON_keyWithNullByte);
eval_fail_test!(fromJSON_overflowing);
eval_fail_test!(fromJSON_valueWithNullByte);
eval_fail_test!(fromTOML_keyWithNullByte);
eval_fail_test!(fromTOML_timestamps);
eval_fail_test!(fromTOML_valueWithNullByte);
eval_fail_test!(hashfile_missing);
eval_fail_test!(infinite_recursion_lambda);
eval_fail_test!(list);
eval_fail_test!(missing_arg);
eval_fail_test!(mutual_recursion);
eval_fail_test!(nested_list_items);
eval_fail_test!(nonexist_path);
eval_fail_test!(not_throws);
eval_fail_test!(overflowing_add);
eval_fail_test!(overflowing_div);
eval_fail_test!(overflowing_mul);
eval_fail_test!(overflowing_sub);
eval_fail_test!(path_slash);
eval_fail_test!(pipe_operators);
eval_fail_test!(recursion);
eval_fail_test!(remove);
eval_fail_test!(scope_5);
eval_fail_test!(seq);
eval_fail_test!(set);
eval_fail_test!(set_override);
eval_fail_test!(string_nul_1);
eval_fail_test!(string_nul_2);
eval_fail_test!(substring);
eval_fail_test!(toJSON);
eval_fail_test!(toJSON_non_utf_8);
eval_fail_test!(to_path);
eval_fail_test!(undeclared_arg);
eval_fail_test!(using_set_as_attr_name);
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 563 files, more files were not shown because too many files have changed in this diff. Show more