feat: persist JsRuntime

This commit is contained in:
2026-01-05 17:58:48 +08:00
parent c43d796dc0
commit c9455bd0a8
6 changed files with 147 additions and 104 deletions

1
Cargo.lock generated
View File

@@ -1053,6 +1053,7 @@ dependencies = [
"itertools 0.14.0",
"mimalloc",
"nix-js-macros",
"pin-project",
"regex",
"rnix",
"rustyline",

View File

@@ -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"

View File

@@ -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]

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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() {
]))
)])))
);
}
}