Files
nix-js/nix-js/src/runtime.rs
2026-01-10 10:28:48 +08:00

380 lines
12 KiB
Rust

use std::borrow::Cow;
use std::pin::Pin;
use std::sync::Once;
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpDecl, OpState, RuntimeOptions, v8};
use crate::codegen::{CodegenContext, Compile};
use crate::context::{CtxPtr, PathDropGuard};
use crate::error::{Error, Result};
use crate::ir::DowngradeContext;
use crate::value::{AttrSet, List, Symbol, Value};
type ScopeRef<'p, 's> = v8::PinnedRef<'p, v8::HandleScope<'s>>;
type LocalValue<'a> = v8::Local<'a, v8::Value>;
type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>;
fn runtime_extension(ctx: CtxPtr) -> Extension {
const ESM: &[ExtensionFileSource] =
&deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js");
const OPS: &[OpDecl] = &[
op_import(),
op_read_file(),
op_path_exists(),
op_resolve_path(),
];
Extension {
name: "nix_runtime",
esm_files: Cow::Borrowed(ESM),
esm_entry_point: Some("ext:nix_runtime/runtime.js"),
ops: Cow::Borrowed(OPS),
op_state_fn: Some(Box::new(move |state| {
state.put(ctx);
})),
enabled: true,
..Default::default()
}
}
mod private {
use deno_error::js_error_wrapper;
#[allow(dead_code)]
#[derive(Debug)]
pub struct SimpleErrorWrapper(pub(crate) 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 {}
js_error_wrapper!(SimpleErrorWrapper, NixError, "EvalError");
impl From<String> for NixError {
fn from(value: String) -> Self {
NixError(SimpleErrorWrapper(value))
}
}
impl From<&str> for NixError {
fn from(value: &str) -> Self {
NixError(SimpleErrorWrapper(value.to_string()))
}
}
}
use private::NixError;
#[deno_core::op2]
#[string]
fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result<String, NixError> {
let ptr = state.borrow_mut::<CtxPtr>();
let ctx = unsafe { ptr.as_mut() };
let current_dir = ctx.get_current_dir();
let absolute_path = current_dir
.join(&path)
.canonicalize()
.map_err(|e| format!("Failed to resolve path {}: {}", path, e))?;
let mut guard = PathDropGuard::new(absolute_path.clone(), ctx);
let ctx = guard.as_ctx();
let content = std::fs::read_to_string(&absolute_path)
.map_err(|e| format!("Failed to read {}: {}", absolute_path.display(), e))?;
let root = rnix::Root::parse(&content);
if !root.errors().is_empty() {
return Err(format!(
"Parse error in {}: {:?}",
absolute_path.display(),
root.errors()
)
.into());
}
let expr = root.tree().expr().ok_or("No expression in file")?;
let expr_id = ctx
.as_mut()
.downgrade_ctx()
.downgrade(expr)
.map_err(|e| format!("Downgrade error: {}", e))?;
Ok(ctx.get_ir(expr_id).compile(Pin::get_ref(ctx.as_ref())))
}
#[deno_core::op2]
#[string]
fn op_read_file(#[string] path: String) -> std::result::Result<String, NixError> {
Ok(std::fs::read_to_string(&path).map_err(|e| format!("Failed to read {}: {}", path, e))?)
}
#[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::<CtxPtr>();
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();
Ok(current_dir
.join(&path)
.canonicalize()
.map(|p| p.to_string_lossy().to_string())
.map_err(|e| format!("Failed to resolve path {}: {}", path, e))?)
}
pub(crate) struct Runtime {
js_runtime: JsRuntime,
is_thunk_symbol: v8::Global<v8::Symbol>,
primop_metadata_symbol: v8::Global<v8::Symbol>,
}
impl Runtime {
pub(crate) fn new(ctx: CtxPtr) -> Result<Self> {
// Initialize V8 once
static INIT: Once = Once::new();
INIT.call_once(|| {
JsRuntime::init_platform(
Some(v8::new_default_platform(0, false).make_shared()),
false,
);
});
let mut js_runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![runtime_extension(ctx)],
..Default::default()
});
let (is_thunk_symbol, primop_metadata_symbol) = {
deno_core::scope!(scope, &mut js_runtime);
Self::get_symbols(scope)?
};
Ok(Self {
js_runtime,
is_thunk_symbol,
primop_metadata_symbol,
})
}
pub(crate) fn eval(&mut self, script: String) -> Result<Value> {
let global_value = self
.js_runtime
.execute_script("<eval>", script)
.map_err(|e| Error::eval_error(format!("Execution error: {:?}", e)))?;
// Retrieve scope from JsRuntime
deno_core::scope!(scope, self.js_runtime);
let local_value = v8::Local::new(scope, &global_value);
let is_thunk_symbol = v8::Local::new(scope, &self.is_thunk_symbol);
let primop_metadata_symbol = v8::Local::new(scope, &self.primop_metadata_symbol);
Ok(to_value(
local_value,
scope,
is_thunk_symbol,
primop_metadata_symbol,
))
}
/// get (IS_THUNK, PRIMOP_METADATA)
fn get_symbols(scope: &ScopeRef) -> Result<(v8::Global<v8::Symbol>, v8::Global<v8::Symbol>)> {
let global = scope.get_current_context().global(scope);
let nix_key = v8::String::new(scope, "Nix")
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let nix_obj = global
.get(scope, nix_key.into())
.ok_or_else(|| Error::internal("failed to get global Nix object".into()))?
.to_object(scope)
.ok_or_else(|| {
Error::internal("failed to convert global Nix Value to object".into())
})?;
let is_thunk_sym_key = v8::String::new(scope, "IS_THUNK")
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let is_thunk_sym = nix_obj
.get(scope, is_thunk_sym_key.into())
.ok_or_else(|| Error::internal("failed to get IS_THUNK Symbol".into()))?;
let is_thunk = is_thunk_sym.try_cast::<v8::Symbol>().map_err(|err| {
Error::internal(format!(
"failed to convert IS_THUNK Value to Symbol ({err})"
))
})?;
let is_thunk = v8::Global::new(scope, is_thunk);
let primop_metadata_sym_key = v8::String::new(scope, "PRIMOP_METADATA")
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
let primop_metadata_sym = nix_obj
.get(scope, primop_metadata_sym_key.into())
.ok_or_else(|| Error::internal("failed to get PRIMOP_METADATA Symbol".into()))?;
let primop_metadata = primop_metadata_sym
.try_cast::<v8::Symbol>()
.map_err(|err| {
Error::internal(format!(
"failed to convert PRIMOP_METADATA Value to Symbol ({err})"
))
})?;
let primop_metadata = v8::Global::new(scope, primop_metadata);
Ok((is_thunk, primop_metadata))
}
}
fn to_value<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,
is_thunk_symbol: LocalSymbol<'a>,
primop_metadata_symbol: LocalSymbol<'a>,
) -> Value {
match () {
_ if val.is_big_int() => {
let (val, lossless) = val
.to_big_int(scope)
.expect("infallible conversion")
.i64_value();
if !lossless {
panic!("BigInt value out of i64 range: conversion lost precision");
}
Value::Int(val)
}
_ if val.is_number() => {
let val = val.to_number(scope).expect("infallible conversion").value();
// number is always NixFloat
Value::Float(val)
}
_ if val.is_true() => Value::Bool(true),
_ if val.is_false() => Value::Bool(false),
_ if val.is_null() => Value::Null,
_ if val.is_string() => {
let val = val.to_string(scope).expect("infallible conversion");
Value::String(val.to_rust_string_lossy(scope))
}
_ if val.is_array() => {
let val = val.try_cast::<v8::Array>().expect("infallible conversion");
let len = val.length();
let list = (0..len)
.map(|i| {
let val = val.get_index(scope, i).expect("infallible index operation");
to_value(val, scope, is_thunk_symbol, primop_metadata_symbol)
})
.collect();
Value::List(List::new(list))
}
_ if val.is_function() => {
if let Some(primop) = to_primop(val, scope, primop_metadata_symbol) {
primop
} else {
Value::Func
}
}
_ if val.is_object() => {
if is_thunk(val, scope, is_thunk_symbol) {
return Value::Thunk;
}
let val = val.to_object(scope).expect("infallible conversion");
let keys = val
.get_own_property_names(scope, v8::GetPropertyNamesArgsBuilder::new().build())
.expect("infallible operation");
let len = keys.length();
let attrs = (0..len)
.map(|i| {
let key = keys
.get_index(scope, i)
.expect("infallible index operation");
let val = val.get(scope, key).expect("infallible operation");
let key = key.to_rust_string_lossy(scope);
(
Symbol::new(key),
to_value(val, scope, is_thunk_symbol, primop_metadata_symbol),
)
})
.collect();
Value::AttrSet(AttrSet::new(attrs))
}
_ => unimplemented!("can not convert {} to NixValue", val.type_repr()),
}
}
fn is_thunk<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymbol<'a>) -> bool {
if !val.is_object() {
return false;
}
let obj = val.to_object(scope).expect("infallible conversion");
matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true())
}
fn to_primop<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,
symbol: LocalSymbol<'a>,
) -> Option<Value> {
if !val.is_function() {
return None;
}
let obj = val.to_object(scope).expect("infallible conversion");
let metadata = obj.get(scope, symbol.into())?.to_object(scope)?;
let name_key = v8::String::new(scope, "name")?;
let name = metadata
.get(scope, name_key.into())?
.to_rust_string_lossy(scope);
let applied_key = v8::String::new(scope, "applied")?;
let applied_val = metadata.get(scope, applied_key.into())?;
let applied = applied_val.to_number(scope)?.value();
if applied == 0.0 {
Some(Value::PrimOp(name))
} else {
Some(Value::PrimOpApp(name))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use super::*;
use crate::context::Context;
#[test]
fn to_value_working() {
let mut ctx = Context::new().unwrap();
assert_eq!(
ctx.eval_js(
"({
test: [1., 9223372036854775807n, true, false, 'hello world!']
})"
.into(),
)
.unwrap(),
Value::AttrSet(AttrSet::new(std::collections::BTreeMap::from([(
Symbol::from("test"),
Value::List(List::new(vec![
Value::Float(1.),
Value::Int(9223372036854775807),
Value::Bool(true),
Value::Bool(false),
Value::String("hello world!".to_string())
]))
)])))
);
}
}