Files
nix-js/fix-vm/src/lib.rs
T

607 lines
19 KiB
Rust

#![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;
mod boxing;
mod bytecode_reader;
#[cfg(feature = "tailcall")]
mod dispatch_tailcall;
mod forced;
mod value;
pub use value::StaticValue;
use value::*;
mod helpers;
pub(crate) mod instructions;
pub(crate) use bytecode_reader::BytecodeReader;
pub(crate) use forced::Forced;
use helpers::*;
type VmResult<T> = std::result::Result<T, VmError>;
#[allow(dead_code)]
enum VmError {
Catchable(String),
Uncatchable(Box<Error>),
}
impl From<Box<Error>> for VmError {
fn from(e: Box<Error>) -> Self {
VmError::Uncatchable(e)
}
}
impl VmError {
fn into_error(self) -> Box<Error> {
match self {
VmError::Catchable(_) => todo!("Check for tryEval catch frames"),
VmError::Uncatchable(e) => e,
}
}
}
#[derive(Collect, Clone, Copy, Debug, PartialEq, Eq, Default)]
#[collect(require_static)]
pub enum ForceMode {
#[default]
AsIs,
Shallow,
Deep,
}
pub trait VmContext {
fn intern_string(&mut self, s: impl AsRef<str>) -> StringId;
fn resolve_string(&self, id: StringId) -> &str;
fn bytecode(&self) -> &[u8];
fn get_const(&self, id: u32) -> StaticValue;
fn compile(&mut self, source: Source);
}
pub(crate) trait VmContextExt: VmContext {
fn get_string<'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: VmContext> VmContextExt 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())
}
}
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 {
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(attrs) = val.as_gc::<AttrSet>() {
let mut map = std::collections::BTreeMap::new();
for &(key, val) in attrs.iter() {
let key = self.resolve_string(key).to_owned();
let converted = self.convert_value(val);
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 items: Vec<_> = list
.inner
.iter()
.copied()
.map(|v| self.convert_value(v))
.collect();
Value::List(fix_common::List::new(items))
} else if val.is::<Closure>() {
Value::Func
} else if val.is::<Thunk>() {
Value::Thunk
} else if val.as_inline::<PrimOp>().is_some() {
Value::PrimOp("primop".into())
} else if val.is::<PrimOpApp>() {
Value::PrimOpApp("primop-app".into())
} else {
Value::Null
}
}
}
#[repr(u8)]
pub(crate) enum Break {
Force,
Done,
}
pub(crate) type Step = std::ops::ControlFlow<Break>;
#[derive(Collect)]
#[collect(no_drop)]
pub struct Vm<'gc> {
pub(crate) stack: Vec<Value<'gc>>,
pub(crate) call_stack: Vec<CallFrame<'gc>>,
pub(crate) call_depth: usize,
#[allow(dead_code)]
#[collect(require_static)]
pub(crate) error_context: Vec<ErrorFrame>,
pub(crate) env: GcEnv<'gc>,
pub(crate) with_env: Option<GcWithEnv<'gc>>,
pub(crate) import_cache: HashMap<PathBuf, Value<'gc>>,
pub(crate) builtins: Value<'gc>,
pub(crate) empty_list: Value<'gc>,
pub(crate) empty_attrs: Value<'gc>,
pub(crate) force_mode: ForceMode,
#[collect(require_static)]
pub(crate) result: Option<Result<fix_common::Value>>,
}
pub(crate) enum OperandData {
Const(StaticValue),
Local { layer: u8, idx: u32 },
Builtins,
BigInt(i64),
}
impl OperandData {
pub(crate) fn resolve<'gc>(&self, mc: &Mutation<'gc>, root: &Vm<'gc>) -> Value<'gc> {
match *self {
OperandData::Const(sv) => sv.into(),
OperandData::Local { layer, idx } => {
let mut cur = root.env;
for _ in 0..layer {
let prev = cur.borrow().prev.expect("env chain too short");
cur = prev;
}
cur.borrow().locals[idx as usize]
}
OperandData::Builtins => root.builtins,
OperandData::BigInt(val) => Value::new_gc(Gc::new(mc, val)),
}
}
}
pub(crate) enum AttrKeyData {
Static(StringId),
Dynamic(OperandData),
}
fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmContext) -> 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);
entries.push((name, Value::new_inline(PrimOp { id, arity })));
}
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 {
inner: SmallVec::new(),
},
)),
),
("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));
Value::new_gc(builtins_set)
}
impl<'gc> Vm<'gc> {
fn new(force_mode: ForceMode, mc: &Mutation<'gc>, ctx: &mut impl VmContext) -> 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())),
with_env: None,
import_cache: HashMap::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,
}
}
#[inline(always)]
pub(crate) fn finish_ok(&mut self, val: fix_common::Value) -> Step {
self.result = Some(Ok(val));
Step::Break(Break::Done)
}
#[inline(always)]
pub(crate) fn finish_err(&mut self, err: Box<Error>) -> Step {
self.result = Some(Err(err));
Step::Break(Break::Done)
}
#[inline(always)]
pub(crate) 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)]
pub(crate) fn finish_vm_err(&mut self, err: VmError) -> Step {
self.finish_err(err.into_error())
}
#[inline(always)]
pub(crate) fn push(&mut self, val: Value<'gc>) {
self.stack.push(val);
}
#[inline(always)]
#[must_use]
pub(crate) fn pop(&mut self) -> Value<'gc> {
self.stack.pop().expect("stack underflow")
}
#[inline(always)]
#[must_use]
pub(crate) fn peek(&mut self, depth: usize) -> Value<'gc> {
*self
.stack
.get(self.stack.len() - depth - 1)
.expect("stack underflow")
}
#[inline(always)]
#[must_use]
pub(crate) fn peek_forced(&mut self, depth: usize) -> StrictValue<'gc> {
self
.stack
.get(self.stack.len() - depth - 1)
.expect("stack underflow")
.restrict()
.expect("forced")
}
#[inline(always)]
pub(crate) 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)]
pub(crate) fn pop_forced(&mut self) -> StrictValue<'gc> {
self.stack
.pop()
.expect("stack underflow")
.restrict()
.expect("forced")
}
#[inline(always)]
pub(crate) fn try_force<T: Forced<'gc>>(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> std::ops::ControlFlow<Break, T> {
T::force_and_check(self, reader, mc, 0)?;
std::ops::ControlFlow::Continue(T::pop_converted(self))
}
#[inline(always)]
pub(crate) fn force_slot(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> 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, with_env } => {
*state = ThunkState::Blackhole;
drop(state);
self.call_stack.push(CallFrame {
thunk: Some(thunk),
stack_depth: depth,
pc: reader.inst_start_pc(),
env: self.env,
with_env: self.with_env,
});
self.env = env;
self.with_env = with_env;
reader.set_pc(ip);
Step::Break(Break::Force)
}
ThunkState::Evaluated(v) => {
drop(state);
self.replace(depth, v.relax());
Step::Continue(())
}
ThunkState::Apply { .. } => todo!("force apply"),
ThunkState::Blackhole => {
drop(state);
self.finish_err(Error::eval_error("infinite recursion encountered"))
}
}
}
}
#[allow(dead_code)]
struct ErrorFrame {
span_id: u32,
message: Option<String>,
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) struct CallFrame<'gc> {
pub(crate) pc: usize,
pub(crate) stack_depth: usize,
pub(crate) thunk: Option<Gc<'gc, Thunk<'gc>>>,
pub(crate) env: Gc<'gc, RefLock<Env<'gc>>>,
pub(crate) with_env: Option<Gc<'gc, WithEnv<'gc>>>,
}
pub(crate) enum Action {
Continue { pc: usize },
Done(Result<fix_common::Value>),
}
pub(crate) enum NixNum {
Int(i64),
Float(f64),
}
impl Vm<'_> {
pub fn run<C: VmContext>(
mut ctx: C,
ip: InstructionPtr,
force_mode: ForceMode,
) -> Result<fix_common::Value> {
let mut arena: Arena<Rootable![Vm<'_>]> =
Arena::new(|mc| Vm::new(force_mode, mc, &mut ctx));
const COLLECTOR_GRANULARITY: f64 = 1024.0;
let mut pc = ip.0;
let bytecode: Vec<u8> = ctx.bytecode().to_vec();
loop {
match arena.mutate_root(|mc, root| root.dispatch_batch(&bytecode, &mut ctx, 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::Done(done) => break done,
}
}
}
}
impl<'gc> Vm<'gc> {
const DEFAULT_FUEL_AMOUNT: u32 = 1024;
#[inline(always)]
fn dispatch_batch<C: VmContext>(
&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"))
}
}
}
}
#[inline(always)]
#[cfg(not(feature = "tailcall"))]
fn execute_batch(
&mut self,
bytecode: &[u8],
ctx: &mut impl VmContext,
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),
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(),
OpNot => self.op_not(),
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),
ResolvePath => self.op_resolve_path(ctx),
Assert => self.op_assert(&mut reader),
PushWith => self.op_push_with(ctx, &mut reader, mc),
PopWith => self.op_pop_with(),
LookupWith => self.op_lookup_with(ctx, &mut reader, mc),
PrepareWith => self.op_prepare_with(),
LoadBuiltins => self.op_load_builtins(),
LoadBuiltin => self.op_load_builtin(&mut reader),
MkPos => self.op_mk_pos(&mut reader),
LoadReplBinding => self.op_load_repl_binding(&mut reader),
LoadScopedBinding => self.op_load_scoped_binding(&mut reader),
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"),
);
}
}
}
}
}