diff --git a/Cargo.lock b/Cargo.lock index 73873a2..b59101f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,18 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + [[package]] name = "anyhow" version = "1.0.98" @@ -165,6 +177,12 @@ dependencies = [ "syn", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.34" @@ -195,6 +213,33 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -206,6 +251,31 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + [[package]] name = "clipboard-win" version = "5.4.1" @@ -263,6 +333,73 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "data-encoding" version = "2.9.0" @@ -507,6 +644,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "foldhash" version = "0.1.5" @@ -660,6 +803,17 @@ dependencies = [ "crc32fast", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -692,6 +846,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "home" version = "0.5.11" @@ -865,6 +1025,26 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1046,6 +1226,7 @@ name = "nix-js" version = "0.1.0" dependencies = [ "anyhow", + "criterion", "deno_core", "deno_error", "derive_more", @@ -1053,6 +1234,7 @@ dependencies = [ "itertools 0.14.0", "mimalloc", "nix-js-macros", + "petgraph", "regex", "rnix", "rustyline", @@ -1116,6 +1298,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "outref" version = "0.5.2" @@ -1157,6 +1345,18 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", + "serde", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -1189,6 +1389,34 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1265,6 +1493,26 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1410,6 +1658,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1748,6 +2005,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tokio" version = "1.48.0" @@ -1863,6 +2130,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1933,6 +2210,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "which" version = "6.0.3" @@ -1961,6 +2248,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2188,6 +2484,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/nix-js/Cargo.toml b/nix-js/Cargo.toml index 081f882..a8470ec 100644 --- a/nix-js/Cargo.toml +++ b/nix-js/Cargo.toml @@ -15,6 +15,7 @@ derive_more = { version = "2", features = ["full"] } thiserror = "2" hashbrown = "0.16" +petgraph = "0.8" string-interner = "0.19" itertools = "0.14" @@ -30,3 +31,20 @@ nix-js-macros = { path = "../nix-js-macros" } [dev-dependencies] tempfile = "3.24" +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "basic_ops" +harness = false + +[[bench]] +name = "builtins" +harness = false + +[[bench]] +name = "scc_optimization" +harness = false + +[[bench]] +name = "compile_time" +harness = false diff --git a/nix-js/benches/basic_ops.rs b/nix-js/benches/basic_ops.rs new file mode 100644 index 0000000..8d6c1cc --- /dev/null +++ b/nix-js/benches/basic_ops.rs @@ -0,0 +1,111 @@ +mod utils; + +use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use utils::eval; + +fn bench_arithmetic(c: &mut Criterion) { + let mut group = c.benchmark_group("arithmetic"); + + group.bench_function("addition", |b| b.iter(|| eval(black_box("1 + 1")))); + group.bench_function("subtraction", |b| b.iter(|| eval(black_box("10 - 5")))); + group.bench_function("multiplication", |b| b.iter(|| eval(black_box("6 * 7")))); + group.bench_function("division", |b| b.iter(|| eval(black_box("100 / 5")))); + group.bench_function("complex_expression", |b| { + b.iter(|| eval(black_box("(5 + 3) * (10 - 2) / 4"))) + }); + + group.finish(); +} + +fn bench_comparison(c: &mut Criterion) { + let mut group = c.benchmark_group("comparison"); + + group.bench_function("equality", |b| b.iter(|| eval(black_box("42 == 42")))); + group.bench_function("less_than", |b| b.iter(|| eval(black_box("5 < 10")))); + group.bench_function("logical_and", |b| { + b.iter(|| eval(black_box("true && false"))) + }); + group.bench_function("logical_or", |b| { + b.iter(|| eval(black_box("true || false"))) + }); + + group.finish(); +} + +fn bench_function_application(c: &mut Criterion) { + let mut group = c.benchmark_group("function_application"); + + group.bench_function("simple_identity", |b| { + b.iter(|| eval(black_box("(x: x) 42"))) + }); + group.bench_function("curried_function", |b| { + b.iter(|| eval(black_box("(x: y: x + y) 10 20"))) + }); + group.bench_function("nested_application", |b| { + b.iter(|| eval(black_box("((x: y: z: x + y + z) 1) 2 3"))) + }); + + group.finish(); +} + +fn bench_let_bindings(c: &mut Criterion) { + let mut group = c.benchmark_group("let_bindings"); + + group.bench_function("simple_let", |b| { + b.iter(|| eval(black_box("let x = 5; in x + 10"))) + }); + group.bench_function("multiple_bindings", |b| { + b.iter(|| eval(black_box("let a = 1; b = 2; c = 3; in a + b + c"))) + }); + group.bench_function("dependent_bindings", |b| { + b.iter(|| eval(black_box("let x = 5; y = x * 2; z = y + 3; in z"))) + }); + + group.finish(); +} + +fn bench_attrsets(c: &mut Criterion) { + let mut group = c.benchmark_group("attrsets"); + + group.bench_function("simple_attrset", |b| { + b.iter(|| eval(black_box("{ a = 1; b = 2; }.a"))) + }); + group.bench_function("nested_attrset", |b| { + b.iter(|| eval(black_box("{ a.b.c = 42; }.a.b.c"))) + }); + group.bench_function("rec_attrset", |b| { + b.iter(|| eval(black_box("rec { a = 1; b = a + 1; }.b"))) + }); + group.bench_function("attrset_update", |b| { + b.iter(|| eval(black_box("{ a = 1; } // { b = 2; }"))) + }); + + group.finish(); +} + +fn bench_lists(c: &mut Criterion) { + let mut group = c.benchmark_group("lists"); + + group.bench_function("simple_list", |b| { + b.iter(|| eval(black_box("[ 1 2 3 4 5 ]"))) + }); + group.bench_function("list_concatenation", |b| { + b.iter(|| eval(black_box("[ 1 2 3 ] ++ [ 4 5 6 ]"))) + }); + group.bench_function("nested_list", |b| { + b.iter(|| eval(black_box("[ [ 1 2 ] [ 3 4 ] [ 5 6 ] ]"))) + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_arithmetic, + bench_comparison, + bench_function_application, + bench_let_bindings, + bench_attrsets, + bench_lists +); +criterion_main!(benches); diff --git a/nix-js/benches/builtins.rs b/nix-js/benches/builtins.rs new file mode 100644 index 0000000..10c2f1a --- /dev/null +++ b/nix-js/benches/builtins.rs @@ -0,0 +1,143 @@ +mod utils; + +use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use utils::eval; + +fn bench_builtin_math(c: &mut Criterion) { + let mut group = c.benchmark_group("builtin_math"); + + group.bench_function("add", |b| b.iter(|| eval(black_box("builtins.add 10 20")))); + group.bench_function("sub", |b| b.iter(|| eval(black_box("builtins.sub 20 10")))); + group.bench_function("mul", |b| b.iter(|| eval(black_box("builtins.mul 6 7")))); + group.bench_function("div", |b| b.iter(|| eval(black_box("builtins.div 100 5")))); + + group.finish(); +} + +fn bench_builtin_list(c: &mut Criterion) { + let mut group = c.benchmark_group("builtin_list"); + + group.bench_function("length_small", |b| { + b.iter(|| eval(black_box("builtins.length [1 2 3 4 5]"))) + }); + group.bench_function("length_large", |b| { + b.iter(|| { + eval(black_box( + "builtins.length [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20]", + )) + }) + }); + group.bench_function("head", |b| { + b.iter(|| eval(black_box("builtins.head [1 2 3 4 5]"))) + }); + group.bench_function("tail", |b| { + b.iter(|| eval(black_box("builtins.tail [1 2 3 4 5]"))) + }); + group.bench_function("elem_found", |b| { + b.iter(|| eval(black_box("builtins.elem 3 [1 2 3 4 5]"))) + }); + group.bench_function("elem_not_found", |b| { + b.iter(|| eval(black_box("builtins.elem 10 [1 2 3 4 5]"))) + }); + group.bench_function("concat_lists", |b| { + b.iter(|| eval(black_box("builtins.concatLists [[1 2] [3 4] [5 6]]"))) + }); + + group.finish(); +} + +fn bench_builtin_map_filter(c: &mut Criterion) { + let mut group = c.benchmark_group("builtin_map_filter"); + + group.bench_function("map_small", |b| { + b.iter(|| eval(black_box("builtins.map (x: x * 2) [1 2 3 4 5]"))) + }); + group.bench_function("map_large", |b| { + b.iter(|| { + eval(black_box( + "builtins.map (x: x * 2) [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20]", + )) + }) + }); + group.bench_function("filter_small", |b| { + b.iter(|| eval(black_box("builtins.filter (x: x > 2) [1 2 3 4 5]"))) + }); + group.bench_function("filter_large", |b| { + b.iter(|| { + eval(black_box( + "builtins.filter (x: x > 10) [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20]", + )) + }) + }); + group.bench_function("foldl", |b| { + b.iter(|| { + eval(black_box( + "builtins.foldl' (acc: x: acc + x) 0 [1 2 3 4 5 6 7 8 9 10]", + )) + }) + }); + + group.finish(); +} + +fn bench_builtin_attrset(c: &mut Criterion) { + let mut group = c.benchmark_group("builtin_attrset"); + + group.bench_function("attrNames", |b| { + b.iter(|| eval(black_box("builtins.attrNames { a = 1; b = 2; c = 3; }"))) + }); + group.bench_function("attrValues", |b| { + b.iter(|| eval(black_box("builtins.attrValues { a = 1; b = 2; c = 3; }"))) + }); + group.bench_function("hasAttr", |b| { + b.iter(|| eval(black_box("builtins.hasAttr \"a\" { a = 1; b = 2; c = 3; }"))) + }); + group.bench_function("getAttr", |b| { + b.iter(|| eval(black_box("builtins.getAttr \"b\" { a = 1; b = 2; c = 3; }"))) + }); + + group.finish(); +} + +fn bench_builtin_type_checks(c: &mut Criterion) { + let mut group = c.benchmark_group("builtin_type_checks"); + + group.bench_function("isInt", |b| b.iter(|| eval(black_box("builtins.isInt 42")))); + group.bench_function("isList", |b| { + b.iter(|| eval(black_box("builtins.isList [1 2 3]"))) + }); + group.bench_function("isAttrs", |b| { + b.iter(|| eval(black_box("builtins.isAttrs { a = 1; }"))) + }); + group.bench_function("isFunction", |b| { + b.iter(|| eval(black_box("builtins.isFunction (x: x)"))) + }); + group.bench_function("typeOf", |b| { + b.iter(|| eval(black_box("builtins.typeOf 42"))) + }); + + group.finish(); +} + +fn bench_free_globals(c: &mut Criterion) { + let mut group = c.benchmark_group("free_globals"); + + group.bench_function("map", |b| { + b.iter(|| eval(black_box("map (x: x * 2) [1 2 3 4 5]"))) + }); + group.bench_function("isNull", |b| b.iter(|| eval(black_box("isNull null")))); + group.bench_function("toString", |b| b.iter(|| eval(black_box("toString 42")))); + + group.finish(); +} + +criterion_group!( + benches, + bench_builtin_math, + bench_builtin_list, + bench_builtin_map_filter, + bench_builtin_attrset, + bench_builtin_type_checks, + bench_free_globals +); +criterion_main!(benches); diff --git a/nix-js/benches/compile_time.rs b/nix-js/benches/compile_time.rs new file mode 100644 index 0000000..9d61f6b --- /dev/null +++ b/nix-js/benches/compile_time.rs @@ -0,0 +1,139 @@ +mod utils; + +use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use nix_js::context::Context; +use utils::compile; + +fn bench_parse_and_downgrade(c: &mut Criterion) { + let mut group = c.benchmark_group("parse_and_downgrade"); + + group.bench_function("simple_expression", |b| { + b.iter(|| { + compile(black_box("1 + 1")); + }) + }); + + group.bench_function("complex_function", |b| { + b.iter(|| { + compile(black_box( + "let fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); in fib", + )); + }) + }); + + group.bench_function("large_attrset", |b| { + b.iter(|| { + compile(black_box( + "{ a = 1; b = 2; c = 3; d = 4; e = 5; f = 6; g = 7; h = 8; i = 9; j = 10; k = 11; l = 12; m = 13; n = 14; o = 15; }", + )); + }) + }); + + group.bench_function("nested_let_bindings", |b| { + b.iter(|| { + compile(black_box( + "let a = 1; b = 2; c = 3; in let d = a + b; e = b + c; in let f = d + e; in f", + )); + }) + }); + + group.finish(); +} + +fn bench_codegen(c: &mut Criterion) { + let mut group = c.benchmark_group("codegen"); + + group.bench_function("arithmetic_expression", |b| { + b.iter(|| compile(black_box("(1 + 2) * (3 - 4) / 5"))) + }); + + group.bench_function("function_with_closure", |b| { + b.iter(|| compile(black_box("let x = 10; f = y: x + y; in f 5"))) + }); + + group.bench_function("recursive_attrset", |b| { + b.iter(|| { + compile(black_box( + "rec { a = 1; b = a + 1; c = b + 1; d = c + 1; e = d + 1; }", + )) + }) + }); + + group.finish(); +} + +fn bench_full_pipeline(c: &mut Criterion) { + let mut group = c.benchmark_group("full_pipeline"); + + group.bench_function("simple_eval", |b| b.iter(|| compile(black_box("1 + 1")))); + + group.bench_function("fibonacci_10", |b| { + b.iter(|| { + compile(black_box( + "let fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); in fib 10", + )) + }) + }); + + group.bench_function("map_operation", |b| { + b.iter(|| compile(black_box("map (x: x * 2) [1 2 3 4 5 6 7 8 9 10]"))) + }); + + group.bench_function("complex_attrset_access", |b| { + b.iter(|| { + compile(black_box( + "let attrs = { a.b.c = { d.e = 42; }; }; in attrs.a.b.c.d.e", + )) + }) + }); + + group.bench_function("with_expression", |b| { + b.iter(|| { + compile(black_box( + "let attrs = { x = 1; y = 2; z = 3; }; in with attrs; x + y + z", + )) + }) + }); + + group.finish(); +} + +fn bench_context_creation(c: &mut Criterion) { + c.bench_function("context_new", |b| { + b.iter(|| { + let _ = Context::new(); + }) + }); +} + +fn bench_symbol_interning(c: &mut Criterion) { + let mut group = c.benchmark_group("symbol_interning"); + + group.bench_function("many_unique_symbols", |b| { + b.iter(|| { + compile(black_box( + "let a1 = 1; a2 = 2; a3 = 3; a4 = 4; a5 = 5; a6 = 6; a7 = 7; a8 = 8; a9 = 9; a10 = 10; in a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10", + )) + }) + }); + + group.bench_function("repeated_symbols", |b| { + b.iter(|| { + compile(black_box( + "let x = 1; y = x; z = x; a = x; b = x; c = x; in x + y + z + a + b + c", + )) + }) + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_parse_and_downgrade, + bench_codegen, + bench_full_pipeline, + bench_context_creation, + bench_symbol_interning +); +criterion_main!(benches); diff --git a/nix-js/benches/scc_optimization.rs b/nix-js/benches/scc_optimization.rs new file mode 100644 index 0000000..55436cd --- /dev/null +++ b/nix-js/benches/scc_optimization.rs @@ -0,0 +1,172 @@ +mod utils; + +use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use utils::eval; + +fn bench_non_recursive(c: &mut Criterion) { + let mut group = c.benchmark_group("non_recursive"); + + group.bench_function("simple_bindings", |b| { + b.iter(|| eval(black_box("let x = 1; y = 2; z = x + y; in z"))) + }); + + group.bench_function("many_bindings", |b| { + b.iter(|| { + eval(black_box( + "let a = 1; b = 2; c = 3; d = 4; e = 5; f = 6; g = 7; h = 8; i = 9; j = 10; in a + b + c + d + e + f + g + h + i + j", + )) + }) + }); + + group.bench_function("dependent_chain", |b| { + b.iter(|| { + eval(black_box( + "let a = 1; b = a + 1; c = b + 1; d = c + 1; e = d + 1; in e", + )) + }) + }); + + group.bench_function("rec_attrset_non_recursive", |b| { + b.iter(|| eval(black_box("rec { a = 1; b = 2; c = 3; d = a + b + c; }.d"))) + }); + + group.finish(); +} + +fn bench_recursive(c: &mut Criterion) { + let mut group = c.benchmark_group("recursive"); + + group.bench_function("simple_recursion", |b| { + b.iter(|| { + eval(black_box( + "let f = n: if n == 0 then 0 else f (n - 1); in f 10", + )) + }) + }); + + group.bench_function("fibonacci", |b| { + b.iter(|| { + eval(black_box( + "let fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); in fib 10", + )) + }) + }); + + group.bench_function("factorial", |b| { + b.iter(|| { + eval(black_box( + "let factorial = n: if n == 0 then 1 else n * factorial (n - 1); in factorial 10", + )) + }) + }); + + group.bench_function("rec_attrset_recursive", |b| { + b.iter(|| { + eval(black_box( + "rec { f = n: if n == 0 then 1 else f (n - 1); }.f 10", + )) + }) + }); + + group.finish(); +} + +fn bench_mutual_recursion(c: &mut Criterion) { + let mut group = c.benchmark_group("mutual_recursion"); + + group.bench_function("two_way", |b| { + b.iter(|| { + eval(black_box( + "let f = n: if n == 0 then 0 else g (n - 1); g = n: if n == 0 then 1 else f (n - 1); in f 10", + )) + }) + }); + + group.bench_function("even_odd", |b| { + b.iter(|| { + eval(black_box( + "let even = n: if n == 0 then true else odd (n - 1); odd = n: if n == 0 then false else even (n - 1); in even 20", + )) + }) + }); + + group.bench_function("three_way", |b| { + b.iter(|| { + eval(black_box( + "let a = n: if n == 0 then 1 else b (n - 1); b = n: if n == 0 then 2 else c (n - 1); c = n: if n == 0 then 3 else a (n - 1); in a 15", + )) + }) + }); + + group.finish(); +} + +fn bench_mixed(c: &mut Criterion) { + let mut group = c.benchmark_group("mixed"); + + group.bench_function("recursive_with_constants", |b| { + b.iter(|| { + eval(black_box( + "let x = 10; fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); in fib x", + )) + }) + }); + + group.bench_function("multiple_recursive_functions", |b| { + b.iter(|| { + eval(black_box( + "let fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); fact = n: if n == 0 then 1 else n * fact (n - 1); in (fib 8) + (fact 5)", + )) + }) + }); + + group.bench_function("complex_dependency_graph", |b| { + b.iter(|| { + eval(black_box( + "let a = 1; b = 2; c = a + b; fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); result = c + fib 8; in result", + )) + }) + }); + + group.finish(); +} + +fn bench_nested_scopes(c: &mut Criterion) { + let mut group = c.benchmark_group("nested_scopes"); + + group.bench_function("nested_let_non_recursive", |b| { + b.iter(|| { + eval(black_box( + "let x = 1; in let y = x + 1; in let z = y + 1; in z", + )) + }) + }); + + group.bench_function("nested_let_with_recursive", |b| { + b.iter(|| { + eval(black_box( + "let f = n: if n == 0 then 0 else f (n - 1); in let g = m: f m; in g 10", + )) + }) + }); + + group.bench_function("deeply_nested", |b| { + b.iter(|| { + eval(black_box( + "let a = 1; in let b = a + 1; in let c = b + 1; in let d = c + 1; in let e = d + 1; in e", + )) + }) + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_non_recursive, + bench_recursive, + bench_mutual_recursion, + bench_mixed, + bench_nested_scopes +); +criterion_main!(benches); diff --git a/nix-js/benches/utils.rs b/nix-js/benches/utils.rs new file mode 100644 index 0000000..d45290f --- /dev/null +++ b/nix-js/benches/utils.rs @@ -0,0 +1,16 @@ +#![allow(dead_code)] + +use nix_js::context::Context; +use nix_js::value::Value; + +pub fn eval(expr: &str) -> Value { + Context::new().unwrap().eval_code(expr).unwrap() +} + +pub fn eval_result(expr: &str) -> Result { + Context::new().unwrap().eval_code(expr) +} + +pub fn compile(expr: &str) -> String { + Context::new().unwrap().compile_code(expr).unwrap() +} diff --git a/nix-js/runtime-ts/src/builtins/index.ts b/nix-js/runtime-ts/src/builtins/index.ts index aa4867e..1c3c0ec 100644 --- a/nix-js/runtime-ts/src/builtins/index.ts +++ b/nix-js/runtime-ts/src/builtins/index.ts @@ -3,7 +3,7 @@ * Combines all builtin function categories into the global `builtins` object */ -import { create_thunk } from "../thunk"; +import { createThunk } from "../thunk"; /** * Symbol used to mark functions as primops (primitive operations) @@ -247,11 +247,11 @@ export const builtins: any = { tryEval: mkPrimop(misc.tryEval, "tryEval", 1), zipAttrsWith: mkPrimop(misc.zipAttrsWith, "zipAttrsWith", 2), - builtins: create_thunk(() => builtins), - currentSystem: create_thunk(() => { + builtins: createThunk(() => builtins), + currentSystem: createThunk(() => { throw new Error("Not implemented: currentSystem"); }), - currentTime: create_thunk(() => Date.now()), + currentTime: createThunk(() => Date.now()), false: false, true: true, diff --git a/nix-js/runtime-ts/src/builtins/misc.ts b/nix-js/runtime-ts/src/builtins/misc.ts index e2cab1f..6304d27 100644 --- a/nix-js/runtime-ts/src/builtins/misc.ts +++ b/nix-js/runtime-ts/src/builtins/misc.ts @@ -1,5 +1,5 @@ /** - * Miscellaneous unimplemented builtin functions + * Miscellaneous builtin functions */ import { force } from "../thunk"; diff --git a/nix-js/runtime-ts/src/helpers.ts b/nix-js/runtime-ts/src/helpers.ts index 545ddd4..e9a125a 100644 --- a/nix-js/runtime-ts/src/helpers.ts +++ b/nix-js/runtime-ts/src/helpers.ts @@ -13,7 +13,7 @@ import { isAttrs } from "./builtins/type-check"; * @param path - Path string (may be relative or absolute) * @returns Absolute path string */ -export const resolve_path = (path: NixValue): string => { +export const resolvePath = (path: NixValue): string => { const path_str = force_string(path); return Deno.core.ops.op_resolve_path(path_str); }; @@ -47,7 +47,7 @@ export const select = (obj: NixValue, key: NixValue): NixValue => { * @param default_val - Value to return if key not found * @returns obj[key] if exists, otherwise default_val */ -export const select_with_default = (obj: NixValue, key: NixValue, default_val: NixValue): NixValue => { +export const selectWithDefault = (obj: NixValue, key: NixValue, default_val: NixValue): NixValue => { const attrs = force_attrs(obj); const forced_key = force_string(key); @@ -58,7 +58,7 @@ export const select_with_default = (obj: NixValue, key: NixValue, default_val: N return attrs[forced_key]; }; -export const has_attr = (obj: NixValue, attrpath: NixValue[]): NixBool => { +export const hasAttr = (obj: NixValue, attrpath: NixValue[]): NixBool => { if (!isAttrs(obj)) { return false; } @@ -89,7 +89,7 @@ export const has_attr = (obj: NixValue, attrpath: NixValue[]): NixBool => { * @returns The forced argument object * @throws Error if required param missing or unexpected param present */ -export const validate_params = ( +export const validateParams = ( arg: NixValue, required: string[] | null, allowed: string[] | null, diff --git a/nix-js/runtime-ts/src/index.ts b/nix-js/runtime-ts/src/index.ts index a11e53d..c37cb88 100644 --- a/nix-js/runtime-ts/src/index.ts +++ b/nix-js/runtime-ts/src/index.ts @@ -4,8 +4,8 @@ * All functionality is exported via the global `Nix` object */ -import { create_thunk, force, is_thunk, IS_THUNK } from "./thunk"; -import { select, select_with_default, validate_params, resolve_path, has_attr } from "./helpers"; +import { createThunk, force, is_thunk, IS_THUNK } from "./thunk"; +import { select, selectWithDefault, validateParams, resolvePath, hasAttr } from "./helpers"; import { op } from "./operators"; import { builtins, PRIMOP_METADATA } from "./builtins"; @@ -15,16 +15,16 @@ export type NixRuntime = typeof Nix; * The global Nix runtime object */ export const Nix = { - create_thunk, + createThunk, force, is_thunk, IS_THUNK, - has_attr, + hasAttr, select, - select_with_default, - validate_params, - resolve_path, + selectWithDefault, + validateParams, + resolvePath, op, builtins, diff --git a/nix-js/runtime-ts/src/thunk.ts b/nix-js/runtime-ts/src/thunk.ts index 488f904..5ee1a7d 100644 --- a/nix-js/runtime-ts/src/thunk.ts +++ b/nix-js/runtime-ts/src/thunk.ts @@ -69,6 +69,6 @@ export const force = (value: NixValue): NixStrictValue => { * @param func - Function that produces a value when called * @returns A new NixThunk wrapping the function */ -export const create_thunk = (func: () => NixValue): NixThunkInterface => { +export const createThunk = (func: () => NixValue): NixThunkInterface => { return new NixThunk(func); }; diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index 11b498d..84e8e13 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -36,7 +36,7 @@ impl Compile for Ir { Ir::Path(p) => { // Path needs runtime resolution for interpolated paths let path_expr = ctx.get_ir(p.expr).compile(ctx); - format!("Nix.resolve_path({})", path_expr) + format!("Nix.resolvePath({})", path_expr) } &Ir::If(If { cond, consq, alter }) => { let cond = ctx.get_ir(cond).compile(ctx); @@ -59,7 +59,7 @@ impl Compile for Ir { Ir::Select(x) => x.compile(ctx), &Ir::Thunk(expr_id) => { let inner = ctx.get_ir(expr_id).compile(ctx); - format!("Nix.create_thunk(()=>({}))", inner) + format!("Nix.createThunk(()=>({}))", inner) } &Ir::ExprRef(expr_id) => { format!("expr{}", expr_id.0) @@ -166,34 +166,42 @@ impl Func { "null".to_string() }; - // Call Nix.validate_params and store the result - format!("Nix.validate_params(arg{},{},{});", id, required, allowed) + // Call Nix.validateParams and store the result + format!("Nix.validateParams(arg{},{},{});", id, required, allowed) } } impl Compile for Let { fn compile(&self, ctx: &Ctx) -> String { - let declarations: Vec = self - .bindings - .iter() - .map(|&expr| format!("let expr{}", expr.0)) - .collect(); + let info = &self.binding_sccs; + let mut js_statements = Vec::new(); - let assignments: Vec = self - .bindings - .iter() - .map(|&expr| { - let value = ctx.get_ir(expr).compile(ctx); - format!("expr{}={}", expr.0, value) - }) - .collect(); + for (scc_exprs, is_recursive) in info.sccs.iter() { + if *is_recursive { + for &expr in scc_exprs { + js_statements.push(format!("let expr{}", expr.0)); + } + for &expr in scc_exprs { + let value = ctx.get_ir(expr).compile(ctx); + js_statements.push(format!("expr{}={}", expr.0, value)); + } + } else { + for &expr in scc_exprs { + let ir = ctx.get_ir(expr); + let value = if let Ir::Thunk(inner) = ir { + ctx.get_ir(*inner).compile(ctx) + } else { + ir.compile(ctx) + }; + js_statements.push(format!("let expr{}={}", expr.0, value)); + } + } + } let body = ctx.get_ir(self.body).compile(ctx); - format!( - "(()=>{{{}; {}; return {}}})()", - declarations.join(";"), - assignments.join(";"), + "(()=>{{{}; return {}}})()", + js_statements.join(";"), body ) } @@ -216,7 +224,7 @@ impl Compile for Select { { let default_val = ctx.get_ir(default).compile(ctx); format!( - "Nix.select_with_default({}, \"{}\", {})", + "Nix.selectWithDefault({}, \"{}\", {})", result, key, default_val ) } else { @@ -230,7 +238,7 @@ impl Compile for Select { { let default_val = ctx.get_ir(default).compile(ctx); format!( - "Nix.select_with_default({}, {}, {})", + "Nix.selectWithDefault({}, {}, {})", result, key, default_val ) } else { @@ -307,6 +315,6 @@ impl Compile for HasAttr { Attr::Dynamic(expr_id) => ctx.get_ir(*expr_id).compile(ctx), }) .join(","); - format!("Nix.has_attr({lhs}, [{attrpath}])") + format!("Nix.hasAttr({lhs}, [{attrpath}])") } } diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index 04e5289..7513267 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -56,6 +56,12 @@ mod private { } use private::CtxPtr; +#[derive(Debug)] +pub struct SccInfo { + /// list of SCCs (exprs, recursive) + pub sccs: Vec<(Vec, bool)>, +} + pub struct Context { ctx: Ctx, runtime: Runtime, @@ -78,6 +84,10 @@ impl Context { self.runtime.eval(code, CtxPtr::new(&mut self.ctx)) } + pub fn compile_code(&mut self, expr: &str) -> Result { + self.ctx.compile_code(expr) + } + #[allow(dead_code)] pub(crate) fn eval_js(&mut self, code: String) -> Result { self.runtime.eval(code, CtxPtr::new(&mut self.ctx)) @@ -180,7 +190,8 @@ impl Ctx { .downgrade(root.tree().expr().unwrap())?; let code = self.get_ir(root).compile(self); let code = format!("Nix.force({})", code); - println!("[DEBUG] generated code: {}", &code); + #[cfg(debug_assertions)] + eprintln!("[DEBUG] generated code: {}", &code); Ok(code) } } @@ -200,708 +211,3 @@ impl PathStackProvider for Ctx { &mut self.path_stack } } - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod test { - use std::collections::BTreeMap; - - use super::*; - use crate::value::{AttrSet, List, Symbol}; - - #[test] - fn basic_eval() { - assert_eq!( - Context::new().unwrap().eval_code("1 + 1").unwrap(), - Value::Int(2) - ); - assert_eq!( - Context::new().unwrap().eval_code("(x: x) 1").unwrap(), - Value::Int(1) - ); - assert_eq!( - Context::new() - .unwrap() - .eval_code("(x: y: x - y) 2 1") - .unwrap(), - Value::Int(1) - ); - assert_eq!( - Context::new() - .unwrap() - .eval_code("rec { b = a; a = 1; }.b") - .unwrap(), - Value::Int(1) - ); - assert_eq!( - Context::new() - .unwrap() - .eval_code("let b = a; a = 1; in b") - .unwrap(), - Value::Int(1) - ); - assert_eq!( - Context::new().unwrap().eval_code("let fib = n: if n == 1 || n == 2 then 1 else (fib (n - 1)) + (fib (n - 2)); in fib 30").unwrap(), - Value::Int(832040) - ); - assert_eq!( - Context::new() - .unwrap() - .eval_code("((f: let x = f x; in x)(self: { x = 1; y = self.x + 1; })).y") - .unwrap(), - Value::Int(2) - ); - } - - #[test] - fn test_binop() { - let tests = [ - ("1 + 1", Value::Int(2)), - ("2 - 1", Value::Int(1)), - ("1. * 1", Value::Float(1.)), - ("1 / 1.", Value::Float(1.)), - ("1 == 1", Value::Bool(true)), - ("1 != 1", Value::Bool(false)), - ("2 < 1", Value::Bool(false)), - ("2 > 1", Value::Bool(true)), - ("1 <= 1", Value::Bool(true)), - ("1 >= 1", Value::Bool(true)), - // Short-circuit evaluation: true || should not evaluate - ("true || (1 / 0)", Value::Bool(true)), - ("true && 1 == 0", Value::Bool(false)), - ( - "[ 1 2 3 ] ++ [ 4 5 6 ]", - Value::List(List::new((1..=6).map(Value::Int).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::Int(2), - )]))), - ), - (Symbol::from("b"), Value::Int(2)), - ]))), - ), - ]; - for (expr, expected) in tests { - assert_eq!(Context::new().unwrap().eval_code(expr).unwrap(), expected); - } - } - - #[test] - fn test_param_check_required() { - // Test function with required parameters - assert_eq!( - Context::new() - .unwrap() - .eval_code("({ a, b }: a + b) { a = 1; b = 2; }") - .unwrap(), - Value::Int(3) - ); - - // Test missing required parameter should fail - let result = Context::new() - .unwrap() - .eval_code("({ a, b }: a + b) { a = 1; }"); - assert!(result.is_err()); - - // Test all required parameters present - assert_eq!( - Context::new() - .unwrap() - .eval_code("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }") - .unwrap(), - Value::Int(6) - ); - } - - #[test] - fn test_param_check_allowed() { - // Test function without ellipsis - should reject unexpected arguments - let result = Context::new() - .unwrap() - .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() - .unwrap() - .eval_code("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }") - .unwrap(), - Value::Int(3) - ); - } - - #[test] - fn test_param_check_with_default() { - // Test function with default parameters - assert_eq!( - Context::new() - .unwrap() - .eval_code("({ a, b ? 5 }: a + b) { a = 1; }") - .unwrap(), - Value::Int(6) - ); - - // Test overriding default parameter - assert_eq!( - Context::new() - .unwrap() - .eval_code("({ a, b ? 5 }: a + b) { a = 1; b = 10; }") - .unwrap(), - Value::Int(11) - ); - } - - #[test] - fn test_param_check_with_alias() { - // Test function with @ pattern (alias) - assert_eq!( - Context::new() - .unwrap() - .eval_code("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }") - .unwrap(), - Value::Int(3) - ); - } - - #[test] - fn test_simple_param_no_check() { - // Test simple parameter (no pattern) should not have validation - assert_eq!( - Context::new() - .unwrap() - .eval_code("(x: x.a + x.b) { a = 1; b = 2; }") - .unwrap(), - Value::Int(3) - ); - - // Simple parameter accepts any argument - assert_eq!( - Context::new().unwrap().eval_code("(x: x) 42").unwrap(), - Value::Int(42) - ); - } - - #[test] - fn test_builtins_basic_access() { - // Test that builtins identifier is accessible - let result = Context::new().unwrap().eval_code("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() - .unwrap() - .eval_code("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() - .unwrap() - .eval_code("builtins.add 1 2") - .unwrap(), - Value::Int(3) - ); - } - - #[test] - fn test_builtin_function_length() { - // Test builtin with list: builtins.length [1 2 3] - assert_eq!( - Context::new() - .unwrap() - .eval_code("builtins.length [1 2 3]") - .unwrap(), - Value::Int(3) - ); - } - - #[test] - fn test_builtin_function_map() { - // Test higher-order builtin: map (x: x * 2) [1 2 3] - assert_eq!( - Context::new() - .unwrap() - .eval_code("map (x: x * 2) [1 2 3]") - .unwrap(), - Value::List(List::new( - vec![Value::Int(2), Value::Int(4), Value::Int(6),] - )) - ); - } - - #[test] - fn test_builtin_function_filter() { - // Test predicate builtin: builtins.filter (x: x > 1) [1 2 3] - assert_eq!( - Context::new() - .unwrap() - .eval_code("builtins.filter (x: x > 1) [1 2 3]") - .unwrap(), - Value::List(List::new(vec![Value::Int(2), Value::Int(3),])) - ); - } - - #[test] - fn test_builtin_function_attrnames() { - // Test builtins.attrNames { a = 1; b = 2; } - let result = Context::new() - .unwrap() - .eval_code("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() - .unwrap() - .eval_code("builtins.head [1 2 3]") - .unwrap(), - Value::Int(1) - ); - } - - #[test] - fn test_builtin_function_tail() { - // Test builtins.tail [1 2 3] - assert_eq!( - Context::new() - .unwrap() - .eval_code("builtins.tail [1 2 3]") - .unwrap(), - Value::List(List::new(vec![Value::Int(2), Value::Int(3),])) - ); - } - - #[test] - fn test_builtin_in_let() { - // Test builtins in let binding - assert_eq!( - Context::new() - .unwrap() - .eval_code("let b = builtins; in b.add 5 3") - .unwrap(), - Value::Int(8) - ); - } - - #[test] - fn test_builtin_in_with() { - // Test builtins with 'with' expression - assert_eq!( - Context::new() - .unwrap() - .eval_code("with builtins; add 10 20") - .unwrap(), - Value::Int(30) - ); - } - - #[test] - fn test_builtin_nested_access() { - // Test nested function calls with builtins - assert_eq!( - Context::new() - .unwrap() - .eval_code("builtins.add (builtins.mul 2 3) (builtins.sub 10 5)") - .unwrap(), - Value::Int(11) // (2*3) + (10-5 = 6 + 5 = 11 - ); - } - - #[test] - fn test_builtin_type_checks() { - // Test type checking functions - assert_eq!( - Context::new() - .unwrap() - .eval_code("builtins.isList [1 2 3]") - .unwrap(), - Value::Bool(true) - ); - assert_eq!( - Context::new() - .unwrap() - .eval_code("builtins.isAttrs { a = 1; }") - .unwrap(), - Value::Bool(true) - ); - assert_eq!( - Context::new() - .unwrap() - .eval_code("builtins.isFunction (x: x)") - .unwrap(), - Value::Bool(true) - ); - assert_eq!( - Context::new() - .unwrap() - .eval_code("builtins.isNull null") - .unwrap(), - Value::Bool(true) - ); - assert_eq!( - Context::new() - .unwrap() - .eval_code("builtins.isBool true") - .unwrap(), - Value::Bool(true) - ); - } - - #[test] - fn test_builtin_shadowing() { - // Test that user can shadow builtins (Nix allows this) - assert_eq!( - Context::new() - .unwrap() - .eval_code("let builtins = { add = x: y: x - y; }; in builtins.add 5 3") - .unwrap(), - Value::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() - .unwrap() - .eval_code("builtins.builtins.builtins.add 1 1") - .unwrap(); - assert_eq!(result, Value::Int(2)); - } - - // Free globals tests - #[test] - fn test_free_global_true() { - assert_eq!( - Context::new().unwrap().eval_code("true").unwrap(), - Value::Bool(true) - ); - } - - #[test] - fn test_free_global_false() { - assert_eq!( - Context::new().unwrap().eval_code("false").unwrap(), - Value::Bool(false) - ); - } - - #[test] - fn test_free_global_null() { - assert_eq!( - Context::new().unwrap().eval_code("null").unwrap(), - Value::Null - ); - } - - #[test] - fn test_free_global_map() { - // Test free global function: map (x: x * 2) [1 2 3] - assert_eq!( - Context::new() - .unwrap() - .eval_code("map (x: x * 2) [1 2 3]") - .unwrap(), - Value::List(List::new( - vec![Value::Int(2), Value::Int(4), Value::Int(6),] - )) - ); - } - - #[test] - fn test_free_global_isnull() { - // Test isNull function - assert_eq!( - Context::new().unwrap().eval_code("isNull null").unwrap(), - Value::Bool(true) - ); - assert_eq!( - Context::new().unwrap().eval_code("isNull 5").unwrap(), - Value::Bool(false) - ); - } - - #[test] - fn test_free_global_shadowing() { - // Test shadowing of free globals - assert_eq!( - Context::new() - .unwrap() - .eval_code("let true = false; in true") - .unwrap(), - Value::Bool(false) - ); - assert_eq!( - Context::new() - .unwrap() - .eval_code("let map = x: y: x; in map 1 2") - .unwrap(), - Value::Int(1) - ); - } - - #[test] - fn test_free_global_mixed_usage() { - // Test mixing free globals in expressions - assert_eq!( - Context::new() - .unwrap() - .eval_code("if true then map (x: x + 1) [1 2] else []") - .unwrap(), - Value::List(List::new(vec![Value::Int(2), Value::Int(3),])) - ); - } - - #[test] - fn test_free_global_in_let() { - // Test free globals in let bindings - assert_eq!( - Context::new() - .unwrap() - .eval_code("let x = true; y = false; in x && y") - .unwrap(), - Value::Bool(false) - ); - } - - // BigInt and numeric type tests - #[test] - fn test_bigint_precision() { - let mut ctx = Context::new().unwrap(); - - // Test large i64 values - assert_eq!( - ctx.eval_code("9223372036854775807").unwrap(), - Value::Int(9223372036854775807) - ); - - // Test negative large value - // Can't use -9223372036854775808 since unary minus is actually desugared to (0 - ) - assert_eq!( - ctx.eval_code("-9223372036854775807").unwrap(), - Value::Int(-9223372036854775807) - ); - - // Test large number arithmetic - assert_eq!( - ctx.eval_code("5000000000000000000 + 3000000000000000000") - .unwrap(), - Value::Int(8000000000000000000i64) - ); - } - - #[test] - fn test_int_float_distinction() { - let mut ctx = Context::new().unwrap(); - - // isInt tests - assert_eq!( - ctx.eval_code("builtins.isInt 42").unwrap(), - Value::Bool(true) - ); - assert_eq!( - ctx.eval_code("builtins.isInt 42.0").unwrap(), - Value::Bool(false) - ); - - // isFloat tests - assert_eq!( - ctx.eval_code("builtins.isFloat 42").unwrap(), - Value::Bool(false) - ); - assert_eq!( - ctx.eval_code("builtins.isFloat 42.5").unwrap(), - Value::Bool(true) - ); - assert_eq!( - ctx.eval_code("builtins.isFloat 1.0").unwrap(), - Value::Bool(true) - ); - - // typeOf tests - assert_eq!( - ctx.eval_code("builtins.typeOf 1").unwrap(), - Value::String("int".to_string()) - ); - assert_eq!( - ctx.eval_code("builtins.typeOf 1.0").unwrap(), - Value::String("float".to_string()) - ); - assert_eq!( - ctx.eval_code("builtins.typeOf 3.14").unwrap(), - Value::String("float".to_string()) - ); - - // literal tests - assert_eq!(ctx.eval_code("1").unwrap(), Value::Int(1)); - assert_eq!(ctx.eval_code("1.").unwrap(), Value::Float(1.)) - } - - #[test] - fn test_arithmetic_type_preservation() { - let mut ctx = Context::new().unwrap(); - - // int + int = int - assert_eq!( - ctx.eval_code("builtins.typeOf (1 + 2)").unwrap(), - Value::String("int".to_string()) - ); - - // int + float = float - assert_eq!( - ctx.eval_code("builtins.typeOf (1 + 2.0)").unwrap(), - Value::String("float".to_string()) - ); - - // int * int = int - assert_eq!( - ctx.eval_code("builtins.typeOf (3 * 4)").unwrap(), - Value::String("int".to_string()) - ); - - // int * float = float - assert_eq!( - ctx.eval_code("builtins.typeOf (3 * 4.0)").unwrap(), - Value::String("float".to_string()) - ); - } - - #[test] - fn test_integer_division() { - let mut ctx = Context::new().unwrap(); - - assert_eq!(ctx.eval_code("5 / 2").unwrap(), Value::Int(2)); - - assert_eq!(ctx.eval_code("7 / 3").unwrap(), Value::Int(2)); - - assert_eq!(ctx.eval_code("10 / 3").unwrap(), Value::Int(3)); - - // Float division returns float - assert_eq!(ctx.eval_code("5 / 2.0").unwrap(), Value::Float(2.5)); - - assert_eq!(ctx.eval_code("7.0 / 2").unwrap(), Value::Float(3.5)); - - assert_eq!(ctx.eval_code("(-7) / 3").unwrap(), Value::Int(-2)); - } - - #[test] - fn test_builtin_arithmetic_with_bigint() { - let mut ctx = Context::new().unwrap(); - - // Test builtin add with large numbers - assert_eq!( - ctx.eval_code("builtins.add 5000000000000000000 3000000000000000000") - .unwrap(), - Value::Int(8000000000000000000i64) - ); - - // Test builtin mul with large numbers - assert_eq!( - ctx.eval_code("builtins.mul 1000000000 1000000000").unwrap(), - Value::Int(1000000000000000000i64) - ); - } - - #[test] - fn test_import_absolute_path() { - let mut ctx = Context::new().unwrap(); - - let temp_dir = tempfile::tempdir().unwrap(); - let lib_path = temp_dir.path().join("nix_test_lib.nix"); - - std::fs::write(&lib_path, "{ add = a: b: a + b; }").unwrap(); - - let expr = format!(r#"(import "{}").add 3 5"#, lib_path.display()); - assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(8)); - } - - #[test] - fn test_import_nested() { - let mut ctx = Context::new().unwrap(); - - let temp_dir = tempfile::tempdir().unwrap(); - - let lib_path = temp_dir.path().join("lib.nix"); - std::fs::write(&lib_path, "{ add = a: b: a + b; }").unwrap(); - - 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() - ); - std::fs::write(&main_path, main_content).unwrap(); - - let expr = format!(r#"(import "{}").result"#, main_path.display()); - assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(30)); - } - - #[test] - fn test_import_relative_path() { - let mut ctx = Context::new().unwrap(); - - let temp_dir = tempfile::tempdir().unwrap(); - let subdir = temp_dir.path().join("subdir"); - std::fs::create_dir_all(&subdir).unwrap(); - - let lib_path = temp_dir.path().join("lib.nix"); - std::fs::write(&lib_path, "{ multiply = a: b: a * b; }").unwrap(); - - let helper_path = subdir.join("helper.nix"); - std::fs::write(&helper_path, "{ subtract = a: b: a - b; }").unwrap(); - - let main_path = temp_dir.path().join("main.nix"); - let main_content = r#" - let - lib = import ./lib.nix; - helper = import ./subdir/helper.nix; - in { - result1 = lib.multiply 3 4; - result2 = helper.subtract 10 3; - } - "#; - std::fs::write(&main_path, main_content).unwrap(); - - let expr = format!(r#"let x = import "{}"; in x.result1"#, main_path.display()); - assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(12)); - - let expr = format!(r#"let x = import "{}"; in x.result2"#, main_path.display()); - assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(7)); - } - - #[test] - fn test_import_returns_function() { - let mut ctx = Context::new().unwrap(); - - 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 expr = format!(r#"(import "{}") 5"#, func_path.display()); - assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(10)); - } -} diff --git a/nix-js/src/context/downgrade.rs b/nix-js/src/context/downgrade.rs index 9e4e099..5eb5ce0 100644 --- a/nix-js/src/context/downgrade.rs +++ b/nix-js/src/context/downgrade.rs @@ -1,10 +1,21 @@ use hashbrown::HashMap; +use hashbrown::HashSet; +use petgraph::Directed; +use petgraph::Graph; +use petgraph::graph::NodeIndex; use crate::codegen::CodegenContext; use crate::error::{Error, Result}; use crate::ir::{ArgId, Downgrade, DowngradeContext, ExprId, Ir, SymId, ToIr}; -use super::Ctx; +use super::{Ctx, SccInfo}; + +struct DependencyTracker { + expr_to_node: HashMap, + graph: Graph, + current_binding: Option, + let_scope_exprs: HashSet, +} enum Scope<'ctx> { Global(&'ctx HashMap), @@ -34,6 +45,7 @@ pub struct DowngradeCtx<'ctx> { irs: Vec>, scopes: Vec>, arg_id: usize, + dep_tracker_stack: Vec, } impl<'ctx> DowngradeCtx<'ctx> { @@ -42,6 +54,7 @@ impl<'ctx> DowngradeCtx<'ctx> { scopes: vec![Scope::Global(global)], irs: vec![], arg_id: 0, + dep_tracker_stack: Vec::new(), ctx, } } @@ -77,7 +90,16 @@ impl DowngradeContext for DowngradeCtx<'_> { } Scope::Let(let_scope) => { if let Some(&expr) = let_scope.get(&sym) { - // Wrap in ExprRef to reference the binding instead of recompiling + if let Some(tracker) = self.dep_tracker_stack.last_mut() + && let Some(current) = tracker.current_binding + && tracker.let_scope_exprs.contains(¤t) + && tracker.let_scope_exprs.contains(&expr) + { + let from = tracker.expr_to_node[¤t]; + let to = tracker.expr_to_node[&expr]; + tracker.graph.add_edge(from, to, ()); + } + return Ok(self.new_expr(Ir::ExprRef(expr))); } } @@ -177,4 +199,59 @@ impl DowngradeContext for DowngradeCtx<'_> { fn get_current_dir(&self) -> std::path::PathBuf { self.ctx.get_current_dir() } + + fn push_dep_tracker(&mut self, slots: &[ExprId]) { + let mut graph = Graph::new(); + let mut expr_to_node = HashMap::new(); + let mut let_scope_exprs = HashSet::new(); + + for &expr in slots.iter() { + let node = graph.add_node(expr); + expr_to_node.insert(expr, node); + let_scope_exprs.insert(expr); + } + + self.dep_tracker_stack.push(DependencyTracker { + expr_to_node, + graph, + current_binding: None, + let_scope_exprs, + }); + } + + fn set_current_binding(&mut self, expr: Option) { + if let Some(tracker) = self.dep_tracker_stack.last_mut() { + tracker.current_binding = expr; + } + } + + fn pop_dep_tracker(&mut self) -> Result { + let tracker = self + .dep_tracker_stack + .pop() + .expect("pop_dep_tracker without active tracker"); + + use petgraph::algo::kosaraju_scc; + let sccs = kosaraju_scc(&tracker.graph); + + let mut sccs_topo = Vec::new(); + + for scc_nodes in sccs.iter() { + let mut scc_exprs = Vec::new(); + let mut is_recursive = scc_nodes.len() > 1; + + for &node_idx in scc_nodes { + let expr = tracker.graph[node_idx]; + scc_exprs.push(expr); + + if !is_recursive && tracker.graph.contains_edge(node_idx, node_idx) { + is_recursive = true; + } + } + + sccs_topo.push((scc_exprs, is_recursive)); + } + + Ok(SccInfo { sccs: sccs_topo }) + } } diff --git a/nix-js/src/ir.rs b/nix-js/src/ir.rs index a3ec6c2..ced34c8 100644 --- a/nix-js/src/ir.rs +++ b/nix-js/src/ir.rs @@ -3,6 +3,7 @@ use hashbrown::{HashMap, HashSet}; use rnix::ast; use string_interner::symbol::SymbolU32; +use crate::context::SccInfo; use crate::error::{Error, Result}; use crate::value::format_symbol; use nix_js_macros::ir; @@ -38,6 +39,10 @@ pub trait DowngradeContext { F: FnOnce(&mut Self) -> R; fn get_current_dir(&self) -> std::path::PathBuf; + + fn push_dep_tracker(&mut self, slots: &[ExprId]); + fn set_current_binding(&mut self, expr: Option); + fn pop_dep_tracker(&mut self) -> Result; } ir! { @@ -315,8 +320,8 @@ pub struct Func { /// Represents a `let ... in ...` expression. #[derive(Debug)] pub struct Let { - /// The bindings in the let expression. - pub bindings: Vec, + /// The bindings in the `let` expression, group in SCCs + pub binding_sccs: SccInfo, /// The body expression evaluated in the scope of the bindings. pub body: ExprId, } diff --git a/nix-js/src/ir/downgrade.rs b/nix-js/src/ir/downgrade.rs index 65dbd5c..dd31593 100644 --- a/nix-js/src/ir/downgrade.rs +++ b/nix-js/src/ir/downgrade.rs @@ -172,22 +172,29 @@ impl Downgrade 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 { - stcs: HashMap::new(), - dyns: Vec::new(), - }; + let (binding_sccs, body) = + downgrade_let_bindings(entries, ctx, |ctx, binding_keys| { + // Create plain attrset as body with inherit + let mut attrs = AttrSet { + stcs: HashMap::new(), + dyns: Vec::new(), + }; - for sym in binding_keys { - let expr = ctx.lookup(*sym)?; - attrs.stcs.insert(*sym, expr); + for sym in binding_keys { + let expr = ctx.lookup(*sym)?; + attrs.stcs.insert(*sym, expr); + } + + Ok(ctx.new_expr(attrs.to_ir())) + })?; + + Ok(ctx.new_expr( + Let { + body, + binding_sccs, } - - Ok(ctx.new_expr(attrs.to_ir())) - })?; - - Ok(ctx.new_expr(Let { bindings, body }.to_ir())) + .to_ir(), + )) } } @@ -289,10 +296,16 @@ impl Downgrade for ast::LetIn { let entries: Vec<_> = self.entries().collect(); let body_expr = self.body().unwrap(); - let (bindings, body) = + let (binding_sccs, body) = downgrade_let_bindings(entries, ctx, |ctx, _binding_keys| body_expr.downgrade(ctx))?; - Ok(ctx.new_expr(Let { bindings, body }.to_ir())) + Ok(ctx.new_expr( + Let { + body, + binding_sccs, + } + .to_ir(), + )) } } @@ -333,76 +346,35 @@ impl Downgrade for ast::Lambda { .with_param_scope(param_sym, arg, |ctx| self.body().unwrap().downgrade(ctx))?; } ast::Param::Pattern(pattern) => { - // Complex case: `{ a, b ? 2, ... }@args: body` let alias = pattern .pat_bind() .map(|alias| ctx.new_sym(alias.ident().unwrap().to_string())); ident = alias; - let entries = pattern - .pat_entries() - .map(|entry| { - let ident = ctx.new_sym(entry.ident().unwrap().to_string()); - if entry.default().is_none() { - Ok((ident, None)) - } else { - entry - .default() - .unwrap() - .downgrade(ctx) - .map(|ok| (ident, Some(ok))) - } - }) - .collect::>>()?; + let has_ellipsis = pattern.ellipsis_token().is_some(); + let pat_entries = pattern.pat_entries(); - required = Some( - entries - .iter() - .filter_map(|(k, d)| if d.is_none() { Some(*k) } else { None }) - .collect(), - ); - allowed = if pattern.ellipsis_token().is_some() { - None // `...` means any attribute is allowed. - } else { - Some(entries.iter().map(|(k, _)| *k).collect()) - }; + let PatternBindings { + body: inner_body, + scc_info, + required_params, + allowed_params, + } = downgrade_pattern_bindings( + pat_entries, + alias, + arg, + has_ellipsis, + ctx, + |ctx, _| self.body().unwrap().downgrade(ctx), + )?; - // Desugar pattern matching in function arguments into a `let` expression. - // For example, `({ a, b ? 2 }): a + b` is desugared into: - // `arg: let a = arg.a; b = arg.b or 2; in a + b` - let mut bindings: HashMap<_, _> = entries - .into_iter() - .map(|(k, default)| { - // For each formal parameter, create a `Select` expression to extract it from the argument set. - ( - k, - ctx.new_expr( - Select { - expr: arg, - attrpath: vec![Attr::Str(k)], - default, - } - .to_ir(), - ), - ) - }) - .collect(); + required = Some(required_params); + allowed = allowed_params; - // If there's an alias (`... }@alias`), bind the alias name to the raw argument set. - if let Some(alias) = alias { - bindings.insert(alias, arg); - } - - // 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))?; - - // Create Let expression to wrap the bindings body = ctx.new_expr( Let { - bindings: bindings_vec, body: inner_body, + binding_sccs: scc_info, } .to_ir(), ); diff --git a/nix-js/src/ir/utils.rs b/nix-js/src/ir/utils.rs index 8cee768..12a4ea1 100644 --- a/nix-js/src/ir/utils.rs +++ b/nix-js/src/ir/utils.rs @@ -219,22 +219,177 @@ pub fn downgrade_static_attrpathvalue( attrs.insert(path, value, ctx) } +pub struct PatternBindings { + pub body: ExprId, + pub scc_info: SccInfo, + pub required_params: Vec, + pub allowed_params: Option>, +} + +/// Helper function for Lambda pattern parameters with SCC analysis. +/// Processes pattern entries like `{ a, b ? 2, ... }@alias` and creates optimized bindings. +/// +/// # Parameters +/// - `pat_entries`: Iterator over pattern entries from the AST +/// - `alias`: Optional alias symbol (from @alias syntax) +/// - `arg`: The argument expression to extract from +/// +/// Returns a tuple of (binding slots, body, SCC info, required params, allowed params) +pub fn downgrade_pattern_bindings( + pat_entries: impl Iterator, + alias: Option, + arg: ExprId, + has_ellipsis: bool, + ctx: &mut Ctx, + body_fn: impl FnOnce(&mut Ctx, &[SymId]) -> Result, +) -> Result +where + Ctx: DowngradeContext, +{ + let mut param_syms = Vec::new(); + let mut param_defaults = Vec::new(); + let mut seen_params = HashSet::new(); + + for entry in pat_entries { + let sym = ctx.new_sym(entry.ident().unwrap().to_string()); + + if !seen_params.insert(sym) { + return Err(Error::downgrade_error(format!( + "duplicate parameter '{}'", + format_symbol(ctx.get_sym(sym)) + ))); + } + + let default_ast = entry.default(); + param_syms.push(sym); + param_defaults.push(default_ast); + } + + let mut binding_keys: Vec = param_syms.clone(); + if let Some(alias_sym) = alias { + binding_keys.push(alias_sym); + } + + let required: Vec = param_syms + .iter() + .zip(param_defaults.iter()) + .filter_map(|(&sym, default)| if default.is_none() { Some(sym) } else { None }) + .collect(); + + let allowed: Option> = if has_ellipsis { + None + } else { + Some(param_syms.iter().copied().collect()) + }; + + let (scc_info, body) = downgrade_bindings_generic( + ctx, + binding_keys, + |ctx, sym_to_slot| { + let mut bindings = HashMap::new(); + + for (sym, default_ast) in param_syms.iter().zip(param_defaults.iter()) { + let slot = *sym_to_slot.get(sym).unwrap(); + ctx.set_current_binding(Some(slot)); + + let default = if let Some(default_expr) = default_ast { + Some(default_expr.clone().downgrade(ctx)?) + } else { + None + }; + + let select_expr = ctx.new_expr( + Select { + expr: arg, + attrpath: vec![Attr::Str(*sym)], + default, + } + .to_ir(), + ); + bindings.insert(*sym, select_expr); + ctx.set_current_binding(None); + } + + if let Some(alias_sym) = alias { + bindings.insert(alias_sym, arg); + } + + Ok(bindings) + }, + body_fn, + )?; + + Ok(PatternBindings { + body, + scc_info, + required_params: required, + allowed_params: allowed + }) +} + +/// Generic helper function to downgrade bindings with SCC analysis. +/// This is the core logic for let bindings, extracted for reuse. +/// +/// # Parameters +/// - `binding_keys`: The symbols for all bindings +/// - `compute_bindings_fn`: Called in let scope with sym_to_slot mapping to compute binding values +/// - `body_fn`: Called in let scope to compute the body expression +/// +/// Returns a tuple of (binding slots, body result, SCC info) +pub fn downgrade_bindings_generic( + ctx: &mut Ctx, + binding_keys: Vec, + compute_bindings_fn: B, + body_fn: F, +) -> Result<(SccInfo, ExprId)> +where + Ctx: DowngradeContext, + B: FnOnce(&mut Ctx, &HashMap) -> Result>, + F: FnOnce(&mut Ctx, &[SymId]) -> Result, +{ + let slots: Vec<_> = ctx.reserve_slots(binding_keys.len()).collect(); + let let_bindings: HashMap<_, _> = binding_keys.iter().copied().zip(slots.iter().copied()).collect(); + + ctx.push_dep_tracker(&slots); + + ctx.with_let_scope(let_bindings.clone(), |ctx| { + let bindings = compute_bindings_fn(ctx, &let_bindings)?; + + let scc_info = ctx.pop_dep_tracker()?; + + for (sym, slot) in binding_keys.iter().copied().zip(slots.iter()) { + if let Some(&expr) = bindings.get(&sym) { + ctx.replace_expr(*slot, Ir::Thunk(expr)); + } else { + return Err(Error::downgrade_error(format!( + "binding '{}' not found", + format_symbol(ctx.get_sym(sym)) + ))); + } + } + + let body = body_fn(ctx, &binding_keys)?; + + Ok((scc_info, body)) + }) +} + /// Helper function to downgrade entries with let bindings semantics. /// This extracts common logic for both `rec` attribute sets and `let...in` expressions. /// -/// Returns a tuple of (binding slots, body result) where: +/// Returns a tuple of (binding slots, body result, SCC info) where: /// - binding slots: pre-allocated expression slots for the bindings /// - body result: the result of calling `body_fn` in the let scope -pub fn downgrade_let_bindings( +/// - SCC info: strongly connected components information for optimization +pub fn downgrade_let_bindings( entries: Vec, ctx: &mut Ctx, body_fn: F, -) -> Result<(Vec, R)> +) -> Result<(SccInfo, ExprId)> where Ctx: DowngradeContext, - F: FnOnce(&mut Ctx, &[SymId]) -> Result, + F: FnOnce(&mut Ctx, &[SymId]) -> Result, { - // Collect all top-level binding keys let mut binding_syms = HashSet::new(); for entry in &entries { @@ -271,47 +426,45 @@ where let binding_keys: Vec<_> = binding_syms.into_iter().collect(); - // Reserve slots for bindings - let slots_iter = ctx.reserve_slots(binding_keys.len()); - let slots_clone = slots_iter.clone(); + downgrade_bindings_generic( + ctx, + binding_keys, + |ctx, sym_to_slot| { + let mut temp_attrs = AttrSet { + stcs: HashMap::new(), + dyns: Vec::new(), + }; - // Create let scope bindings - let let_bindings: HashMap<_, _> = binding_keys.iter().copied().zip(slots_iter).collect(); - - // Process entries in let scope - let body = ctx.with_let_scope(let_bindings, |ctx| { - // Collect all bindings in a temporary AttrSet - let mut temp_attrs = AttrSet { - stcs: HashMap::new(), - dyns: Vec::new(), - }; - - for entry in entries { - match entry { - ast::Entry::Inherit(inherit) => { - downgrade_inherit(inherit, &mut temp_attrs.stcs, ctx)?; - } - ast::Entry::AttrpathValue(value) => { - downgrade_static_attrpathvalue(value, &mut temp_attrs, ctx)?; + for entry in entries { + match entry { + ast::Entry::Inherit(inherit) => { + for attr in inherit.attrs() { + if let ast::Attr::Ident(ident) = attr { + let sym = ctx.new_sym(ident.to_string()); + let slot = *sym_to_slot.get(&sym).unwrap(); + ctx.set_current_binding(Some(slot)); + } + } + downgrade_inherit(inherit, &mut temp_attrs.stcs, ctx)?; + ctx.set_current_binding(None); + } + ast::Entry::AttrpathValue(value) => { + let attrpath = value.attrpath().unwrap(); + if let Some(first_attr) = attrpath.attrs().next() + && let ast::Attr::Ident(ident) = first_attr + { + let sym = ctx.new_sym(ident.to_string()); + let slot = *sym_to_slot.get(&sym).unwrap(); + ctx.set_current_binding(Some(slot)); + } + downgrade_static_attrpathvalue(value, &mut temp_attrs, ctx)?; + ctx.set_current_binding(None); + } } } - } - // Fill pre-allocated slots with top-level bindings - for (sym, slot) in binding_keys.iter().copied().zip(slots_clone.clone()) { - if let Some(&expr) = temp_attrs.stcs.get(&sym) { - ctx.replace_expr(slot, Ir::Thunk(expr)); - } else { - return Err(Error::downgrade_error(format!( - "binding '{}' not found", - format_symbol(ctx.get_sym(sym)) - ))); - } - } - - // Call the body function with the binding keys - body_fn(ctx, &binding_keys) - })?; - - Ok((slots_clone.collect(), body)) + Ok(temp_attrs.stcs) + }, + body_fn, + ) } diff --git a/nix-js/tests/basic_eval.rs b/nix-js/tests/basic_eval.rs new file mode 100644 index 0000000..af85462 --- /dev/null +++ b/nix-js/tests/basic_eval.rs @@ -0,0 +1,65 @@ +mod utils; + +use nix_js::value::Value; +use utils::eval; + +#[test] +fn arithmetic() { + assert_eq!(eval("1 + 1"), Value::Int(2)); +} + +#[test] +fn simple_function_application() { + assert_eq!(eval("(x: x) 1"), Value::Int(1)); +} + +#[test] +fn curried_function() { + assert_eq!(eval("(x: y: x - y) 2 1"), Value::Int(1)); +} + +#[test] +fn rec_attrset() { + assert_eq!(eval("rec { b = a; a = 1; }.b"), Value::Int(1)); +} + +#[test] +fn let_binding() { + assert_eq!(eval("let b = a; a = 1; in b"), Value::Int(1)); +} + +#[test] +fn fibonacci() { + assert_eq!( + eval( + "let fib = n: if n == 1 || n == 2 then 1 else (fib (n - 1)) + (fib (n - 2)); in fib 30" + ), + Value::Int(832040) + ); +} + +#[test] +fn fixed_point_combinator() { + assert_eq!( + eval("((f: let x = f x; in x)(self: { x = 1; y = self.x + 1; })).y"), + Value::Int(2) + ); +} + +#[test] +fn conditional_true() { + assert_eq!(eval("if true then 1 else 0"), Value::Int(1)); +} + +#[test] +fn conditional_false() { + assert_eq!(eval("if false then 1 else 0"), Value::Int(0)); +} + +#[test] +fn nested_let() { + assert_eq!( + eval("let x = 1; in let y = x + 1; z = y + 1; in z"), + Value::Int(3) + ); +} diff --git a/nix-js/tests/builtins.rs b/nix-js/tests/builtins.rs new file mode 100644 index 0000000..dafa28b --- /dev/null +++ b/nix-js/tests/builtins.rs @@ -0,0 +1,149 @@ +mod utils; + +use nix_js::value::{List, Value}; +use utils::eval; + +#[test] +fn builtins_accessible() { + let result = eval("builtins"); + assert!(matches!(result, Value::AttrSet(_))); +} + +#[test] +fn builtins_self_reference() { + let result = eval("builtins.builtins"); + assert!(matches!(result, Value::AttrSet(_))); +} + +#[test] +fn builtins_add() { + assert_eq!(eval("builtins.add 1 2"), Value::Int(3)); +} + +#[test] +fn builtins_length() { + assert_eq!(eval("builtins.length [1 2 3]"), Value::Int(3)); +} + +#[test] +fn builtins_map() { + assert_eq!( + eval("builtins.map (x: x * 2) [1 2 3]"), + Value::List(List::new(vec![Value::Int(2), Value::Int(4), Value::Int(6)])) + ); +} + +#[test] +fn builtins_filter() { + assert_eq!( + eval("builtins.filter (x: x > 1) [1 2 3]"), + Value::List(List::new(vec![Value::Int(2), Value::Int(3)])) + ); +} + +#[test] +fn builtins_attrnames() { + let result = eval("builtins.attrNames { a = 1; b = 2; }"); + assert!(matches!(result, Value::List(_))); + if let Value::List(list) = result { + assert_eq!(format!("{:?}", list).matches(',').count() + 1, 2); + } +} + +#[test] +fn builtins_head() { + assert_eq!(eval("builtins.head [1 2 3]"), Value::Int(1)); +} + +#[test] +fn builtins_tail() { + assert_eq!( + eval("builtins.tail [1 2 3]"), + Value::List(List::new(vec![Value::Int(2), Value::Int(3)])) + ); +} + +#[test] +fn builtins_in_let() { + assert_eq!(eval("let b = builtins; in b.add 5 3"), Value::Int(8)); +} + +#[test] +fn builtins_in_with() { + assert_eq!(eval("with builtins; add 10 20"), Value::Int(30)); +} + +#[test] +fn builtins_nested_calls() { + assert_eq!( + eval("builtins.add (builtins.mul 2 3) (builtins.sub 10 5)"), + Value::Int(11) + ); +} + +#[test] +fn builtins_is_list() { + assert_eq!(eval("builtins.isList [1 2 3]"), Value::Bool(true)); +} + +#[test] +fn builtins_is_attrs() { + assert_eq!(eval("builtins.isAttrs { a = 1; }"), Value::Bool(true)); +} + +#[test] +fn builtins_is_function() { + assert_eq!(eval("builtins.isFunction (x: x)"), Value::Bool(true)); +} + +#[test] +fn builtins_is_null() { + assert_eq!(eval("builtins.isNull null"), Value::Bool(true)); +} + +#[test] +fn builtins_is_bool() { + assert_eq!(eval("builtins.isBool true"), Value::Bool(true)); +} + +#[test] +fn builtins_shadowing() { + assert_eq!( + eval("let builtins = { add = x: y: x - y; }; in builtins.add 5 3"), + Value::Int(2) + ); +} + +#[test] +fn builtins_lazy_evaluation() { + let result = eval("builtins.builtins.builtins.add 1 1"); + assert_eq!(result, Value::Int(2)); +} + +#[test] +fn builtins_foldl() { + assert_eq!( + eval("builtins.foldl' (acc: x: acc + x) 0 [1 2 3 4 5]"), + Value::Int(15) + ); +} + +#[test] +fn builtins_elem() { + assert_eq!(eval("builtins.elem 2 [1 2 3]"), Value::Bool(true)); + assert_eq!(eval("builtins.elem 5 [1 2 3]"), Value::Bool(false)); +} + +#[test] +fn builtins_concat_lists() { + assert_eq!( + eval("builtins.concatLists [[1 2] [3 4] [5]]"), + Value::List(List::new(vec![ + Value::Int(1), + Value::Int(2), + Value::Int(3), + Value::Int(4), + Value::Int(5) + ])) + ); +} diff --git a/nix-js/tests/free_globals.rs b/nix-js/tests/free_globals.rs new file mode 100644 index 0000000..77dbf32 --- /dev/null +++ b/nix-js/tests/free_globals.rs @@ -0,0 +1,75 @@ +mod utils; + +use nix_js::value::{List, Value}; +use utils::eval; + +#[test] +fn true_literal() { + assert_eq!(eval("true"), Value::Bool(true)); +} + +#[test] +fn false_literal() { + assert_eq!(eval("false"), Value::Bool(false)); +} + +#[test] +fn null_literal() { + assert_eq!(eval("null"), Value::Null); +} + +#[test] +fn map_function() { + assert_eq!( + eval("map (x: x * 2) [1 2 3]"), + Value::List(List::new(vec![Value::Int(2), Value::Int(4), Value::Int(6)])) + ); +} + +#[test] +fn is_null_function() { + assert_eq!(eval("isNull null"), Value::Bool(true)); + assert_eq!(eval("isNull 5"), Value::Bool(false)); +} + +#[test] +fn shadow_true() { + assert_eq!(eval("let true = false; in true"), Value::Bool(false)); +} + +#[test] +fn shadow_map() { + assert_eq!(eval("let map = x: y: x; in map 1 2"), Value::Int(1)); +} + +#[test] +fn mixed_usage() { + assert_eq!( + eval("if true then map (x: x + 1) [1 2] else []"), + Value::List(List::new(vec![Value::Int(2), Value::Int(3)])) + ); +} + +#[test] +fn in_let_bindings() { + assert_eq!( + eval("let x = true; y = false; in x && y"), + Value::Bool(false) + ); +} + +#[test] +fn shadow_in_function() { + assert_eq!(eval("(true: true) false"), Value::Bool(false)); +} + +#[test] +fn throw_function() { + let result = utils::eval_result("throw \"error message\""); + assert!(result.is_err()); +} + +#[test] +fn to_string_function() { + assert_eq!(eval("toString 42"), Value::String("42".to_string())); +} diff --git a/nix-js/tests/functions.rs b/nix-js/tests/functions.rs new file mode 100644 index 0000000..912a292 --- /dev/null +++ b/nix-js/tests/functions.rs @@ -0,0 +1,124 @@ +mod utils; + +use nix_js::value::Value; +use utils::{eval, eval_result}; + +#[test] +fn required_parameters() { + assert_eq!(eval("({ a, b }: a + b) { a = 1; b = 2; }"), Value::Int(3)); +} + +#[test] +fn missing_required_parameter() { + let result = eval_result("({ a, b }: a + b) { a = 1; }"); + assert!(result.is_err()); +} + +#[test] +fn all_required_parameters_present() { + assert_eq!( + eval("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }"), + Value::Int(6) + ); +} + +#[test] +fn reject_unexpected_arguments() { + let result = eval_result("({ a, b }: a + b) { a = 1; b = 2; c = 3; }"); + assert!(result.is_err()); +} + +#[test] +fn ellipsis_accepts_extra_arguments() { + assert_eq!( + eval("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }"), + Value::Int(3) + ); +} + +#[test] +fn default_parameters() { + assert_eq!(eval("({ a, b ? 5 }: a + b) { a = 1; }"), Value::Int(6)); +} + +#[test] +fn override_default_parameter() { + assert_eq!( + eval("({ a, b ? 5 }: a + b) { a = 1; b = 10; }"), + Value::Int(11) + ); +} + +#[test] +fn at_pattern_alias() { + assert_eq!( + eval("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }"), + Value::Int(3) + ); +} + +#[test] +fn simple_parameter_no_validation() { + assert_eq!(eval("(x: x.a + x.b) { a = 1; b = 2; }"), Value::Int(3)); +} + +#[test] +fn simple_parameter_accepts_any_argument() { + assert_eq!(eval("(x: x) 42"), Value::Int(42)); +} + +#[test] +fn nested_function_parameters() { + assert_eq!( + eval("({ a }: { b }: a + b) { a = 5; } { b = 3; }"), + Value::Int(8) + ); +} + +#[test] +fn pattern_param_simple_reference_in_default() { + assert_eq!( + eval("({ a, b ? a }: b) { a = 10; }"), + Value::Int(10) + ); +} + +#[test] +fn pattern_param_multiple_references_in_default() { + assert_eq!( + eval("({ a, b ? a + 5, c ? 1 }: b + c) { a = 10; }"), + Value::Int(16) + ); +} + +#[test] +fn pattern_param_mutual_reference() { + assert_eq!( + eval("({ a, b ? c + 1, c ? 5 }: b) { a = 1; }"), + Value::Int(6) + ); +} + +#[test] +fn pattern_param_override_mutual_reference() { + assert_eq!( + eval("({ a, b ? c + 1, c ? 5 }: b) { a = 1; c = 10; }"), + Value::Int(11) + ); +} + +#[test] +fn pattern_param_reference_list() { + assert_eq!( + eval("({ a, b ? [ a 2 ] }: builtins.elemAt b 0) { a = 42; }"), + Value::Int(42) + ); +} + +#[test] +fn pattern_param_alias_in_default() { + assert_eq!( + eval("(args@{ a, b ? args.a + 10 }: b) { a = 5; }"), + Value::Int(15) + ); +} diff --git a/nix-js/tests/io_operations.rs b/nix-js/tests/io_operations.rs new file mode 100644 index 0000000..84c6ebb --- /dev/null +++ b/nix-js/tests/io_operations.rs @@ -0,0 +1,103 @@ +mod utils; + +use nix_js::context::Context; +use nix_js::value::Value; + +#[test] +fn import_absolute_path() { + let mut ctx = Context::new().unwrap(); + + let temp_dir = tempfile::tempdir().unwrap(); + let lib_path = temp_dir.path().join("nix_test_lib.nix"); + + std::fs::write(&lib_path, "{ add = a: b: a + b; }").unwrap(); + + let expr = format!(r#"(import "{}").add 3 5"#, lib_path.display()); + assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(8)); +} + +#[test] +fn import_nested() { + let mut ctx = Context::new().unwrap(); + + let temp_dir = tempfile::tempdir().unwrap(); + + let lib_path = temp_dir.path().join("lib.nix"); + std::fs::write(&lib_path, "{ add = a: b: a + b; }").unwrap(); + + 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() + ); + std::fs::write(&main_path, main_content).unwrap(); + + let expr = format!(r#"(import "{}").result"#, main_path.display()); + assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(30)); +} + +#[test] +fn import_relative_path() { + let mut ctx = Context::new().unwrap(); + + let temp_dir = tempfile::tempdir().unwrap(); + let subdir = temp_dir.path().join("subdir"); + std::fs::create_dir_all(&subdir).unwrap(); + + let lib_path = temp_dir.path().join("lib.nix"); + std::fs::write(&lib_path, "{ multiply = a: b: a * b; }").unwrap(); + + let helper_path = subdir.join("helper.nix"); + std::fs::write(&helper_path, "{ subtract = a: b: a - b; }").unwrap(); + + let main_path = temp_dir.path().join("main.nix"); + let main_content = r#" + let + lib = import ./lib.nix; + helper = import ./subdir/helper.nix; + in { + result1 = lib.multiply 3 4; + result2 = helper.subtract 10 3; + } + "#; + std::fs::write(&main_path, main_content).unwrap(); + + let expr = format!(r#"let x = import "{}"; in x.result1"#, main_path.display()); + assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(12)); + + let expr = format!(r#"let x = import "{}"; in x.result2"#, main_path.display()); + assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(7)); +} + +#[test] +fn import_returns_function() { + let mut ctx = Context::new().unwrap(); + + 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 expr = format!(r#"(import "{}") 5"#, func_path.display()); + assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(10)); +} + +#[test] +fn import_with_complex_dependency_graph() { + let mut ctx = Context::new().unwrap(); + let temp_dir = tempfile::tempdir().unwrap(); + + let utils_path = temp_dir.path().join("utils.nix"); + std::fs::write(&utils_path, "{ double = x: x * 2; }").unwrap(); + + let math_path = temp_dir.path().join("math.nix"); + let math_content = + r#"let utils = import ./utils.nix; in { triple = x: x + utils.double x; }"#; + std::fs::write(&math_path, math_content).unwrap(); + + let main_path = temp_dir.path().join("main.nix"); + let main_content = r#"let math = import ./math.nix; in math.triple 5"#; + std::fs::write(&main_path, main_content).unwrap(); + + let expr = format!(r#"import "{}""#, main_path.display()); + assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(15)); +} diff --git a/nix-js/tests/numeric_types.rs b/nix-js/tests/numeric_types.rs new file mode 100644 index 0000000..46cdd17 --- /dev/null +++ b/nix-js/tests/numeric_types.rs @@ -0,0 +1,139 @@ +mod utils; + +use nix_js::value::Value; +use utils::eval; + +#[test] +fn large_i64_max() { + assert_eq!(eval("9223372036854775807"), Value::Int(9223372036854775807)); +} + +#[test] +fn large_i64_negative() { + assert_eq!( + eval("-9223372036854775807"), + Value::Int(-9223372036854775807) + ); +} + +#[test] +fn large_number_arithmetic() { + assert_eq!( + eval("5000000000000000000 + 3000000000000000000"), + Value::Int(8000000000000000000i64) + ); +} + +#[test] +fn is_int_with_int() { + assert_eq!(eval("builtins.isInt 42"), Value::Bool(true)); +} + +#[test] +fn is_int_with_float() { + assert_eq!(eval("builtins.isInt 42.0"), Value::Bool(false)); +} + +#[test] +fn is_float_with_int() { + assert_eq!(eval("builtins.isFloat 42"), Value::Bool(false)); +} + +#[test] +fn is_float_with_float() { + assert_eq!(eval("builtins.isFloat 42.5"), Value::Bool(true)); + assert_eq!(eval("builtins.isFloat 1.0"), Value::Bool(true)); +} + +#[test] +fn typeof_int() { + assert_eq!(eval("builtins.typeOf 1"), Value::String("int".to_string())); +} + +#[test] +fn typeof_float() { + assert_eq!( + eval("builtins.typeOf 1.0"), + Value::String("float".to_string()) + ); + assert_eq!( + eval("builtins.typeOf 3.14"), + Value::String("float".to_string()) + ); +} + +#[test] +fn int_literal() { + assert_eq!(eval("1"), Value::Int(1)); +} + +#[test] +fn float_literal() { + assert_eq!(eval("1."), Value::Float(1.)); +} + +#[test] +fn int_plus_int() { + assert_eq!( + eval("builtins.typeOf (1 + 2)"), + Value::String("int".to_string()) + ); +} + +#[test] +fn int_plus_float() { + assert_eq!( + eval("builtins.typeOf (1 + 2.0)"), + Value::String("float".to_string()) + ); +} + +#[test] +fn int_times_int() { + assert_eq!( + eval("builtins.typeOf (3 * 4)"), + Value::String("int".to_string()) + ); +} + +#[test] +fn int_times_float() { + assert_eq!( + eval("builtins.typeOf (3 * 4.0)"), + Value::String("float".to_string()) + ); +} + +#[test] +fn integer_division() { + assert_eq!(eval("5 / 2"), Value::Int(2)); + assert_eq!(eval("7 / 3"), Value::Int(2)); + assert_eq!(eval("10 / 3"), Value::Int(3)); +} + +#[test] +fn float_division() { + assert_eq!(eval("5 / 2.0"), Value::Float(2.5)); + assert_eq!(eval("7.0 / 2"), Value::Float(3.5)); +} + +#[test] +fn negative_integer_division() { + assert_eq!(eval("(-7) / 3"), Value::Int(-2)); +} + +#[test] +fn builtin_add_with_large_numbers() { + assert_eq!( + eval("builtins.add 5000000000000000000 3000000000000000000"), + Value::Int(8000000000000000000i64) + ); +} + +#[test] +fn builtin_mul_with_large_numbers() { + assert_eq!( + eval("builtins.mul 1000000000 1000000000"), + Value::Int(1000000000000000000i64) + ); +} diff --git a/nix-js/tests/operators.rs b/nix-js/tests/operators.rs new file mode 100644 index 0000000..32dd2cd --- /dev/null +++ b/nix-js/tests/operators.rs @@ -0,0 +1,101 @@ +mod utils; + +use nix_js::value::{AttrSet, List, Symbol, Value}; +use std::collections::BTreeMap; +use utils::eval; + +#[test] +fn addition() { + assert_eq!(eval("1 + 1"), Value::Int(2)); +} + +#[test] +fn subtraction() { + assert_eq!(eval("2 - 1"), Value::Int(1)); +} + +#[test] +fn multiplication() { + assert_eq!(eval("1. * 1"), Value::Float(1.)); +} + +#[test] +fn division() { + assert_eq!(eval("1 / 1."), Value::Float(1.)); +} + +#[test] +fn equality() { + assert_eq!(eval("1 == 1"), Value::Bool(true)); +} + +#[test] +fn inequality() { + assert_eq!(eval("1 != 1"), Value::Bool(false)); +} + +#[test] +fn less_than() { + assert_eq!(eval("2 < 1"), Value::Bool(false)); +} + +#[test] +fn greater_than() { + assert_eq!(eval("2 > 1"), Value::Bool(true)); +} + +#[test] +fn less_than_or_equal() { + assert_eq!(eval("1 <= 1"), Value::Bool(true)); +} + +#[test] +fn greater_than_or_equal() { + assert_eq!(eval("1 >= 1"), Value::Bool(true)); +} + +#[test] +fn logical_or_short_circuit() { + assert_eq!(eval("true || (1 / 0)"), Value::Bool(true)); +} + +#[test] +fn logical_and() { + assert_eq!(eval("true && 1 == 0"), Value::Bool(false)); +} + +#[test] +fn list_concatenation() { + assert_eq!( + eval("[ 1 2 3 ] ++ [ 4 5 6 ]"), + Value::List(List::new((1..=6).map(Value::Int).collect())) + ); +} + +#[test] +fn attrset_update() { + assert_eq!( + eval("{ 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::Int(2), + )]))), + ), + (Symbol::from("b"), Value::Int(2)), + ]))) + ); +} + +#[test] +fn unary_negation() { + assert_eq!(eval("-5"), Value::Int(-5)); +} + +#[test] +fn logical_not() { + assert_eq!(eval("!true"), Value::Bool(false)); + assert_eq!(eval("!false"), Value::Bool(true)); +} diff --git a/nix-js/tests/scc_optimization.rs b/nix-js/tests/scc_optimization.rs new file mode 100644 index 0000000..b85df65 --- /dev/null +++ b/nix-js/tests/scc_optimization.rs @@ -0,0 +1,120 @@ +mod utils; + +use nix_js::value::Value; +use utils::eval; + +#[test] +fn non_recursive_bindings() { + assert_eq!(eval("let x = 1; y = 2; z = x + y; in z"), Value::Int(3)); +} + +#[test] +fn non_recursive_multiple_bindings() { + assert_eq!( + eval("let a = 10; b = 20; c = 30; d = a + b + c; in d"), + Value::Int(60) + ); +} + +#[test] +fn recursive_fibonacci() { + assert_eq!( + eval("let fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); in fib 5"), + Value::Int(8) + ); +} + +#[test] +fn recursive_factorial() { + assert_eq!( + eval("let factorial = n: if n == 0 then 1 else n * factorial (n - 1); in factorial 5"), + Value::Int(120) + ); +} + +#[test] +fn mutual_recursion_simple() { + assert_eq!( + eval( + "let f = n: if n == 0 then 0 else g (n - 1); g = n: if n == 0 then 1 else f (n - 1); in f 5" + ), + Value::Int(1) + ); +} + +#[test] +fn mutual_recursion_even_odd() { + assert_eq!( + eval( + "let even = n: if n == 0 then true else odd (n - 1); odd = n: if n == 0 then false else even (n - 1); in even 4" + ), + Value::Bool(true) + ); +} + +#[test] +fn mixed_recursive_and_non_recursive() { + assert_eq!( + eval("let x = 1; f = n: if n == 0 then x else f (n - 1); in f 5"), + Value::Int(1) + ); +} + +#[test] +fn mixed_with_multiple_non_recursive() { + assert_eq!( + eval( + "let a = 10; b = 20; sum = a + b; countdown = n: if n == 0 then sum else countdown (n - 1); in countdown 3" + ), + Value::Int(30) + ); +} + +#[test] +fn rec_attrset_non_recursive() { + assert_eq!(eval("rec { x = 1; y = 2; z = x + y; }.z"), Value::Int(3)); +} + +#[test] +fn rec_attrset_recursive() { + assert_eq!( + eval("rec { f = n: if n == 0 then 0 else f (n - 1); }.f 10"), + Value::Int(0) + ); +} + +#[test] +fn nested_let_non_recursive() { + assert_eq!( + eval("let x = 1; in let y = x + 1; z = y + 1; in z"), + Value::Int(3) + ); +} + +#[test] +fn nested_let_with_recursive() { + assert_eq!( + eval("let f = n: if n == 0 then 0 else f (n - 1); in let g = m: f m; in g 5"), + Value::Int(0) + ); +} + +#[test] +fn three_way_mutual_recursion() { + assert_eq!( + eval( + "let a = n: if n == 0 then 1 else b (n - 1); b = n: if n == 0 then 2 else c (n - 1); c = n: if n == 0 then 3 else a (n - 1); in a 6" + ), + Value::Int(1) + ); +} + +#[test] +fn complex_mixed_dependencies() { + assert_eq!( + eval( + "let x = 5; y = 10; sum = x + y; fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); result = sum + fib 5; in result" + ), + Value::Int(23) + ); +} diff --git a/nix-js/tests/to_string.rs b/nix-js/tests/to_string.rs new file mode 100644 index 0000000..80b863d --- /dev/null +++ b/nix-js/tests/to_string.rs @@ -0,0 +1,251 @@ +mod utils; + +use nix_js::value::Value; +use utils::eval; + +#[test] +fn string_returns_as_is() { + assert_eq!(eval(r#"toString "hello""#), Value::String("hello".to_string())); +} + +#[test] +fn integer_to_string() { + assert_eq!(eval("toString 42"), Value::String("42".to_string())); + assert_eq!(eval("toString (-5)"), Value::String("-5".to_string())); + assert_eq!(eval("toString 0"), Value::String("0".to_string())); +} + +#[test] +fn float_to_string() { + assert_eq!(eval("toString 3.14"), Value::String("3.14".to_string())); + assert_eq!(eval("toString 0.0"), Value::String("0".to_string())); + assert_eq!(eval("toString (-2.5)"), Value::String("-2.5".to_string())); +} + +#[test] +fn bool_to_string() { + assert_eq!(eval("toString true"), Value::String("1".to_string())); + assert_eq!(eval("toString false"), Value::String("".to_string())); +} + +#[test] +fn null_to_string() { + assert_eq!(eval("toString null"), Value::String("".to_string())); +} + +#[test] +fn simple_list_to_string() { + assert_eq!(eval("toString [1 2 3]"), Value::String("1 2 3".to_string())); + assert_eq!( + eval(r#"toString ["a" "b" "c"]"#), + Value::String("a b c".to_string()) + ); +} + +#[test] +fn nested_list_flattens() { + assert_eq!( + eval("toString [[1 2] [3 4]]"), + Value::String("1 2 3 4".to_string()) + ); + assert_eq!( + eval("toString [1 [2 3] 4]"), + Value::String("1 2 3 4".to_string()) + ); +} + +#[test] +fn empty_list_in_list_no_extra_space() { + assert_eq!(eval("toString [1 [] 2]"), Value::String("1 2".to_string())); + assert_eq!(eval("toString [[] 1 2]"), Value::String("1 2".to_string())); + assert_eq!(eval("toString [1 2 []]"), Value::String("1 2 ".to_string())); +} + +#[test] +fn list_with_multiple_empty_lists() { + assert_eq!( + eval("toString [1 [] [] 2]"), + Value::String("1 2".to_string()) + ); + assert_eq!( + eval("toString [[] [] 1]"), + Value::String("1".to_string()) + ); +} + +#[test] +fn list_with_bool_and_null() { + assert_eq!( + eval("toString [true false null]"), + Value::String("1 ".to_string()) + ); + assert_eq!( + eval("toString [1 true 2 false 3]"), + Value::String("1 1 2 3".to_string()) + ); +} + +#[test] +fn mixed_type_list() { + assert_eq!( + eval(r#"toString [1 "hello" 2.5 true]"#), + Value::String("1 hello 2.5 1".to_string()) + ); +} + +#[test] +fn attrs_with_out_path() { + assert_eq!( + eval(r#"toString { outPath = "/nix/store/foo"; }"#), + Value::String("/nix/store/foo".to_string()) + ); +} + +#[test] +fn attrs_with_to_string_method() { + assert_eq!( + eval(r#"toString { __toString = self: "custom"; }"#), + Value::String("custom".to_string()) + ); +} + +#[test] +fn attrs_to_string_self_reference() { + assert_eq!( + eval(r#"let obj = { x = 42; __toString = self: "x is ${toString self.x}"; }; in toString obj"#), + Value::String("x is 42".to_string()) + ); +} + +#[test] +fn attrs_to_string_priority() { + assert_eq!( + eval( + r#"toString { __toString = self: "custom"; outPath = "/nix/store/foo"; }"# + ), + Value::String("custom".to_string()) + ); +} + +#[test] +fn derivation_like_object() { + assert_eq!( + eval( + r#"let drv = { type = "derivation"; outPath = "/nix/store/hash-pkg"; }; in toString drv"# + ), + Value::String("/nix/store/hash-pkg".to_string()) + ); +} + +#[test] +fn string_interpolation_with_int() { + assert_eq!( + eval(r#""value: ${toString 42}""#), + Value::String("value: 42".to_string()) + ); +} + +#[test] +fn string_interpolation_with_list() { + assert_eq!( + eval(r#""items: ${toString [1 2 3]}""#), + Value::String("items: 1 2 3".to_string()) + ); +} + +#[test] +fn nested_to_string_calls() { + assert_eq!( + eval(r#"toString (toString 42)"#), + Value::String("42".to_string()) + ); +} + +#[test] +fn to_string_in_let_binding() { + assert_eq!( + eval(r#"let x = toString 42; y = toString 10; in "${x}-${y}""#), + Value::String("42-10".to_string()) + ); +} + +#[test] +fn empty_string() { + assert_eq!(eval(r#"toString """#), Value::String("".to_string())); +} + +#[test] +fn empty_list() { + assert_eq!(eval("toString []"), Value::String("".to_string())); +} + +#[test] +fn to_string_preserves_spaces_in_strings() { + assert_eq!( + eval(r#"toString "hello world""#), + Value::String("hello world".to_string()) + ); +} + +#[test] +fn list_of_empty_strings() { + assert_eq!( + eval(r#"toString ["" "" ""]"#), + Value::String(" ".to_string()) + ); +} + +#[test] +fn deeply_nested_lists() { + assert_eq!( + eval("toString [[[1] [2]] [[3] [4]]]"), + Value::String("1 2 3 4".to_string()) + ); +} + +#[test] +fn list_with_nested_empty_lists() { + assert_eq!( + eval("toString [1 [[]] 2]"), + Value::String("1 2".to_string()) + ); +} + +#[test] +fn attrs_without_out_path_or_to_string_fails() { + let result = utils::eval_result(r#"toString { foo = "bar"; }"#); + assert!(result.is_err()); +} + +#[test] +fn function_to_string_fails() { + let result = utils::eval_result("toString (x: x)"); + assert!(result.is_err()); +} + +#[test] +fn to_string_method_must_return_string() { + let result = utils::eval_result(r#"toString { __toString = self: 42; }"#); + assert!(result.is_err()); +} + +#[test] +fn out_path_can_be_nested() { + assert_eq!( + eval(r#"toString { outPath = { outPath = "/final/path"; }; }"#), + Value::String("/final/path".to_string()) + ); +} + +#[test] +fn list_spacing_matches_nix_behavior() { + assert_eq!( + eval(r#"toString ["a" "b"]"#), + Value::String("a b".to_string()) + ); + + assert_eq!( + eval(r#"toString ["a" ["b" "c"] "d"]"#), + Value::String("a b c d".to_string()) + ); +} diff --git a/nix-js/tests/utils.rs b/nix-js/tests/utils.rs new file mode 100644 index 0000000..5eafd8f --- /dev/null +++ b/nix-js/tests/utils.rs @@ -0,0 +1,12 @@ +#![allow(dead_code)] + +use nix_js::context::Context; +use nix_js::value::Value; + +pub fn eval(expr: &str) -> Value { + Context::new().unwrap().eval_code(expr).unwrap() +} + +pub fn eval_result(expr: &str) -> Result { + Context::new().unwrap().eval_code(expr) +}