diff --git a/Cargo.lock b/Cargo.lock index f81f19a..14a0d32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1029,6 +1029,7 @@ version = "0.1.0" dependencies = [ "anyhow", "deno_core", + "deno_error", "derive_more", "hashbrown 0.16.1", "itertools 0.14.0", diff --git a/nix-js/Cargo.toml b/nix-js/Cargo.toml index e629f80..2ac741c 100644 --- a/nix-js/Cargo.toml +++ b/nix-js/Cargo.toml @@ -20,6 +20,7 @@ itertools = "0.14" v8 = "142.2" deno_core = "0.376" +deno_error = "0.7" rnix = "0.12" diff --git a/nix-js/runtime-ts/src/builtins/attrs.ts b/nix-js/runtime-ts/src/builtins/attrs.ts index 3641890..fdc81cb 100644 --- a/nix-js/runtime-ts/src/builtins/attrs.ts +++ b/nix-js/runtime-ts/src/builtins/attrs.ts @@ -17,7 +17,7 @@ export const getAttr = export const hasAttr = (s: NixValue) => (set: NixValue): boolean => - Object.prototype.hasOwnProperty.call(force_attrs(set), force_string(s)); + Object.hasOwn(force_attrs(set), force_string(s)); export const mapAttrs = (f: NixValue) => @@ -48,7 +48,7 @@ export const intersectAttrs = const f2 = force_attrs(e2); const attrs: NixAttrs = {}; for (const key of Object.keys(f2)) { - if (Object.prototype.hasOwnProperty.call(f1, key)) { + if (Object.hasOwn(f1, key)) { attrs[key] = f2[key]; } } diff --git a/nix-js/runtime-ts/src/builtins/io.ts b/nix-js/runtime-ts/src/builtins/io.ts index c157585..2baa9c4 100644 --- a/nix-js/runtime-ts/src/builtins/io.ts +++ b/nix-js/runtime-ts/src/builtins/io.ts @@ -1,12 +1,25 @@ /** - * I/O and filesystem builtin functions (unimplemented) - * These functions require Node.js/Deno APIs not available in V8 + * I/O and filesystem builtin functions + * Implemented via Rust ops exposed through deno_core */ import type { NixValue } from "../types"; +import { force_string } from "../type-assert"; -export const importFunc = (path: NixValue): never => { - throw "Not implemented: import"; +// Declare Deno.core.ops global (provided by deno_core runtime) + +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 = @@ -39,16 +52,18 @@ export const readDir = (path: NixValue): never => { throw "Not implemented: readDir"; }; -export const readFile = (path: NixValue): never => { - throw "Not implemented: readFile"; +export const readFile = (path: NixValue): string => { + const pathStr = force_string(path); + return Deno.core.ops.op_read_file(pathStr); }; export const readFileType = (path: NixValue): never => { throw "Not implemented: readFileType"; }; -export const pathExists = (path: NixValue): never => { - throw "Not implemented: pathExists"; +export const pathExists = (path: NixValue): boolean => { + const pathStr = force_string(path); + return Deno.core.ops.op_path_exists(pathStr); }; export const path = (args: NixValue): never => { diff --git a/nix-js/runtime-ts/src/helpers.ts b/nix-js/runtime-ts/src/helpers.ts index a49dca6..98c0d9b 100644 --- a/nix-js/runtime-ts/src/helpers.ts +++ b/nix-js/runtime-ts/src/helpers.ts @@ -6,6 +6,18 @@ import type { NixValue, NixAttrs } from "./types"; 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 * Used by codegen for attribute access (e.g., obj.key) @@ -75,7 +87,7 @@ export const validate_params = ( // Check required parameters if (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}'`); } } diff --git a/nix-js/runtime-ts/src/index.ts b/nix-js/runtime-ts/src/index.ts index 5a248de..44232c2 100644 --- a/nix-js/runtime-ts/src/index.ts +++ b/nix-js/runtime-ts/src/index.ts @@ -5,7 +5,7 @@ */ 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 { builtins, IS_PRIMOP } from "./builtins"; @@ -23,6 +23,7 @@ export const Nix = { select, select_with_default, validate_params, + resolve_path, op, builtins, @@ -30,6 +31,3 @@ export const Nix = { }; globalThis.Nix = Nix; -declare global { - var Nix: NixRuntime; -} diff --git a/nix-js/runtime-ts/src/types/global.d.ts b/nix-js/runtime-ts/src/types/global.d.ts new file mode 100644 index 0000000..dca85d8 --- /dev/null +++ b/nix-js/runtime-ts/src/types/global.d.ts @@ -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; + } + } + } +} diff --git a/nix-js/runtime-ts/tsconfig.json b/nix-js/runtime-ts/tsconfig.json index 2a65ddd..3bd1cc1 100644 --- a/nix-js/runtime-ts/tsconfig.json +++ b/nix-js/runtime-ts/tsconfig.json @@ -14,7 +14,8 @@ "allowSyntheticDefaultImports": true, "declaration": false, "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", + "typeRoots": ["./src/types"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index 7e7975a..e719236 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -20,6 +20,21 @@ impl Compile for Ir { crate::value::Const::Float(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 }) => { let cond = ctx.get_ir(cond).compile(ctx); let consq = ctx.get_ir(consq).compile(ctx); @@ -47,6 +62,8 @@ impl Compile for Ir { format!("expr{}", expr_id.0) } Ir::Builtin(_) => "Nix.builtins".to_string(), + Ir::ConcatStrings(x) => x.compile(ctx), + Ir::HasAttr(x) => x.compile(ctx), ir => todo!("{ir:?}"), } } @@ -247,3 +264,46 @@ impl Compile for List { format!("[{list}]") } } + +impl Compile 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 = 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 Compile 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 + } +} diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index 44aa6bb..2720a27 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -98,6 +98,9 @@ impl Context { } pub fn eval(&mut self, expr: &str) -> Result { + // 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); if !root.errors().is_empty() { 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 = format!("Nix.force({})", code); println!("[DEBUG] generated code: {}", &code); - crate::runtime::run(&code) + crate::runtime::run(code, self) } } @@ -168,9 +171,8 @@ mod test { let tests = [ ("1 + 1", Value::Const(Const::Int(2))), ("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(false))), ("2 < 1", Value::Const(Const::Bool(false))), @@ -696,4 +698,137 @@ mod test { 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(); + } } diff --git a/nix-js/src/ir/downgrade.rs b/nix-js/src/ir/downgrade.rs index 0931383..6a571d0 100644 --- a/nix-js/src/ir/downgrade.rs +++ b/nix-js/src/ir/downgrade.rs @@ -55,20 +55,74 @@ impl Downgrade for ast::IfElse { impl Downgrade for ast::Path { fn downgrade(self, ctx: &mut Ctx) -> Result { - let parts = self - .parts() - .map(|part| match part { - ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr( - Str { - val: lit.to_string(), - } - .to_ir(), - )), - ast::InterpolPart::Interpolation(interpol) => { - interpol.expr().unwrap().downgrade(ctx) + // Collect all parts and check if there are any interpolations + 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, } - }) - .collect::>>()?; + .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 { + ast::InterpolPart::Literal(lit) => { + // Keep literal as-is (don't resolve) + Ok(ctx.new_expr( + Str { + val: lit.to_string(), + } + .to_ir(), + )) + } + ast::InterpolPart::Interpolation(interpol) => { + interpol.expr().unwrap().downgrade(ctx) + } + }) + .collect::>>()? + }; + let expr = if parts.len() == 1 { parts.into_iter().next().unwrap() } else { diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 157ad88..1643091 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -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 = - RefCell::new(v8::Isolate::new(Default::default())); + static CONTEXT_HOLDER: RefCell>> = const { RefCell::new(None) }; } -pub fn run(script: &str) -> Result { - 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> = 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 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 { + 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 { + 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 { + // 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>, @@ -72,66 +265,37 @@ impl<'a, 'b> RuntimeContext<'a, 'b> { } } -fn run_impl(script: &str, isolate: &mut v8::Isolate) -> Result { - 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 { + 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_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("", 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"),