├── .cargo └── config.toml ├── .envrc ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .vscode └── settings.json ├── CONTRIBUTE.md ├── Cargo.lock ├── Cargo.toml ├── README.md ├── flake.lock ├── flake.nix ├── nixjs-rt ├── .prettierignore ├── .prettierrc ├── CONTRIBUTE.md ├── README.md ├── jest.config.json ├── package-lock.json ├── package.json ├── pkg.nix ├── scripts │ ├── check-nix-pkg.sh │ ├── check-npm-deps-hash.sh │ ├── check-npm.sh │ └── update-npm-deps-hash.sh ├── src │ ├── builtins.ts │ ├── errors │ │ ├── abort.ts │ │ ├── attribute.ts │ │ ├── errorMessage.ts │ │ ├── function.ts │ │ ├── index.ts │ │ ├── other.ts │ │ ├── typeError.ts │ │ └── variable.ts │ ├── globals.d.ts │ ├── legacyTests.test.ts │ ├── lib.ts │ ├── testUtils.ts │ └── utils.ts └── tsconfig.json ├── rust-toolchain.toml ├── src ├── cmd │ ├── eval.rs │ ├── mod.rs │ └── transpile.rs ├── eval │ ├── emit_js.rs │ ├── error.rs │ ├── execution.rs │ ├── helpers.rs │ ├── mod.rs │ └── types.rs ├── lib.rs ├── main.rs └── tests │ ├── attr_set.rs │ ├── builtins.rs │ ├── import_tests │ ├── basic.nix │ ├── child-folder-import.nix │ ├── nested │ │ ├── basic.nix │ │ └── parent-folder-import.nix │ └── same-folder-import.nix │ ├── lambda.rs │ ├── literals.rs │ ├── mod.rs │ └── operators.rs └── tests ├── cmd ├── eval.rs ├── mod.rs └── transpile.rs └── mod.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [registries.crates-io] 2 | protocol = "sparse" 3 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | nix profile wipe-history --profile "$(direnv_layout_dir)/flake-profile" --older-than 14d 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: builder 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-22.04 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: DeterminateSystems/nix-installer-action@main 10 | - uses: DeterminateSystems/magic-nix-cache-action@main 11 | - uses: DeterminateSystems/flake-checker-action@main 12 | 13 | - name: Cache Rust Artifacts 14 | uses: actions/cache@v4 15 | env: 16 | cache-name: cache-rust-artifacts 17 | with: 18 | path: | 19 | /home/runner/.rustup 20 | /home/runner/.cargo 21 | target 22 | key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.toml', '**/Cargo.lock', '**/.cargo/config.toml', '**/rust-toolchain.toml', '**/flake.nix', '**/flake.lock') }} 23 | 24 | - name: nixjs-rt 25 | run: | 26 | cd nixjs-rt 27 | eval "$(nix print-dev-env)" 28 | 29 | parallel --line-buffer --ctagstring "{}>\033[0m" scripts/{} ::: \ 30 | check-nix-pkg.sh \ 31 | check-npm-deps-hash.sh \ 32 | check-npm.sh 33 | 34 | - name: Build 35 | run: | 36 | eval "$(nix print-dev-env)" 37 | 38 | set -x 39 | cargo fmt --check 40 | cargo clippy -- --deny "warnings" 41 | cargo test 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.direnv 3 | 4 | # nixjs-rt 5 | nixjs-rt/coverage 6 | nixjs-rt/dist 7 | nixjs-rt/node_modules -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.watcherExclude": { 4 | ".direnv/**": true 5 | }, 6 | "search.exclude": { 7 | ".direnv/**": true 8 | }, 9 | "cSpell.words": ["rnix"], 10 | "editor.tabSize": 4, 11 | "rust-analyzer.check.overrideCommand": [ 12 | "cargo", 13 | "clippy", 14 | "--all", 15 | "--message-format=json", 16 | "--all-targets" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # First-time dev env set up 2 | 3 | ## Shell 4 | 5 | 1. Install the nix package manager: 6 | https://nixos.org/manual/nix/stable/installation/installation.html 7 | 8 | 2. Install direnv: https://direnv.net/ 9 | 10 | Once you enter this directory in your shell, the rust tooling should be 11 | automatically set up. You can verify this with: 12 | 13 | ```bash 14 | # This should match the version specified in `rust-toolchain.toml` 15 | cargo --version 16 | ``` 17 | 18 | Rix uses `nixrt` (a JavaScript library), which is located in the `nixrt-rt` folder 19 | in this repository. It needs to be built before `rix` can be run. 20 | 21 | ## Editor 22 | 23 | You must follow the "Shell" instructions above to make sure the `.direnv` folder 24 | is populated. After that all the needed tooling will be in the `PATH`. 25 | 26 | Your editor should pick up the Rust toolchain as specified in 27 | `rust-toolchain.toml`. 28 | 29 | ### VSCode 30 | 31 | Install the [direnv vscode extension](https://github.com/direnv/direnv-vscode). 32 | 33 | # Build, Test, and Iterate 34 | 35 | In the first shell you can continuously build `nixjs-rt` (the Nix JavaScript run-time library): 36 | 37 | ```bash 38 | cd nixjs-rt 39 | npm ci 40 | npm run build-watch 41 | ``` 42 | 43 | In the second shell continuously test `nixjs-rt`: 44 | 45 | ```bash 46 | cd nixjs-rt 47 | npm run test-watch 48 | ``` 49 | 50 | In the third shell continuously check and test `rix`: 51 | 52 | ```bash 53 | cargo-watch -x clippy -x test 54 | ``` 55 | 56 | # Run `rix` 57 | 58 | First build nixjs-rt: 59 | 60 | ```bash 61 | cd nixjs-rt 62 | npm ci 63 | npm run build-watch 64 | ``` 65 | 66 | Now run `rix` in debug mode: 67 | 68 | ```bash 69 | cargo run -- --help 70 | cargo run -- eval --expr '1 + 1' 71 | ``` 72 | 73 | ## Updating dependencies 74 | 75 | Update tools like `rustup`, `npm`, and other dependencies: 76 | 77 | ```bash 78 | nix flake update 79 | ``` 80 | 81 | Update the version of Rust: 82 | 83 | 1. find the latest version on https://www.rust-lang.org/ 84 | 2. Replace the old version of rust in 85 | [`rust-toolchain.toml`](./rust-toolchain.toml) with the new version. 86 | 87 | Update Rust dependencies: 88 | 89 | ```bash 90 | cargo update 91 | ``` 92 | 93 | Update JavaScript dependencies: 94 | 95 | ```bash 96 | cd nixjs-rt 97 | npm update 98 | ``` 99 | 100 | # Troubleshooting 101 | 102 | ## Getting a cargo error after an update 103 | 104 | If you're seeing an error like this: 105 | 106 | ``` 107 | error: the 'cargo' binary, normally provided by the 'cargo' component, is not applicable to the '' toolchain 108 | ``` 109 | 110 | Then run the following to fix it: 111 | 112 | ```bash 113 | rustup toolchain uninstall 114 | ``` 115 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustnix" 3 | description = "Nix language interpreter" 4 | documentation = "https://github.com/urbas/rix/blob/master/README.md" 5 | edition = "2021" 6 | homepage = "https://github.com/urbas/rix" 7 | license = "MIT" 8 | repository = "https://github.com/urbas/rix" 9 | version = "0.0.1" 10 | 11 | [lib] 12 | name = "rix" 13 | path = "src/lib.rs" 14 | 15 | [[bin]] 16 | name = "rix" 17 | path = "src/main.rs" 18 | 19 | [dev-dependencies] 20 | assert_cmd = "2.0.5" 21 | predicates = "3" 22 | 23 | [dependencies] 24 | clap = "4.0.18" 25 | colored = "2.0.0" 26 | rowan = "0" 27 | rnix = "0" 28 | deno_core = "0" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rix 2 | 3 | [![builder](https://github.com/urbas/rix/actions/workflows/build.yml/badge.svg)](https://github.com/urbas/rix/actions/workflows/build.yml) 4 | 5 | Nix language interpreter. 6 | 7 | # Trying it out 8 | 9 | Currently `rix` is not published anywhere, so you'll have to build it yourself. 10 | Please follow instructions in [`CONTRIBUTE.md`](./CONTRIBUTE.md) on how to build 11 | and run `rix`. 12 | 13 | Keep in mind that `rix` is still in development and many features are not yet 14 | implemented. 15 | 16 | # Notable design choices 17 | 18 | Rix transpiles Nix expressions to JavaScript and evaluates them with V8. The idea 19 | is to leverage all the great work in the JS ecosystem (such as debuggers, 20 | fast JIT compilers, profilers, libraries, compiled code caching, and source 21 | mapping just to name a few). 22 | 23 | # Progress 24 | 25 | - 🌕 stage 0: evaluate basic expressions, rec attrsets, let bindings, `with` 26 | statement, functions 27 | 28 | - 🌕 stage 1: lazy evaluation 29 | 30 | - 🌘 stage 2: 31 | 32 | - 🌘 built-in functions (progress: 7 out of 113) 33 | - 🌑 derivations (hello world derivation) 34 | 35 | - 🌑 stage 3: full implementation (all derivations in nixpkgs, nice error 36 | messages, etc.) 37 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1720181791, 6 | "narHash": "sha256-i4vJL12/AdyuQuviMMd1Hk2tsGt02hDNhA0Zj1m16N8=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "4284c2b73c8bce4b46a6adf23e16d9e2ec8da4bb", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "id": "nixpkgs", 14 | "ref": "nixpkgs-unstable", 15 | "type": "indirect" 16 | } 17 | }, 18 | "root": { 19 | "inputs": { 20 | "nixpkgs": "nixpkgs" 21 | } 22 | } 23 | }, 24 | "root": "root", 25 | "version": 7 26 | } 27 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A reimplementation or nix in Rust."; 3 | 4 | inputs.nixpkgs.url = "nixpkgs/nixpkgs-unstable"; 5 | 6 | outputs = { self, nixpkgs }: 7 | let 8 | supportedSystems = [ "x86_64-linux" "aarch64-linux" ]; 9 | forSupportedSystems = f: with nixpkgs.lib; foldl' (resultAttrset: system: recursiveUpdate resultAttrset (f { inherit system; pkgs = import nixpkgs { inherit system; }; })) { } supportedSystems; 10 | 11 | in 12 | forSupportedSystems ({ pkgs, system, ... }: 13 | let 14 | rix-deps = with pkgs; [ 15 | busybox-sandbox-shell 16 | coreutils 17 | cargo-watch 18 | nix 19 | nixpkgs-fmt 20 | rustup 21 | ]; 22 | 23 | nixjs-rt-deps = with pkgs; [ 24 | nodejs 25 | parallel 26 | prefetch-npm-deps 27 | ]; 28 | 29 | nixjs-rt = import ./nixjs-rt/pkg.nix { inherit pkgs; self = "${self}/nixjs-rt"; }; 30 | 31 | in 32 | { 33 | packages.${system} = { inherit nixjs-rt pkgs; }; 34 | devShells.${system}.default = pkgs.stdenv.mkDerivation { 35 | name = "rix"; 36 | buildInputs = rix-deps ++ nixjs-rt-deps; 37 | }; 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /nixjs-rt/.prettierignore: -------------------------------------------------------------------------------- 1 | /.direnv 2 | /node_modules 3 | /dist 4 | package-lock.json 5 | /.vscode -------------------------------------------------------------------------------- /nixjs-rt/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all" 3 | } 4 | -------------------------------------------------------------------------------- /nixjs-rt/CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # Building & Testing 2 | 3 | ```bash 4 | # Install dependencies: 5 | npm ci 6 | 7 | # Building: 8 | # This will build TypeScript srouces from `src/*.ts` and place the resulting files into the `dist` folder 9 | npm run build 10 | # Same as `build`, but will watch changes in the `src` folder and continuously update the `dist` folder. 11 | npm run build-watch 12 | 13 | # Testing: 14 | # This runs all tests once 15 | npm run test 16 | # This runs tests continuously every time sources in the `src` folder change 17 | npm run test-watch 18 | 19 | # Formatting: 20 | npm run fmt 21 | ``` 22 | 23 | # Debugging 24 | 25 | 1. Use the `Debug: JavaScript Debug Terminal` action and VSCode will open a new terminal. 26 | 2. Now set a breakpoint somewhere in your code. 27 | 3. Run tests with `npm run test` and VSCode will break at the given breakpoint. 28 | 29 | ## Updating dependencies 30 | 31 | Update the version of NodeJS: 32 | 33 | ```bash 34 | nix flake update 35 | ``` 36 | 37 | Update the version of JavaScript dependencies: 38 | 39 | ```bash 40 | npm update 41 | ``` 42 | 43 | Finally, update the dependencies hash: 44 | 45 | ```bash 46 | scripts/update-npm-deps-hash.sh 47 | ``` 48 | -------------------------------------------------------------------------------- /nixjs-rt/README.md: -------------------------------------------------------------------------------- 1 | # nixjs-rt 2 | 3 | Nix JavaScript Run-time, an implementation of nix language semantics in TypeScript. 4 | 5 | This library is intended for use in transpiling the Nix language to JavaScript. 6 | -------------------------------------------------------------------------------- /nixjs-rt/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node", 4 | "modulePathIgnorePatterns": ["dist"] 5 | } 6 | -------------------------------------------------------------------------------- /nixjs-rt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nixjs-rt", 3 | "version": "0.0.1", 4 | "description": "A library that implements Nix language semantics in JS.", 5 | "main": "dist/lib.mjs", 6 | "scripts": { 7 | "build": "tsup ./src/lib.ts --format esm --sourcemap", 8 | "build-watch": "tsup ./src/lib.ts --format esm --sourcemap --watch", 9 | "fmt": "prettier --write .", 10 | "fmt-check": "prettier --check .", 11 | "test": "jest --color", 12 | "test-watch": "jest --color --watch" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/urbas/nixjs-rt.git" 17 | }, 18 | "author": "", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/urbas/nixjs-rt/issues" 22 | }, 23 | "homepage": "https://github.com/urbas/nixjs-rt#readme", 24 | "devDependencies": { 25 | "@jest/globals": "29.7.0", 26 | "jest": "29.7.0", 27 | "prettier": "3.2.5", 28 | "ts-jest": "29.1.2", 29 | "tsup": "^8.0.2", 30 | "typescript": "5.3.3" 31 | }, 32 | "files": [ 33 | "dist/**" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /nixjs-rt/pkg.nix: -------------------------------------------------------------------------------- 1 | { self, pkgs }: 2 | 3 | let 4 | 5 | inherit (pkgs) buildNpmPackage; 6 | 7 | in 8 | buildNpmPackage rec { 9 | name = "nixjs-rt"; 10 | src = self; 11 | npmDepsHash = "sha256-ev5i6sL2mQgxu7kuLVaMIJfUVEXcP27n8u2RiGE0Cd8="; 12 | } 13 | -------------------------------------------------------------------------------- /nixjs-rt/scripts/check-nix-pkg.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | outLink=$(mktemp -d)/result 6 | nix build --out-link $outLink ..#nixjs-rt 7 | 8 | function expectFile() { 9 | local theFile=$1 10 | [ -f "$theFile" ] \ 11 | && ( echo "✅ The file '$theFile' exists." ) \ 12 | || ( echo "❌ Could not find the file '$theFile'." && exit 1 ) 13 | } 14 | 15 | expectFile $outLink/lib/node_modules/nixjs-rt/dist/lib.mjs 16 | expectFile $outLink/lib/node_modules/nixjs-rt/dist/lib.mjs.map 17 | -------------------------------------------------------------------------------- /nixjs-rt/scripts/check-npm-deps-hash.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | expectedHash=$(prefetch-npm-deps package-lock.json 2> /dev/null) 4 | 5 | grep -q --fixed-string "$expectedHash" pkg.nix \ 6 | && ( echo "✅ The 'npmDepsHash' attribute in 'pkg.nix' is up-to-date." ) \ 7 | || ( echo "❌ The 'npmDepsHash' attribute in 'pkg.nix' is not up-to-date (expected: '$expectedHash')." && exit 1 ) 8 | -------------------------------------------------------------------------------- /nixjs-rt/scripts/check-npm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | npm ci 6 | npm run fmt-check 7 | npm run test 8 | npm run build 9 | -------------------------------------------------------------------------------- /nixjs-rt/scripts/update-npm-deps-hash.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | newHash=$(prefetch-npm-deps package-lock.json 2> /dev/null) 4 | 5 | sed -i "s,npmDepsHash = \".*\";,npmDepsHash = \"$newHash\";," pkg.nix 6 | -------------------------------------------------------------------------------- /nixjs-rt/src/builtins.ts: -------------------------------------------------------------------------------- 1 | import { err, errType, errTypes, highlighted } from "./errors"; 2 | import { abortError } from "./errors/abort"; 3 | import { otherError } from "./errors/other"; 4 | import { typeMismatchError } from "./errors/typeError"; 5 | import { 6 | Attrset, 7 | EvalCtx, 8 | FALSE, 9 | Lambda, 10 | NULL, 11 | NixBool, 12 | NixFloat, 13 | NixInt, 14 | NixList, 15 | NixNull, 16 | NixString, 17 | NixType, 18 | NixTypeClass, 19 | Path, 20 | TRUE, 21 | nixBoolFromJs, 22 | } from "./lib"; 23 | import { dirOf, isAbsolutePath, normalizePath } from "./utils"; 24 | 25 | type BuiltinsRecord = Record NixType>; 26 | 27 | function builtinBasicTypeMismatchError( 28 | fnName: string, 29 | got: NixType, 30 | expects: NixTypeClass | NixTypeClass[], 31 | ) { 32 | if (!Array.isArray(expects)) { 33 | expects = [expects]; 34 | } 35 | 36 | return typeMismatchError( 37 | got, 38 | expects, 39 | err`${fnName} expects ${errTypes(...expects)}, got ${errType(got)}.`, 40 | ); 41 | } 42 | 43 | export function getBuiltins() { 44 | // Builtins are sorted by the order they appear in the Nix manual 45 | // https://nixos.org/manual/nix/stable/language/builtins.html 46 | 47 | const builtins: BuiltinsRecord = { 48 | derivation: (arg) => { 49 | throw new Error("unimplemented"); 50 | }, 51 | 52 | abort: (message) => { 53 | throw abortError(message.asString()); 54 | }, 55 | 56 | add: (lhs) => { 57 | return new Lambda((rhs) => { 58 | let lhsStrict = lhs.toStrict(); 59 | if (!(lhsStrict instanceof NixInt || lhsStrict instanceof NixFloat)) { 60 | let expected = [NixInt, NixFloat]; 61 | throw builtinBasicTypeMismatchError("add", lhsStrict, expected); 62 | } 63 | let rhsStrict = rhs.toStrict(); 64 | if (!(rhsStrict instanceof NixInt || rhsStrict instanceof NixFloat)) { 65 | let expected = [NixInt, NixFloat]; 66 | throw builtinBasicTypeMismatchError("add", rhsStrict, expected); 67 | } 68 | return lhsStrict.add(rhsStrict); 69 | }); 70 | }, 71 | 72 | addDrvOutputDependencies: (arg) => { 73 | throw new Error("unimplemented"); 74 | }, 75 | 76 | all: (pred) => { 77 | const lambdaStrict = pred.toStrict(); 78 | if (!(lambdaStrict instanceof Lambda)) { 79 | throw builtinBasicTypeMismatchError("all", lambdaStrict, Lambda); 80 | } 81 | 82 | return new Lambda((list) => { 83 | const listStrict = list.toStrict(); 84 | if (!(listStrict instanceof NixList)) { 85 | throw builtinBasicTypeMismatchError("all", listStrict, NixList); 86 | } 87 | 88 | for (const element of listStrict.values) { 89 | const result = lambdaStrict.apply(element); 90 | if (!result.asBoolean()) { 91 | return FALSE; 92 | } 93 | } 94 | 95 | return TRUE; 96 | }); 97 | }, 98 | 99 | any: (pred) => { 100 | const lambdaStrict = pred.toStrict(); 101 | if (!(lambdaStrict instanceof Lambda)) { 102 | throw builtinBasicTypeMismatchError("any", lambdaStrict, Lambda); 103 | } 104 | 105 | return new Lambda((list) => { 106 | const listStrict = list.toStrict(); 107 | if (!(listStrict instanceof NixList)) { 108 | throw builtinBasicTypeMismatchError("any", listStrict, NixList); 109 | } 110 | 111 | for (const element of listStrict.values) { 112 | const result = lambdaStrict.apply(element); 113 | if (result.asBoolean()) { 114 | return TRUE; 115 | } 116 | } 117 | 118 | return FALSE; 119 | }); 120 | }, 121 | 122 | attrNames: (attrset) => { 123 | const attrsetStrict = attrset.toStrict(); 124 | if (!(attrsetStrict instanceof Attrset)) { 125 | throw builtinBasicTypeMismatchError( 126 | "attrNames", 127 | attrsetStrict, 128 | Attrset, 129 | ); 130 | } 131 | 132 | const keys = Array.from(attrsetStrict.keys()); 133 | keys.sort(); 134 | 135 | return new NixList(keys.map((key) => new NixString(key))); 136 | }, 137 | 138 | attrValues: (attrset) => { 139 | const attrsetStrict = attrset.toStrict(); 140 | if (!(attrsetStrict instanceof Attrset)) { 141 | throw builtinBasicTypeMismatchError( 142 | "attrValues", 143 | attrsetStrict, 144 | Attrset, 145 | ); 146 | } 147 | 148 | const keys = Array.from(attrsetStrict.keys()); 149 | keys.sort(); 150 | 151 | return new NixList( 152 | keys.map((key) => attrset.select([new NixString(key)], NULL)), 153 | ); 154 | }, 155 | 156 | baseNameOf: (path) => { 157 | // Can take a string or path 158 | const pathStrict = path.toStrict(); 159 | if (!(pathStrict instanceof Path || pathStrict instanceof NixString)) { 160 | const expected = [Path, NixString]; 161 | throw builtinBasicTypeMismatchError("baseNameOf", pathStrict, expected); 162 | } 163 | 164 | let pathValue = pathStrict.toJs(); 165 | if (pathValue.endsWith("/")) { 166 | pathValue = pathValue.slice(0, -1); 167 | } 168 | 169 | const parts = pathValue.split("/"); 170 | return new NixString(parts[parts.length - 1]); 171 | }, 172 | 173 | bitAnd: (arg) => { 174 | throw new Error("unimplemented"); 175 | }, 176 | 177 | bitOr: (arg) => { 178 | throw new Error("unimplemented"); 179 | }, 180 | 181 | bitXor: (arg) => { 182 | throw new Error("unimplemented"); 183 | }, 184 | 185 | break: (arg) => { 186 | throw new Error("unimplemented"); 187 | }, 188 | 189 | catAttrs: (arg) => { 190 | throw new Error("unimplemented"); 191 | }, 192 | 193 | ceil: (arg) => { 194 | throw new Error("unimplemented"); 195 | }, 196 | 197 | compareVersions: (arg) => { 198 | throw new Error("unimplemented"); 199 | }, 200 | 201 | concatLists: (arg) => { 202 | throw new Error("unimplemented"); 203 | }, 204 | 205 | concatMap: (arg) => { 206 | throw new Error("unimplemented"); 207 | }, 208 | 209 | concatStringsSep: (arg) => { 210 | throw new Error("unimplemented"); 211 | }, 212 | 213 | convertHash: (arg) => { 214 | throw new Error("unimplemented"); 215 | }, 216 | 217 | deepSeq: (arg) => { 218 | throw new Error("unimplemented"); 219 | }, 220 | 221 | dirOf: (arg) => { 222 | throw new Error("unimplemented"); 223 | }, 224 | 225 | div: (arg) => { 226 | throw new Error("unimplemented"); 227 | }, 228 | 229 | elem: (arg) => { 230 | throw new Error("unimplemented"); 231 | }, 232 | 233 | elemAt: (arg) => { 234 | throw new Error("unimplemented"); 235 | }, 236 | 237 | fetchClosure: (arg) => { 238 | throw new Error("unimplemented"); 239 | }, 240 | 241 | fetchGit: (arg) => { 242 | throw new Error("unimplemented"); 243 | }, 244 | 245 | fetchTarball: (arg) => { 246 | throw new Error("unimplemented"); 247 | }, 248 | 249 | fetchTree: (arg) => { 250 | throw new Error("unimplemented"); 251 | }, 252 | 253 | fetchurl: (arg) => { 254 | throw new Error("unimplemented"); 255 | }, 256 | 257 | filter: (arg) => { 258 | throw new Error("unimplemented"); 259 | }, 260 | 261 | filterSource: (arg) => { 262 | throw new Error("unimplemented"); 263 | }, 264 | 265 | findFile: (arg) => { 266 | throw new Error("unimplemented"); 267 | }, 268 | 269 | flakeRefToString: (arg) => { 270 | throw new Error("unimplemented"); 271 | }, 272 | 273 | floor: (arg) => { 274 | throw new Error("unimplemented"); 275 | }, 276 | 277 | foldl: (arg) => { 278 | throw new Error("unimplemented"); 279 | }, 280 | 281 | fromJSON: (arg) => { 282 | throw new Error("unimplemented"); 283 | }, 284 | 285 | fromTOML: (arg) => { 286 | throw new Error("unimplemented"); 287 | }, 288 | 289 | functionArgs: (arg) => { 290 | throw new Error("unimplemented"); 291 | }, 292 | 293 | genList: (arg) => { 294 | throw new Error("unimplemented"); 295 | }, 296 | 297 | genericClosure: (arg) => { 298 | throw new Error("unimplemented"); 299 | }, 300 | 301 | getAttr: (arg) => { 302 | throw new Error("unimplemented"); 303 | }, 304 | 305 | getContext: (arg) => { 306 | throw new Error("unimplemented"); 307 | }, 308 | 309 | getEnv: (arg) => { 310 | throw new Error("unimplemented"); 311 | }, 312 | 313 | getFlake: (arg) => { 314 | throw new Error("unimplemented"); 315 | }, 316 | 317 | groupBy: (arg) => { 318 | throw new Error("unimplemented"); 319 | }, 320 | 321 | hasAttr: (arg) => { 322 | throw new Error("unimplemented"); 323 | }, 324 | 325 | hasContext: (arg) => { 326 | throw new Error("unimplemented"); 327 | }, 328 | 329 | hashFile: (arg) => { 330 | throw new Error("unimplemented"); 331 | }, 332 | 333 | hashString: (arg) => { 334 | throw new Error("unimplemented"); 335 | }, 336 | 337 | head: (list) => { 338 | const listStrict = list.toStrict(); 339 | if (!(listStrict instanceof NixList)) { 340 | throw typeMismatchError( 341 | listStrict, 342 | NixList, 343 | err`Cannot apply the 'head' function on '${errType(listStrict)}', expected ${errType(NixList)}.`, 344 | ); 345 | } 346 | if (listStrict.values.length === 0) { 347 | throw otherError( 348 | "Cannot fetch the first element in an empty list.", 349 | "builtins-head-on-empty-list", 350 | ); 351 | } 352 | return listStrict.values[0]; 353 | }, 354 | 355 | import: (path) => { 356 | const pathStrict = path.toStrict(); 357 | 358 | if (!(pathStrict instanceof Path || pathStrict instanceof NixString)) { 359 | const expected = [Path, NixString]; 360 | throw builtinBasicTypeMismatchError("import", pathStrict, expected); 361 | } 362 | 363 | let pathValue = ""; 364 | if (pathStrict instanceof NixString) { 365 | pathValue = normalizePath(pathStrict.toJs()); 366 | } else if (pathStrict instanceof Path) { 367 | pathValue = pathStrict.toJs(); 368 | } 369 | 370 | // Check if it's an absolute path. Relative paths are not allowed. 371 | // Path data types are always automatically absolute. 372 | if (!isAbsolutePath(pathValue)) { 373 | throw otherError( 374 | err`string ${highlighted(JSON.stringify(pathValue))} doesn't represent an absolute path. Only absolute paths are allowed for imports.`, 375 | "builtins-import-non-absolute-path", 376 | ); 377 | } 378 | 379 | const resultingFn = importNixModule(pathValue); 380 | 381 | const newCtx = new EvalCtx(dirOf(pathValue)); 382 | return resultingFn(newCtx); 383 | }, 384 | 385 | intersectAttrs: (arg) => { 386 | throw new Error("unimplemented"); 387 | }, 388 | 389 | isAttrs: (arg) => { 390 | return nixBoolFromJs(arg instanceof Attrset); 391 | }, 392 | 393 | isBool: (arg) => { 394 | return nixBoolFromJs(arg instanceof NixBool); 395 | }, 396 | 397 | isFloat: (arg) => { 398 | return nixBoolFromJs(arg instanceof NixFloat); 399 | }, 400 | 401 | isFunction: (arg) => { 402 | return nixBoolFromJs(arg instanceof Lambda); 403 | }, 404 | 405 | isInt: (arg) => { 406 | return nixBoolFromJs(arg instanceof NixInt); 407 | }, 408 | 409 | isList: (arg) => { 410 | return nixBoolFromJs(arg instanceof NixList); 411 | }, 412 | 413 | isNull: (arg) => { 414 | return nixBoolFromJs(arg instanceof NixNull); 415 | }, 416 | 417 | isPath: (arg) => { 418 | return nixBoolFromJs(arg instanceof Path); 419 | }, 420 | 421 | isString: (arg) => { 422 | return nixBoolFromJs(arg instanceof NixString); 423 | }, 424 | 425 | length: (arg) => { 426 | throw new Error("unimplemented"); 427 | }, 428 | 429 | lessThan: (arg) => { 430 | throw new Error("unimplemented"); 431 | }, 432 | 433 | listToAttrs: (arg) => { 434 | throw new Error("unimplemented"); 435 | }, 436 | 437 | map: (arg) => { 438 | throw new Error("unimplemented"); 439 | }, 440 | 441 | mapAttrs: (arg) => { 442 | throw new Error("unimplemented"); 443 | }, 444 | 445 | match: (arg) => { 446 | throw new Error("unimplemented"); 447 | }, 448 | 449 | mul: (arg) => { 450 | throw new Error("unimplemented"); 451 | }, 452 | 453 | outputOf: (arg) => { 454 | throw new Error("unimplemented"); 455 | }, 456 | 457 | parseDrvName: (arg) => { 458 | throw new Error("unimplemented"); 459 | }, 460 | 461 | parseFlakeRef: (arg) => { 462 | throw new Error("unimplemented"); 463 | }, 464 | 465 | partition: (arg) => { 466 | throw new Error("unimplemented"); 467 | }, 468 | 469 | path: (arg) => { 470 | throw new Error("unimplemented"); 471 | }, 472 | 473 | pathExists: (arg) => { 474 | throw new Error("unimplemented"); 475 | }, 476 | 477 | placeholder: (arg) => { 478 | throw new Error("unimplemented"); 479 | }, 480 | 481 | readDir: (arg) => { 482 | throw new Error("unimplemented"); 483 | }, 484 | 485 | readFile: (arg) => { 486 | throw new Error("unimplemented"); 487 | }, 488 | 489 | readFileType: (arg) => { 490 | throw new Error("unimplemented"); 491 | }, 492 | 493 | removeAttrs: (arg) => { 494 | throw new Error("unimplemented"); 495 | }, 496 | 497 | replaceStrings: (arg) => { 498 | throw new Error("unimplemented"); 499 | }, 500 | 501 | seq: (arg) => { 502 | throw new Error("unimplemented"); 503 | }, 504 | 505 | sort: (arg) => { 506 | throw new Error("unimplemented"); 507 | }, 508 | 509 | split: (arg) => { 510 | throw new Error("unimplemented"); 511 | }, 512 | 513 | splitVersion: (arg) => { 514 | throw new Error("unimplemented"); 515 | }, 516 | 517 | storePath: (arg) => { 518 | throw new Error("unimplemented"); 519 | }, 520 | 521 | stringLength: (arg) => { 522 | throw new Error("unimplemented"); 523 | }, 524 | 525 | sub: (arg) => { 526 | throw new Error("unimplemented"); 527 | }, 528 | 529 | substring: (arg) => { 530 | throw new Error("unimplemented"); 531 | }, 532 | 533 | tail: (arg) => { 534 | throw new Error("unimplemented"); 535 | }, 536 | 537 | throw: (arg) => { 538 | throw new Error("unimplemented"); 539 | }, 540 | 541 | toFile: (arg) => { 542 | throw new Error("unimplemented"); 543 | }, 544 | 545 | toJSON: (arg) => { 546 | throw new Error("unimplemented"); 547 | }, 548 | 549 | toPath: (arg) => { 550 | throw new Error("unimplemented"); 551 | }, 552 | 553 | toString: (arg: NixType) => { 554 | if (arg instanceof NixString) { 555 | return arg; 556 | } else if (arg instanceof Path) { 557 | return new NixString(arg.path); 558 | } 559 | 560 | // TODO: Expand on this 561 | throw new Error("unimplemented"); 562 | }, 563 | 564 | toXML: (arg) => { 565 | throw new Error("unimplemented"); 566 | }, 567 | 568 | trace: (arg) => { 569 | throw new Error("unimplemented"); 570 | }, 571 | 572 | traceVerbose: (arg) => { 573 | throw new Error("unimplemented"); 574 | }, 575 | 576 | tryEval: (arg) => { 577 | throw new Error("unimplemented"); 578 | }, 579 | 580 | typeOf: (arg) => { 581 | throw new Error("unimplemented"); 582 | }, 583 | 584 | unsafeDiscardOutputDependency: (arg) => { 585 | throw new Error("unimplemented"); 586 | }, 587 | 588 | zipAttrsWith: (arg) => { 589 | throw new Error("unimplemented"); 590 | }, 591 | }; 592 | 593 | return builtins; 594 | } 595 | -------------------------------------------------------------------------------- /nixjs-rt/src/errors/abort.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage, err, NixError, instanceToClass } from "."; 2 | import { NixTypeClass, NixTypeInstance } from "../lib"; 3 | 4 | export class NixAbortError { 5 | constructor(public readonly message: string) {} 6 | 7 | toDefaultErrorMessage(): ErrorMessage { 8 | return err`Aborted: '${this.message}'`; 9 | } 10 | } 11 | 12 | export function abortError(message: string) { 13 | let abort = new NixAbortError(message); 14 | return new NixError(abort, abort.toDefaultErrorMessage()); 15 | } 16 | -------------------------------------------------------------------------------- /nixjs-rt/src/errors/attribute.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage, err, NixError, instanceToClass, highlighted } from "."; 2 | import { NixTypeClass, NixTypeInstance } from "../lib"; 3 | 4 | export class NixAttributeAlreadyDefinedError { 5 | constructor(public readonly attrPath: string[]) {} 6 | 7 | toDefaultErrorMessage(): ErrorMessage { 8 | return err`Attribute '${highlighted(this.attrPath.join("."))}' is already defined'`; 9 | } 10 | } 11 | 12 | export function attributeAlreadyDefinedError(attrPath: string[]) { 13 | let error = new NixAttributeAlreadyDefinedError(attrPath); 14 | return new NixError(error, error.toDefaultErrorMessage()); 15 | } 16 | 17 | export class NixMissingAttributeError { 18 | constructor(public readonly attrPath: string[]) {} 19 | 20 | toDefaultErrorMessage(): ErrorMessage { 21 | return err`Attribute '${highlighted(this.attrPath.join("."))}' is missing`; 22 | } 23 | } 24 | 25 | export function missingAttributeError(attrPath: string[]) { 26 | let error = new NixMissingAttributeError(attrPath); 27 | return new NixError(error, error.toDefaultErrorMessage()); 28 | } 29 | -------------------------------------------------------------------------------- /nixjs-rt/src/errors/errorMessage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Attrset, 3 | Lambda, 4 | Lazy, 5 | NixBool, 6 | NixFloat, 7 | NixInt, 8 | NixList, 9 | NixNull, 10 | NixString, 11 | NixTypeClass, 12 | NixTypeInstance, 13 | Path, 14 | } from "../lib"; 15 | 16 | type PlainErrorMessagePart = { 17 | kind: "plain"; 18 | value: string; 19 | }; 20 | 21 | type HighlightedErrorMessagePart = { 22 | kind: "highlighted"; 23 | value: string; 24 | }; 25 | 26 | type ErrorMessagePart = PlainErrorMessagePart | HighlightedErrorMessagePart; 27 | export type ErrorMessage = ErrorMessagePart[]; 28 | 29 | /** A hack-y way of finding whether an object is an ErrorMessagePart. Essential for error message building. */ 30 | function isErrorMessagePart(part: any): part is ErrorMessagePart { 31 | return ( 32 | typeof part === "object" && 33 | part !== null && 34 | "kind" in part && 35 | typeof part.kind === "string" 36 | ); 37 | } 38 | 39 | /** A helper to convert instances to their respective class in a type-safe way, for better error messages */ 40 | export function instanceToClass(instance: NixTypeInstance | NixTypeClass) { 41 | if (instance instanceof NixBool) { 42 | return NixBool; 43 | } else if (instance instanceof NixFloat) { 44 | return NixFloat; 45 | } else if (instance instanceof NixInt) { 46 | return NixInt; 47 | } else if (instance instanceof NixList) { 48 | return NixList; 49 | } else if (instance instanceof NixNull) { 50 | return NixNull; 51 | } else if (instance instanceof NixString) { 52 | return NixString; 53 | } else if (instance instanceof Path) { 54 | return Path; 55 | } else if (instance instanceof Lambda) { 56 | return Lambda; 57 | } else if (instance instanceof Attrset) { 58 | return Attrset; 59 | } else if (instance instanceof Lazy) { 60 | return instanceToClass(instance.toStrict()); 61 | } else { 62 | return instance; 63 | } 64 | } 65 | 66 | export function stringifyErrorMessage(message: ErrorMessage): string { 67 | return message.map((part) => part.value).join(""); 68 | } 69 | 70 | type ErrorMessageBuilderPart = string | ErrorMessage; 71 | 72 | function builderPartToErrMessage(part: ErrorMessageBuilderPart): ErrorMessage { 73 | if (typeof part === "string") { 74 | return [{ kind: "plain", value: part }]; 75 | } else { 76 | return part; 77 | } 78 | } 79 | 80 | /** 81 | * Tag function for building error messages, especially type mismatch messages. 82 | * 83 | * # Example: 84 | * ```ts 85 | * err`Expected ${errTypes(NixInt, NixString)}, but got ${errType(value)}` 86 | * ``` 87 | */ 88 | export function err( 89 | strings: readonly string[], 90 | ...parts: readonly ErrorMessageBuilderPart[] 91 | ): ErrorMessage { 92 | // Join the strings and parts together 93 | const messageParts: ErrorMessagePart[] = []; 94 | for (let i = 0; i < strings.length; i++) { 95 | messageParts.push(...builderPartToErrMessage(strings[i])); 96 | if (i < parts.length) { 97 | messageParts.push(...builderPartToErrMessage(parts[i])); 98 | } 99 | } 100 | 101 | return messageParts; 102 | } 103 | 104 | /** 105 | * Generates a highlighted human-readable representation of a single type 106 | * 107 | * E.g. `NixInt` becomes `a number` 108 | */ 109 | export function errType(type: NixTypeClass | NixTypeInstance): ErrorMessage { 110 | return [ 111 | { kind: "highlighted", value: instanceToClass(type).toHumanReadable() }, 112 | ]; 113 | } 114 | 115 | /** 116 | * Generates a highlighted human-readable representation of multiple types 117 | * 118 | * E.g. `[NixInt, NixFloat, NixString]` becomes `a number, a float, or a string` 119 | */ 120 | export function errTypes(...types: NixTypeClass[]): ErrorMessage { 121 | if (types.length === 0) { 122 | throw new Error("errTypes: types array is empty"); 123 | } 124 | 125 | if (types.length === 1) { 126 | return errType(types[0]); 127 | } 128 | 129 | // Dynamically build the error message, separating with commas and "or" 130 | const message: ErrorMessage = []; 131 | for (let i = 0; i < types.length; i++) { 132 | if (i === types.length - 1) { 133 | message.push(...err`or`); 134 | } else if (i > 0) { 135 | message.push(...err`,`); 136 | } 137 | message.push(...errType(types[i])); 138 | } 139 | 140 | return message; 141 | } 142 | 143 | export function highlighted(message: string | ErrorMessage): ErrorMessage { 144 | const msg = err`${message}`; 145 | return msg.map((part) => ({ 146 | kind: "highlighted", 147 | value: part.value, 148 | })); 149 | } 150 | -------------------------------------------------------------------------------- /nixjs-rt/src/errors/function.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage, err, NixError, instanceToClass, highlighted } from "."; 2 | import { NixTypeClass, NixTypeInstance } from "../lib"; 3 | 4 | export class NixFunctionCallWithoutArgumentError { 5 | constructor(public readonly argument: string) {} 6 | 7 | toDefaultErrorMessage(): ErrorMessage { 8 | return err`Function call is missing required argument '${highlighted(this.argument)}'`; 9 | } 10 | } 11 | 12 | export function functionCallWithoutArgumentError(argument: string) { 13 | let error = new NixFunctionCallWithoutArgumentError(argument); 14 | return new NixError(error, error.toDefaultErrorMessage()); 15 | } 16 | -------------------------------------------------------------------------------- /nixjs-rt/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Attrset, 3 | Lambda, 4 | Lazy, 5 | NixBool, 6 | NixFloat, 7 | NixInt, 8 | NixList, 9 | NixNull, 10 | NixString, 11 | NixType, 12 | NixTypeClass, 13 | NixTypeInstance, 14 | Path, 15 | } from "../lib"; 16 | import { NixAbortError } from "./abort"; 17 | import { 18 | NixAttributeAlreadyDefinedError, 19 | NixMissingAttributeError, 20 | } from "./attribute"; 21 | import { ErrorMessage } from "./errorMessage"; 22 | import { NixFunctionCallWithoutArgumentError } from "./function"; 23 | import { NixOtherError } from "./other"; 24 | import { NixTypeMismatchError } from "./typeError"; 25 | import { NixCouldntFindVariableError } from "./variable"; 26 | 27 | export * from "./errorMessage"; 28 | 29 | type NixErrorKind = 30 | | NixTypeMismatchError 31 | | NixAbortError 32 | | NixOtherError 33 | | NixMissingAttributeError 34 | | NixAttributeAlreadyDefinedError 35 | | NixFunctionCallWithoutArgumentError 36 | | NixCouldntFindVariableError; 37 | 38 | /** The base error class. This class gets parsed in rix by Rust code. */ 39 | export class NixError extends Error { 40 | constructor( 41 | public readonly kind: NixErrorKind, 42 | public readonly richMessage: ErrorMessage, 43 | ) { 44 | // TODO: In the future, make error messages have color highlighting for special error parts 45 | const messageString = richMessage.map((part) => part.value).join(""); 46 | 47 | super(messageString); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /nixjs-rt/src/errors/other.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage, err, NixError, instanceToClass } from "."; 2 | import { NixTypeClass, NixTypeInstance } from "../lib"; 3 | 4 | export class NixOtherError { 5 | constructor( 6 | public readonly message: ErrorMessage, 7 | public readonly codename: string, 8 | ) {} 9 | 10 | toDefaultErrorMessage(): ErrorMessage { 11 | return this.message; 12 | } 13 | } 14 | 15 | export function otherError(message: string | ErrorMessage, codename: string) { 16 | if (typeof message === "string") { 17 | message = err`${message}`; 18 | } 19 | 20 | let other = new NixOtherError(message, codename); 21 | return new NixError(other, other.toDefaultErrorMessage()); 22 | } 23 | -------------------------------------------------------------------------------- /nixjs-rt/src/errors/typeError.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ErrorMessage, 3 | err, 4 | NixError, 5 | instanceToClass, 6 | errType, 7 | errTypes, 8 | } from "."; 9 | import { NixTypeClass, NixTypeInstance } from "../lib"; 10 | 11 | export class NixTypeMismatchError { 12 | constructor( 13 | public readonly expected: NixTypeClass[], 14 | public readonly got: NixTypeClass, 15 | ) {} 16 | 17 | toDefaultErrorMessage(): ErrorMessage { 18 | return err`Expected ${errTypes(...this.expected)}, but got ${errType(this.got)}`; 19 | } 20 | } 21 | 22 | export function typeMismatchError( 23 | got: NixTypeClass | NixTypeInstance, 24 | expected: NixTypeClass | NixTypeClass[], 25 | message?: ErrorMessage, 26 | ) { 27 | if (!Array.isArray(expected)) { 28 | expected = [expected]; 29 | } 30 | 31 | const error = new NixTypeMismatchError(expected, instanceToClass(got)); 32 | return new NixError(error, message ?? error.toDefaultErrorMessage()); 33 | } 34 | 35 | /** Similar to a type mismatch error, but with expected being [] and the message is required */ 36 | export function invalidTypeError( 37 | got: NixTypeClass | NixTypeInstance, 38 | message: ErrorMessage, 39 | ) { 40 | const error = new NixTypeMismatchError([], instanceToClass(got)); 41 | return new NixError(error, message); 42 | } 43 | -------------------------------------------------------------------------------- /nixjs-rt/src/errors/variable.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage, err, NixError, instanceToClass, highlighted } from "."; 2 | import { NixTypeClass, NixTypeInstance } from "../lib"; 3 | 4 | export class NixCouldntFindVariableError { 5 | constructor(public readonly varName: string) {} 6 | 7 | toDefaultErrorMessage(): ErrorMessage { 8 | return err`Couldn't find variable '${highlighted(this.varName)}'`; 9 | } 10 | } 11 | 12 | export function couldntFindVariableError(varName: string) { 13 | let error = new NixCouldntFindVariableError(varName); 14 | return new NixError(error, error.toDefaultErrorMessage()); 15 | } 16 | -------------------------------------------------------------------------------- /nixjs-rt/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | import type { NixType, EvalCtx } from "./lib"; 2 | 3 | declare global { 4 | /** 5 | * Import a Nix module from the given path. The path is absolute. 6 | * Returns a transpiled version of the module, executed, with a function 7 | * that takes an EvalCtx and returns the module's value. 8 | */ 9 | var importNixModule: (path: string) => (ctx: EvalCtx) => NixType; 10 | 11 | /** 12 | * Log the string provided, purely for debugging purposes. 13 | */ 14 | var debugLog: (log: string) => void; 15 | } 16 | -------------------------------------------------------------------------------- /nixjs-rt/src/legacyTests.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, expect, test } from "@jest/globals"; 2 | import * as n from "./lib"; 3 | import { 4 | Attrset, 5 | attrset, 6 | AttrsetBody, 7 | EMPTY_ATTRSET, 8 | EvalCtx, 9 | EvalException, 10 | Lambda, 11 | Lazy, 12 | NixFloat, 13 | NixInt, 14 | NixList, 15 | NixString, 16 | NixType, 17 | Path, 18 | StrictAttrset, 19 | } from "./lib"; 20 | import { evalCtx, keyVals, toAttrpath } from "./testUtils"; 21 | 22 | // Apply: 23 | test("calling a lambda should return its value", () => { 24 | expect(new Lambda((_) => new NixInt(1n)).apply(EMPTY_ATTRSET)).toStrictEqual( 25 | new NixInt(1n), 26 | ); 27 | }); 28 | 29 | // Arithmetic: 30 | test("unary '-' operator on integers", () => { 31 | expect(new NixInt(1n).neg()).toStrictEqual(new NixInt(-1n)); 32 | }); 33 | 34 | test("unary '-' operator on floats", () => { 35 | expect(new NixFloat(2.5).neg()).toStrictEqual(new NixFloat(-2.5)); 36 | }); 37 | 38 | test("'+' operator on integers", () => { 39 | expect((new NixInt(1n).add(new NixInt(2n)) as NixInt).number).toBe(3); 40 | expect( 41 | ( 42 | new NixInt(4611686018427387904n).add( 43 | new NixInt(4611686018427387904n), 44 | ) as NixInt 45 | ).int64, 46 | ).toBe(-9223372036854775808n); 47 | }); 48 | 49 | test("'+' operator on floats", () => { 50 | expect(new NixFloat(1.0).add(new NixFloat(2.0)).toJs()).toBe(3); 51 | }); 52 | 53 | test("'+' operator on mixed integers and floats", () => { 54 | expect(new NixInt(1n).add(new NixFloat(2.0)).toJs()).toBe(3.0); 55 | expect(new NixFloat(2.0).add(new NixInt(1n)).toJs()).toBe(3.0); 56 | }); 57 | 58 | test("'+' operator on strings", () => { 59 | expect(new NixString("a").add(new NixString("b")).toJs()).toBe("ab"); 60 | }); 61 | 62 | test("'+' operator on paths and strings", () => { 63 | expect(new Path("/").add(new NixString("b"))).toStrictEqual(new Path("/b")); 64 | expect(new Path("/a").add(new NixString("b"))).toStrictEqual(new Path("/ab")); 65 | expect(new Path("/").add(new NixString("/"))).toStrictEqual(new Path("/")); 66 | expect(new Path("/").add(new NixString("."))).toStrictEqual(new Path("/")); 67 | expect(new Path("/a").add(new NixString("."))).toStrictEqual(new Path("/a.")); 68 | expect(new Path("/").add(new NixString("./a"))).toStrictEqual(new Path("/a")); 69 | }); 70 | 71 | test("'+' operator on paths", () => { 72 | expect(new Path("/").add(new Path("/a"))).toStrictEqual(new Path("/a")); 73 | expect( 74 | n.toPath(evalCtx(), "./a").add(n.toPath(evalCtx(), "./b")), 75 | ).toStrictEqual(new Path("/test_base/a/test_base/b")); 76 | }); 77 | 78 | test("'-' operator on integers", () => { 79 | const result = new NixInt(1n).sub(new NixInt(2n)) as NixInt; 80 | expect(result.number).toBe(-1); 81 | }); 82 | 83 | test("'-' operator on floats", () => { 84 | expect(new NixFloat(1).sub(new NixFloat(2)).toJs()).toBe(-1); 85 | }); 86 | 87 | test("'-' operator on mixed integers and floats", () => { 88 | expect(new NixInt(1n).sub(new NixFloat(2)).toJs()).toBe(-1); 89 | expect(new NixFloat(2.0).sub(new NixInt(1n)).toJs()).toBe(1); 90 | }); 91 | 92 | test("'*' operator on integers", () => { 93 | const result = new NixInt(2n).mul(new NixInt(3n)) as NixInt; 94 | expect(result.number).toBe(6); 95 | }); 96 | 97 | test("'*' operator on floats", () => { 98 | expect(new NixFloat(2.0).mul(new NixFloat(3.5))).toStrictEqual( 99 | new NixFloat(7), 100 | ); 101 | }); 102 | 103 | test("'*' operator on mixed integers and floats", () => { 104 | expect(new NixInt(2n).mul(new NixFloat(3.5))).toStrictEqual(new NixFloat(7)); 105 | expect(new NixFloat(3.5).mul(new NixInt(2n))).toStrictEqual(new NixFloat(7)); 106 | }); 107 | 108 | test("'/' operator on integers", () => { 109 | expect(new NixInt(5n).div(new NixInt(2n))).toStrictEqual(new NixInt(2n)); 110 | }); 111 | 112 | test("'/' operator on floats", () => { 113 | expect(new NixFloat(5.0).div(new NixFloat(2))).toStrictEqual( 114 | new NixFloat(2.5), 115 | ); 116 | }); 117 | 118 | test("'/' operator on mixed integers and floats", () => { 119 | expect(new NixInt(5n).div(new NixFloat(2.0))).toStrictEqual( 120 | new NixFloat(2.5), 121 | ); 122 | expect(new NixFloat(5.0).div(new NixInt(2n))).toStrictEqual( 123 | new NixFloat(2.5), 124 | ); 125 | }); 126 | 127 | // Attrset: 128 | test("attrset construction", () => { 129 | expect(attrset(evalCtx(), keyVals()).toJs()).toStrictEqual(new Map()); 130 | expect( 131 | attrset(evalCtx(), keyVals(["a", new NixFloat(1)])).toJs(), 132 | ).toStrictEqual(new Map([["a", 1]])); 133 | const nestedAttrset = new Map([["a", new Map([["b", 1]])]]); 134 | expect( 135 | attrset(evalCtx(), keyVals(["a.b", new NixFloat(1)])).toJs(), 136 | ).toStrictEqual(nestedAttrset); 137 | expect( 138 | attrset( 139 | evalCtx(), 140 | keyVals(["a", attrset(evalCtx(), keyVals())], ["a.b", new NixFloat(1)]), 141 | ).toJs(), 142 | ).toStrictEqual(nestedAttrset); 143 | expect( 144 | attrset( 145 | evalCtx(), 146 | keyVals( 147 | ["x.a", attrset(evalCtx(), keyVals())], 148 | ["x.a.b", new NixFloat(1)], 149 | ), 150 | ).toJs(), 151 | ).toStrictEqual(new Map([["x", nestedAttrset]])); 152 | }); 153 | 154 | test("attrsets ignore null attrs", () => { 155 | expect( 156 | attrset(evalCtx(), (_) => [ 157 | [[n.NULL, new NixString("a")], new NixFloat(1)], 158 | ]).toJs(), 159 | ).toStrictEqual(new Map()); 160 | expect( 161 | attrset(evalCtx(), (_) => [[[n.NULL], new NixFloat(1)]]).toJs(), 162 | ).toStrictEqual(new Map()); 163 | expect( 164 | attrset(evalCtx(), (_) => [ 165 | [[new NixString("a"), n.NULL], new NixFloat(1)], 166 | ]).toJs(), 167 | ).toStrictEqual(new Map([["a", new Map()]])); 168 | }); 169 | 170 | test("'//' operator on attrsets", () => { 171 | expect( 172 | attrset(evalCtx(), keyVals()).update(attrset(evalCtx(), keyVals())).toJs(), 173 | ).toStrictEqual(new Map()); 174 | expect( 175 | attrset(evalCtx(), keyVals(["a", new NixFloat(1)])) 176 | .update(attrset(evalCtx(), keyVals())) 177 | .toJs(), 178 | ).toStrictEqual(new Map([["a", 1]])); 179 | expect( 180 | attrset(evalCtx(), keyVals(["a", new NixFloat(1)])) 181 | .update(attrset(evalCtx(), keyVals(["b", new NixFloat(2)]))) 182 | .toJs(), 183 | ).toStrictEqual( 184 | new Map([ 185 | ["a", 1], 186 | ["b", 2], 187 | ]), 188 | ); 189 | expect( 190 | attrset(evalCtx(), keyVals(["a", new NixFloat(1)])) 191 | .update(attrset(evalCtx(), keyVals(["a", new NixFloat(2)]))) 192 | .toJs(), 193 | ).toStrictEqual(new Map([["a", 2]])); 194 | }); 195 | 196 | test("'?' operator", () => { 197 | expect(attrset(evalCtx(), keyVals()).has([new NixString("a")])).toBe(n.FALSE); 198 | expect( 199 | attrset(evalCtx(), keyVals(["a", new NixFloat(1)])).has([ 200 | new NixString("a"), 201 | ]), 202 | ).toBe(n.TRUE); 203 | expect( 204 | attrset(evalCtx(), keyVals(["a", new NixFloat(1)])).has([ 205 | new NixString("a"), 206 | new NixString("b"), 207 | ]), 208 | ).toBe(n.FALSE); 209 | expect( 210 | attrset(evalCtx(), keyVals(["a.b", new NixFloat(-1)])).has([ 211 | new NixString("a"), 212 | new NixString("b"), 213 | ]), 214 | ).toBe(n.TRUE); 215 | }); 216 | 217 | test("'?' operator on other types returns false", () => { 218 | expect(new NixFloat(1).has([new NixString("a")])).toStrictEqual(n.FALSE); 219 | expect(n.FALSE.has([new NixString("a")])).toStrictEqual(n.FALSE); 220 | }); 221 | 222 | test("'.' operator", () => { 223 | expect( 224 | attrset(evalCtx(), keyVals(["a", new NixFloat(1)])) 225 | .select([new NixString("a")], undefined) 226 | .toJs(), 227 | ).toBe(1); 228 | 229 | expect( 230 | attrset(evalCtx(), keyVals(["a.b", new NixFloat(1)])) 231 | .select([new NixString("a"), new NixString("b")], undefined) 232 | .toJs(), 233 | ).toBe(1); 234 | expect( 235 | attrset(evalCtx(), keyVals()).select([new NixString("a")], new NixFloat(1)), 236 | ).toStrictEqual(new NixFloat(1)); 237 | expect( 238 | attrset(evalCtx(), keyVals(["a.a", new NixFloat(1)])) 239 | .select([new NixString("a"), new NixString("b")], new NixFloat(1)) 240 | .toJs(), 241 | ).toBe(1); 242 | expect( 243 | n 244 | .attrset( 245 | evalCtx(), 246 | keyVals(["a", new NixFloat(1)], ["b.c", new NixFloat(2)]), 247 | ) 248 | .select([new NixString("a"), new NixString("c")], new NixFloat(5)) 249 | .toJs(), 250 | ).toBe(5); 251 | }); 252 | 253 | test("recursive attrsets allow referencing attributes defined later", () => { 254 | expect( 255 | n 256 | .recAttrset(evalCtx(), (ctx) => [ 257 | [ 258 | toAttrpath("a"), 259 | new Lazy(ctx, (ctx) => ctx.lookup("b").add(new NixFloat(1))), 260 | ], 261 | [toAttrpath("b"), new NixFloat(1)], 262 | ]) 263 | .select([new NixString("a")], undefined) 264 | .toJs(), 265 | ).toBe(2); 266 | }); 267 | 268 | test("recursive attrsets allow referencing attributes from other attribute names", () => { 269 | expect( 270 | n 271 | .recAttrset(evalCtx(), (ctx) => [ 272 | [[new Lazy(ctx, (ctx) => ctx.lookup("a"))], new NixFloat(1)], 273 | [[new NixString("a")], new NixString("b")], 274 | ]) 275 | .toJs(), 276 | ).toStrictEqual( 277 | new Map([ 278 | ["a", "b"], 279 | ["b", 1], 280 | ]), 281 | ); 282 | // This fails in nix but work with our implementation: `rec { ${a} = 1; ${b} = "c"; b = "a"; }` 283 | expect( 284 | n 285 | .recAttrset(evalCtx(), (ctx) => [ 286 | [[new Lazy(ctx, (ctx) => ctx.lookup("a"))], new NixFloat(1)], 287 | [[new Lazy(ctx, (ctx) => ctx.lookup("b"))], new NixString("c")], 288 | [[new NixString("b")], new NixString("a")], 289 | ]) 290 | .toJs(), 291 | ).toStrictEqual( 292 | new Map([ 293 | ["c", 1], 294 | ["a", "c"], 295 | ["b", "a"], 296 | ]), 297 | ); 298 | }); 299 | 300 | // Boolean: 301 | test("'&&' operator on booleans", () => { 302 | expect(n.TRUE.and(n.FALSE)).toBe(n.FALSE); 303 | expect(n.FALSE.and(new NixFloat(1))).toBe(n.FALSE); // emulates nix's behaviour 304 | }); 305 | 306 | test("'->' operator on booleans", () => { 307 | expect(n.FALSE.implication(n.FALSE)).toBe(n.TRUE); 308 | expect(n.FALSE.implication(new NixFloat(1))).toBe(n.TRUE); // emulates nix's behaviour 309 | }); 310 | 311 | test("'!' operator on booleans", () => { 312 | expect(n.FALSE.invert()).toBe(n.TRUE); 313 | }); 314 | 315 | test("'||' operator on booleans", () => { 316 | expect(n.TRUE.or(n.FALSE).toJs()).toBe(true); 317 | expect(n.TRUE.or(new NixFloat(1)).toJs()).toBe(true); // emulates nix's behaviour 318 | }); 319 | 320 | // Comparison: 321 | test("'==' operator on numbers", () => { 322 | expect(new NixFloat(1).eq(new NixFloat(2))).toBe(n.FALSE); 323 | expect(new NixFloat(1).eq(new NixFloat(1))).toBe(n.TRUE); 324 | expect(new NixInt(1n).eq(new NixInt(2n))).toBe(n.FALSE); 325 | expect(new NixInt(1n).eq(new NixInt(1n))).toBe(n.TRUE); 326 | expect(new NixInt(1n).eq(new NixFloat(1.1))).toBe(n.FALSE); 327 | expect(new NixInt(1n).eq(new NixFloat(1.0))).toBe(n.TRUE); 328 | expect(new NixFloat(1.0).eq(new NixInt(1n))).toBe(n.TRUE); 329 | }); 330 | 331 | test("'==' operator on booleans", () => { 332 | expect(n.TRUE.eq(n.FALSE)).toBe(n.FALSE); 333 | expect(n.TRUE.eq(n.TRUE)).toBe(n.TRUE); 334 | }); 335 | 336 | test("'==' operator on strings", () => { 337 | expect(new NixString("").eq(new NixString(""))).toBe(n.TRUE); 338 | expect(new NixString("a").eq(new NixString("b"))).toBe(n.FALSE); 339 | }); 340 | 341 | test("'==' operator on lists", () => { 342 | expect(new NixList([]).eq(new NixList([]))).toBe(n.TRUE); 343 | expect( 344 | new NixList([new NixFloat(1)]).eq(new NixList([new NixFloat(1)])), 345 | ).toBe(n.TRUE); 346 | expect( 347 | new NixList([new NixList([new NixFloat(1)])]).eq( 348 | new NixList([new NixList([new NixFloat(1)])]), 349 | ), 350 | ).toBe(n.TRUE); 351 | expect( 352 | new NixList([new NixFloat(1)]).eq(new NixList([new NixFloat(2)])), 353 | ).toBe(n.FALSE); 354 | expect(new NixList([new NixInt(1n)]).eq(new NixList([new NixInt(1n)]))).toBe( 355 | n.TRUE, 356 | ); 357 | expect(new NixList([new NixInt(1n)]).eq(new NixList([new NixInt(2n)]))).toBe( 358 | n.FALSE, 359 | ); 360 | }); 361 | 362 | test("'==' operator on nulls", () => { 363 | expect(n.NULL.eq(n.NULL)).toBe(n.TRUE); 364 | expect(n.NULL.eq(new NixFloat(1))).toBe(n.FALSE); 365 | expect(new NixString("a").eq(n.NULL)).toBe(n.FALSE); 366 | }); 367 | 368 | test("'==' operator on attrsets", () => { 369 | expect(attrset(evalCtx(), keyVals()).eq(attrset(evalCtx(), keyVals()))).toBe( 370 | n.TRUE, 371 | ); 372 | expect( 373 | attrset(evalCtx(), keyVals()).eq( 374 | attrset(evalCtx(), keyVals(["a", new NixFloat(1)])), 375 | ), 376 | ).toBe(n.FALSE); 377 | expect( 378 | attrset(evalCtx(), keyVals(["a", new NixFloat(1)])).eq( 379 | attrset(evalCtx(), keyVals(["a", new NixFloat(1)])), 380 | ), 381 | ).toBe(n.TRUE); 382 | expect( 383 | attrset(evalCtx(), keyVals(["a", new NixFloat(1)])).eq( 384 | attrset(evalCtx(), keyVals(["a", new NixFloat(2)])), 385 | ), 386 | ).toBe(n.FALSE); 387 | }); 388 | 389 | test("'!=' operator on floats", () => { 390 | expect(new NixFloat(1).neq(new NixFloat(2))).toBe(n.TRUE); 391 | expect(new NixFloat(1).neq(new NixFloat(1))).toBe(n.FALSE); 392 | }); 393 | 394 | test("'<' operator on numbers", () => { 395 | expect(new NixFloat(1).less(new NixFloat(2))).toBe(n.TRUE); 396 | expect(new NixInt(1n).less(new NixInt(2n))).toBe(n.TRUE); 397 | expect(new NixInt(1n).less(new NixFloat(2))).toBe(n.TRUE); 398 | expect(new NixFloat(1).less(new NixInt(2n))).toBe(n.TRUE); 399 | }); 400 | 401 | test("'<' operator on strings", () => { 402 | expect(new NixString("a").less(new NixString("b"))).toBe(n.TRUE); 403 | expect(new NixString("foo").less(new NixString("b"))).toBe(n.FALSE); 404 | }); 405 | 406 | test("'<' operator on lists", () => { 407 | expect(new NixList([]).less(new NixList([]))).toBe(n.FALSE); 408 | expect(new NixList([]).less(new NixList([new NixFloat(1)]))).toBe(n.TRUE); 409 | expect(new NixList([new NixFloat(1)]).less(new NixList([]))).toBe(n.FALSE); 410 | expect( 411 | new NixList([new NixFloat(1)]).less( 412 | new NixList([new NixFloat(1), new NixFloat(2)]), 413 | ), 414 | ).toBe(n.TRUE); 415 | expect( 416 | new NixList([new NixFloat(1), new NixFloat(2)]).less( 417 | new NixList([new NixFloat(1)]), 418 | ), 419 | ).toBe(n.FALSE); 420 | expect( 421 | new NixList([new NixFloat(1), new NixFloat(1)]).less( 422 | new NixList([new NixFloat(1), new NixFloat(2)]), 423 | ), 424 | ).toBe(n.TRUE); 425 | expect( 426 | new NixList([new NixFloat(1), n.TRUE]).less(new NixList([new NixFloat(1)])), 427 | ).toBe(n.FALSE); 428 | expect( 429 | new NixList([new NixInt(1n)]).less(new NixList([new NixInt(2n)])), 430 | ).toBe(n.TRUE); 431 | 432 | // This reproduces nix's observed behaviour 433 | expect(new NixList([n.TRUE]).less(new NixList([n.TRUE]))).toBe(n.FALSE); 434 | expect(new NixList([n.FALSE]).less(new NixList([n.FALSE]))).toBe(n.FALSE); 435 | expect( 436 | new NixList([n.FALSE, new NixFloat(1)]).less( 437 | new NixList([n.FALSE, new NixFloat(2)]), 438 | ), 439 | ).toBe(n.TRUE); 440 | expect(new NixList([n.NULL]).less(new NixList([n.NULL]))).toBe(n.FALSE); 441 | }); 442 | 443 | test("'<' operator on lists with lazy values", () => { 444 | expect( 445 | new NixList([new Lazy(evalCtx(), (_) => new NixFloat(1))]).less( 446 | new NixList([new Lazy(evalCtx(), (_) => new NixFloat(1))]), 447 | ), 448 | ).toBe(n.FALSE); 449 | 450 | expect( 451 | new NixList([new Lazy(evalCtx(), (_) => new NixFloat(1))]).less( 452 | new NixList([new Lazy(evalCtx(), (_) => new NixFloat(2))]), 453 | ), 454 | ).toBe(n.TRUE); 455 | 456 | expect( 457 | new NixList([new Lazy(evalCtx(), (_) => n.TRUE)]).less( 458 | new NixList([new Lazy(evalCtx(), (_) => n.TRUE)]), 459 | ), 460 | ).toBe(n.FALSE); 461 | }); 462 | 463 | test("'<' operator on paths", () => { 464 | expect(new Path("./a").less(new Path("./b"))).toStrictEqual(n.TRUE); 465 | expect(new Path("./a").less(new Path("./a"))).toStrictEqual(n.FALSE); 466 | }); 467 | 468 | test("'<=' operator", () => { 469 | expect(new NixFloat(1).lessEq(new NixFloat(0))).toBe(n.FALSE); 470 | expect(new NixFloat(1).lessEq(new NixFloat(1))).toBe(n.TRUE); 471 | expect(new NixFloat(1).lessEq(new NixFloat(2))).toBe(n.TRUE); 472 | 473 | // This reproduces nix's observed behaviour 474 | expect(new NixList([n.TRUE]).lessEq(new NixList([n.TRUE]))).toBe(n.TRUE); 475 | expect(new NixList([n.NULL]).lessEq(new NixList([n.NULL]))).toBe(n.TRUE); 476 | }); 477 | 478 | test("'>=' operator", () => { 479 | expect(new NixFloat(1).moreEq(new NixFloat(0))).toBe(n.TRUE); 480 | expect(new NixFloat(1).moreEq(new NixFloat(1))).toBe(n.TRUE); 481 | expect(new NixFloat(1).moreEq(new NixFloat(2))).toBe(n.FALSE); 482 | 483 | // This reproduces nix's observed behaviour 484 | expect(new NixList([n.TRUE]).moreEq(new NixList([n.TRUE]))).toBe(n.TRUE); 485 | expect(new NixList([n.NULL]).moreEq(new NixList([n.NULL]))).toBe(n.TRUE); 486 | }); 487 | 488 | test("'>' operator", () => { 489 | expect(new NixFloat(1).more(new NixFloat(0))).toBe(n.TRUE); 490 | expect(new NixFloat(1).more(new NixFloat(1))).toBe(n.FALSE); 491 | expect(new NixFloat(1).more(new NixFloat(2))).toBe(n.FALSE); 492 | }); 493 | 494 | // Lambda: 495 | test("parameter lambda", () => { 496 | expect( 497 | n 498 | .paramLambda(evalCtx(), "foo", (evalCtx) => evalCtx.lookup("foo")) 499 | .apply(n.TRUE), 500 | ).toBe(n.TRUE); 501 | }); 502 | 503 | test("pattern lambda", () => { 504 | const arg = n.attrset(evalCtx(), keyVals(["a", new NixFloat(1)])); 505 | expect( 506 | n 507 | .patternLambda(evalCtx(), undefined, [["a", undefined]], (evalCtx) => 508 | evalCtx.lookup("a"), 509 | ) 510 | .apply(arg) 511 | .toJs(), 512 | ).toBe(1); 513 | }); 514 | 515 | test("pattern lambda with default values", () => { 516 | const arg = n.attrset(evalCtx(), keyVals()); 517 | expect( 518 | n 519 | .patternLambda(evalCtx(), undefined, [["a", 1]], (evalCtx) => 520 | evalCtx.lookup("a"), 521 | ) 522 | .apply(arg), 523 | ).toBe(1); 524 | }); 525 | 526 | test("pattern lambda with arguments binding", () => { 527 | const arg = n.attrset(evalCtx(), keyVals(["a", new NixFloat(1)])); 528 | expect( 529 | n 530 | .patternLambda(evalCtx(), "args", [["a", undefined]], (evalCtx) => 531 | evalCtx.lookup("args").select([new NixString("a")], undefined), 532 | ) 533 | .apply(arg) 534 | .toJs(), 535 | ).toBe(1); 536 | }); 537 | 538 | // Lazy: 539 | test("'Lazy.toStrict' evaluates the body only once", () => { 540 | let sentinel = new NixFloat(0); 541 | let lazyValue = new Lazy(evalCtx(), (_) => { 542 | sentinel = sentinel.add(new NixFloat(1)) as NixFloat; 543 | return sentinel; 544 | }); 545 | expect(lazyValue.toStrict().toJs()).toEqual(1); 546 | expect(lazyValue.toStrict().toJs()).toEqual(1); 547 | }); 548 | 549 | test("'Lazy.toStrict' uses the construction-time evaluation context", () => { 550 | const innerValue = new NixFloat(42); 551 | let innerCtx = evalCtx().withShadowingScope( 552 | n.attrset(evalCtx(), keyVals(["a", innerValue])), 553 | ); 554 | let lazyValue = new Lazy(innerCtx, (evalCtx) => evalCtx.lookup("a")); 555 | expect(lazyValue.toStrict()).toEqual(innerValue); 556 | }); 557 | 558 | test("'Lazy.toStrict' drops the body and the evaluation context", () => { 559 | let lazyValue = new Lazy(evalCtx(), (_) => n.TRUE); 560 | lazyValue.toStrict(); 561 | expect(lazyValue.body).toBeUndefined(); 562 | expect(lazyValue.evalCtx).toBeUndefined(); 563 | }); 564 | 565 | // List: 566 | test("'++' operator", () => { 567 | const list_1 = new NixList([new NixFloat(1)]); 568 | const list_2 = new NixList([new NixFloat(2)]); 569 | expect(list_1.concat(list_2)).toStrictEqual( 570 | new NixList([new NixFloat(1), new NixFloat(2)]), 571 | ); 572 | // Here's we're verifying that neither of the operands is mutated. 573 | expect(list_1).toStrictEqual(new NixList([new NixFloat(1)])); 574 | expect(list_2).toStrictEqual(new NixList([new NixFloat(2)])); 575 | }); 576 | 577 | test("'++' operator on lazy lists with lazy values", () => { 578 | expect( 579 | new NixList([new Lazy(evalCtx(), (_) => new NixFloat(1))]) 580 | .concat( 581 | new Lazy( 582 | evalCtx(), 583 | (_) => new NixList([new Lazy(evalCtx(), (_) => new NixFloat(2))]), 584 | ), 585 | ) 586 | .toJs(), 587 | ).toStrictEqual([1, 2]); 588 | }); 589 | // Path: 590 | test("toPath on absolute paths", () => { 591 | expect(n.toPath(evalCtx(), "/a")).toStrictEqual(new Path("/a")); 592 | expect(n.toPath(evalCtx(), "/./a/../b")).toStrictEqual(new Path("/b")); 593 | expect(n.toPath(evalCtx(), "//./a//..///b/")).toStrictEqual(new Path("/b")); 594 | }); 595 | 596 | test("toPath transforms relative paths with 'joinPaths'", () => { 597 | expect(n.toPath(evalCtx(), "a")).toStrictEqual(new Path("/test_base/a")); 598 | expect(n.toPath(evalCtx(), "./a")).toStrictEqual(new Path("/test_base/a")); 599 | }); 600 | 601 | test("variable in shadowing scope", () => { 602 | expect( 603 | evalCtx() 604 | .withShadowingScope(n.attrset(evalCtx(), keyVals(["foo", n.TRUE]))) 605 | .lookup("foo") 606 | .toJs(), 607 | ).toBe(true); 608 | }); 609 | 610 | // Type functions: 611 | test("typeOf", () => { 612 | expect(new NixInt(1n).typeOf()).toBe("int"); 613 | expect(new NixFloat(5.0).typeOf()).toBe("float"); 614 | expect(new NixString("a").typeOf()).toBe("string"); 615 | expect(n.TRUE.typeOf()).toBe("bool"); 616 | expect(n.NULL.typeOf()).toBe("null"); 617 | expect(new NixList([]).typeOf()).toBe("list"); 618 | expect(attrset(evalCtx(), keyVals()).typeOf()).toBe("set"); 619 | expect(new Path("/").typeOf()).toBe("path"); 620 | expect(new Lambda((_) => n.TRUE).typeOf()).toBe("lambda"); 621 | // TODO: cover other Nix types 622 | }); 623 | 624 | // With: 625 | test("'with' expression puts attrs into scope", () => { 626 | const namespace = n.attrset(evalCtx(), keyVals(["a", new NixFloat(1)])); 627 | expect( 628 | n.withExpr(evalCtx(), namespace, (evalCtx) => evalCtx.lookup("a")).toJs(), 629 | ).toBe(1); 630 | }); 631 | 632 | test("'with' expression does not shadow variables", () => { 633 | const namespace = n.attrset(evalCtx(), keyVals(["a", new NixFloat(1)])); 634 | let outerCtx = evalCtx().withShadowingScope( 635 | n.attrset(evalCtx(), keyVals(["a", new NixFloat(2)])), 636 | ); 637 | expect(n.withExpr(outerCtx, namespace, (ctx) => ctx.lookup("a")).toJs()).toBe( 638 | 2, 639 | ); 640 | }); 641 | 642 | test("'with' expressions shadow themselves", () => { 643 | const outerNamespace = n.attrset(evalCtx(), keyVals(["a", new NixFloat(1)])); 644 | const innerNamespace = n.attrset(evalCtx(), keyVals(["a", new NixFloat(2)])); 645 | const innerExpr = (ctx) => 646 | n.withExpr(ctx, innerNamespace, (ctx) => ctx.lookup("a")); 647 | expect(n.withExpr(evalCtx(), outerNamespace, innerExpr).toJs()).toBe(2); 648 | }); 649 | -------------------------------------------------------------------------------- /nixjs-rt/src/lib.ts: -------------------------------------------------------------------------------- 1 | import { getBuiltins } from "./builtins"; 2 | import { NixError, err, errType } from "./errors"; 3 | import { 4 | NixFunctionCallWithoutArgumentError, 5 | functionCallWithoutArgumentError, 6 | } from "./errors/function"; 7 | import { 8 | NixAttributeAlreadyDefinedError, 9 | NixMissingAttributeError, 10 | missingAttributeError, 11 | } from "./errors/attribute"; 12 | import { NixOtherError, otherError } from "./errors/other"; 13 | import { 14 | NixTypeMismatchError, 15 | invalidTypeError, 16 | typeMismatchError, 17 | } from "./errors/typeError"; 18 | import { 19 | NixCouldntFindVariableError, 20 | couldntFindVariableError, 21 | } from "./errors/variable"; 22 | import { NixAbortError } from "./errors/abort"; 23 | import { isAbsolutePath, joinPaths, normalizePath } from "./utils"; 24 | 25 | // Error re-exports 26 | export { NixError } from "./errors"; 27 | export { NixFunctionCallWithoutArgumentError } from "./errors/function"; 28 | export { 29 | NixAttributeAlreadyDefinedError, 30 | NixMissingAttributeError, 31 | } from "./errors/attribute"; 32 | export { NixOtherError } from "./errors/other"; 33 | export { NixTypeMismatchError } from "./errors/typeError"; 34 | export { NixCouldntFindVariableError } from "./errors/variable"; 35 | export { NixAbortError } from "./errors/abort"; 36 | 37 | // Types: 38 | export class EvalException extends Error { 39 | constructor(message: string) { 40 | super(message); 41 | } 42 | } 43 | 44 | export type Body = (evalCtx: EvalCtx) => NixType; 45 | 46 | export type InnerAttrPath = (evalCtx: EvalCtx) => NixType[]; 47 | 48 | interface Scope { 49 | lookup(name: string): NixType | undefined; 50 | } 51 | 52 | class CompoundScope implements Scope { 53 | readonly childScope: Scope; 54 | readonly parent: Scope; 55 | 56 | constructor(parentScope: Scope, childScope: Scope) { 57 | this.childScope = childScope; 58 | this.parent = parentScope; 59 | } 60 | 61 | lookup(name: string): NixType | undefined { 62 | const value = this.childScope.lookup(name); 63 | if (value === undefined) return this.parent.lookup(name); 64 | return value; 65 | } 66 | } 67 | 68 | class GlobalScope implements Scope { 69 | readonly scope: Map; 70 | 71 | constructor(scope: Map) { 72 | this.scope = scope; 73 | } 74 | 75 | lookup(name: string): NixType | undefined { 76 | return this.scope.get(name); 77 | } 78 | } 79 | 80 | export class EvalCtx implements Scope { 81 | /** 82 | * The absolute resolved path of the directory of the script that's currently being executed. 83 | */ 84 | readonly scriptDir: string; 85 | readonly shadowScope: Scope; 86 | readonly nonShadowScope: Scope; 87 | 88 | constructor( 89 | scriptDir: string, 90 | shadowScope: Scope | undefined = undefined, 91 | nonShadowScope: Scope | undefined = undefined, 92 | ) { 93 | this.scriptDir = scriptDir; 94 | this.shadowScope = 95 | shadowScope === undefined ? _buildGlobalScope() : shadowScope; 96 | this.nonShadowScope = nonShadowScope; 97 | } 98 | 99 | withShadowingScope(lookupTable: Scope): EvalCtx { 100 | return new EvalCtx( 101 | this.scriptDir, 102 | new CompoundScope(this.shadowScope, lookupTable), 103 | this.nonShadowScope, 104 | ); 105 | } 106 | 107 | withNonShadowingScope(lookupTable: Scope): EvalCtx { 108 | return new EvalCtx( 109 | this.scriptDir, 110 | this.shadowScope, 111 | new CompoundScope(this.nonShadowScope, lookupTable), 112 | ); 113 | } 114 | 115 | lookup(name: string): NixType { 116 | let value = this.shadowScope.lookup(name); 117 | if (value !== undefined) { 118 | return value; 119 | } 120 | if (this.nonShadowScope !== undefined) { 121 | value = this.nonShadowScope.lookup(name); 122 | if (value !== undefined) { 123 | return value; 124 | } 125 | } 126 | throw couldntFindVariableError(name); 127 | } 128 | } 129 | 130 | export abstract class NixType { 131 | /** 132 | * This method implements the `+` operator. It adds the `rhs` value to this value. 133 | */ 134 | add(rhs: NixType): NixType { 135 | throw invalidTypeError( 136 | this, 137 | err`Cannot add ${errType(rhs)} to ${errType(this)}`, 138 | ); 139 | } 140 | 141 | and(rhs: NixType): NixBool { 142 | return nixBoolFromJs(this.asBoolean() && rhs.asBoolean()); 143 | } 144 | 145 | apply(param: NixType): NixType { 146 | throw invalidTypeError( 147 | this, 148 | err`Attempt to call something which is not a function but is ${errType(this)}`, 149 | ); 150 | } 151 | 152 | asBoolean(): boolean { 153 | throw typeMismatchError(this, NixBool); 154 | } 155 | 156 | asString(): string { 157 | throw typeMismatchError(this, [NixString, Path]); 158 | } 159 | 160 | concat(other: NixType): NixList { 161 | throw invalidTypeError( 162 | this, 163 | err`Cannot concatenate ${errType(this)} and ${errType(other)}`, 164 | ); 165 | } 166 | 167 | div(rhs: NixType): NixInt | NixFloat { 168 | throw invalidTypeError( 169 | this, 170 | err`Cannot divide ${errType(this)} with ${errType(rhs)}`, 171 | ); 172 | } 173 | 174 | /** 175 | * This method implements the `==` operator. It compares the `rhs` value with this value for equality. 176 | */ 177 | eq(rhs: NixType): NixBool { 178 | return FALSE; 179 | } 180 | 181 | has(attrPath: NixType[]): NixBool { 182 | return FALSE; 183 | } 184 | 185 | implication(rhs: NixType): NixBool { 186 | return nixBoolFromJs(!this.asBoolean() || rhs.asBoolean()); 187 | } 188 | 189 | invert(): NixBool { 190 | return nixBoolFromJs(!this.asBoolean()); 191 | } 192 | 193 | /** 194 | * This method implements the `<` operator. It checks whether the `rhs` value is lower than this value. 195 | */ 196 | less(rhs: NixType): NixBool { 197 | throw invalidTypeError( 198 | this, 199 | err`Cannot compare ${errType(this)} with ${errType(rhs)}`, 200 | ); 201 | } 202 | 203 | lessEq(rhs: NixType): NixBool { 204 | return rhs.less(this).invert(); 205 | } 206 | 207 | more(rhs: NixType): NixBool { 208 | return rhs.less(this); 209 | } 210 | 211 | moreEq(rhs: NixType): NixBool { 212 | return this.less(rhs).invert(); 213 | } 214 | 215 | mul(rhs: NixType): NixInt | NixFloat { 216 | throw invalidTypeError( 217 | this, 218 | err`Cannot multiply ${errType(this)} with ${errType(rhs)}`, 219 | ); 220 | } 221 | 222 | neg(): NixInt | NixFloat { 223 | throw invalidTypeError(this, err`Cannot negate ${errType(this)}`); 224 | } 225 | 226 | neq(rhs: NixType): NixBool { 227 | return this.eq(rhs).invert(); 228 | } 229 | 230 | or(rhs: NixType): NixBool { 231 | return nixBoolFromJs(this.asBoolean() || rhs.asBoolean()); 232 | } 233 | 234 | select(attrPath: NixType[], defaultValue: NixType | undefined): NixType { 235 | throw invalidTypeError( 236 | this, 237 | err`Cannot select attribute from ${errType(this)}`, 238 | ); 239 | } 240 | 241 | /** 242 | * This method implements the `-` operator. It subtracts the `rhs` value from this value. 243 | */ 244 | sub(rhs: NixType): NixInt | NixFloat { 245 | throw invalidTypeError( 246 | this, 247 | err`Cannot subtract ${errType(rhs)} from ${errType(this)}`, 248 | ); 249 | } 250 | 251 | /** 252 | * Converts this Nix value into a JavaScript value. 253 | */ 254 | abstract toJs(): any; 255 | 256 | /** 257 | * If this nix value is lazy this method computes the value stored 258 | * by the lazy value and returns it. Otherwise this method returns 259 | * the value itself. 260 | */ 261 | toStrict(): NixType { 262 | return this; 263 | } 264 | 265 | /** 266 | * Returns a human-readable name of the type of this value. 267 | */ 268 | abstract typeOf(): string; 269 | 270 | /** 271 | * Returns a human-readable string representation of this value, that can be inserted into a sentence. 272 | * 273 | * For example, "a string", "an array", etc. 274 | * 275 | * Static functions can't be made abstract, so abstract is omitted here. 276 | */ 277 | static toHumanReadable(): string { 278 | throw new Error("abstract"); 279 | } 280 | 281 | /** 282 | * Returns the name of the type (as a string, which effectively acts as an enum). 283 | * 284 | * This is used for identifying types for error messages. 285 | */ 286 | static toTypeName(): NixTypeName { 287 | throw new Error("abstract"); 288 | } 289 | 290 | /** 291 | * Returns a new attrset whose attributes are a union of this attrset and the right-hand-side attrset. 292 | * The values are taken from the right-hand-side attrset or from this attrset. Values from the 293 | * right-hand-side attrset override values from this attrset. 294 | */ 295 | update(rhs: NixType): Attrset { 296 | throw invalidTypeError( 297 | this, 298 | err`Cannot merge ${errType(this)} with ${errType(rhs)}`, 299 | ); 300 | } 301 | } 302 | 303 | export class NixBool extends NixType { 304 | readonly value: boolean; 305 | 306 | constructor(value: boolean) { 307 | super(); 308 | this.value = value; 309 | } 310 | 311 | override asBoolean(): boolean { 312 | return this.value; 313 | } 314 | 315 | typeOf(): string { 316 | return "bool"; 317 | } 318 | 319 | static toHumanReadable(): string { 320 | return "a boolean"; 321 | } 322 | 323 | static toTypeName(): NixTypeName { 324 | return "bool"; 325 | } 326 | 327 | toJs(): boolean { 328 | return this.value; 329 | } 330 | 331 | override eq(rhs: NixType): NixBool { 332 | rhs = rhs.toStrict(); 333 | if (!(rhs instanceof NixBool)) { 334 | return FALSE; 335 | } 336 | return nixBoolFromJs(this.value === rhs.value); 337 | } 338 | } 339 | 340 | export abstract class Attrset extends NixType implements Scope { 341 | override eq(rhs: NixType): NixBool { 342 | rhs = rhs.toStrict(); 343 | if (!(rhs instanceof Attrset)) { 344 | return FALSE; 345 | } 346 | if (this.size() !== rhs.size()) { 347 | return FALSE; 348 | } 349 | for (const key of this.keys()) { 350 | if (!this.lookup(key).eq(rhs.lookup(key)).value) { 351 | return FALSE; 352 | } 353 | } 354 | return TRUE; 355 | } 356 | 357 | /** 358 | * Returns raw lazy values without evaluating them. 359 | * Keys of this attrset will be strictly evaluated before this method returns. 360 | * @param attrName the attribute name (the key) for which to fetch the value. 361 | * @returns the value or the lazy placeholder of the value, or `undefined`, if the 362 | * attribute doesn't exist. 363 | */ 364 | get(attrName: NixType): undefined | NixType { 365 | attrName = attrName.toStrict(); 366 | if (!(attrName instanceof NixString)) { 367 | throw typeMismatchError( 368 | attrName, 369 | NixString, 370 | err`Attribute name must be ${errType(NixString)}, but got ${errType(attrName)}`, 371 | ); 372 | } 373 | return this.lookup(attrName.value); 374 | } 375 | 376 | /** 377 | * Same as the `get(attrName: NixType)` function, but the `attrName` parameter is 378 | * a JavaScript string. 379 | */ 380 | lookup(attrName: string): NixType { 381 | return this.underlyingMap().get(attrName); 382 | } 383 | 384 | override has(attrPath: NixType[]): NixBool { 385 | let foundValue: NixType = this; 386 | for (const attrName of attrPath) { 387 | // It could be that the given value is still lazy. If we want to check 388 | // if the value is an attrset, we need to evaluate the Lazy value. 389 | foundValue = foundValue.toStrict(); 390 | if (!(foundValue instanceof Attrset)) { 391 | return FALSE; 392 | } 393 | foundValue = foundValue.get(attrName); 394 | } 395 | return nixBoolFromJs(foundValue !== undefined); 396 | } 397 | 398 | /** 399 | * Returns an iterable of attribute names. The keys of this attrset will 400 | * all be strictly evaluated before this method returns the iterable. 401 | * Note that values will remain unevaluated (unless they are used in attribute 402 | * names). 403 | * @returns an iterable of attribute names in this attrset. 404 | */ 405 | keys(): Iterable { 406 | return this.underlyingMap().keys(); 407 | } 408 | 409 | override select( 410 | attrPath: NixType[], 411 | defaultValue: NixType | undefined, 412 | ): NixType { 413 | let curAttrset: Attrset = this; 414 | const nestingDepth = attrPath.length - 1; 415 | for (let nestingLevel = 0; nestingLevel < nestingDepth; nestingLevel++) { 416 | const attrName = attrPath[nestingLevel]; 417 | let nestedValue = curAttrset.get(attrName); 418 | if (nestedValue === undefined) { 419 | return defaultValue; 420 | } 421 | let nestedAttrset = nestedValue.toStrict(); 422 | if (!(nestedAttrset instanceof Attrset)) { 423 | return defaultValue; 424 | } 425 | curAttrset = nestedAttrset; 426 | } 427 | 428 | let value = curAttrset.get(attrPath[nestingDepth]); 429 | 430 | if (value === undefined) { 431 | if (defaultValue === undefined) { 432 | throw missingAttributeError(attrPath.map((attr) => attr.asString())); 433 | } 434 | return defaultValue; 435 | } 436 | 437 | return value; 438 | } 439 | 440 | /** 441 | * The number of keys in this attrset. 442 | */ 443 | size(): number { 444 | return this.underlyingMap().size; 445 | } 446 | 447 | typeOf(): string { 448 | return "set"; 449 | } 450 | 451 | static toHumanReadable(): string { 452 | return "a set"; 453 | } 454 | 455 | static toTypeName(): NixTypeName { 456 | return "set"; 457 | } 458 | 459 | /** 460 | * Returns a copy of this attrset as a strict (fully-evaluated) JavaScript Map. 461 | */ 462 | toJs(): Map { 463 | let jsMap = new Map(); 464 | for (const key of this.keys()) { 465 | let value = this.lookup(key).toJs(); 466 | jsMap.set(key, value); 467 | } 468 | return jsMap; 469 | } 470 | 471 | /** 472 | * Returns the underlying JS Map fully populated with strict keys (values will remain untouched, i.e. lazy). 473 | * This should return the actual backing map of this attrset, not a copy. 474 | */ 475 | abstract underlyingMap(): Map; 476 | 477 | override update(rhs: NixType): Attrset { 478 | rhs = rhs.toStrict(); 479 | if (!(rhs instanceof Attrset)) { 480 | return super.update(rhs); 481 | } 482 | let mergedMap = new Map(this.underlyingMap()); 483 | for (const attr of rhs.keys()) { 484 | mergedMap.set(attr, rhs.lookup(attr)); 485 | } 486 | return new StrictAttrset(mergedMap); 487 | } 488 | } 489 | 490 | export class StrictAttrset extends Attrset { 491 | readonly map: Map; 492 | 493 | constructor(map: Map) { 494 | super(); 495 | this.map = map; 496 | } 497 | 498 | underlyingMap(): Map { 499 | return this.map; 500 | } 501 | } 502 | 503 | export const EMPTY_ATTRSET = new StrictAttrset(new Map()); 504 | export type AttrsetBody = ( 505 | ctx: EvalCtx, 506 | ) => [attrPath: NixType[], value: NixType][]; 507 | 508 | class AttrsetBuilder implements Scope { 509 | attrsetBody: AttrsetBody; 510 | entries: [attrPath: NixType[], value: NixType][]; 511 | evalCtx: EvalCtx; 512 | // The final map into which this builder will insert fully-evaluated 513 | // attrnames and their corresponding values. 514 | map: Map; 515 | // The index of the next entry to be processed when building the attrset. 516 | pendingEntryIdx: number = 0; 517 | 518 | constructor( 519 | evalCtx: EvalCtx, 520 | isRecursive: boolean, 521 | attrsetBody: AttrsetBody, 522 | ) { 523 | this.evalCtx = isRecursive ? evalCtx.withShadowingScope(this) : evalCtx; 524 | this.attrsetBody = attrsetBody; 525 | } 526 | 527 | build(): Map { 528 | // This method is re-entrant. This means that at any point while 529 | // evaluating this method, this method might be called again. So, 530 | // every re-entrant call must make some progress or detect 531 | // infinite recursion. 532 | let map = this.underlyingMap(); 533 | while (this.pendingEntryIdx < this.entries.length) { 534 | const currentEntryIdx = this.pendingEntryIdx++; 535 | const [attrPath, value] = this.entries[currentEntryIdx]; 536 | if (attrPath.length === 0) { 537 | throw otherError( 538 | "Cannot add an undefined attribute name to the attrset.", 539 | "attrset-add-undefined-attrname", 540 | ); 541 | } 542 | const attrName = attrPath[0].toStrict(); 543 | const currentValue = _attrPathToValue(this.evalCtx, attrPath, value); 544 | 545 | if (currentValue === undefined) { 546 | continue; 547 | } 548 | 549 | const attrNameStr = attrName.asString(); 550 | const existingValue = map.get(attrNameStr); 551 | let newValue = 552 | existingValue !== undefined 553 | ? new Lazy(this.evalCtx, (ctx) => 554 | _recursiveDisjointMerge(ctx, existingValue, currentValue, [ 555 | attrNameStr, 556 | ]), 557 | ) 558 | : currentValue; 559 | 560 | map.set(attrNameStr, newValue); 561 | } 562 | return map; 563 | } 564 | 565 | lookup(attrName: string): NixType { 566 | return this.build().get(attrName); 567 | } 568 | 569 | underlyingMap(): Map { 570 | if (this.map === undefined) { 571 | this.entries = this.attrsetBody(this.evalCtx); 572 | this.attrsetBody = undefined; 573 | this.map = new Map(); 574 | } 575 | return this.map; 576 | } 577 | } 578 | 579 | function _recursiveDisjointMerge( 580 | ctx: EvalCtx, 581 | lhs: NixType, 582 | rhs: NixType, 583 | attrPath: string[], 584 | ): Attrset { 585 | const lhsAttrset = _assertIsMergeable(lhs, attrPath); 586 | const rhsAttrset = _assertIsMergeable(rhs, attrPath); 587 | 588 | let mergedMap = new Map(lhsAttrset.underlyingMap()); 589 | for (const nestedAttrName of rhsAttrset.keys()) { 590 | let existingValue = mergedMap.get(nestedAttrName); 591 | let newValue = rhsAttrset.lookup(nestedAttrName); 592 | 593 | if (existingValue === undefined) { 594 | mergedMap.set(nestedAttrName, newValue); 595 | continue; 596 | } 597 | 598 | let mergedNestedValue = new Lazy(ctx, (ctx) => 599 | _recursiveDisjointMerge(ctx, existingValue, newValue, [ 600 | ...attrPath, 601 | nestedAttrName, 602 | ]), 603 | ); 604 | mergedMap.set(nestedAttrName, mergedNestedValue); 605 | } 606 | return new StrictAttrset(mergedMap); 607 | } 608 | 609 | function _assertIsMergeable(value: NixType, attrPath: string[]): Attrset { 610 | const valueStrict = value.toStrict(); 611 | if (!(valueStrict instanceof Attrset)) { 612 | throw typeMismatchError( 613 | valueStrict, 614 | Attrset, 615 | err`Cannot merge ${errType(valueStrict)} with ${errType(Attrset)}`, 616 | ); 617 | } 618 | return valueStrict; 619 | } 620 | 621 | export class LazyAttrset extends Attrset { 622 | attrsetBuilder: AttrsetBuilder; 623 | map: Map; 624 | 625 | constructor(evalCtx: EvalCtx, isRecursive: boolean, entries: AttrsetBody) { 626 | super(); 627 | this.attrsetBuilder = new AttrsetBuilder(evalCtx, isRecursive, entries); 628 | } 629 | 630 | underlyingMap(): Map { 631 | if (this.map === undefined) { 632 | this.map = this.attrsetBuilder.build(); 633 | this.attrsetBuilder = undefined; 634 | } 635 | return this.map; 636 | } 637 | } 638 | 639 | export class NixFloat extends NixType { 640 | readonly value: number; 641 | 642 | constructor(value: number) { 643 | super(); 644 | this.value = value; 645 | } 646 | 647 | override add(rhs: NixType): NixType { 648 | rhs = rhs.toStrict(); 649 | if (rhs instanceof NixFloat) { 650 | return new NixFloat(this.value + rhs.value); 651 | } 652 | if (rhs instanceof NixInt) { 653 | return new NixFloat(this.value + rhs.number); 654 | } 655 | return super.add(rhs); 656 | } 657 | 658 | override div(rhs: NixType): NixInt | NixFloat { 659 | rhs = rhs.toStrict(); 660 | if (rhs instanceof NixInt) { 661 | return new NixFloat(this.value / rhs.number); 662 | } 663 | if (rhs instanceof NixFloat) { 664 | return new NixFloat(this.value / rhs.value); 665 | } 666 | return super.div(rhs); 667 | } 668 | 669 | override eq(rhs: NixType): NixBool { 670 | rhs = rhs.toStrict(); 671 | if (rhs instanceof NixInt) { 672 | return nixBoolFromJs(this.value === rhs.number); 673 | } 674 | if (rhs instanceof NixFloat) { 675 | return nixBoolFromJs(this.value === rhs.value); 676 | } 677 | return FALSE; 678 | } 679 | 680 | override less(rhs: NixType): NixBool { 681 | rhs = rhs.toStrict(); 682 | if (rhs instanceof NixInt) { 683 | return nixBoolFromJs(this.value < rhs.number); 684 | } 685 | if (rhs instanceof NixFloat) { 686 | return nixBoolFromJs(this.value < rhs.value); 687 | } 688 | return super.less(rhs); 689 | } 690 | 691 | override mul(rhs: NixType): NixFloat | NixInt { 692 | rhs = rhs.toStrict(); 693 | if (rhs instanceof NixInt) { 694 | return new NixFloat(this.value * rhs.number); 695 | } 696 | if (rhs instanceof NixFloat) { 697 | return new NixFloat(this.value * rhs.value); 698 | } 699 | return super.mul(rhs); 700 | } 701 | 702 | override neg(): NixFloat | NixInt { 703 | return new NixFloat(-this.value); 704 | } 705 | 706 | override sub(rhs: NixType): NixInt | NixFloat { 707 | rhs = rhs.toStrict(); 708 | if (rhs instanceof NixInt) { 709 | return new NixFloat(this.value - rhs.number); 710 | } 711 | if (rhs instanceof NixFloat) { 712 | return new NixFloat(this.value - rhs.value); 713 | } 714 | return super.sub(rhs); 715 | } 716 | 717 | toJs(): any { 718 | return this.value; 719 | } 720 | 721 | typeOf(): string { 722 | return "float"; 723 | } 724 | 725 | static toHumanReadable(): string { 726 | return "a float"; 727 | } 728 | 729 | static toTypeName(): NixTypeName { 730 | return "float"; 731 | } 732 | } 733 | 734 | export class NixInt extends NixType { 735 | readonly value: BigInt64Array; 736 | 737 | constructor(value: bigint) { 738 | super(); 739 | this.value = new BigInt64Array(1); 740 | this.value[0] = value; 741 | } 742 | 743 | get number(): number { 744 | return Number(this.value[0]); 745 | } 746 | 747 | get int64(): bigint { 748 | return this.value[0]; 749 | } 750 | 751 | override add(rhs: NixType): NixType { 752 | rhs = rhs.toStrict(); 753 | if (rhs instanceof NixInt) { 754 | return new NixInt(this.int64 + rhs.int64); 755 | } 756 | if (rhs instanceof NixFloat) { 757 | return new NixFloat(this.number + rhs.value); 758 | } 759 | return super.add(rhs); 760 | } 761 | 762 | override div(rhs: NixType): NixInt | NixFloat { 763 | rhs = rhs.toStrict(); 764 | if (rhs instanceof NixInt) { 765 | return new NixInt(this.int64 / rhs.int64); 766 | } 767 | if (rhs instanceof NixFloat) { 768 | return new NixFloat(this.number / rhs.value); 769 | } 770 | return super.div(rhs); 771 | } 772 | 773 | override eq(rhs: NixType): NixBool { 774 | rhs = rhs.toStrict(); 775 | if (rhs instanceof NixInt) { 776 | return nixBoolFromJs(this.int64 === rhs.int64); 777 | } 778 | if (rhs instanceof NixFloat) { 779 | return nixBoolFromJs(this.number === rhs.value); 780 | } 781 | return super.eq(rhs); 782 | } 783 | 784 | override less(rhs: NixType): NixBool { 785 | rhs = rhs.toStrict(); 786 | if (rhs instanceof NixInt) { 787 | return nixBoolFromJs(this.int64 < rhs.int64); 788 | } 789 | if (rhs instanceof NixFloat) { 790 | return nixBoolFromJs(this.number < rhs.value); 791 | } 792 | return super.less(rhs); 793 | } 794 | 795 | override mul(rhs: NixType): NixInt | NixFloat { 796 | rhs = rhs.toStrict(); 797 | if (rhs instanceof NixInt) { 798 | return new NixInt(this.int64 * rhs.int64); 799 | } 800 | if (rhs instanceof NixFloat) { 801 | return new NixFloat(this.number * rhs.value); 802 | } 803 | return super.mul(rhs); 804 | } 805 | 806 | override neg(): NixInt | NixFloat { 807 | return new NixInt(-this.int64); 808 | } 809 | 810 | override sub(rhs: NixType): NixInt | NixFloat { 811 | rhs = rhs.toStrict(); 812 | if (rhs instanceof NixInt) { 813 | return new NixInt(this.int64 - rhs.int64); 814 | } 815 | if (rhs instanceof NixFloat) { 816 | return new NixFloat(this.number - rhs.value); 817 | } 818 | return super.sub(rhs); 819 | } 820 | 821 | toJs(): bigint { 822 | return this.int64; 823 | } 824 | 825 | typeOf(): string { 826 | return "int"; 827 | } 828 | 829 | static toHumanReadable(): string { 830 | return "an int"; 831 | } 832 | 833 | static toTypeName(): NixTypeName { 834 | return "int"; 835 | } 836 | } 837 | 838 | export class NixList extends NixType { 839 | readonly values: NixType[]; 840 | 841 | constructor(values: NixType[]) { 842 | super(); 843 | this.values = values; 844 | } 845 | 846 | override concat(other: NixType): NixList { 847 | other = other.toStrict(); 848 | if (other instanceof NixList) { 849 | return new NixList(this.values.concat(other.values)); 850 | } 851 | return super.concat(other); 852 | } 853 | 854 | override eq(rhs: NixType): NixBool { 855 | rhs = rhs.toStrict(); 856 | if (!(rhs instanceof NixList)) { 857 | return FALSE; 858 | } 859 | if (this.values.length !== rhs.values.length) { 860 | return FALSE; 861 | } 862 | for (let idx = 0; idx < this.values.length; idx++) { 863 | if (!this.values[idx].eq(rhs.values[idx]).value) { 864 | return FALSE; 865 | } 866 | } 867 | return TRUE; 868 | } 869 | 870 | override less(rhs: NixType): NixBool { 871 | rhs = rhs.toStrict(); 872 | if (!(rhs instanceof NixList)) { 873 | return super.less(rhs); 874 | } 875 | 876 | const minLen = Math.min(this.values.length, rhs.values.length); 877 | for (let idx = 0; idx < minLen; idx++) { 878 | const currentLhs = this.values[idx].toStrict(); 879 | const currentRhs = rhs.values[idx].toStrict(); 880 | // This special-casing for booleans and nulls replicates nix's behaviour. Some examples: 881 | // - nix evaluates this: `[true] < [true] == false` rather than throwing an exception, 882 | // - the same for `[false] < [false] == false`, and 883 | // - the same for `[null] < [null] == false`. 884 | if ( 885 | (currentLhs === TRUE && currentRhs === TRUE) || 886 | (currentLhs === FALSE && currentRhs === FALSE) 887 | ) { 888 | continue; 889 | } 890 | if (currentLhs === NULL && currentRhs === NULL) { 891 | continue; 892 | } 893 | if (currentLhs.less(currentRhs).value) { 894 | return TRUE; 895 | } 896 | } 897 | return nixBoolFromJs(this.values.length < rhs.values.length); 898 | } 899 | 900 | toJs(): NixType[] { 901 | return this.values.map((element) => element.toJs()); 902 | } 903 | 904 | typeOf(): string { 905 | return "list"; 906 | } 907 | 908 | static toHumanReadable(): string { 909 | return "a list"; 910 | } 911 | 912 | static toTypeName(): NixTypeName { 913 | return "list"; 914 | } 915 | } 916 | 917 | export class NixNull extends NixType { 918 | override eq(rhs: NixType): NixBool { 919 | return nixBoolFromJs(rhs.toStrict() instanceof NixNull); 920 | } 921 | 922 | toJs(): boolean { 923 | return null; 924 | } 925 | 926 | typeOf(): string { 927 | return "null"; 928 | } 929 | 930 | static toHumanReadable(): string { 931 | return "a null"; 932 | } 933 | 934 | static toTypeName(): NixTypeName { 935 | return "null"; 936 | } 937 | } 938 | 939 | export class NixString extends NixType { 940 | readonly value: string; 941 | 942 | constructor(value: string) { 943 | super(); 944 | this.value = value; 945 | } 946 | 947 | override add(rhs: NixType): NixType { 948 | rhs = rhs.toStrict(); 949 | if (rhs instanceof NixString) { 950 | return new NixString(this.value + rhs.value); 951 | } 952 | if (rhs instanceof Path) { 953 | return new NixString(normalizePath(this.value + rhs.path)); 954 | } 955 | return super.add(rhs); 956 | } 957 | 958 | override asString(): string { 959 | return this.value; 960 | } 961 | 962 | override eq(rhs: NixType): NixBool { 963 | rhs = rhs.toStrict(); 964 | if (!(rhs instanceof NixString)) { 965 | return FALSE; 966 | } 967 | return nixBoolFromJs(this.value === rhs.value); 968 | } 969 | 970 | override less(rhs: NixType): NixBool { 971 | rhs = rhs.toStrict(); 972 | if (!(rhs instanceof NixString)) { 973 | return super.less(rhs); 974 | } 975 | return nixBoolFromJs(this.value < rhs.value); 976 | } 977 | 978 | toJs(): string { 979 | return this.value; 980 | } 981 | 982 | typeOf(): string { 983 | return "string"; 984 | } 985 | 986 | static toHumanReadable(): string { 987 | return "a string"; 988 | } 989 | 990 | static toTypeName(): NixTypeName { 991 | return "string"; 992 | } 993 | } 994 | 995 | export class Path extends NixType { 996 | readonly path: string; 997 | 998 | constructor(path: string) { 999 | super(); 1000 | this.path = path; 1001 | } 1002 | 1003 | override add(rhs: NixType): NixType { 1004 | rhs = rhs.toStrict(); 1005 | if (rhs instanceof Path) { 1006 | return new Path(normalizePath(joinPaths(this.path, rhs.path))); 1007 | } 1008 | if (rhs instanceof NixString) { 1009 | return new Path(normalizePath(this.path + rhs.value)); 1010 | } 1011 | return this; 1012 | } 1013 | 1014 | override less(rhs: NixType): NixBool { 1015 | rhs = rhs.toStrict(); 1016 | if (!(rhs instanceof Path)) { 1017 | return super.less(rhs); 1018 | } 1019 | return nixBoolFromJs(this.path < rhs.path); 1020 | } 1021 | 1022 | asString(): string { 1023 | return this.path; 1024 | } 1025 | 1026 | toJs() { 1027 | return this.path; 1028 | } 1029 | 1030 | typeOf(): string { 1031 | return "path"; 1032 | } 1033 | 1034 | static toHumanReadable(): string { 1035 | return "a path"; 1036 | } 1037 | 1038 | static toTypeName(): NixTypeName { 1039 | return "path"; 1040 | } 1041 | } 1042 | 1043 | export class Lazy extends NixType { 1044 | body: Body; 1045 | evalCtx: EvalCtx; 1046 | value: NixType; 1047 | 1048 | constructor(evalCtx: EvalCtx, body: Body) { 1049 | super(); 1050 | this.body = body; 1051 | this.evalCtx = evalCtx; 1052 | } 1053 | 1054 | override add(rhs: NixType): NixType { 1055 | return this.toStrict().add(rhs); 1056 | } 1057 | 1058 | override and(rhs: NixType): NixBool { 1059 | return this.toStrict().and(rhs); 1060 | } 1061 | 1062 | override apply(param: NixType): NixType { 1063 | return this.toStrict().apply(param); 1064 | } 1065 | 1066 | override asBoolean(): boolean { 1067 | return this.toStrict().asBoolean(); 1068 | } 1069 | 1070 | override asString(): string { 1071 | return this.toStrict().asString(); 1072 | } 1073 | 1074 | override concat(other: NixType): NixList { 1075 | return this.toStrict().concat(other); 1076 | } 1077 | 1078 | override div(rhs: NixType): NixInt | NixFloat { 1079 | return this.toStrict().div(rhs); 1080 | } 1081 | 1082 | override eq(rhs: NixType): NixBool { 1083 | return this.toStrict().eq(rhs); 1084 | } 1085 | 1086 | override has(attrPath: NixType[]): NixBool { 1087 | return this.toStrict().has(attrPath); 1088 | } 1089 | 1090 | override implication(rhs: NixType): NixBool { 1091 | return this.toStrict().implication(rhs); 1092 | } 1093 | 1094 | override invert(): NixBool { 1095 | return this.toStrict().invert(); 1096 | } 1097 | 1098 | override less(rhs: NixType): NixBool { 1099 | return this.toStrict().less(rhs); 1100 | } 1101 | 1102 | override lessEq(rhs: NixType): NixBool { 1103 | return this.toStrict().lessEq(rhs); 1104 | } 1105 | 1106 | override more(rhs: NixType): NixBool { 1107 | return this.toStrict().more(rhs); 1108 | } 1109 | 1110 | override moreEq(rhs: NixType): NixBool { 1111 | return this.toStrict().moreEq(rhs); 1112 | } 1113 | 1114 | override mul(rhs: NixType): NixInt | NixFloat { 1115 | return this.toStrict().mul(rhs); 1116 | } 1117 | 1118 | override neg(): NixInt | NixFloat { 1119 | return this.toStrict().neg(); 1120 | } 1121 | 1122 | override neq(rhs: NixType): NixBool { 1123 | return this.toStrict().neq(rhs); 1124 | } 1125 | 1126 | override or(rhs: NixType): NixBool { 1127 | return this.toStrict().or(rhs); 1128 | } 1129 | 1130 | override select( 1131 | attrPath: NixType[], 1132 | defaultValue: NixType | undefined, 1133 | ): NixType { 1134 | return this.toStrict().select(attrPath, defaultValue); 1135 | } 1136 | 1137 | override sub(rhs: NixType): NixInt | NixFloat { 1138 | return this.toStrict().sub(rhs); 1139 | } 1140 | 1141 | toJs() { 1142 | return this.toStrict().toJs(); 1143 | } 1144 | 1145 | override toStrict(): NixType { 1146 | if (this.value === undefined) { 1147 | this.value = this.body(this.evalCtx); 1148 | // Now that we have evaluated this lazy value already, we don't have to do it again. 1149 | // This means we can let go of the `body` and the `evalCtx` so they can be garbage-collected. 1150 | this.body = undefined; 1151 | this.evalCtx = undefined; 1152 | 1153 | // Let's flatten any nested lazy values. 1154 | this.value = this.value.toStrict(); 1155 | } 1156 | return this.value; 1157 | } 1158 | 1159 | typeOf(): string { 1160 | return this.toStrict().typeOf(); 1161 | } 1162 | 1163 | static toHumanReadable(): string { 1164 | // This static method should never be called 1165 | throw new Error("Lazy value isn't a real type"); 1166 | } 1167 | 1168 | static toTypeName(): NixTypeName { 1169 | // This static method should never be called 1170 | throw new Error("Lazy value isn't a real type"); 1171 | } 1172 | } 1173 | 1174 | export class Lambda extends NixType { 1175 | body: (param: NixType) => NixType; 1176 | 1177 | constructor(body: (param: NixType) => NixType) { 1178 | super(); 1179 | this.body = body; 1180 | } 1181 | 1182 | override apply(param: NixType): NixType { 1183 | return this.body(param); 1184 | } 1185 | 1186 | toJs(): any { 1187 | return this.body; 1188 | } 1189 | 1190 | typeOf(): string { 1191 | return "lambda"; 1192 | } 1193 | 1194 | static toHumanReadable(): string { 1195 | return "a lambda"; 1196 | } 1197 | 1198 | static toTypeName(): NixTypeName { 1199 | return "lambda"; 1200 | } 1201 | } 1202 | 1203 | export const NULL = new NixNull(); 1204 | export const TRUE = new NixBool(true); 1205 | export const FALSE = new NixBool(false); 1206 | 1207 | // For creating a bool without allocating a new object. 1208 | export function nixBoolFromJs(value: boolean): NixBool { 1209 | return value ? TRUE : FALSE; 1210 | } 1211 | 1212 | // Attrset: 1213 | export function attrset(evalCtx: EvalCtx, entries: AttrsetBody): Attrset { 1214 | return new LazyAttrset(evalCtx, false, entries); 1215 | } 1216 | 1217 | export function recAttrset(evalCtx: EvalCtx, entries: AttrsetBody): Attrset { 1218 | return new LazyAttrset(evalCtx, true, entries); 1219 | } 1220 | 1221 | // Builtins: 1222 | function _createBuiltinsAttrset() { 1223 | const builtinsRecord = getBuiltins(); 1224 | 1225 | const builtins = new Map(); 1226 | 1227 | for (const [name, value] of Object.entries(builtinsRecord)) { 1228 | builtins.set(name, new Lambda(value)); 1229 | } 1230 | 1231 | return new StrictAttrset(builtins); 1232 | } 1233 | 1234 | // Lambda: 1235 | export function paramLambda( 1236 | ctx: EvalCtx, 1237 | paramName: string, 1238 | body: Body, 1239 | ): Lambda { 1240 | return new Lambda((param) => { 1241 | let paramScope = new Map(); 1242 | paramScope.set(paramName, param); 1243 | return letIn(ctx, new StrictAttrset(paramScope), body); 1244 | }); 1245 | } 1246 | 1247 | export function patternLambda( 1248 | ctx: EvalCtx, 1249 | argsBind: string | undefined, 1250 | patterns: [[string, any]], 1251 | body: Body, 1252 | ): any { 1253 | return new Lambda((param: Attrset) => { 1254 | let paramScope = new Map(); 1255 | for (const [paramName, defaultValue] of patterns) { 1256 | let paramValue = param.lookup(paramName); 1257 | if (paramValue === undefined) { 1258 | if (defaultValue === undefined) { 1259 | throw functionCallWithoutArgumentError(paramName); 1260 | } 1261 | paramValue = defaultValue; 1262 | } 1263 | paramScope.set(paramName, paramValue); 1264 | } 1265 | if (argsBind !== undefined) { 1266 | paramScope.set(argsBind, param); 1267 | } 1268 | return letIn(ctx, new StrictAttrset(paramScope), body); 1269 | }); 1270 | } 1271 | 1272 | // Let in: 1273 | export function letIn(evalCtx: EvalCtx, attrs: Scope, body: Body): NixType { 1274 | return body(evalCtx.withShadowingScope(attrs)); 1275 | } 1276 | 1277 | // Path: 1278 | export function toPath(evalCtx: EvalCtx, path: string): Path { 1279 | if (!isAbsolutePath(path)) { 1280 | path = joinPaths(evalCtx.scriptDir, path); 1281 | } 1282 | return new Path(normalizePath(path)); 1283 | } 1284 | 1285 | // Utilities: 1286 | export function recursiveStrict(value: NixType): NixType { 1287 | if (value instanceof Attrset) { 1288 | return recursiveStrictAttrset(value); 1289 | } 1290 | return value; 1291 | } 1292 | 1293 | export function recursiveStrictAttrset(theAttrset: Attrset): Attrset { 1294 | for (const key of theAttrset.keys()) { 1295 | const value = theAttrset.lookup(key).toStrict(); 1296 | recursiveStrict(value); 1297 | } 1298 | return theAttrset; 1299 | } 1300 | 1301 | /** 1302 | * If given an attrset entry like `a = value`, then this function returns just the given value. 1303 | * If the attrset has multiple segments (e.g. `a.b.c = value`), then this function returns 1304 | * a nested attrset (e.g. `{ b = { c = value; }; }`). 1305 | */ 1306 | function _attrPathToValue( 1307 | ctx: EvalCtx, 1308 | attrPath: NixType[], 1309 | value: NixType, 1310 | ): undefined | NixType { 1311 | if (attrPath.length === 0) { 1312 | throw otherError( 1313 | "Unexpected attr path of zero length.", 1314 | "attrset-attrpath-zero-length", 1315 | ); 1316 | } 1317 | 1318 | let attrName = attrPath[0].toStrict(); 1319 | 1320 | // It turns out `null` attrnames are ignored by nix. 1321 | if (attrName === NULL) { 1322 | return undefined; 1323 | } 1324 | 1325 | if (attrPath.length === 1) { 1326 | // The attr path has only one segment (e.g. `a = 1;`). 1327 | return value; 1328 | } 1329 | 1330 | return new Lazy(ctx, (ctx) => { 1331 | let nestedValue = _attrPathToValue(ctx, attrPath.slice(1), value); 1332 | if (nestedValue === undefined) { 1333 | return EMPTY_ATTRSET; 1334 | } 1335 | 1336 | let map = new Map(); 1337 | map.set(attrPath[1].asString(), nestedValue); 1338 | return new StrictAttrset(map); 1339 | }); 1340 | } 1341 | 1342 | function _buildGlobalScope() { 1343 | const scope = new Map(); 1344 | const builtins = _createBuiltinsAttrset(); 1345 | scope.set("builtins", builtins); 1346 | 1347 | // Nix makes some builtins available directly in the global scope: 1348 | scope.set("abort", builtins.lookup("abort")); 1349 | 1350 | return new GlobalScope(scope); 1351 | } 1352 | 1353 | // With: 1354 | export function withExpr( 1355 | evalCtx: EvalCtx, 1356 | namespace: Attrset, 1357 | body: Body, 1358 | ): any { 1359 | return body(evalCtx.withNonShadowingScope(namespace)); 1360 | } 1361 | 1362 | export const allNixTypeClasses = [ 1363 | NixBool, 1364 | NixFloat, 1365 | NixInt, 1366 | NixList, 1367 | NixNull, 1368 | NixString, 1369 | Path, 1370 | Lazy, 1371 | Lambda, 1372 | Attrset, 1373 | ]; 1374 | 1375 | export type NixTypeName = 1376 | | "bool" 1377 | | "float" 1378 | | "int" 1379 | | "list" 1380 | | "null" 1381 | | "string" 1382 | | "path" 1383 | | "lambda" 1384 | | "set"; 1385 | 1386 | export type NixTypeClass = (typeof allNixTypeClasses)[number]; 1387 | export type NixTypeInstance = InstanceType; 1388 | -------------------------------------------------------------------------------- /nixjs-rt/src/testUtils.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach } from "@jest/globals"; 2 | import { NixType, NixString, EvalCtx, AttrsetBody } from "./lib"; 3 | import { execSync } from "child_process"; 4 | 5 | let _evalCtx: EvalCtx | null = null; 6 | export function evalCtx() { 7 | if (_evalCtx === null) { 8 | _evalCtx = new EvalCtx("/test_base"); 9 | } 10 | return _evalCtx; 11 | } 12 | 13 | export function toAttrpath(attrPathStr: string): NixType[] { 14 | return attrPathStr.split(".").map((val) => new NixString(val) as NixType); 15 | } 16 | 17 | export function getBuiltin(builtinName: string): NixType { 18 | return evalCtx() 19 | .lookup("builtins") 20 | .select([new NixString(builtinName)], undefined); 21 | } 22 | 23 | export function keyVals( 24 | ...entries: [attrpathStr: string, value: NixType][] 25 | ): AttrsetBody { 26 | return (_ctx) => entries.map((entry) => [toAttrpath(entry[0]), entry[1]]); 27 | } 28 | -------------------------------------------------------------------------------- /nixjs-rt/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { NixBool, NixNull } from "./lib"; 2 | 3 | export function isAbsolutePath(path: string): boolean { 4 | return path.startsWith("/"); 5 | } 6 | 7 | export function joinPaths(abs_base: string, path: string): string { 8 | return `${abs_base}/${path}`; 9 | } 10 | 11 | export function normalizePath(path: string): string { 12 | let segments = path.split("/"); 13 | let normalizedSegments: string[] = []; 14 | for (const segment of segments) { 15 | switch (segment) { 16 | case "": 17 | break; 18 | case ".": 19 | break; 20 | case "..": 21 | normalizedSegments.pop(); 22 | break; 23 | default: 24 | normalizedSegments.push(segment); 25 | break; 26 | } 27 | } 28 | return (isAbsolutePath(path) ? "/" : "") + normalizedSegments.join("/"); 29 | } 30 | 31 | export function dirOf(path: string) { 32 | // Return everything before the final slash 33 | const lastSlash = path.lastIndexOf("/"); 34 | return path.substring(0, lastSlash); 35 | } 36 | -------------------------------------------------------------------------------- /nixjs-rt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "ES2022", 5 | "module": "ES2022", 6 | "moduleResolution": "Bundler", 7 | "forceConsistentCasingInFileNames": true, 8 | "esModuleInterop": true, 9 | "inlineSourceMap": true, 10 | "typeRoots": ["src/globals.d.ts"] 11 | }, 12 | "include": ["src/**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.79.0" 3 | components = [ "clippy", "rust-analyzer", "rustfmt" ] 4 | profile = "minimal" -------------------------------------------------------------------------------- /src/cmd/eval.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::cmd::{to_cmd_err, RixSubCommand}; 4 | use crate::eval::error::NixError; 5 | use crate::eval::execution; 6 | use crate::eval::types::Value; 7 | use clap::{Arg, ArgAction, ArgMatches}; 8 | 9 | pub fn cmd() -> RixSubCommand { 10 | RixSubCommand { 11 | name: "eval", 12 | handler: |args| to_cmd_err(handle_cmd(args)), 13 | cmd: |subcommand| { 14 | subcommand 15 | .about("evaluates the given expression and prints the result") 16 | .arg(Arg::new("INSTALLABLE").help("The thing to evaluate.")) 17 | .arg( 18 | Arg::new("expr") 19 | .long("expr") 20 | .action(ArgAction::Set) 21 | .help("The expression to evaluate. Installables are treated as attribute paths of the attrset returned by the expression."), 22 | ) 23 | }, 24 | } 25 | } 26 | 27 | pub fn handle_cmd(parsed_args: &ArgMatches) -> Result<(), NixError> { 28 | let expr = parsed_args 29 | .get_one::("expr") 30 | .ok_or("You must use the '--expr' option. Nothing else is implemented :)")?; 31 | 32 | let current_dir = std::env::current_dir().map_err(|_| "Couldn't get the current directory")?; 33 | 34 | print_value(&execution::evaluate(expr, ¤t_dir)?); 35 | println!(); 36 | Ok(()) 37 | } 38 | 39 | fn print_value(value: &Value) { 40 | match value { 41 | Value::AttrSet(hash_map) => print_attrset(hash_map), 42 | Value::Bool(boolean) => print!("{boolean}"), 43 | Value::Float(float) => print!("{float}"), 44 | Value::Int(int) => print!("{int}"), 45 | Value::Lambda => print!(""), 46 | Value::List(vector) => print_list(vector), 47 | Value::Path(string) => print!("\"{string}\""), 48 | Value::Str(string) => print!("\"{string}\""), 49 | } 50 | } 51 | 52 | fn print_list(vector: &Vec) { 53 | print!("[ "); 54 | for value in vector { 55 | print_value(value); 56 | print!(" "); 57 | } 58 | print!("]"); 59 | } 60 | 61 | fn print_attrset(hash_map: &HashMap) { 62 | print!("{{ "); 63 | for (attr_name, value) in hash_map { 64 | print!("{attr_name} = "); 65 | print_value(value); 66 | print!("; "); 67 | } 68 | print!("}}"); 69 | } 70 | -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod eval; 2 | pub mod transpile; 3 | use clap::{ArgMatches, Command}; 4 | use colored::*; 5 | use std::process::ExitCode; 6 | 7 | use crate::eval::error::NixError; 8 | 9 | pub struct RixSubCommand { 10 | pub name: &'static str, 11 | pub cmd: fn(Command) -> Command, 12 | pub handler: fn(&ArgMatches) -> Result<(), ExitCode>, 13 | } 14 | 15 | pub fn print_err(msg: NixError) { 16 | eprintln!("{}: {}", "error".red(), msg); 17 | } 18 | 19 | pub fn to_cmd_err(result: Result<(), NixError>) -> Result<(), ExitCode> { 20 | result.map_err(print_and_err) 21 | } 22 | 23 | pub fn print_and_err(msg: NixError) -> ExitCode { 24 | print_err(msg); 25 | ExitCode::FAILURE 26 | } 27 | -------------------------------------------------------------------------------- /src/cmd/transpile.rs: -------------------------------------------------------------------------------- 1 | use crate::cmd::{to_cmd_err, RixSubCommand}; 2 | use crate::eval::emit_js; 3 | use crate::eval::error::NixError; 4 | use clap::{Arg, ArgAction, ArgMatches}; 5 | 6 | pub fn cmd() -> RixSubCommand { 7 | RixSubCommand { 8 | name: "transpile", 9 | handler: |args| to_cmd_err(handle_cmd(args)), 10 | cmd: |subcommand| { 11 | subcommand 12 | .about("transpiles the given nix expression file into JavaScript.") 13 | .arg(Arg::new("EXPRESSION").help("The nix expression to transpile.")) 14 | .arg( 15 | Arg::new("expr") 16 | .long("expr") 17 | .action(ArgAction::SetTrue) 18 | .help("The 'EXPRESSION' argument will be treated as an expression rather than a file."), 19 | ) 20 | }, 21 | } 22 | } 23 | 24 | pub fn handle_cmd(parsed_args: &ArgMatches) -> Result<(), NixError> { 25 | let expression = parsed_args 26 | .get_one::("EXPRESSION") 27 | .ok_or("You must provide a single expression to transpile.")?; 28 | let is_expression = parsed_args.get_one::("expr").unwrap(); 29 | if *is_expression { 30 | let js_source = emit_js::emit_module(expression) 31 | .map_err(|_err| "Failed to transpile the expression.".to_owned())?; 32 | print!("{js_source}"); 33 | } else { 34 | todo!("Support to transpile files is not yet implemented.") 35 | } 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /src/eval/emit_js.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use rnix::{ast, SyntaxKind}; 4 | use rowan::ast::AstNode; 5 | 6 | pub fn emit_module(nix_expr: &str) -> Result { 7 | let root = rnix::Root::parse(nix_expr).tree(); 8 | let root_expr = root.expr().expect("Not implemented"); 9 | let mut out_src = String::new(); 10 | out_src += "export default (ctx) => "; 11 | emit_expr(&root_expr, &mut out_src)?; 12 | out_src += ";\n"; 13 | Ok(out_src) 14 | } 15 | 16 | fn emit_expr(nix_ast: &ast::Expr, out_src: &mut String) -> Result<(), String> { 17 | match nix_ast { 18 | ast::Expr::Apply(apply) => emit_apply(apply, out_src), 19 | ast::Expr::AttrSet(attrset) => emit_attrset(attrset, out_src), 20 | ast::Expr::BinOp(bin_op) => emit_bin_op(bin_op, out_src), 21 | ast::Expr::HasAttr(has_attr) => emit_has_attr(has_attr, out_src), 22 | ast::Expr::Ident(ident) => emit_ident(ident, out_src), 23 | ast::Expr::IfElse(if_else) => emit_if_else(if_else, out_src), 24 | ast::Expr::Lambda(lambda) => emit_lambda(lambda, out_src), 25 | ast::Expr::LetIn(let_in) => emit_let_in(let_in, out_src), 26 | ast::Expr::List(list) => emit_list(list, out_src), 27 | ast::Expr::Literal(literal) => emit_literal(literal, out_src), 28 | ast::Expr::Paren(paren) => emit_paren(paren, out_src), 29 | ast::Expr::Path(path) => emit_path(path, out_src), 30 | ast::Expr::Select(select) => emit_select_expr(select, out_src), 31 | ast::Expr::Str(string) => emit_string_expr(string, out_src), 32 | ast::Expr::UnaryOp(unary_op) => emit_unary_op(unary_op, out_src), 33 | ast::Expr::With(with) => emit_with(with, out_src), 34 | _ => panic!("emit_expr: not implemented: {:?}", nix_ast), 35 | } 36 | } 37 | 38 | fn emit_apply(apply: &ast::Apply, out_src: &mut String) -> Result<(), String> { 39 | emit_expr( 40 | &apply 41 | .lambda() 42 | .expect("Unexpected lambda application without the lambda."), 43 | out_src, 44 | )?; 45 | out_src.push_str(".apply("); 46 | emit_expr( 47 | &apply 48 | .argument() 49 | .expect("Unexpected lambda application without arguments."), 50 | out_src, 51 | )?; 52 | out_src.push(')'); 53 | Ok(()) 54 | } 55 | 56 | fn emit_attrset(attrset: &ast::AttrSet, out_src: &mut String) -> Result<(), String> { 57 | emit_has_entry(attrset, attrset.rec_token().is_some(), out_src) 58 | } 59 | 60 | fn emit_has_entry( 61 | has_entry: &impl ast::HasEntry, 62 | is_recursive: bool, 63 | out_src: &mut String, 64 | ) -> Result<(), String> { 65 | *out_src += "n."; 66 | *out_src += if is_recursive { 67 | "recAttrset" 68 | } else { 69 | "attrset" 70 | }; 71 | *out_src += "(ctx,(ctx) => ["; 72 | for attrpath_value in has_entry.attrpath_values() { 73 | out_src.push('['); 74 | let attrpath = attrpath_value.attrpath().expect("Not implemented"); 75 | let value = &attrpath_value.value().expect("Not implemented"); 76 | emit_attrpath(&attrpath, out_src)?; 77 | *out_src += ",new n.Lazy(ctx,(ctx) => "; 78 | emit_expr(value, out_src)?; 79 | *out_src += ")],"; 80 | } 81 | *out_src += "])"; 82 | Ok(()) 83 | } 84 | 85 | fn emit_attrpath(attrpath: &ast::Attrpath, out_src: &mut String) -> Result<(), String> { 86 | *out_src += "["; 87 | for attr in attrpath.attrs() { 88 | out_src.push_str("new n.Lazy(ctx,(ctx) =>"); 89 | match attr { 90 | ast::Attr::Ident(ident) => { 91 | emit_nix_string(ident.ident_token().expect("Missing token.").text(), out_src) 92 | } 93 | ast::Attr::Str(str_expression) => emit_string_expr(&str_expression, out_src)?, 94 | ast::Attr::Dynamic(expr) => { 95 | emit_expr(&expr.expr().expect("Expected an expression."), out_src)? 96 | } 97 | } 98 | out_src.push_str("),"); 99 | } 100 | *out_src += "]"; 101 | Ok(()) 102 | } 103 | 104 | fn emit_nix_string(string: &str, out_src: &mut String) { 105 | *out_src += "new n.NixString(\""; 106 | *out_src += string; 107 | *out_src += "\")"; 108 | } 109 | 110 | fn emit_bin_op(bin_op: &ast::BinOp, out_src: &mut String) -> Result<(), String> { 111 | let operator = bin_op.operator().expect("Not implemented"); 112 | let lhs = &bin_op.lhs().expect("Not implemented"); 113 | let rhs = &bin_op.rhs().expect("Not implemented"); 114 | match operator { 115 | // Arithmetic 116 | ast::BinOpKind::Add => emit_nixrt_bin_op(lhs, rhs, "add", out_src)?, 117 | ast::BinOpKind::Div => emit_nixrt_bin_op(lhs, rhs, "div", out_src)?, 118 | ast::BinOpKind::Mul => emit_nixrt_bin_op(lhs, rhs, "mul", out_src)?, 119 | ast::BinOpKind::Sub => emit_nixrt_bin_op(lhs, rhs, "sub", out_src)?, 120 | 121 | // Attrset 122 | ast::BinOpKind::Update => emit_nixrt_bin_op(lhs, rhs, "update", out_src)?, 123 | 124 | // Boolean 125 | ast::BinOpKind::And => emit_nixrt_bin_op(lhs, rhs, "and", out_src)?, 126 | ast::BinOpKind::Implication => emit_nixrt_bin_op(lhs, rhs, "implication", out_src)?, 127 | ast::BinOpKind::Or => emit_nixrt_bin_op(lhs, rhs, "or", out_src)?, 128 | 129 | // Comparison 130 | ast::BinOpKind::Equal => emit_nixrt_bin_op(lhs, rhs, "eq", out_src)?, 131 | ast::BinOpKind::Less => emit_nixrt_bin_op(lhs, rhs, "less", out_src)?, 132 | ast::BinOpKind::LessOrEq => emit_nixrt_bin_op(lhs, rhs, "lessEq", out_src)?, 133 | ast::BinOpKind::More => emit_nixrt_bin_op(lhs, rhs, "more", out_src)?, 134 | ast::BinOpKind::MoreOrEq => emit_nixrt_bin_op(lhs, rhs, "moreEq", out_src)?, 135 | ast::BinOpKind::NotEqual => emit_nixrt_bin_op(lhs, rhs, "neq", out_src)?, 136 | 137 | // List 138 | ast::BinOpKind::Concat => emit_nixrt_bin_op(lhs, rhs, "concat", out_src)?, 139 | } 140 | Ok(()) 141 | } 142 | 143 | fn emit_nixrt_bin_op( 144 | lhs: &ast::Expr, 145 | rhs: &ast::Expr, 146 | nixrt_function: &str, 147 | out_src: &mut String, 148 | ) -> Result<(), String> { 149 | emit_expr(lhs, out_src)?; 150 | out_src.push('.'); 151 | *out_src += nixrt_function; 152 | out_src.push('('); 153 | emit_expr(rhs, out_src)?; 154 | out_src.push(')'); 155 | Ok(()) 156 | } 157 | 158 | fn emit_ident(ident: &ast::Ident, out_src: &mut String) -> Result<(), String> { 159 | let token = ident.ident_token().expect("Unexpected ident without name."); 160 | let token_text = token.text(); 161 | match token_text { 162 | "true" => out_src.push_str("n.TRUE"), 163 | "false" => out_src.push_str("n.FALSE"), 164 | "null" => out_src.push_str("n.NULL"), 165 | _ => { 166 | out_src.push_str("ctx.lookup(\""); 167 | js_string_escape_into(token_text, out_src); 168 | out_src.push_str("\")"); 169 | } 170 | } 171 | Ok(()) 172 | } 173 | 174 | fn emit_has_attr(has_attr: &ast::HasAttr, out_src: &mut String) -> Result<(), String> { 175 | emit_expr(&has_attr.expr().expect("Unreachable"), out_src)?; 176 | *out_src += ".has("; 177 | emit_attrpath(&has_attr.attrpath().expect("Unreachable"), out_src)?; 178 | *out_src += ")"; 179 | Ok(()) 180 | } 181 | 182 | fn emit_if_else(lambda: &ast::IfElse, out_src: &mut String) -> Result<(), String> { 183 | let condition = lambda 184 | .condition() 185 | .expect("Unexpected 'if-then-else' expression without a condition."); 186 | let body = lambda 187 | .body() 188 | .expect("Unexpected 'if-then-else' expression without a body."); 189 | let else_body = lambda 190 | .else_body() 191 | .expect("Unexpected 'if-then-else' expression without an 'else' body."); 192 | emit_expr(&condition, out_src)?; 193 | *out_src += ".asBoolean() ? ("; 194 | emit_expr(&body, out_src)?; 195 | *out_src += ") : ("; 196 | emit_expr(&else_body, out_src)?; 197 | *out_src += ")"; 198 | Ok(()) 199 | } 200 | 201 | fn emit_lambda(lambda: &ast::Lambda, out_src: &mut String) -> Result<(), String> { 202 | let param = lambda 203 | .param() 204 | .expect("Unexpected lambda without parameters."); 205 | let body = lambda 206 | .body() 207 | .expect("Unexpected lambda without parameters."); 208 | match param { 209 | ast::Param::IdentParam(ident_param) => emit_param_lambda(&ident_param, &body, out_src), 210 | ast::Param::Pattern(pattern) => emit_pattern_lambda(&pattern, &body, out_src), 211 | } 212 | } 213 | 214 | fn emit_param_lambda( 215 | ident_param: &ast::IdentParam, 216 | body: &ast::Expr, 217 | out_src: &mut String, 218 | ) -> Result<(), String> { 219 | *out_src += "n.paramLambda(ctx,"; 220 | emit_ident_as_js_string( 221 | &ident_param 222 | .ident() 223 | .expect("Unexpected missing lambda parameter identifier."), 224 | out_src, 225 | ); 226 | *out_src += ",(ctx) => "; 227 | emit_expr(body, out_src)?; 228 | *out_src += ")"; 229 | Ok(()) 230 | } 231 | 232 | fn emit_pattern_lambda( 233 | pattern: &ast::Pattern, 234 | body: &ast::Expr, 235 | out_src: &mut String, 236 | ) -> Result<(), String> { 237 | let mut formal_arg_names = HashSet::new(); 238 | *out_src += "n.patternLambda(ctx,"; 239 | if let Some(indent) = pattern.pat_bind().and_then(|pat_bind| pat_bind.ident()) { 240 | formal_arg_names.insert(indent.to_string()); 241 | emit_ident_as_js_string(&indent, out_src); 242 | } else { 243 | *out_src += "undefined"; 244 | } 245 | *out_src += ",["; 246 | for pattern_entry in pattern.pat_entries() { 247 | let ident = pattern_entry.ident().ok_or_else(|| { 248 | "Unsupported lambda pattern parameter without an identifier.".to_owned() 249 | })?; 250 | if !formal_arg_names.insert(ident.to_string()) { 251 | return Err(format!("duplicate formal function argument '{}'.", ident)); 252 | } 253 | *out_src += "["; 254 | emit_ident_as_js_string(&ident, out_src); 255 | *out_src += ","; 256 | if let Some(default_value) = pattern_entry.default() { 257 | emit_expr(&default_value, out_src)?; 258 | } 259 | *out_src += "],"; 260 | } 261 | *out_src += "],(ctx) => "; 262 | emit_expr(body, out_src)?; 263 | *out_src += ")"; 264 | Ok(()) 265 | } 266 | 267 | fn emit_ident_as_js_string(ident: &ast::Ident, out_src: &mut String) { 268 | out_src.push('"'); 269 | js_string_escape_into(&ident.to_string(), out_src); 270 | out_src.push('"'); 271 | } 272 | 273 | fn emit_let_in(let_in: &ast::LetIn, out_src: &mut String) -> Result<(), String> { 274 | *out_src += "n.letIn(ctx,"; 275 | emit_has_entry(let_in, true, out_src)?; 276 | *out_src += ",(ctx) => "; 277 | emit_expr( 278 | &let_in 279 | .body() 280 | .expect("Unexpected let-in expression without a body."), 281 | out_src, 282 | )?; 283 | *out_src += ")"; 284 | Ok(()) 285 | } 286 | 287 | fn emit_list(list: &ast::List, out_src: &mut String) -> Result<(), String> { 288 | *out_src += "new n.NixList(["; 289 | for element in list.items() { 290 | out_src.push_str("new n.Lazy(ctx,(ctx) => "); 291 | emit_expr(&element, out_src)?; 292 | out_src.push_str("),"); 293 | } 294 | *out_src += "])"; 295 | Ok(()) 296 | } 297 | 298 | fn emit_literal(literal: &ast::Literal, out_src: &mut String) -> Result<(), String> { 299 | let token = literal.syntax().first_token().expect("Not implemented"); 300 | match token.kind() { 301 | SyntaxKind::TOKEN_INTEGER => { 302 | out_src.push_str("new n.NixInt("); 303 | out_src.push_str(token.text()); 304 | out_src.push_str("n)"); 305 | } 306 | SyntaxKind::TOKEN_FLOAT => { 307 | out_src.push_str("new n.NixFloat("); 308 | out_src.push_str(token.text()); 309 | out_src.push(')'); 310 | } 311 | SyntaxKind::TOKEN_URI => emit_nix_string(token.text(), out_src), 312 | _ => todo!("emit_literal: {:?} token kind: {:?}", literal, token.kind()), 313 | } 314 | Ok(()) 315 | } 316 | 317 | fn emit_paren(paren: &ast::Paren, out_src: &mut String) -> Result<(), String> { 318 | *out_src += "("; 319 | let body = paren 320 | .expr() 321 | .expect("Unexpected parenthesis without a body."); 322 | emit_expr(&body, out_src)?; 323 | *out_src += ")"; 324 | Ok(()) 325 | } 326 | 327 | fn emit_path(path: &ast::Path, out_src: &mut String) -> Result<(), String> { 328 | *out_src += "n.toPath(ctx,`"; 329 | js_string_escape_into(&path.to_string(), out_src); 330 | *out_src += "`)"; 331 | Ok(()) 332 | } 333 | 334 | fn emit_select_expr(select: &ast::Select, out_src: &mut String) -> Result<(), String> { 335 | emit_expr(&select.expr().expect("Unreachable"), out_src)?; 336 | *out_src += ".select("; 337 | emit_attrpath(&select.attrpath().expect("Unreachable"), out_src)?; 338 | *out_src += ","; 339 | match select.default_expr() { 340 | Some(default_expr) => emit_expr(&default_expr, out_src)?, 341 | None => *out_src += "undefined", 342 | } 343 | *out_src += ")"; 344 | Ok(()) 345 | } 346 | 347 | fn emit_string_expr(string: &ast::Str, out_src: &mut String) -> Result<(), String> { 348 | *out_src += "new n.NixString(`"; 349 | for string_part in string.normalized_parts() { 350 | match string_part { 351 | ast::InterpolPart::Literal(literal) => { 352 | js_string_escape_into(&literal, out_src); 353 | } 354 | ast::InterpolPart::Interpolation(interpolation_body) => { 355 | *out_src += "${"; 356 | emit_expr( 357 | &interpolation_body 358 | .expr() 359 | .expect("String interpolation body missing."), 360 | out_src, 361 | )?; 362 | *out_src += ".asString()}"; 363 | } 364 | } 365 | } 366 | *out_src += "`)"; 367 | Ok(()) 368 | } 369 | 370 | fn emit_unary_op(unary_op: &ast::UnaryOp, out_src: &mut String) -> Result<(), String> { 371 | let operator = unary_op.operator().expect("Not implemented"); 372 | let operand = unary_op.expr().expect("Not implemented"); 373 | emit_unary_op_kind(operator, &operand, out_src) 374 | } 375 | 376 | fn emit_unary_op_kind( 377 | operator: ast::UnaryOpKind, 378 | operand: &ast::Expr, 379 | out_src: &mut String, 380 | ) -> Result<(), String> { 381 | match operator { 382 | ast::UnaryOpKind::Invert => emit_nixrt_unary_op(operand, "invert", out_src), 383 | ast::UnaryOpKind::Negate => emit_nixrt_unary_op(operand, "neg", out_src), 384 | } 385 | } 386 | 387 | fn emit_nixrt_unary_op( 388 | operand: &ast::Expr, 389 | nixrt_function: &str, 390 | out_src: &mut String, 391 | ) -> Result<(), String> { 392 | emit_expr(operand, out_src)?; 393 | out_src.push('.'); 394 | *out_src += nixrt_function; 395 | *out_src += "()"; 396 | Ok(()) 397 | } 398 | 399 | fn emit_with(with: &ast::With, out_src: &mut String) -> Result<(), String> { 400 | *out_src += "n.withExpr(ctx,"; 401 | emit_expr( 402 | &with 403 | .namespace() 404 | .ok_or_else(|| "Unexpected 'with' expression without a namespace.".to_string())?, 405 | out_src, 406 | )?; 407 | *out_src += ",(ctx) => "; 408 | emit_expr( 409 | &with 410 | .body() 411 | .ok_or_else(|| "Unexpected 'with' expression without a body.".to_string())?, 412 | out_src, 413 | )?; 414 | *out_src += ")"; 415 | Ok(()) 416 | } 417 | 418 | fn js_string_escape_into(string: &str, out_string: &mut String) { 419 | for character in string.chars() { 420 | match character { 421 | '`' => out_string.push_str(r#"\`"#), 422 | '$' => out_string.push_str(r#"\$"#), 423 | '\\' => out_string.push_str(r#"\\"#), 424 | '\r' => out_string.push_str(r#"\r"#), 425 | character => out_string.push(character), 426 | } 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /src/eval/error.rs: -------------------------------------------------------------------------------- 1 | use deno_core::v8; 2 | 3 | use super::{ 4 | helpers::{call_js_function, get_js_value_key, is_nixrt_type}, 5 | types::NixTypeKind, 6 | }; 7 | 8 | #[derive(Debug)] 9 | pub struct NixError { 10 | pub message: Vec, 11 | pub kind: NixErrorKind, 12 | } 13 | 14 | impl std::fmt::Display for NixError { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | for part in &self.message { 17 | match part { 18 | NixErrorMessagePart::Plain(text) => write!(f, "{}", text)?, 19 | NixErrorMessagePart::Highlighted(text) => write!(f, "{}", text)?, 20 | } 21 | } 22 | 23 | Ok(()) 24 | } 25 | } 26 | 27 | impl From for NixError { 28 | fn from(message: String) -> Self { 29 | NixError { 30 | message: vec![NixErrorMessagePart::Plain(message.clone())], 31 | kind: NixErrorKind::UnexpectedRustError { message }, 32 | } 33 | } 34 | } 35 | 36 | impl<'a> From<&'a str> for NixError { 37 | fn from(message: &'a str) -> Self { 38 | NixError { 39 | message: vec![NixErrorMessagePart::Plain(message.to_string())], 40 | kind: NixErrorKind::UnexpectedRustError { 41 | message: message.to_string(), 42 | }, 43 | } 44 | } 45 | } 46 | 47 | impl From for NixError { 48 | fn from(error: v8::DataError) -> Self { 49 | NixError { 50 | message: vec![NixErrorMessagePart::Plain(error.to_string())], 51 | kind: NixErrorKind::UnexpectedJsError { 52 | message: error.to_string(), 53 | }, 54 | } 55 | } 56 | } 57 | 58 | #[derive(Debug)] 59 | pub enum NixErrorMessagePart { 60 | Plain(String), 61 | Highlighted(String), 62 | } 63 | 64 | #[derive(Debug, PartialEq, Eq)] 65 | pub enum NixErrorKind { 66 | Abort { 67 | message: String, 68 | }, 69 | CouldntFindVariable { 70 | var_name: String, 71 | }, 72 | TypeMismatch { 73 | expected: Vec, 74 | got: NixTypeKind, 75 | }, 76 | Other { 77 | codename: String, 78 | }, 79 | MissingAttribute { 80 | attr_path: Vec, 81 | }, 82 | AttributeAlreadyDefined { 83 | attr_path: Vec, 84 | }, 85 | FunctionCallWithoutArgument { 86 | argument: String, 87 | }, 88 | 89 | // For non-nix errors thrown in js or rust 90 | UnexpectedJsError { 91 | message: String, 92 | }, 93 | UnexpectedRustError { 94 | message: String, 95 | }, 96 | } 97 | 98 | pub fn js_error_to_rust( 99 | scope: &mut v8::HandleScope, 100 | nixrt: v8::Local, 101 | error: v8::Local, 102 | ) -> NixError { 103 | let result = try_js_error_to_rust(scope, nixrt, error); 104 | 105 | match result { 106 | Ok(ok) => ok, 107 | Err(err) => err, 108 | } 109 | } 110 | 111 | fn try_js_error_to_rust( 112 | scope: &mut v8::HandleScope, 113 | nixrt: v8::Local, 114 | error: v8::Local, 115 | ) -> Result { 116 | // If the error is not a NixError instance, then it's an unexpected error. 117 | if !is_nixrt_type(scope, &nixrt, &error, "NixError")? { 118 | return Ok(NixError { 119 | message: vec![NixErrorMessagePart::Plain("An error occurred.".to_owned())], 120 | kind: NixErrorKind::UnexpectedJsError { 121 | message: error.to_rust_string_lossy(scope), 122 | }, 123 | }); 124 | } 125 | 126 | let message_js = get_js_value_key(scope, &error, "richMessage")?; 127 | let message = js_error_message_to_rust(scope, message_js)?; 128 | 129 | let kind_js = get_js_value_key(scope, &error, "kind")?; 130 | 131 | let err_constructor = get_js_value_key(scope, &kind_js, "constructor")?; 132 | let err_name = get_js_value_key(scope, &err_constructor, "name")?; 133 | 134 | let kind = match err_name.to_rust_string_lossy(scope).as_str() { 135 | "NixAbortError" => { 136 | let message_js = get_js_value_key(scope, &kind_js, "message")?; 137 | let message = message_js.to_rust_string_lossy(scope); 138 | NixErrorKind::Abort { message } 139 | } 140 | "NixCouldntFindVariableError" => { 141 | let var_name_js = get_js_value_key(scope, &kind_js, "varName")?; 142 | let var_name = var_name_js.to_rust_string_lossy(scope); 143 | NixErrorKind::CouldntFindVariable { var_name } 144 | } 145 | "NixTypeMismatchError" => { 146 | let expected_js = get_js_value_key(scope, &kind_js, "expected")?; 147 | let got_js = get_js_value_key(scope, &kind_js, "got")?; 148 | 149 | let mut expected = nix_type_class_array_to_enum_array(scope, nixrt, expected_js)?; 150 | let got = nix_type_class_to_enum(scope, nixrt, got_js)?; 151 | 152 | // Sort expected array, for normalization 153 | expected.sort_unstable(); 154 | 155 | NixErrorKind::TypeMismatch { expected, got } 156 | } 157 | "NixOtherError" => { 158 | let codename_js = get_js_value_key(scope, &kind_js, "codename")?; 159 | let codename = codename_js.to_rust_string_lossy(scope); 160 | 161 | NixErrorKind::Other { codename } 162 | } 163 | "NixMissingAttributeError" => { 164 | let attr_path_js = get_js_value_key(scope, &kind_js, "attrPath")?; 165 | let attr_path = js_string_array_to_rust_string_array(scope, attr_path_js)?; 166 | NixErrorKind::MissingAttribute { attr_path } 167 | } 168 | "NixAttributeAlreadyDefinedError" => { 169 | let attr_path_js = get_js_value_key(scope, &kind_js, "attrPath")?; 170 | let attr_path = js_string_array_to_rust_string_array(scope, attr_path_js)?; 171 | NixErrorKind::AttributeAlreadyDefined { attr_path } 172 | } 173 | "NixFunctionCallWithoutArgumentError" => { 174 | let argument_js = get_js_value_key(scope, &kind_js, "argument")?; 175 | let argument = argument_js.to_rust_string_lossy(scope); 176 | NixErrorKind::FunctionCallWithoutArgument { argument } 177 | } 178 | _ => { 179 | return Ok(NixError { 180 | message: vec![NixErrorMessagePart::Plain( 181 | "An unrecognized NixError occurred.".to_owned(), 182 | )], 183 | kind: NixErrorKind::UnexpectedJsError { 184 | message: error.to_rust_string_lossy(scope), 185 | }, 186 | }); 187 | } 188 | }; 189 | 190 | Ok(NixError { message, kind }) 191 | } 192 | 193 | fn nix_type_class_to_enum( 194 | scope: &mut v8::HandleScope, 195 | nixrt: v8::Local, 196 | class: v8::Local, 197 | ) -> Result { 198 | let name_fn = get_js_value_key(scope, &class, "toTypeName")?.try_into()?; 199 | let name_js_str = call_js_function(scope, &name_fn, nixrt, &[])?; 200 | let name = name_js_str.to_rust_string_lossy(scope); 201 | 202 | match name.as_str() { 203 | "bool" => Ok(NixTypeKind::Bool), 204 | "float" => Ok(NixTypeKind::Float), 205 | "int" => Ok(NixTypeKind::Int), 206 | "list" => Ok(NixTypeKind::List), 207 | "null" => Ok(NixTypeKind::Null), 208 | "string" => Ok(NixTypeKind::String), 209 | "path" => Ok(NixTypeKind::Path), 210 | "lambda" => Ok(NixTypeKind::Lambda), 211 | "set" => Ok(NixTypeKind::Set), 212 | _ => Err(NixError { 213 | message: vec![NixErrorMessagePart::Plain(format!( 214 | "Unexpected type name: {name}" 215 | ))], 216 | kind: NixErrorKind::Other { 217 | codename: "unknown-type".to_owned(), 218 | }, 219 | }), 220 | } 221 | } 222 | 223 | fn nix_type_class_array_to_enum_array( 224 | scope: &mut v8::HandleScope, 225 | nixrt: v8::Local, 226 | class_array: v8::Local, 227 | ) -> Result, NixError> { 228 | let class_array: v8::Local = class_array.try_into()?; 229 | 230 | let len_num: v8::Local = 231 | get_js_value_key(scope, &class_array, "length")?.try_into()?; 232 | let len = len_num.value() as u32; 233 | 234 | let mut result = Vec::with_capacity(len as usize); 235 | 236 | for i in 0..len { 237 | let item_class = class_array 238 | .get_index(scope, i) 239 | .ok_or_else(|| format!("Expected index {i} not found."))?; 240 | 241 | let kind = nix_type_class_to_enum(scope, nixrt, item_class)?; 242 | result.push(kind); 243 | } 244 | 245 | Ok(result) 246 | } 247 | 248 | fn js_string_array_to_rust_string_array( 249 | scope: &mut v8::HandleScope, 250 | js_array: v8::Local, 251 | ) -> Result, NixError> { 252 | let js_array: v8::Local = js_array.try_into()?; 253 | 254 | let len_num: v8::Local = 255 | get_js_value_key(scope, &js_array, "length")?.try_into()?; 256 | let len = len_num.value() as u32; 257 | 258 | let mut result = Vec::with_capacity(len as usize); 259 | 260 | for i in 0..len { 261 | let item_js = js_array 262 | .get_index(scope, i) 263 | .ok_or_else(|| format!("Expected index {i} not found."))?; 264 | 265 | let item = item_js.to_rust_string_lossy(scope); 266 | result.push(item); 267 | } 268 | 269 | Ok(result) 270 | } 271 | 272 | fn js_error_message_part_to_rust( 273 | scope: &mut v8::HandleScope, 274 | error_part: v8::Local, 275 | ) -> Result { 276 | let kind_js = get_js_value_key(scope, &error_part, "kind")?; 277 | let kind = kind_js.to_rust_string_lossy(scope); 278 | 279 | match kind.as_str() { 280 | "plain" => { 281 | let value_js = get_js_value_key(scope, &error_part, "value")?; 282 | let value = value_js.to_rust_string_lossy(scope); 283 | Ok(NixErrorMessagePart::Plain(value)) 284 | } 285 | "highlighted" => { 286 | let value_js = get_js_value_key(scope, &error_part, "value")?; 287 | let value = value_js.to_rust_string_lossy(scope); 288 | Ok(NixErrorMessagePart::Highlighted(value)) 289 | } 290 | _ => Err("Unexpected error message part kind.".into()), 291 | } 292 | } 293 | 294 | fn js_error_message_to_rust( 295 | scope: &mut v8::HandleScope, 296 | error: v8::Local, 297 | ) -> Result, NixError> { 298 | let error: v8::Local = error.try_into()?; 299 | 300 | let len_num: v8::Local = get_js_value_key(scope, &error, "length")?.try_into()?; 301 | let len = len_num.value() as u32; 302 | 303 | let mut result = Vec::with_capacity(len as usize); 304 | 305 | for i in 0..len { 306 | let error_part = error 307 | .get_index(scope, i) 308 | .ok_or_else(|| format!("Expected index {i} not found."))?; 309 | 310 | let part = js_error_message_part_to_rust(scope, error_part)?; 311 | result.push(part); 312 | } 313 | 314 | Ok(result) 315 | } 316 | -------------------------------------------------------------------------------- /src/eval/execution.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use deno_core::v8; 4 | use deno_core::v8::{HandleScope, Local, ModuleStatus, Object}; 5 | 6 | use crate::eval::types::EvalResult; 7 | 8 | use super::emit_js::emit_module; 9 | use super::error::NixError; 10 | use super::helpers::{call_js_function, get_nixrt_type, try_get_js_object_key}; 11 | use super::types::js_value_to_nix; 12 | 13 | pub fn evaluate(nix_expr: &str, workdir: &Path) -> EvalResult { 14 | deno_core::JsRuntime::init_platform(None); 15 | // Declare the V8 execution context 16 | let isolate = &mut v8::Isolate::new(Default::default()); 17 | let scope = &mut v8::HandleScope::new(isolate); 18 | let context = v8::Context::new(scope); 19 | let scope = &mut v8::ContextScope::new(scope, context); 20 | let global = context.global(scope); 21 | 22 | // Insert all globals, as defined by globals.d.ts 23 | let globals: &[(_, v8::Local)] = &[ 24 | ( 25 | "importNixModule", 26 | v8::Function::new(scope, import_nix_module).unwrap().into(), 27 | ), 28 | ( 29 | "debugLog", 30 | v8::Function::new(scope, debug_log).unwrap().into(), 31 | ), 32 | ]; 33 | 34 | for (name, value) in globals { 35 | let global_var_name = v8::String::new(scope, name).unwrap(); 36 | global.set(scope, global_var_name.into(), *value).unwrap(); 37 | } 38 | 39 | // Execute the Nix runtime JS module, get its exports 40 | let nixjs_rt_str = include_str!("../../nixjs-rt/dist/lib.mjs"); 41 | let nixjs_rt_obj = exec_module(nixjs_rt_str, scope)?; 42 | 43 | // Set them to a global variable 44 | let nixrt_attr = v8::String::new(scope, "n").unwrap(); 45 | global 46 | .set(scope, nixrt_attr.into(), nixjs_rt_obj.into()) 47 | .unwrap(); 48 | 49 | let root_nix_fn = nix_expr_to_js_function(scope, nix_expr)?; 50 | 51 | nix_value_from_module(scope, root_nix_fn, nixjs_rt_obj, workdir) 52 | } 53 | 54 | fn nix_expr_to_js_function<'s>( 55 | scope: &mut HandleScope<'s>, 56 | nix_expr: &str, 57 | ) -> Result, NixError> { 58 | let source_str = emit_module(nix_expr)?; 59 | let module_source_v8 = to_v8_source(scope, &source_str, ""); 60 | let module = v8::script_compiler::compile_module(scope, module_source_v8) 61 | .ok_or("Failed to compile the module.")?; 62 | 63 | if module 64 | .instantiate_module(scope, resolve_module_callback) 65 | .is_none() 66 | { 67 | todo!("Instantiation failure.") 68 | } 69 | 70 | if module.evaluate(scope).is_none() { 71 | todo!("evaluation failed") 72 | }; 73 | 74 | if module.get_status() == ModuleStatus::Errored { 75 | let exception = module.get_exception(); 76 | let string = exception.to_rust_string_lossy(scope); 77 | 78 | todo!("evaluation failed:\n{}", string); 79 | } 80 | 81 | let namespace_obj = module 82 | .get_module_namespace() 83 | .to_object(scope) 84 | .ok_or("Failed to get the module namespace.")?; 85 | 86 | let Some(nix_value) = try_get_js_object_key(scope, &namespace_obj.into(), "default")? else { 87 | todo!( 88 | "Could not find the nix value: {:?}", 89 | namespace_obj.to_rust_string_lossy(scope) 90 | ) 91 | }; 92 | let nix_value: v8::Local = 93 | nix_value.try_into().expect("Nix value must be a function."); 94 | 95 | Ok(nix_value) 96 | } 97 | 98 | fn import_nix_module<'s>( 99 | scope: &mut HandleScope<'s>, 100 | args: v8::FunctionCallbackArguments<'s>, 101 | mut ret: v8::ReturnValue, 102 | ) { 103 | let module_path = args.get(0).to_rust_string_lossy(scope); 104 | let module_source_str = std::fs::read_to_string(module_path).unwrap(); 105 | 106 | let nix_fn = nix_expr_to_js_function(scope, &module_source_str); 107 | 108 | let nix_fn = match nix_fn { 109 | Ok(nix_fn) => nix_fn, 110 | Err(err) => { 111 | let err_str = v8::String::new(scope, &err.to_string()).unwrap(); 112 | let err_obj = v8::Exception::error(scope, err_str); 113 | ret.set(err_obj); 114 | return; 115 | } 116 | }; 117 | 118 | ret.set(nix_fn.into()); 119 | } 120 | 121 | fn debug_log<'s>( 122 | scope: &mut HandleScope<'s>, 123 | args: v8::FunctionCallbackArguments<'s>, 124 | _ret: v8::ReturnValue, 125 | ) { 126 | // Log the first argument 127 | let log_str = args.get(0).to_rust_string_lossy(scope); 128 | eprintln!("Log from JS: {log_str}"); 129 | } 130 | 131 | fn exec_module<'a>( 132 | code: &str, 133 | scope: &mut v8::HandleScope<'a>, 134 | ) -> Result, NixError> { 135 | let source = to_v8_source(scope, code, ""); 136 | let module = v8::script_compiler::compile_module(scope, source) 137 | .ok_or("Failed to compile the module.")?; 138 | 139 | if module 140 | .instantiate_module(scope, resolve_module_callback) 141 | .is_none() 142 | { 143 | return Err("Instantiation failure.".to_owned().into()); 144 | } 145 | 146 | if module.evaluate(scope).is_none() { 147 | return Err("Evaluation failure.".to_owned().into()); 148 | } 149 | 150 | let obj = module 151 | .get_module_namespace() 152 | .to_object(scope) 153 | .ok_or("Failed to get the module namespace.")?; 154 | 155 | Ok(obj) 156 | } 157 | 158 | fn nix_value_from_module( 159 | scope: &mut v8::ContextScope, 160 | nix_module_fn: v8::Local, 161 | nixjs_rt_obj: v8::Local, 162 | workdir: &Path, 163 | ) -> EvalResult { 164 | let nixrt: v8::Local = nixjs_rt_obj.into(); 165 | 166 | let eval_ctx = create_eval_ctx(scope, &nixrt, workdir)?; 167 | 168 | let nix_value = call_js_function(scope, &nix_module_fn, nixjs_rt_obj, &[eval_ctx.into()])?; 169 | 170 | let to_strict_fn: v8::Local = 171 | try_get_js_object_key(scope, &nixrt, "recursiveStrict")? 172 | .expect("Could not find the function `recursiveStrict` in `nixrt`.") 173 | .try_into() 174 | .expect("`n.recursiveStrict` is not a function."); 175 | let strict_nix_value = call_js_function(scope, &to_strict_fn, nixjs_rt_obj, &[nix_value])?; 176 | 177 | js_value_to_nix(scope, &nixjs_rt_obj, &strict_nix_value) 178 | } 179 | 180 | fn create_eval_ctx<'s>( 181 | scope: &mut v8::HandleScope<'s>, 182 | nixrt: &v8::Local, 183 | script_path: &Path, 184 | ) -> Result, String> { 185 | let eval_ctx_type = get_nixrt_type(scope, nixrt, "EvalCtx")?; 186 | let eval_ctx_constructor: v8::Local = eval_ctx_type 187 | .try_into() 188 | .expect("Could not get the constructor of the evaluation context class."); 189 | 190 | let real_path = script_path 191 | .canonicalize() 192 | .map_err(|err| format!("Failed to resolve the script path. Error: {err}."))?; 193 | let script_dir = real_path 194 | .parent() 195 | .ok_or_else(|| format!("Failed to determine the directory of path {real_path:?}."))?; 196 | let script_dir_str = real_path 197 | .to_str() 198 | .ok_or_else(|| format!("Failed to converft the path {script_dir:?} to a string."))?; 199 | let js_script_dir_path = 200 | v8::String::new(scope, script_dir_str).expect("Unexpected internal error."); 201 | 202 | Ok(eval_ctx_constructor 203 | .new_instance(scope, &[js_script_dir_path.into()]) 204 | .expect("Could not construct the global evaluation context.")) 205 | } 206 | 207 | fn new_script_origin<'s>( 208 | scope: &mut v8::HandleScope<'s>, 209 | resource_name: &str, 210 | source_map_url: &str, 211 | ) -> v8::ScriptOrigin<'s> { 212 | let resource_name_v8_str = v8::String::new(scope, resource_name).unwrap(); 213 | let resource_line_offset = 0; 214 | let resource_column_offset = 0; 215 | let resource_is_shared_cross_origin = true; 216 | let script_id = 123; 217 | let source_map_url = v8::String::new(scope, source_map_url).unwrap(); 218 | let resource_is_opaque = false; 219 | let is_wasm = false; 220 | let is_module = true; 221 | v8::ScriptOrigin::new( 222 | scope, 223 | resource_name_v8_str.into(), 224 | resource_line_offset, 225 | resource_column_offset, 226 | resource_is_shared_cross_origin, 227 | script_id, 228 | source_map_url.into(), 229 | resource_is_opaque, 230 | is_wasm, 231 | is_module, 232 | ) 233 | } 234 | 235 | fn to_v8_source( 236 | scope: &mut v8::HandleScope, 237 | js_code: &str, 238 | source_path: &str, 239 | ) -> v8::script_compiler::Source { 240 | let code = v8::String::new(scope, js_code).unwrap(); 241 | let origin = new_script_origin(scope, source_path, &format!("file://{source_path}.map")); 242 | v8::script_compiler::Source::new(code, Some(&origin)) 243 | } 244 | 245 | fn resolve_module_callback<'a>( 246 | _: v8::Local<'a, v8::Context>, 247 | _: v8::Local<'a, v8::String>, 248 | _: v8::Local<'a, v8::FixedArray>, 249 | _: v8::Local<'a, v8::Module>, 250 | ) -> Option> { 251 | panic!("Module resolution not supported.") 252 | } 253 | -------------------------------------------------------------------------------- /src/eval/helpers.rs: -------------------------------------------------------------------------------- 1 | use deno_core::v8; 2 | 3 | use super::error::{js_error_to_rust, NixError}; 4 | 5 | pub fn is_nixrt_type<'s, T>( 6 | scope: &mut v8::HandleScope<'_>, 7 | nixrt: &v8::Local<'s, T>, 8 | js_value: &v8::Local, 9 | type_name: &str, 10 | ) -> Result 11 | where 12 | v8::Local<'s, T>: Into>, 13 | { 14 | let nixrt_type = get_nixrt_type(scope, &(*nixrt).into(), type_name)?; 15 | js_value.instance_of(scope, nixrt_type).ok_or_else(|| { 16 | format!( 17 | "Failed to check whether value '{}' is '{type_name}'.", 18 | js_value.to_rust_string_lossy(scope) 19 | ) 20 | }) 21 | } 22 | 23 | pub fn get_nixrt_type<'s>( 24 | scope: &mut v8::HandleScope<'s>, 25 | nixrt: &v8::Local, 26 | type_name: &str, 27 | ) -> Result, String> { 28 | let nix_int_class_name = v8::String::new(scope, type_name).unwrap(); 29 | nixrt 30 | .to_object(scope) 31 | .unwrap() 32 | .get(scope, nix_int_class_name.into()) 33 | .unwrap() 34 | .to_object(scope) 35 | .ok_or_else(|| format!("Could not find the type {type_name}.")) 36 | } 37 | 38 | pub fn try_get_js_object_key<'s>( 39 | scope: &mut v8::HandleScope<'s>, 40 | js_value: &v8::Local, 41 | key: &str, 42 | ) -> Result>, String> { 43 | let js_object = js_value 44 | .to_object(scope) 45 | .ok_or_else(|| "Not an object.".to_owned())?; 46 | let key_js_str = v8::String::new(scope, key).unwrap(); 47 | Ok(js_object.get(scope, key_js_str.into())) 48 | } 49 | 50 | pub fn get_js_value_key<'s: 'v, 'v, T>( 51 | scope: &mut v8::HandleScope<'s>, 52 | js_value: &v8::Local<'v, T>, 53 | key: &str, 54 | ) -> Result, String> 55 | where 56 | v8::Local<'v, T>: TryInto>, 57 | { 58 | let js_object: v8::Local<'v, v8::Object> = (*js_value) 59 | .try_into() 60 | .map_err(|_| "Not an object.".to_owned())?; 61 | let key_js_str = v8::String::new(scope, key).unwrap(); 62 | 63 | if let Some(value) = js_object.get(scope, key_js_str.into()) { 64 | Ok(value) 65 | } else { 66 | Err(format!("Expected key '{key}' not found.")) 67 | } 68 | } 69 | 70 | pub fn call_js_function<'s>( 71 | scope: &mut v8::HandleScope<'s>, 72 | js_function: &v8::Local, 73 | nixrt: v8::Local, 74 | args: &[v8::Local], 75 | ) -> Result, NixError> { 76 | let try_scope = &mut v8::TryCatch::new(scope); 77 | let this = v8::undefined(try_scope).into(); 78 | let Some(strict_nix_value) = js_function.call(try_scope, this, args) else { 79 | let exception = try_scope.exception(); 80 | return Err(map_js_exception_value_to_rust(try_scope, nixrt, exception)); 81 | }; 82 | Ok(strict_nix_value) 83 | } 84 | 85 | pub fn call_js_instance_mehod<'s>( 86 | scope: &mut v8::HandleScope<'s>, 87 | js_function: &v8::Local, 88 | this: v8::Local, 89 | nixrt: v8::Local, 90 | args: &[v8::Local], 91 | ) -> Result, NixError> { 92 | let try_scope = &mut v8::TryCatch::new(scope); 93 | let Some(strict_nix_value) = js_function.call(try_scope, this, args) else { 94 | let exception = try_scope.exception(); 95 | return Err(map_js_exception_value_to_rust(try_scope, nixrt, exception)); 96 | }; 97 | Ok(strict_nix_value) 98 | } 99 | 100 | pub fn map_js_exception_value_to_rust<'s>( 101 | scope: &mut v8::HandleScope<'s>, 102 | nixrt: v8::Local, 103 | exception: Option>, 104 | ) -> NixError { 105 | // TODO: Again, the stack trace needs to be source-mapped. 106 | if let Some(error) = exception { 107 | js_error_to_rust(scope, nixrt, error) 108 | } else { 109 | "Unknown evaluation error.".into() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/eval/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod emit_js; 2 | pub mod error; 3 | pub mod execution; 4 | pub mod helpers; 5 | pub mod types; 6 | -------------------------------------------------------------------------------- /src/eval/types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use deno_core::v8; 4 | 5 | use super::{ 6 | error::NixError, 7 | helpers::{call_js_instance_mehod, is_nixrt_type, try_get_js_object_key}, 8 | }; 9 | 10 | #[derive(Debug, PartialEq)] 11 | pub enum Value { 12 | AttrSet(HashMap), 13 | Bool(bool), 14 | Float(f64), 15 | Int(i64), 16 | Lambda, 17 | List(Vec), 18 | Path(String), 19 | Str(String), 20 | } 21 | 22 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] 23 | pub enum NixTypeKind { 24 | Bool, 25 | Float, 26 | Int, 27 | List, 28 | Null, 29 | String, 30 | Path, 31 | Lambda, 32 | Set, 33 | } 34 | 35 | pub type EvalResult = Result; 36 | 37 | pub fn js_value_to_nix( 38 | scope: &mut v8::HandleScope<'_>, 39 | nixrt: &v8::Local, 40 | js_value: &v8::Local, 41 | ) -> EvalResult { 42 | if js_value.is_function() { 43 | return Ok(Value::Lambda); 44 | } 45 | if let Some(value) = from_js_attrset(scope, nixrt, js_value)? { 46 | return Ok(value); 47 | } 48 | if let Some(value) = from_js_string(scope, nixrt, js_value)? { 49 | return Ok(value); 50 | } 51 | if let Some(value) = from_js_lazy(scope, nixrt, js_value)? { 52 | return Ok(value); 53 | } 54 | if let Some(value) = from_js_int(scope, nixrt, js_value)? { 55 | return Ok(value); 56 | } 57 | if let Some(value) = from_js_bool(scope, nixrt, js_value)? { 58 | return Ok(value); 59 | } 60 | if let Some(value) = from_js_float(scope, nixrt, js_value)? { 61 | return Ok(value); 62 | } 63 | if let Some(value) = from_js_list(scope, nixrt, js_value)? { 64 | return Ok(value); 65 | } 66 | if let Some(value) = from_js_path(scope, nixrt, js_value)? { 67 | return Ok(value); 68 | } 69 | if let Some(value) = from_js_lambda(scope, nixrt, js_value)? { 70 | return Ok(value); 71 | } 72 | todo!( 73 | "js_value_to_nix: {:?}", 74 | js_value.to_rust_string_lossy(scope), 75 | ) 76 | } 77 | 78 | fn from_js_int( 79 | scope: &mut v8::HandleScope<'_>, 80 | nixrt: &v8::Local, 81 | js_value: &v8::Local, 82 | ) -> Result, NixError> { 83 | if is_nixrt_type(scope, nixrt, js_value, "NixInt")? { 84 | let Some(int64_js_value) = try_get_js_object_key(scope, js_value, "int64")? else { 85 | return Ok(None); 86 | }; 87 | let big_int_value: v8::Local = int64_js_value.try_into().map_err(|err| { 88 | format!("Expected an int64 value. Internal conversion error: {err:?}") 89 | })?; 90 | return Ok(Some(Value::Int(big_int_value.i64_value().0))); 91 | } 92 | Ok(None) 93 | } 94 | 95 | fn from_js_string( 96 | scope: &mut v8::HandleScope<'_>, 97 | nixrt: &v8::Local, 98 | js_value: &v8::Local, 99 | ) -> Result, NixError> { 100 | if is_nixrt_type(scope, nixrt, js_value, "NixString")? { 101 | let Some(value) = try_get_js_object_key(scope, js_value, "value")? else { 102 | return Ok(None); 103 | }; 104 | let value_js_string: v8::Local = value.try_into().map_err(|err| { 105 | format!("Expected a string value. Internal conversion error: {err:?}") 106 | })?; 107 | return Ok(Some(Value::Str( 108 | value_js_string.to_rust_string_lossy(scope), 109 | ))); 110 | } 111 | Ok(None) 112 | } 113 | 114 | fn from_js_lazy( 115 | scope: &mut v8::HandleScope<'_>, 116 | nixrt: &v8::Local, 117 | js_value: &v8::Local, 118 | ) -> Result, NixError> { 119 | if is_nixrt_type(scope, nixrt, js_value, "Lazy")? { 120 | let to_strict = try_get_js_object_key(scope, js_value, "toStrict")?.ok_or_else(|| { 121 | "Internal error: could not find the `toStrict` method on the Lazy object.".to_string() 122 | })?; 123 | let to_strict_method: v8::Local = to_strict.try_into().map_err(|err| { 124 | format!( 125 | "Expected `toStrict` to be a method on the Lazy object. Internal conversion error: {err:?}" 126 | ) 127 | })?; 128 | 129 | let strict_value = 130 | call_js_instance_mehod(scope, &to_strict_method, *js_value, *nixrt, &[])?; 131 | 132 | return Ok(Some(js_value_to_nix(scope, nixrt, &strict_value)?)); 133 | } 134 | Ok(None) 135 | } 136 | 137 | fn from_js_bool( 138 | scope: &mut v8::HandleScope<'_>, 139 | nixrt: &v8::Local, 140 | js_value: &v8::Local, 141 | ) -> Result, NixError> { 142 | if is_nixrt_type(scope, nixrt, js_value, "NixBool")? { 143 | let value = try_get_js_object_key(scope, js_value, "value")?.ok_or_else(|| { 144 | "Internal error: could not find the `value` property on the NixBool object.".to_string() 145 | })?; 146 | let value_as_bool: v8::Local = value.try_into().map_err(|err| { 147 | format!( 148 | "Expected `value` to be a boolean on the NixBool object. Internal conversion error: {err:?}" 149 | ) 150 | })?; 151 | return Ok(Some(Value::Bool(value_as_bool.boolean_value(scope)))); 152 | } 153 | Ok(None) 154 | } 155 | 156 | fn from_js_float( 157 | scope: &mut v8::HandleScope<'_>, 158 | nixrt: &v8::Local, 159 | js_value: &v8::Local, 160 | ) -> Result, NixError> { 161 | if is_nixrt_type(scope, nixrt, js_value, "NixFloat")? { 162 | let value = try_get_js_object_key(scope, js_value, "value")?.ok_or_else(|| { 163 | "Internal error: could not find the `value` property on the NixFloat object." 164 | .to_string() 165 | })?; 166 | let value_as_number: v8::Local = value.try_into().map_err(|err| { 167 | format!( 168 | "Expected `value` to be a number on the NixFloat object. Internal conversion error: {err:?}" 169 | ) 170 | })?; 171 | return Ok(Some(Value::Float( 172 | value_as_number.number_value(scope).ok_or_else(|| { 173 | "Could not convert the JavaScript number to a floating point number.".to_string() 174 | })?, 175 | ))); 176 | } 177 | Ok(None) 178 | } 179 | 180 | fn from_js_attrset( 181 | scope: &mut v8::HandleScope<'_>, 182 | nixrt: &v8::Local, 183 | js_value: &v8::Local, 184 | ) -> Result, NixError> { 185 | if is_nixrt_type(scope, nixrt, js_value, "Attrset")? { 186 | let underlying_map_value = try_get_js_object_key(scope, js_value, "underlyingMap")? 187 | .ok_or_else(|| { 188 | "Internal error: could not find the `underlyingMap` method on the Attrset object." 189 | .to_string() 190 | })?; 191 | let underlying_map_function: v8::Local = underlying_map_value.try_into().map_err(|err| { 192 | format!( 193 | "Expected `underlyingMap` to be a method on the Attrset object. Internal conversion error: {err:?}" 194 | ) 195 | })?; 196 | let underlying_map: v8::Local = underlying_map_function 197 | .call(scope, *js_value, &[]) 198 | .ok_or_else(|| "Could not get the underlying map of the Attrset.".to_string())? 199 | .try_into() 200 | .map_err(|err| { 201 | format!( 202 | "Expected `underlyingMap` to return a Map. Internal conversion error: {err:?}" 203 | ) 204 | })?; 205 | return Ok(Some(js_map_as_attrset(scope, nixrt, &underlying_map)?)); 206 | } 207 | Ok(None) 208 | } 209 | 210 | fn from_js_list( 211 | scope: &mut v8::HandleScope<'_>, 212 | nixrt: &v8::Local, 213 | js_value: &v8::Local, 214 | ) -> Result, NixError> { 215 | if is_nixrt_type(scope, nixrt, js_value, "NixList")? { 216 | let value = try_get_js_object_key(scope, js_value, "values")?.ok_or_else(|| { 217 | "Internal error: could not find the `values` property on the NixList object." 218 | .to_string() 219 | })?; 220 | let value_as_array: v8::Local = value.try_into().map_err(|err| { 221 | format!( 222 | "Expected `values` to be an array in the NixList object. Internal conversion error: {err:?}" 223 | ) 224 | })?; 225 | return Ok(Some(js_value_as_nix_array(scope, nixrt, &value_as_array)?)); 226 | } 227 | Ok(None) 228 | } 229 | 230 | fn from_js_path( 231 | scope: &mut v8::HandleScope<'_>, 232 | nixrt: &v8::Local, 233 | js_value: &v8::Local, 234 | ) -> Result, NixError> { 235 | if !is_nixrt_type(scope, nixrt, js_value, "Path")? { 236 | return Ok(None); 237 | } 238 | let Some(path) = try_get_js_object_key(scope, js_value, "path")? else { 239 | return Ok(None); 240 | }; 241 | Ok(Some(Value::Path(path.to_rust_string_lossy(scope)))) 242 | } 243 | 244 | fn from_js_lambda( 245 | scope: &mut v8::HandleScope<'_>, 246 | nixrt: &v8::Local, 247 | js_value: &v8::Local, 248 | ) -> Result, NixError> { 249 | if !is_nixrt_type(scope, nixrt, js_value, "Lambda")? { 250 | return Ok(None); 251 | } 252 | Ok(Some(Value::Lambda)) 253 | } 254 | 255 | fn js_value_as_nix_array( 256 | scope: &mut v8::HandleScope<'_>, 257 | nixrt: &v8::Local, 258 | js_array: &v8::Local, 259 | ) -> EvalResult { 260 | let length = js_array.length(); 261 | let mut rust_array = Vec::with_capacity(length as usize); 262 | for idx in 0..length { 263 | let js_element = js_array.get_index(scope, idx).unwrap(); 264 | match js_value_to_nix(scope, nixrt, &js_element) { 265 | Ok(value) => rust_array.push(value), 266 | err => return err, 267 | } 268 | } 269 | Ok(Value::List(rust_array)) 270 | } 271 | 272 | fn js_map_as_attrset( 273 | scope: &mut v8::HandleScope<'_>, 274 | nixrt: &v8::Local, 275 | js_map: &v8::Local, 276 | ) -> EvalResult { 277 | let mut map: HashMap = HashMap::new(); 278 | let js_map_array = js_map.as_array(scope); 279 | for idx in 0..js_map_array.length() / 2 { 280 | let key_idx = idx * 2; 281 | let value_idx = key_idx + 1; 282 | let key: v8::Local = js_map_array 283 | .get_index(scope, key_idx) 284 | .expect("Unexpected index out-of-bounds.") 285 | .try_into() 286 | .expect("Attr names must be strings."); 287 | let value = js_map_array 288 | .get_index(scope, value_idx) 289 | .expect("Unexpected index out-of-bounds."); 290 | map.insert( 291 | key.to_rust_string_lossy(scope), 292 | js_value_to_nix(scope, nixrt, &value)?, 293 | ); 294 | } 295 | Ok(Value::AttrSet(map)) 296 | } 297 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cmd; 2 | pub mod eval; 3 | pub mod tests; 4 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Command; 2 | use rix::cmd; 3 | use std::process::ExitCode; 4 | 5 | fn main() -> ExitCode { 6 | let mut cmd = Command::new("rix") 7 | .version("0.0.1") 8 | .about("Rix is another nix."); 9 | 10 | let subcommands = &[&cmd::eval::cmd(), &cmd::transpile::cmd()]; 11 | 12 | for subcommand in subcommands { 13 | cmd = cmd.subcommand((subcommand.cmd)(Command::new(subcommand.name))); 14 | } 15 | 16 | dispatch_cmd(&cmd.get_matches(), subcommands) 17 | } 18 | 19 | fn dispatch_cmd(parsed_args: &clap::ArgMatches, subcommands: &[&cmd::RixSubCommand]) -> ExitCode { 20 | for subcommand in subcommands { 21 | if let Some(subcommand_args) = parsed_args.subcommand_matches(subcommand.name) { 22 | return (subcommand.handler)(subcommand_args) 23 | .map_or_else(|err| err, |_| ExitCode::SUCCESS); 24 | } 25 | } 26 | cmd::print_and_err("operation not supported".into()) 27 | } 28 | -------------------------------------------------------------------------------- /src/tests/attr_set.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ 4 | eval::{ 5 | error::NixErrorKind, 6 | types::{NixTypeKind, Value}, 7 | }, 8 | tests::{eval_err, eval_ok}, 9 | }; 10 | 11 | #[test] 12 | fn eval_attrset_literal() { 13 | assert_eq!(eval_ok("{}"), Value::AttrSet(HashMap::new())); 14 | assert_eq!( 15 | eval_ok("{a = 1;}"), 16 | Value::AttrSet(HashMap::from([("a".to_owned(), Value::Int(1))])) 17 | ); 18 | } 19 | 20 | #[test] 21 | fn eval_attrset_literal_nesting() { 22 | let expected_attrset = Value::AttrSet(HashMap::from([( 23 | "a".to_owned(), 24 | Value::AttrSet(HashMap::from([("b".to_owned(), Value::Int(1))])), 25 | )])); 26 | assert_eq!(eval_ok("{a.b = 1;}"), expected_attrset); 27 | assert_eq!(eval_ok("{ a = {}; a.b = 1; }"), expected_attrset); 28 | // TODO: Improve this error? Instead of a basic type mismatch 29 | assert_eq!( 30 | eval_err("{ a = 1; a.b = 1; }"), 31 | NixErrorKind::TypeMismatch { 32 | expected: vec![NixTypeKind::Set], 33 | got: NixTypeKind::Int 34 | } 35 | ); 36 | // TODO: Replicate behaviour: nix currently throws an error while evaluating the following: 37 | // { a = builtins.trace "Evaluated" {}; a.b = 1; } 38 | // error: attribute 'a.b' already defined at 39 | // In similar vein, `nix` throws evaluating this expression: 40 | // let c = {}; in { a = c; a.b = 1; } 41 | // We should reproduce this here. 42 | } 43 | 44 | #[test] 45 | fn eval_attrset_interpolated_attrs() { 46 | assert_eq!(eval_ok(r#"{${"a"} = 1;}.a"#), Value::Int(1)); 47 | assert_eq!(eval_ok(r#"{${"a"}.b = 1;}.a.b"#), Value::Int(1)); 48 | assert_eq!(eval_ok(r#"{a.${"b"} = 1;}.a.b"#), Value::Int(1)); 49 | } 50 | 51 | #[test] 52 | fn eval_attrset_null_attr() { 53 | assert_eq!( 54 | eval_ok(r#"{ ${null} = true; }"#), 55 | Value::AttrSet(HashMap::new()), 56 | ); 57 | assert_eq!( 58 | eval_ok(r#"{ a.${null} = true; }"#), 59 | Value::AttrSet(HashMap::from([( 60 | "a".to_owned(), 61 | Value::AttrSet(HashMap::new()), 62 | )])), 63 | ); 64 | } 65 | 66 | #[test] 67 | fn eval_recursive_attrset() { 68 | assert_eq!(eval_ok("rec { a = 1; b = a + 1; }.b"), Value::Int(2)); 69 | assert_eq!(eval_ok(r#"rec { a = "b"; ${a} = 1; }.b"#), Value::Int(1)); 70 | } 71 | 72 | #[test] 73 | fn eval_attrset_non_string_attr() { 74 | assert_eq!( 75 | eval_err(r#"{ ${1} = true; }"#), 76 | NixErrorKind::TypeMismatch { 77 | expected: vec![NixTypeKind::String, NixTypeKind::Path], 78 | got: NixTypeKind::Int 79 | } 80 | ); 81 | } 82 | 83 | #[test] 84 | fn eval_attrset_update() { 85 | assert_eq!(eval_ok("{} // {}"), Value::AttrSet(HashMap::new())); 86 | assert_eq!( 87 | eval_ok("{a = 1; b = 2;} // {a = 3; c = 1;}"), 88 | Value::AttrSet(HashMap::from([ 89 | ("a".to_owned(), Value::Int(3)), 90 | ("b".to_owned(), Value::Int(2)), 91 | ("c".to_owned(), Value::Int(1)), 92 | ])) 93 | ); 94 | 95 | // TODO: Improve the two errors below? 96 | assert_eq!( 97 | eval_err("{} // 1"), 98 | NixErrorKind::TypeMismatch { 99 | expected: vec![], 100 | got: NixTypeKind::Set 101 | } 102 | ); 103 | assert_eq!( 104 | eval_err("1 // {}"), 105 | NixErrorKind::TypeMismatch { 106 | expected: vec![], 107 | got: NixTypeKind::Int 108 | } 109 | ); 110 | } 111 | 112 | #[test] 113 | fn eval_attrset_has() { 114 | assert_eq!(eval_ok("{a = 1;} ? a"), Value::Bool(true)); 115 | assert_eq!(eval_ok("{a = 1;} ? \"a\""), Value::Bool(true)); 116 | assert_eq!(eval_ok("{a = {b = 1;};} ? a.c"), Value::Bool(false)); 117 | } 118 | 119 | #[test] 120 | fn eval_attrset_select() { 121 | assert_eq!(eval_ok("{a = 1;}.a"), Value::Int(1)); 122 | assert_eq!(eval_ok("{a = 1;}.b or 2"), Value::Int(2)); 123 | } 124 | -------------------------------------------------------------------------------- /src/tests/builtins.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | #![allow(non_snake_case)] 3 | 4 | use crate::{eval::error::NixErrorKind, tests::eval_err}; 5 | use crate::{ 6 | eval::types::{NixTypeKind, Value}, 7 | tests::eval_ok, 8 | }; 9 | 10 | // Builtins are sorted by the order they appear in the Nix manual 11 | // https://nixos.org/manual/nix/stable/language/builtins.html 12 | 13 | mod derivation { 14 | use super::*; 15 | } 16 | 17 | mod abort { 18 | use super::*; 19 | 20 | #[test] 21 | fn eval() { 22 | assert_eq!( 23 | eval_err("builtins.abort \"foo\""), 24 | NixErrorKind::Abort { 25 | message: "foo".to_owned() 26 | } 27 | ); 28 | } 29 | } 30 | 31 | mod add { 32 | use super::*; 33 | 34 | #[test] 35 | fn eval_ints() { 36 | assert_eq!(eval_ok("builtins.add 1 2"), Value::Int(3)); 37 | } 38 | 39 | #[test] 40 | fn eval_floats() { 41 | assert_eq!(eval_ok("builtins.add 1.0 2.0"), Value::Float(3.0)); 42 | } 43 | 44 | #[test] 45 | fn eval_mixed() { 46 | assert_eq!(eval_ok("builtins.add 1 2.0"), Value::Float(3.0)); 47 | assert_eq!(eval_ok("builtins.add 1.0 2"), Value::Float(3.0)); 48 | } 49 | } 50 | 51 | mod addDrvOutputDependencies { 52 | use super::*; 53 | } 54 | 55 | mod all { 56 | use super::*; 57 | 58 | #[test] 59 | fn eval() { 60 | assert_eq!( 61 | eval_ok("builtins.all (a: a == 1) [ 1 1 ]"), 62 | Value::Bool(true) 63 | ); 64 | assert_eq!( 65 | eval_ok("builtins.all (a: a == 1) [ 1 2 ]"), 66 | Value::Bool(false) 67 | ); 68 | } 69 | 70 | #[test] 71 | fn eval_lazy() { 72 | assert_eq!( 73 | eval_ok("builtins.all (a: false) [ 1 (1 / 0) ]"), 74 | Value::Bool(false) 75 | ); 76 | } 77 | 78 | #[test] 79 | fn eval_empty() { 80 | assert_eq!(eval_ok("builtins.all (a: a == 1) []"), Value::Bool(true)); 81 | } 82 | 83 | #[test] 84 | fn eval_non_lambda() { 85 | assert_eq!( 86 | eval_err("builtins.all 1 [ 1 2 ]"), 87 | NixErrorKind::TypeMismatch { 88 | expected: vec![NixTypeKind::Lambda], 89 | got: NixTypeKind::Int 90 | } 91 | ); 92 | } 93 | 94 | #[test] 95 | fn eval_non_list() { 96 | assert_eq!( 97 | eval_err("builtins.all (a: a == 1) 1"), 98 | NixErrorKind::TypeMismatch { 99 | expected: vec![NixTypeKind::List], 100 | got: NixTypeKind::Int 101 | } 102 | ); 103 | } 104 | } 105 | 106 | mod any { 107 | use super::*; 108 | 109 | #[test] 110 | fn eval() { 111 | assert_eq!( 112 | eval_ok("builtins.any (a: a == 1) [ 1 2 ]"), 113 | Value::Bool(true) 114 | ); 115 | assert_eq!( 116 | eval_ok("builtins.any (a: a == 1) [ 2 2 ]"), 117 | Value::Bool(false) 118 | ); 119 | } 120 | 121 | #[test] 122 | fn eval_lazy() { 123 | assert_eq!( 124 | eval_ok("builtins.any (a: true) [ 1 (1 / 0) ]"), 125 | Value::Bool(true) 126 | ); 127 | } 128 | 129 | #[test] 130 | fn eval_empty() { 131 | assert_eq!(eval_ok("builtins.any (a: a == 1) []"), Value::Bool(false)); 132 | } 133 | 134 | #[test] 135 | fn eval_non_lambda() { 136 | assert_eq!( 137 | eval_err("builtins.any 1 [ 1 2 ]"), 138 | NixErrorKind::TypeMismatch { 139 | expected: vec![NixTypeKind::Lambda], 140 | got: NixTypeKind::Int 141 | } 142 | ); 143 | } 144 | 145 | #[test] 146 | fn eval_non_list() { 147 | assert_eq!( 148 | eval_err("builtins.any (a: a == 1) 1"), 149 | NixErrorKind::TypeMismatch { 150 | expected: vec![NixTypeKind::List], 151 | got: NixTypeKind::Int 152 | } 153 | ); 154 | } 155 | } 156 | 157 | mod attrNames { 158 | use super::*; 159 | 160 | #[test] 161 | fn eval() { 162 | assert_eq!( 163 | eval_ok("builtins.attrNames { b = true; a = false; }"), 164 | Value::List(vec![Value::Str("a".into()), Value::Str("b".into())]) 165 | ); 166 | } 167 | 168 | #[test] 169 | fn eval_lazy() { 170 | assert_eq!( 171 | eval_ok("builtins.head (builtins.attrNames { b = 1 / 0; a = false; })"), 172 | Value::Str("a".into()) 173 | ); 174 | } 175 | 176 | #[test] 177 | fn eval_empty() { 178 | assert_eq!(eval_ok("builtins.attrNames {}"), Value::List(Vec::new())); 179 | } 180 | 181 | #[test] 182 | fn eval_nested() { 183 | assert_eq!( 184 | eval_ok("builtins.attrNames { a = { b = 1; }; }"), 185 | Value::List(vec![Value::Str("a".into())]) 186 | ); 187 | } 188 | 189 | #[test] 190 | fn eval_non_attr_set() { 191 | assert_eq!( 192 | eval_err("builtins.attrNames 1"), 193 | NixErrorKind::TypeMismatch { 194 | expected: vec![NixTypeKind::Set], 195 | got: NixTypeKind::Int 196 | } 197 | ); 198 | } 199 | } 200 | 201 | mod attrValues { 202 | use super::*; 203 | 204 | #[test] 205 | fn eval() { 206 | assert_eq!( 207 | eval_ok("builtins.attrValues { b = true; a = false; }"), 208 | Value::List(vec![Value::Bool(false), Value::Bool(true)]) 209 | ); 210 | } 211 | 212 | #[test] 213 | fn eval_lazy() { 214 | assert_eq!( 215 | eval_ok("builtins.head (builtins.attrValues { b = 1 / 0; a = false; })"), 216 | Value::Bool(false) 217 | ); 218 | } 219 | 220 | #[test] 221 | fn eval_empty() { 222 | assert_eq!(eval_ok("builtins.attrValues {}"), Value::List(Vec::new())); 223 | } 224 | 225 | #[test] 226 | fn eval_nested() { 227 | assert_eq!( 228 | eval_ok("builtins.attrValues { a = { b = 1; }; }"), 229 | Value::List(vec![Value::AttrSet( 230 | vec![("b".into(), Value::Int(1))].into_iter().collect() 231 | )]) 232 | ); 233 | } 234 | 235 | #[test] 236 | fn eval_non_attr_set() { 237 | assert_eq!( 238 | eval_err("builtins.attrValues 1"), 239 | NixErrorKind::TypeMismatch { 240 | expected: vec![NixTypeKind::Set], 241 | got: NixTypeKind::Int 242 | } 243 | ); 244 | } 245 | } 246 | 247 | mod baseNameOf { 248 | use super::*; 249 | 250 | // Returns everything following the final slash 251 | 252 | #[test] 253 | fn eval() { 254 | assert_eq!( 255 | eval_ok("builtins.baseNameOf \"/foo/bar/baz\""), 256 | Value::Str("baz".into()) 257 | ); 258 | assert_eq!( 259 | eval_ok("builtins.baseNameOf \"/foo/bar/baz/\""), 260 | Value::Str("baz".into()) 261 | ); 262 | assert_eq!( 263 | eval_ok("builtins.baseNameOf \"/foo/bar/baz//\""), 264 | Value::Str("".into()) 265 | ); 266 | assert_eq!( 267 | eval_ok("builtins.baseNameOf \"foo\""), 268 | Value::Str("foo".into()) 269 | ); 270 | } 271 | 272 | #[test] 273 | fn eval_path() { 274 | assert_eq!( 275 | eval_ok("builtins.baseNameOf /foo/bar/baz"), 276 | Value::Str("baz".into()) 277 | ); 278 | assert_eq!( 279 | eval_ok("builtins.baseNameOf ./foo"), 280 | Value::Str("foo".into()) 281 | ); 282 | } 283 | 284 | #[test] 285 | fn eval_invalid_types() { 286 | assert_eq!( 287 | eval_err("builtins.baseNameOf 1"), 288 | NixErrorKind::TypeMismatch { 289 | expected: vec![NixTypeKind::String, NixTypeKind::Path], 290 | got: NixTypeKind::Int 291 | } 292 | ); 293 | } 294 | } 295 | 296 | mod bitAnd { 297 | use super::*; 298 | } 299 | 300 | mod bitOr { 301 | use super::*; 302 | } 303 | 304 | mod bitXor { 305 | use super::*; 306 | } 307 | 308 | mod break_ { 309 | use super::*; 310 | } 311 | 312 | mod catAttrs { 313 | use super::*; 314 | } 315 | 316 | mod ceil { 317 | use super::*; 318 | } 319 | 320 | mod compareVersions { 321 | use super::*; 322 | } 323 | 324 | mod concatLists { 325 | use super::*; 326 | } 327 | 328 | mod concatMap { 329 | use super::*; 330 | } 331 | 332 | mod concatStringsSep { 333 | use super::*; 334 | } 335 | 336 | mod convertHash { 337 | use super::*; 338 | } 339 | 340 | mod deepSeq { 341 | use super::*; 342 | } 343 | 344 | mod dirOf { 345 | use super::*; 346 | } 347 | 348 | mod div { 349 | use super::*; 350 | } 351 | 352 | mod elem { 353 | use super::*; 354 | } 355 | 356 | mod elemAt { 357 | use super::*; 358 | } 359 | 360 | mod fetchClosure { 361 | use super::*; 362 | } 363 | 364 | mod fetchGit { 365 | use super::*; 366 | } 367 | 368 | mod fetchTarball { 369 | use super::*; 370 | } 371 | 372 | mod fetchTree { 373 | use super::*; 374 | } 375 | 376 | mod fetchurl { 377 | use super::*; 378 | } 379 | 380 | mod filter { 381 | use super::*; 382 | } 383 | 384 | mod filterSource { 385 | use super::*; 386 | } 387 | 388 | mod findFile { 389 | use super::*; 390 | } 391 | 392 | mod flakeRefToString { 393 | use super::*; 394 | } 395 | 396 | mod floor { 397 | use super::*; 398 | } 399 | 400 | mod foldl { 401 | use super::*; 402 | } 403 | 404 | mod fromJSON { 405 | use super::*; 406 | } 407 | 408 | mod fromTOML { 409 | use super::*; 410 | } 411 | 412 | mod functionArgs { 413 | use super::*; 414 | } 415 | 416 | mod genList { 417 | use super::*; 418 | } 419 | 420 | mod genericClosure { 421 | use super::*; 422 | } 423 | 424 | mod getAttr { 425 | use super::*; 426 | } 427 | 428 | mod getContext { 429 | use super::*; 430 | } 431 | 432 | mod getEnv { 433 | use super::*; 434 | } 435 | 436 | mod getFlake { 437 | use super::*; 438 | } 439 | 440 | mod groupBy { 441 | use super::*; 442 | } 443 | 444 | mod hasAttr { 445 | use super::*; 446 | } 447 | 448 | mod hasContext { 449 | use super::*; 450 | } 451 | 452 | mod hashFile { 453 | use super::*; 454 | } 455 | 456 | mod hashString { 457 | use super::*; 458 | } 459 | 460 | mod head { 461 | use super::*; 462 | 463 | #[test] 464 | fn eval() { 465 | assert_eq!(eval_ok("builtins.head [ 1 2 ]"), Value::Int(1)); 466 | } 467 | 468 | #[test] 469 | fn eval_lazy() { 470 | assert_eq!(eval_ok("builtins.head [ 1 (1 / 0) ]"), Value::Int(1)); 471 | } 472 | 473 | #[test] 474 | fn eval_empty() { 475 | // Would be weird to have a custom error message kind for this, imo. 476 | assert_eq!( 477 | eval_err("builtins.head []"), 478 | NixErrorKind::Other { 479 | codename: "builtins-head-on-empty-list".to_string() 480 | } 481 | ); 482 | } 483 | } 484 | 485 | mod import { 486 | use super::*; 487 | 488 | #[test] 489 | fn eval() { 490 | assert_eq!( 491 | eval_ok("(builtins.import ./src/tests/import_tests/basic.nix).data"), 492 | Value::Str("imported!".into()) 493 | ); 494 | } 495 | 496 | #[test] 497 | fn eval_same_folder_import() { 498 | assert_eq!( 499 | eval_ok("(builtins.import ./src/tests/import_tests/same-folder-import.nix).dataPath"), 500 | Value::Str("imported!".into()) 501 | ); 502 | assert_eq!( 503 | eval_ok("(builtins.import ./src/tests/import_tests/same-folder-import.nix).dataString"), 504 | Value::Str("imported!".into()) 505 | ); 506 | } 507 | 508 | #[test] 509 | fn eval_child_folder_import() { 510 | assert_eq!( 511 | eval_ok("(builtins.import ./src/tests/import_tests/child-folder-import.nix).dataPath"), 512 | Value::Str("imported!".into()) 513 | ); 514 | assert_eq!( 515 | eval_ok( 516 | "(builtins.import ./src/tests/import_tests/child-folder-import.nix).dataString" 517 | ), 518 | Value::Str("imported!".into()) 519 | ); 520 | } 521 | 522 | #[test] 523 | fn eval_parent_folder_import() { 524 | assert_eq!( 525 | eval_ok("(builtins.import ./src/tests/import_tests/nested/parent-folder-import.nix).dataPath"), 526 | Value::Str("imported!".into()) 527 | ); 528 | assert_eq!( 529 | eval_ok("(builtins.import ./src/tests/import_tests/nested/parent-folder-import.nix).dataString"), 530 | Value::Str("imported!".into()) 531 | ); 532 | } 533 | 534 | #[test] 535 | fn eval_relative_string() { 536 | assert_eq!( 537 | eval_err(r#"builtins.import "./foo.nix""#), 538 | NixErrorKind::Other { 539 | codename: "builtins-import-non-absolute-path".to_owned() 540 | } 541 | ) 542 | } 543 | 544 | #[test] 545 | fn eval_lazy() { 546 | assert_eq!( 547 | eval_ok("let value = (builtins.import ./error.nix); in 1"), 548 | Value::Int(1) 549 | ); 550 | } 551 | 552 | // TODO: Make this test work. 553 | // fn eval_invalid_file() { 554 | // assert_eq!( 555 | // eval_err("builtins.import ./non_existent_file.nix"), 556 | // NixErrorKind::Import { 557 | // path: "./non_existent_file.nix".to_owned() 558 | // } 559 | // ); 560 | // } 561 | } 562 | 563 | mod intersectAttrs { 564 | use super::*; 565 | } 566 | 567 | mod isAttrs { 568 | use super::*; 569 | 570 | #[test] 571 | fn eval_true() { 572 | assert_eq!( 573 | eval_ok("builtins.isAttrs { a = 1; b = 2; }"), 574 | Value::Bool(true) 575 | ); 576 | assert_eq!(eval_ok("builtins.isAttrs { a = 1; }"), Value::Bool(true)); 577 | } 578 | 579 | #[test] 580 | fn eval_false() { 581 | assert_eq!(eval_ok("builtins.isAttrs 1"), Value::Bool(false)); 582 | assert_eq!(eval_ok("builtins.isAttrs [ 1 2 ]"), Value::Bool(false)); 583 | } 584 | } 585 | 586 | mod isBool { 587 | use super::*; 588 | 589 | #[test] 590 | fn eval_true() { 591 | assert_eq!(eval_ok("builtins.isBool true"), Value::Bool(true)); 592 | assert_eq!(eval_ok("builtins.isBool false"), Value::Bool(true)); 593 | } 594 | 595 | #[test] 596 | fn eval_false() { 597 | assert_eq!(eval_ok("builtins.isBool 1"), Value::Bool(false)); 598 | assert_eq!(eval_ok("builtins.isBool [ 1 2 ]"), Value::Bool(false)); 599 | } 600 | } 601 | 602 | mod isFloat { 603 | use super::*; 604 | 605 | #[test] 606 | fn eval_true() { 607 | assert_eq!(eval_ok("builtins.isFloat 1.0"), Value::Bool(true)); 608 | } 609 | 610 | #[test] 611 | fn eval_false() { 612 | assert_eq!(eval_ok("builtins.isFloat { }"), Value::Bool(false)); 613 | assert_eq!(eval_ok("builtins.isFloat true"), Value::Bool(false)); 614 | } 615 | } 616 | 617 | mod isFunction { 618 | use super::*; 619 | 620 | #[test] 621 | fn eval_true() { 622 | assert_eq!(eval_ok("builtins.isFunction (x: x)"), Value::Bool(true)); 623 | } 624 | 625 | #[test] 626 | fn eval_false() { 627 | assert_eq!(eval_ok("builtins.isFunction 1"), Value::Bool(false)); 628 | assert_eq!(eval_ok("builtins.isFunction [ 1 2 ]"), Value::Bool(false)); 629 | } 630 | } 631 | 632 | mod isInt { 633 | use super::*; 634 | 635 | #[test] 636 | fn eval_true() { 637 | assert_eq!(eval_ok("builtins.isInt 1"), Value::Bool(true)); 638 | } 639 | 640 | #[test] 641 | fn eval_false() { 642 | assert_eq!(eval_ok("builtins.isInt { }"), Value::Bool(false)); 643 | assert_eq!(eval_ok("builtins.isInt true"), Value::Bool(false)); 644 | } 645 | } 646 | 647 | mod isList { 648 | use super::*; 649 | 650 | #[test] 651 | fn eval_true() { 652 | assert_eq!(eval_ok("builtins.isList [ 1 2 ]"), Value::Bool(true)); 653 | } 654 | 655 | #[test] 656 | fn eval_false() { 657 | assert_eq!(eval_ok("builtins.isList { }"), Value::Bool(false)); 658 | assert_eq!(eval_ok("builtins.isList true"), Value::Bool(false)); 659 | } 660 | } 661 | 662 | mod isNull { 663 | use super::*; 664 | 665 | #[test] 666 | fn eval_true() { 667 | assert_eq!(eval_ok("builtins.isNull null"), Value::Bool(true)); 668 | } 669 | 670 | #[test] 671 | fn eval_false() { 672 | assert_eq!(eval_ok("builtins.isNull { }"), Value::Bool(false)); 673 | assert_eq!(eval_ok("builtins.isNull true"), Value::Bool(false)); 674 | } 675 | } 676 | 677 | mod isPath { 678 | use super::*; 679 | 680 | #[test] 681 | fn eval_true() { 682 | assert_eq!(eval_ok("builtins.isPath ./foo"), Value::Bool(true)); 683 | } 684 | 685 | #[test] 686 | fn eval_false() { 687 | assert_eq!(eval_ok("builtins.isPath { }"), Value::Bool(false)); 688 | assert_eq!(eval_ok("builtins.isPath true"), Value::Bool(false)); 689 | } 690 | } 691 | 692 | mod isString { 693 | use super::*; 694 | 695 | #[test] 696 | fn eval_true() { 697 | assert_eq!(eval_ok("builtins.isString \"foo\""), Value::Bool(true)); 698 | } 699 | 700 | #[test] 701 | fn eval_false() { 702 | assert_eq!(eval_ok("builtins.isString { }"), Value::Bool(false)); 703 | assert_eq!(eval_ok("builtins.isString true"), Value::Bool(false)); 704 | } 705 | } 706 | 707 | mod length { 708 | use super::*; 709 | } 710 | 711 | mod lessThan { 712 | use super::*; 713 | } 714 | 715 | mod listToAttrs { 716 | use super::*; 717 | } 718 | 719 | mod map { 720 | use super::*; 721 | } 722 | 723 | mod mapAttrs { 724 | use super::*; 725 | } 726 | 727 | mod match_ { 728 | use super::*; 729 | } 730 | 731 | mod mul { 732 | use super::*; 733 | } 734 | 735 | mod outputOf { 736 | use super::*; 737 | } 738 | 739 | mod parseDrvName { 740 | use super::*; 741 | } 742 | 743 | mod parseFlakeRef { 744 | use super::*; 745 | } 746 | 747 | mod partition { 748 | use super::*; 749 | } 750 | 751 | mod path { 752 | use super::*; 753 | } 754 | 755 | mod pathExists { 756 | use super::*; 757 | } 758 | 759 | mod placeholder { 760 | use super::*; 761 | } 762 | 763 | mod readDir { 764 | use super::*; 765 | } 766 | 767 | mod readFile { 768 | use super::*; 769 | } 770 | 771 | mod readFileType { 772 | use super::*; 773 | } 774 | 775 | mod removeAttrs { 776 | use super::*; 777 | } 778 | 779 | mod replaceStrings { 780 | use super::*; 781 | } 782 | 783 | mod seq { 784 | use super::*; 785 | } 786 | 787 | mod sort { 788 | use super::*; 789 | } 790 | 791 | mod split { 792 | use super::*; 793 | } 794 | 795 | mod splitVersion { 796 | use super::*; 797 | } 798 | 799 | mod storePath { 800 | use super::*; 801 | } 802 | 803 | mod stringLength { 804 | use super::*; 805 | } 806 | 807 | mod sub { 808 | use super::*; 809 | } 810 | 811 | mod substring { 812 | use super::*; 813 | } 814 | 815 | mod tail { 816 | use super::*; 817 | } 818 | 819 | mod throw { 820 | use super::*; 821 | } 822 | 823 | mod toFile { 824 | use super::*; 825 | } 826 | 827 | mod toJSON { 828 | use super::*; 829 | } 830 | 831 | mod toPath { 832 | use super::*; 833 | } 834 | 835 | mod toString { 836 | use super::*; 837 | } 838 | 839 | mod toXML { 840 | use super::*; 841 | } 842 | 843 | mod trace { 844 | use super::*; 845 | } 846 | 847 | mod traceVerbose { 848 | use super::*; 849 | } 850 | 851 | mod tryEval { 852 | use super::*; 853 | } 854 | 855 | mod typeOf { 856 | use super::*; 857 | } 858 | 859 | mod unsafeDiscardOutputDependency { 860 | use super::*; 861 | } 862 | 863 | mod zipAttrsWith { 864 | use super::*; 865 | } 866 | -------------------------------------------------------------------------------- /src/tests/import_tests/basic.nix: -------------------------------------------------------------------------------- 1 | { 2 | data = "imported!"; 3 | } 4 | -------------------------------------------------------------------------------- /src/tests/import_tests/child-folder-import.nix: -------------------------------------------------------------------------------- 1 | { 2 | dataPath = (builtins.import ./nested/basic.nix).data; 3 | dataString = (builtins.import (builtins.toString ./nested/basic.nix)).data; 4 | } 5 | -------------------------------------------------------------------------------- /src/tests/import_tests/nested/basic.nix: -------------------------------------------------------------------------------- 1 | { 2 | data = "imported!"; 3 | } 4 | -------------------------------------------------------------------------------- /src/tests/import_tests/nested/parent-folder-import.nix: -------------------------------------------------------------------------------- 1 | { 2 | dataPath = (builtins.import ../basic.nix).data; 3 | dataString = (builtins.import (builtins.toString ../basic.nix)).data; 4 | } 5 | -------------------------------------------------------------------------------- /src/tests/import_tests/same-folder-import.nix: -------------------------------------------------------------------------------- 1 | { 2 | dataPath = (builtins.import ./basic.nix).data; 3 | dataString = (builtins.import (builtins.toString ./basic.nix)).data; 4 | } 5 | -------------------------------------------------------------------------------- /src/tests/lambda.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | eval::{error::NixErrorKind, types::Value}, 3 | tests::{eval_err, eval_ok}, 4 | }; 5 | 6 | #[test] 7 | fn eval_lambda() { 8 | assert_eq!(eval_ok("a: 1"), Value::Lambda); 9 | } 10 | 11 | #[test] 12 | fn eval_lambda_application() { 13 | assert_eq!(eval_ok("(a: 1) 2"), Value::Int(1)); 14 | assert_eq!(eval_ok("(a: a + 1) 2"), Value::Int(3)); 15 | } 16 | 17 | #[test] 18 | fn eval_pattern_lambda() { 19 | assert_eq!(eval_ok("({a, b}: a + b) {a = 1; b = 2;}"), Value::Int(3)); 20 | assert_eq!(eval_ok("({a, b ? 2}: a + b) {a = 1;}"), Value::Int(3)); 21 | // TODO: Improve error 22 | assert_eq!( 23 | eval_err("{a, a}: a"), 24 | NixErrorKind::UnexpectedRustError { 25 | message: "duplicate formal function argument 'a'.".to_string() 26 | } 27 | ); 28 | } 29 | 30 | #[test] 31 | fn eval_pattern_lambda_args_binding() { 32 | assert_eq!(eval_ok("({a}@args: args.a) {a = 1;}"), Value::Int(1)); 33 | // TODO: Improve error 34 | assert_eq!( 35 | eval_err("{a}@a: a"), 36 | NixErrorKind::UnexpectedRustError { 37 | message: "duplicate formal function argument 'a'.".to_string() 38 | } 39 | ); 40 | assert_eq!( 41 | eval_err("({a ? 1}@args: args.a) {}"), 42 | NixErrorKind::MissingAttribute { 43 | attr_path: vec!["a".to_string()] 44 | } 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/tests/literals.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | eval::{ 3 | error::NixErrorKind, 4 | types::{NixTypeKind, Value}, 5 | }, 6 | tests::{eval_err, eval_ok}, 7 | }; 8 | 9 | #[test] 10 | fn eval_int_literals() { 11 | assert_eq!(eval_ok("1"), Value::Int(1)); 12 | assert_eq!(eval_ok("-1"), Value::Int(-1)); 13 | assert_eq!(eval_ok("0"), Value::Int(0)); 14 | assert_eq!(eval_ok("1234567890"), Value::Int(1234567890)); 15 | 16 | // Test i64 limits 17 | assert_eq!( 18 | eval_ok("9223372036854775807"), 19 | Value::Int(9223372036854775807) 20 | ); 21 | assert_eq!( 22 | eval_ok("-9223372036854775808"), 23 | Value::Int(-9223372036854775808) 24 | ); 25 | } 26 | 27 | #[test] 28 | fn eval_float_literals() { 29 | assert_eq!(eval_ok("1.0"), Value::Float(1.0)); 30 | assert_eq!(eval_ok("-1.0"), Value::Float(-1.0)); 31 | assert_eq!(eval_ok("0.0"), Value::Float(0.0)); 32 | assert_eq!(eval_ok("3.14"), Value::Float(3.14)); 33 | assert_eq!(eval_ok("-3.14"), Value::Float(-3.14)); 34 | } 35 | 36 | #[test] 37 | fn eval_complex_float_literals() { 38 | // Turns out nix doesn't support scientific notation without a decimal point 39 | // assert_eq!(eval_ok("1e10"), Value::Float(1e10)); 40 | // assert_eq!(eval_ok("-1e10"), Value::Float(-1e10)); 41 | 42 | assert_eq!(eval_ok("2.5e-3"), Value::Float(2.5e-3)); 43 | assert_eq!(eval_ok("-3.14e2"), Value::Float(-3.14e2)); 44 | assert_eq!(eval_ok(".25e-3"), Value::Float(0.25e-3)); 45 | assert_eq!(eval_ok("-.314e2"), Value::Float(-0.314e2)); 46 | } 47 | 48 | #[test] 49 | fn eval_bool_literals() { 50 | assert_eq!(eval_ok("true"), Value::Bool(true)); 51 | assert_eq!(eval_ok("false"), Value::Bool(false)); 52 | } 53 | 54 | #[test] 55 | fn eval_string_literal() { 56 | assert_eq!(eval_ok(r#""Hello!""#), Value::Str("Hello!".to_owned())); 57 | } 58 | 59 | #[test] 60 | fn eval_string_literal_escape_codes() { 61 | assert_eq!( 62 | eval_ok(r#""\"\$\n\r\t\\`""#), 63 | Value::Str("\"$\n\r\t\\`".to_owned()) 64 | ); 65 | assert_eq!(eval_ok("\"a \n b\""), Value::Str("a \n b".to_owned())); 66 | } 67 | 68 | #[test] 69 | fn eval_string_uri() { 70 | assert_eq!( 71 | eval_ok("http://foo.bat/moo"), 72 | Value::Str("http://foo.bat/moo".to_owned()) 73 | ); 74 | } 75 | 76 | #[test] 77 | fn eval_indented_string() { 78 | assert_eq!( 79 | eval_ok( 80 | "'' 81 | Hello 82 | World!''" 83 | ), 84 | Value::Str("Hello\nWorld!".to_owned()) 85 | ); 86 | assert_eq!( 87 | eval_ok( 88 | "'' 89 | a 90 | b 91 | c''" 92 | ), 93 | Value::Str(" a\nb\n c".to_owned()) 94 | ); 95 | assert_eq!( 96 | eval_ok("''''$'''$${}''\\n''\\t''\\r''\\\\''"), 97 | Value::Str("$''$${}\n\t\r\\".to_owned()) 98 | ); 99 | } 100 | 101 | #[test] 102 | fn eval_string_interpolation() { 103 | let path = std::env::current_dir().unwrap(); 104 | 105 | assert_eq!(eval_ok(r#""${"A"}""#), Value::Str("A".to_owned())); 106 | assert_eq!( 107 | eval_ok(r#""${./foo}""#), 108 | Value::Str(format!("{}/foo", path.display())) 109 | ); 110 | assert_eq!( 111 | eval_err(r#""${1}""#), 112 | NixErrorKind::TypeMismatch { 113 | expected: vec![NixTypeKind::String, NixTypeKind::Path], 114 | got: NixTypeKind::Int 115 | } 116 | ); 117 | } 118 | 119 | #[test] 120 | fn eval_path() { 121 | assert_eq!(eval_ok("/."), Value::Path("/".to_owned())); 122 | assert_eq!(eval_ok("/a"), Value::Path("/a".to_owned())); 123 | assert_eq!( 124 | eval_ok("./a"), 125 | Value::Path(format!("{}/a", std::env::current_dir().unwrap().display())) 126 | ); 127 | assert_eq!( 128 | eval_ok("./a/../b"), 129 | Value::Path(format!("{}/b", std::env::current_dir().unwrap().display())) 130 | ); 131 | } 132 | 133 | #[test] 134 | fn eval_list_literal() { 135 | assert_eq!(eval_ok("[]"), Value::List(vec![])); 136 | assert_eq!( 137 | eval_ok(r#"[42 true "answer"]"#), 138 | Value::List(vec![ 139 | Value::Int(42), 140 | Value::Bool(true), 141 | Value::Str("answer".to_owned()) 142 | ]) 143 | ); 144 | assert_eq!( 145 | eval_ok( 146 | r#"[ 147 | 42 148 | true 149 | "answer" 150 | ]"# 151 | ), 152 | Value::List(vec![ 153 | Value::Int(42), 154 | Value::Bool(true), 155 | Value::Str("answer".to_owned()) 156 | ]) 157 | ); 158 | } 159 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | #![allow(clippy::expect_fun_call)] 3 | #![allow(clippy::approx_constant)] 4 | 5 | use crate::eval::{ 6 | error::NixErrorKind, 7 | execution::evaluate, 8 | types::{NixTypeKind, Value}, 9 | }; 10 | 11 | mod attr_set; 12 | mod builtins; 13 | mod lambda; 14 | mod literals; 15 | mod operators; 16 | 17 | fn eval_ok(nix_expr: &str) -> Value { 18 | let workdir = std::env::current_dir().unwrap(); 19 | match evaluate(nix_expr, &workdir) { 20 | Ok(val) => val, 21 | Err(err) => panic!("eval '{nix_expr}' shouldn't fail.\nError message: {err:?}",), 22 | } 23 | } 24 | 25 | fn eval_err(nix_expr: &str) -> NixErrorKind { 26 | let workdir = std::env::current_dir().unwrap(); 27 | evaluate(nix_expr, &workdir) 28 | .expect_err(&format!("eval '{nix_expr}' expected an error")) 29 | .kind 30 | } 31 | 32 | #[test] 33 | fn eval_if_then_else() { 34 | assert_eq!(eval_ok("if true then 1 else 0"), Value::Int(1)); 35 | assert_eq!(eval_ok("if false then 1 else 0"), Value::Int(0)); 36 | } 37 | 38 | #[test] 39 | fn eval_if_then_else_invalid_type() { 40 | assert_eq!( 41 | eval_err("if 0 then 1 else 0"), 42 | NixErrorKind::TypeMismatch { 43 | expected: vec![NixTypeKind::Bool], 44 | got: NixTypeKind::Int, 45 | } 46 | ); 47 | } 48 | 49 | #[test] 50 | fn eval_let_in() { 51 | assert_eq!(eval_ok("let a = 1; in a"), Value::Int(1)); 52 | } 53 | 54 | #[test] 55 | fn eval_with() { 56 | assert_eq!(eval_ok("with {a = 1;}; a"), Value::Int(1)); 57 | assert_eq!(eval_ok("let a = 2; in with {a = 1;}; a"), Value::Int(2)); 58 | assert_eq!(eval_ok("with {a = 1;}; with {a = 2;}; a"), Value::Int(2)); 59 | } 60 | 61 | #[test] 62 | fn eval_recursive_let() { 63 | assert_eq!(eval_ok("let a = 1; b = a + 1; in b"), Value::Int(2)); 64 | } 65 | -------------------------------------------------------------------------------- /src/tests/operators.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | eval::{ 3 | error::NixErrorKind, 4 | types::{NixTypeKind, Value}, 5 | }, 6 | tests::{eval_err, eval_ok}, 7 | }; 8 | 9 | #[test] 10 | fn eval_int_arithmetic() { 11 | assert_eq!(eval_ok("1 + 2"), Value::Int(3)); 12 | assert_eq!(eval_ok("1 - 2"), Value::Int(-1)); 13 | assert_eq!(eval_ok("1 * 2"), Value::Int(2)); 14 | assert_eq!(eval_ok("1 / 2"), Value::Int(0)); 15 | } 16 | 17 | #[test] 18 | fn eval_float_arithmetic() { 19 | assert_eq!(eval_ok("1.0 + 2.0"), Value::Float(3.0)); 20 | assert_eq!(eval_ok("1.0 - 2.0"), Value::Float(-1.0)); 21 | assert_eq!(eval_ok("1.0 * 2.0"), Value::Float(2.0)); 22 | assert_eq!(eval_ok("1.0 / 2.0"), Value::Float(0.5)); 23 | } 24 | 25 | #[test] 26 | fn eval_mixed_arithmetic() { 27 | assert_eq!(eval_ok("1 + 2.0"), Value::Float(3.0)); 28 | assert_eq!(eval_ok("1 - 2.0"), Value::Float(-1.0)); 29 | assert_eq!(eval_ok("1 * 2.0"), Value::Float(2.0)); 30 | assert_eq!(eval_ok("1 / 2.0"), Value::Float(0.5)); 31 | assert_eq!(eval_ok("2.0 + 1"), Value::Float(3.0)); 32 | assert_eq!(eval_ok("2.0 - 1"), Value::Float(1.0)); 33 | assert_eq!(eval_ok("2.0 * 1"), Value::Float(2.0)); 34 | assert_eq!(eval_ok("2.0 / 1"), Value::Float(2.0)); 35 | } 36 | 37 | #[test] 38 | fn eval_string_concatenation() { 39 | assert_eq!( 40 | eval_ok("\"hello\" + \"world\""), 41 | Value::Str("helloworld".to_string()) 42 | ); 43 | assert_eq!( 44 | eval_ok("\"hello\" + \" \" + \"world\""), 45 | Value::Str("hello world".to_string()) 46 | ); 47 | } 48 | 49 | #[test] 50 | fn eval_int_comparison() { 51 | assert_eq!(eval_ok("1 == 1"), Value::Bool(true)); 52 | assert_eq!(eval_ok("1 == 2"), Value::Bool(false)); 53 | assert_eq!(eval_ok("1 != 2"), Value::Bool(true)); 54 | assert_eq!(eval_ok("1 != 1"), Value::Bool(false)); 55 | assert_eq!(eval_ok("1 < 2"), Value::Bool(true)); 56 | assert_eq!(eval_ok("1 < 1"), Value::Bool(false)); 57 | assert_eq!(eval_ok("1 > 2"), Value::Bool(false)); 58 | assert_eq!(eval_ok("1 > 1"), Value::Bool(false)); 59 | assert_eq!(eval_ok("1 <= 2"), Value::Bool(true)); 60 | assert_eq!(eval_ok("1 <= 1"), Value::Bool(true)); 61 | assert_eq!(eval_ok("1 >= 2"), Value::Bool(false)); 62 | assert_eq!(eval_ok("1 >= 1"), Value::Bool(true)); 63 | } 64 | 65 | #[test] 66 | fn eval_float_comparison() { 67 | assert_eq!(eval_ok("1.0 == 1.0"), Value::Bool(true)); 68 | assert_eq!(eval_ok("1.0 == 2.0"), Value::Bool(false)); 69 | assert_eq!(eval_ok("1.0 != 2.0"), Value::Bool(true)); 70 | assert_eq!(eval_ok("1.0 != 1.0"), Value::Bool(false)); 71 | assert_eq!(eval_ok("1.0 < 2.0"), Value::Bool(true)); 72 | assert_eq!(eval_ok("1.0 < 1.0"), Value::Bool(false)); 73 | assert_eq!(eval_ok("1.0 > 2.0"), Value::Bool(false)); 74 | assert_eq!(eval_ok("1.0 > 1.0"), Value::Bool(false)); 75 | assert_eq!(eval_ok("1.0 <= 2.0"), Value::Bool(true)); 76 | assert_eq!(eval_ok("1.0 <= 1.0"), Value::Bool(true)); 77 | assert_eq!(eval_ok("1.0 >= 2.0"), Value::Bool(false)); 78 | assert_eq!(eval_ok("1.0 >= 1.0"), Value::Bool(true)); 79 | } 80 | 81 | #[test] 82 | fn eval_float_int_comparison() { 83 | assert_eq!(eval_ok("1.0 == 1"), Value::Bool(true)); 84 | assert_eq!(eval_ok("1.0 == 2"), Value::Bool(false)); 85 | assert_eq!(eval_ok("1.0 != 2"), Value::Bool(true)); 86 | assert_eq!(eval_ok("1.0 != 1"), Value::Bool(false)); 87 | assert_eq!(eval_ok("1.0 < 2"), Value::Bool(true)); 88 | assert_eq!(eval_ok("1.0 < 1"), Value::Bool(false)); 89 | assert_eq!(eval_ok("1.0 > 2"), Value::Bool(false)); 90 | assert_eq!(eval_ok("1.0 > 1"), Value::Bool(false)); 91 | assert_eq!(eval_ok("1.0 <= 2"), Value::Bool(true)); 92 | assert_eq!(eval_ok("1.0 <= 1"), Value::Bool(true)); 93 | assert_eq!(eval_ok("1.0 >= 2"), Value::Bool(false)); 94 | assert_eq!(eval_ok("1.0 >= 1"), Value::Bool(true)); 95 | 96 | assert_eq!(eval_ok("1 == 1.0"), Value::Bool(true)); 97 | assert_eq!(eval_ok("1 == 2.0"), Value::Bool(false)); 98 | assert_eq!(eval_ok("1 != 2.0"), Value::Bool(true)); 99 | assert_eq!(eval_ok("1 != 1.0"), Value::Bool(false)); 100 | assert_eq!(eval_ok("1 < 2.0"), Value::Bool(true)); 101 | assert_eq!(eval_ok("1 < 1.0"), Value::Bool(false)); 102 | assert_eq!(eval_ok("1 > 2.0"), Value::Bool(false)); 103 | assert_eq!(eval_ok("1 > 1.0"), Value::Bool(false)); 104 | assert_eq!(eval_ok("1 <= 2.0"), Value::Bool(true)); 105 | assert_eq!(eval_ok("1 <= 1.0"), Value::Bool(true)); 106 | assert_eq!(eval_ok("1 >= 2.0"), Value::Bool(false)); 107 | assert_eq!(eval_ok("1 >= 1.0"), Value::Bool(true)); 108 | } 109 | 110 | #[test] 111 | fn eval_string_comparison() { 112 | assert_eq!(eval_ok("\"abc\" == \"abc\""), Value::Bool(true)); 113 | assert_eq!(eval_ok("\"abc\" == \"def\""), Value::Bool(false)); 114 | assert_eq!(eval_ok("\"abc\" != \"def\""), Value::Bool(true)); 115 | assert_eq!(eval_ok("\"abc\" != \"abc\""), Value::Bool(false)); 116 | assert_eq!(eval_ok("\"abc\" < \"def\""), Value::Bool(true)); 117 | assert_eq!(eval_ok("\"abc\" < \"abc\""), Value::Bool(false)); 118 | assert_eq!(eval_ok("\"abc\" > \"def\""), Value::Bool(false)); 119 | assert_eq!(eval_ok("\"abc\" > \"abc\""), Value::Bool(false)); 120 | assert_eq!(eval_ok("\"abc\" <= \"def\""), Value::Bool(true)); 121 | assert_eq!(eval_ok("\"abc\" <= \"abc\""), Value::Bool(true)); 122 | assert_eq!(eval_ok("\"abc\" >= \"def\""), Value::Bool(false)); 123 | assert_eq!(eval_ok("\"abc\" >= \"abc\""), Value::Bool(true)); 124 | } 125 | 126 | #[test] 127 | fn eval_path_string_concatenation() { 128 | let curr_dir = std::env::current_dir().unwrap(); 129 | 130 | assert_eq!( 131 | eval_ok("./hello + \"world\""), 132 | Value::Path(format!("{}/helloworld", curr_dir.display())) 133 | ); 134 | assert_eq!( 135 | eval_ok("\"hello\" + ./world"), 136 | Value::Str(format!("hello{}/world", curr_dir.display())) 137 | ); 138 | } 139 | 140 | #[test] 141 | fn eval_path_concat() { 142 | let curr_dir = std::env::current_dir().unwrap(); 143 | 144 | assert_eq!( 145 | eval_ok("./foo + ./bar"), 146 | Value::Path(format!( 147 | "{}/foo{}/bar", 148 | curr_dir.display(), 149 | curr_dir.display() 150 | )) 151 | ); 152 | 153 | assert_eq!(eval_ok(r#"/. + "a""#), Value::Path("/a".to_owned())); 154 | assert_eq!(eval_ok(r#"/. + "./a/../b""#), Value::Path("/b".to_owned())); 155 | } 156 | 157 | #[test] 158 | fn eval_order_of_operations() { 159 | // Addition and subtraction have the same precedence, so they should be evaluated from left to right 160 | assert_eq!(eval_ok("1 + 2 - 3"), Value::Int(0)); 161 | assert_eq!(eval_ok("1 - 2 + 3"), Value::Int(2)); 162 | 163 | // Multiplication and division have higher precedence than addition and subtraction 164 | assert_eq!(eval_ok("1 + 2 * 3"), Value::Int(7)); 165 | assert_eq!(eval_ok("1 * 2 + 3"), Value::Int(5)); 166 | 167 | // Parentheses can be used to change the order of operations 168 | assert_eq!(eval_ok("(1 + 2) * 3"), Value::Int(9)); 169 | assert_eq!(eval_ok("1 * (2 + 3)"), Value::Int(5)); 170 | } 171 | 172 | #[test] 173 | fn eval_string_operator_errors() { 174 | assert_eq!( 175 | eval_err("1 + \"hello\""), 176 | NixErrorKind::TypeMismatch { 177 | expected: vec![], 178 | got: NixTypeKind::Int 179 | } 180 | ); 181 | assert_eq!( 182 | eval_err("\"hello\" - \"world\""), 183 | NixErrorKind::TypeMismatch { 184 | expected: vec![], 185 | got: NixTypeKind::String 186 | } 187 | ); 188 | assert_eq!( 189 | eval_err("\"hello\" * \"world\""), 190 | NixErrorKind::TypeMismatch { 191 | expected: vec![], 192 | got: NixTypeKind::String 193 | } 194 | ); 195 | assert_eq!( 196 | eval_err("\"hello\" / \"world\""), 197 | NixErrorKind::TypeMismatch { 198 | expected: vec![], 199 | got: NixTypeKind::String 200 | } 201 | ); 202 | 203 | // FIXME: It says string instead of int, because nixjs flips the operator. Should we address? 204 | assert_eq!( 205 | eval_err("\"hello\" < 123"), 206 | NixErrorKind::TypeMismatch { 207 | expected: vec![], 208 | got: NixTypeKind::String 209 | } 210 | ); 211 | assert_eq!( 212 | eval_err("\"hello\" > 123"), 213 | NixErrorKind::TypeMismatch { 214 | expected: vec![], 215 | got: NixTypeKind::Int 216 | } 217 | ); 218 | assert_eq!( 219 | eval_err("\"hello\" <= 123"), 220 | NixErrorKind::TypeMismatch { 221 | expected: vec![], 222 | got: NixTypeKind::Int 223 | } 224 | ); 225 | // FIXME: It says string instead of int, because nixjs flips the operator. Should we address? 226 | assert_eq!( 227 | eval_err("\"hello\" >= 123"), 228 | NixErrorKind::TypeMismatch { 229 | expected: vec![], 230 | got: NixTypeKind::String 231 | } 232 | ); 233 | } 234 | 235 | #[test] 236 | fn eval_bool_operations() { 237 | assert_eq!(eval_ok("!false"), Value::Bool(true)); 238 | assert_eq!(eval_ok("false || true"), Value::Bool(true)); 239 | assert_eq!(eval_ok("false || !false"), Value::Bool(true)); 240 | assert_eq!(eval_ok("true && true"), Value::Bool(true)); 241 | assert_eq!(eval_ok("false || true && false"), Value::Bool(false)); 242 | assert_eq!(eval_ok("false && true || false"), Value::Bool(false)); 243 | assert_eq!(eval_ok("true -> false"), Value::Bool(false)); 244 | } 245 | 246 | #[test] 247 | fn eval_list_operations() { 248 | assert_eq!( 249 | eval_ok("[1] ++ [2]"), 250 | Value::List(vec![Value::Int(1), Value::Int(2)]) 251 | ); 252 | assert_eq!( 253 | eval_ok("[1] ++ [2] ++ [3]"), 254 | Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)]) 255 | ); 256 | assert_eq!( 257 | eval_ok(r#"["a"] ++ [1] ++ [[] [] "b"]"#), 258 | Value::List(vec![ 259 | Value::Str("a".to_owned()), 260 | Value::Int(1), 261 | Value::List(vec![]), 262 | Value::List(vec![]), 263 | Value::Str("b".to_owned()) 264 | ]) 265 | ); 266 | } 267 | -------------------------------------------------------------------------------- /tests/cmd/eval.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::*; 2 | use predicates::prelude::*; 3 | use std::process::Command; 4 | 5 | #[test] 6 | fn help() { 7 | assert_cmd(&["--help"]) 8 | .success() 9 | .stderr(predicate::str::is_empty()); 10 | } 11 | 12 | #[test] 13 | fn eval_bool_expr() { 14 | assert_cmd(&["--expr", "false || true"]) 15 | .success() 16 | .stdout(predicate::str::diff("true\n")) 17 | .stderr(predicate::str::is_empty()); 18 | } 19 | 20 | #[test] 21 | fn eval_float_arithmetic_expr() { 22 | assert_cmd(&["--expr", "3.0 * 4.0 + 1.0 / 2.0"]) 23 | .success() 24 | .stdout(predicate::str::diff("12.5\n")) 25 | .stderr(predicate::str::is_empty()); 26 | } 27 | 28 | fn assert_cmd(eval_args: &[&str]) -> assert_cmd::assert::Assert { 29 | let mut rix_args = vec!["eval"]; 30 | rix_args.extend_from_slice(eval_args); 31 | return Command::cargo_bin("rix").unwrap().args(rix_args).assert(); 32 | } 33 | -------------------------------------------------------------------------------- /tests/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod eval; 2 | pub mod transpile; 3 | -------------------------------------------------------------------------------- /tests/cmd/transpile.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::*; 2 | use predicates::prelude::*; 3 | use std::process::Command; 4 | 5 | #[test] 6 | fn transpile_help() { 7 | assert_cmd(&["--help"]) 8 | .success() 9 | .stderr(predicate::str::is_empty()); 10 | } 11 | 12 | #[test] 13 | fn transpile_bool_expr() { 14 | assert_cmd(&["--expr", "1.0"]) 15 | .success() 16 | .stdout(predicate::str::contains( 17 | "export default (ctx) => new n.NixFloat(1.0);\n", 18 | )) 19 | .stderr(predicate::str::is_empty()); 20 | } 21 | 22 | fn assert_cmd(eval_args: &[&str]) -> assert_cmd::assert::Assert { 23 | let mut rix_args = vec!["transpile"]; 24 | rix_args.extend_from_slice(eval_args); 25 | return Command::cargo_bin("rix").unwrap().args(rix_args).assert(); 26 | } 27 | -------------------------------------------------------------------------------- /tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cmd; 2 | --------------------------------------------------------------------------------