From bd77cde8672ca569583a1f643b189b0b2b2c7f46 Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Fri, 2 Jan 2026 13:05:36 +0800 Subject: [PATCH] feat: list concat & attrs update --- Cargo.lock | 16 ++++++-- nix-js/Cargo.toml | 1 + nix-js/src/codegen.rs | 61 ++++++++++++++++----------- nix-js/src/context.rs | 77 +++++++++++++++++++++++++++++++---- nix-js/src/runtime/runtime.js | 7 +++- 5 files changed, 124 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5de1e8..f81f19a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,7 +69,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -481,7 +481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -856,6 +856,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -1022,6 +1031,7 @@ dependencies = [ "deno_core", "derive_more", "hashbrown 0.16.1", + "itertools 0.14.0", "mimalloc", "nix-js-macros", "regex", @@ -1344,7 +1354,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] diff --git a/nix-js/Cargo.toml b/nix-js/Cargo.toml index eb797de..27f8762 100644 --- a/nix-js/Cargo.toml +++ b/nix-js/Cargo.toml @@ -15,6 +15,7 @@ hashbrown = "0.16" derive_more = { version = "2", features = ["full"] } thiserror = "2" string-interner = "0.19" +itertools = "0.14" v8 = "142.2" deno_core = "0.376" diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index 982bfed..f8853a4 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -1,3 +1,5 @@ +use itertools::Itertools as _; + use crate::ir::*; pub trait Compile { @@ -28,17 +30,18 @@ impl Compile for Ir { Ir::UnOp(x) => x.compile(ctx), Ir::Func(x) => x.compile(ctx), Ir::AttrSet(x) => x.compile(ctx), + Ir::List(x) => x.compile(ctx), &Ir::Call(Call { func, arg }) => { let func = ctx.get_ir(func).compile(ctx); let arg = ctx.get_ir(arg).compile(ctx); - format!("NixRuntime.force({func})({arg})") + format!("Nix.force({func})({arg})") } Ir::Arg(x) => format!("arg{}", x.0), Ir::Let(x) => x.compile(ctx), Ir::Select(x) => x.compile(ctx), &Ir::Thunk(expr_id) => { let inner = ctx.get_ir(expr_id).compile(ctx); - format!("NixRuntime.create_thunk(()=>({}))", inner) + format!("Nix.create_thunk(()=>({}))", inner) } &Ir::ExprRef(expr_id) => { format!("expr{}", expr_id.0) @@ -54,20 +57,23 @@ impl Compile for BinOp { let lhs = ctx.get_ir(self.lhs).compile(ctx); let rhs = ctx.get_ir(self.rhs).compile(ctx); match self.kind { - Add => format!("NixRuntime.op.add({},{})", lhs, rhs), - Sub => format!("NixRuntime.op.sub({},{})", lhs, rhs), - Mul => format!("NixRuntime.op.mul({},{})", lhs, rhs), - Div => format!("NixRuntime.op.div({},{})", lhs, rhs), - Eq => format!("NixRuntime.op.eq({},{})", lhs, rhs), - Neq => format!("NixRuntime.op.neq({},{})", lhs, rhs), - Lt => format!("NixRuntime.op.lt({},{})", lhs, rhs), - Gt => format!("NixRuntime.op.gt({},{})", lhs, rhs), - Leq => format!("NixRuntime.op.lte({},{})", lhs, rhs), - Geq => format!("NixRuntime.op.gte({},{})", lhs, rhs), - And => format!("NixRuntime.op.band({},{})", lhs, rhs), - Or => format!("NixRuntime.op.bor({},{})", lhs, rhs), - Impl => format!("NixRuntime.op.bor(NixRuntime.op.bnot({}),{})", lhs, rhs), - _ => todo!("BinOpKind::{:?}", self.kind), + Add => format!("Nix.op.add({},{})", lhs, rhs), + Sub => format!("Nix.op.sub({},{})", lhs, rhs), + Mul => format!("Nix.op.mul({},{})", lhs, rhs), + Div => format!("Nix.op.div({},{})", lhs, rhs), + Eq => format!("Nix.op.eq({},{})", lhs, rhs), + Neq => format!("Nix.op.neq({},{})", lhs, rhs), + Lt => format!("Nix.op.lt({},{})", lhs, rhs), + Gt => format!("Nix.op.gt({},{})", lhs, rhs), + Leq => format!("Nix.op.lte({},{})", lhs, rhs), + Geq => format!("Nix.op.gte({},{})", lhs, rhs), + And => format!("Nix.op.band({},{})", lhs, rhs), + Or => format!("Nix.op.bor({},{})", lhs, rhs), + Impl => format!("Nix.op.bor(Nix.op.bnot({}),{})", lhs, rhs), + Con => format!("Nix.op.concat({},{})", lhs, rhs), + Upd => format!("Nix.op.update({},{})", lhs, rhs), + PipeL => format!("Nix.force({})({})", rhs, lhs), + PipeR => format!("Nix.force({})({})", lhs, rhs), } } } @@ -77,8 +83,8 @@ impl Compile for UnOp { use UnOpKind::*; let rhs = ctx.get_ir(self.rhs).compile(ctx); match self.kind { - Neg => format!("NixRuntime.op.sub(0,{rhs})"), - Not => format!("NixRuntime.op.bnot({rhs})") + Neg => format!("Nix.op.sub(0,{rhs})"), + Not => format!("Nix.op.bnot({rhs})") } } } @@ -134,8 +140,8 @@ impl Func { "null".to_string() }; - // Call NixRuntime.validate_params and store the result - format!("NixRuntime.validate_params(arg{},{},{});", id, required, allowed) + // Call Nix.validate_params and store the result + format!("Nix.validate_params(arg{},{},{});", id, required, allowed) } } @@ -183,18 +189,18 @@ 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!("NixRuntime.select_with_default({}, \"{}\", {})", result, key, default_val) + format!("Nix.select_with_default({}, \"{}\", {})", result, key, default_val) } else { - format!("NixRuntime.select({}, \"{}\")", result, key) + format!("Nix.select({}, \"{}\")", result, key) } } Attr::Dynamic(expr_id) => { let key = ctx.get_ir(*expr_id).compile(ctx); if has_default { let default_val = ctx.get_ir(self.default.unwrap()).compile(ctx); - format!("NixRuntime.select_with_default({}, {}, {})", result, key, default_val) + format!("Nix.select_with_default({}, {}, {})", result, key, default_val) } else { - format!("NixRuntime.select({}, {})", result, key) + format!("Nix.select({}, {})", result, key) } } }; @@ -223,3 +229,10 @@ impl Compile for AttrSet { format!("{{{}}}", attrs.join(", ")) } } + +impl Compile for List { + fn compile(&self, ctx: &Ctx) -> String { + 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 9aedd85..fb1a2ee 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -63,7 +63,7 @@ impl Context { .downgrade_ctx() .downgrade(root.tree().expr().unwrap())?; let code = self.get_ir(root).compile(self); - let code = format!("NixRuntime.force({})", code); + let code = format!("Nix.force({})", code); println!("[DEBUG] generated code: {}", &code); crate::runtime::run(&code) } @@ -81,8 +81,10 @@ impl CodegenContext for Context { #[cfg(test)] mod test { - use crate::value::Const; + use std::collections::BTreeMap; + use super::*; + use crate::value::{AttrSet, Const, List, Symbol}; #[test] fn basic_eval() { @@ -118,11 +120,56 @@ mod test { ); } + #[test] + fn test_binop() { + let tests = [ + ("1 + 1", Value::Const(Const::Int(2))), + ("2 - 1", Value::Const(Const::Int(1))), + // FIXME: Floating point + // ("1. * 1", Value::Const(Const::Float(1.))), + // ("1 / 1.", Value::Const(Const::Float(1.))), + ("1 == 1", Value::Const(Const::Bool(true))), + ("1 != 1", Value::Const(Const::Bool(false))), + ("2 < 1", Value::Const(Const::Bool(false))), + ("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))), + ( + "[ 1 2 3 ] ++ [ 4 5 6 ]", + Value::List(List::new( + (1..=6).map(Const::Int).map(Value::Const).collect(), + )), + ), + ( + "{ a.b = 1; b = 2; } // { a.c = 2; }", + Value::AttrSet(AttrSet::new(BTreeMap::from([ + ( + Symbol::from("a"), + Value::AttrSet(AttrSet::new(BTreeMap::from([( + Symbol::from("c"), + Value::Const(Const::Int(2)), + )]))), + ), + (Symbol::from("b"), Value::Const(Const::Int(2))), + ]))), + ), + ]; + for (expr, expected) in tests { + assert_eq!(Context::new().eval(expr).unwrap(), expected); + } + } + #[test] fn test_param_check_required() { // Test function with required parameters assert_eq!( - Context::new().eval("({ a, b }: a + b) { a = 1; b = 2; }").unwrap(), + Context::new() + .eval("({ a, b }: a + b) { a = 1; b = 2; }") + .unwrap(), Value::Const(Const::Int(3)) ); @@ -132,7 +179,9 @@ mod test { // Test all required parameters present assert_eq!( - Context::new().eval("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }").unwrap(), + Context::new() + .eval("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }") + .unwrap(), Value::Const(Const::Int(6)) ); } @@ -145,7 +194,9 @@ mod test { // Test function with ellipsis - should accept extra arguments assert_eq!( - Context::new().eval("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }").unwrap(), + Context::new() + .eval("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }") + .unwrap(), Value::Const(Const::Int(3)) ); } @@ -154,13 +205,17 @@ mod test { fn test_param_check_with_default() { // Test function with default parameters assert_eq!( - Context::new().eval("({ a, b ? 5 }: a + b) { a = 1; }").unwrap(), + Context::new() + .eval("({ a, b ? 5 }: a + b) { a = 1; }") + .unwrap(), Value::Const(Const::Int(6)) ); // Test overriding default parameter assert_eq!( - Context::new().eval("({ a, b ? 5 }: a + b) { a = 1; b = 10; }").unwrap(), + Context::new() + .eval("({ a, b ? 5 }: a + b) { a = 1; b = 10; }") + .unwrap(), Value::Const(Const::Int(11)) ); } @@ -169,7 +224,9 @@ mod test { fn test_param_check_with_alias() { // Test function with @ pattern (alias) assert_eq!( - Context::new().eval("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }").unwrap(), + Context::new() + .eval("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }") + .unwrap(), Value::Const(Const::Int(3)) ); } @@ -178,7 +235,9 @@ mod test { fn test_simple_param_no_check() { // Test simple parameter (no pattern) should not have validation assert_eq!( - Context::new().eval("(x: x.a + x.b) { a = 1; b = 2; }").unwrap(), + Context::new() + .eval("(x: x.a + x.b) { a = 1; b = 2; }") + .unwrap(), Value::Const(Const::Int(3)) ); diff --git a/nix-js/src/runtime/runtime.js b/nix-js/src/runtime/runtime.js index ba17dcd..b64dac5 100644 --- a/nix-js/src/runtime/runtime.js +++ b/nix-js/src/runtime/runtime.js @@ -1,4 +1,4 @@ -const NixRuntime = (() => { +const Nix = (() => { const IS_THUNK = Symbol("is_thunk"); class NixThunk { @@ -128,7 +128,10 @@ const NixRuntime = (() => { band: (a, b) => force(a) && force(b), bor: (a, b) => force(a) || force(b), - bnot: (a) => !force(a) + bnot: (a) => !force(a), + + concat: (a, b) => Array.prototype.concat.apply(force(a), force(b)), + update: (a, b) => ({...force(a), ...force(b)}), }; return {