feat: persist JsRuntime
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1053,6 +1053,7 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"mimalloc",
|
||||
"nix-js-macros",
|
||||
"pin-project",
|
||||
"regex",
|
||||
"rnix",
|
||||
"rustyline",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<Box<Ctx>>,
|
||||
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<Value> {
|
||||
// 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<Ir>,
|
||||
symbols: DefaultStringInterner,
|
||||
global: NonNull<HashMap<SymId, ExprId>>,
|
||||
path_stack: Vec<PathBuf>,
|
||||
#[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<Value> {
|
||||
// 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]
|
||||
|
||||
@@ -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<SymId, ExprId>),
|
||||
@@ -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<Option<Ir>>,
|
||||
scopes: Vec<Scope<'ctx>>,
|
||||
arg_id: usize,
|
||||
}
|
||||
|
||||
impl<'ctx> DowngradeCtx<'ctx> {
|
||||
pub fn new(ctx: &'ctx mut Context, global: &'ctx HashMap<SymId, ExprId>) -> Self {
|
||||
pub fn new(ctx: Pin<&'ctx mut Ctx>, global: &'ctx HashMap<SymId, ExprId>) -> 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<ExprId> {
|
||||
let root = root.downgrade(&mut self)?;
|
||||
self.ctx
|
||||
.as_mut()
|
||||
.project()
|
||||
.irs
|
||||
.extend(self.irs.into_iter().map(Option::unwrap));
|
||||
Ok(root)
|
||||
|
||||
@@ -94,14 +94,12 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path {
|
||||
parts_ast
|
||||
.into_iter()
|
||||
.map(|part| match part {
|
||||
ast::InterpolPart::Literal(lit) => {
|
||||
Ok(ctx.new_expr(
|
||||
ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr(
|
||||
Str {
|
||||
val: lit.to_string(),
|
||||
}
|
||||
.to_ir(),
|
||||
))
|
||||
}
|
||||
)),
|
||||
ast::InterpolPart::Interpolation(interpol) => {
|
||||
interpol.expr().unwrap().downgrade(ctx)
|
||||
}
|
||||
|
||||
@@ -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<Ctx: DowngradeContext>(&mut self) -> (&mut JsRuntime, &mut Ctx);
|
||||
}
|
||||
|
||||
fn nix_runtime(ctx: &mut Context) -> Extension {
|
||||
fn nix_runtime(ctx: NonNull<Ctx>) -> 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<String, NixError> {
|
||||
let mut ptr = state.borrow::<RefCell<NonNull<Context>>>().borrow_mut();
|
||||
let ctx = unsafe { ptr.as_mut() };
|
||||
let mut ptr = state.borrow::<RefCell<NonNull<Ctx>>>().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<String, NixError> {
|
||||
let ptr = state.borrow::<RefCell<NonNull<Context>>>().borrow();
|
||||
fn op_resolve_path(
|
||||
state: &mut OpState,
|
||||
#[string] path: String,
|
||||
) -> std::result::Result<String, NixError> {
|
||||
let ptr = state.borrow::<RefCell<NonNull<Ctx>>>().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<Ctx>) -> 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<Value> {
|
||||
let mut runtime = new_js_runtime(ctx);
|
||||
|
||||
pub fn run(script: String, runtime: &mut JsRuntime) -> Result<Value> {
|
||||
// Execute user script
|
||||
let global_value = runtime
|
||||
.execute_script("<eval>", 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<String> {
|
||||
fn primop_name<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeCtx<'a, 'b>) -> Option<String> {
|
||||
if !val.is_function() {
|
||||
return None;
|
||||
}
|
||||
@@ -373,8 +361,13 @@ fn primop_app_name<'a, 'b>(
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_value_working() {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::context::Context;
|
||||
|
||||
#[test]
|
||||
fn to_value_working() {
|
||||
let mut ctx = Context::new();
|
||||
assert_eq!(
|
||||
run(
|
||||
@@ -382,7 +375,7 @@ fn to_value_working() {
|
||||
test: [1., 9223372036854775807n, true, false, 'hello world!']
|
||||
})"
|
||||
.into(),
|
||||
&mut ctx
|
||||
&mut ctx.js_runtime
|
||||
)
|
||||
.unwrap(),
|
||||
Value::AttrSet(AttrSet::new(std::collections::BTreeMap::from([(
|
||||
@@ -396,4 +389,5 @@ fn to_value_working() {
|
||||
]))
|
||||
)])))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user