diff --git a/nix-js/src/bin/eval.rs b/nix-js/src/bin/eval.rs index 2924d6f..2675413 100644 --- a/nix-js/src/bin/eval.rs +++ b/nix-js/src/bin/eval.rs @@ -1,6 +1,6 @@ -use std::process::exit; use anyhow::Result; use nix_js::context::Context; +use std::process::exit; fn main() -> Result<()> { let mut args = std::env::args(); @@ -14,10 +14,10 @@ fn main() -> Result<()> { Ok(value) => { println!("{value}"); Ok(()) - }, + } Err(err) => { eprintln!("Error: {err}"); Err(anyhow::anyhow!("{err}")) - }, + } } } diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index f8853a4..baa0d29 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -46,6 +46,7 @@ impl Compile for Ir { &Ir::ExprRef(expr_id) => { format!("expr{}", expr_id.0) } + Ir::Builtin(_) => "Nix.builtins".to_string(), ir => todo!("{ir:?}"), } } @@ -84,12 +85,11 @@ impl Compile for UnOp { let rhs = ctx.get_ir(self.rhs).compile(ctx); match self.kind { Neg => format!("Nix.op.sub(0,{rhs})"), - Not => format!("Nix.op.bnot({rhs})") + Not => format!("Nix.op.bnot({rhs})"), } } } - impl Compile for Func { fn compile(&self, ctx: &Ctx) -> String { let id = ctx.get_ir(self.arg).as_ref().unwrap_arg().0; @@ -189,7 +189,10 @@ impl Compile for Select { let key = ctx.get_sym(*sym); if has_default { let default_val = ctx.get_ir(self.default.unwrap()).compile(ctx); - format!("Nix.select_with_default({}, \"{}\", {})", result, key, default_val) + format!( + "Nix.select_with_default({}, \"{}\", {})", + result, key, default_val + ) } else { format!("Nix.select({}, \"{}\")", result, key) } @@ -198,7 +201,10 @@ impl Compile for Select { let key = ctx.get_ir(*expr_id).compile(ctx); if has_default { let default_val = ctx.get_ir(self.default.unwrap()).compile(ctx); - format!("Nix.select_with_default({}, {}, {})", result, key, default_val) + format!( + "Nix.select_with_default({}, {}, {})", + result, key, default_val + ) } else { format!("Nix.select({}, {})", result, key) } @@ -232,7 +238,11 @@ impl Compile for AttrSet { impl Compile for List { fn compile(&self, ctx: &Ctx) -> String { - let list = self.items.iter().map(|item| ctx.get_ir(*item).compile(ctx)).join(","); + let list = self + .items + .iter() + .map(|item| ctx.get_ir(*item).compile(ctx)) + .join(","); format!("[{list}]") } } diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index fb1a2ee..3d31232 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -28,10 +28,60 @@ impl Drop for Context { impl Default for Context { fn default() -> Self { + use crate::ir::{Attr, Builtin, Select, ToIr}; + + let mut symbols = DefaultStringInterner::new(); + let mut irs = Vec::new(); + let mut global = HashMap::new(); + + irs.push(Builtin.to_ir()); + let builtins_expr = ExprId(0); + + let builtins_sym = symbols.get_or_intern("builtins"); + global.insert(builtins_sym, builtins_expr); + + let free_globals = [ + "true", + "false", + "null", + + "abort", + "baseNameOf", + "break", + "dirOf", + "derivation", + "derivationStrict", + "fetchGit", + "fetchMercurial", + "fetchTarball", + "fetchTree", + "fromTOML", + "import", + "isNull", + "map", + "placeholder", + "removeAttrs", + "scopedImport", + "throw", + "toString", + ]; + + for name in free_globals { + let name_sym = symbols.get_or_intern(name); + let select_ir = Select { + expr: builtins_expr, + attrpath: vec![Attr::Str(name_sym)], + default: None, + }; + let select_expr = ExprId(irs.len()); + irs.push(select_ir.to_ir()); + global.insert(name_sym, select_expr); + } + Self { - symbols: DefaultStringInterner::new(), - irs: Vec::new(), - global: unsafe { NonNull::new_unchecked(Box::leak(Box::new(HashMap::new()))) }, + symbols, + irs, + global: unsafe { NonNull::new_unchecked(Box::leak(Box::new(global))) }, } } } @@ -134,10 +184,8 @@ mod test { ("2 > 1", Value::Const(Const::Bool(true))), ("1 <= 1", Value::Const(Const::Bool(true))), ("1 >= 1", Value::Const(Const::Bool(true))), - ("1 == 1 || (1 / 0)", Value::Const(Const::Bool(true))), - ("1 == 1 && 1 == 0", Value::Const(Const::Bool(false))), - // ("true || (1 / 0)", Value::Const(Const::Bool(true))), - // ("true && 1 == 0", Value::Const(Const::Bool(false))), + ("true || (1 / 0)", Value::Const(Const::Bool(true))), + ("true && 1 == 0", Value::Const(Const::Bool(false))), ( "[ 1 2 3 ] ++ [ 4 5 6 ]", Value::List(List::new( @@ -247,4 +295,269 @@ mod test { Value::Const(Const::Int(42)) ); } + + #[test] + fn test_builtins_basic_access() { + // Test that builtins identifier is accessible + let result = Context::new().eval("builtins").unwrap(); + // Should return an AttrSet with builtin functions + assert!(matches!(result, Value::AttrSet(_))); + } + + #[test] + fn test_builtins_self_reference() { + // Test builtins.builtins (self-reference as thunk) + let result = Context::new().eval("builtins.builtins").unwrap(); + assert!(matches!(result, Value::AttrSet(_))); + } + + #[test] + fn test_builtin_function_add() { + // Test calling builtin function: builtins.add 1 2 + assert_eq!( + Context::new().eval("builtins.add 1 2").unwrap(), + Value::Const(Const::Int(3)) + ); + } + + #[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(), + Value::Const(Const::Int(3)) + ); + } + + #[test] + fn test_builtin_function_map() { + // Test higher-order builtin: builtins.map (x: x * 2) [1 2 3] + assert_eq!( + Context::new() + .eval("builtins.map (x: x * 2) [1 2 3]") + .unwrap(), + Value::List(List::new(vec![ + Value::Const(Const::Int(2)), + Value::Const(Const::Int(4)), + Value::Const(Const::Int(6)), + ])) + ); + } + + #[test] + fn test_builtin_function_filter() { + // Test predicate builtin: builtins.filter (x: x > 1) [1 2 3] + assert_eq!( + Context::new() + .eval("builtins.filter (x: x > 1) [1 2 3]") + .unwrap(), + Value::List(List::new(vec![ + Value::Const(Const::Int(2)), + Value::Const(Const::Int(3)), + ])) + ); + } + + #[test] + fn test_builtin_function_attrnames() { + // Test builtins.attrNames { a = 1; b = 2; } + let result = Context::new() + .eval("builtins.attrNames { a = 1; b = 2; }") + .unwrap(); + // Should return a list of attribute names + assert!(matches!(result, Value::List(_))); + if let Value::List(list) = result { + // List should contain 2 elements + assert_eq!(format!("{:?}", list).matches(',').count() + 1, 2); + } + } + + #[test] + fn test_builtin_function_head() { + // Test builtins.head [1 2 3] + assert_eq!( + Context::new().eval("builtins.head [1 2 3]").unwrap(), + Value::Const(Const::Int(1)) + ); + } + + #[test] + fn test_builtin_function_tail() { + // Test builtins.tail [1 2 3] + assert_eq!( + Context::new().eval("builtins.tail [1 2 3]").unwrap(), + Value::List(List::new(vec![ + Value::Const(Const::Int(2)), + Value::Const(Const::Int(3)), + ])) + ); + } + + #[test] + fn test_builtin_in_let() { + // Test builtins in let binding + assert_eq!( + Context::new() + .eval("let b = builtins; in b.add 5 3") + .unwrap(), + Value::Const(Const::Int(8)) + ); + } + + #[test] + fn test_builtin_in_with() { + // Test builtins with 'with' expression + assert_eq!( + Context::new().eval("with builtins; add 10 20").unwrap(), + Value::Const(Const::Int(30)) + ); + } + + #[test] + fn test_builtin_nested_access() { + // Test nested function calls with builtins + assert_eq!( + Context::new() + .eval("builtins.add (builtins.mul 2 3) (builtins.sub 10 5)") + .unwrap(), + Value::Const(Const::Int(11)) // (2*3) + (10-5) = 6 + 5 = 11 + ); + } + + #[test] + fn test_builtin_type_checks() { + // Test type checking functions + assert_eq!( + Context::new().eval("builtins.isList [1 2 3]").unwrap(), + Value::Const(Const::Bool(true)) + ); + assert_eq!( + Context::new().eval("builtins.isAttrs { a = 1; }").unwrap(), + Value::Const(Const::Bool(true)) + ); + assert_eq!( + Context::new().eval("builtins.isFunction (x: x)").unwrap(), + Value::Const(Const::Bool(true)) + ); + assert_eq!( + Context::new().eval("builtins.isNull null").unwrap(), + Value::Const(Const::Bool(true)) + ); + assert_eq!( + Context::new().eval("builtins.isBool true").unwrap(), + Value::Const(Const::Bool(true)) + ); + } + + #[test] + fn test_builtin_shadowing() { + // 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") + .unwrap(), + Value::Const(Const::Int(2)) // Uses shadowed version + ); + } + + #[test] + fn test_builtin_lazy_evaluation() { + // Test that builtins.builtins is lazy (thunk) + // This should not cause infinite recursion + let result = Context::new() + .eval("builtins.builtins.builtins.add 1 1") + .unwrap(); + assert_eq!(result, Value::Const(Const::Int(2))); + } + + // Free globals tests + #[test] + fn test_free_global_true() { + assert_eq!( + Context::new().eval("true").unwrap(), + Value::Const(Const::Bool(true)) + ); + } + + #[test] + fn test_free_global_false() { + assert_eq!( + Context::new().eval("false").unwrap(), + Value::Const(Const::Bool(false)) + ); + } + + #[test] + fn test_free_global_null() { + assert_eq!( + Context::new().eval("null").unwrap(), + Value::Const(Const::Null) + ); + } + + #[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(), + Value::List(List::new(vec![ + Value::Const(Const::Int(2)), + Value::Const(Const::Int(4)), + Value::Const(Const::Int(6)), + ])) + ); + } + + #[test] + fn test_free_global_isnull() { + // Test isNull function + assert_eq!( + Context::new().eval("isNull null").unwrap(), + Value::Const(Const::Bool(true)) + ); + assert_eq!( + Context::new().eval("isNull 5").unwrap(), + Value::Const(Const::Bool(false)) + ); + } + + #[test] + fn test_free_global_shadowing() { + // Test shadowing of free globals + assert_eq!( + Context::new().eval("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") + .unwrap(), + Value::Const(Const::Int(1)) + ); + } + + #[test] + fn test_free_global_mixed_usage() { + // Test mixing free globals in expressions + assert_eq!( + Context::new() + .eval("if true then map (x: x + 1) [1 2] else []") + .unwrap(), + Value::List(List::new(vec![ + Value::Const(Const::Int(2)), + Value::Const(Const::Int(3)), + ])) + ); + } + + #[test] + fn test_free_global_in_let() { + // Test free globals in let bindings + assert_eq!( + Context::new() + .eval("let x = true; y = false; in x && y") + .unwrap(), + Value::Const(Const::Bool(false)) + ); + } } diff --git a/nix-js/src/context/downgrade.rs b/nix-js/src/context/downgrade.rs index f120417..6afe43d 100644 --- a/nix-js/src/context/downgrade.rs +++ b/nix-js/src/context/downgrade.rs @@ -114,17 +114,20 @@ impl DowngradeContext for DowngradeCtx<'_> { } fn extract_expr(&mut self, id: ExprId) -> Ir { - self.irs.get_mut(id.0).unwrap().take().unwrap() + let local_id = id.0 - self.ctx.irs.len(); + self.irs.get_mut(local_id).unwrap().take().unwrap() } fn replace_expr(&mut self, id: ExprId, expr: Ir) { - let _ = self.irs.get_mut(id.0).unwrap().insert(expr); + let local_id = id.0 - self.ctx.irs.len(); + let _ = self.irs.get_mut(local_id).unwrap().insert(expr); } #[allow(refining_impl_trait)] fn reserve_slots(&mut self, slots: usize) -> impl Iterator + Clone + use<> { + let start = self.ctx.irs.len() + self.irs.len(); self.irs.extend(std::iter::repeat_with(|| None).take(slots)); - (self.irs.len() - slots..self.irs.len()).map(ExprId) + (start..start + slots).map(ExprId) } fn downgrade(mut self, root: rnix::ast::Expr) -> Result { diff --git a/nix-js/src/ir.rs b/nix-js/src/ir.rs index 87cfe59..d9ddbd2 100644 --- a/nix-js/src/ir.rs +++ b/nix-js/src/ir.rs @@ -62,6 +62,7 @@ ir! { PrimOp(PrimOpId), ExprRef(ExprId), Thunk(ExprId), + Builtin, } impl AttrSet { @@ -391,3 +392,8 @@ pub struct Path { /// This can be a simple `Str` or a `ConcatStrings` for interpolated paths. pub expr: ExprId, } + +/// Represents the special `builtins` global object. +/// This is a unit struct with no fields as it maps directly to the runtime builtins. +#[derive(Debug)] +pub struct Builtin; diff --git a/nix-js/src/ir/downgrade.rs b/nix-js/src/ir/downgrade.rs index e838db3..0931383 100644 --- a/nix-js/src/ir/downgrade.rs +++ b/nix-js/src/ir/downgrade.rs @@ -250,9 +250,8 @@ impl Downgrade for ast::LetIn { let entries: Vec<_> = self.entries().collect(); let body_expr = self.body().unwrap(); - let (bindings, body) = downgrade_let_bindings(entries, ctx, |ctx, _binding_keys| { - body_expr.downgrade(ctx) - })?; + let (bindings, body) = + downgrade_let_bindings(entries, ctx, |ctx, _binding_keys| body_expr.downgrade(ctx))?; Ok(ctx.new_expr(Let { bindings, body }.to_ir())) } @@ -357,13 +356,17 @@ impl Downgrade for ast::Lambda { // Downgrade body in Let scope and create Let expression let bindings_vec: Vec = bindings.values().copied().collect(); - let inner_body = ctx.with_let_scope(bindings, |ctx| self.body().unwrap().downgrade(ctx))?; + let inner_body = + ctx.with_let_scope(bindings, |ctx| self.body().unwrap().downgrade(ctx))?; // Create Let expression to wrap the bindings - body = ctx.new_expr(Let { - bindings: bindings_vec, - body: inner_body, - }.to_ir()); + body = ctx.new_expr( + Let { + bindings: bindings_vec, + body: inner_body, + } + .to_ir(), + ); } } diff --git a/nix-js/src/ir/utils.rs b/nix-js/src/ir/utils.rs index 341b364..be21a4b 100644 --- a/nix-js/src/ir/utils.rs +++ b/nix-js/src/ir/utils.rs @@ -1,5 +1,5 @@ -use hashbrown::{HashMap, HashSet}; use hashbrown::hash_map::Entry; +use hashbrown::{HashMap, HashSet}; use rnix::ast; use crate::error::{Error, Result}; diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 56541ed..559a12e 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -31,7 +31,9 @@ fn run_impl(script: &str, isolate: &mut v8::Isolate) -> Result { let runtime_script = v8::Script::compile(scope, runtime_source, None).unwrap(); if runtime_script.run(scope).is_none() { - return Err(Error::eval_error("Failed to initialize runtime".to_string())); + return Err(Error::eval_error( + "Failed to initialize runtime".to_string(), + )); } let source = v8::String::new(scope, script).unwrap(); @@ -47,7 +49,10 @@ fn run_impl(script: &str, isolate: &mut v8::Isolate) -> Result { .to_string(try_catch) .unwrap() .to_rust_string_lossy(try_catch); - return Err(Error::eval_error(format!("Compilation error: {}", exception_string))); + return Err(Error::eval_error(format!( + "Compilation error: {}", + exception_string + ))); } else { return Err(Error::eval_error("Unknown compilation error".to_string())); } @@ -62,7 +67,10 @@ fn run_impl(script: &str, isolate: &mut v8::Isolate) -> Result { .to_string(try_catch) .unwrap() .to_rust_string_lossy(try_catch); - Err(Error::eval_error(format!("Runtime error: {}", exception_string))) + Err(Error::eval_error(format!( + "Runtime error: {}", + exception_string + ))) } else { Err(Error::eval_error("Unknown runtime error".to_string())) } @@ -91,7 +99,7 @@ fn to_value<'a>( } _ if val.is_true() => Value::Const(Const::Bool(true)), _ if val.is_false() => Value::Const(Const::Bool(false)), - _ if val.is_null() => Value::Const(Const::Bool(true)), + _ if val.is_null() => Value::Const(Const::Null), _ if val.is_string() => { let val = val.to_string(scope).unwrap(); Value::String(val.to_rust_string_lossy(scope)) @@ -107,7 +115,12 @@ fn to_value<'a>( .collect(); Value::List(List::new(list)) } + _ if val.is_function() => Value::Func, _ if val.is_object() => { + if is_thunk(val, scope) { + return Value::Thunk; + } + let val = val.to_object(scope).unwrap(); let keys = val .get_own_property_names(scope, v8::GetPropertyNamesArgsBuilder::new().build()) @@ -123,18 +136,42 @@ fn to_value<'a>( .collect(); Value::AttrSet(AttrSet::new(attrs)) } - _ if val.is_function_template() => Value::PrimOp, - _ if val.is_function() => Value::Func, _ => todo!("{}", val.type_repr()), } } +fn is_thunk<'a>( + val: v8::Local<'a, v8::Value>, + scope: &v8::PinnedRef<'a, v8::HandleScope>, +) -> bool { + if !val.is_object() { + return false; + } + + let global = scope.get_current_context().global(scope); + let nix_key = v8::String::new(scope, "Nix").unwrap(); + let nix_obj = match global.get(scope, nix_key.into()) { + Some(obj) if obj.is_object() => obj.to_object(scope).unwrap(), + _ => return false, + }; + + let is_thunk_sym_key = v8::String::new(scope, "IS_THUNK").unwrap(); + let is_thunk_sym = match nix_obj.get(scope, is_thunk_sym_key.into()) { + Some(sym) if sym.is_symbol() => sym, + _ => return false, + }; + + let obj = val.to_object(scope).unwrap(); + matches!(obj.get(scope, is_thunk_sym), Some(v) if v.is_true()) +} + #[test] fn to_value_working() { assert_eq!( run("({ test: [1, 9223372036854775807n, true, false, 'hello world!'] - })").unwrap(), + })") + .unwrap(), Value::AttrSet(AttrSet::new(std::collections::BTreeMap::from([( Symbol::from("test"), Value::List(List::new(vec![ diff --git a/nix-js/src/runtime/runtime.js b/nix-js/src/runtime/runtime.js index b64dac5..5b2ff69 100644 --- a/nix-js/src/runtime/runtime.js +++ b/nix-js/src/runtime/runtime.js @@ -5,7 +5,6 @@ const Nix = (() => { constructor(func) { this[IS_THUNK] = true; this.func = func; - this.is_forced = false; this.result = null; } } @@ -19,14 +18,12 @@ const Nix = (() => { return value; } - if (value.is_forced) { + if (value.func === null) { return value.result; } const result = force(value.func()); value.result = result; - value.is_forced = true; - value.func = null; return result; @@ -53,11 +50,6 @@ const Nix = (() => { }); }; - const trace = (msg, value) => { - console.log(`[TRACE] ${msg}`); - return force(value); - }; - const select = (obj, key) => { const forced_obj = force(obj); const forced_key = force(key); @@ -78,11 +70,11 @@ const Nix = (() => { const forced_key = force(key); if (forced_obj === null || forced_obj === undefined) { - return force(default_val); + return default_val; } if (!(forced_key in forced_obj)) { - return force(default_val); + return default_val; } return forced_obj[forced_key]; @@ -119,19 +111,356 @@ const Nix = (() => { mul: (a, b) => force(a) * force(b), div: (a, b) => force(a) / force(b), - eq: (a, b) => force(a) === force(b), + eq: (a, b) => force(a) === force(b), neq: (a, b) => force(a) !== force(b), - lt: (a, b) => force(a) < force(b), + lt: (a, b) => force(a) < force(b), lte: (a, b) => force(a) <= force(b), - gt: (a, b) => force(a) > force(b), + gt: (a, b) => force(a) > force(b), gte: (a, b) => force(a) >= force(b), band: (a, b) => force(a) && force(b), - bor: (a, b) => force(a) || force(b), + bor: (a, b) => force(a) || force(b), bnot: (a) => !force(a), concat: (a, b) => Array.prototype.concat.apply(force(a), force(b)), - update: (a, b) => ({...force(a), ...force(b)}), + update: (a, b) => ({ ...force(a), ...force(b) }), + }; + + const builtins = { + add: (a) => (b) => force(a) + force(b), + sub: (a) => (b) => force(a) - force(b), + mul: (a) => (b) => force(a) * force(b), + div: (a) => (b) => force(a) / force(b), + bitAnd: (a) => (b) => force(a) & force(b), + bitOr: (a) => (b) => force(a) | force(b), + bitXor: (a) => (b) => force(a) ^ force(b), + lessThan: (a) => (b) => force(a) < force(b), + + ceil: (x) => Math.ceil(force(x)), + floor: (x) => Math.floor(force(x)), + + deepSeq: (e1) => (e2) => { + throw "Not implemented: deepSeq" + }, + seq: (e1) => (e2) => { + force(e1); + return e2; + }, + + fetchClosure: (args) => { + throw "Not implemented: fetchClosure" + }, + fetchGit: (args) => { + throw "Not implemented: fetchGit" + }, + fetchTarball: (args) => { + throw "Not implemented: fetchTarball" + }, + fetchTree: (args) => { + throw "Not implemented: fetchTree" + }, + fetchurl: (args) => { + throw "Not implemented: fetchurl" + }, + + fromJSON: (e) => { + throw "Not implemented: fromJSON" + }, + fromTOML: (e) => { + throw "Not implemented: fromTOML" + }, + toJSON: (e) => { + throw "Not implemented: toJSON" + }, + toXML: (e) => { + throw "Not implemented: toXML" + }, + + getContext: (s) => { + throw "Not implemented: getContext" + }, + hasContext: (s) => { + throw "Not implemented: hasContext" + }, + + hashFile: (type) => (p) => { + throw "Not implemented: hashFile" + }, + hashString: (type) => (p) => { + throw "Not implemented: hashString" + }, + convertHash: (args) => { + throw "Not implemented: convertHash" + }, + + isAttrs: (e) => { + const val = force(e); + return typeof val === "object" && !Array.isArray(val) && val !== null; + }, + isBool: (e) => typeof force(e) === "boolean", + isFloat: (e) => { + throw "Not implemented: isFloat" + }, + isFunction: (e) => typeof force(e) === "function", + isInt: (e) => { + throw "Not implemented: isInt" + }, + isList: (e) => Array.isArray(force(e)), + isNull: (e) => force(e) === null, + isPath: (e) => { + throw "Not implemented: isPath" + }, + isString: (e) => typeof force(e) === "string", + typeOf: (e) => { + throw "Not implemented: typeOf" + }, + + import: (path) => { + throw "Not implemented: import" + }, + scopedImport: (scope) => (path) => { + throw "Not implemented: scopedImport" + }, + + unsafeDiscardOutputDependency: (s) => { + throw "Not implemented: unsafeDiscardOutputDependency" + }, + unsafeDiscardStringContext: (s) => { + throw "Not implemented: unsafeDiscardStringContext" + }, + unsafeGetAttrPos: (s) => { + throw "Not implemented: unsafeGetAttrPos" + }, + + abort: (s) => { + throw `evaluation aborted with the following error message: '${force(s)}'` + }, + addDrvOutputDependencies: (s) => { + throw "Not implemented: addDrvOutputDependencies" + }, + all: (pred) => (list) => Array.prototype.every.call(force(list), force(pred)), + any: (pred) => (list) => Array.prototype.some.call(force(list), force(pred)), + attrNames: (set) => Object.keys(force(set)), + attrValues: (set) => Object.values(force(set)), + baseNameOf: (x) => { + const str = force(x); + if (str.length === 0) + return ""; + let last = str.length - 1; + if (str[last] === "/" && last > 0) + last -= 1; + let pos = last; + while (pos >= 0 && str[pos] !== "/") + pos -= 1; + if (pos !== 0 || (pos === 0 && str[pos] === "/")) { + pos += 1; + } + return String.prototype.substring.call(str, pos, last + 1); + }, + break: (v) => v, + catAttrs: (attr) => (list) => { + const key = force(attr); + return force(list).map((set) => force(set)[key]).filter((val) => val !== undefined); + }, + compareVersions: (s1) => (s2) => { + throw "Not implemented: compareVersions" + }, + concatLists: (lists) => Array.prototype.reduce.call(force(lists), (acc, cur) => Array.prototype.concat(acc, force(cur)), []), + concatMap: (f) => (lists) => { + const fn = force(f); + return Array.prototype.reduce.call(force(lists), (acc, cur) => Array.prototype.concat(acc, force(fn(cur))), []); + }, + concatStringsSep: (sep) => (list) => Array.prototype.join.call(force(list), force(sep)), + dirOf: (s) => { + throw "Not implemented: dirOf" + }, + elem: (x) => (xs) => Array.prototype.includes.call(force(xs), force(x)), + elemAt: (xs) => (n) => force(xs)[force(n)], + filter: (f) => (list) => Array.prototype.filter.call(force(list), force(f)), + filterSource: (args) => { + throw "Not implemented: filterSource" + }, + findFile: (search) => (lookup) => { + throw "Not implemented: findFile" + }, + flakeRefToString: (attrs) => { + throw "Not implemented: flakeRefToString" + }, + ["foldl'"]:(op_fn) => (nul) => (list) => { + const forced_op = force(op_fn); + return Array.prototype.reduce.call(force(list), (acc, cur) => forced_op(acc)(cur), nul); + }, + functionArgs: (f) => { + throw "Not implemented: functionArgs" + }, + genList: (f) => (len) => { + const forced_f = force(f); + const forced_len = force(len); + return [...Array(forced_len).keys()].map(i => forced_f(i)); + }, + genericClosure: (args) => { + throw "Not implemented: genericClosure" + }, + getAttr: (s) => (set) => force(set)[force(s)], + getEnv: (s) => { + throw "Not implemented: getEnv" + }, + getFlake: (attrs) => { + throw "Not implemented: getFlake" + }, + groupBy: (f) => (list) => { + let attrs = {}; + const forced_f = force(f); + const forced_list = force(list); + for (const elem of forced_list) { + const key = force(forced_f(elem)); + if (!attrs[key]) attrs[key] = []; + attrs[key].push(elem); + } + return attrs; + }, + hasAttr: (s) => (set) => Object.prototype.hasOwnProperty.call(force(set), force(s)), + head: (list) => force(list)[0], + intersectAttrs: (e1) => (e2) => { + const f1 = force(e1); + const f2 = force(e2); + let attrs = {}; + for (const key of Object.keys(f2)) { + if (Object.prototype.hasOwnProperty.call(f1, key)) { + attrs[key] = f2[key]; + } + } + return attrs; + }, + length: (e) => force(e).length, + listToAttrs: (e) => { + let attrs = {}; + const forced_e = [...force(e)].reverse(); + for (const obj of forced_e) { + const item = force(obj); + attrs[force(item.name)] = item.value; + } + return attrs; + }, + map: (f) => (list) => Array.prototype.map.call(force(list), force(f)), + mapAttrs: (f) => (attrs) => { + let new_attrs = {}; + const forced_attrs = force(attrs); + const forced_f = force(f); + for (const key in forced_attrs) { + new_attrs[key] = forced_f(key)(forced_attrs[key]); + } + return new_attrs; + }, + match: (regex) => (str) => { + throw "Not implemented: match" + }, + outputOf: (drv) => (out) => { + throw "Not implemented: outputOf" + }, + parseDrvName: (s) => { + throw "Not implemented: parseDrvName" + }, + parseFlakeName: (s) => { + throw "Not implemented: parseFlakeName" + }, + partition: (pred) => (list) => { + const forced_list = force(list); + const forced_pred = force(pred); + let attrs = { + right: [], + wrong: [], + }; + for (const elem of forced_list) { + if (force(forced_pred(elem))) { + attrs.right.push(elem); + } else { + attrs.wrong.push(elem); + } + } + return attrs; + }, + path: (args) => { + throw "Not implemented: path" + }, + pathExists: (path) => { + throw "Not implemented: pathExists" + }, + placeholder: (output) => { + throw "Not implemented: placeholder" + }, + readDir: (path) => { + throw "Not implemented: readDir" + }, + readFile: (path) => { + throw "Not implemented: readFile" + }, + readFileType: (path) => { + throw "Not implemented: readFileType" + }, + replaceStrings: (from) => (to) => (s) => { + throw "Not implemented: replaceStrings" + }, + sort: (cmp) => (list) => { + const forced_list = [...force(list)]; + const forced_cmp = force(cmp); + return forced_list.sort((a, b) => { + if (force(forced_cmp(a)(b))) return -1; + if (force(forced_cmp(b)(a))) return 1; + return 0; + }); + }, + split: (regex, str) => { + throw "Not implemented: split" + }, + splitVersion: (s) => { + throw "Not implemented: splitVersion" + }, + stringLength: (e) => force(e).length, + substring: (start) => (len) => (s) => String.prototype.substring.call(force(s), force(start), force(start) + force(len)), + tail: (list) => Array.prototype.slice.call(force(list), 1), + throw: (s) => { + throw force(s); + }, + toFile: (name, s) => { + throw "Not implemented: toFile" + }, + toPath: (name, s) => { + throw "Not implemented: toPath" + }, + toString: (name, s) => { + throw "Not implemented: toString" + }, + trace: (e1, e2) => { + console.log(`trace: ${force(e1)}`); + return e2; + }, + traceVerbose: (e1, e2) => { + throw "Not implemented: traceVerbose" + }, + tryEval: (e1) => (e2) => { + throw "Not implemented: tryEval" + }, + warn: (e1) => (e2) => { + console.log(`evaluation warning: ${force(e1)}`); + return e2; + }, + zipAttrsWith: (f) => (list) => { + throw "Not implemented: zipAttrsWith" + }, + + builtins: create_thunk(() => builtins), + currentSystem: create_thunk(() => { + throw "Not implemented: currentSystem" + }), + currentTime: create_thunk(() => Date.now()), + [false]: false, + [true]: true, + [null]: null, + langVersion: 6, + nixPath: [], + nixVersion: "NIX_JS_VERSION", + storeDir: "/nix/store", }; return { @@ -139,10 +468,14 @@ const Nix = (() => { force, is_thunk, create_lazy_set, - trace, select, select_with_default, validate_params, - op + op, + builtins, + IS_THUNK // Export the Symbol for Rust to use }; })(); + +// Ensure Nix is available on the global object for Rust access +globalThis.Nix = Nix;