feat: builtins.import
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1029,6 +1029,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deno_core",
|
||||
"deno_error",
|
||||
"derive_more",
|
||||
"hashbrown 0.16.1",
|
||||
"itertools 0.14.0",
|
||||
|
||||
@@ -20,6 +20,7 @@ itertools = "0.14"
|
||||
|
||||
v8 = "142.2"
|
||||
deno_core = "0.376"
|
||||
deno_error = "0.7"
|
||||
|
||||
rnix = "0.12"
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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}'`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
15
nix-js/runtime-ts/src/types/global.d.ts
vendored
Normal file
15
nix-js/runtime-ts/src/types/global.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,8 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": false,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
"rootDir": "./src",
|
||||
"typeRoots": ["./src/types"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
||||
@@ -20,6 +20,21 @@ impl<Ctx: CodegenContext> Compile<Ctx> 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<Ctx: CodegenContext> Compile<Ctx> 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<Ctx: CodegenContext> Compile<Ctx> for 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,9 @@ impl Context {
|
||||
}
|
||||
|
||||
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);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,20 +55,74 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::IfElse {
|
||||
|
||||
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path {
|
||||
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
|
||||
let parts = self
|
||||
.parts()
|
||||
// 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,
|
||||
}
|
||||
.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) => Ok(ctx.new_expr(
|
||||
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::<Result<Vec<_>>>()?;
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
};
|
||||
|
||||
let expr = if parts.len() == 1 {
|
||||
parts.into_iter().next().unwrap()
|
||||
} else {
|
||||
|
||||
@@ -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())
|
||||
});
|
||||
|
||||
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> {
|
||||
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"),
|
||||
|
||||
Reference in New Issue
Block a user