feat: list concat & attrs update

This commit is contained in:
2026-01-02 13:05:36 +08:00
parent 6c8356fd48
commit bd77cde867
5 changed files with 124 additions and 38 deletions

16
Cargo.lock generated
View File

@@ -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]]

View File

@@ -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"

View File

@@ -1,3 +1,5 @@
use itertools::Itertools as _;
use crate::ir::*;
pub trait Compile<Ctx: CodegenContext> {
@@ -28,17 +30,18 @@ impl<Ctx: CodegenContext> Compile<Ctx> 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<Ctx: CodegenContext> Compile<Ctx> 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<Ctx: CodegenContext> Compile<Ctx> 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<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!("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<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
format!("{{{}}}", attrs.join(", "))
}
}
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(",");
format!("[{list}]")
}
}

View File

@@ -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))
);

View File

@@ -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 {