feat: builtins.import

This commit is contained in:
2026-01-03 14:52:46 +08:00
parent c79eb0951e
commit 40884c21ad
12 changed files with 557 additions and 97 deletions

View File

@@ -1,25 +1,218 @@
use std::cell::RefCell;
use std::path::PathBuf;
use std::ptr::NonNull;
use std::sync::Once;
use deno_core::{JsRuntime, RuntimeOptions};
use deno_error::js_error_wrapper;
use crate::codegen::{CodegenContext, Compile};
use crate::context::Context;
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 ISOLATE: RefCell<v8::OwnedIsolate> =
RefCell::new(v8::Isolate::new(Default::default()));
static CONTEXT_HOLDER: RefCell<Option<NonNull<Context>>> = const { RefCell::new(None) };
}
pub fn run(script: &str) -> Result<Value> {
INIT.call_once(|| {
v8::V8::initialize_platform(v8::new_default_platform(0, false).make_shared());
v8::V8::initialize();
// for relative path resolution
thread_local! {
pub(crate) static IMPORT_PATH_STACK: RefCell<Vec<PathBuf>> = const { RefCell::new(Vec::new()) };
}
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
}
}
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);
impl std::fmt::Display for SimpleErrorWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
impl std::error::Error for SimpleErrorWrapper {
fn cause(&self) -> Option<&dyn std::error::Error> {
None
}
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
fn description(&self) -> &str {
&self.0
}
}
impl From<String> for NixError {
fn from(value: String) -> Self {
NixError(SimpleErrorWrapper(value))
}
}
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() };
// 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()
})?;
// 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]
#[string]
fn op_read_file(#[string] path: String) -> std::result::Result<String, NixError> {
std::fs::read_to_string(&path)
.map_err(|e| -> NixError {
format!("Failed to read {}: {}", path, e).into()
})
}
#[deno_core::op2(fast)]
fn op_path_exists(#[string] path: String) -> bool {
std::path::Path::new(&path).exists()
}
#[deno_core::op2]
#[string]
fn op_resolve_path(#[string] path: String) -> std::result::Result<String, NixError> {
// 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())
});
ISOLATE.with_borrow_mut(|isolate| run_impl(script, isolate))
current_dir
.join(&path)
.canonicalize()
.map(|p| p.to_string_lossy().to_string())
.map_err(|e| -> NixError {
format!("Failed to resolve path {}: {}", path, e).into()
})
}
// Runtime context for V8 value conversion
struct RuntimeContext<'a, 'b> {
scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>,
is_thunk_symbol: Option<v8::Local<'a, v8::Symbol>>,
@@ -72,66 +265,37 @@ impl<'a, 'b> RuntimeContext<'a, 'b> {
}
}
fn run_impl(script: &str, isolate: &mut v8::Isolate) -> Result<Value> {
let handle_scope = std::pin::pin!(v8::HandleScope::new(isolate));
let handle_scope = &mut handle_scope.init();
let context = v8::Context::new(handle_scope, v8::ContextOptions::default());
let scope = &mut v8::ContextScope::new(handle_scope, context);
// Main entry point
pub fn run(script: String, ctx: &mut Context) -> Result<Value> {
let _guard = ContextGuard::set(ctx);
// Initialize V8 once
INIT.call_once(|| {
JsRuntime::init_platform(Some(v8::new_default_platform(0, false).make_shared()), false);
});
// Create a new JsRuntime for each evaluation to avoid state issues
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![nix_extension()],
..Default::default()
});
// Load runtime.js
let runtime_code = include_str!("../runtime-ts/dist/runtime.js");
let runtime_source = v8::String::new(scope, runtime_code).unwrap();
let runtime_script = v8::Script::compile(scope, runtime_source, None).unwrap();
runtime
.execute_script("<runtime>", runtime_code)
.map_err(|e| Error::eval_error(format!("Failed to load runtime: {:?}", e)))?;
if runtime_script.run(scope).is_none() {
return Err(Error::eval_error(
"Failed to initialize runtime".to_string(),
));
}
// Execute user script
let global_value = runtime
.execute_script("<eval>", script)
.map_err(|e| Error::eval_error(format!("Execution error: {:?}", e)))?;
let source = v8::String::new(scope, script).unwrap();
deno_core::scope!(scope, runtime);
let local_value = v8::Local::new(scope, &global_value);
// Use TryCatch to capture JavaScript exceptions
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
let try_catch = &mut try_catch.init();
let script = match v8::Script::compile(try_catch, source, None) {
Some(script) => script,
None => {
if let Some(exception) = try_catch.exception() {
let exception_string = exception
.to_string(try_catch)
.unwrap()
.to_rust_string_lossy(try_catch);
return Err(Error::eval_error(format!(
"Compilation error: {}",
exception_string
)));
} else {
return Err(Error::eval_error("Unknown compilation error".to_string()));
}
}
};
match script.run(try_catch) {
Some(result) => {
// Initialize runtime context once before conversion
let ctx = RuntimeContext::new(try_catch);
Ok(to_value(result, &ctx))
}
None => {
if let Some(exception) = try_catch.exception() {
let exception_string = exception
.to_string(try_catch)
.unwrap()
.to_rust_string_lossy(try_catch);
Err(Error::eval_error(format!(
"Runtime error: {}",
exception_string
)))
} else {
Err(Error::eval_error("Unknown runtime error".to_string()))
}
}
}
let runtime_ctx = RuntimeContext::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 {
@@ -270,10 +434,14 @@ fn primop_app_name<'a, 'b>(
#[test]
fn to_value_working() {
let mut ctx = Context::new();
assert_eq!(
run("({
run(
"({
test: [1., 9223372036854775807n, true, false, 'hello world!']
})")
})".into(),
&mut ctx
)
.unwrap(),
Value::AttrSet(AttrSet::new(std::collections::BTreeMap::from([(
Symbol::from("test"),