Files
nix-js/nix-js/src/runtime.rs

400 lines
12 KiB
Rust

use std::borrow::Cow;
use std::cell::RefCell;
use std::ptr::NonNull;
use std::sync::Once;
use deno_core::{
Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8
};
use deno_error::js_error_wrapper;
use crate::codegen::{CodegenContext, Compile};
use crate::context::{Context, PathDropGuard};
use crate::error::{Error, Result};
use crate::ir::DowngradeContext;
use crate::value::{AttrSet, Const, List, Symbol, Value};
pub trait RuntimeContext {
fn split<Ctx: DowngradeContext>(&mut self) -> (&mut JsRuntime, &mut Ctx);
}
fn nix_runtime(ctx: &mut Context) -> Extension {
const ESM: &[ExtensionFileSource] =
&deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js");
// TODO: SAFETY
let ptr = unsafe { NonNull::new_unchecked(ctx) };
Extension {
name: "nix_runtime",
esm_files: Cow::Borrowed(ESM),
esm_entry_point: Some("ext:nix_runtime/runtime.js"),
ops: Cow::Owned(vec![
op_import(),
op_read_file(),
op_path_exists(),
op_resolve_path(),
]),
op_state_fn: Some(Box::new(move |state| {
state.put(RefCell::new(ptr));
})),
enabled: true,
..Default::default()
}
}
#[derive(Debug)]
pub struct SimpleErrorWrapper(pub String);
impl std::fmt::Display for SimpleErrorWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
impl std::error::Error for SimpleErrorWrapper {
fn cause(&self) -> Option<&dyn std::error::Error> {
None
}
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
fn description(&self) -> &str {
&self.0
}
}
impl From<String> for NixError {
fn from(value: String) -> Self {
NixError(SimpleErrorWrapper(value))
}
}
js_error_wrapper!(SimpleErrorWrapper, NixError, "EvalError");
#[deno_core::op2]
#[string]
fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result<String, NixError> {
let mut ptr = state.borrow::<RefCell<NonNull<Context>>>().borrow_mut();
let ctx = unsafe { ptr.as_mut() };
// 1. Resolve path relative to current file (or CWD if top-level)
let current_dir = ctx.get_current_dir();
let absolute_path = current_dir
.join(&path)
.canonicalize()
.map_err(|e| -> NixError {
format!("Failed to resolve path {}: {}", path, e).into()
})?;
// 2. Psh to stack for nested imports (RAII guard ensures pop on drop)
let mut guard = PathDropGuard::new(absolute_path.clone(), ctx);
let ctx = guard.as_ctx();
// 3. Read file
let content = std::fs::read_to_string(&absolute_path).map_err(|e| -> NixError {
format!("Failed to read {}: {}", absolute_path.display(), e).into()
})?;
// 4. Parse
let root = rnix::Root::parse(&content);
if !root.errors().is_empty() {
return Err(format!(
"Parse error in {}: {:?}",
absolute_path.display(),
root.errors()
)
.into());
}
// 5. Downgrade to IR
let expr = root
.tree()
.expr()
.ok_or_else(|| -> NixError { "No expression in file".to_string().into() })?;
let expr_id = ctx
.downgrade_ctx()
.downgrade(expr)
.map_err(|e| -> NixError { format!("Downgrade error: {}", e).into() })?;
// 6. Codegen - returns JS code string
Ok(ctx.get_ir(expr_id).compile(ctx))
}
#[deno_core::op2]
#[string]
fn op_read_file(#[string] path: String) -> std::result::Result<String, NixError> {
std::fs::read_to_string(&path)
.map_err(|e| -> NixError { format!("Failed to read {}: {}", path, e).into() })
}
#[deno_core::op2(fast)]
fn op_path_exists(#[string] path: String) -> bool {
std::path::Path::new(&path).exists()
}
#[deno_core::op2]
#[string]
fn op_resolve_path(state: &mut OpState, #[string] path: String) -> std::result::Result<String, NixError> {
let ptr = state.borrow::<RefCell<NonNull<Context>>>().borrow();
let ctx = unsafe { ptr.as_ref() };
// If already absolute, return as-is
if path.starts_with('/') {
return Ok(path);
}
// Resolve relative path against current file directory (or CWD)
let current_dir = ctx.get_current_dir();
current_dir
.join(&path)
.canonicalize()
.map(|p| p.to_string_lossy().to_string())
.map_err(|e| -> NixError { format!("Failed to resolve path {}: {}", path, e).into() })
}
// Runtime context for V8 value conversion
struct RuntimeCtx<'a, 'b> {
scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>,
is_thunk_symbol: Option<v8::Local<'a, v8::Symbol>>,
primop_metadata_symbol: Option<v8::Local<'a, v8::Symbol>>,
}
impl<'a, 'b> RuntimeCtx<'a, 'b> {
fn new(scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>) -> Self {
let is_thunk_symbol = Self::get_is_thunk_symbol(scope);
let primop_metadata_symbol = Self::get_primop_metadata_symbol(scope);
Self {
scope,
is_thunk_symbol,
primop_metadata_symbol,
}
}
fn get_is_thunk_symbol(
scope: &v8::PinnedRef<'a, v8::HandleScope<'b>>,
) -> Option<v8::Local<'a, v8::Symbol>> {
let global = scope.get_current_context().global(scope);
let nix_key = v8::String::new(scope, "Nix")?;
let nix_obj = global.get(scope, nix_key.into())?.to_object(scope)?;
let is_thunk_sym_key = v8::String::new(scope, "IS_THUNK")?;
let is_thunk_sym = nix_obj.get(scope, is_thunk_sym_key.into())?;
if is_thunk_sym.is_symbol() {
is_thunk_sym.try_cast().ok()
} else {
None
}
}
fn get_primop_metadata_symbol(
scope: &v8::PinnedRef<'a, v8::HandleScope<'b>>,
) -> Option<v8::Local<'a, v8::Symbol>> {
let global = scope.get_current_context().global(scope);
let nix_key = v8::String::new(scope, "Nix")?;
let nix_obj = global.get(scope, nix_key.into())?.to_object(scope)?;
let primop_metadata_sym_key = v8::String::new(scope, "PRIMOP_METADATA")?;
let primop_metadata_sym = nix_obj.get(scope, primop_metadata_sym_key.into())?;
if primop_metadata_sym.is_symbol() {
primop_metadata_sym.try_cast().ok()
} else {
None
}
}
}
pub fn new_js_runtime(ctx: &mut Context) -> JsRuntime {
// Initialize V8 once
static INIT: Once = Once::new();
INIT.call_once(|| {
JsRuntime::init_platform(
Some(v8::new_default_platform(0, false).make_shared()),
false,
);
});
JsRuntime::new(RuntimeOptions {
extensions: vec![nix_runtime(ctx)],
..Default::default()
})
}
// Main entry point
pub fn run(script: String, ctx: &mut Context) -> Result<Value> {
let mut runtime = new_js_runtime(ctx);
// Execute user script
let global_value = runtime
.execute_script("<eval>", script)
.map_err(|e| Error::eval_error(format!("Execution error: {:?}", e)))?;
// Retrieve scope from JsRuntime
deno_core::scope!(scope, runtime);
let local_value = v8::Local::new(scope, &global_value);
let runtime_ctx = RuntimeCtx::new(scope);
Ok(to_value(local_value, &runtime_ctx))
}
fn to_value<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeCtx<'a, 'b>) -> Value {
let scope = ctx.scope;
match () {
_ if val.is_big_int() => {
let (val, lossless) = val.to_big_int(scope).unwrap().i64_value();
if !lossless {
panic!("BigInt value out of i64 range: conversion lost precision");
}
Value::Const(Const::Int(val))
}
_ if val.is_number() => {
let val = val.to_number(scope).unwrap().value();
// number is always NixFloat
Value::Const(Const::Float(val))
}
_ if val.is_true() => Value::Const(Const::Bool(true)),
_ if val.is_false() => Value::Const(Const::Bool(false)),
_ if val.is_null() => Value::Const(Const::Null),
_ if val.is_string() => {
let val = val.to_string(scope).unwrap();
Value::String(val.to_rust_string_lossy(scope))
}
_ if val.is_array() => {
let val = val.try_cast::<v8::Array>().unwrap();
let len = val.length();
let list = (0..len)
.map(|i| {
let val = val.get_index(scope, i).unwrap();
to_value(val, ctx)
})
.collect();
Value::List(List::new(list))
}
_ if val.is_function() => {
if let Some(name) = primop_app_name(val, ctx) {
Value::PrimOpApp(name)
} else if let Some(name) = primop_name(val, ctx) {
Value::PrimOp(name)
} else {
Value::Func
}
}
_ if val.is_object() => {
if is_thunk(val, ctx) {
return Value::Thunk;
}
let val = val.to_object(scope).unwrap();
let keys = val
.get_own_property_names(scope, v8::GetPropertyNamesArgsBuilder::new().build())
.unwrap();
let len = keys.length();
let attrs = (0..len)
.map(|i| {
let key = keys.get_index(scope, i).unwrap();
let val = val.get(scope, key).unwrap();
let key = key.to_rust_string_lossy(scope);
(Symbol::new(key), to_value(val, ctx))
})
.collect();
Value::AttrSet(AttrSet::new(attrs))
}
_ => unimplemented!("can not convert {} to NixValue", val.type_repr()),
}
}
fn is_thunk<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeCtx<'a, 'b>) -> bool {
if !val.is_object() {
return false;
}
// Use cached IS_THUNK symbol from context
let is_thunk_sym = match ctx.is_thunk_symbol {
Some(sym) => sym,
None => return false,
};
let scope = ctx.scope;
let obj = val.to_object(scope).unwrap();
matches!(obj.get(scope, is_thunk_sym.into()), Some(v) if v.is_true())
}
/// Check if a function is a primop
fn primop_name<'a, 'b>(
val: v8::Local<'a, v8::Value>,
ctx: &RuntimeCtx<'a, 'b>,
) -> Option<String> {
if !val.is_function() {
return None;
}
// Use cached PRIMOP_METADATA symbol from context
let primop_metadata_sym = ctx.primop_metadata_symbol?;
let scope = ctx.scope;
let obj = val.to_object(scope).unwrap();
if let Some(metadata) = obj.get(scope, primop_metadata_sym.into())
&& let Some(metadata_obj) = metadata.to_object(scope)
&& let Some(name_key) = v8::String::new(scope, "name")
&& let Some(name_val) = metadata_obj.get(scope, name_key.into())
{
Some(name_val.to_rust_string_lossy(scope))
} else {
None
}
}
/// Check if a primop is partially applied (has applied > 0)
fn primop_app_name<'a, 'b>(
val: v8::Local<'a, v8::Value>,
ctx: &RuntimeCtx<'a, 'b>,
) -> Option<String> {
let name = primop_name(val, ctx)?;
// Get cached PRIMOP_METADATA symbol
let primop_metadata_sym = ctx.primop_metadata_symbol?;
let scope = ctx.scope;
let obj = val.to_object(scope).unwrap();
if let Some(metadata) = obj.get(scope, primop_metadata_sym.into())
&& let Some(metadata_obj) = metadata.to_object(scope)
&& let Some(applied_key) = v8::String::new(scope, "applied")
&& let Some(applied_val) = metadata_obj.get(scope, applied_key.into())
&& let Some(applied_num) = applied_val.to_number(scope)
&& applied_num.value() > 0.0
{
Some(name)
} else {
None
}
}
#[test]
fn to_value_working() {
let mut ctx = Context::new();
assert_eq!(
run(
"({
test: [1., 9223372036854775807n, true, false, 'hello world!']
})"
.into(),
&mut ctx
)
.unwrap(),
Value::AttrSet(AttrSet::new(std::collections::BTreeMap::from([(
Symbol::from("test"),
Value::List(List::new(vec![
Value::Const(Const::Float(1.)),
Value::Const(Const::Int(9223372036854775807)),
Value::Const(Const::Bool(true)),
Value::Const(Const::Bool(false)),
Value::String("hello world!".to_string())
]))
)])))
);
}