brewery

nixir

public

Import-resolving Nix IR plugin

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/nixir.git

Recent commits

log
  1. irc/evaluator: speed it up

    Signed-off-by: NotAShelf <raf@notashelf.dev>

    NotAShelf2w ago
    unverifiedbfbcef9c284c
  2. tests: cover flake refs and lexer/parser regressions

    Signed-off-by: NotAShelf <raf@notashelf.dev>

    NotAShelf2mo ago
    unverified531855d91ac4
  3. irc/parser: fix lexer ownership, errors, and implication parsing

    Signed-off-by: NotAShelf <raf@notashelf.dev>

    NotAShelf2mo ago
    unverifiedb319ef6f3f9f

Files

12 entries
  1. .clang-format451 B5mo ago
  2. .clang-tidy241 B5mo ago
  3. .envrc10 B5mo ago
  4. .gitignore136 B5mo ago
  5. CMakeLists.txt2765 B2mo ago
  6. README.md7681 B2mo ago
  7. docs 2mo ago
  8. flake.lock567 B5mo ago
  9. flake.nix1408 B2mo ago
  10. justfile2513 B2mo ago
  11. src 2w ago
  12. tests 2mo ago
README

Nixir - Import-Resolving IR Plugin

Nixir, for the lack of a more imaginative name, is a Nix plugin with a fancy hybrid compilation architecture for optimized evaluation. We provide two complementary paths for Nix evaluator. It is either On-the-fly compilation where the plugin parses and compiles Nix code at runtime, or; ahead-of-time compilation where the nix-irc tool pre-compiles .nix files into .nixir files.

Supported Nix Constructs

  • Literals: integers, strings, booleans, null, paths
  • Attrsets: { name: value; }
  • Recursive attrsets: rec { ... }
  • Let bindings: let x: 1; in x
  • Recursion let: let rec x: y; y: x; in x
  • Conditionals: if cond then a else b
  • Lambdas: (basic support, patterns coming in Phase 5)
  • Applications: function calls
  • Selections: attrset.attribute
  • Assertions: assert condition; expr
  • With expressions: with attrs; expr
  • Operators:
    • Binary: +, -, *, /, ++, ==, !=, <, >, <=, >=, &&, ||, ->
    • Unary: -, !

Overview

Nixir is a Nix evaluator plugin that compiles Nix expressions to a custom binary intermediate representation (IR). Think of it like a compiler for Nix: it translates human-readable Nix code into a compact, fast-to-execute format that runs in a custom virtual machine.

The plugin works in two ways:

  1. Ahead-of-time: Use the nix-irc tool to compile .nix files to .nixir once, then load them instantly
  2. On-the-fly: Let the plugin parse and compile Nix code at runtime when you need it

While Nixir is designed as a toy research project, I envision1 a few potential use cases built around working with Nix. Sure, you probably would not go work with Nix willingly, science is not about why, it is about why not.

Some potential use cases for Nixir might include:

  • CI/CD Acceleration: Pre-compile stable Nix expressions to .nixir for faster repeated evaluation in CI pipelines
  • Embedded Nix: Use Nix as a configuration language in C++ applications without bundling the full evaluator
  • Plugin Ecosystem: Extend Nix with custom evaluation strategies via the plugin API
  • Build Caching: Cache compiled IR alongside source for instant startup of Nix-based tools
1

I’m not entirely convinced either, do not ask.

Architecture

Mermaid diagram

The same compiler code runs both in the standalone nix-irc CLI tool and inside the plugin for on-the-fly compilation. This ensures consistent behavior between pre-compiled and runtime-compiled paths. The intermediate representation (IR) design uses De Brujin indices instead of names for variable binding, which eliminates string lookup and the binary format uses a versioned header (0x4E495258). In addition, we make use of string interning for repeated identifiers and type-tagged nodes for efficient dispatching.

The runtime implements lazy evaluation using heap-allocated thunks. Each thunk holds a delayed computation and is evaluated at most once through memoization. Recursive definitions are handled through a blackhole mechanism that detects cycles at runtime. Variable lookup uses array-based closure environments, providing O(1) access by index rather than name-based lookup.

The plugin integrates with Nix through the RegisterPrimOp API, exposing three operations: nixIR_loadIR for loading pre-compiled .nixir bundles, nixIR_compile for on-the-fly compilation, and nixIR_info for metadata. This integration path is compatible with Nix 2.32+.

IR Format

The .nixir files use a versioned binary format:

Header:
  - Magic: 0x4E495258 ("NIRX")
  - Version: 1 (uint32)
  - Source count: uint32
  - Import count: uint32
  - String table size: uint32

String Table:
  - Interned strings for efficient storage

Nodes:
  - Binary encoding of IR nodes
  - Each node has type tag + inline data

Entry:
  - Main expression node index

Usage

Building

# Using just (recommended)
$ just build

# Or manually with CMake
$ cmake -B build -G Ninja
$ cmake --build build

# The nix-irc executable will be in build/
$ ./build/nix-irc --help

Available Tasks

Run just to see all available tasks:

  • just build - Build all targets
  • just test - Run all tests (unit, compile, integration)
  • just bench - Run performance benchmarks
  • just clean - Clean build artifacts
  • just smoke - Run quick smoke test
  • just stats - Show project statistics

See just --list for the complete list of available commands.

Compiling Nix to IR

# Basic compilation
$ ./build/nix-irc input.nix output.nixir

# With import search paths
$ ./build/nix-irc -I ./lib -I /nix/store/... input.nix output.nixir

# Disable import resolution
$ ./build/nix-irc --no-imports input.nix output.nixir

# Using just
$ just compile input.nix output.nixir

Runtime Evaluation (Plugin)

# Load the plugin and evaluate IR
$ nix --plugin-files ./nix-ir-plugin.so eval --expr 'builtins.nixIR_loadIR "output.nixir"'

# On-the-fly compilation and evaluation
$ nix --plugin-files ./nix-ir-plugin.so eval --expr 'builtins.nixIR_compile "1 + 2 * 3"'

# Get plugin info
$ nix --plugin-files ./nix-ir-plugin.so eval --expr 'builtins.nixIR_info'

Running Tests

# Run all tests
$ just test

# Run specific test suites
$ just test-unit        # Unit tests only
$ just test-compile     # Compilation tests only
$ just test-integration # Integration tests only

# Manually test all fixtures
$ for f in tests/fixtures/*.nix; do
    ./build/nix-irc "$f" "${f%.nix}.nixir"
done

# Verify IR format
$ hexdump -C tests/fixtures/simple.nixir | head -3

Contributing

This is a research project (with no formal association, i.e., no thesis or anything) that I’m working on entirely for fun and out of curiousity. Extremely experimental, could change any time. While I do not suggest running this project in a serious context, I am happy to receive any kind of feedback you might have. You will notice very quickly that I’m a little out of my depth, and the code is in a rough shape. Areas where help is needed:

  • Compiler semantics
  • Performance optimization
  • Test coverage
  • Documentation improvements
  • Expanding parser to handle more Nix syntax (module system in particular)

Contributions are welcome!