Compare commits
No commits in common. "5b80e3aa63c68867d85647d8cf4428395ea5b045" and "22c92bfdd581d4e0d49461b84a6e9348adeed2a6" have entirely different histories.
5b80e3aa63
...
22c92bfdd5
37 changed files with 26 additions and 639 deletions
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
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
14
.gitignore
vendored
|
|
@ -1,14 +0,0 @@
|
|||
build/
|
||||
nix/
|
||||
result*
|
||||
/.direnv
|
||||
|
||||
# Build artifacts
|
||||
nix-irc
|
||||
*.so
|
||||
**/*.nixir
|
||||
regression_test
|
||||
|
||||
# Generated files
|
||||
cmake_install.cmake
|
||||
Makefile
|
||||
|
|
@ -73,22 +73,3 @@ 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
197
README.md
|
|
@ -7,6 +7,8 @@ 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
|
||||
|
|
@ -25,160 +27,8 @@ files.
|
|||
`||`, `->`
|
||||
- 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
|
||||
|
|
@ -192,48 +42,13 @@ $ 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 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:
|
||||
This is a research/experimental project. Contributions welcome!
|
||||
|
||||
- Compiler semantics
|
||||
Areas where help is needed:
|
||||
|
||||
- Expanding parser to handle more Nix syntax
|
||||
- Performance optimization
|
||||
- Test coverage
|
||||
- Documentation improvements
|
||||
- Expanding parser to handle more Nix syntax (module system in particular)
|
||||
|
||||
Contributions _are_ welcome!
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ 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);
|
||||
}
|
||||
|
|
@ -100,7 +104,7 @@ struct Evaluator::Impl {
|
|||
|
||||
thunk->blackholed = true;
|
||||
eval_node(thunk->expr, *v, thunk->env);
|
||||
thunks.erase(v);
|
||||
thunks.erase(it);
|
||||
}
|
||||
|
||||
void eval_node(const std::shared_ptr<Node>& node, Value& v, IREnvironment* env) {
|
||||
|
|
@ -227,8 +231,6 @@ 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();
|
||||
}
|
||||
|
|
@ -266,8 +268,6 @@ 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,8 +275,6 @@ 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();
|
||||
}
|
||||
|
|
@ -284,8 +282,6 @@ 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();
|
||||
}
|
||||
|
|
@ -293,15 +289,16 @@ 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:
|
||||
// ++ is list concatenation in Nix; string concat uses ADD (+)
|
||||
state.error<EvalError>("list concatenation not yet implemented").debugThrow();
|
||||
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();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
state.error<EvalError>("unknown binary operator").debugThrow();
|
||||
|
|
@ -413,9 +410,7 @@ struct Evaluator::Impl {
|
|||
auto attr = obj->attrs()->get(sym);
|
||||
|
||||
if (attr) {
|
||||
Value* val = attr->value;
|
||||
force(val);
|
||||
v = *val;
|
||||
v = *attr->value;
|
||||
} else if (n->default_expr) {
|
||||
eval_node(*n->default_expr, v, env);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -27,10 +27,7 @@ static std::string read_file(const std::string& path) {
|
|||
long size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
std::string content(size, '\0');
|
||||
if (fread(content.data(), 1, size, f) != static_cast<size_t>(size)) {
|
||||
fclose(f);
|
||||
throw std::runtime_error("Failed to read file: " + path);
|
||||
}
|
||||
fread(content.data(), 1, size, f);
|
||||
fclose(f);
|
||||
return content;
|
||||
}
|
||||
|
|
@ -764,11 +761,7 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
if (!consume(Token::RBRACE)) {
|
||||
// Not a lambda pattern, restore
|
||||
pos = saved_pos;
|
||||
return nullptr;
|
||||
}
|
||||
expect(Token::RBRACE);
|
||||
|
||||
if (!consume(Token::COLON)) {
|
||||
// Not a lambda, restore
|
||||
|
|
@ -824,7 +817,6 @@ 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));
|
||||
}
|
||||
|
||||
|
|
@ -850,33 +842,13 @@ 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 (!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 (raw[i] == '{') depth++;
|
||||
else if (raw[i] == '}') depth--;
|
||||
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);
|
||||
|
||||
|
|
@ -921,8 +893,7 @@ public:
|
|||
|
||||
auto result = parts[0];
|
||||
for (size_t j = 1; j < parts.size(); j++) {
|
||||
// Use ADD (+) for string concatenation; CONCAT (++) is Nix list concatenation
|
||||
result = std::make_shared<Node>(BinaryOpNode(BinaryOp::ADD, result, parts[j]));
|
||||
result = std::make_shared<Node>(BinaryOpNode(BinaryOp::CONCAT, result, parts[j]));
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -271,14 +271,7 @@ struct Deserializer::Impl {
|
|||
case NodeType::SELECT: {
|
||||
auto expr = read_node();
|
||||
auto attr = read_node();
|
||||
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));
|
||||
return std::make_shared<Node>(SelectNode(expr, attr, line));
|
||||
}
|
||||
case NodeType::HAS_ATTR: {
|
||||
auto expr = read_node();
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
namespace nix_irc {
|
||||
|
||||
constexpr uint32_t IR_MAGIC = 0x4E495258;
|
||||
constexpr uint32_t IR_VERSION = 2;
|
||||
constexpr uint32_t IR_VERSION = 1;
|
||||
|
||||
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 = 0x34,
|
||||
WITH = 0x32,
|
||||
HAS_ATTR = 0x32,
|
||||
WITH = 0x33,
|
||||
IF = 0x40,
|
||||
LET = 0x50,
|
||||
LETREC = 0x51,
|
||||
|
|
@ -94,7 +94,6 @@ 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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
# Attrset test
|
||||
{
|
||||
name = "test";
|
||||
value = 123;
|
||||
nested = {
|
||||
inner = true;
|
||||
};
|
||||
}
|
||||
Binary file not shown.
|
|
@ -1,4 +0,0 @@
|
|||
let
|
||||
x = 10;
|
||||
in
|
||||
{ a = x; }
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# Test comparison operators
|
||||
let
|
||||
a = 10;
|
||||
b = 20;
|
||||
in
|
||||
if a < b then true else false
|
||||
Binary file not shown.
|
|
@ -1,2 +0,0 @@
|
|||
# Conditional test
|
||||
if true then 1 else 2
|
||||
BIN
tests/if.nixir
BIN
tests/if.nixir
Binary file not shown.
|
|
@ -1,17 +0,0 @@
|
|||
# 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;
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
let
|
||||
attrs = { a = 1; };
|
||||
in
|
||||
{ inherit (attrs) a; }
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
let
|
||||
x = 10;
|
||||
in
|
||||
{ inherit x; }
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# 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;
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Let binding test
|
||||
let
|
||||
x = 10;
|
||||
y = 20;
|
||||
in x
|
||||
BIN
tests/let.nixir
BIN
tests/let.nixir
Binary file not shown.
|
|
@ -1,6 +0,0 @@
|
|||
# Test logical operators
|
||||
let
|
||||
x = true;
|
||||
y = false;
|
||||
in
|
||||
if x && y then 1 else if x || y then 2 else 3
|
||||
Binary file not shown.
|
|
@ -1,6 +0,0 @@
|
|||
# Test arithmetic operators
|
||||
let
|
||||
x = 10;
|
||||
y = 5;
|
||||
in
|
||||
(x + y) * 2
|
||||
Binary file not shown.
|
|
@ -1,8 +0,0 @@
|
|||
# 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; }
|
||||
Binary file not shown.
|
|
@ -1,184 +0,0 @@
|
|||
#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;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# Test short-circuit evaluation
|
||||
let
|
||||
alwaysFalse = false;
|
||||
alwaysTrue = true;
|
||||
x = 10;
|
||||
in
|
||||
{
|
||||
and_false = alwaysFalse && alwaysTrue;
|
||||
or_true = alwaysTrue || alwaysFalse;
|
||||
impl_false = alwaysFalse -> alwaysFalse;
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# Test short-circuit evaluation
|
||||
{
|
||||
and_false = false && true;
|
||||
or_true = true || false;
|
||||
impl_false = false -> false;
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
# Simple constant test
|
||||
42
|
||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
1 + 2
|
||||
Binary file not shown.
|
|
@ -1,19 +0,0 @@
|
|||
# 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";
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# Test unary operators
|
||||
let
|
||||
x = 10;
|
||||
y = true;
|
||||
in
|
||||
{ neg = -x; not = !y; }
|
||||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue