Wechselbalg translates Nix values (including lambdas) into portable code for multiple targets: JavaScript, TypeScript, C++, Python, Kotlin, Rust and Nix.
The project performs a single-pass traversal over Nix values to produce an intermediate representation (IR), which is then rendered by target-specific backends. Wechselbalg attempts to preserve essential Nix semantics, so the generated code behaves similarly to the original value whenever possible.
Note: Wechselbalg depends on a fork of Nix that exposes the builtins
reify and sameFunction. See the fork at https://github.com/lomenzel/nix.
The fork only exposes them with the experimental feature ast-introspection
enabled, e.g.:
nix run github:lomenzel/nix -- --extra-experimental-features ast-introspection build .#my-cool-project-that-uses-wechselbalgAn overlay (overlays.default) adds helper functions to lib. The primary
translators are:
toNixtoCpptoRusttoJStoTStoPythontoKotlin
All non-Nix targets require a Value class (which handles
laziness and helper functions). Runtime snippets are available as:
lib.cppRuntimelib.jsRuntimelib.tsRuntimelib.pythonRuntimelib.kotlinRuntime
Example:
pkgs.writeText "fun.js" ''
${lib.jsRuntime}
let increment = ${lib.toJS (x: x + 1)};
// Generated functions expect `Value` objects. Wrap plain values using
// `Value.fromInt`, or pass wrapped values produced by `lib.toJS`.
console.log(increment(${lib.toJS (2 + 4)})); // using lib.toJS
console.log(increment(Value.fromInt(6))); // wrapping a literal
// To extract plain JS values from a `Value`, use the asX accessors:
console.log(${lib.toJS (1 == 2)}.asBool());
''Other languages work similarly using lib.pythonRuntime and lib.cppRuntime etc..
For large values, the single-TU output of lib.toCpp value may be too
large for the C++ compiler to handle efficiently. To solve this there is lib.mkCppLibrary
let
cppApp = lib.mkCppLibrary {
namespace = "myapp"; # can be linked with -lmyapp
# produces a myapp.hpp
# all attrs of value are accessible through that namespace
value = {
name = "VeryGoodApp";
version = "1.0.0";
otherCode = largeValue;
};
};
lib-drv = cppApp.mkDerivation pkgs;
in
pkgs.stdenv.mkDerivation {
name = "my-consumer";
nativeBuildInputs = [ lib-drv ];
# …
buildPhase = ''
g++ -std=c++23 main.cpp \
-lmyapp -o main
'';
}The consumer includes the exported value like this:
#include <myapp.hpp>
int main() {
auto r = myapp::name.asString();
auto s = myapp::otherCode(myapp::version);
// ...
}Generating Nix is simpler and does not require a runtime:
# Serializing a function to a Nix string
string = lib.toNix (x: x + 1);- Infinite attribute sets: i have not yet found a way to detect infinitly recursive attr sets. They are translated to a depth of 500. Any runtime access deeper than that will throw. Generating them at runtime should work tho.
- Performance: as allready mentioned compiling large values with gcc causes problems. but translation and runtime are not perfect by any means. running complex things with compiled code quickly creates stack overflows or needs a long time to finish.
- Paths: Paths are completely unsupported. they can be translated but accessing them at runtime throws
- Partial Primop implementation: Not every primop is implemented in every language. especially the ones operating on paths. Unknown primops translate to a runtime throw.
- translated tryEval catches every error. even stack overflows. this is intentional but a difference to the real nix
- direnv provides the nix fork
- Run tests and checks with Nix:
nix flake check. - See
./testsfor current coverage.
Contributions are welcome.
This project is licensed under the GNU Affero General Public License version 3 (AGPL-3.0). See the LICENSE file for the full license text and terms.