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 = [
|
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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
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,
|
"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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
Reference in New Issue
Block a user