diff --git a/Cargo.lock b/Cargo.lock index 73873a2..ed4630a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1053,6 +1053,7 @@ dependencies = [ "itertools 0.14.0", "mimalloc", "nix-js-macros", + "pin-project", "regex", "rnix", "rustyline", diff --git a/nix-js/Cargo.toml b/nix-js/Cargo.toml index 2d308f3..b4a63b3 100644 --- a/nix-js/Cargo.toml +++ b/nix-js/Cargo.toml @@ -17,6 +17,7 @@ derive_more = { version = "2", features = ["full"] } thiserror = "2" string-interner = "0.19" itertools = "0.14" +pin-project = "1" deno_core = "0.376" deno_error = "0.7" diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index 8cd276f..b49183d 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; +use std::pin::Pin; use std::ptr::NonNull; +use deno_core::JsRuntime; use hashbrown::HashMap; use itertools::Itertools as _; use string_interner::DefaultStringInterner; @@ -15,14 +17,55 @@ use downgrade::DowngradeCtx; mod downgrade; pub struct Context { + ctx: Pin>, + pub(crate) js_runtime: JsRuntime, +} + +impl Default for Context { + fn default() -> Self { + Self::new() + } +} + +impl Context { + pub fn new() -> Self { + let mut ctx = Box::pin(Ctx::new()); + let ptr = unsafe { NonNull::new_unchecked(Pin::get_unchecked_mut(ctx.as_mut())) }; + let js_runtime = crate::runtime::new_js_runtime(ptr); + + Self { ctx, js_runtime } + } + + pub fn eval_code(&mut self, expr: &str) -> Result { + // Initialize `path_stack` with current directory for relative path resolution + let mut guard = PathDropGuard::new_cwd(self.ctx.as_mut()); + let ctx = guard.as_ctx(); + + let root = rnix::Root::parse(expr); + if !root.errors().is_empty() { + return Err(Error::parse_error(root.errors().iter().join("; "))); + } + let root = ctx.as_mut().downgrade_ctx().downgrade(root.tree().expr().unwrap())?; + let code = ctx.get_ir(root).compile(Pin::get_ref(ctx.as_ref())); + let code = format!("Nix.force({})", code); + println!("[DEBUG] generated code: {}", &code); + crate::runtime::run(code, &mut self.js_runtime) + } +} + +#[pin_project::pin_project(PinnedDrop)] +pub struct Ctx { irs: Vec, symbols: DefaultStringInterner, global: NonNull>, path_stack: Vec, + #[pin] + _marker: std::marker::PhantomPinned, } -impl Drop for Context { - fn drop(&mut self) { +#[pin_project::pinned_drop] +impl PinnedDrop for Ctx { + fn drop(self: Pin<&mut Self>) { unsafe { drop(Box::from_raw(self.global.as_ptr())); } @@ -30,32 +73,32 @@ impl Drop for Context { } pub struct PathDropGuard<'ctx> { - ctx: &'ctx mut Context, + ctx: Pin<&'ctx mut Ctx>, } impl<'ctx> PathDropGuard<'ctx> { - pub fn new(path: PathBuf, ctx: &'ctx mut Context) -> Self { - ctx.path_stack.push(path); + pub fn new(path: PathBuf, mut ctx: Pin<&'ctx mut Ctx>) -> Self { + ctx.as_mut().project().path_stack.push(path); Self { ctx } } - pub fn new_cwd(ctx: &'ctx mut Context) -> Self { + pub fn new_cwd(mut ctx: Pin<&'ctx mut Ctx>) -> Self { let cwd = std::env::current_dir().unwrap(); let virtual_file = cwd.join("__eval__.nix"); - ctx.path_stack.push(virtual_file); + ctx.as_mut().project().path_stack.push(virtual_file); Self { ctx } } - pub fn as_ctx(&mut self) -> &mut Context { - self.ctx + pub fn as_ctx<'a>(&'a mut self) -> &'a mut Pin<&'ctx mut Ctx> { + &mut self.ctx } } impl Drop for PathDropGuard<'_> { fn drop(&mut self) { - self.ctx.path_stack.pop(); + self.ctx.as_mut().project().path_stack.pop(); } } -impl Default for Context { +impl Default for Ctx { fn default() -> Self { use crate::ir::{Attr, Builtins, Select, ToIr}; @@ -111,45 +154,33 @@ impl Default for Context { irs, global: unsafe { NonNull::new_unchecked(Box::leak(Box::new(global))) }, path_stack: Vec::new(), + _marker: std::marker::PhantomPinned, } } } -impl Context { +impl Ctx { pub fn new() -> Self { Self::default() } - pub fn downgrade_ctx<'a>(&'a mut self) -> DowngradeCtx<'a> { + pub fn downgrade_ctx<'a>(self: Pin<&'a mut Self>) -> DowngradeCtx<'a> { // SAFETY: `global` is readonly let global_ref = unsafe { self.global.as_ref() }; DowngradeCtx::new(self, global_ref) } - pub fn eval_code(&mut self, expr: &str) -> Result { - // Initialize `path_stack` with current directory for relative path resolution - let mut guard = PathDropGuard::new_cwd(self); - let ctx = guard.as_ctx(); - - let root = rnix::Root::parse(expr); - if !root.errors().is_empty() { - return Err(Error::parse_error(root.errors().iter().join("; "))); - } - let root = ctx - .downgrade_ctx() - .downgrade(root.tree().expr().unwrap())?; - let code = ctx.get_ir(root).compile(ctx); - let code = format!("Nix.force({})", code); - println!("[DEBUG] generated code: {}", &code); - crate::runtime::run(code, ctx) - } - pub fn get_current_dir(&self) -> PathBuf { - self.path_stack.last().unwrap().parent().unwrap().to_path_buf() + self.path_stack + .last() + .unwrap() + .parent() + .unwrap() + .to_path_buf() } } -impl CodegenContext for Context { +impl CodegenContext for Ctx { fn get_ir(&self, id: ExprId) -> &Ir { self.irs.get(id.0).unwrap() } @@ -436,7 +467,9 @@ mod test { fn test_builtin_in_with() { // Test builtins with 'with' expression assert_eq!( - Context::new().eval_code("with builtins; add 10 20").unwrap(), + Context::new() + .eval_code("with builtins; add 10 20") + .unwrap(), Value::Const(Const::Int(30)) ); } @@ -460,11 +493,15 @@ mod test { Value::Const(Const::Bool(true)) ); assert_eq!( - Context::new().eval_code("builtins.isAttrs { a = 1; }").unwrap(), + Context::new() + .eval_code("builtins.isAttrs { a = 1; }") + .unwrap(), Value::Const(Const::Bool(true)) ); assert_eq!( - Context::new().eval_code("builtins.isFunction (x: x)").unwrap(), + Context::new() + .eval_code("builtins.isFunction (x: x)") + .unwrap(), Value::Const(Const::Bool(true)) ); assert_eq!( @@ -553,7 +590,9 @@ mod test { fn test_free_global_shadowing() { // Test shadowing of free globals assert_eq!( - Context::new().eval_code("let true = false; in true").unwrap(), + Context::new() + .eval_code("let true = false; in true") + .unwrap(), Value::Const(Const::Bool(false)) ); assert_eq!( @@ -699,7 +738,10 @@ mod test { assert_eq!(ctx.eval_code("7 / 3").unwrap(), Value::Const(Const::Int(2))); - assert_eq!(ctx.eval_code("10 / 3").unwrap(), Value::Const(Const::Int(3))); + assert_eq!( + ctx.eval_code("10 / 3").unwrap(), + Value::Const(Const::Int(3)) + ); // Float division returns float assert_eq!( @@ -712,7 +754,10 @@ mod test { Value::Const(Const::Float(3.5)) ); - assert_eq!(ctx.eval_code("(-7) / 3").unwrap(), Value::Const(Const::Int(-2))); + assert_eq!( + ctx.eval_code("(-7) / 3").unwrap(), + Value::Const(Const::Int(-2)) + ); } #[test] diff --git a/nix-js/src/context/downgrade.rs b/nix-js/src/context/downgrade.rs index 851298b..bec8346 100644 --- a/nix-js/src/context/downgrade.rs +++ b/nix-js/src/context/downgrade.rs @@ -1,9 +1,11 @@ +use std::pin::Pin; + use hashbrown::HashMap; use crate::error::{Error, Result}; use crate::ir::{ArgId, Downgrade, DowngradeContext, ExprId, Ir, SymId, ToIr}; -use super::Context; +use super::Ctx; enum Scope<'ctx> { Global(&'ctx HashMap), @@ -29,14 +31,14 @@ impl<'a, 'ctx> ScopeGuard<'a, 'ctx> { } pub struct DowngradeCtx<'ctx> { - ctx: &'ctx mut Context, + ctx: Pin<&'ctx mut Ctx>, irs: Vec>, scopes: Vec>, arg_id: usize, } impl<'ctx> DowngradeCtx<'ctx> { - pub fn new(ctx: &'ctx mut Context, global: &'ctx HashMap) -> Self { + pub fn new(ctx: Pin<&'ctx mut Ctx>, global: &'ctx HashMap) -> Self { Self { scopes: vec![Scope::Global(global)], irs: vec![], @@ -59,7 +61,7 @@ impl DowngradeContext for DowngradeCtx<'_> { } fn new_sym(&mut self, sym: String) -> SymId { - self.ctx.symbols.get_or_intern(sym) + self.ctx.as_mut().project().symbols.get_or_intern(sym) } fn get_sym(&self, id: SymId) -> &str { @@ -133,6 +135,8 @@ impl DowngradeContext for DowngradeCtx<'_> { fn downgrade(mut self, root: rnix::ast::Expr) -> Result { let root = root.downgrade(&mut self)?; self.ctx + .as_mut() + .project() .irs .extend(self.irs.into_iter().map(Option::unwrap)); Ok(root) diff --git a/nix-js/src/ir/downgrade.rs b/nix-js/src/ir/downgrade.rs index d042847..e0b5e1d 100644 --- a/nix-js/src/ir/downgrade.rs +++ b/nix-js/src/ir/downgrade.rs @@ -94,14 +94,12 @@ impl Downgrade for ast::Path { parts_ast .into_iter() .map(|part| match part { - ast::InterpolPart::Literal(lit) => { - Ok(ctx.new_expr( - Str { - val: lit.to_string(), - } - .to_ir(), - )) - } + ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr( + Str { + val: lit.to_string(), + } + .to_ir(), + )), ast::InterpolPart::Interpolation(interpol) => { interpol.expr().unwrap().downgrade(ctx) } diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 4a59b5d..f8823dc 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -1,15 +1,14 @@ use std::borrow::Cow; use std::cell::RefCell; +use std::pin::Pin; use std::ptr::NonNull; use std::sync::Once; -use deno_core::{ - Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8 -}; +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::context::{Ctx, PathDropGuard}; use crate::error::{Error, Result}; use crate::ir::DowngradeContext; use crate::value::{AttrSet, Const, List, Symbol, Value}; @@ -18,12 +17,10 @@ pub trait RuntimeContext { fn split(&mut self) -> (&mut JsRuntime, &mut Ctx); } -fn nix_runtime(ctx: &mut Context) -> Extension { +fn nix_runtime(ctx: NonNull) -> 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), @@ -35,7 +32,7 @@ fn nix_runtime(ctx: &mut Context) -> Extension { op_resolve_path(), ]), op_state_fn: Some(Box::new(move |state| { - state.put(RefCell::new(ptr)); + state.put(RefCell::new(ctx)); })), enabled: true, ..Default::default() @@ -74,28 +71,22 @@ js_error_wrapper!(SimpleErrorWrapper, NixError, "EvalError"); #[deno_core::op2] #[string] fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result { - let mut ptr = state.borrow::>>().borrow_mut(); - let ctx = unsafe { ptr.as_mut() }; + let mut ptr = state.borrow::>>().borrow_mut(); + let ctx = unsafe { Pin::new_unchecked(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() - })?; + .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!( @@ -106,18 +97,17 @@ fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result .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 + .as_mut() .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)) + Ok(ctx.get_ir(expr_id).compile(Pin::get_ref(ctx.as_ref()))) } #[deno_core::op2] @@ -134,8 +124,11 @@ fn op_path_exists(#[string] path: String) -> bool { #[deno_core::op2] #[string] -fn op_resolve_path(state: &mut OpState, #[string] path: String) -> std::result::Result { - let ptr = state.borrow::>>().borrow(); +fn op_resolve_path( + state: &mut OpState, + #[string] path: String, +) -> std::result::Result { + let ptr = state.borrow::>>().borrow(); let ctx = unsafe { ptr.as_ref() }; // If already absolute, return as-is @@ -206,7 +199,7 @@ impl<'a, 'b> RuntimeCtx<'a, 'b> { } } -pub fn new_js_runtime(ctx: &mut Context) -> JsRuntime { +pub fn new_js_runtime(ctx: NonNull) -> JsRuntime { // Initialize V8 once static INIT: Once = Once::new(); INIT.call_once(|| { @@ -223,9 +216,7 @@ pub fn new_js_runtime(ctx: &mut Context) -> JsRuntime { } // Main entry point -pub fn run(script: String, ctx: &mut Context) -> Result { - let mut runtime = new_js_runtime(ctx); - +pub fn run(script: String, runtime: &mut JsRuntime) -> Result { // Execute user script let global_value = runtime .execute_script("", script) @@ -322,10 +313,7 @@ fn is_thunk<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeCtx<'a, 'b>) -> } /// Check if a function is a primop -fn primop_name<'a, 'b>( - val: v8::Local<'a, v8::Value>, - ctx: &RuntimeCtx<'a, 'b>, -) -> Option { +fn primop_name<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeCtx<'a, 'b>) -> Option { if !val.is_function() { return None; } @@ -373,27 +361,33 @@ fn primop_app_name<'a, 'b>( } } -#[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()) - ])) - )]))) - ); +#[cfg(test)] +mod test { + use super::*; + use crate::context::Context; + + #[test] + fn to_value_working() { + let mut ctx = Context::new(); + assert_eq!( + run( + "({ + test: [1., 9223372036854775807n, true, false, 'hello world!'] + })" + .into(), + &mut ctx.js_runtime + ) + .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()) + ])) + )]))) + ); + } }