refactor: avoid global state
This commit is contained in:
@@ -1,81 +1,47 @@
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::Once;
|
||||
|
||||
use deno_core::{JsRuntime, RuntimeOptions};
|
||||
use deno_core::{
|
||||
Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8
|
||||
};
|
||||
use deno_error::js_error_wrapper;
|
||||
|
||||
use crate::codegen::{CodegenContext, Compile};
|
||||
use crate::context::Context;
|
||||
use crate::context::{Context, PathDropGuard};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ir::DowngradeContext;
|
||||
use crate::value::{AttrSet, Const, List, Symbol, Value};
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
thread_local! {
|
||||
static CONTEXT_HOLDER: RefCell<Option<NonNull<Context>>> = const { RefCell::new(None) };
|
||||
pub trait RuntimeContext {
|
||||
fn split<Ctx: DowngradeContext>(&mut self) -> (&mut JsRuntime, &mut Ctx);
|
||||
}
|
||||
|
||||
// for relative path resolution
|
||||
thread_local! {
|
||||
pub(crate) static IMPORT_PATH_STACK: RefCell<Vec<PathBuf>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
fn nix_runtime(ctx: &mut Context) -> Extension {
|
||||
const ESM: &[ExtensionFileSource] =
|
||||
&deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js");
|
||||
|
||||
struct ContextGuard;
|
||||
|
||||
impl ContextGuard {
|
||||
fn set(ctx: &mut Context) -> Self {
|
||||
CONTEXT_HOLDER.with(|holder| {
|
||||
let ptr = NonNull::new(ctx as *mut Context).unwrap();
|
||||
*holder.borrow_mut() = Some(ptr);
|
||||
});
|
||||
Self
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ContextGuard {
|
||||
fn drop(&mut self) {
|
||||
CONTEXT_HOLDER.with(|holder| {
|
||||
*holder.borrow_mut() = None;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImportPathGuard;
|
||||
|
||||
impl ImportPathGuard {
|
||||
pub fn push_cwd() -> Self {
|
||||
// Push a virtual file path in cwd so .parent() returns cwd
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let virtual_file = cwd.join("__eval__.nix");
|
||||
IMPORT_PATH_STACK.with(|stack| stack.borrow_mut().push(virtual_file));
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn push(path: PathBuf) -> Self {
|
||||
IMPORT_PATH_STACK.with(|stack| stack.borrow_mut().push(path));
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ImportPathGuard {
|
||||
fn drop(&mut self) {
|
||||
IMPORT_PATH_STACK.with(|stack| stack.borrow_mut().pop());
|
||||
}
|
||||
}
|
||||
|
||||
// injects to Deno.core.ops
|
||||
deno_core::extension!(
|
||||
nix_ops,
|
||||
ops = [op_import, op_read_file, op_path_exists, op_resolve_path]
|
||||
);
|
||||
|
||||
fn nix_extension() -> deno_core::Extension {
|
||||
nix_ops::init()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SimpleErrorWrapper(pub String);
|
||||
|
||||
@@ -107,62 +73,51 @@ js_error_wrapper!(SimpleErrorWrapper, NixError, "EvalError");
|
||||
|
||||
#[deno_core::op2]
|
||||
#[string]
|
||||
fn op_import(#[string] path: String) -> std::result::Result<String, NixError> {
|
||||
CONTEXT_HOLDER.with(|holder| {
|
||||
let mut ptr = holder
|
||||
.borrow()
|
||||
.ok_or_else(|| -> NixError { "No context available".to_string().into() })?;
|
||||
let ctx = unsafe { ptr.as_mut() };
|
||||
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 = IMPORT_PATH_STACK.with(|stack| {
|
||||
stack
|
||||
.borrow()
|
||||
.last()
|
||||
.and_then(|p| p.parent())
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_else(|| std::env::current_dir().unwrap())
|
||||
});
|
||||
|
||||
let absolute_path = current_dir
|
||||
.join(&path)
|
||||
.canonicalize()
|
||||
.map_err(|e| -> NixError {
|
||||
format!("Failed to resolve path {}: {}", path, e).into()
|
||||
})?;
|
||||
|
||||
// 2. Push to stack for nested imports (RAII guard ensures pop on drop)
|
||||
let _guard = ImportPathGuard::push(absolute_path.clone());
|
||||
|
||||
// 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()
|
||||
// 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()
|
||||
})?;
|
||||
|
||||
// 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());
|
||||
}
|
||||
// 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();
|
||||
|
||||
// 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() })?;
|
||||
// 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()
|
||||
})?;
|
||||
|
||||
// 6. Codegen - returns JS code string
|
||||
Ok(ctx.get_ir(expr_id).compile(ctx))
|
||||
})
|
||||
// 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]
|
||||
@@ -179,21 +134,17 @@ fn op_path_exists(#[string] path: String) -> bool {
|
||||
|
||||
#[deno_core::op2]
|
||||
#[string]
|
||||
fn op_resolve_path(#[string] path: String) -> std::result::Result<String, NixError> {
|
||||
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 = IMPORT_PATH_STACK.with(|stack| {
|
||||
stack
|
||||
.borrow()
|
||||
.last()
|
||||
.and_then(|p| p.parent())
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_else(|| std::env::current_dir().unwrap())
|
||||
});
|
||||
let current_dir = ctx.get_current_dir();
|
||||
|
||||
current_dir
|
||||
.join(&path)
|
||||
@@ -203,13 +154,13 @@ fn op_resolve_path(#[string] path: String) -> std::result::Result<String, NixErr
|
||||
}
|
||||
|
||||
// Runtime context for V8 value conversion
|
||||
struct RuntimeContext<'a, 'b> {
|
||||
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> RuntimeContext<'a, 'b> {
|
||||
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);
|
||||
@@ -255,11 +206,9 @@ impl<'a, 'b> RuntimeContext<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
pub fn run(script: String, ctx: &mut Context) -> Result<Value> {
|
||||
let _guard = ContextGuard::set(ctx);
|
||||
|
||||
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()),
|
||||
@@ -267,31 +216,30 @@ pub fn run(script: String, ctx: &mut Context) -> Result<Value> {
|
||||
);
|
||||
});
|
||||
|
||||
// Create a new JsRuntime for each evaluation to avoid state issues
|
||||
let mut runtime = JsRuntime::new(RuntimeOptions {
|
||||
extensions: vec![nix_extension()],
|
||||
JsRuntime::new(RuntimeOptions {
|
||||
extensions: vec![nix_runtime(ctx)],
|
||||
..Default::default()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Load runtime.js
|
||||
let runtime_code = include_str!("../runtime-ts/dist/runtime.js");
|
||||
runtime
|
||||
.execute_script("<runtime>", runtime_code)
|
||||
.map_err(|e| Error::eval_error(format!("Failed to load runtime: {:?}", e)))?;
|
||||
// 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 = RuntimeContext::new(scope);
|
||||
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: &RuntimeContext<'a, 'b>) -> Value {
|
||||
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() => {
|
||||
@@ -357,7 +305,7 @@ fn to_value<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_thunk<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>) -> bool {
|
||||
fn is_thunk<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeCtx<'a, 'b>) -> bool {
|
||||
if !val.is_object() {
|
||||
return false;
|
||||
}
|
||||
@@ -376,7 +324,7 @@ fn is_thunk<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>)
|
||||
/// Check if a function is a primop
|
||||
fn primop_name<'a, 'b>(
|
||||
val: v8::Local<'a, v8::Value>,
|
||||
ctx: &RuntimeContext<'a, 'b>,
|
||||
ctx: &RuntimeCtx<'a, 'b>,
|
||||
) -> Option<String> {
|
||||
if !val.is_function() {
|
||||
return None;
|
||||
@@ -402,7 +350,7 @@ fn primop_name<'a, 'b>(
|
||||
/// 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>,
|
||||
ctx: &RuntimeCtx<'a, 'b>,
|
||||
) -> Option<String> {
|
||||
let name = primop_name(val, ctx)?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user