298 lines
9.9 KiB
Rust
298 lines
9.9 KiB
Rust
use std::cell::RefCell;
|
|
use std::sync::Once;
|
|
|
|
use crate::error::{Error, Result};
|
|
use crate::value::{AttrSet, Const, List, Symbol, Value};
|
|
|
|
static INIT: Once = Once::new();
|
|
|
|
thread_local! {
|
|
static ISOLATE: RefCell<v8::OwnedIsolate> =
|
|
RefCell::new(v8::Isolate::new(Default::default()));
|
|
}
|
|
|
|
pub fn run(script: &str) -> Result<Value> {
|
|
INIT.call_once(|| {
|
|
v8::V8::initialize_platform(v8::new_default_platform(0, false).make_shared());
|
|
v8::V8::initialize();
|
|
});
|
|
|
|
ISOLATE.with_borrow_mut(|isolate| run_impl(script, isolate))
|
|
}
|
|
|
|
struct RuntimeContext<'a, 'b> {
|
|
scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>,
|
|
is_thunk_symbol: Option<v8::Local<'a, v8::Symbol>>,
|
|
is_primop_symbol: Option<v8::Local<'a, v8::Symbol>>,
|
|
}
|
|
|
|
impl<'a, 'b> RuntimeContext<'a, 'b> {
|
|
fn new(scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>) -> Self {
|
|
let is_thunk_symbol = Self::get_is_thunk_symbol(scope);
|
|
let is_primop_symbol = Self::get_is_primop_symbol(scope);
|
|
Self {
|
|
scope,
|
|
is_thunk_symbol,
|
|
is_primop_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_is_primop_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_primop_sym_key = v8::String::new(scope, "IS_PRIMOP")?;
|
|
let is_primop_sym = nix_obj.get(scope, is_primop_sym_key.into())?;
|
|
|
|
if is_primop_sym.is_symbol() {
|
|
is_primop_sym.try_cast().ok()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
fn run_impl(script: &str, isolate: &mut v8::Isolate) -> Result<Value> {
|
|
let handle_scope = std::pin::pin!(v8::HandleScope::new(isolate));
|
|
let handle_scope = &mut handle_scope.init();
|
|
let context = v8::Context::new(handle_scope, v8::ContextOptions::default());
|
|
let scope = &mut v8::ContextScope::new(handle_scope, context);
|
|
|
|
let runtime_code = include_str!("../runtime-ts/dist/runtime.js");
|
|
let runtime_source = v8::String::new(scope, runtime_code).unwrap();
|
|
let runtime_script = v8::Script::compile(scope, runtime_source, None).unwrap();
|
|
|
|
if runtime_script.run(scope).is_none() {
|
|
return Err(Error::eval_error(
|
|
"Failed to initialize runtime".to_string(),
|
|
));
|
|
}
|
|
|
|
let source = v8::String::new(scope, script).unwrap();
|
|
|
|
// Use TryCatch to capture JavaScript exceptions
|
|
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
|
|
let try_catch = &mut try_catch.init();
|
|
let script = match v8::Script::compile(try_catch, source, None) {
|
|
Some(script) => script,
|
|
None => {
|
|
if let Some(exception) = try_catch.exception() {
|
|
let exception_string = exception
|
|
.to_string(try_catch)
|
|
.unwrap()
|
|
.to_rust_string_lossy(try_catch);
|
|
return Err(Error::eval_error(format!(
|
|
"Compilation error: {}",
|
|
exception_string
|
|
)));
|
|
} else {
|
|
return Err(Error::eval_error("Unknown compilation error".to_string()));
|
|
}
|
|
}
|
|
};
|
|
|
|
match script.run(try_catch) {
|
|
Some(result) => {
|
|
// Initialize runtime context once before conversion
|
|
let ctx = RuntimeContext::new(try_catch);
|
|
Ok(to_value(result, &ctx))
|
|
}
|
|
None => {
|
|
if let Some(exception) = try_catch.exception() {
|
|
let exception_string = exception
|
|
.to_string(try_catch)
|
|
.unwrap()
|
|
.to_rust_string_lossy(try_catch);
|
|
Err(Error::eval_error(format!(
|
|
"Runtime error: {}",
|
|
exception_string
|
|
)))
|
|
} else {
|
|
Err(Error::eval_error("Unknown runtime error".to_string()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn to_value<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'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();
|
|
// Heuristic: convert whole numbers to Int (for backward compatibility and JS interop)
|
|
if val.is_finite()
|
|
&& val.fract() == 0.0
|
|
&& val >= i64::MIN as f64
|
|
&& val <= i64::MAX as f64
|
|
{
|
|
Value::Const(Const::Int(val as i64))
|
|
} else {
|
|
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))
|
|
}
|
|
_ => todo!("{}", val.type_repr()),
|
|
}
|
|
}
|
|
|
|
fn is_thunk<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'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: &RuntimeContext<'a, 'b>,
|
|
) -> Option<String> {
|
|
if !val.is_function() {
|
|
return None;
|
|
}
|
|
|
|
// Use cached IS_PRIMOP symbol from context
|
|
let is_primop_sym = ctx.is_primop_symbol?;
|
|
|
|
let scope = ctx.scope;
|
|
let obj = val.to_object(scope).unwrap();
|
|
|
|
if let Some(metadata) = obj.get(scope, is_primop_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: &RuntimeContext<'a, 'b>,
|
|
) -> Option<String> {
|
|
let name = primop_name(val, ctx)?;
|
|
|
|
// Get cached IS_PRIMOP symbol
|
|
let is_primop_sym = ctx.is_primop_symbol?;
|
|
|
|
let scope = ctx.scope;
|
|
let obj = val.to_object(scope).unwrap();
|
|
|
|
if let Some(metadata) = obj.get(scope, is_primop_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() {
|
|
assert_eq!(
|
|
run("({
|
|
test: [1, 9223372036854775807n, true, false, 'hello world!']
|
|
})")
|
|
.unwrap(),
|
|
Value::AttrSet(AttrSet::new(std::collections::BTreeMap::from([(
|
|
Symbol::from("test"),
|
|
Value::List(List::new(vec![
|
|
Value::Const(Const::Int(1)),
|
|
Value::Const(Const::Int(9223372036854775807)),
|
|
Value::Const(Const::Bool(true)),
|
|
Value::Const(Const::Bool(false)),
|
|
Value::String("hello world!".to_string())
|
|
]))
|
|
)])))
|
|
);
|
|
}
|