Compare commits

...

10 commits

Author SHA1 Message Date
5b80e3aa63
chore: set up clang-format
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ia4928173791618598031d584f7b6cf166a6a6964
2026-02-22 00:07:52 +03:00
4aa514d83d
meta: ignore more generated files and nix artifacts
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Id142ee9a1bdf076542f684130ba925216a6a6964
2026-02-22 00:07:51 +03:00
e36693ac3f
docs: document project architechture
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Iaa99d706d61857fbd51d3b757b5066ab6a6a6964
2026-02-22 00:07:50 +03:00
2539ff7ca3
chore: add CMake test target; ignore build directories
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I0db5e008e65f5a5109d8eaa6119b3c246a6a6964
2026-02-22 00:07:49 +03:00
ddfbc91b58
tests: add regression test suite; update test fixtures
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I5ccf7cb25394bdcae068b49f66787c3a6a6a6964
2026-02-22 00:07:48 +03:00
f4135a5dca
types/serializer: add HasAttrNode binary encoding for ? operator
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ibfb89151eb80ab1ae1d8878b6849d2c96a6a6964
2026-02-22 00:07:47 +03:00
3441853eef
various: fix string comparison, interpolation and ADD op. for strings
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ice1bfb5682ab48a967dc16f1378e23ae6a6a6964
2026-02-22 00:07:47 +03:00
da9be4b014
docs: document binary format; add testing instructions
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I2cb3440f97b4add57860b212a60442336a6a6964
2026-02-22 00:07:46 +03:00
860460c402
chore: update gitignore for build artifacts
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I3fd18bc9e15b0a33557fcd95368b49776a6a6964
2026-02-22 00:07:45 +03:00
49f64c9c98
tests: initial test suite for IR compiler
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I70cd1dfa45add9df58a44add69fbd30a6a6a6964
2026-02-22 00:07:44 +03:00
37 changed files with 639 additions and 26 deletions

18
.clang-format Normal file
View file

@ -0,0 +1,18 @@
---
Language: Cpp
BasedOnStyle: LLVM
IndentWidth: 2
TabWidth: 2
UseTab: Never
ColumnLimit: 100
BreakBeforeBraces: Attach
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
SpaceAfterCStyleCast: true
SpaceBeforeParens: ControlStatements
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
NamespaceIndentation: None
PointerAlignment: Left
ReferenceAlignment: Left

14
.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
build/
nix/
result*
/.direnv
# Build artifacts
nix-irc
*.so
**/*.nixir
regression_test
# Generated files
cmake_install.cmake
Makefile

View file

@ -73,3 +73,22 @@ set_target_properties(nix-ir-plugin PROPERTIES
# Install to plugin directory
install(TARGETS nix-ir-plugin LIBRARY DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/nix/plugins")
# Regression tests
add_executable(regression_test
tests/regression_test.cpp
src/irc/serializer.cpp
)
target_include_directories(regression_test PRIVATE
${CMAKE_SOURCE_DIR}/src
${NIX_STORE_INCLUDE_DIRS}
${NIX_EXPR_INCLUDE_DIRS}
${NIX_UTIL_INCLUDE_DIRS}
)
target_link_libraries(regression_test PRIVATE
${NIX_STORE_LINK_LIBRARIES}
${NIX_EXPR_LINK_LIBRARIES}
${NIX_UTIL_LINK_LIBRARIES}
)

197
README.md
View file

@ -7,8 +7,6 @@ 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.
The plugin automatically chooses the fastest path based on file availability.
## Supported Nix Constructs
- Literals: integers, strings, booleans, null, paths
@ -27,8 +25,160 @@ The plugin automatically chooses the fastest path based on file availability.
`||`, `->`
- 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 _envision_[^1] 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
flowchart TD
subgraph Source["User Source"]
A[".nix Source Files"]
end
subgraph Compiler["External Tool: nix-irc"]
B1["Parse Nix"]
B2["Static Import Resolution"]
B3["Flatten Import Graph"]
B4["Desugar + De Bruijn Conversion"]
B5["Emit Versioned IR Bundle (.nixir)"]
end
subgraph IR["IR Bundle"]
C1["Binary IR Format"]
C2["Versioned Header"]
C3["No Names, Indexed Vars"]
end
subgraph Plugin["nix-ir-plugin.so"]
D1["Primop Registration"]
D2["prim_loadIR"]
D3["prim_compileNix"]
D4["prim_info"]
end
subgraph CompilePath["On-the-fly Path"]
E1["Parse Source String"]
E2["IR Generation"]
end
subgraph LoadPath["Pre-compiled Path"]
F1["Deserialize .nixir"]
end
subgraph VM["Custom Lazy VM"]
G1["Heap-Allocated Thunks"]
G2["Memoization"]
G3["Cycle Detection"]
G4["Closure Environments (Array-Based)"]
G5["FORCE / THUNK Execution"]
end
A --> B1
B1 --> B2
B2 --> B3
B3 --> B4
B4 --> B5
B5 --> C1
C1 --> D1
D2 -->|explicit| F1
F1 --> G1
D3 -->|explicit| E1
E1 --> E2
E2 --> G1
G1 -.-> G2 -.-> G3 -.-> G4 -.-> G5
```
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:
```plaintext
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
```bash
# Configure
$ cmake -B build
# Build
$ make
# The nix-irc executable will be in the project root
$ ./nix-irc --help
```
### Compiling Nix to IR
```bash
@ -42,13 +192,48 @@ $ nix-irc -I ./lib -I /nix/store/... input.nix output.nixir
$ nix-irc --no-imports input.nix output.nixir
```
### Runtime Evaluation (Plugin)
<!--markdownlint-disable MD013-->
```bash
# 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'
```
<!--markdownlint-enable MD013-->
### Running Tests
```bash
# Test all sample files
for f in tests/*.nix; do
./nix-irc "$f" "${f%.nix}.nixir"
done
# Verify IR format
$ hexdump -C tests/simple.nixir | head -3
```
## Contributing
This is a research/experimental project. Contributions welcome!
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:
Areas where help is needed:
- Expanding parser to handle more Nix syntax
- Compiler semantics
- Performance optimization
- Test coverage
- Documentation improvements
- Expanding parser to handle more Nix syntax (module system in particular)
Contributions _are_ welcome!

View file

@ -21,10 +21,6 @@ struct IREnvironment {
explicit IREnvironment(IREnvironment* p = nullptr) : parent(p), with_attrs(nullptr) {}
IREnvironment* push() {
return new IREnvironment(this);
}
void bind(Value* val) {
bindings.push_back(val);
}
@ -104,7 +100,7 @@ struct Evaluator::Impl {
thunk->blackholed = true;
eval_node(thunk->expr, *v, thunk->env);
thunks.erase(it);
thunks.erase(v);
}
void eval_node(const std::shared_ptr<Node>& node, Value& v, IREnvironment* env) {
@ -231,6 +227,8 @@ struct Evaluator::Impl {
case BinaryOp::ADD:
if (left->type() == nInt && right->type() == nInt) {
v.mkInt((left->integer() + right->integer()).valueWrapping());
} else if (left->type() == nString && right->type() == nString) {
v.mkString(std::string(left->c_str()) + std::string(right->c_str()));
} else {
state.error<EvalError>("type error in addition").debugThrow();
}
@ -268,6 +266,8 @@ struct Evaluator::Impl {
case BinaryOp::LT:
if (left->type() == nInt && right->type() == nInt) {
v.mkBool(left->integer() < right->integer());
} else if (left->type() == nString && right->type() == nString) {
v.mkBool(std::string(left->c_str()) < std::string(right->c_str()));
} else {
state.error<EvalError>("type error in comparison").debugThrow();
}
@ -275,6 +275,8 @@ struct Evaluator::Impl {
case BinaryOp::GT:
if (left->type() == nInt && right->type() == nInt) {
v.mkBool(left->integer() > right->integer());
} else if (left->type() == nString && right->type() == nString) {
v.mkBool(std::string(left->c_str()) > std::string(right->c_str()));
} else {
state.error<EvalError>("type error in comparison").debugThrow();
}
@ -282,6 +284,8 @@ struct Evaluator::Impl {
case BinaryOp::LE:
if (left->type() == nInt && right->type() == nInt) {
v.mkBool(left->integer() <= right->integer());
} else if (left->type() == nString && right->type() == nString) {
v.mkBool(std::string(left->c_str()) <= std::string(right->c_str()));
} else {
state.error<EvalError>("type error in comparison").debugThrow();
}
@ -289,16 +293,15 @@ struct Evaluator::Impl {
case BinaryOp::GE:
if (left->type() == nInt && right->type() == nInt) {
v.mkBool(left->integer() >= right->integer());
} else if (left->type() == nString && right->type() == nString) {
v.mkBool(std::string(left->c_str()) >= std::string(right->c_str()));
} else {
state.error<EvalError>("type error in comparison").debugThrow();
}
break;
case BinaryOp::CONCAT:
if (left->type() == nString && right->type() == nString) {
v.mkString(std::string(left->c_str()) + std::string(right->c_str()));
} else {
state.error<EvalError>("type error in concatenation").debugThrow();
}
// ++ is list concatenation in Nix; string concat uses ADD (+)
state.error<EvalError>("list concatenation not yet implemented").debugThrow();
break;
default:
state.error<EvalError>("unknown binary operator").debugThrow();
@ -410,7 +413,9 @@ struct Evaluator::Impl {
auto attr = obj->attrs()->get(sym);
if (attr) {
v = *attr->value;
Value* val = attr->value;
force(val);
v = *val;
} else if (n->default_expr) {
eval_node(*n->default_expr, v, env);
} else {

View file

@ -27,7 +27,10 @@ static std::string read_file(const std::string& path) {
long size = ftell(f);
fseek(f, 0, SEEK_SET);
std::string content(size, '\0');
fread(content.data(), 1, size, f);
if (fread(content.data(), 1, size, f) != static_cast<size_t>(size)) {
fclose(f);
throw std::runtime_error("Failed to read file: " + path);
}
fclose(f);
return content;
}
@ -761,7 +764,11 @@ public:
}
}
expect(Token::RBRACE);
if (!consume(Token::RBRACE)) {
// Not a lambda pattern, restore
pos = saved_pos;
return nullptr;
}
if (!consume(Token::COLON)) {
// Not a lambda, restore
@ -817,6 +824,7 @@ public:
// Create lambda
auto lambda = LambdaNode(1, let_node);
lambda.param_name = arg_name;
lambda.strict_pattern = !has_ellipsis;
return std::make_shared<Node>(std::move(lambda));
}
@ -842,13 +850,33 @@ public:
i += 2; // Skip ${
int depth = 1;
size_t expr_start = i;
bool in_string = false;
char string_quote = 0;
while (i < raw.size() && depth > 0) {
if (raw[i] == '{') depth++;
else if (raw[i] == '}') depth--;
if (!in_string) {
if (raw[i] == '"' || raw[i] == '\'') {
in_string = true;
string_quote = raw[i];
} else if (raw[i] == '{') {
depth++;
} else if (raw[i] == '}') {
depth--;
}
} else {
if (raw[i] == string_quote && (i == 0 || raw[i-1] != '\\')) {
in_string = false;
} else if (raw[i] == '\\') {
i++;
}
}
if (depth > 0) i++;
}
if (depth > 0) {
throw std::runtime_error("unterminated ${ in string interpolation");
}
// Parse the expression
std::string expr_str = raw.substr(expr_start, i - expr_start);
@ -893,7 +921,8 @@ public:
auto result = parts[0];
for (size_t j = 1; j < parts.size(); j++) {
result = std::make_shared<Node>(BinaryOpNode(BinaryOp::CONCAT, result, parts[j]));
// Use ADD (+) for string concatenation; CONCAT (++) is Nix list concatenation
result = std::make_shared<Node>(BinaryOpNode(BinaryOp::ADD, result, parts[j]));
}
return result;

View file

@ -271,7 +271,14 @@ struct Deserializer::Impl {
case NodeType::SELECT: {
auto expr = read_node();
auto attr = read_node();
return std::make_shared<Node>(SelectNode(expr, attr, line));
uint8_t has_default = read_u8();
std::optional<std::shared_ptr<Node>> default_expr;
if (has_default) {
default_expr = read_node();
}
SelectNode select_node(expr, attr, line);
select_node.default_expr = default_expr;
return std::make_shared<Node>(std::move(select_node));
}
case NodeType::HAS_ATTR: {
auto expr = read_node();

View file

@ -14,7 +14,7 @@
namespace nix_irc {
constexpr uint32_t IR_MAGIC = 0x4E495258;
constexpr uint32_t IR_VERSION = 1;
constexpr uint32_t IR_VERSION = 2;
enum class NodeType : uint8_t {
CONST_INT = 0x01,
@ -29,8 +29,8 @@ enum class NodeType : uint8_t {
UNARY_OP = 0x23,
ATTRSET = 0x30,
SELECT = 0x31,
HAS_ATTR = 0x32,
WITH = 0x33,
HAS_ATTR = 0x34,
WITH = 0x32,
IF = 0x40,
LET = 0x50,
LETREC = 0x51,
@ -94,6 +94,7 @@ struct LambdaNode {
uint32_t arity = 1;
std::shared_ptr<Node> body;
std::optional<std::string> param_name;
bool strict_pattern = true;
uint32_t line = 0;
LambdaNode(uint32_t a, std::shared_ptr<Node> b, uint32_t l = 0);
};

8
tests/attrset.nix Normal file
View file

@ -0,0 +1,8 @@
# Attrset test
{
name = "test";
value = 123;
nested = {
inner = true;
};
}

BIN
tests/attrset.nixir Normal file

Binary file not shown.

4
tests/attrset_var.nix Normal file
View file

@ -0,0 +1,4 @@
let
x = 10;
in
{ a = x; }

6
tests/comparison.nix Normal file
View file

@ -0,0 +1,6 @@
# Test comparison operators
let
a = 10;
b = 20;
in
if a < b then true else false

BIN
tests/comparison.nixir Normal file

Binary file not shown.

2
tests/if.nix Normal file
View file

@ -0,0 +1,2 @@
# Conditional test
if true then 1 else 2

BIN
tests/if.nixir Normal file

Binary file not shown.

17
tests/inherit.nix Normal file
View file

@ -0,0 +1,17 @@
# Test inherit keyword
let
x = 10;
y = 20;
attrs = { a = 1; b = 2; c = 3; };
in
{
# Basic inherit from outer scope
inherit x y;
# Inherit from expression
inherit (attrs) a b;
# Mixed
z = 30;
inherit (attrs) c;
}

4
tests/inherit_from.nix Normal file
View file

@ -0,0 +1,4 @@
let
attrs = { a = 1; };
in
{ inherit (attrs) a; }

4
tests/inherit_simple.nix Normal file
View file

@ -0,0 +1,4 @@
let
x = 10;
in
{ inherit x; }

36
tests/lambda_pattern.nix Normal file
View file

@ -0,0 +1,36 @@
# Test lambda patterns
let
# Basic destructuring
f1 = { a, b }: a + b;
# With default values
f2 = { a, b ? 10 }: a + b;
# With ellipsis (extra fields allowed)
f3 = { a, ... }: a * 2;
# Named pattern with ellipsis to allow extra fields
f4 = arg@{ a, b, ... }: a + b + arg.c;
# Simple lambda (not a pattern)
f5 = x: x + 1;
in
{
# Test basic destructuring
test1 = f1 { a = 3; b = 4; };
# Test with defaults (provide both)
test2a = f2 { a = 5; b = 6; };
# Test with defaults (use default for b)
test2b = f2 { a = 5; };
# Test ellipsis (extra field ignored)
test3 = f3 { a = 7; extra = 999; };
# Test named pattern
test4 = f4 { a = 1; b = 2; c = 3; };
# Test simple lambda
test5 = f5 10;
}

5
tests/let.nix Normal file
View file

@ -0,0 +1,5 @@
# Let binding test
let
x = 10;
y = 20;
in x

BIN
tests/let.nixir Normal file

Binary file not shown.

6
tests/logical.nix Normal file
View file

@ -0,0 +1,6 @@
# Test logical operators
let
x = true;
y = false;
in
if x && y then 1 else if x || y then 2 else 3

BIN
tests/logical.nixir Normal file

Binary file not shown.

6
tests/operators.nix Normal file
View file

@ -0,0 +1,6 @@
# Test arithmetic operators
let
x = 10;
y = 5;
in
(x + y) * 2

BIN
tests/operators.nixir Normal file

Binary file not shown.

8
tests/precedence.nix Normal file
View file

@ -0,0 +1,8 @@
# Test operator precedence
let
a = 1 + 2 * 3; # Should be 1 + (2 * 3) = 7
b = 10 - 5 - 2; # Should be (10 - 5) - 2 = 3
c = true && false || true; # Should be (true && false) || true = true
d = 1 < 2 && 3 > 2; # Should be (1 < 2) && (3 > 2) = true
in
{ a = a; b = b; c = c; d = d; }

BIN
tests/precedence.nixir Normal file

Binary file not shown.

184
tests/regression_test.cpp Normal file
View file

@ -0,0 +1,184 @@
#include "irc/serializer.h"
#include "irc/types.h"
#include <cassert>
#include <iostream>
using namespace nix_irc;
int failures = 0;
#define TEST_CHECK(cond, msg) \
do { \
if (!(cond)) { \
std::cerr << " FAIL: " << msg << std::endl; \
failures++; \
} else { \
std::cout << " PASS: " << msg << std::endl; \
} \
} while (0)
#define TEST_PASS(msg) std::cout << " PASS: " << msg << std::endl
#define TEST_FAIL(msg) \
do { \
std::cerr << " FAIL: " << msg << std::endl; \
failures++; \
} while (0)
void test_enum_compatibility() {
std::cout << "> Enum compatibility..." << std::endl;
if (static_cast<uint8_t>(NodeType::WITH) == 0x32) {
std::cout << " PASS: WITH has correct value 0x32" << std::endl;
} else {
std::cerr << " FAIL: WITH should be 0x32, got "
<< static_cast<uint8_t>(NodeType::WITH) << std::endl;
}
if (static_cast<uint8_t>(NodeType::HAS_ATTR) == 0x34) {
std::cout << " PASS: HAS_ATTR has value 0x34 (new slot after WITH bump)"
<< std::endl;
} else if (static_cast<uint8_t>(NodeType::HAS_ATTR) == 0x33 &&
static_cast<uint8_t>(NodeType::WITH) == 0x32) {
std::cout << " PASS: HAS_ATTR has value 0x33 (restored original with WITH "
"at 0x32)"
<< std::endl;
} else {
std::cerr << " FAIL: HAS_ATTR value is "
<< static_cast<uint8_t>(NodeType::HAS_ATTR)
<< " (expected 0x34 or 0x33 with WITH=0x32)" << std::endl;
}
if (IR_VERSION == 2) {
std::cout << " PASS: IR_VERSION bumped to 2 for breaking change"
<< std::endl;
} else if (static_cast<uint8_t>(NodeType::WITH) == 0x32) {
std::cout << " PASS: IR_VERSION unchanged but WITH restored to 0x32"
<< std::endl;
} else {
std::cerr << " FAIL: Either bump IR_VERSION or fix enum values"
<< std::endl;
}
}
void test_serializer_select_with_default() {
std::cout << "> SELECT serialization with default_expr..." << std::endl;
auto expr = std::make_shared<Node>(ConstIntNode(42));
auto attr = std::make_shared<Node>(ConstStringNode("key"));
auto default_val = std::make_shared<Node>(ConstIntNode(100));
SelectNode select_node(expr, attr);
select_node.default_expr = default_val;
auto select = std::make_shared<Node>(select_node);
IRModule module;
module.entry = select;
Serializer ser;
auto bytes = ser.serialize_to_bytes(module);
Deserializer deser;
auto loaded = deser.deserialize(bytes);
auto *loaded_select = loaded.entry->get_if<SelectNode>();
if (loaded_select && loaded_select->default_expr &&
*loaded_select->default_expr) {
auto *def_val = (*loaded_select->default_expr)->get_if<ConstIntNode>();
if (def_val && def_val->value == 100) {
std::cout << " PASS: SELECT with default_expr round-trips correctly"
<< std::endl;
} else {
std::cerr << " FAIL: default_expr value incorrect" << std::endl;
}
} else {
std::cerr << " FAIL: default_expr not deserialized (missing u8 flag read)"
<< std::endl;
}
}
void test_serializer_select_without_default() {
std::cout << "> SELECT serialization without default_expr..." << std::endl;
auto expr = std::make_shared<Node>(ConstIntNode(42));
auto attr = std::make_shared<Node>(ConstStringNode("key"));
SelectNode select_node(expr, attr);
auto select = std::make_shared<Node>(select_node);
IRModule module;
module.entry = select;
Serializer ser;
auto bytes = ser.serialize_to_bytes(module);
Deserializer deser;
auto loaded = deser.deserialize(bytes);
auto *loaded_select = loaded.entry->get_if<SelectNode>();
if (loaded_select &&
(!loaded_select->default_expr || !*loaded_select->default_expr)) {
std::cout << " PASS: SELECT without default_expr round-trips correctly"
<< std::endl;
} else {
std::cerr << " FAIL: default_expr should be null/absent" << std::endl;
}
}
void test_parser_brace_depth_in_strings() {
std::cout << "> Parser brace depth handling in strings..." << std::endl;
std::string test_input = R"(
let s = "test}"; in ${s}
)";
std::cout << " Test input contains '}' inside string - should not end "
"interpolation"
<< std::endl;
std::cout << " NOTE: This test requires running through actual parser"
<< std::endl;
}
void test_parser_has_ellipsis_usage() {
std::cout << "> Parser has_ellipsis usage..." << std::endl;
std::cout << " NOTE: LambdaNode should have strict_pattern field when "
"has_ellipsis is false"
<< std::endl;
std::cout << " This requires checking the parser output for strict patterns"
<< std::endl;
}
void test_parser_expect_in_speculative_parsing() {
std::cout << "> Parser expect() in speculative parsing..." << std::endl;
std::cout << " NOTE: try_parse_lambda should not throw on non-lambda input"
<< std::endl;
std::cout << " This requires testing parser with invalid lambda patterns"
<< std::endl;
}
int main() {
std::cout << "=== Regression Tests for Nixir ===" << std::endl << std::endl;
test_enum_compatibility();
std::cout << std::endl;
test_serializer_select_with_default();
std::cout << std::endl;
test_serializer_select_without_default();
std::cout << std::endl;
test_parser_brace_depth_in_strings();
std::cout << std::endl;
test_parser_has_ellipsis_usage();
std::cout << std::endl;
test_parser_expect_in_speculative_parsing();
std::cout << std::endl;
std::cout << "=== Tests Complete ===" << std::endl;
std::cout << "Failures: " << failures << std::endl;
return failures > 0 ? 1 : 0;
}

11
tests/shortcircuit.nix Normal file
View file

@ -0,0 +1,11 @@
# Test short-circuit evaluation
let
alwaysFalse = false;
alwaysTrue = true;
x = 10;
in
{
and_false = alwaysFalse && alwaysTrue;
or_true = alwaysTrue || alwaysFalse;
impl_false = alwaysFalse -> alwaysFalse;
}

6
tests/shortcircuit2.nix Normal file
View file

@ -0,0 +1,6 @@
# Test short-circuit evaluation
{
and_false = false && true;
or_true = true || false;
impl_false = false -> false;
}

2
tests/simple.nix Normal file
View file

@ -0,0 +1,2 @@
# Simple constant test
42

BIN
tests/simple.nixir Normal file

Binary file not shown.

1
tests/simple_op.nix Normal file
View file

@ -0,0 +1 @@
1 + 2

BIN
tests/simple_op.nixir Normal file

Binary file not shown.

19
tests/string_interp.nix Normal file
View file

@ -0,0 +1,19 @@
# Test string interpolation
let
name = "world";
x = 42;
bool_val = true;
in
{
# Simple interpolation
greeting = "Hello ${name}!";
# Multiple interpolations
multi = "x is ${x} and name is ${name}";
# Nested expression
nested = "Result: ${if bool_val then "yes" else "no"}";
# Just a string (no interpolation)
plain = "plain text";
}

6
tests/unary.nix Normal file
View file

@ -0,0 +1,6 @@
# Test unary operators
let
x = 10;
y = true;
in
{ neg = -x; not = !y; }

BIN
tests/unary.nixir Normal file

Binary file not shown.