refactor: avoid Pin hack

This commit is contained in:
2026-01-10 11:50:28 +08:00
parent fdda1ae682
commit 36ccc735f9
8 changed files with 158 additions and 134 deletions

1
Cargo.lock generated
View File

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

View File

@@ -12,12 +12,11 @@ anyhow = "1.0"
rustyline = "14.0" rustyline = "14.0"
derive_more = { version = "2", features = ["full"] } derive_more = { version = "2", features = ["full"] }
pin-project = "1" thiserror = "2"
hashbrown = "0.16" hashbrown = "0.16"
string-interner = "0.19" string-interner = "0.19"
thiserror = "2"
itertools = "0.14" itertools = "0.14"
regex = "1.11" regex = "1.11"

View File

@@ -1,5 +1,4 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::pin::Pin;
use std::ptr::NonNull; use std::ptr::NonNull;
use hashbrown::HashMap; use hashbrown::HashMap;
@@ -9,109 +8,87 @@ use string_interner::DefaultStringInterner;
use crate::codegen::{CodegenContext, Compile}; use crate::codegen::{CodegenContext, Compile};
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::ir::{Builtin, DowngradeContext, ExprId, Ir, SymId}; use crate::ir::{Builtin, DowngradeContext, ExprId, Ir, SymId};
use crate::runtime::Runtime; use crate::runtime::{Runtime, RuntimeCtx};
use crate::value::Value; use crate::value::Value;
use downgrade::DowngradeCtx; use downgrade::DowngradeCtx;
use drop_guard::{PathDropGuard, PathStackProvider};
mod downgrade; mod downgrade;
mod drop_guard;
mod private {
use super::*;
use std::ops::DerefMut;
use std::ptr::NonNull;
pub struct CtxPtr(NonNull<Ctx>);
impl CtxPtr {
pub fn new(ctx: &mut Ctx) -> Self {
unsafe { CtxPtr(NonNull::new_unchecked(ctx)) }
}
fn as_ref(&self) -> &Ctx {
// SAFETY: This is safe since inner `NonNull<Ctx>` is obtained from `&mut Ctx`
unsafe { self.0.as_ref() }
}
fn as_mut(&mut self) -> &mut Ctx {
// SAFETY: This is safe since inner `NonNull<Ctx>` is obtained from `&mut Ctx`
unsafe { self.0.as_mut() }
}
}
impl PathStackProvider for CtxPtr {
fn path_stack(&mut self) -> &mut Vec<PathBuf> {
&mut self.as_mut().path_stack
}
}
impl RuntimeCtx for CtxPtr {
fn get_current_dir(&self) -> PathBuf {
self.as_ref().get_current_dir()
}
fn push_path_stack(&mut self, path: PathBuf) -> impl DerefMut<Target = Self> {
PathDropGuard::new(path, self)
}
fn compile_code(&mut self, expr: &str) -> Result<String> {
self.as_mut().compile_code(expr)
}
}
}
use private::CtxPtr;
pub struct Context { pub struct Context {
ctx: Pin<Box<Ctx>>, ctx: Ctx,
runtime: Runtime, runtime: Runtime<CtxPtr>,
}
pub(crate) struct CtxPtr(NonNull<Ctx>);
impl CtxPtr {
pub(crate) unsafe fn as_ref(&self) -> &Ctx {
unsafe { self.0.as_ref() }
}
pub(crate) unsafe fn as_mut(&mut self) -> Pin<&mut Ctx> {
unsafe { Pin::new_unchecked(self.0.as_mut()) }
}
} }
impl Context { impl Context {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let mut ctx = Box::pin(Ctx::new()); let ctx = Ctx::new();
let ptr = unsafe { CtxPtr(NonNull::new_unchecked(Pin::get_unchecked_mut(ctx.as_mut()))) }; let runtime = Runtime::new()?;
let runtime = Runtime::new(ptr)?;
Ok(Self { ctx, runtime }) Ok(Self { ctx, runtime })
} }
pub fn eval_code(&mut self, expr: &str) -> Result<Value> { pub fn eval_code(&mut self, expr: &str) -> Result<Value> {
// Initialize `path_stack` with current directory for relative path resolution // Initialize `path_stack` with current directory for relative path resolution
let mut guard = PathDropGuard::new_cwd(self.ctx.as_mut())?; let mut guard = PathDropGuard::new_cwd(&mut self.ctx)?;
let ctx = guard.as_ctx(); let ctx = guard.as_ctx();
let root = rnix::Root::parse(expr); let code = ctx.compile_code(expr)?;
if !root.errors().is_empty() { self.runtime.eval(code, CtxPtr::new(&mut self.ctx))
return Err(Error::parse_error(root.errors().iter().join("; ")));
}
#[allow(clippy::unwrap_used)]
// Always `Some` since there is no parse error
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);
self.runtime.eval(code)
} }
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) fn eval_js(&mut self, code: String) -> Result<Value> { pub(crate) fn eval_js(&mut self, code: String) -> Result<Value> {
self.runtime.eval(code) self.runtime.eval(code, CtxPtr::new(&mut self.ctx))
} }
} }
#[pin_project::pin_project(PinnedDrop)]
pub(crate) struct Ctx { pub(crate) struct Ctx {
irs: Vec<Ir>, irs: Vec<Ir>,
symbols: DefaultStringInterner, symbols: DefaultStringInterner,
global: NonNull<HashMap<SymId, ExprId>>, global: NonNull<HashMap<SymId, ExprId>>,
path_stack: Vec<PathBuf>, path_stack: Vec<PathBuf>,
#[pin]
_marker: std::marker::PhantomPinned,
}
#[pin_project::pinned_drop]
impl PinnedDrop for Ctx {
fn drop(self: Pin<&mut Self>) {
unsafe {
drop(Box::from_raw(self.global.as_ptr()));
}
}
}
pub(crate) struct PathDropGuard<'ctx> {
ctx: Pin<&'ctx mut Ctx>,
}
impl<'ctx> PathDropGuard<'ctx> {
pub(crate) fn new(path: PathBuf, mut ctx: Pin<&'ctx mut Ctx>) -> Self {
ctx.as_mut().project().path_stack.push(path);
Self { ctx }
}
pub(crate) fn new_cwd(mut ctx: Pin<&'ctx mut Ctx>) -> Result<Self> {
let cwd = std::env::current_dir()
.map_err(|err| Error::downgrade_error(format!("cannot get cwd: {err}")))?;
let virtual_file = cwd.join("__eval__.nix");
ctx.as_mut().project().path_stack.push(virtual_file);
Ok(Self { ctx })
}
pub(crate) 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.as_mut().project().path_stack.pop();
}
} }
impl Default for Ctx { impl Default for Ctx {
@@ -165,7 +142,6 @@ impl Default for Ctx {
irs, irs,
global: unsafe { NonNull::new_unchecked(Box::leak(Box::new(global))) }, global: unsafe { NonNull::new_unchecked(Box::leak(Box::new(global))) },
path_stack: Vec::new(), path_stack: Vec::new(),
_marker: std::marker::PhantomPinned,
} }
} }
} }
@@ -175,7 +151,7 @@ impl Ctx {
Self::default() Self::default()
} }
pub(crate) fn downgrade_ctx<'a>(self: Pin<&'a mut Self>) -> DowngradeCtx<'a> { pub(crate) fn downgrade_ctx<'a>(&'a mut self) -> DowngradeCtx<'a> {
let global_ref = unsafe { self.global.as_ref() }; let global_ref = unsafe { self.global.as_ref() };
DowngradeCtx::new(self, global_ref) DowngradeCtx::new(self, global_ref)
} }
@@ -190,6 +166,23 @@ impl Ctx {
.expect("path in path_stack should always have a parent dir. this is a bug") .expect("path in path_stack should always have a parent dir. this is a bug")
.to_path_buf() .to_path_buf()
} }
fn compile_code(&mut self, expr: &str) -> Result<String> {
let root = rnix::Root::parse(expr);
if !root.errors().is_empty() {
return Err(Error::parse_error(root.errors().iter().join("; ")));
}
#[allow(clippy::unwrap_used)]
// Always `Some` since there is no parse error
let root = self
.downgrade_ctx()
.downgrade(root.tree().expr().unwrap())?;
let code = self.get_ir(root).compile(self);
let code = format!("Nix.force({})", code);
println!("[DEBUG] generated code: {}", &code);
Ok(code)
}
} }
impl CodegenContext for Ctx { impl CodegenContext for Ctx {
@@ -202,6 +195,12 @@ impl CodegenContext for Ctx {
} }
} }
impl PathStackProvider for Ctx {
fn path_stack(&mut self) -> &mut Vec<PathBuf> {
&mut self.path_stack
}
}
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
mod test { mod test {

View File

@@ -1,5 +1,3 @@
use std::pin::Pin;
use hashbrown::HashMap; use hashbrown::HashMap;
use crate::codegen::CodegenContext; use crate::codegen::CodegenContext;
@@ -32,14 +30,14 @@ impl<'a, 'ctx> ScopeGuard<'a, 'ctx> {
} }
pub struct DowngradeCtx<'ctx> { pub struct DowngradeCtx<'ctx> {
ctx: Pin<&'ctx mut Ctx>, ctx: &'ctx mut Ctx,
irs: Vec<Option<Ir>>, irs: Vec<Option<Ir>>,
scopes: Vec<Scope<'ctx>>, scopes: Vec<Scope<'ctx>>,
arg_id: usize, arg_id: usize,
} }
impl<'ctx> DowngradeCtx<'ctx> { impl<'ctx> DowngradeCtx<'ctx> {
pub fn new(ctx: Pin<&'ctx mut Ctx>, global: &'ctx HashMap<SymId, ExprId>) -> Self { pub fn new(ctx: &'ctx mut Ctx, global: &'ctx HashMap<SymId, ExprId>) -> Self {
Self { Self {
scopes: vec![Scope::Global(global)], scopes: vec![Scope::Global(global)],
irs: vec![], irs: vec![],
@@ -62,7 +60,7 @@ impl DowngradeContext for DowngradeCtx<'_> {
} }
fn new_sym(&mut self, sym: String) -> SymId { fn new_sym(&mut self, sym: String) -> SymId {
self.ctx.as_mut().project().symbols.get_or_intern(sym) self.ctx.symbols.get_or_intern(sym)
} }
fn get_sym(&self, id: SymId) -> &str { fn get_sym(&self, id: SymId) -> &str {
@@ -144,8 +142,6 @@ impl DowngradeContext for DowngradeCtx<'_> {
fn downgrade(mut self, root: rnix::ast::Expr) -> Result<ExprId> { fn downgrade(mut self, root: rnix::ast::Expr) -> Result<ExprId> {
let root = root.downgrade(&mut self)?; let root = root.downgrade(&mut self)?;
self.ctx self.ctx
.as_mut()
.project()
.irs .irs
.extend(self.irs.into_iter().map(Option::unwrap)); .extend(self.irs.into_iter().map(Option::unwrap));
Ok(root) Ok(root)

View File

@@ -0,0 +1,41 @@
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use crate::error::{Error, Result};
pub trait PathStackProvider {
fn path_stack(&mut self) -> &mut Vec<PathBuf>;
}
pub struct PathDropGuard<'ctx, Ctx: PathStackProvider> {
ctx: &'ctx mut Ctx,
}
impl<'ctx, Ctx: PathStackProvider> PathDropGuard<'ctx, Ctx> {
pub fn new(path: PathBuf, ctx: &'ctx mut Ctx) -> Self {
ctx.path_stack().push(path);
Self { ctx }
}
pub fn new_cwd(ctx: &'ctx mut Ctx) -> Result<Self> {
let cwd = std::env::current_dir()
.map_err(|err| Error::downgrade_error(format!("cannot get cwd: {err}")))?;
let virtual_file = cwd.join("__eval__.nix");
ctx.path_stack().push(virtual_file);
Ok(Self { ctx })
}
pub fn as_ctx(&mut self) -> &mut Ctx {
self.ctx
}
}
impl<Ctx: PathStackProvider> Deref for PathDropGuard<'_, Ctx> {
type Target = Ctx;
fn deref(&self) -> &Self::Target {
self.ctx
}
}
impl<Ctx: PathStackProvider> DerefMut for PathDropGuard<'_, Ctx> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.ctx
}
}

View File

@@ -77,6 +77,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path {
path_str path_str
} else { } else {
let current_dir = ctx.get_current_dir(); let current_dir = ctx.get_current_dir();
dbg!(&current_dir);
current_dir current_dir
.join(&path_str) .join(&path_str)

View File

@@ -234,7 +234,7 @@ where
Ctx: DowngradeContext, Ctx: DowngradeContext,
F: FnOnce(&mut Ctx, &[SymId]) -> Result<R>, F: FnOnce(&mut Ctx, &[SymId]) -> Result<R>,
{ {
// 1. Collect all top-level binding keys // Collect all top-level binding keys
let mut binding_syms = HashSet::new(); let mut binding_syms = HashSet::new();
for entry in &entries { for entry in &entries {
@@ -271,14 +271,14 @@ where
let binding_keys: Vec<_> = binding_syms.into_iter().collect(); let binding_keys: Vec<_> = binding_syms.into_iter().collect();
// 2. Reserve slots for bindings // Reserve slots for bindings
let slots_iter = ctx.reserve_slots(binding_keys.len()); let slots_iter = ctx.reserve_slots(binding_keys.len());
let slots_clone = slots_iter.clone(); let slots_clone = slots_iter.clone();
// 3. Create let scope bindings // Create let scope bindings
let let_bindings: HashMap<_, _> = binding_keys.iter().copied().zip(slots_iter).collect(); let let_bindings: HashMap<_, _> = binding_keys.iter().copied().zip(slots_iter).collect();
// 4. Process entries in let scope // Process entries in let scope
let body = ctx.with_let_scope(let_bindings, |ctx| { let body = ctx.with_let_scope(let_bindings, |ctx| {
// Collect all bindings in a temporary AttrSet // Collect all bindings in a temporary AttrSet
let mut temp_attrs = AttrSet { let mut temp_attrs = AttrSet {
@@ -313,6 +313,5 @@ where
body_fn(ctx, &binding_keys) body_fn(ctx, &binding_keys)
})?; })?;
// 5. Return the slots and body
Ok((slots_clone.collect(), body)) Ok((slots_clone.collect(), body))
} }

View File

@@ -1,38 +1,40 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::pin::Pin; use std::marker::PhantomData;
use std::ops::DerefMut;
use std::path::PathBuf;
use std::sync::Once; use std::sync::Once;
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpDecl, OpState, RuntimeOptions, v8}; use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8};
use deno_error::JsErrorClass; use deno_error::JsErrorClass;
use crate::codegen::{CodegenContext, Compile};
use crate::context::{CtxPtr, PathDropGuard};
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::ir::DowngradeContext;
use crate::value::{AttrSet, List, Symbol, Value}; use crate::value::{AttrSet, List, Symbol, Value};
type ScopeRef<'p, 's> = v8::PinnedRef<'p, v8::HandleScope<'s>>; type ScopeRef<'p, 's> = v8::PinnedRef<'p, v8::HandleScope<'s>>;
type LocalValue<'a> = v8::Local<'a, v8::Value>; type LocalValue<'a> = v8::Local<'a, v8::Value>;
type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>; type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>;
fn runtime_extension(ctx: CtxPtr) -> Extension { pub(crate) trait RuntimeCtx: 'static {
fn get_current_dir(&self) -> PathBuf;
fn push_path_stack(&mut self, path: PathBuf) -> impl DerefMut<Target = Self>;
fn compile_code(&mut self, code: &str) -> Result<String>;
}
fn runtime_extension<Ctx: RuntimeCtx>() -> Extension {
const ESM: &[ExtensionFileSource] = const ESM: &[ExtensionFileSource] =
&deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js"); &deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js");
const OPS: &[OpDecl] = &[ let ops = vec![
op_import(), op_import::<Ctx>(),
op_read_file(), op_read_file(),
op_path_exists(), op_path_exists(),
op_resolve_path(), op_resolve_path::<Ctx>(),
]; ];
Extension { Extension {
name: "nix_runtime", name: "nix_runtime",
esm_files: Cow::Borrowed(ESM), esm_files: Cow::Borrowed(ESM),
esm_entry_point: Some("ext:nix_runtime/runtime.js"), esm_entry_point: Some("ext:nix_runtime/runtime.js"),
ops: Cow::Borrowed(OPS), ops: Cow::Owned(ops),
op_state_fn: Some(Box::new(move |state| {
state.put(ctx);
})),
enabled: true, enabled: true,
..Default::default() ..Default::default()
} }
@@ -67,9 +69,11 @@ use private::NixError;
#[deno_core::op2] #[deno_core::op2]
#[string] #[string]
fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result<String, NixError> { fn op_import<Ctx: RuntimeCtx>(
let ptr = state.borrow_mut::<CtxPtr>(); state: &mut OpState,
let ctx = unsafe { ptr.as_mut() }; #[string] path: String,
) -> std::result::Result<String, NixError> {
let ctx = state.borrow_mut::<Ctx>();
let current_dir = ctx.get_current_dir(); let current_dir = ctx.get_current_dir();
let mut absolute_path = current_dir let mut absolute_path = current_dir
@@ -80,30 +84,13 @@ fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result
absolute_path.push("default.nix") absolute_path.push("default.nix")
} }
let mut guard = PathDropGuard::new(absolute_path.clone(), ctx);
let ctx = guard.as_ctx();
let content = std::fs::read_to_string(&absolute_path) let content = std::fs::read_to_string(&absolute_path)
.map_err(|e| format!("Failed to read {}: {}", absolute_path.display(), e))?; .map_err(|e| format!("Failed to read {}: {}", absolute_path.display(), e))?;
let root = rnix::Root::parse(&content); let mut guard = ctx.push_path_stack(absolute_path);
if !root.errors().is_empty() { let ctx = guard.deref_mut();
return Err(format!(
"Parse error in {}: {:?}",
absolute_path.display(),
root.errors()
)
.into());
}
let expr = root.tree().expr().ok_or("No expression in file")?; Ok(ctx.compile_code(&content).map_err(|err| err.to_string())?)
let expr_id = ctx
.as_mut()
.downgrade_ctx()
.downgrade(expr)
.map_err(|e| format!("Downgrade error: {}", e))?;
Ok(ctx.get_ir(expr_id).compile(Pin::get_ref(ctx.as_ref())))
} }
#[deno_core::op2] #[deno_core::op2]
@@ -119,12 +106,11 @@ fn op_path_exists(#[string] path: String) -> bool {
#[deno_core::op2] #[deno_core::op2]
#[string] #[string]
fn op_resolve_path( fn op_resolve_path<Ctx: RuntimeCtx>(
state: &mut OpState, state: &mut OpState,
#[string] path: String, #[string] path: String,
) -> std::result::Result<String, NixError> { ) -> std::result::Result<String, NixError> {
let ptr = state.borrow::<CtxPtr>(); let ctx = state.borrow::<Ctx>();
let ctx = unsafe { ptr.as_ref() };
// If already absolute, return as-is // If already absolute, return as-is
if path.starts_with('/') { if path.starts_with('/') {
@@ -141,14 +127,15 @@ fn op_resolve_path(
.map_err(|e| format!("Failed to resolve path {}: {}", path, e))?) .map_err(|e| format!("Failed to resolve path {}: {}", path, e))?)
} }
pub(crate) struct Runtime { pub(crate) struct Runtime<Ctx: RuntimeCtx> {
js_runtime: JsRuntime, js_runtime: JsRuntime,
is_thunk_symbol: v8::Global<v8::Symbol>, is_thunk_symbol: v8::Global<v8::Symbol>,
primop_metadata_symbol: v8::Global<v8::Symbol>, primop_metadata_symbol: v8::Global<v8::Symbol>,
_marker: PhantomData<Ctx>,
} }
impl Runtime { impl<Ctx: RuntimeCtx> Runtime<Ctx> {
pub(crate) fn new(ctx: CtxPtr) -> Result<Self> { pub(crate) fn new() -> Result<Self> {
// Initialize V8 once // Initialize V8 once
static INIT: Once = Once::new(); static INIT: Once = Once::new();
INIT.call_once(|| { INIT.call_once(|| {
@@ -159,7 +146,7 @@ impl Runtime {
}); });
let mut js_runtime = JsRuntime::new(RuntimeOptions { let mut js_runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![runtime_extension(ctx)], extensions: vec![runtime_extension::<Ctx>()],
..Default::default() ..Default::default()
}); });
@@ -172,10 +159,13 @@ impl Runtime {
js_runtime, js_runtime,
is_thunk_symbol, is_thunk_symbol,
primop_metadata_symbol, primop_metadata_symbol,
_marker: PhantomData,
}) })
} }
pub(crate) fn eval(&mut self, script: String) -> Result<Value> { pub(crate) fn eval(&mut self, script: String, ctx: Ctx) -> Result<Value> {
self.js_runtime.op_state().borrow_mut().put(ctx);
let global_value = self let global_value = self
.js_runtime .js_runtime
.execute_script("<eval>", script) .execute_script("<eval>", script)