refactor: avoid global state
This commit is contained in:
72
Cargo.lock
generated
72
Cargo.lock
generated
@@ -490,6 +490,12 @@ version = "3.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fd-lock"
|
||||
version = "4.0.4"
|
||||
@@ -497,7 +503,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustix 1.0.8",
|
||||
"rustix 1.1.3",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -627,6 +633,18 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
@@ -889,9 +907,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
version = "0.2.179"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -927,9 +945,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -1039,8 +1057,8 @@ dependencies = [
|
||||
"rnix",
|
||||
"rustyline",
|
||||
"string-interner",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"v8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1210,6 +1228,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
@@ -1347,15 +1371,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.8"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.4",
|
||||
"windows-sys 0.60.2",
|
||||
"linux-raw-sys 0.11.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1629,6 +1653,19 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix 1.1.3",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "temporal_capi"
|
||||
version = "0.1.2"
|
||||
@@ -1832,6 +1869,15 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.1+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.106"
|
||||
@@ -2098,6 +2144,12 @@ version = "0.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.2"
|
||||
|
||||
@@ -18,10 +18,12 @@ thiserror = "2"
|
||||
string-interner = "0.19"
|
||||
itertools = "0.14"
|
||||
|
||||
v8 = "142.2"
|
||||
deno_core = "0.376"
|
||||
deno_error = "0.7"
|
||||
|
||||
rnix = "0.12"
|
||||
|
||||
nix-js-macros = { path = "../nix-js-macros" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.24"
|
||||
|
||||
@@ -10,7 +10,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
args.next();
|
||||
let expr = args.next().unwrap();
|
||||
match Context::new().eval(&expr) {
|
||||
match Context::new().eval_code(&expr) {
|
||||
Ok(value) => {
|
||||
println!("{value}");
|
||||
Ok(())
|
||||
|
||||
@@ -30,7 +30,7 @@ fn main() -> Result<()> {
|
||||
eprintln!("Error: {}", err);
|
||||
} */
|
||||
} else {
|
||||
match context.eval(&line) {
|
||||
match context.eval_code(&line) {
|
||||
Ok(value) => println!("{value}"),
|
||||
Err(err) => eprintln!("Error: {err}"),
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
@@ -17,6 +18,7 @@ pub struct Context {
|
||||
irs: Vec<Ir>,
|
||||
symbols: DefaultStringInterner,
|
||||
global: NonNull<HashMap<SymId, ExprId>>,
|
||||
path_stack: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Drop for Context {
|
||||
@@ -27,6 +29,32 @@ impl Drop for Context {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PathDropGuard<'ctx> {
|
||||
ctx: &'ctx mut Context,
|
||||
}
|
||||
|
||||
impl<'ctx> PathDropGuard<'ctx> {
|
||||
pub fn new(path: PathBuf, ctx: &'ctx mut Context) -> Self {
|
||||
ctx.path_stack.push(path);
|
||||
Self { ctx }
|
||||
}
|
||||
pub fn new_cwd(ctx: &'ctx mut Context) -> Self {
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let virtual_file = cwd.join("__eval__.nix");
|
||||
ctx.path_stack.push(virtual_file);
|
||||
Self { ctx }
|
||||
}
|
||||
pub fn as_ctx(&mut self) -> &mut Context {
|
||||
self.ctx
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PathDropGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.ctx.path_stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Context {
|
||||
fn default() -> Self {
|
||||
use crate::ir::{Attr, Builtins, Select, ToIr};
|
||||
@@ -82,6 +110,7 @@ impl Default for Context {
|
||||
symbols,
|
||||
irs,
|
||||
global: unsafe { NonNull::new_unchecked(Box::leak(Box::new(global))) },
|
||||
path_stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,21 +126,26 @@ impl Context {
|
||||
DowngradeCtx::new(self, global_ref)
|
||||
}
|
||||
|
||||
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();
|
||||
pub fn eval_code(&mut self, expr: &str) -> Result<Value> {
|
||||
// Initialize `path_stack` with current directory for relative path resolution
|
||||
let mut guard = PathDropGuard::new_cwd(self);
|
||||
let ctx = guard.as_ctx();
|
||||
|
||||
let root = rnix::Root::parse(expr);
|
||||
if !root.errors().is_empty() {
|
||||
return Err(Error::parse_error(root.errors().iter().join("; ")));
|
||||
}
|
||||
let root = self
|
||||
let root = ctx
|
||||
.downgrade_ctx()
|
||||
.downgrade(root.tree().expr().unwrap())?;
|
||||
let code = self.get_ir(root).compile(self);
|
||||
let code = ctx.get_ir(root).compile(ctx);
|
||||
let code = format!("Nix.force({})", code);
|
||||
println!("[DEBUG] generated code: {}", &code);
|
||||
crate::runtime::run(code, self)
|
||||
crate::runtime::run(code, ctx)
|
||||
}
|
||||
|
||||
pub fn get_current_dir(&self) -> PathBuf {
|
||||
self.path_stack.last().unwrap().parent().unwrap().to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,32 +169,32 @@ mod test {
|
||||
#[test]
|
||||
fn basic_eval() {
|
||||
assert_eq!(
|
||||
Context::new().eval("1 + 1").unwrap(),
|
||||
Context::new().eval_code("1 + 1").unwrap(),
|
||||
Value::Const(Const::Int(2))
|
||||
);
|
||||
assert_eq!(
|
||||
Context::new().eval("(x: x) 1").unwrap(),
|
||||
Context::new().eval_code("(x: x) 1").unwrap(),
|
||||
Value::Const(Const::Int(1))
|
||||
);
|
||||
assert_eq!(
|
||||
Context::new().eval("(x: y: x - y) 2 1").unwrap(),
|
||||
Context::new().eval_code("(x: y: x - y) 2 1").unwrap(),
|
||||
Value::Const(Const::Int(1))
|
||||
);
|
||||
assert_eq!(
|
||||
Context::new().eval("rec { b = a; a = 1; }.b").unwrap(),
|
||||
Context::new().eval_code("rec { b = a; a = 1; }.b").unwrap(),
|
||||
Value::Const(Const::Int(1))
|
||||
);
|
||||
assert_eq!(
|
||||
Context::new().eval("let b = a; a = 1; in b").unwrap(),
|
||||
Context::new().eval_code("let b = a; a = 1; in b").unwrap(),
|
||||
Value::Const(Const::Int(1))
|
||||
);
|
||||
assert_eq!(
|
||||
Context::new().eval("let fib = n: if n == 1 || n == 2 then 1 else (fib (n - 1)) + (fib (n - 2)); in fib 30").unwrap(),
|
||||
Context::new().eval_code("let fib = n: if n == 1 || n == 2 then 1 else (fib (n - 1)) + (fib (n - 2)); in fib 30").unwrap(),
|
||||
Value::Const(Const::Int(832040))
|
||||
);
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("((f: let x = f x; in x)(self: { x = 1; y = self.x + 1; })).y")
|
||||
.eval_code("((f: let x = f x; in x)(self: { x = 1; y = self.x + 1; })).y")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(2))
|
||||
);
|
||||
@@ -203,7 +237,7 @@ mod test {
|
||||
),
|
||||
];
|
||||
for (expr, expected) in tests {
|
||||
assert_eq!(Context::new().eval(expr).unwrap(), expected);
|
||||
assert_eq!(Context::new().eval_code(expr).unwrap(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,19 +246,19 @@ mod test {
|
||||
// Test function with required parameters
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("({ a, b }: a + b) { a = 1; b = 2; }")
|
||||
.eval_code("({ a, b }: a + b) { a = 1; b = 2; }")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(3))
|
||||
);
|
||||
|
||||
// Test missing required parameter should fail
|
||||
let result = Context::new().eval("({ a, b }: a + b) { a = 1; }");
|
||||
let result = Context::new().eval_code("({ a, b }: a + b) { a = 1; }");
|
||||
assert!(result.is_err());
|
||||
|
||||
// Test all required parameters present
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }")
|
||||
.eval_code("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(6))
|
||||
);
|
||||
@@ -233,13 +267,13 @@ mod test {
|
||||
#[test]
|
||||
fn test_param_check_allowed() {
|
||||
// Test function without ellipsis - should reject unexpected arguments
|
||||
let result = Context::new().eval("({ a, b }: a + b) { a = 1; b = 2; c = 3; }");
|
||||
let result = Context::new().eval_code("({ a, b }: a + b) { a = 1; b = 2; c = 3; }");
|
||||
assert!(result.is_err());
|
||||
|
||||
// Test function with ellipsis - should accept extra arguments
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }")
|
||||
.eval_code("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(3))
|
||||
);
|
||||
@@ -250,7 +284,7 @@ mod test {
|
||||
// Test function with default parameters
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("({ a, b ? 5 }: a + b) { a = 1; }")
|
||||
.eval_code("({ a, b ? 5 }: a + b) { a = 1; }")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(6))
|
||||
);
|
||||
@@ -258,7 +292,7 @@ mod test {
|
||||
// Test overriding default parameter
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("({ a, b ? 5 }: a + b) { a = 1; b = 10; }")
|
||||
.eval_code("({ a, b ? 5 }: a + b) { a = 1; b = 10; }")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(11))
|
||||
);
|
||||
@@ -269,7 +303,7 @@ mod test {
|
||||
// Test function with @ pattern (alias)
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }")
|
||||
.eval_code("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(3))
|
||||
);
|
||||
@@ -280,14 +314,14 @@ mod test {
|
||||
// Test simple parameter (no pattern) should not have validation
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("(x: x.a + x.b) { a = 1; b = 2; }")
|
||||
.eval_code("(x: x.a + x.b) { a = 1; b = 2; }")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(3))
|
||||
);
|
||||
|
||||
// Simple parameter accepts any argument
|
||||
assert_eq!(
|
||||
Context::new().eval("(x: x) 42").unwrap(),
|
||||
Context::new().eval_code("(x: x) 42").unwrap(),
|
||||
Value::Const(Const::Int(42))
|
||||
);
|
||||
}
|
||||
@@ -295,7 +329,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_builtins_basic_access() {
|
||||
// Test that builtins identifier is accessible
|
||||
let result = Context::new().eval("builtins").unwrap();
|
||||
let result = Context::new().eval_code("builtins").unwrap();
|
||||
// Should return an AttrSet with builtin functions
|
||||
assert!(matches!(result, Value::AttrSet(_)));
|
||||
}
|
||||
@@ -303,7 +337,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_builtins_self_reference() {
|
||||
// Test builtins.builtins (self-reference as thunk)
|
||||
let result = Context::new().eval("builtins.builtins").unwrap();
|
||||
let result = Context::new().eval_code("builtins.builtins").unwrap();
|
||||
assert!(matches!(result, Value::AttrSet(_)));
|
||||
}
|
||||
|
||||
@@ -311,7 +345,7 @@ mod test {
|
||||
fn test_builtin_function_add() {
|
||||
// Test calling builtin function: builtins.add 1 2
|
||||
assert_eq!(
|
||||
Context::new().eval("builtins.add 1 2").unwrap(),
|
||||
Context::new().eval_code("builtins.add 1 2").unwrap(),
|
||||
Value::Const(Const::Int(3))
|
||||
);
|
||||
}
|
||||
@@ -320,7 +354,7 @@ mod test {
|
||||
fn test_builtin_function_length() {
|
||||
// Test builtin with list: builtins.length [1 2 3]
|
||||
assert_eq!(
|
||||
Context::new().eval("builtins.length [1 2 3]").unwrap(),
|
||||
Context::new().eval_code("builtins.length [1 2 3]").unwrap(),
|
||||
Value::Const(Const::Int(3))
|
||||
);
|
||||
}
|
||||
@@ -329,7 +363,7 @@ mod test {
|
||||
fn test_builtin_function_map() {
|
||||
// Test higher-order builtin: map (x: x * 2) [1 2 3]
|
||||
assert_eq!(
|
||||
Context::new().eval("map (x: x * 2) [1 2 3]").unwrap(),
|
||||
Context::new().eval_code("map (x: x * 2) [1 2 3]").unwrap(),
|
||||
Value::List(List::new(vec![
|
||||
Value::Const(Const::Int(2)),
|
||||
Value::Const(Const::Int(4)),
|
||||
@@ -343,7 +377,7 @@ mod test {
|
||||
// Test predicate builtin: builtins.filter (x: x > 1) [1 2 3]
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("builtins.filter (x: x > 1) [1 2 3]")
|
||||
.eval_code("builtins.filter (x: x > 1) [1 2 3]")
|
||||
.unwrap(),
|
||||
Value::List(List::new(vec![
|
||||
Value::Const(Const::Int(2)),
|
||||
@@ -356,7 +390,7 @@ mod test {
|
||||
fn test_builtin_function_attrnames() {
|
||||
// Test builtins.attrNames { a = 1; b = 2; }
|
||||
let result = Context::new()
|
||||
.eval("builtins.attrNames { a = 1; b = 2; }")
|
||||
.eval_code("builtins.attrNames { a = 1; b = 2; }")
|
||||
.unwrap();
|
||||
// Should return a list of attribute names
|
||||
assert!(matches!(result, Value::List(_)));
|
||||
@@ -370,7 +404,7 @@ mod test {
|
||||
fn test_builtin_function_head() {
|
||||
// Test builtins.head [1 2 3]
|
||||
assert_eq!(
|
||||
Context::new().eval("builtins.head [1 2 3]").unwrap(),
|
||||
Context::new().eval_code("builtins.head [1 2 3]").unwrap(),
|
||||
Value::Const(Const::Int(1))
|
||||
);
|
||||
}
|
||||
@@ -379,7 +413,7 @@ mod test {
|
||||
fn test_builtin_function_tail() {
|
||||
// Test builtins.tail [1 2 3]
|
||||
assert_eq!(
|
||||
Context::new().eval("builtins.tail [1 2 3]").unwrap(),
|
||||
Context::new().eval_code("builtins.tail [1 2 3]").unwrap(),
|
||||
Value::List(List::new(vec![
|
||||
Value::Const(Const::Int(2)),
|
||||
Value::Const(Const::Int(3)),
|
||||
@@ -392,7 +426,7 @@ mod test {
|
||||
// Test builtins in let binding
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("let b = builtins; in b.add 5 3")
|
||||
.eval_code("let b = builtins; in b.add 5 3")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(8))
|
||||
);
|
||||
@@ -402,7 +436,7 @@ mod test {
|
||||
fn test_builtin_in_with() {
|
||||
// Test builtins with 'with' expression
|
||||
assert_eq!(
|
||||
Context::new().eval("with builtins; add 10 20").unwrap(),
|
||||
Context::new().eval_code("with builtins; add 10 20").unwrap(),
|
||||
Value::Const(Const::Int(30))
|
||||
);
|
||||
}
|
||||
@@ -412,7 +446,7 @@ mod test {
|
||||
// Test nested function calls with builtins
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("builtins.add (builtins.mul 2 3) (builtins.sub 10 5)")
|
||||
.eval_code("builtins.add (builtins.mul 2 3) (builtins.sub 10 5)")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(11)) // (2*3) + (10-5) = 6 + 5 = 11
|
||||
);
|
||||
@@ -422,23 +456,23 @@ mod test {
|
||||
fn test_builtin_type_checks() {
|
||||
// Test type checking functions
|
||||
assert_eq!(
|
||||
Context::new().eval("builtins.isList [1 2 3]").unwrap(),
|
||||
Context::new().eval_code("builtins.isList [1 2 3]").unwrap(),
|
||||
Value::Const(Const::Bool(true))
|
||||
);
|
||||
assert_eq!(
|
||||
Context::new().eval("builtins.isAttrs { a = 1; }").unwrap(),
|
||||
Context::new().eval_code("builtins.isAttrs { a = 1; }").unwrap(),
|
||||
Value::Const(Const::Bool(true))
|
||||
);
|
||||
assert_eq!(
|
||||
Context::new().eval("builtins.isFunction (x: x)").unwrap(),
|
||||
Context::new().eval_code("builtins.isFunction (x: x)").unwrap(),
|
||||
Value::Const(Const::Bool(true))
|
||||
);
|
||||
assert_eq!(
|
||||
Context::new().eval("builtins.isNull null").unwrap(),
|
||||
Context::new().eval_code("builtins.isNull null").unwrap(),
|
||||
Value::Const(Const::Bool(true))
|
||||
);
|
||||
assert_eq!(
|
||||
Context::new().eval("builtins.isBool true").unwrap(),
|
||||
Context::new().eval_code("builtins.isBool true").unwrap(),
|
||||
Value::Const(Const::Bool(true))
|
||||
);
|
||||
}
|
||||
@@ -448,7 +482,7 @@ mod test {
|
||||
// Test that user can shadow builtins (Nix allows this)
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("let builtins = { add = x: y: x - y; }; in builtins.add 5 3")
|
||||
.eval_code("let builtins = { add = x: y: x - y; }; in builtins.add 5 3")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(2)) // Uses shadowed version
|
||||
);
|
||||
@@ -459,7 +493,7 @@ mod test {
|
||||
// Test that builtins.builtins is lazy (thunk)
|
||||
// This should not cause infinite recursion
|
||||
let result = Context::new()
|
||||
.eval("builtins.builtins.builtins.add 1 1")
|
||||
.eval_code("builtins.builtins.builtins.add 1 1")
|
||||
.unwrap();
|
||||
assert_eq!(result, Value::Const(Const::Int(2)));
|
||||
}
|
||||
@@ -468,7 +502,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_free_global_true() {
|
||||
assert_eq!(
|
||||
Context::new().eval("true").unwrap(),
|
||||
Context::new().eval_code("true").unwrap(),
|
||||
Value::Const(Const::Bool(true))
|
||||
);
|
||||
}
|
||||
@@ -476,7 +510,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_free_global_false() {
|
||||
assert_eq!(
|
||||
Context::new().eval("false").unwrap(),
|
||||
Context::new().eval_code("false").unwrap(),
|
||||
Value::Const(Const::Bool(false))
|
||||
);
|
||||
}
|
||||
@@ -484,7 +518,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_free_global_null() {
|
||||
assert_eq!(
|
||||
Context::new().eval("null").unwrap(),
|
||||
Context::new().eval_code("null").unwrap(),
|
||||
Value::Const(Const::Null)
|
||||
);
|
||||
}
|
||||
@@ -493,7 +527,7 @@ mod test {
|
||||
fn test_free_global_map() {
|
||||
// Test free global function: map (x: x * 2) [1 2 3]
|
||||
assert_eq!(
|
||||
Context::new().eval("map (x: x * 2) [1 2 3]").unwrap(),
|
||||
Context::new().eval_code("map (x: x * 2) [1 2 3]").unwrap(),
|
||||
Value::List(List::new(vec![
|
||||
Value::Const(Const::Int(2)),
|
||||
Value::Const(Const::Int(4)),
|
||||
@@ -506,11 +540,11 @@ mod test {
|
||||
fn test_free_global_isnull() {
|
||||
// Test isNull function
|
||||
assert_eq!(
|
||||
Context::new().eval("isNull null").unwrap(),
|
||||
Context::new().eval_code("isNull null").unwrap(),
|
||||
Value::Const(Const::Bool(true))
|
||||
);
|
||||
assert_eq!(
|
||||
Context::new().eval("isNull 5").unwrap(),
|
||||
Context::new().eval_code("isNull 5").unwrap(),
|
||||
Value::Const(Const::Bool(false))
|
||||
);
|
||||
}
|
||||
@@ -519,12 +553,12 @@ mod test {
|
||||
fn test_free_global_shadowing() {
|
||||
// Test shadowing of free globals
|
||||
assert_eq!(
|
||||
Context::new().eval("let true = false; in true").unwrap(),
|
||||
Context::new().eval_code("let true = false; in true").unwrap(),
|
||||
Value::Const(Const::Bool(false))
|
||||
);
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("let map = x: y: x; in map 1 2")
|
||||
.eval_code("let map = x: y: x; in map 1 2")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(1))
|
||||
);
|
||||
@@ -535,7 +569,7 @@ mod test {
|
||||
// Test mixing free globals in expressions
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("if true then map (x: x + 1) [1 2] else []")
|
||||
.eval_code("if true then map (x: x + 1) [1 2] else []")
|
||||
.unwrap(),
|
||||
Value::List(List::new(vec![
|
||||
Value::Const(Const::Int(2)),
|
||||
@@ -549,7 +583,7 @@ mod test {
|
||||
// Test free globals in let bindings
|
||||
assert_eq!(
|
||||
Context::new()
|
||||
.eval("let x = true; y = false; in x && y")
|
||||
.eval_code("let x = true; y = false; in x && y")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Bool(false))
|
||||
);
|
||||
@@ -562,20 +596,20 @@ mod test {
|
||||
|
||||
// Test large i64 values
|
||||
assert_eq!(
|
||||
ctx.eval("9223372036854775807").unwrap(),
|
||||
ctx.eval_code("9223372036854775807").unwrap(),
|
||||
Value::Const(Const::Int(9223372036854775807))
|
||||
);
|
||||
|
||||
// Test negative large value
|
||||
// Can't use -9223372036854775808 since unary minus is actually desugared to (0 - <expr>)
|
||||
assert_eq!(
|
||||
ctx.eval("-9223372036854775807").unwrap(),
|
||||
ctx.eval_code("-9223372036854775807").unwrap(),
|
||||
Value::Const(Const::Int(-9223372036854775807))
|
||||
);
|
||||
|
||||
// Test large number arithmetic
|
||||
assert_eq!(
|
||||
ctx.eval("5000000000000000000 + 3000000000000000000")
|
||||
ctx.eval_code("5000000000000000000 + 3000000000000000000")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(8000000000000000000i64))
|
||||
);
|
||||
@@ -587,45 +621,45 @@ mod test {
|
||||
|
||||
// isInt tests
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.isInt 42").unwrap(),
|
||||
ctx.eval_code("builtins.isInt 42").unwrap(),
|
||||
Value::Const(Const::Bool(true))
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.isInt 42.0").unwrap(),
|
||||
ctx.eval_code("builtins.isInt 42.0").unwrap(),
|
||||
Value::Const(Const::Bool(false))
|
||||
);
|
||||
|
||||
// isFloat tests
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.isFloat 42").unwrap(),
|
||||
ctx.eval_code("builtins.isFloat 42").unwrap(),
|
||||
Value::Const(Const::Bool(false))
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.isFloat 42.5").unwrap(),
|
||||
ctx.eval_code("builtins.isFloat 42.5").unwrap(),
|
||||
Value::Const(Const::Bool(true))
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.isFloat 1.0").unwrap(),
|
||||
ctx.eval_code("builtins.isFloat 1.0").unwrap(),
|
||||
Value::Const(Const::Bool(true))
|
||||
);
|
||||
|
||||
// typeOf tests
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.typeOf 1").unwrap(),
|
||||
ctx.eval_code("builtins.typeOf 1").unwrap(),
|
||||
Value::String("int".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.typeOf 1.0").unwrap(),
|
||||
ctx.eval_code("builtins.typeOf 1.0").unwrap(),
|
||||
Value::String("float".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.typeOf 3.14").unwrap(),
|
||||
ctx.eval_code("builtins.typeOf 3.14").unwrap(),
|
||||
Value::String("float".to_string())
|
||||
);
|
||||
|
||||
// literal tests
|
||||
assert_eq!(ctx.eval("1").unwrap(), Value::Const(Const::Int(1)));
|
||||
assert_eq!(ctx.eval("1.").unwrap(), Value::Const(Const::Float(1.)))
|
||||
assert_eq!(ctx.eval_code("1").unwrap(), Value::Const(Const::Int(1)));
|
||||
assert_eq!(ctx.eval_code("1.").unwrap(), Value::Const(Const::Float(1.)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -634,25 +668,25 @@ mod test {
|
||||
|
||||
// int + int = int
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.typeOf (1 + 2)").unwrap(),
|
||||
ctx.eval_code("builtins.typeOf (1 + 2)").unwrap(),
|
||||
Value::String("int".to_string())
|
||||
);
|
||||
|
||||
// int + float = float
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.typeOf (1 + 2.0)").unwrap(),
|
||||
ctx.eval_code("builtins.typeOf (1 + 2.0)").unwrap(),
|
||||
Value::String("float".to_string())
|
||||
);
|
||||
|
||||
// int * int = int
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.typeOf (3 * 4)").unwrap(),
|
||||
ctx.eval_code("builtins.typeOf (3 * 4)").unwrap(),
|
||||
Value::String("int".to_string())
|
||||
);
|
||||
|
||||
// int * float = float
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.typeOf (3 * 4.0)").unwrap(),
|
||||
ctx.eval_code("builtins.typeOf (3 * 4.0)").unwrap(),
|
||||
Value::String("float".to_string())
|
||||
);
|
||||
}
|
||||
@@ -661,24 +695,24 @@ mod test {
|
||||
fn test_integer_division() {
|
||||
let mut ctx = Context::new();
|
||||
|
||||
assert_eq!(ctx.eval("5 / 2").unwrap(), Value::Const(Const::Int(2)));
|
||||
assert_eq!(ctx.eval_code("5 / 2").unwrap(), Value::Const(Const::Int(2)));
|
||||
|
||||
assert_eq!(ctx.eval("7 / 3").unwrap(), Value::Const(Const::Int(2)));
|
||||
assert_eq!(ctx.eval_code("7 / 3").unwrap(), Value::Const(Const::Int(2)));
|
||||
|
||||
assert_eq!(ctx.eval("10 / 3").unwrap(), Value::Const(Const::Int(3)));
|
||||
assert_eq!(ctx.eval_code("10 / 3").unwrap(), Value::Const(Const::Int(3)));
|
||||
|
||||
// Float division returns float
|
||||
assert_eq!(
|
||||
ctx.eval("5 / 2.0").unwrap(),
|
||||
ctx.eval_code("5 / 2.0").unwrap(),
|
||||
Value::Const(Const::Float(2.5))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ctx.eval("7.0 / 2").unwrap(),
|
||||
ctx.eval_code("7.0 / 2").unwrap(),
|
||||
Value::Const(Const::Float(3.5))
|
||||
);
|
||||
|
||||
assert_eq!(ctx.eval("(-7) / 3").unwrap(), Value::Const(Const::Int(-2)));
|
||||
assert_eq!(ctx.eval_code("(-7) / 3").unwrap(), Value::Const(Const::Int(-2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -687,101 +721,66 @@ mod test {
|
||||
|
||||
// Test builtin add with large numbers
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.add 5000000000000000000 3000000000000000000")
|
||||
ctx.eval_code("builtins.add 5000000000000000000 3000000000000000000")
|
||||
.unwrap(),
|
||||
Value::Const(Const::Int(8000000000000000000i64))
|
||||
);
|
||||
|
||||
// Test builtin mul with large numbers
|
||||
assert_eq!(
|
||||
ctx.eval("builtins.mul 1000000000 1000000000").unwrap(),
|
||||
ctx.eval_code("builtins.mul 1000000000 1000000000").unwrap(),
|
||||
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 temp_dir = tempfile::tempdir().unwrap();
|
||||
let lib_path = temp_dir.path().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);
|
||||
std::fs::write(&lib_path, "{ add = a: b: a + b; }").unwrap();
|
||||
|
||||
// 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();
|
||||
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Const(Const::Int(8)));
|
||||
}
|
||||
|
||||
#[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();
|
||||
let temp_dir = tempfile::tempdir().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);
|
||||
let lib_path = temp_dir.path().join("lib.nix");
|
||||
std::fs::write(&lib_path, "{ add = a: b: a + b; }").unwrap();
|
||||
|
||||
// Create main.nix that imports lib.nix
|
||||
let main_path = temp_dir.join("main.nix");
|
||||
let main_path = temp_dir.path().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);
|
||||
std::fs::write(&main_path, main_content).unwrap();
|
||||
|
||||
// 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();
|
||||
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Const(Const::Int(30)));
|
||||
}
|
||||
|
||||
#[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");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let subdir = temp_dir.path().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);
|
||||
let lib_path = temp_dir.path().join("lib.nix");
|
||||
std::fs::write(&lib_path, "{ multiply = a: b: a * b; }").unwrap();
|
||||
|
||||
// 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);
|
||||
std::fs::write(&helper_path, "{ subtract = a: b: a - b; }").unwrap();
|
||||
|
||||
// Create main.nix with relative path imports
|
||||
let main_path = temp_dir.join("main.nix");
|
||||
let main_path = temp_dir.path().join("main.nix");
|
||||
let main_content = r#"
|
||||
let
|
||||
lib = import ./lib.nix;
|
||||
@@ -791,44 +790,24 @@ in {
|
||||
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);
|
||||
std::fs::write(&main_path, main_content).unwrap();
|
||||
|
||||
// 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)));
|
||||
assert_eq!(ctx.eval_code(&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();
|
||||
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Const(Const::Int(7)));
|
||||
}
|
||||
|
||||
#[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 temp_dir = tempfile::tempdir().unwrap();
|
||||
let func_path = temp_dir.path().join("nix_test_func.nix");
|
||||
std::fs::write(&func_path, "x: x * 2").unwrap();
|
||||
|
||||
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();
|
||||
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Const(Const::Int(10)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,4 +164,8 @@ impl DowngradeContext for DowngradeCtx<'_> {
|
||||
let mut guard = ScopeGuard { ctx: self };
|
||||
f(guard.as_ctx())
|
||||
}
|
||||
|
||||
fn get_current_dir(&self) -> std::path::PathBuf {
|
||||
self.ctx.get_current_dir()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ pub trait DowngradeContext {
|
||||
fn with_with_scope<F, R>(&mut self, namespace: ExprId, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Self) -> R;
|
||||
|
||||
fn get_current_dir(&self) -> std::path::PathBuf;
|
||||
}
|
||||
|
||||
ir! {
|
||||
|
||||
@@ -55,14 +55,13 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::IfElse {
|
||||
|
||||
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path {
|
||||
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
|
||||
// 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
|
||||
// Resolve at compile time
|
||||
let path_str: String = parts_ast
|
||||
.into_iter()
|
||||
.filter_map(|part| match part {
|
||||
@@ -71,20 +70,10 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path {
|
||||
})
|
||||
.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())
|
||||
});
|
||||
let current_dir = ctx.get_current_dir();
|
||||
|
||||
current_dir
|
||||
.join(&path_str)
|
||||
@@ -99,16 +88,13 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path {
|
||||
.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
|
||||
// Resolve at 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(),
|
||||
@@ -183,9 +169,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::AttrSet {
|
||||
}
|
||||
|
||||
// rec { a = 1; b = a; } => let a = 1; b = a; in { inherit a b; }
|
||||
|
||||
let entries: Vec<_> = self.entries().collect();
|
||||
|
||||
let (bindings, body) = downgrade_let_bindings(entries, ctx, |ctx, binding_keys| {
|
||||
// Create plain attrset as body with inherit
|
||||
let mut attrs = AttrSet {
|
||||
@@ -201,7 +185,6 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::AttrSet {
|
||||
Ok(ctx.new_expr(attrs.to_ir()))
|
||||
})?;
|
||||
|
||||
// Create Let expression
|
||||
Ok(ctx.new_expr(Let { bindings, body }.to_ir()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +1,47 @@
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::Once;
|
||||
|
||||
use deno_core::{JsRuntime, RuntimeOptions};
|
||||
use deno_core::{
|
||||
Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8
|
||||
};
|
||||
use deno_error::js_error_wrapper;
|
||||
|
||||
use crate::codegen::{CodegenContext, Compile};
|
||||
use crate::context::Context;
|
||||
use crate::context::{Context, PathDropGuard};
|
||||
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 CONTEXT_HOLDER: RefCell<Option<NonNull<Context>>> = const { RefCell::new(None) };
|
||||
pub trait RuntimeContext {
|
||||
fn split<Ctx: DowngradeContext>(&mut self) -> (&mut JsRuntime, &mut Ctx);
|
||||
}
|
||||
|
||||
// for relative path resolution
|
||||
thread_local! {
|
||||
pub(crate) static IMPORT_PATH_STACK: RefCell<Vec<PathBuf>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
fn nix_runtime(ctx: &mut Context) -> Extension {
|
||||
const ESM: &[ExtensionFileSource] =
|
||||
&deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js");
|
||||
|
||||
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
|
||||
// TODO: SAFETY
|
||||
let ptr = unsafe { NonNull::new_unchecked(ctx) };
|
||||
Extension {
|
||||
name: "nix_runtime",
|
||||
esm_files: Cow::Borrowed(ESM),
|
||||
esm_entry_point: Some("ext:nix_runtime/runtime.js"),
|
||||
ops: Cow::Owned(vec![
|
||||
op_import(),
|
||||
op_read_file(),
|
||||
op_path_exists(),
|
||||
op_resolve_path(),
|
||||
]),
|
||||
op_state_fn: Some(Box::new(move |state| {
|
||||
state.put(RefCell::new(ptr));
|
||||
})),
|
||||
enabled: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -107,23 +73,12 @@ 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() })?;
|
||||
fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result<String, NixError> {
|
||||
let mut ptr = state.borrow::<RefCell<NonNull<Context>>>().borrow_mut();
|
||||
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 current_dir = ctx.get_current_dir();
|
||||
let absolute_path = current_dir
|
||||
.join(&path)
|
||||
.canonicalize()
|
||||
@@ -131,8 +86,9 @@ fn op_import(#[string] path: String) -> std::result::Result<String, 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());
|
||||
// 2. Psh to stack for nested imports (RAII guard ensures pop on drop)
|
||||
let mut guard = PathDropGuard::new(absolute_path.clone(), ctx);
|
||||
let ctx = guard.as_ctx();
|
||||
|
||||
// 3. Read file
|
||||
let content = std::fs::read_to_string(&absolute_path).map_err(|e| -> NixError {
|
||||
@@ -162,7 +118,6 @@ fn op_import(#[string] path: String) -> std::result::Result<String, NixError> {
|
||||
|
||||
// 6. Codegen - returns JS code string
|
||||
Ok(ctx.get_ir(expr_id).compile(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
#[deno_core::op2]
|
||||
@@ -179,21 +134,17 @@ fn op_path_exists(#[string] path: String) -> bool {
|
||||
|
||||
#[deno_core::op2]
|
||||
#[string]
|
||||
fn op_resolve_path(#[string] path: String) -> std::result::Result<String, NixError> {
|
||||
fn op_resolve_path(state: &mut OpState, #[string] path: String) -> std::result::Result<String, NixError> {
|
||||
let ptr = state.borrow::<RefCell<NonNull<Context>>>().borrow();
|
||||
let ctx = unsafe { ptr.as_ref() };
|
||||
|
||||
// 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())
|
||||
});
|
||||
let current_dir = ctx.get_current_dir();
|
||||
|
||||
current_dir
|
||||
.join(&path)
|
||||
@@ -203,13 +154,13 @@ fn op_resolve_path(#[string] path: String) -> std::result::Result<String, NixErr
|
||||
}
|
||||
|
||||
// Runtime context for V8 value conversion
|
||||
struct RuntimeContext<'a, 'b> {
|
||||
struct RuntimeCtx<'a, 'b> {
|
||||
scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>,
|
||||
is_thunk_symbol: Option<v8::Local<'a, v8::Symbol>>,
|
||||
primop_metadata_symbol: Option<v8::Local<'a, v8::Symbol>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> RuntimeContext<'a, 'b> {
|
||||
impl<'a, 'b> RuntimeCtx<'a, 'b> {
|
||||
fn new(scope: &'a v8::PinnedRef<'a, v8::HandleScope<'b>>) -> Self {
|
||||
let is_thunk_symbol = Self::get_is_thunk_symbol(scope);
|
||||
let primop_metadata_symbol = Self::get_primop_metadata_symbol(scope);
|
||||
@@ -255,11 +206,9 @@ impl<'a, 'b> RuntimeContext<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
pub fn run(script: String, ctx: &mut Context) -> Result<Value> {
|
||||
let _guard = ContextGuard::set(ctx);
|
||||
|
||||
pub fn new_js_runtime(ctx: &mut Context) -> JsRuntime {
|
||||
// Initialize V8 once
|
||||
static INIT: Once = Once::new();
|
||||
INIT.call_once(|| {
|
||||
JsRuntime::init_platform(
|
||||
Some(v8::new_default_platform(0, false).make_shared()),
|
||||
@@ -267,31 +216,30 @@ pub fn run(script: String, ctx: &mut Context) -> Result<Value> {
|
||||
);
|
||||
});
|
||||
|
||||
// Create a new JsRuntime for each evaluation to avoid state issues
|
||||
let mut runtime = JsRuntime::new(RuntimeOptions {
|
||||
extensions: vec![nix_extension()],
|
||||
JsRuntime::new(RuntimeOptions {
|
||||
extensions: vec![nix_runtime(ctx)],
|
||||
..Default::default()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Load runtime.js
|
||||
let runtime_code = include_str!("../runtime-ts/dist/runtime.js");
|
||||
runtime
|
||||
.execute_script("<runtime>", runtime_code)
|
||||
.map_err(|e| Error::eval_error(format!("Failed to load runtime: {:?}", e)))?;
|
||||
// Main entry point
|
||||
pub fn run(script: String, ctx: &mut Context) -> Result<Value> {
|
||||
let mut runtime = new_js_runtime(ctx);
|
||||
|
||||
// Execute user script
|
||||
let global_value = runtime
|
||||
.execute_script("<eval>", script)
|
||||
.map_err(|e| Error::eval_error(format!("Execution error: {:?}", e)))?;
|
||||
|
||||
// Retrieve scope from JsRuntime
|
||||
deno_core::scope!(scope, runtime);
|
||||
let local_value = v8::Local::new(scope, &global_value);
|
||||
|
||||
let runtime_ctx = RuntimeContext::new(scope);
|
||||
let runtime_ctx = RuntimeCtx::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 {
|
||||
fn to_value<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeCtx<'a, 'b>) -> Value {
|
||||
let scope = ctx.scope;
|
||||
match () {
|
||||
_ if val.is_big_int() => {
|
||||
@@ -357,7 +305,7 @@ fn to_value<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_thunk<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>) -> bool {
|
||||
fn is_thunk<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeCtx<'a, 'b>) -> bool {
|
||||
if !val.is_object() {
|
||||
return false;
|
||||
}
|
||||
@@ -376,7 +324,7 @@ fn is_thunk<'a, 'b>(val: v8::Local<'a, v8::Value>, ctx: &RuntimeContext<'a, 'b>)
|
||||
/// Check if a function is a primop
|
||||
fn primop_name<'a, 'b>(
|
||||
val: v8::Local<'a, v8::Value>,
|
||||
ctx: &RuntimeContext<'a, 'b>,
|
||||
ctx: &RuntimeCtx<'a, 'b>,
|
||||
) -> Option<String> {
|
||||
if !val.is_function() {
|
||||
return None;
|
||||
@@ -402,7 +350,7 @@ fn primop_name<'a, 'b>(
|
||||
/// Check if a primop is partially applied (has applied > 0)
|
||||
fn primop_app_name<'a, 'b>(
|
||||
val: v8::Local<'a, v8::Value>,
|
||||
ctx: &RuntimeContext<'a, 'b>,
|
||||
ctx: &RuntimeCtx<'a, 'b>,
|
||||
) -> Option<String> {
|
||||
let name = primop_name(val, ctx)?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user