Compare commits
5 Commits
e29e432328
...
e33770c1bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
e33770c1bf
|
|||
|
fbf35ba4cd
|
|||
|
1adb7a24a9
|
|||
|
36ccc735f9
|
|||
|
fdda1ae682
|
7
.lazy.lua
Normal file
7
.lazy.lua
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
vim.lsp.config("biome", {
|
||||||
|
root_dir = function (bufnr, on_dir)
|
||||||
|
on_dir(vim.fn.getcwd())
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
return {}
|
||||||
317
Cargo.lock
generated
317
Cargo.lock
generated
@@ -23,6 +23,18 @@ version = "0.2.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
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]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.98"
|
version = "1.0.98"
|
||||||
@@ -165,6 +177,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cast"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.34"
|
version = "1.2.34"
|
||||||
@@ -195,6 +213,33 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
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]]
|
[[package]]
|
||||||
name = "clang-sys"
|
name = "clang-sys"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@@ -206,6 +251,31 @@ dependencies = [
|
|||||||
"libloading",
|
"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]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
version = "5.4.1"
|
version = "5.4.1"
|
||||||
@@ -263,6 +333,73 @@ dependencies = [
|
|||||||
"cfg-if",
|
"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]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@@ -507,6 +644,12 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -660,6 +803,17 @@ dependencies = [
|
|||||||
"crc32fast",
|
"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]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -692,6 +846,12 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "home"
|
name = "home"
|
||||||
version = "0.5.11"
|
version = "0.5.11"
|
||||||
@@ -865,6 +1025,26 @@ dependencies = [
|
|||||||
"hashbrown 0.16.1",
|
"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]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -1046,6 +1226,7 @@ name = "nix-js"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"criterion",
|
||||||
"deno_core",
|
"deno_core",
|
||||||
"deno_error",
|
"deno_error",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
@@ -1053,7 +1234,7 @@ dependencies = [
|
|||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"mimalloc",
|
"mimalloc",
|
||||||
"nix-js-macros",
|
"nix-js-macros",
|
||||||
"pin-project",
|
"petgraph",
|
||||||
"regex",
|
"regex",
|
||||||
"rnix",
|
"rnix",
|
||||||
"rustyline",
|
"rustyline",
|
||||||
@@ -1117,6 +1298,12 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oorandom"
|
||||||
|
version = "11.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "outref"
|
name = "outref"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -1158,6 +1345,18 @@ version = "2.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
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]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.10"
|
version = "1.1.10"
|
||||||
@@ -1190,6 +1389,34 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
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]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -1266,6 +1493,26 @@ version = "0.6.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -1411,6 +1658,15 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -1749,6 +2005,16 @@ dependencies = [
|
|||||||
"zerovec",
|
"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]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.48.0"
|
version = "1.48.0"
|
||||||
@@ -1864,6 +2130,16 @@ version = "0.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
|
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]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
@@ -1934,6 +2210,16 @@ dependencies = [
|
|||||||
"thiserror",
|
"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]]
|
[[package]]
|
||||||
name = "which"
|
name = "which"
|
||||||
version = "6.0.3"
|
version = "6.0.3"
|
||||||
@@ -1962,6 +2248,15 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
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]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -2189,6 +2484,26 @@ dependencies = [
|
|||||||
"synstructure",
|
"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]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.3.6/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.3.9/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
@@ -17,6 +17,13 @@
|
|||||||
"lineWidth": 110,
|
"lineWidth": 110,
|
||||||
"lineEnding": "lf"
|
"lineEnding": "lf"
|
||||||
},
|
},
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"style": {
|
||||||
|
"useNamingConvention": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"javascript": {
|
"javascript": {
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"arrowParentheses": "always",
|
"arrowParentheses": "always",
|
||||||
|
|||||||
@@ -22,13 +22,16 @@
|
|||||||
"rustfmt"
|
"rustfmt"
|
||||||
"rust-analyzer"
|
"rust-analyzer"
|
||||||
])
|
])
|
||||||
|
cargo-outdated
|
||||||
lldb
|
lldb
|
||||||
valgrind
|
valgrind
|
||||||
claude-code
|
hyperfine
|
||||||
|
|
||||||
nodejs
|
nodejs
|
||||||
nodePackages.npm
|
nodePackages.npm
|
||||||
biome
|
biome
|
||||||
|
|
||||||
|
claude-code
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,16 @@ mimalloc = "0.1"
|
|||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
rustyline = "14.0"
|
rustyline = "14.0"
|
||||||
|
|
||||||
regex = "1.11"
|
|
||||||
hashbrown = "0.16"
|
|
||||||
derive_more = { version = "2", features = ["full"] }
|
derive_more = { version = "2", features = ["full"] }
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
|
|
||||||
|
hashbrown = "0.16"
|
||||||
|
petgraph = "0.8"
|
||||||
string-interner = "0.19"
|
string-interner = "0.19"
|
||||||
|
|
||||||
itertools = "0.14"
|
itertools = "0.14"
|
||||||
pin-project = "1"
|
|
||||||
|
regex = "1.11"
|
||||||
|
|
||||||
deno_core = "0.376"
|
deno_core = "0.376"
|
||||||
deno_error = "0.7"
|
deno_error = "0.7"
|
||||||
@@ -28,3 +31,20 @@ nix-js-macros = { path = "../nix-js-macros" }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.24"
|
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
|
||||||
|
|||||||
111
nix-js/benches/basic_ops.rs
Normal file
111
nix-js/benches/basic_ops.rs
Normal file
@@ -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);
|
||||||
143
nix-js/benches/builtins.rs
Normal file
143
nix-js/benches/builtins.rs
Normal file
@@ -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);
|
||||||
139
nix-js/benches/compile_time.rs
Normal file
139
nix-js/benches/compile_time.rs
Normal file
@@ -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);
|
||||||
172
nix-js/benches/scc_optimization.rs
Normal file
172
nix-js/benches/scc_optimization.rs
Normal file
@@ -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);
|
||||||
16
nix-js/benches/utils.rs
Normal file
16
nix-js/benches/utils.rs
Normal file
@@ -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<Value, nix_js::error::Error> {
|
||||||
|
Context::new().unwrap().eval_code(expr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compile(expr: &str) -> String {
|
||||||
|
Context::new().unwrap().compile_code(expr).unwrap()
|
||||||
|
}
|
||||||
@@ -3,33 +3,33 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NixBool, NixInt, NixNumber, NixValue } from "../types";
|
import type { NixBool, NixInt, NixNumber, NixValue } from "../types";
|
||||||
import { force_numeric, coerce_numeric, force_int } from "../type-assert";
|
import { forceNumeric, coerceNumeric, forceInt } from "../type-assert";
|
||||||
|
|
||||||
export const add =
|
export const add =
|
||||||
(a: NixValue) =>
|
(a: NixValue) =>
|
||||||
(b: NixValue): bigint | number => {
|
(b: NixValue): bigint | number => {
|
||||||
const [av, bv] = coerce_numeric(force_numeric(a), force_numeric(b));
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (av as any) + (bv as any);
|
return (av as any) + (bv as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sub =
|
export const sub =
|
||||||
(a: NixValue) =>
|
(a: NixValue) =>
|
||||||
(b: NixValue): bigint | number => {
|
(b: NixValue): bigint | number => {
|
||||||
const [av, bv] = coerce_numeric(force_numeric(a), force_numeric(b));
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (av as any) - (bv as any);
|
return (av as any) - (bv as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mul =
|
export const mul =
|
||||||
(a: NixValue) =>
|
(a: NixValue) =>
|
||||||
(b: NixValue): bigint | number => {
|
(b: NixValue): bigint | number => {
|
||||||
const [av, bv] = coerce_numeric(force_numeric(a), force_numeric(b));
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (av as any) * (bv as any);
|
return (av as any) * (bv as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const div =
|
export const div =
|
||||||
(a: NixValue) =>
|
(a: NixValue) =>
|
||||||
(b: NixValue): NixNumber => {
|
(b: NixValue): NixNumber => {
|
||||||
const [av, bv] = coerce_numeric(force_numeric(a), force_numeric(b));
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
|
|
||||||
if (bv === 0 || bv === 0n) {
|
if (bv === 0 || bv === 0n) {
|
||||||
throw new RangeError("Division by zero");
|
throw new RangeError("Division by zero");
|
||||||
@@ -42,28 +42,28 @@ export const div =
|
|||||||
export const bitAnd =
|
export const bitAnd =
|
||||||
(a: NixValue) =>
|
(a: NixValue) =>
|
||||||
(b: NixValue): NixInt => {
|
(b: NixValue): NixInt => {
|
||||||
const av = force_int(a);
|
const av = forceInt(a);
|
||||||
const bv = force_int(b);
|
const bv = forceInt(b);
|
||||||
return av & bv;
|
return av & bv;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bitOr =
|
export const bitOr =
|
||||||
(a: NixValue) =>
|
(a: NixValue) =>
|
||||||
(b: NixValue): NixInt => {
|
(b: NixValue): NixInt => {
|
||||||
const av = force_int(a);
|
const av = forceInt(a);
|
||||||
const bv = force_int(b);
|
const bv = forceInt(b);
|
||||||
return av | bv;
|
return av | bv;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bitXor =
|
export const bitXor =
|
||||||
(a: NixValue) =>
|
(a: NixValue) =>
|
||||||
(b: NixValue): NixInt => {
|
(b: NixValue): NixInt => {
|
||||||
const av = force_int(a);
|
const av = forceInt(a);
|
||||||
const bv = force_int(b);
|
const bv = forceInt(b);
|
||||||
return av ^ bv;
|
return av ^ bv;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const lessThan =
|
export const lessThan =
|
||||||
(a: NixValue) =>
|
(a: NixValue) =>
|
||||||
(b: NixValue): NixBool =>
|
(b: NixValue): NixBool =>
|
||||||
force_numeric(a) < force_numeric(b);
|
forceNumeric(a) < forceNumeric(b);
|
||||||
|
|||||||
@@ -3,30 +3,30 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NixValue, NixAttrs, NixList } from "../types";
|
import type { NixValue, NixAttrs, NixList } from "../types";
|
||||||
import { force_attrs, force_string, force_function, force_list } from "../type-assert";
|
import { forceAttrs, forceString, forceFunction, forceList } from "../type-assert";
|
||||||
|
|
||||||
export const attrNames = (set: NixValue): string[] => Object.keys(force_attrs(set)).sort();
|
export const attrNames = (set: NixValue): string[] => Object.keys(forceAttrs(set)).sort();
|
||||||
|
|
||||||
export const attrValues = (set: NixValue): NixValue[] => Object.values(force_attrs(set));
|
export const attrValues = (set: NixValue): NixValue[] => Object.values(forceAttrs(set));
|
||||||
|
|
||||||
export const getAttr =
|
export const getAttr =
|
||||||
(s: NixValue) =>
|
(s: NixValue) =>
|
||||||
(set: NixValue): NixValue =>
|
(set: NixValue): NixValue =>
|
||||||
force_attrs(set)[force_string(s)];
|
forceAttrs(set)[forceString(s)];
|
||||||
|
|
||||||
export const hasAttr =
|
export const hasAttr =
|
||||||
(s: NixValue) =>
|
(s: NixValue) =>
|
||||||
(set: NixValue): boolean =>
|
(set: NixValue): boolean =>
|
||||||
Object.hasOwn(force_attrs(set), force_string(s));
|
Object.hasOwn(forceAttrs(set), forceString(s));
|
||||||
|
|
||||||
export const mapAttrs =
|
export const mapAttrs =
|
||||||
(f: NixValue) =>
|
(f: NixValue) =>
|
||||||
(attrs: NixValue): NixAttrs => {
|
(attrs: NixValue): NixAttrs => {
|
||||||
const new_attrs: NixAttrs = {};
|
const new_attrs: NixAttrs = {};
|
||||||
const forced_attrs = force_attrs(attrs);
|
const forced_attrs = forceAttrs(attrs);
|
||||||
const forced_f = force_function(f);
|
const forced_f = forceFunction(f);
|
||||||
for (const key in forced_attrs) {
|
for (const key in forced_attrs) {
|
||||||
new_attrs[key] = forced_f(key)(forced_attrs[key]);
|
new_attrs[key] = forceFunction(forced_f(key))(forced_attrs[key]);
|
||||||
}
|
}
|
||||||
return new_attrs;
|
return new_attrs;
|
||||||
};
|
};
|
||||||
@@ -35,8 +35,8 @@ export const removeAttrs =
|
|||||||
(attrs: NixValue) =>
|
(attrs: NixValue) =>
|
||||||
(list: NixValue): NixAttrs => {
|
(list: NixValue): NixAttrs => {
|
||||||
const new_attrs: NixAttrs = {};
|
const new_attrs: NixAttrs = {};
|
||||||
const forced_attrs = force_attrs(attrs);
|
const forced_attrs = forceAttrs(attrs);
|
||||||
const forced_list = force_list(list);
|
const forced_list = forceList(list);
|
||||||
|
|
||||||
for (const key in forced_attrs) {
|
for (const key in forced_attrs) {
|
||||||
if (!(key in forced_list)) {
|
if (!(key in forced_list)) {
|
||||||
@@ -48,10 +48,10 @@ export const removeAttrs =
|
|||||||
|
|
||||||
export const listToAttrs = (e: NixValue): NixAttrs => {
|
export const listToAttrs = (e: NixValue): NixAttrs => {
|
||||||
const attrs: NixAttrs = {};
|
const attrs: NixAttrs = {};
|
||||||
const forced_e = [...force_list(e)].reverse();
|
const forced_e = [...forceList(e)].reverse();
|
||||||
for (const obj of forced_e) {
|
for (const obj of forced_e) {
|
||||||
const item = force_attrs(obj);
|
const item = forceAttrs(obj);
|
||||||
attrs[force_string(item.name)] = item.value;
|
attrs[forceString(item.name)] = item.value;
|
||||||
}
|
}
|
||||||
return attrs;
|
return attrs;
|
||||||
};
|
};
|
||||||
@@ -59,8 +59,8 @@ export const listToAttrs = (e: NixValue): NixAttrs => {
|
|||||||
export const intersectAttrs =
|
export const intersectAttrs =
|
||||||
(e1: NixValue) =>
|
(e1: NixValue) =>
|
||||||
(e2: NixValue): NixAttrs => {
|
(e2: NixValue): NixAttrs => {
|
||||||
const f1 = force_attrs(e1);
|
const f1 = forceAttrs(e1);
|
||||||
const f2 = force_attrs(e2);
|
const f2 = forceAttrs(e2);
|
||||||
const attrs: NixAttrs = {};
|
const attrs: NixAttrs = {};
|
||||||
for (const key of Object.keys(f2)) {
|
for (const key of Object.keys(f2)) {
|
||||||
if (Object.hasOwn(f1, key)) {
|
if (Object.hasOwn(f1, key)) {
|
||||||
@@ -73,9 +73,9 @@ export const intersectAttrs =
|
|||||||
export const catAttrs =
|
export const catAttrs =
|
||||||
(attr: NixValue) =>
|
(attr: NixValue) =>
|
||||||
(list: NixValue): NixList => {
|
(list: NixValue): NixList => {
|
||||||
const key = force_string(attr);
|
const key = forceString(attr);
|
||||||
return force_list(list)
|
return forceList(list)
|
||||||
.map((set) => force_attrs(set)[key])
|
.map((set) => forceAttrs(set)[key])
|
||||||
.filter((val) => val !== undefined);
|
.filter((val) => val !== undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,10 +83,10 @@ export const groupBy =
|
|||||||
(f: NixValue) =>
|
(f: NixValue) =>
|
||||||
(list: NixValue): NixAttrs => {
|
(list: NixValue): NixAttrs => {
|
||||||
const attrs: NixAttrs = {};
|
const attrs: NixAttrs = {};
|
||||||
const forced_f = force_function(f);
|
const forced_f = forceFunction(f);
|
||||||
const forced_list = force_list(list);
|
const forced_list = forceList(list);
|
||||||
for (const elem of forced_list) {
|
for (const elem of forced_list) {
|
||||||
const key = force_string(forced_f(elem));
|
const key = forceString(forced_f(elem));
|
||||||
if (!attrs[key]) attrs[key] = [];
|
if (!attrs[key]) attrs[key] = [];
|
||||||
(attrs[key] as NixList).push(elem);
|
(attrs[key] as NixList).push(elem);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Conversion and serialization builtin functions (unimplemented)
|
* Conversion and serialization builtin functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NixValue } from "../types";
|
import type { NixValue } from "../types";
|
||||||
|
import { force } from "../thunk";
|
||||||
|
|
||||||
export const fromJSON = (e: NixValue): never => {
|
export const fromJSON = (e: NixValue): never => {
|
||||||
throw new Error("Not implemented: fromJSON");
|
throw new Error("Not implemented: fromJSON");
|
||||||
@@ -20,6 +21,176 @@ export const toXML = (e: NixValue): never => {
|
|||||||
throw new Error("Not implemented: toXML");
|
throw new Error("Not implemented: toXML");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toString = (name: NixValue, s: NixValue): never => {
|
/**
|
||||||
throw new Error("Not implemented: toString");
|
* String coercion modes control which types can be coerced to strings
|
||||||
|
*
|
||||||
|
* - Base: Only strings are allowed (no coercion)
|
||||||
|
* - Interpolation: Used in string interpolation "${expr}" - allows strings and integers
|
||||||
|
* - ToString: Used in builtins.toString - allows all types (bools, floats, null, lists, etc.)
|
||||||
|
*/
|
||||||
|
export enum StringCoercionMode {
|
||||||
|
Base = 0,
|
||||||
|
Interpolation = 1,
|
||||||
|
ToString = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get human-readable type names for error messages
|
||||||
|
*/
|
||||||
|
const typeName = (value: NixValue): string => {
|
||||||
|
const val = force(value);
|
||||||
|
|
||||||
|
if (typeof val === "bigint") return "int";
|
||||||
|
if (typeof val === "number") return "float";
|
||||||
|
if (typeof val === "boolean") return "boolean";
|
||||||
|
if (typeof val === "string") return "string";
|
||||||
|
if (val === null) return "null";
|
||||||
|
if (Array.isArray(val)) return "list";
|
||||||
|
if (typeof val === "function") return "lambda";
|
||||||
|
if (typeof val === "object") return "attribute set";
|
||||||
|
|
||||||
|
return `unknown type`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce a Nix value to a string according to the specified mode.
|
||||||
|
* This implements the same behavior as Lix's EvalState::coerceToString.
|
||||||
|
*
|
||||||
|
* @param value - The value to coerce
|
||||||
|
* @param mode - The coercion mode (controls which types are allowed)
|
||||||
|
* @param copyToStore - If true, paths should be copied to the Nix store (not implemented yet)
|
||||||
|
* @returns The string representation of the value
|
||||||
|
* @throws TypeError if the value cannot be coerced in the given mode
|
||||||
|
*
|
||||||
|
* Coercion rules by type:
|
||||||
|
* - String: Always returns as-is
|
||||||
|
* - Path: Returns the path string (copyToStore not implemented yet)
|
||||||
|
* - Integer: Only in Interpolation or ToString mode
|
||||||
|
* - Float: Only in ToString mode
|
||||||
|
* - Boolean: Only in ToString mode (true → "1", false → "")
|
||||||
|
* - Null: Only in ToString mode (→ "")
|
||||||
|
* - List: Only in ToString mode (recursively coerce elements, join with spaces)
|
||||||
|
* - Attrs: Check for __toString method or outPath attribute
|
||||||
|
* - Function: Never coercible (throws error)
|
||||||
|
*/
|
||||||
|
export const coerceToString = (
|
||||||
|
value: NixValue,
|
||||||
|
mode: StringCoercionMode = StringCoercionMode.ToString,
|
||||||
|
copyToStore: boolean = false,
|
||||||
|
): string => {
|
||||||
|
const v = force(value);
|
||||||
|
|
||||||
|
// Strings are always returned as-is, regardless of mode
|
||||||
|
if (typeof v === "string") {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute sets can define custom string conversion via __toString method
|
||||||
|
// or may have an outPath attribute (for derivations and paths)
|
||||||
|
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
||||||
|
// First, try the __toString method if present
|
||||||
|
// This allows custom types to define their own string representation
|
||||||
|
if ("__toString" in v) {
|
||||||
|
// Force the method in case it's a thunk
|
||||||
|
const toStringMethod = force(v["__toString"]);
|
||||||
|
if (typeof toStringMethod === "function") {
|
||||||
|
// Call the method with self as argument
|
||||||
|
const result = force(toStringMethod(v));
|
||||||
|
if (typeof result !== "string") {
|
||||||
|
throw new TypeError(`__toString returned ${typeName(result)} instead of string`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no __toString, try outPath (used for derivations and store paths)
|
||||||
|
// This allows derivation objects like { outPath = "/nix/store/..."; } to be coerced
|
||||||
|
if ("outPath" in v) {
|
||||||
|
// Recursively coerce the outPath value (it might itself be an attrs with __toString)
|
||||||
|
return coerceToString(v["outPath"], mode, copyToStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute sets without __toString or outPath cannot be coerced
|
||||||
|
throw new TypeError(`cannot coerce ${typeName(v)} to a string`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integer coercion is allowed in Interpolation and ToString modes
|
||||||
|
// This enables string interpolation like "value: ${42}"
|
||||||
|
if (mode >= StringCoercionMode.Interpolation) {
|
||||||
|
if (typeof v === "bigint") {
|
||||||
|
return v.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following types are only coercible in ToString mode (builtins.toString)
|
||||||
|
if (mode >= StringCoercionMode.ToString) {
|
||||||
|
// Booleans: true → "1", false → ""
|
||||||
|
// This is for shell scripting convenience (same as null)
|
||||||
|
if (typeof v === "boolean") {
|
||||||
|
return v ? "1" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Floats are converted using JavaScript's default toString
|
||||||
|
if (typeof v === "number") {
|
||||||
|
return v.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null becomes empty string (for shell scripting convenience)
|
||||||
|
if (v === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lists are recursively converted and joined with spaces
|
||||||
|
// We cannot use Array.join() directly because of special spacing rules:
|
||||||
|
// - Elements are recursively coerced to strings
|
||||||
|
// - Spaces are added between elements, BUT:
|
||||||
|
// * No space is added after an element if it's an empty list
|
||||||
|
// * The last element never gets a trailing space
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// - [ 1 2 3 ] → "1 2 3"
|
||||||
|
// - [ 1 [ ] 2 ] → "1 2" (empty list doesn't add space)
|
||||||
|
// - [ 1 [ [ ] ] 2 ] → "1 2" (nested empty list is not itself empty, so adds space)
|
||||||
|
// - [ [ 1 2 ] [ 3 4 ] ] → "1 2 3 4" (nested lists flatten)
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < v.length; i++) {
|
||||||
|
const item = v[i];
|
||||||
|
// Recursively convert element to string
|
||||||
|
const str = coerceToString(item, mode, copyToStore);
|
||||||
|
result += str;
|
||||||
|
|
||||||
|
// Add space after this element if:
|
||||||
|
// 1. It's not the last element, AND
|
||||||
|
// 2. The element is not an empty list
|
||||||
|
//
|
||||||
|
// Note: We check if the ELEMENT is an empty list, not if its
|
||||||
|
// string representation is empty.
|
||||||
|
// For example, [[]] is not an empty list (length 1), so it gets
|
||||||
|
// a trailing space even though its toString is "".
|
||||||
|
if (i < v.length - 1) {
|
||||||
|
const forcedItem = force(item);
|
||||||
|
if (!Array.isArray(forcedItem) || forcedItem.length !== 0) {
|
||||||
|
result += " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError(`cannot coerce ${typeName(v)} to a string`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.toString - Convert a value to a string
|
||||||
|
*
|
||||||
|
* This is the public builtin function exposed to Nix code.
|
||||||
|
* It uses ToString mode, which allows coercing all types except functions.
|
||||||
|
*
|
||||||
|
* @param value - The value to convert to a string
|
||||||
|
* @returns The string representation
|
||||||
|
*/
|
||||||
|
export const toStringFunc = (value: NixValue): string => {
|
||||||
|
return coerceToString(value, StringCoercionMode.ToString, false);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { CatchableError, type NixValue } from "../types";
|
import { CatchableError, type NixValue } from "../types";
|
||||||
import { force } from "../thunk";
|
import { force } from "../thunk";
|
||||||
import { force_string } from "../type-assert";
|
import { forceString } from "../type-assert";
|
||||||
|
|
||||||
export const seq =
|
export const seq =
|
||||||
(e1: NixValue) =>
|
(e1: NixValue) =>
|
||||||
@@ -24,7 +24,7 @@ export const abort = (s: NixValue): never => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const throwFunc = (s: NixValue): never => {
|
export const throwFunc = (s: NixValue): never => {
|
||||||
throw new CatchableError(force_string(s));
|
throw new CatchableError(forceString(s));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const trace = (e1: NixValue, e2: NixValue): NixValue => {
|
export const trace = (e1: NixValue, e2: NixValue): NixValue => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Combines all builtin function categories into the global `builtins` object
|
* 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)
|
* Symbol used to mark functions as primops (primitive operations)
|
||||||
@@ -211,7 +211,7 @@ export const builtins: any = {
|
|||||||
fromTOML: mkPrimop(conversion.fromTOML, "fromTOML", 1),
|
fromTOML: mkPrimop(conversion.fromTOML, "fromTOML", 1),
|
||||||
toJSON: mkPrimop(conversion.toJSON, "toJSON", 1),
|
toJSON: mkPrimop(conversion.toJSON, "toJSON", 1),
|
||||||
toXML: mkPrimop(conversion.toXML, "toXML", 1),
|
toXML: mkPrimop(conversion.toXML, "toXML", 1),
|
||||||
toString: mkPrimop(conversion.toString, "toString", 1),
|
toString: mkPrimop(conversion.toStringFunc, "toString", 1),
|
||||||
|
|
||||||
addErrorContext: mkPrimop(misc.addErrorContext, "addErrorContext", 1),
|
addErrorContext: mkPrimop(misc.addErrorContext, "addErrorContext", 1),
|
||||||
appendContext: mkPrimop(misc.appendContext, "appendContext", 1),
|
appendContext: mkPrimop(misc.appendContext, "appendContext", 1),
|
||||||
@@ -247,11 +247,11 @@ export const builtins: any = {
|
|||||||
tryEval: mkPrimop(misc.tryEval, "tryEval", 1),
|
tryEval: mkPrimop(misc.tryEval, "tryEval", 1),
|
||||||
zipAttrsWith: mkPrimop(misc.zipAttrsWith, "zipAttrsWith", 2),
|
zipAttrsWith: mkPrimop(misc.zipAttrsWith, "zipAttrsWith", 2),
|
||||||
|
|
||||||
builtins: create_thunk(() => builtins),
|
builtins: createThunk(() => builtins),
|
||||||
currentSystem: create_thunk(() => {
|
currentSystem: createThunk(() => {
|
||||||
throw new Error("Not implemented: currentSystem");
|
throw new Error("Not implemented: currentSystem");
|
||||||
}),
|
}),
|
||||||
currentTime: create_thunk(() => Date.now()),
|
currentTime: createThunk(() => Date.now()),
|
||||||
|
|
||||||
false: false,
|
false: false,
|
||||||
true: true,
|
true: true,
|
||||||
|
|||||||
@@ -3,23 +3,20 @@
|
|||||||
* Implemented via Rust ops exposed through deno_core
|
* Implemented via Rust ops exposed through deno_core
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { forceString } from "../type-assert";
|
||||||
import type { NixValue } from "../types";
|
import type { NixValue } from "../types";
|
||||||
import { force_string } from "../type-assert";
|
|
||||||
|
|
||||||
// Declare Deno.core.ops global (provided by deno_core runtime)
|
// Declare Deno.core.ops global (provided by deno_core runtime)
|
||||||
|
|
||||||
export const importFunc = (path: NixValue): NixValue => {
|
export const importFunc = (path: NixValue): NixValue => {
|
||||||
// For MVP: only support string paths
|
// For MVP: only support string paths
|
||||||
// TODO: After implementing path type, also accept path values
|
// TODO: After implementing path type, also accept path values
|
||||||
const pathStr = force_string(path);
|
const pathStr = forceString(path);
|
||||||
|
|
||||||
// Call Rust op - returns JS code string
|
// Call Rust op - returns JS code string
|
||||||
const code = Deno.core.ops.op_import(pathStr);
|
const code = Deno.core.ops.op_import(pathStr);
|
||||||
|
|
||||||
// Eval in current context - returns V8 value directly!
|
return Function(`return (${code})`)();
|
||||||
// (0, eval) = indirect eval = global scope
|
|
||||||
// Wrap in parentheses to ensure object literals are parsed correctly
|
|
||||||
return (0, eval)(`(${code})`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const scopedImport =
|
export const scopedImport =
|
||||||
@@ -61,7 +58,7 @@ export const readDir = (path: NixValue): never => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const readFile = (path: NixValue): string => {
|
export const readFile = (path: NixValue): string => {
|
||||||
const pathStr = force_string(path);
|
const pathStr = forceString(path);
|
||||||
return Deno.core.ops.op_read_file(pathStr);
|
return Deno.core.ops.op_read_file(pathStr);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,7 +67,7 @@ export const readFileType = (path: NixValue): never => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const pathExists = (path: NixValue): boolean => {
|
export const pathExists = (path: NixValue): boolean => {
|
||||||
const pathStr = force_string(path);
|
const pathStr = forceString(path);
|
||||||
return Deno.core.ops.op_path_exists(pathStr);
|
return Deno.core.ops.op_path_exists(pathStr);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,38 +5,38 @@
|
|||||||
|
|
||||||
import type { NixValue, NixList, NixAttrs } from "../types";
|
import type { NixValue, NixList, NixAttrs } from "../types";
|
||||||
import { force } from "../thunk";
|
import { force } from "../thunk";
|
||||||
import { force_list, force_function, force_numeric, force_int } from "../type-assert";
|
import { forceList, forceFunction, forceInt } from "../type-assert";
|
||||||
|
|
||||||
export const map =
|
export const map =
|
||||||
(f: NixValue) =>
|
(f: NixValue) =>
|
||||||
(list: NixValue): NixList =>
|
(list: NixValue): NixList =>
|
||||||
force_list(list).map(force_function(f));
|
forceList(list).map(forceFunction(f));
|
||||||
|
|
||||||
export const filter =
|
export const filter =
|
||||||
(f: NixValue) =>
|
(f: NixValue) =>
|
||||||
(list: NixValue): NixList =>
|
(list: NixValue): NixList =>
|
||||||
force_list(list).filter(force_function(f));
|
forceList(list).filter(forceFunction(f));
|
||||||
|
|
||||||
export const length = (e: NixValue): bigint => {
|
export const length = (e: NixValue): bigint => {
|
||||||
const forced = force(e);
|
const forced = force(e);
|
||||||
if (typeof forced === "string") return BigInt(forced.length);
|
if (typeof forced === "string") return BigInt(forced.length);
|
||||||
return BigInt(force_list(forced).length);
|
return BigInt(forceList(forced).length);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const head = (list: NixValue): NixValue => force_list(list)[0];
|
export const head = (list: NixValue): NixValue => forceList(list)[0];
|
||||||
|
|
||||||
export const tail = (list: NixValue): NixList => force_list(list).slice(1);
|
export const tail = (list: NixValue): NixList => forceList(list).slice(1);
|
||||||
|
|
||||||
export const elem =
|
export const elem =
|
||||||
(x: NixValue) =>
|
(x: NixValue) =>
|
||||||
(xs: NixValue): boolean =>
|
(xs: NixValue): boolean =>
|
||||||
force_list(xs).includes(force(x));
|
forceList(xs).includes(force(x));
|
||||||
|
|
||||||
export const elemAt =
|
export const elemAt =
|
||||||
(xs: NixValue) =>
|
(xs: NixValue) =>
|
||||||
(n: NixValue): NixValue => {
|
(n: NixValue): NixValue => {
|
||||||
const list = force_list(xs);
|
const list = forceList(xs);
|
||||||
const idx = Number(force_int(n));
|
const idx = Number(forceInt(n));
|
||||||
|
|
||||||
if (idx < 0 || idx >= list.length) {
|
if (idx < 0 || idx >= list.length) {
|
||||||
throw new RangeError(`Index ${idx} out of bounds for list of length ${list.length}`);
|
throw new RangeError(`Index ${idx} out of bounds for list of length ${list.length}`);
|
||||||
@@ -46,16 +46,16 @@ export const elemAt =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const concatLists = (lists: NixValue): NixList => {
|
export const concatLists = (lists: NixValue): NixList => {
|
||||||
return force_list(lists).reduce((acc: NixList, cur: NixValue) => {
|
return forceList(lists).reduce((acc: NixList, cur: NixValue) => {
|
||||||
return acc.concat(force_list(cur));
|
return acc.concat(forceList(cur));
|
||||||
}, []);
|
}, []);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const concatMap =
|
export const concatMap =
|
||||||
(f: NixValue) =>
|
(f: NixValue) =>
|
||||||
(lists: NixValue): NixList => {
|
(lists: NixValue): NixList => {
|
||||||
const fn = force_function(f);
|
const fn = forceFunction(f);
|
||||||
return force_list(lists).reduce((acc: NixList, cur: NixValue) => {
|
return forceList(lists).reduce((acc: NixList, cur: NixValue) => {
|
||||||
return acc.concat(force(fn(cur)) as NixList);
|
return acc.concat(force(fn(cur)) as NixList);
|
||||||
}, []);
|
}, []);
|
||||||
};
|
};
|
||||||
@@ -64,20 +64,20 @@ export const foldlPrime =
|
|||||||
(op_fn: NixValue) =>
|
(op_fn: NixValue) =>
|
||||||
(nul: NixValue) =>
|
(nul: NixValue) =>
|
||||||
(list: NixValue): NixValue => {
|
(list: NixValue): NixValue => {
|
||||||
const forced_op = force_function(op_fn);
|
const forced_op = forceFunction(op_fn);
|
||||||
return force_list(list).reduce((acc: NixValue, cur: NixValue) => {
|
return forceList(list).reduce((acc: NixValue, cur: NixValue) => {
|
||||||
return forced_op(acc)(cur);
|
return forceFunction(forced_op(acc))(cur);
|
||||||
}, nul);
|
}, nul);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sort =
|
export const sort =
|
||||||
(cmp: NixValue) =>
|
(cmp: NixValue) =>
|
||||||
(list: NixValue): NixList => {
|
(list: NixValue): NixList => {
|
||||||
const forced_list = [...force_list(list)];
|
const forced_list = [...forceList(list)];
|
||||||
const forced_cmp = force_function(cmp);
|
const forced_cmp = forceFunction(cmp);
|
||||||
return forced_list.sort((a, b) => {
|
return forced_list.sort((a, b) => {
|
||||||
if (force(forced_cmp(a)(b))) return -1;
|
if (force(forceFunction(forced_cmp(a))(b))) return -1;
|
||||||
if (force(forced_cmp(b)(a))) return 1;
|
if (force(forceFunction(forced_cmp(b))(a))) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -85,8 +85,8 @@ export const sort =
|
|||||||
export const partition =
|
export const partition =
|
||||||
(pred: NixValue) =>
|
(pred: NixValue) =>
|
||||||
(list: NixValue): NixAttrs => {
|
(list: NixValue): NixAttrs => {
|
||||||
const forced_list = force_list(list);
|
const forced_list = forceList(list);
|
||||||
const forced_pred = force_function(pred);
|
const forced_pred = forceFunction(pred);
|
||||||
const attrs: NixAttrs = {
|
const attrs: NixAttrs = {
|
||||||
right: [],
|
right: [],
|
||||||
wrong: [],
|
wrong: [],
|
||||||
@@ -104,8 +104,8 @@ export const partition =
|
|||||||
export const genList =
|
export const genList =
|
||||||
(f: NixValue) =>
|
(f: NixValue) =>
|
||||||
(len: NixValue): NixList => {
|
(len: NixValue): NixList => {
|
||||||
const func = force_function(f);
|
const func = forceFunction(f);
|
||||||
const length = force_int(len);
|
const length = forceInt(len);
|
||||||
|
|
||||||
if (length < 0) {
|
if (length < 0) {
|
||||||
throw new TypeError(`genList length must be non-negative integer, got ${length}`);
|
throw new TypeError(`genList length must be non-negative integer, got ${length}`);
|
||||||
@@ -117,9 +117,9 @@ export const genList =
|
|||||||
export const all =
|
export const all =
|
||||||
(pred: NixValue) =>
|
(pred: NixValue) =>
|
||||||
(list: NixValue): boolean =>
|
(list: NixValue): boolean =>
|
||||||
force_list(list).every(force_function(pred));
|
forceList(list).every(forceFunction(pred));
|
||||||
|
|
||||||
export const any =
|
export const any =
|
||||||
(pred: NixValue) =>
|
(pred: NixValue) =>
|
||||||
(list: NixValue): boolean =>
|
(list: NixValue): boolean =>
|
||||||
force_list(list).some(force_function(pred));
|
forceList(list).some(forceFunction(pred));
|
||||||
|
|||||||
@@ -3,16 +3,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NixValue } from "../types";
|
import type { NixValue } from "../types";
|
||||||
import { force_numeric } from "../type-assert";
|
import { forceNumeric } from "../type-assert";
|
||||||
|
|
||||||
export const ceil = (x: NixValue): bigint => {
|
export const ceil = (x: NixValue): bigint => {
|
||||||
const val = force_numeric(x);
|
const val = forceNumeric(x);
|
||||||
if (typeof val === "bigint") return val; // Already an integer
|
if (typeof val === "bigint") return val; // Already an integer
|
||||||
return BigInt(Math.ceil(val)); // Convert to integer
|
return BigInt(Math.ceil(val)); // Convert to integer
|
||||||
};
|
};
|
||||||
|
|
||||||
export const floor = (x: NixValue): bigint => {
|
export const floor = (x: NixValue): bigint => {
|
||||||
const val = force_numeric(x);
|
const val = forceNumeric(x);
|
||||||
if (typeof val === "bigint") return val; // Already an integer
|
if (typeof val === "bigint") return val; // Already an integer
|
||||||
return BigInt(Math.floor(val)); // Convert to integer
|
return BigInt(Math.floor(val)); // Convert to integer
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Miscellaneous unimplemented builtin functions
|
* Miscellaneous builtin functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { force } from "../thunk";
|
import { force } from "../thunk";
|
||||||
|
|||||||
@@ -3,27 +3,27 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NixValue } from "../types";
|
import type { NixValue } from "../types";
|
||||||
import { force_string, force_list, force_int } from "../type-assert";
|
import { forceString, forceList, forceInt } from "../type-assert";
|
||||||
|
|
||||||
export const stringLength = (e: NixValue): number => force_string(e).length;
|
export const stringLength = (e: NixValue): number => forceString(e).length;
|
||||||
|
|
||||||
export const substring =
|
export const substring =
|
||||||
(start: NixValue) =>
|
(start: NixValue) =>
|
||||||
(len: NixValue) =>
|
(len: NixValue) =>
|
||||||
(s: NixValue): string => {
|
(s: NixValue): string => {
|
||||||
const str = force_string(s);
|
const str = forceString(s);
|
||||||
const startPos = Number(force_int(start));
|
const startPos = Number(forceInt(start));
|
||||||
const length = Number(force_int(len));
|
const length = Number(forceInt(len));
|
||||||
return str.substring(startPos, startPos + length);
|
return str.substring(startPos, startPos + length);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const concatStringsSep =
|
export const concatStringsSep =
|
||||||
(sep: NixValue) =>
|
(sep: NixValue) =>
|
||||||
(list: NixValue): string =>
|
(list: NixValue): string =>
|
||||||
force_list(list).join(force_string(sep));
|
forceList(list).join(forceString(sep));
|
||||||
|
|
||||||
export const baseNameOf = (x: NixValue): string => {
|
export const baseNameOf = (x: NixValue): string => {
|
||||||
const str = force_string(x);
|
const str = forceString(x);
|
||||||
if (str.length === 0) return "";
|
if (str.length === 0) return "";
|
||||||
|
|
||||||
let last = str.length - 1;
|
let last = str.length - 1;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NixValue, NixAttrs, NixBool } from "./types";
|
import type { NixValue, NixAttrs, NixBool } from "./types";
|
||||||
import { force_attrs, force_string } from "./type-assert";
|
import { forceAttrs, forceString } from "./type-assert";
|
||||||
import { isAttrs } from "./builtins/type-check";
|
import { isAttrs } from "./builtins/type-check";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,8 +13,8 @@ import { isAttrs } from "./builtins/type-check";
|
|||||||
* @param path - Path string (may be relative or absolute)
|
* @param path - Path string (may be relative or absolute)
|
||||||
* @returns Absolute path string
|
* @returns Absolute path string
|
||||||
*/
|
*/
|
||||||
export const resolve_path = (path: NixValue): string => {
|
export const resolvePath = (path: NixValue): string => {
|
||||||
const path_str = force_string(path);
|
const path_str = forceString(path);
|
||||||
return Deno.core.ops.op_resolve_path(path_str);
|
return Deno.core.ops.op_resolve_path(path_str);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,8 +28,8 @@ export const resolve_path = (path: NixValue): string => {
|
|||||||
* @throws Error if obj is null/undefined or key not found
|
* @throws Error if obj is null/undefined or key not found
|
||||||
*/
|
*/
|
||||||
export const select = (obj: NixValue, key: NixValue): NixValue => {
|
export const select = (obj: NixValue, key: NixValue): NixValue => {
|
||||||
const forced_obj = force_attrs(obj);
|
const forced_obj = forceAttrs(obj);
|
||||||
const forced_key = force_string(key);
|
const forced_key = forceString(key);
|
||||||
|
|
||||||
if (!(forced_key in forced_obj)) {
|
if (!(forced_key in forced_obj)) {
|
||||||
throw new Error(`Attribute '${forced_key}' not found`);
|
throw new Error(`Attribute '${forced_key}' not found`);
|
||||||
@@ -47,9 +47,9 @@ export const select = (obj: NixValue, key: NixValue): NixValue => {
|
|||||||
* @param default_val - Value to return if key not found
|
* @param default_val - Value to return if key not found
|
||||||
* @returns obj[key] if exists, otherwise default_val
|
* @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 attrs = forceAttrs(obj);
|
||||||
const forced_key = force_string(key);
|
const forced_key = forceString(key);
|
||||||
|
|
||||||
if (!(forced_key in attrs)) {
|
if (!(forced_key in attrs)) {
|
||||||
return default_val;
|
return default_val;
|
||||||
@@ -58,14 +58,14 @@ export const select_with_default = (obj: NixValue, key: NixValue, default_val: N
|
|||||||
return attrs[forced_key];
|
return attrs[forced_key];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const has_attr = (obj: NixValue, attrpath: NixValue[]): NixBool => {
|
export const hasAttr = (obj: NixValue, attrpath: NixValue[]): NixBool => {
|
||||||
if (!isAttrs(obj)) {
|
if (!isAttrs(obj)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let attrs = obj;
|
let attrs = obj;
|
||||||
|
|
||||||
for (const attr of attrpath.slice(0, -1)) {
|
for (const attr of attrpath.slice(0, -1)) {
|
||||||
const cur = attrs[force_string(attr)];
|
const cur = attrs[forceString(attr)];
|
||||||
if (!isAttrs(cur)) {
|
if (!isAttrs(cur)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -89,12 +89,12 @@ export const has_attr = (obj: NixValue, attrpath: NixValue[]): NixBool => {
|
|||||||
* @returns The forced argument object
|
* @returns The forced argument object
|
||||||
* @throws Error if required param missing or unexpected param present
|
* @throws Error if required param missing or unexpected param present
|
||||||
*/
|
*/
|
||||||
export const validate_params = (
|
export const validateParams = (
|
||||||
arg: NixValue,
|
arg: NixValue,
|
||||||
required: string[] | null,
|
required: string[] | null,
|
||||||
allowed: string[] | null,
|
allowed: string[] | null,
|
||||||
): NixAttrs => {
|
): NixAttrs => {
|
||||||
const forced_arg = force_attrs(arg);
|
const forced_arg = forceAttrs(arg);
|
||||||
|
|
||||||
// Check required parameters
|
// Check required parameters
|
||||||
if (required) {
|
if (required) {
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
* All functionality is exported via the global `Nix` object
|
* All functionality is exported via the global `Nix` object
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { create_thunk, force, is_thunk, IS_THUNK } from "./thunk";
|
import { createThunk, force, isThunk, IS_THUNK } from "./thunk";
|
||||||
import { select, select_with_default, validate_params, resolve_path, has_attr } from "./helpers";
|
import { select, selectWithDefault, validateParams, resolvePath, hasAttr } from "./helpers";
|
||||||
import { op } from "./operators";
|
import { op } from "./operators";
|
||||||
import { builtins, PRIMOP_METADATA } from "./builtins";
|
import { builtins, PRIMOP_METADATA } from "./builtins";
|
||||||
|
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||||
|
|
||||||
export type NixRuntime = typeof Nix;
|
export type NixRuntime = typeof Nix;
|
||||||
|
|
||||||
@@ -15,16 +16,18 @@ export type NixRuntime = typeof Nix;
|
|||||||
* The global Nix runtime object
|
* The global Nix runtime object
|
||||||
*/
|
*/
|
||||||
export const Nix = {
|
export const Nix = {
|
||||||
create_thunk,
|
createThunk,
|
||||||
force,
|
force,
|
||||||
is_thunk,
|
isThunk,
|
||||||
IS_THUNK,
|
IS_THUNK,
|
||||||
|
|
||||||
has_attr,
|
hasAttr,
|
||||||
select,
|
select,
|
||||||
select_with_default,
|
selectWithDefault,
|
||||||
validate_params,
|
validateParams,
|
||||||
resolve_path,
|
resolvePath,
|
||||||
|
coerceToString,
|
||||||
|
StringCoercionMode,
|
||||||
|
|
||||||
op,
|
op,
|
||||||
builtins,
|
builtins,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import type { NixValue, NixList, NixAttrs } from "./types";
|
import type { NixValue, NixList, NixAttrs } from "./types";
|
||||||
import { force } from "./thunk";
|
import { force } from "./thunk";
|
||||||
import { force_numeric, force_list, force_attrs, coerce_numeric } from "./type-assert";
|
import { forceNumeric, forceList, forceAttrs, coerceNumeric } from "./type-assert";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Operator object exported as Nix.op
|
* Operator object exported as Nix.op
|
||||||
@@ -15,22 +15,22 @@ export const op = {
|
|||||||
// Arithmetic operators - preserve int/float distinction
|
// Arithmetic operators - preserve int/float distinction
|
||||||
add: (a: NixValue, b: NixValue): bigint | number => {
|
add: (a: NixValue, b: NixValue): bigint | number => {
|
||||||
// FIXME: String & Path
|
// FIXME: String & Path
|
||||||
const [av, bv] = coerce_numeric(force_numeric(a), force_numeric(b));
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (av as any) + (bv as any);
|
return (av as any) + (bv as any);
|
||||||
},
|
},
|
||||||
|
|
||||||
sub: (a: NixValue, b: NixValue): bigint | number => {
|
sub: (a: NixValue, b: NixValue): bigint | number => {
|
||||||
const [av, bv] = coerce_numeric(force_numeric(a), force_numeric(b));
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (av as any) - (bv as any);
|
return (av as any) - (bv as any);
|
||||||
},
|
},
|
||||||
|
|
||||||
mul: (a: NixValue, b: NixValue): bigint | number => {
|
mul: (a: NixValue, b: NixValue): bigint | number => {
|
||||||
const [av, bv] = coerce_numeric(force_numeric(a), force_numeric(b));
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (av as any) * (bv as any);
|
return (av as any) * (bv as any);
|
||||||
},
|
},
|
||||||
|
|
||||||
div: (a: NixValue, b: NixValue): bigint | number => {
|
div: (a: NixValue, b: NixValue): bigint | number => {
|
||||||
const [av, bv] = coerce_numeric(force_numeric(a), force_numeric(b));
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
|
|
||||||
if (bv === 0 || bv === 0n) {
|
if (bv === 0 || bv === 0n) {
|
||||||
throw new RangeError("Division by zero");
|
throw new RangeError("Division by zero");
|
||||||
@@ -54,22 +54,22 @@ export const op = {
|
|||||||
},
|
},
|
||||||
lt: (a: NixValue, b: NixValue): boolean => {
|
lt: (a: NixValue, b: NixValue): boolean => {
|
||||||
// FIXME: Non-numeric
|
// FIXME: Non-numeric
|
||||||
const [av, bv] = coerce_numeric(force_numeric(a), force_numeric(b));
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (av as any) < (bv as any);
|
return (av as any) < (bv as any);
|
||||||
},
|
},
|
||||||
lte: (a: NixValue, b: NixValue): boolean => {
|
lte: (a: NixValue, b: NixValue): boolean => {
|
||||||
// FIXME: Non-numeric
|
// FIXME: Non-numeric
|
||||||
const [av, bv] = coerce_numeric(force_numeric(a), force_numeric(b));
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (av as any) <= (bv as any);
|
return (av as any) <= (bv as any);
|
||||||
},
|
},
|
||||||
gt: (a: NixValue, b: NixValue): boolean => {
|
gt: (a: NixValue, b: NixValue): boolean => {
|
||||||
// FIXME: Non-numeric
|
// FIXME: Non-numeric
|
||||||
const [av, bv] = coerce_numeric(force_numeric(a), force_numeric(b));
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (av as any) > (bv as any);
|
return (av as any) > (bv as any);
|
||||||
},
|
},
|
||||||
gte: (a: NixValue, b: NixValue): boolean => {
|
gte: (a: NixValue, b: NixValue): boolean => {
|
||||||
// FIXME: Non-numeric
|
// FIXME: Non-numeric
|
||||||
const [av, bv] = coerce_numeric(force_numeric(a), force_numeric(b));
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (av as any) >= (bv as any);
|
return (av as any) >= (bv as any);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -81,11 +81,11 @@ export const op = {
|
|||||||
|
|
||||||
// List concatenation
|
// List concatenation
|
||||||
concat: (a: NixValue, b: NixValue): NixList => {
|
concat: (a: NixValue, b: NixValue): NixList => {
|
||||||
return Array.prototype.concat.call(force_list(a), force_list(b));
|
return Array.prototype.concat.call(forceList(a), forceList(b));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Attribute set update (merge)
|
// Attribute set update (merge)
|
||||||
update: (a: NixValue, b: NixValue): NixAttrs => {
|
update: (a: NixValue, b: NixValue): NixAttrs => {
|
||||||
return { ...force_attrs(a), ...force_attrs(b) };
|
return { ...forceAttrs(a), ...forceAttrs(b) };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class NixThunk implements NixThunkInterface {
|
|||||||
* @param value - Value to check
|
* @param value - Value to check
|
||||||
* @returns true if value is a NixThunk
|
* @returns true if value is a NixThunk
|
||||||
*/
|
*/
|
||||||
export const is_thunk = (value: unknown): value is NixThunkInterface => {
|
export const isThunk = (value: unknown): value is NixThunkInterface => {
|
||||||
return value !== null && typeof value === "object" && IS_THUNK in value && value[IS_THUNK] === true;
|
return value !== null && typeof value === "object" && IS_THUNK in value && value[IS_THUNK] === true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export const is_thunk = (value: unknown): value is NixThunkInterface => {
|
|||||||
* @returns The forced/evaluated value
|
* @returns The forced/evaluated value
|
||||||
*/
|
*/
|
||||||
export const force = (value: NixValue): NixStrictValue => {
|
export const force = (value: NixValue): NixStrictValue => {
|
||||||
if (!is_thunk(value)) {
|
if (!isThunk(value)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +69,6 @@ export const force = (value: NixValue): NixStrictValue => {
|
|||||||
* @param func - Function that produces a value when called
|
* @param func - Function that produces a value when called
|
||||||
* @returns A new NixThunk wrapping the function
|
* @returns A new NixThunk wrapping the function
|
||||||
*/
|
*/
|
||||||
export const create_thunk = (func: () => NixValue): NixThunkInterface => {
|
export const createThunk = (func: () => NixValue): NixThunkInterface => {
|
||||||
return new NixThunk(func);
|
return new NixThunk(func);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
import type { NixValue, NixList, NixAttrs, NixFunction, NixInt, NixFloat, NixNumber } from "./types";
|
import type { NixValue, NixList, NixAttrs, NixFunction, NixInt, NixFloat, NixNumber } from "./types";
|
||||||
import { force } from "./thunk";
|
import { force } from "./thunk";
|
||||||
import { isAttrs } from "./builtins/type-check";
|
|
||||||
|
|
||||||
const typeName = (value: NixValue): string => {
|
const typeName = (value: NixValue): string => {
|
||||||
const val = force(value);
|
const val = force(value);
|
||||||
@@ -26,7 +25,7 @@ const typeName = (value: NixValue): string => {
|
|||||||
* Force a value and assert it's a list
|
* Force a value and assert it's a list
|
||||||
* @throws TypeError if value is not a list after forcing
|
* @throws TypeError if value is not a list after forcing
|
||||||
*/
|
*/
|
||||||
export const force_list = (value: NixValue): NixList => {
|
export const forceList = (value: NixValue): NixList => {
|
||||||
const forced = force(value);
|
const forced = force(value);
|
||||||
if (!Array.isArray(forced)) {
|
if (!Array.isArray(forced)) {
|
||||||
throw new TypeError(`Expected list, got ${typeName(forced)}`);
|
throw new TypeError(`Expected list, got ${typeName(forced)}`);
|
||||||
@@ -38,7 +37,7 @@ export const force_list = (value: NixValue): NixList => {
|
|||||||
* Force a value and assert it's a function
|
* Force a value and assert it's a function
|
||||||
* @throws TypeError if value is not a function after forcing
|
* @throws TypeError if value is not a function after forcing
|
||||||
*/
|
*/
|
||||||
export const force_function = (value: NixValue): NixFunction => {
|
export const forceFunction = (value: NixValue): NixFunction => {
|
||||||
const forced = force(value);
|
const forced = force(value);
|
||||||
if (typeof forced !== "function") {
|
if (typeof forced !== "function") {
|
||||||
throw new TypeError(`Expected function, got ${typeName(forced)}`);
|
throw new TypeError(`Expected function, got ${typeName(forced)}`);
|
||||||
@@ -50,7 +49,7 @@ export const force_function = (value: NixValue): NixFunction => {
|
|||||||
* Force a value and assert it's an attribute set
|
* Force a value and assert it's an attribute set
|
||||||
* @throws TypeError if value is not an attribute set after forcing
|
* @throws TypeError if value is not an attribute set after forcing
|
||||||
*/
|
*/
|
||||||
export const force_attrs = (value: NixValue): NixAttrs => {
|
export const forceAttrs = (value: NixValue): NixAttrs => {
|
||||||
const forced = force(value);
|
const forced = force(value);
|
||||||
if (typeof forced !== "object" || Array.isArray(forced) || forced === null) {
|
if (typeof forced !== "object" || Array.isArray(forced) || forced === null) {
|
||||||
throw new TypeError(`Expected attribute set, got ${typeName(forced)}`);
|
throw new TypeError(`Expected attribute set, got ${typeName(forced)}`);
|
||||||
@@ -62,7 +61,7 @@ export const force_attrs = (value: NixValue): NixAttrs => {
|
|||||||
* Force a value and assert it's a string
|
* Force a value and assert it's a string
|
||||||
* @throws TypeError if value is not a string after forcing
|
* @throws TypeError if value is not a string after forcing
|
||||||
*/
|
*/
|
||||||
export const force_string = (value: NixValue): string => {
|
export const forceString = (value: NixValue): string => {
|
||||||
const forced = force(value);
|
const forced = force(value);
|
||||||
if (typeof forced !== "string") {
|
if (typeof forced !== "string") {
|
||||||
throw new TypeError(`Expected string, got ${typeName(forced)}`);
|
throw new TypeError(`Expected string, got ${typeName(forced)}`);
|
||||||
@@ -74,7 +73,7 @@ export const force_string = (value: NixValue): string => {
|
|||||||
* Force a value and assert it's a boolean
|
* Force a value and assert it's a boolean
|
||||||
* @throws TypeError if value is not a boolean after forcing
|
* @throws TypeError if value is not a boolean after forcing
|
||||||
*/
|
*/
|
||||||
export const force_bool = (value: NixValue): boolean => {
|
export const forceBool = (value: NixValue): boolean => {
|
||||||
const forced = force(value);
|
const forced = force(value);
|
||||||
if (typeof forced !== "boolean") {
|
if (typeof forced !== "boolean") {
|
||||||
throw new TypeError(`Expected boolean, got ${typeName(forced)}`);
|
throw new TypeError(`Expected boolean, got ${typeName(forced)}`);
|
||||||
@@ -86,7 +85,7 @@ export const force_bool = (value: NixValue): boolean => {
|
|||||||
* Force a value and extract int value
|
* Force a value and extract int value
|
||||||
* @throws TypeError if value is not an int
|
* @throws TypeError if value is not an int
|
||||||
*/
|
*/
|
||||||
export const force_int = (value: NixValue): NixInt => {
|
export const forceInt = (value: NixValue): NixInt => {
|
||||||
const forced = force(value);
|
const forced = force(value);
|
||||||
if (typeof forced === "bigint") {
|
if (typeof forced === "bigint") {
|
||||||
return forced;
|
return forced;
|
||||||
@@ -98,7 +97,7 @@ export const force_int = (value: NixValue): NixInt => {
|
|||||||
* Force a value and extract float value
|
* Force a value and extract float value
|
||||||
* @throws TypeError if value is not a float
|
* @throws TypeError if value is not a float
|
||||||
*/
|
*/
|
||||||
export const force_float = (value: NixValue): NixFloat => {
|
export const forceFloat = (value: NixValue): NixFloat => {
|
||||||
const forced = force(value);
|
const forced = force(value);
|
||||||
if (typeof forced === "number") {
|
if (typeof forced === "number") {
|
||||||
return forced;
|
return forced;
|
||||||
@@ -110,7 +109,7 @@ export const force_float = (value: NixValue): NixFloat => {
|
|||||||
* Force a value and extract numeric value (int or float)
|
* Force a value and extract numeric value (int or float)
|
||||||
* @throws TypeError if value is not a numeric type
|
* @throws TypeError if value is not a numeric type
|
||||||
*/
|
*/
|
||||||
export const force_numeric = (value: NixValue): NixNumber => {
|
export const forceNumeric = (value: NixValue): NixNumber => {
|
||||||
const forced = force(value);
|
const forced = force(value);
|
||||||
if (typeof forced === "bigint" || typeof forced === "number") {
|
if (typeof forced === "bigint" || typeof forced === "number") {
|
||||||
return forced;
|
return forced;
|
||||||
@@ -123,7 +122,7 @@ export const force_numeric = (value: NixValue): NixNumber => {
|
|||||||
* Rule: If either is float, convert both to float; otherwise keep as bigint
|
* Rule: If either is float, convert both to float; otherwise keep as bigint
|
||||||
* @returns [a, b] tuple of coerced values
|
* @returns [a, b] tuple of coerced values
|
||||||
*/
|
*/
|
||||||
export const coerce_numeric = (a: NixNumber, b: NixNumber): [NixFloat, NixFloat] | [NixInt, NixInt] => {
|
export const coerceNumeric = (a: NixNumber, b: NixNumber): [NixFloat, NixFloat] | [NixInt, NixInt] => {
|
||||||
const aIsInt = typeof a === "bigint";
|
const aIsInt = typeof a === "bigint";
|
||||||
const bIsInt = typeof b === "bigint";
|
const bIsInt = typeof b === "bigint";
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export type NixNull = null;
|
|||||||
// Nix composite types
|
// Nix composite types
|
||||||
export type NixList = NixValue[];
|
export type NixList = NixValue[];
|
||||||
export type NixAttrs = { [key: string]: NixValue };
|
export type NixAttrs = { [key: string]: NixValue };
|
||||||
export type NixFunction = (...args: any[]) => any;
|
export type NixFunction = (arg: NixValue) => NixValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for lazy thunk values
|
* Interface for lazy thunk values
|
||||||
@@ -42,11 +42,7 @@ export type NixStrictValue = Exclude<NixValue, NixThunkInterface>;
|
|||||||
* CatchableError: Error type thrown by `builtins.throw`
|
* CatchableError: Error type thrown by `builtins.throw`
|
||||||
* This can be caught by `builtins.tryEval`
|
* This can be caught by `builtins.tryEval`
|
||||||
*/
|
*/
|
||||||
export class CatchableError extends Error {
|
export class CatchableError extends Error {}
|
||||||
constructor(msg: string) {
|
|
||||||
super(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Operator function signatures
|
// Operator function signatures
|
||||||
export type BinaryOp<T = NixValue, U = NixValue, R = NixValue> = (a: T, b: U) => R;
|
export type BinaryOp<T = NixValue, U = NixValue, R = NixValue> = (a: T, b: U) => R;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
|
|||||||
Ir::Path(p) => {
|
Ir::Path(p) => {
|
||||||
// Path needs runtime resolution for interpolated paths
|
// Path needs runtime resolution for interpolated paths
|
||||||
let path_expr = ctx.get_ir(p.expr).compile(ctx);
|
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 }) => {
|
&Ir::If(If { cond, consq, alter }) => {
|
||||||
let cond = ctx.get_ir(cond).compile(ctx);
|
let cond = ctx.get_ir(cond).compile(ctx);
|
||||||
@@ -59,7 +59,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
|
|||||||
Ir::Select(x) => x.compile(ctx),
|
Ir::Select(x) => x.compile(ctx),
|
||||||
&Ir::Thunk(expr_id) => {
|
&Ir::Thunk(expr_id) => {
|
||||||
let inner = ctx.get_ir(expr_id).compile(ctx);
|
let inner = ctx.get_ir(expr_id).compile(ctx);
|
||||||
format!("Nix.create_thunk(()=>({}))", inner)
|
format!("Nix.createThunk(()=>({}))", inner)
|
||||||
}
|
}
|
||||||
&Ir::ExprRef(expr_id) => {
|
&Ir::ExprRef(expr_id) => {
|
||||||
format!("expr{}", expr_id.0)
|
format!("expr{}", expr_id.0)
|
||||||
@@ -166,36 +166,40 @@ impl Func {
|
|||||||
"null".to_string()
|
"null".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call Nix.validate_params and store the result
|
// Call Nix.validateParams and store the result
|
||||||
format!("Nix.validate_params(arg{},{},{});", id, required, allowed)
|
format!("Nix.validateParams(arg{},{},{});", id, required, allowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: CodegenContext> Compile<Ctx> for Let {
|
impl<Ctx: CodegenContext> Compile<Ctx> for Let {
|
||||||
fn compile(&self, ctx: &Ctx) -> String {
|
fn compile(&self, ctx: &Ctx) -> String {
|
||||||
let declarations: Vec<String> = self
|
let info = &self.binding_sccs;
|
||||||
.bindings
|
let mut js_statements = Vec::new();
|
||||||
.iter()
|
|
||||||
.map(|&expr| format!("let expr{}", expr.0))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let assignments: Vec<String> = self
|
for (scc_exprs, is_recursive) in info.sccs.iter() {
|
||||||
.bindings
|
if *is_recursive {
|
||||||
.iter()
|
for &expr in scc_exprs {
|
||||||
.map(|&expr| {
|
js_statements.push(format!("let expr{}", expr.0));
|
||||||
|
}
|
||||||
|
for &expr in scc_exprs {
|
||||||
let value = ctx.get_ir(expr).compile(ctx);
|
let value = ctx.get_ir(expr).compile(ctx);
|
||||||
format!("expr{}={}", expr.0, value)
|
js_statements.push(format!("expr{}={}", expr.0, value));
|
||||||
})
|
}
|
||||||
.collect();
|
} 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);
|
let body = ctx.get_ir(self.body).compile(ctx);
|
||||||
|
format!("(()=>{{{}; return {}}})()", js_statements.join(";"), body)
|
||||||
format!(
|
|
||||||
"(()=>{{{}; {}; return {}}})()",
|
|
||||||
declarations.join(";"),
|
|
||||||
assignments.join(";"),
|
|
||||||
body
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +220,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Select {
|
|||||||
{
|
{
|
||||||
let default_val = ctx.get_ir(default).compile(ctx);
|
let default_val = ctx.get_ir(default).compile(ctx);
|
||||||
format!(
|
format!(
|
||||||
"Nix.select_with_default({}, \"{}\", {})",
|
"Nix.selectWithDefault({}, \"{}\", {})",
|
||||||
result, key, default_val
|
result, key, default_val
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -230,7 +234,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Select {
|
|||||||
{
|
{
|
||||||
let default_val = ctx.get_ir(default).compile(ctx);
|
let default_val = ctx.get_ir(default).compile(ctx);
|
||||||
format!(
|
format!(
|
||||||
"Nix.select_with_default({}, {}, {})",
|
"Nix.selectWithDefault({}, {}, {})",
|
||||||
result, key, default_val
|
result, key, default_val
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -284,8 +288,11 @@ impl<Ctx: CodegenContext> Compile<Ctx> for ConcatStrings {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|part| {
|
.map(|part| {
|
||||||
let compiled = ctx.get_ir(*part).compile(ctx);
|
let compiled = ctx.get_ir(*part).compile(ctx);
|
||||||
// TODO: coerce to string
|
// TODO: copyToStore
|
||||||
format!("String(Nix.force({}))", compiled)
|
format!(
|
||||||
|
"Nix.coerceToString({}, Nix.StringCoercionMode.Interpolation, false)",
|
||||||
|
compiled
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -307,6 +314,6 @@ impl<Ctx: CodegenContext> Compile<Ctx> for HasAttr {
|
|||||||
Attr::Dynamic(expr_id) => ctx.get_ir(*expr_id).compile(ctx),
|
Attr::Dynamic(expr_id) => ctx.get_ir(*expr_id).compile(ctx),
|
||||||
})
|
})
|
||||||
.join(",");
|
.join(",");
|
||||||
format!("Nix.has_attr({lhs}, [{attrpath}])")
|
format!("Nix.hasAttr({lhs}, [{attrpath}])")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::pin::Pin;
|
|
||||||
use std::ptr::NonNull;
|
use std::ptr::NonNull;
|
||||||
|
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
@@ -9,109 +8,97 @@ use string_interner::DefaultStringInterner;
|
|||||||
use crate::codegen::{CodegenContext, Compile};
|
use crate::codegen::{CodegenContext, Compile};
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::ir::{Builtin, DowngradeContext, ExprId, Ir, SymId};
|
use crate::ir::{Builtin, DowngradeContext, ExprId, Ir, SymId};
|
||||||
use crate::runtime::Runtime;
|
use crate::runtime::{Runtime, RuntimeCtx};
|
||||||
use crate::value::Value;
|
use crate::value::Value;
|
||||||
|
|
||||||
use downgrade::DowngradeCtx;
|
use downgrade::DowngradeCtx;
|
||||||
|
use drop_guard::{PathDropGuard, PathStackProvider};
|
||||||
|
|
||||||
mod downgrade;
|
mod downgrade;
|
||||||
|
mod drop_guard;
|
||||||
|
|
||||||
pub struct Context {
|
mod private {
|
||||||
ctx: Pin<Box<Ctx>>,
|
use super::*;
|
||||||
runtime: Runtime,
|
use std::ops::DerefMut;
|
||||||
}
|
use std::ptr::NonNull;
|
||||||
|
|
||||||
pub(crate) struct CtxPtr(NonNull<Ctx>);
|
pub struct CtxPtr(NonNull<Ctx>);
|
||||||
impl CtxPtr {
|
impl CtxPtr {
|
||||||
pub(crate) unsafe fn as_ref(&self) -> &Ctx {
|
pub fn new(ctx: &mut Ctx) -> Self {
|
||||||
|
unsafe { CtxPtr(NonNull::new_unchecked(ctx)) }
|
||||||
|
}
|
||||||
|
fn as_ref(&self) -> &Ctx {
|
||||||
|
// SAFETY: This is safe since inner `NonNull<Ctx>` is obtained from `&mut Ctx`
|
||||||
unsafe { self.0.as_ref() }
|
unsafe { self.0.as_ref() }
|
||||||
}
|
}
|
||||||
pub(crate) unsafe fn as_mut(&mut self) -> Pin<&mut Ctx> {
|
fn as_mut(&mut self) -> &mut Ctx {
|
||||||
unsafe { Pin::new_unchecked(self.0.as_mut()) }
|
// SAFETY: This is safe since inner `NonNull<Ctx>` is obtained from `&mut Ctx`
|
||||||
|
unsafe { self.0.as_mut() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl PathStackProvider for CtxPtr {
|
||||||
|
fn path_stack(&mut self) -> &mut Vec<PathBuf> {
|
||||||
|
&mut self.as_mut().path_stack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuntimeCtx for CtxPtr {
|
||||||
|
fn get_current_dir(&self) -> PathBuf {
|
||||||
|
self.as_ref().get_current_dir()
|
||||||
|
}
|
||||||
|
fn push_path_stack(&mut self, path: PathBuf) -> impl DerefMut<Target = Self> {
|
||||||
|
PathDropGuard::new(path, self)
|
||||||
|
}
|
||||||
|
fn compile_code(&mut self, expr: &str) -> Result<String> {
|
||||||
|
self.as_mut().compile_code(expr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
use private::CtxPtr;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SccInfo {
|
||||||
|
/// list of SCCs (exprs, recursive)
|
||||||
|
pub sccs: Vec<(Vec<ExprId>, bool)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
ctx: Ctx,
|
||||||
|
runtime: Runtime<CtxPtr>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let mut ctx = Box::pin(Ctx::new());
|
let ctx = Ctx::new();
|
||||||
let ptr = unsafe { CtxPtr(NonNull::new_unchecked(Pin::get_unchecked_mut(ctx.as_mut()))) };
|
let runtime = Runtime::new()?;
|
||||||
let runtime = Runtime::new(ptr)?;
|
|
||||||
|
|
||||||
Ok(Self { ctx, runtime })
|
Ok(Self { ctx, runtime })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn eval_code(&mut self, expr: &str) -> Result<Value> {
|
pub fn eval_code(&mut self, expr: &str) -> Result<Value> {
|
||||||
// Initialize `path_stack` with current directory for relative path resolution
|
// Initialize `path_stack` with current directory for relative path resolution
|
||||||
let mut guard = PathDropGuard::new_cwd(self.ctx.as_mut())?;
|
let mut guard = PathDropGuard::new_cwd(&mut self.ctx)?;
|
||||||
let ctx = guard.as_ctx();
|
let ctx = guard.as_ctx();
|
||||||
|
|
||||||
let root = rnix::Root::parse(expr);
|
let code = ctx.compile_code(expr)?;
|
||||||
if !root.errors().is_empty() {
|
self.runtime.eval(code, CtxPtr::new(&mut self.ctx))
|
||||||
return Err(Error::parse_error(root.errors().iter().join("; ")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::unwrap_used)]
|
pub fn compile_code(&mut self, expr: &str) -> Result<String> {
|
||||||
// Always `Some` since there is no parse error
|
self.ctx.compile_code(expr)
|
||||||
let root = ctx
|
|
||||||
.as_mut()
|
|
||||||
.downgrade_ctx()
|
|
||||||
.downgrade(root.tree().expr().unwrap())?;
|
|
||||||
let code = ctx.get_ir(root).compile(Pin::get_ref(ctx.as_ref()));
|
|
||||||
let code = format!("Nix.force({})", code);
|
|
||||||
println!("[DEBUG] generated code: {}", &code);
|
|
||||||
self.runtime.eval(code)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn eval_js(&mut self, code: String) -> Result<Value> {
|
pub(crate) fn eval_js(&mut self, code: String) -> Result<Value> {
|
||||||
self.runtime.eval(code)
|
self.runtime.eval(code, CtxPtr::new(&mut self.ctx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pin_project::pin_project(PinnedDrop)]
|
|
||||||
pub(crate) struct Ctx {
|
pub(crate) struct Ctx {
|
||||||
irs: Vec<Ir>,
|
irs: Vec<Ir>,
|
||||||
symbols: DefaultStringInterner,
|
symbols: DefaultStringInterner,
|
||||||
global: NonNull<HashMap<SymId, ExprId>>,
|
global: NonNull<HashMap<SymId, ExprId>>,
|
||||||
path_stack: Vec<PathBuf>,
|
path_stack: Vec<PathBuf>,
|
||||||
#[pin]
|
|
||||||
_marker: std::marker::PhantomPinned,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pin_project::pinned_drop]
|
|
||||||
impl PinnedDrop for Ctx {
|
|
||||||
fn drop(self: Pin<&mut Self>) {
|
|
||||||
unsafe {
|
|
||||||
drop(Box::from_raw(self.global.as_ptr()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct PathDropGuard<'ctx> {
|
|
||||||
ctx: Pin<&'ctx mut Ctx>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'ctx> PathDropGuard<'ctx> {
|
|
||||||
pub(crate) fn new(path: PathBuf, mut ctx: Pin<&'ctx mut Ctx>) -> Self {
|
|
||||||
ctx.as_mut().project().path_stack.push(path);
|
|
||||||
Self { ctx }
|
|
||||||
}
|
|
||||||
pub(crate) fn new_cwd(mut ctx: Pin<&'ctx mut Ctx>) -> Result<Self> {
|
|
||||||
let cwd = std::env::current_dir()
|
|
||||||
.map_err(|err| Error::downgrade_error(format!("cannot get cwd: {err}")))?;
|
|
||||||
let virtual_file = cwd.join("__eval__.nix");
|
|
||||||
ctx.as_mut().project().path_stack.push(virtual_file);
|
|
||||||
Ok(Self { ctx })
|
|
||||||
}
|
|
||||||
pub(crate) fn as_ctx<'a>(&'a mut self) -> &'a mut Pin<&'ctx mut Ctx> {
|
|
||||||
&mut self.ctx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for PathDropGuard<'_> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.ctx.as_mut().project().path_stack.pop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Ctx {
|
impl Default for Ctx {
|
||||||
@@ -165,7 +152,6 @@ impl Default for Ctx {
|
|||||||
irs,
|
irs,
|
||||||
global: unsafe { NonNull::new_unchecked(Box::leak(Box::new(global))) },
|
global: unsafe { NonNull::new_unchecked(Box::leak(Box::new(global))) },
|
||||||
path_stack: Vec::new(),
|
path_stack: Vec::new(),
|
||||||
_marker: std::marker::PhantomPinned,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +161,7 @@ impl Ctx {
|
|||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn downgrade_ctx<'a>(self: Pin<&'a mut Self>) -> DowngradeCtx<'a> {
|
pub(crate) fn downgrade_ctx<'a>(&'a mut self) -> DowngradeCtx<'a> {
|
||||||
let global_ref = unsafe { self.global.as_ref() };
|
let global_ref = unsafe { self.global.as_ref() };
|
||||||
DowngradeCtx::new(self, global_ref)
|
DowngradeCtx::new(self, global_ref)
|
||||||
}
|
}
|
||||||
@@ -190,6 +176,24 @@ impl Ctx {
|
|||||||
.expect("path in path_stack should always have a parent dir. this is a bug")
|
.expect("path in path_stack should always have a parent dir. this is a bug")
|
||||||
.to_path_buf()
|
.to_path_buf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compile_code(&mut self, expr: &str) -> Result<String> {
|
||||||
|
let root = rnix::Root::parse(expr);
|
||||||
|
if !root.errors().is_empty() {
|
||||||
|
return Err(Error::parse_error(root.errors().iter().join("; ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
// Always `Some` since there is no parse error
|
||||||
|
let root = self
|
||||||
|
.downgrade_ctx()
|
||||||
|
.downgrade(root.tree().expr().unwrap())?;
|
||||||
|
let code = self.get_ir(root).compile(self);
|
||||||
|
let code = format!("Nix.force({})", code);
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
eprintln!("[DEBUG] generated code: {}", &code);
|
||||||
|
Ok(code)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CodegenContext for Ctx {
|
impl CodegenContext for Ctx {
|
||||||
@@ -202,707 +206,8 @@ impl CodegenContext for Ctx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
impl PathStackProvider for Ctx {
|
||||||
#[allow(clippy::unwrap_used)]
|
fn path_stack(&mut self) -> &mut Vec<PathBuf> {
|
||||||
mod test {
|
&mut self.path_stack
|
||||||
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 || <expr> should not evaluate <expr>
|
|
||||||
("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 - <expr>)
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
use std::pin::Pin;
|
|
||||||
|
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
|
use hashbrown::HashSet;
|
||||||
|
use petgraph::Directed;
|
||||||
|
use petgraph::Graph;
|
||||||
|
use petgraph::graph::NodeIndex;
|
||||||
|
|
||||||
use crate::codegen::CodegenContext;
|
use crate::codegen::CodegenContext;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::ir::{ArgId, Downgrade, DowngradeContext, ExprId, Ir, SymId, ToIr};
|
use crate::ir::{ArgId, Downgrade, DowngradeContext, ExprId, Ir, SymId, ToIr};
|
||||||
|
|
||||||
use super::Ctx;
|
use super::{Ctx, SccInfo};
|
||||||
|
|
||||||
|
struct DependencyTracker {
|
||||||
|
expr_to_node: HashMap<ExprId, NodeIndex>,
|
||||||
|
graph: Graph<ExprId, (), Directed>,
|
||||||
|
current_binding: Option<ExprId>,
|
||||||
|
let_scope_exprs: HashSet<ExprId>,
|
||||||
|
}
|
||||||
|
|
||||||
enum Scope<'ctx> {
|
enum Scope<'ctx> {
|
||||||
Global(&'ctx HashMap<SymId, ExprId>),
|
Global(&'ctx HashMap<SymId, ExprId>),
|
||||||
@@ -32,18 +41,20 @@ impl<'a, 'ctx> ScopeGuard<'a, 'ctx> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct DowngradeCtx<'ctx> {
|
pub struct DowngradeCtx<'ctx> {
|
||||||
ctx: Pin<&'ctx mut Ctx>,
|
ctx: &'ctx mut Ctx,
|
||||||
irs: Vec<Option<Ir>>,
|
irs: Vec<Option<Ir>>,
|
||||||
scopes: Vec<Scope<'ctx>>,
|
scopes: Vec<Scope<'ctx>>,
|
||||||
arg_id: usize,
|
arg_id: usize,
|
||||||
|
dep_tracker_stack: Vec<DependencyTracker>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'ctx> DowngradeCtx<'ctx> {
|
impl<'ctx> DowngradeCtx<'ctx> {
|
||||||
pub fn new(ctx: Pin<&'ctx mut Ctx>, global: &'ctx HashMap<SymId, ExprId>) -> Self {
|
pub fn new(ctx: &'ctx mut Ctx, global: &'ctx HashMap<SymId, ExprId>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
scopes: vec![Scope::Global(global)],
|
scopes: vec![Scope::Global(global)],
|
||||||
irs: vec![],
|
irs: vec![],
|
||||||
arg_id: 0,
|
arg_id: 0,
|
||||||
|
dep_tracker_stack: Vec::new(),
|
||||||
ctx,
|
ctx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +73,7 @@ impl DowngradeContext for DowngradeCtx<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn new_sym(&mut self, sym: String) -> SymId {
|
fn new_sym(&mut self, sym: String) -> SymId {
|
||||||
self.ctx.as_mut().project().symbols.get_or_intern(sym)
|
self.ctx.symbols.get_or_intern(sym)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_sym(&self, id: SymId) -> &str {
|
fn get_sym(&self, id: SymId) -> &str {
|
||||||
@@ -79,7 +90,16 @@ impl DowngradeContext for DowngradeCtx<'_> {
|
|||||||
}
|
}
|
||||||
Scope::Let(let_scope) => {
|
Scope::Let(let_scope) => {
|
||||||
if let Some(&expr) = let_scope.get(&sym) {
|
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)));
|
return Ok(self.new_expr(Ir::ExprRef(expr)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,8 +164,6 @@ impl DowngradeContext for DowngradeCtx<'_> {
|
|||||||
fn downgrade(mut self, root: rnix::ast::Expr) -> Result<ExprId> {
|
fn downgrade(mut self, root: rnix::ast::Expr) -> Result<ExprId> {
|
||||||
let root = root.downgrade(&mut self)?;
|
let root = root.downgrade(&mut self)?;
|
||||||
self.ctx
|
self.ctx
|
||||||
.as_mut()
|
|
||||||
.project()
|
|
||||||
.irs
|
.irs
|
||||||
.extend(self.irs.into_iter().map(Option::unwrap));
|
.extend(self.irs.into_iter().map(Option::unwrap));
|
||||||
Ok(root)
|
Ok(root)
|
||||||
@@ -181,4 +199,59 @@ impl DowngradeContext for DowngradeCtx<'_> {
|
|||||||
fn get_current_dir(&self) -> std::path::PathBuf {
|
fn get_current_dir(&self) -> std::path::PathBuf {
|
||||||
self.ctx.get_current_dir()
|
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<ExprId>) {
|
||||||
|
if let Some(tracker) = self.dep_tracker_stack.last_mut() {
|
||||||
|
tracker.current_binding = expr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_dep_tracker(&mut self) -> Result<SccInfo> {
|
||||||
|
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 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
nix-js/src/context/drop_guard.rs
Normal file
41
nix-js/src/context/drop_guard.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
pub trait PathStackProvider {
|
||||||
|
fn path_stack(&mut self) -> &mut Vec<PathBuf>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PathDropGuard<'ctx, Ctx: PathStackProvider> {
|
||||||
|
ctx: &'ctx mut Ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx, Ctx: PathStackProvider> PathDropGuard<'ctx, Ctx> {
|
||||||
|
pub fn new(path: PathBuf, ctx: &'ctx mut Ctx) -> Self {
|
||||||
|
ctx.path_stack().push(path);
|
||||||
|
Self { ctx }
|
||||||
|
}
|
||||||
|
pub fn new_cwd(ctx: &'ctx mut Ctx) -> Result<Self> {
|
||||||
|
let cwd = std::env::current_dir()
|
||||||
|
.map_err(|err| Error::downgrade_error(format!("cannot get cwd: {err}")))?;
|
||||||
|
let virtual_file = cwd.join("__eval__.nix");
|
||||||
|
ctx.path_stack().push(virtual_file);
|
||||||
|
Ok(Self { ctx })
|
||||||
|
}
|
||||||
|
pub fn as_ctx(&mut self) -> &mut Ctx {
|
||||||
|
self.ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx: PathStackProvider> Deref for PathDropGuard<'_, Ctx> {
|
||||||
|
type Target = Ctx;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<Ctx: PathStackProvider> DerefMut for PathDropGuard<'_, Ctx> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
self.ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ use hashbrown::{HashMap, HashSet};
|
|||||||
use rnix::ast;
|
use rnix::ast;
|
||||||
use string_interner::symbol::SymbolU32;
|
use string_interner::symbol::SymbolU32;
|
||||||
|
|
||||||
|
use crate::context::SccInfo;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::value::format_symbol;
|
use crate::value::format_symbol;
|
||||||
use nix_js_macros::ir;
|
use nix_js_macros::ir;
|
||||||
@@ -38,6 +39,10 @@ pub trait DowngradeContext {
|
|||||||
F: FnOnce(&mut Self) -> R;
|
F: FnOnce(&mut Self) -> R;
|
||||||
|
|
||||||
fn get_current_dir(&self) -> std::path::PathBuf;
|
fn get_current_dir(&self) -> std::path::PathBuf;
|
||||||
|
|
||||||
|
fn push_dep_tracker(&mut self, slots: &[ExprId]);
|
||||||
|
fn set_current_binding(&mut self, expr: Option<ExprId>);
|
||||||
|
fn pop_dep_tracker(&mut self) -> Result<SccInfo>;
|
||||||
}
|
}
|
||||||
|
|
||||||
ir! {
|
ir! {
|
||||||
@@ -315,8 +320,8 @@ pub struct Func {
|
|||||||
/// Represents a `let ... in ...` expression.
|
/// Represents a `let ... in ...` expression.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Let {
|
pub struct Let {
|
||||||
/// The bindings in the let expression.
|
/// The bindings in the `let` expression, group in SCCs
|
||||||
pub bindings: Vec<ExprId>,
|
pub binding_sccs: SccInfo,
|
||||||
/// The body expression evaluated in the scope of the bindings.
|
/// The body expression evaluated in the scope of the bindings.
|
||||||
pub body: ExprId,
|
pub body: ExprId,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::AttrSet {
|
|||||||
|
|
||||||
// rec { a = 1; b = a; } => let a = 1; b = a; in { inherit a b; }
|
// rec { a = 1; b = a; } => let a = 1; b = a; in { inherit a b; }
|
||||||
let entries: Vec<_> = self.entries().collect();
|
let entries: Vec<_> = self.entries().collect();
|
||||||
let (bindings, body) = downgrade_let_bindings(entries, ctx, |ctx, binding_keys| {
|
let (binding_sccs, body) = downgrade_let_bindings(entries, ctx, |ctx, binding_keys| {
|
||||||
// Create plain attrset as body with inherit
|
// Create plain attrset as body with inherit
|
||||||
let mut attrs = AttrSet {
|
let mut attrs = AttrSet {
|
||||||
stcs: HashMap::new(),
|
stcs: HashMap::new(),
|
||||||
@@ -186,7 +186,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::AttrSet {
|
|||||||
Ok(ctx.new_expr(attrs.to_ir()))
|
Ok(ctx.new_expr(attrs.to_ir()))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(ctx.new_expr(Let { bindings, body }.to_ir()))
|
Ok(ctx.new_expr(Let { body, binding_sccs }.to_ir()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,10 +288,10 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::LetIn {
|
|||||||
let entries: Vec<_> = self.entries().collect();
|
let entries: Vec<_> = self.entries().collect();
|
||||||
let body_expr = self.body().unwrap();
|
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))?;
|
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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,76 +332,35 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Lambda {
|
|||||||
.with_param_scope(param_sym, arg, |ctx| self.body().unwrap().downgrade(ctx))?;
|
.with_param_scope(param_sym, arg, |ctx| self.body().unwrap().downgrade(ctx))?;
|
||||||
}
|
}
|
||||||
ast::Param::Pattern(pattern) => {
|
ast::Param::Pattern(pattern) => {
|
||||||
// Complex case: `{ a, b ? 2, ... }@args: body`
|
|
||||||
let alias = pattern
|
let alias = pattern
|
||||||
.pat_bind()
|
.pat_bind()
|
||||||
.map(|alias| ctx.new_sym(alias.ident().unwrap().to_string()));
|
.map(|alias| ctx.new_sym(alias.ident().unwrap().to_string()));
|
||||||
ident = alias;
|
ident = alias;
|
||||||
|
|
||||||
let entries = pattern
|
let has_ellipsis = pattern.ellipsis_token().is_some();
|
||||||
.pat_entries()
|
let pat_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::<Result<Vec<_>>>()?;
|
|
||||||
|
|
||||||
required = Some(
|
let PatternBindings {
|
||||||
entries
|
body: inner_body,
|
||||||
.iter()
|
scc_info,
|
||||||
.filter_map(|(k, d)| if d.is_none() { Some(*k) } else { None })
|
required_params,
|
||||||
.collect(),
|
allowed_params,
|
||||||
);
|
} = downgrade_pattern_bindings(
|
||||||
allowed = if pattern.ellipsis_token().is_some() {
|
pat_entries,
|
||||||
None // `...` means any attribute is allowed.
|
alias,
|
||||||
} else {
|
arg,
|
||||||
Some(entries.iter().map(|(k, _)| *k).collect())
|
has_ellipsis,
|
||||||
};
|
ctx,
|
||||||
|
|ctx, _| self.body().unwrap().downgrade(ctx),
|
||||||
|
)?;
|
||||||
|
|
||||||
// Desugar pattern matching in function arguments into a `let` expression.
|
required = Some(required_params);
|
||||||
// For example, `({ a, b ? 2 }): a + b` is desugared into:
|
allowed = allowed_params;
|
||||||
// `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();
|
|
||||||
|
|
||||||
// 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<ExprId> = 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(
|
body = ctx.new_expr(
|
||||||
Let {
|
Let {
|
||||||
bindings: bindings_vec,
|
|
||||||
body: inner_body,
|
body: inner_body,
|
||||||
|
binding_sccs: scc_info,
|
||||||
}
|
}
|
||||||
.to_ir(),
|
.to_ir(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -219,76 +219,151 @@ pub fn downgrade_static_attrpathvalue(
|
|||||||
attrs.insert(path, value, ctx)
|
attrs.insert(path, value, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to downgrade entries with let bindings semantics.
|
pub struct PatternBindings {
|
||||||
/// This extracts common logic for both `rec` attribute sets and `let...in` expressions.
|
pub body: ExprId,
|
||||||
|
pub scc_info: SccInfo,
|
||||||
|
pub required_params: Vec<SymId>,
|
||||||
|
pub allowed_params: Option<HashSet<SymId>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function for Lambda pattern parameters with SCC analysis.
|
||||||
|
/// Processes pattern entries like `{ a, b ? 2, ... }@alias` and creates optimized bindings.
|
||||||
///
|
///
|
||||||
/// Returns a tuple of (binding slots, body result) where:
|
/// # Parameters
|
||||||
/// - binding slots: pre-allocated expression slots for the bindings
|
/// - `pat_entries`: Iterator over pattern entries from the AST
|
||||||
/// - body result: the result of calling `body_fn` in the let scope
|
/// - `alias`: Optional alias symbol (from @alias syntax)
|
||||||
pub fn downgrade_let_bindings<Ctx, F, R>(
|
/// - `arg`: The argument expression to extract from
|
||||||
entries: Vec<ast::Entry>,
|
///
|
||||||
|
/// Returns a tuple of (binding slots, body, SCC info, required params, allowed params)
|
||||||
|
pub fn downgrade_pattern_bindings<Ctx>(
|
||||||
|
pat_entries: impl Iterator<Item = ast::PatEntry>,
|
||||||
|
alias: Option<SymId>,
|
||||||
|
arg: ExprId,
|
||||||
|
has_ellipsis: bool,
|
||||||
ctx: &mut Ctx,
|
ctx: &mut Ctx,
|
||||||
body_fn: F,
|
body_fn: impl FnOnce(&mut Ctx, &[SymId]) -> Result<ExprId>,
|
||||||
) -> Result<(Vec<ExprId>, R)>
|
) -> Result<PatternBindings>
|
||||||
where
|
where
|
||||||
Ctx: DowngradeContext,
|
Ctx: DowngradeContext,
|
||||||
F: FnOnce(&mut Ctx, &[SymId]) -> Result<R>,
|
|
||||||
{
|
{
|
||||||
// 1. Collect all top-level binding keys
|
let mut param_syms = Vec::new();
|
||||||
let mut binding_syms = HashSet::new();
|
let mut param_defaults = Vec::new();
|
||||||
|
let mut seen_params = HashSet::new();
|
||||||
|
|
||||||
for entry in &entries {
|
for entry in pat_entries {
|
||||||
match entry {
|
let sym = ctx.new_sym(entry.ident().unwrap().to_string());
|
||||||
ast::Entry::Inherit(inherit) => {
|
|
||||||
for attr in inherit.attrs() {
|
if !seen_params.insert(sym) {
|
||||||
if let ast::Attr::Ident(ident) = attr {
|
return Err(Error::downgrade_error(format!(
|
||||||
binding_syms.insert(ctx.new_sym(ident.to_string()));
|
"duplicate parameter '{}'",
|
||||||
}
|
format_symbol(ctx.get_sym(sym))
|
||||||
}
|
)));
|
||||||
}
|
|
||||||
ast::Entry::AttrpathValue(value) => {
|
|
||||||
let attrpath = value.attrpath().unwrap();
|
|
||||||
if let Some(first_attr) = attrpath.attrs().next()
|
|
||||||
&& let ast::Attr::Ident(ident) = first_attr
|
|
||||||
{
|
|
||||||
binding_syms.insert(ctx.new_sym(ident.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let binding_keys: Vec<_> = binding_syms.into_iter().collect();
|
let default_ast = entry.default();
|
||||||
|
param_syms.push(sym);
|
||||||
|
param_defaults.push(default_ast);
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Reserve slots for bindings
|
let mut binding_keys: Vec<SymId> = param_syms.clone();
|
||||||
let slots_iter = ctx.reserve_slots(binding_keys.len());
|
if let Some(alias_sym) = alias {
|
||||||
let slots_clone = slots_iter.clone();
|
binding_keys.push(alias_sym);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Create let scope bindings
|
let required: Vec<SymId> = param_syms
|
||||||
let let_bindings: HashMap<_, _> = binding_keys.iter().copied().zip(slots_iter).collect();
|
.iter()
|
||||||
|
.zip(param_defaults.iter())
|
||||||
|
.filter_map(|(&sym, default)| if default.is_none() { Some(sym) } else { None })
|
||||||
|
.collect();
|
||||||
|
|
||||||
// 4. Process entries in let scope
|
let allowed: Option<HashSet<SymId>> = if has_ellipsis {
|
||||||
let body = ctx.with_let_scope(let_bindings, |ctx| {
|
None
|
||||||
// Collect all bindings in a temporary AttrSet
|
} else {
|
||||||
let mut temp_attrs = AttrSet {
|
Some(param_syms.iter().copied().collect())
|
||||||
stcs: HashMap::new(),
|
|
||||||
dyns: Vec::new(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for entry in entries {
|
let (scc_info, body) = downgrade_bindings_generic(
|
||||||
match entry {
|
ctx,
|
||||||
ast::Entry::Inherit(inherit) => {
|
binding_keys,
|
||||||
downgrade_inherit(inherit, &mut temp_attrs.stcs, ctx)?;
|
|ctx, sym_to_slot| {
|
||||||
}
|
let mut bindings = HashMap::new();
|
||||||
ast::Entry::AttrpathValue(value) => {
|
|
||||||
downgrade_static_attrpathvalue(value, &mut temp_attrs, ctx)?;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill pre-allocated slots with top-level bindings
|
if let Some(alias_sym) = alias {
|
||||||
for (sym, slot) in binding_keys.iter().copied().zip(slots_clone.clone()) {
|
bindings.insert(alias_sym, arg);
|
||||||
if let Some(&expr) = temp_attrs.stcs.get(&sym) {
|
}
|
||||||
ctx.replace_expr(slot, Ir::Thunk(expr));
|
|
||||||
|
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, B, F>(
|
||||||
|
ctx: &mut Ctx,
|
||||||
|
binding_keys: Vec<SymId>,
|
||||||
|
compute_bindings_fn: B,
|
||||||
|
body_fn: F,
|
||||||
|
) -> Result<(SccInfo, ExprId)>
|
||||||
|
where
|
||||||
|
Ctx: DowngradeContext,
|
||||||
|
B: FnOnce(&mut Ctx, &HashMap<SymId, ExprId>) -> Result<HashMap<SymId, ExprId>>,
|
||||||
|
F: FnOnce(&mut Ctx, &[SymId]) -> Result<ExprId>,
|
||||||
|
{
|
||||||
|
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 {
|
} else {
|
||||||
return Err(Error::downgrade_error(format!(
|
return Err(Error::downgrade_error(format!(
|
||||||
"binding '{}' not found",
|
"binding '{}' not found",
|
||||||
@@ -297,10 +372,103 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the body function with the binding keys
|
let body = body_fn(ctx, &binding_keys)?;
|
||||||
body_fn(ctx, &binding_keys)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// 5. Return the slots and body
|
Ok((scc_info, body))
|
||||||
Ok((slots_clone.collect(), 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, SCC info) where:
|
||||||
|
/// - binding slots: pre-allocated expression slots for the bindings
|
||||||
|
/// - body result: the result of calling `body_fn` in the let scope
|
||||||
|
/// - SCC info: strongly connected components information for optimization
|
||||||
|
pub fn downgrade_let_bindings<Ctx, F>(
|
||||||
|
entries: Vec<ast::Entry>,
|
||||||
|
ctx: &mut Ctx,
|
||||||
|
body_fn: F,
|
||||||
|
) -> Result<(SccInfo, ExprId)>
|
||||||
|
where
|
||||||
|
Ctx: DowngradeContext,
|
||||||
|
F: FnOnce(&mut Ctx, &[SymId]) -> Result<ExprId>,
|
||||||
|
{
|
||||||
|
let mut binding_syms = HashSet::new();
|
||||||
|
|
||||||
|
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());
|
||||||
|
if !binding_syms.insert(sym) {
|
||||||
|
return Err(Error::downgrade_error(format!(
|
||||||
|
"attribute '{}' already defined",
|
||||||
|
format_symbol(ctx.get_sym(sym))
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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());
|
||||||
|
if !binding_syms.insert(sym) {
|
||||||
|
return Err(Error::downgrade_error(format!(
|
||||||
|
"attribute '{}' already defined",
|
||||||
|
format_symbol(ctx.get_sym(sym))
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let binding_keys: Vec<_> = binding_syms.into_iter().collect();
|
||||||
|
|
||||||
|
downgrade_bindings_generic(
|
||||||
|
ctx,
|
||||||
|
binding_keys,
|
||||||
|
|ctx, sym_to_slot| {
|
||||||
|
let mut temp_attrs = AttrSet {
|
||||||
|
stcs: HashMap::new(),
|
||||||
|
dyns: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(temp_attrs.stcs)
|
||||||
|
},
|
||||||
|
body_fn,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,40 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::pin::Pin;
|
use std::marker::PhantomData;
|
||||||
|
use std::ops::DerefMut;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Once;
|
use std::sync::Once;
|
||||||
|
|
||||||
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpDecl, OpState, RuntimeOptions, v8};
|
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8};
|
||||||
use deno_error::JsErrorClass;
|
use deno_error::JsErrorClass;
|
||||||
|
|
||||||
use crate::codegen::{CodegenContext, Compile};
|
|
||||||
use crate::context::{CtxPtr, PathDropGuard};
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::ir::DowngradeContext;
|
|
||||||
use crate::value::{AttrSet, List, Symbol, Value};
|
use crate::value::{AttrSet, List, Symbol, Value};
|
||||||
|
|
||||||
type ScopeRef<'p, 's> = v8::PinnedRef<'p, v8::HandleScope<'s>>;
|
type ScopeRef<'p, 's> = v8::PinnedRef<'p, v8::HandleScope<'s>>;
|
||||||
type LocalValue<'a> = v8::Local<'a, v8::Value>;
|
type LocalValue<'a> = v8::Local<'a, v8::Value>;
|
||||||
type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>;
|
type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>;
|
||||||
|
|
||||||
fn runtime_extension(ctx: CtxPtr) -> Extension {
|
pub(crate) trait RuntimeCtx: 'static {
|
||||||
|
fn get_current_dir(&self) -> PathBuf;
|
||||||
|
fn push_path_stack(&mut self, path: PathBuf) -> impl DerefMut<Target = Self>;
|
||||||
|
fn compile_code(&mut self, code: &str) -> Result<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_extension<Ctx: RuntimeCtx>() -> Extension {
|
||||||
const ESM: &[ExtensionFileSource] =
|
const ESM: &[ExtensionFileSource] =
|
||||||
&deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js");
|
&deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js");
|
||||||
const OPS: &[OpDecl] = &[
|
let ops = vec![
|
||||||
op_import(),
|
op_import::<Ctx>(),
|
||||||
op_read_file(),
|
op_read_file(),
|
||||||
op_path_exists(),
|
op_path_exists(),
|
||||||
op_resolve_path(),
|
op_resolve_path::<Ctx>(),
|
||||||
];
|
];
|
||||||
|
|
||||||
Extension {
|
Extension {
|
||||||
name: "nix_runtime",
|
name: "nix_runtime",
|
||||||
esm_files: Cow::Borrowed(ESM),
|
esm_files: Cow::Borrowed(ESM),
|
||||||
esm_entry_point: Some("ext:nix_runtime/runtime.js"),
|
esm_entry_point: Some("ext:nix_runtime/runtime.js"),
|
||||||
ops: Cow::Borrowed(OPS),
|
ops: Cow::Owned(ops),
|
||||||
op_state_fn: Some(Box::new(move |state| {
|
|
||||||
state.put(ctx);
|
|
||||||
})),
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@@ -67,9 +69,11 @@ use private::NixError;
|
|||||||
|
|
||||||
#[deno_core::op2]
|
#[deno_core::op2]
|
||||||
#[string]
|
#[string]
|
||||||
fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result<String, NixError> {
|
fn op_import<Ctx: RuntimeCtx>(
|
||||||
let ptr = state.borrow_mut::<CtxPtr>();
|
state: &mut OpState,
|
||||||
let ctx = unsafe { ptr.as_mut() };
|
#[string] path: String,
|
||||||
|
) -> std::result::Result<String, NixError> {
|
||||||
|
let ctx = state.borrow_mut::<Ctx>();
|
||||||
|
|
||||||
let current_dir = ctx.get_current_dir();
|
let current_dir = ctx.get_current_dir();
|
||||||
let mut absolute_path = current_dir
|
let mut absolute_path = current_dir
|
||||||
@@ -80,30 +84,13 @@ fn op_import(state: &mut OpState, #[string] path: String) -> std::result::Result
|
|||||||
absolute_path.push("default.nix")
|
absolute_path.push("default.nix")
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut guard = PathDropGuard::new(absolute_path.clone(), ctx);
|
|
||||||
let ctx = guard.as_ctx();
|
|
||||||
|
|
||||||
let content = std::fs::read_to_string(&absolute_path)
|
let content = std::fs::read_to_string(&absolute_path)
|
||||||
.map_err(|e| format!("Failed to read {}: {}", absolute_path.display(), e))?;
|
.map_err(|e| format!("Failed to read {}: {}", absolute_path.display(), e))?;
|
||||||
|
|
||||||
let root = rnix::Root::parse(&content);
|
let mut guard = ctx.push_path_stack(absolute_path);
|
||||||
if !root.errors().is_empty() {
|
let ctx = guard.deref_mut();
|
||||||
return Err(format!(
|
|
||||||
"Parse error in {}: {:?}",
|
|
||||||
absolute_path.display(),
|
|
||||||
root.errors()
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let expr = root.tree().expr().ok_or("No expression in file")?;
|
Ok(ctx.compile_code(&content).map_err(|err| err.to_string())?)
|
||||||
let expr_id = ctx
|
|
||||||
.as_mut()
|
|
||||||
.downgrade_ctx()
|
|
||||||
.downgrade(expr)
|
|
||||||
.map_err(|e| format!("Downgrade error: {}", e))?;
|
|
||||||
|
|
||||||
Ok(ctx.get_ir(expr_id).compile(Pin::get_ref(ctx.as_ref())))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[deno_core::op2]
|
#[deno_core::op2]
|
||||||
@@ -119,12 +106,11 @@ fn op_path_exists(#[string] path: String) -> bool {
|
|||||||
|
|
||||||
#[deno_core::op2]
|
#[deno_core::op2]
|
||||||
#[string]
|
#[string]
|
||||||
fn op_resolve_path(
|
fn op_resolve_path<Ctx: RuntimeCtx>(
|
||||||
state: &mut OpState,
|
state: &mut OpState,
|
||||||
#[string] path: String,
|
#[string] path: String,
|
||||||
) -> std::result::Result<String, NixError> {
|
) -> std::result::Result<String, NixError> {
|
||||||
let ptr = state.borrow::<CtxPtr>();
|
let ctx = state.borrow::<Ctx>();
|
||||||
let ctx = unsafe { ptr.as_ref() };
|
|
||||||
|
|
||||||
// If already absolute, return as-is
|
// If already absolute, return as-is
|
||||||
if path.starts_with('/') {
|
if path.starts_with('/') {
|
||||||
@@ -141,14 +127,15 @@ fn op_resolve_path(
|
|||||||
.map_err(|e| format!("Failed to resolve path {}: {}", path, e))?)
|
.map_err(|e| format!("Failed to resolve path {}: {}", path, e))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Runtime {
|
pub(crate) struct Runtime<Ctx: RuntimeCtx> {
|
||||||
js_runtime: JsRuntime,
|
js_runtime: JsRuntime,
|
||||||
is_thunk_symbol: v8::Global<v8::Symbol>,
|
is_thunk_symbol: v8::Global<v8::Symbol>,
|
||||||
primop_metadata_symbol: v8::Global<v8::Symbol>,
|
primop_metadata_symbol: v8::Global<v8::Symbol>,
|
||||||
|
_marker: PhantomData<Ctx>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Runtime {
|
impl<Ctx: RuntimeCtx> Runtime<Ctx> {
|
||||||
pub(crate) fn new(ctx: CtxPtr) -> Result<Self> {
|
pub(crate) fn new() -> Result<Self> {
|
||||||
// Initialize V8 once
|
// Initialize V8 once
|
||||||
static INIT: Once = Once::new();
|
static INIT: Once = Once::new();
|
||||||
INIT.call_once(|| {
|
INIT.call_once(|| {
|
||||||
@@ -159,7 +146,7 @@ impl Runtime {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut js_runtime = JsRuntime::new(RuntimeOptions {
|
let mut js_runtime = JsRuntime::new(RuntimeOptions {
|
||||||
extensions: vec![runtime_extension(ctx)],
|
extensions: vec![runtime_extension::<Ctx>()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -172,10 +159,13 @@ impl Runtime {
|
|||||||
js_runtime,
|
js_runtime,
|
||||||
is_thunk_symbol,
|
is_thunk_symbol,
|
||||||
primop_metadata_symbol,
|
primop_metadata_symbol,
|
||||||
|
_marker: PhantomData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn eval(&mut self, script: String) -> Result<Value> {
|
pub(crate) fn eval(&mut self, script: String, ctx: Ctx) -> Result<Value> {
|
||||||
|
self.js_runtime.op_state().borrow_mut().put(ctx);
|
||||||
|
|
||||||
let global_value = self
|
let global_value = self
|
||||||
.js_runtime
|
.js_runtime
|
||||||
.execute_script("<eval>", script)
|
.execute_script("<eval>", script)
|
||||||
|
|||||||
65
nix-js/tests/basic_eval.rs
Normal file
65
nix-js/tests/basic_eval.rs
Normal file
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
149
nix-js/tests/builtins.rs
Normal file
149
nix-js/tests/builtins.rs
Normal file
@@ -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)
|
||||||
|
]))
|
||||||
|
);
|
||||||
|
}
|
||||||
75
nix-js/tests/free_globals.rs
Normal file
75
nix-js/tests/free_globals.rs
Normal file
@@ -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()));
|
||||||
|
}
|
||||||
121
nix-js/tests/functions.rs
Normal file
121
nix-js/tests/functions.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
102
nix-js/tests/io_operations.rs
Normal file
102
nix-js/tests/io_operations.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
139
nix-js/tests/numeric_types.rs
Normal file
139
nix-js/tests/numeric_types.rs
Normal file
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
101
nix-js/tests/operators.rs
Normal file
101
nix-js/tests/operators.rs
Normal file
@@ -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));
|
||||||
|
}
|
||||||
120
nix-js/tests/scc_optimization.rs
Normal file
120
nix-js/tests/scc_optimization.rs
Normal file
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
251
nix-js/tests/to_string.rs
Normal file
251
nix-js/tests/to_string.rs
Normal file
@@ -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())
|
||||||
|
);
|
||||||
|
}
|
||||||
12
nix-js/tests/utils.rs
Normal file
12
nix-js/tests/utils.rs
Normal file
@@ -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<Value, nix_js::error::Error> {
|
||||||
|
Context::new().unwrap().eval_code(expr)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user