feat: builtins
This commit is contained in:
@@ -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}"))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> 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<Ctx: CodegenContext> Compile<Ctx> 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<Ctx: CodegenContext> Compile<Ctx> 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<Ctx: CodegenContext> Compile<Ctx> 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<Ctx: CodegenContext> Compile<Ctx> 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<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
|
||||
|
||||
impl<Ctx: CodegenContext> Compile<Ctx> 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}]")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Item = ExprId> + 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<ExprId> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -250,9 +250,8 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> 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<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
|
||||
|
||||
// Downgrade body in Let scope and create Let expression
|
||||
let bindings_vec: Vec<ExprId> = 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 {
|
||||
body = ctx.new_expr(
|
||||
Let {
|
||||
bindings: bindings_vec,
|
||||
body: inner_body,
|
||||
}.to_ir());
|
||||
}
|
||||
.to_ir(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -31,7 +31,9 @@ fn run_impl(script: &str, isolate: &mut v8::Isolate) -> Result<Value> {
|
||||
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<Value> {
|
||||
.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<Value> {
|
||||
.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![
|
||||
|
||||
@@ -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];
|
||||
@@ -131,7 +123,344 @@ const Nix = (() => {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user