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

1
Cargo.lock generated
View File

@@ -1029,6 +1029,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"deno_core", "deno_core",
"deno_error",
"derive_more", "derive_more",
"hashbrown 0.16.1", "hashbrown 0.16.1",
"itertools 0.14.0", "itertools 0.14.0",

View File

@@ -20,6 +20,7 @@ itertools = "0.14"
v8 = "142.2" v8 = "142.2"
deno_core = "0.376" deno_core = "0.376"
deno_error = "0.7"
rnix = "0.12" rnix = "0.12"

View File

@@ -17,7 +17,7 @@ export const getAttr =
export const hasAttr = export const hasAttr =
(s: NixValue) => (s: NixValue) =>
(set: NixValue): boolean => (set: NixValue): boolean =>
Object.prototype.hasOwnProperty.call(force_attrs(set), force_string(s)); Object.hasOwn(force_attrs(set), force_string(s));
export const mapAttrs = export const mapAttrs =
(f: NixValue) => (f: NixValue) =>
@@ -48,7 +48,7 @@ export const intersectAttrs =
const f2 = force_attrs(e2); const f2 = force_attrs(e2);
const attrs: NixAttrs = {}; const attrs: NixAttrs = {};
for (const key of Object.keys(f2)) { for (const key of Object.keys(f2)) {
if (Object.prototype.hasOwnProperty.call(f1, key)) { if (Object.hasOwn(f1, key)) {
attrs[key] = f2[key]; attrs[key] = f2[key];
} }
} }

View File

@@ -1,12 +1,25 @@
/** /**
* I/O and filesystem builtin functions (unimplemented) * I/O and filesystem builtin functions
* These functions require Node.js/Deno APIs not available in V8 * Implemented via Rust ops exposed through deno_core
*/ */
import type { NixValue } from "../types"; import type { NixValue } from "../types";
import { force_string } from "../type-assert";
export const importFunc = (path: NixValue): never => { // Declare Deno.core.ops global (provided by deno_core runtime)
throw "Not implemented: import";
export const importFunc = (path: NixValue): NixValue => {
// For MVP: only support string paths
// TODO: After implementing path type, also accept path values
const pathStr = force_string(path);
// Call Rust op - returns JS code string
const code = Deno.core.ops.op_import(pathStr);
// Eval in current context - returns V8 value directly!
// (0, eval) = indirect eval = global scope
// Wrap in parentheses to ensure object literals are parsed correctly
return (0, eval)(`(${code})`);
}; };
export const scopedImport = export const scopedImport =
@@ -39,16 +52,18 @@ export const readDir = (path: NixValue): never => {
throw "Not implemented: readDir"; throw "Not implemented: readDir";
}; };
export const readFile = (path: NixValue): never => { export const readFile = (path: NixValue): string => {
throw "Not implemented: readFile"; const pathStr = force_string(path);
return Deno.core.ops.op_read_file(pathStr);
}; };
export const readFileType = (path: NixValue): never => { export const readFileType = (path: NixValue): never => {
throw "Not implemented: readFileType"; throw "Not implemented: readFileType";
}; };
export const pathExists = (path: NixValue): never => { export const pathExists = (path: NixValue): boolean => {
throw "Not implemented: pathExists"; const pathStr = force_string(path);
return Deno.core.ops.op_path_exists(pathStr);
}; };
export const path = (args: NixValue): never => { export const path = (args: NixValue): never => {

View File

@@ -6,6 +6,18 @@
import type { NixValue, NixAttrs } from "./types"; import type { NixValue, NixAttrs } from "./types";
import { force_attrs, force_string } from "./type-assert"; import { force_attrs, force_string } from "./type-assert";
/**
* Resolve a path (handles both absolute and relative paths)
* For relative paths, resolves against current import stack
*
* @param path - Path string (may be relative or absolute)
* @returns Absolute path string
*/
export const resolve_path = (path: NixValue): string => {
const path_str = force_string(path);
return Deno.core.ops.op_resolve_path(path_str);
};
/** /**
* Select an attribute from an attribute set * Select an attribute from an attribute set
* Used by codegen for attribute access (e.g., obj.key) * Used by codegen for attribute access (e.g., obj.key)
@@ -75,7 +87,7 @@ export const validate_params = (
// Check required parameters // Check required parameters
if (required) { if (required) {
for (const key of required) { for (const key of required) {
if (!Object.prototype.hasOwnProperty.call(forced_arg, key)) { if (!Object.hasOwn(forced_arg, key)) {
throw new Error(`Function called without required argument '${key}'`); throw new Error(`Function called without required argument '${key}'`);
} }
} }

View File

@@ -5,7 +5,7 @@
*/ */
import { create_thunk, force, is_thunk, IS_THUNK } from "./thunk"; import { create_thunk, force, is_thunk, IS_THUNK } from "./thunk";
import { select, select_with_default, validate_params } from "./helpers"; import { select, select_with_default, validate_params, resolve_path } from "./helpers";
import { op } from "./operators"; import { op } from "./operators";
import { builtins, IS_PRIMOP } from "./builtins"; import { builtins, IS_PRIMOP } from "./builtins";
@@ -23,6 +23,7 @@ export const Nix = {
select, select,
select_with_default, select_with_default,
validate_params, validate_params,
resolve_path,
op, op,
builtins, builtins,
@@ -30,6 +31,3 @@ export const Nix = {
}; };
globalThis.Nix = Nix; globalThis.Nix = Nix;
declare global {
var Nix: NixRuntime;
}

15
nix-js/runtime-ts/src/types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
export {};
declare global {
var Nix: NixRuntime;
namespace Deno {
namespace core {
namespace ops {
function op_resolve_path(path: string): string;
function op_import(path: string): string;
function op_read_file(path: string): string;
function op_path_exists(path: string): boolean;
}
}
}
}

View File

@@ -14,7 +14,8 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"declaration": false, "declaration": false,
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src" "rootDir": "./src",
"typeRoots": ["./src/types"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]

View File

@@ -20,6 +20,21 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
crate::value::Const::Float(val) => val.to_string(), crate::value::Const::Float(val) => val.to_string(),
crate::value::Const::Bool(val) => val.to_string(), crate::value::Const::Bool(val) => val.to_string(),
}, },
Ir::Str(s) => {
// Escape string for JavaScript
let escaped = s.val
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
format!("\"{}\"", escaped)
}
Ir::Path(p) => {
// Path needs runtime resolution for interpolated paths
let path_expr = ctx.get_ir(p.expr).compile(ctx);
format!("Nix.resolve_path({})", path_expr)
}
&Ir::If(If { cond, consq, alter }) => { &Ir::If(If { cond, consq, alter }) => {
let cond = ctx.get_ir(cond).compile(ctx); let cond = ctx.get_ir(cond).compile(ctx);
let consq = ctx.get_ir(consq).compile(ctx); let consq = ctx.get_ir(consq).compile(ctx);
@@ -47,6 +62,8 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
format!("expr{}", expr_id.0) format!("expr{}", expr_id.0)
} }
Ir::Builtin(_) => "Nix.builtins".to_string(), Ir::Builtin(_) => "Nix.builtins".to_string(),
Ir::ConcatStrings(x) => x.compile(ctx),
Ir::HasAttr(x) => x.compile(ctx),
ir => todo!("{ir:?}"), ir => todo!("{ir:?}"),
} }
} }
@@ -247,3 +264,46 @@ impl<Ctx: CodegenContext> Compile<Ctx> for List {
format!("[{list}]") format!("[{list}]")
} }
} }
impl<Ctx: CodegenContext> Compile<Ctx> for ConcatStrings {
fn compile(&self, ctx: &Ctx) -> String {
// Concatenate all parts into a single string
// Use JavaScript template string or array join
let parts: Vec<String> = self
.parts
.iter()
.map(|part| {
let compiled = ctx.get_ir(*part).compile(ctx);
// TODO: coercce to string
format!("String(Nix.force({}))", compiled)
})
.collect();
// Use array join for concatenation
format!("[{}].join('')", parts.join(","))
}
}
impl<Ctx: CodegenContext> Compile<Ctx> for HasAttr {
fn compile(&self, ctx: &Ctx) -> String {
let lhs = ctx.get_ir(self.lhs).compile(ctx);
// Build attrpath check
let mut current = format!("Nix.force({})", lhs);
for attr in &self.rhs {
match attr {
Attr::Str(sym) => {
let key = ctx.get_sym(*sym);
current = format!("(Nix.force({}) !== null && Nix.force({}) !== undefined && \"{}\" in Nix.force({}))", current, current, key, current);
}
Attr::Dynamic(expr_id) => {
let key = ctx.get_ir(*expr_id).compile(ctx);
current = format!("(Nix.force({}) !== null && Nix.force({}) !== undefined && Nix.force({}) in Nix.force({}))", current, current, key, current);
}
}
}
current
}
}

View File

@@ -98,6 +98,9 @@ impl Context {
} }
pub fn eval(&mut self, expr: &str) -> Result<Value> { pub fn eval(&mut self, expr: &str) -> Result<Value> {
// Initialize IMPORT_PATH_STACK with current directory for relative path resolution
let _path_guard = crate::runtime::ImportPathGuard::push_cwd();
let root = rnix::Root::parse(expr); let root = rnix::Root::parse(expr);
if !root.errors().is_empty() { if !root.errors().is_empty() {
return Err(Error::parse_error(root.errors().iter().join("; "))); return Err(Error::parse_error(root.errors().iter().join("; ")));
@@ -108,7 +111,7 @@ impl Context {
let code = self.get_ir(root).compile(self); let code = self.get_ir(root).compile(self);
let code = format!("Nix.force({})", code); let code = format!("Nix.force({})", code);
println!("[DEBUG] generated code: {}", &code); println!("[DEBUG] generated code: {}", &code);
crate::runtime::run(&code) crate::runtime::run(code, self)
} }
} }
@@ -168,9 +171,8 @@ mod test {
let tests = [ let tests = [
("1 + 1", Value::Const(Const::Int(2))), ("1 + 1", Value::Const(Const::Int(2))),
("2 - 1", Value::Const(Const::Int(1))), ("2 - 1", Value::Const(Const::Int(1))),
// FIXME: Floating point ("1. * 1", Value::Const(Const::Float(1.))),
// ("1. * 1", Value::Const(Const::Float(1.))), ("1 / 1.", Value::Const(Const::Float(1.))),
// ("1 / 1.", Value::Const(Const::Float(1.))),
("1 == 1", Value::Const(Const::Bool(true))), ("1 == 1", Value::Const(Const::Bool(true))),
("1 != 1", Value::Const(Const::Bool(false))), ("1 != 1", Value::Const(Const::Bool(false))),
("2 < 1", Value::Const(Const::Bool(false))), ("2 < 1", Value::Const(Const::Bool(false))),
@@ -696,4 +698,137 @@ mod test {
Value::Const(Const::Int(1000000000000000000i64)) Value::Const(Const::Int(1000000000000000000i64))
); );
} }
#[test]
fn test_import_absolute_path() {
use std::io::Write;
let mut ctx = Context::new();
// Create temporary file
let temp_dir = std::env::temp_dir();
let lib_path = temp_dir.join("nix_test_lib.nix");
let mut file = std::fs::File::create(&lib_path).unwrap();
file.write_all(b"{ add = a: b: a + b; }").unwrap();
drop(file);
// Test import with absolute path string
let expr = format!(r#"(import "{}").add 3 5"#, lib_path.display());
assert_eq!(ctx.eval(&expr).unwrap(), Value::Const(Const::Int(8)));
// Cleanup
std::fs::remove_file(&lib_path).ok();
}
#[test]
fn test_import_nested() {
use std::io::Write;
let mut ctx = Context::new();
// Create temporary directory structure
let temp_dir = std::env::temp_dir().join("nix_test_nested");
std::fs::create_dir_all(&temp_dir).unwrap();
// Create lib.nix
let lib_path = temp_dir.join("lib.nix");
let mut file = std::fs::File::create(&lib_path).unwrap();
file.write_all(b"{ add = a: b: a + b; }").unwrap();
drop(file);
// Create main.nix that imports lib.nix
let main_path = temp_dir.join("main.nix");
let main_content = format!(
r#"let lib = import {}; in {{ result = lib.add 10 20; }}"#,
lib_path.display()
);
let mut file = std::fs::File::create(&main_path).unwrap();
file.write_all(main_content.as_bytes()).unwrap();
drop(file);
// Test nested import
let expr = format!(r#"(import "{}").result"#, main_path.display());
assert_eq!(ctx.eval(&expr).unwrap(), Value::Const(Const::Int(30)));
// Cleanup
std::fs::remove_file(&lib_path).ok();
std::fs::remove_file(&main_path).ok();
std::fs::remove_dir(&temp_dir).ok();
}
#[test]
fn test_import_relative_path() {
use std::io::Write;
let mut ctx = Context::new();
// Create temporary directory structure
let temp_dir = std::env::temp_dir().join("nix_test_relative");
let subdir = temp_dir.join("subdir");
std::fs::create_dir_all(&subdir).unwrap();
// Create lib.nix
let lib_path = temp_dir.join("lib.nix");
let mut file = std::fs::File::create(&lib_path).unwrap();
file.write_all(b"{ multiply = a: b: a * b; }").unwrap();
drop(file);
// Create subdir/helper.nix
let helper_path = subdir.join("helper.nix");
let mut file = std::fs::File::create(&helper_path).unwrap();
file.write_all(b"{ subtract = a: b: a - b; }").unwrap();
drop(file);
// Create main.nix with relative path imports
let main_path = temp_dir.join("main.nix");
let main_content = r#"
let
lib = import ./lib.nix;
helper = import ./subdir/helper.nix;
in {
result1 = lib.multiply 3 4;
result2 = helper.subtract 10 3;
}
"#;
let mut file = std::fs::File::create(&main_path).unwrap();
file.write_all(main_content.as_bytes()).unwrap();
drop(file);
// Test relative path imports
let expr = format!(r#"let x = import "{}"; in x.result1"#, main_path.display());
assert_eq!(ctx.eval(&expr).unwrap(), Value::Const(Const::Int(12)));
let expr = format!(r#"let x = import "{}"; in x.result2"#, main_path.display());
assert_eq!(ctx.eval(&expr).unwrap(), Value::Const(Const::Int(7)));
// Cleanup
std::fs::remove_file(&lib_path).ok();
std::fs::remove_file(&helper_path).ok();
std::fs::remove_file(&main_path).ok();
std::fs::remove_dir(&subdir).ok();
std::fs::remove_dir(&temp_dir).ok();
}
#[test]
fn test_import_returns_function() {
use std::io::Write;
let mut ctx = Context::new();
// Create temporary file that exports a function
let temp_dir = std::env::temp_dir();
let func_path = temp_dir.join("nix_test_func.nix");
let mut file = std::fs::File::create(&func_path).unwrap();
file.write_all(b"x: x * 2").unwrap();
drop(file);
// Test importing a function
let expr = format!(r#"(import "{}") 5"#, func_path.display());
assert_eq!(ctx.eval(&expr).unwrap(), Value::Const(Const::Int(10)));
// Cleanup
std::fs::remove_file(&func_path).ok();
}
} }

View File

@@ -55,20 +55,74 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::IfElse {
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path { impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> { fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let parts = self // Collect all parts and check if there are any interpolations
.parts() let parts_ast: Vec<_> = self.parts().collect();
let has_interpolation = parts_ast.iter().any(|part| matches!(part, ast::InterpolPart::Interpolation(_)));
let parts = if !has_interpolation {
// Pure literal path - resolve at compile time
let path_str: String = parts_ast
.into_iter()
.filter_map(|part| match part {
ast::InterpolPart::Literal(lit) => Some(lit.to_string()),
_ => None,
})
.collect();
// Resolve relative paths at compile time
let resolved_path = if path_str.starts_with('/') {
// Absolute path - use as is
path_str
} else {
// Relative path - resolve against current file directory
let current_dir = crate::runtime::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())
});
current_dir
.join(&path_str)
.canonicalize()
.map_err(|e| crate::error::Error::downgrade_error(
format!("Failed to resolve path {}: {}", path_str, e)
))?
.to_string_lossy()
.to_string()
};
// Return single string part with resolved path
vec![ctx.new_expr(
Str {
val: resolved_path,
}
.to_ir(),
)]
} else {
// Path with interpolation - do NOT resolve at compile time
// Keep literal parts as-is and defer resolution to runtime
parts_ast
.into_iter()
.map(|part| match part { .map(|part| match part {
ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr( ast::InterpolPart::Literal(lit) => {
// Keep literal as-is (don't resolve)
Ok(ctx.new_expr(
Str { Str {
val: lit.to_string(), val: lit.to_string(),
} }
.to_ir(), .to_ir(),
)), ))
}
ast::InterpolPart::Interpolation(interpol) => { ast::InterpolPart::Interpolation(interpol) => {
interpol.expr().unwrap().downgrade(ctx) interpol.expr().unwrap().downgrade(ctx)
} }
}) })
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?
};
let expr = if parts.len() == 1 { let expr = if parts.len() == 1 {
parts.into_iter().next().unwrap() parts.into_iter().next().unwrap()
} else { } else {

View File

@@ -1,25 +1,218 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::path::PathBuf;
use std::ptr::NonNull;
use std::sync::Once; 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::error::{Error, Result};
use crate::ir::DowngradeContext;
use crate::value::{AttrSet, Const, List, Symbol, Value}; use crate::value::{AttrSet, Const, List, Symbol, Value};
static INIT: Once = Once::new(); static INIT: Once = Once::new();
thread_local! { thread_local! {
static ISOLATE: RefCell<v8::OwnedIsolate> = static CONTEXT_HOLDER: RefCell<Option<NonNull<Context>>> = const { RefCell::new(None) };
RefCell::new(v8::Isolate::new(Default::default()));
} }
pub fn run(script: &str) -> Result<Value> { // for relative path resolution
INIT.call_once(|| { thread_local! {
v8::V8::initialize_platform(v8::new_default_platform(0, false).make_shared()); pub(crate) static IMPORT_PATH_STACK: RefCell<Vec<PathBuf>> = const { RefCell::new(Vec::new()) };
v8::V8::initialize(); }
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())
}); });
ISOLATE.with_borrow_mut(|isolate| run_impl(script, isolate)) 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())
});
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> { struct RuntimeContext<'a, 'b> {
scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>, scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>,
is_thunk_symbol: Option<v8::Local<'a, v8::Symbol>>, 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> { // Main entry point
let handle_scope = std::pin::pin!(v8::HandleScope::new(isolate)); pub fn run(script: String, ctx: &mut Context) -> Result<Value> {
let handle_scope = &mut handle_scope.init(); let _guard = ContextGuard::set(ctx);
let context = v8::Context::new(handle_scope, v8::ContextOptions::default());
let scope = &mut v8::ContextScope::new(handle_scope, context);
// 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_code = include_str!("../runtime-ts/dist/runtime.js");
let runtime_source = v8::String::new(scope, runtime_code).unwrap(); runtime
let runtime_script = v8::Script::compile(scope, runtime_source, None).unwrap(); .execute_script("<runtime>", runtime_code)
.map_err(|e| Error::eval_error(format!("Failed to load runtime: {:?}", e)))?;
if runtime_script.run(scope).is_none() { // Execute user script
return Err(Error::eval_error( let global_value = runtime
"Failed to initialize runtime".to_string(), .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 runtime_ctx = RuntimeContext::new(scope);
let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); Ok(to_value(local_value, &runtime_ctx))
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()))
}
}
}
} }
fn to_value<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>) -> Value { 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] #[test]
fn to_value_working() { fn to_value_working() {
let mut ctx = Context::new();
assert_eq!( assert_eq!(
run("({ run(
"({
test: [1., 9223372036854775807n, true, false, 'hello world!'] test: [1., 9223372036854775807n, true, false, 'hello world!']
})") })".into(),
&mut ctx
)
.unwrap(), .unwrap(),
Value::AttrSet(AttrSet::new(std::collections::BTreeMap::from([( Value::AttrSet(AttrSet::new(std::collections::BTreeMap::from([(
Symbol::from("test"), Symbol::from("test"),