diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..13d0652 --- /dev/null +++ b/.clang-format @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ddb05bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +build/ +nix/ +result* +/.direnv + +# Build artifacts +nix-irc +*.so +**/*.nixir +regression_test + +# Generated files +cmake_install.cmake +Makefile diff --git a/CMakeLists.txt b/CMakeLists.txt index c310f42..edb503c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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} +) diff --git a/README.md b/README.md index 25cf524..1048d9c 100644 --- a/README.md +++ b/README.md @@ -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) + + + +```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' +``` + + + +### 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! diff --git a/src/irc/evaluator.cpp b/src/irc/evaluator.cpp index 8aa64c7..977ea85 100644 --- a/src/irc/evaluator.cpp +++ b/src/irc/evaluator.cpp @@ -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, 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("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("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("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("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("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("type error in concatenation").debugThrow(); - } + // ++ is list concatenation in Nix; string concat uses ADD (+) + state.error("list concatenation not yet implemented").debugThrow(); break; default: state.error("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 { diff --git a/src/irc/parser.cpp b/src/irc/parser.cpp index 3ec6f86..8a47e2a 100644 --- a/src/irc/parser.cpp +++ b/src/irc/parser.cpp @@ -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)) { + 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(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(BinaryOpNode(BinaryOp::CONCAT, result, parts[j])); + // Use ADD (+) for string concatenation; CONCAT (++) is Nix list concatenation + result = std::make_shared(BinaryOpNode(BinaryOp::ADD, result, parts[j])); } return result; diff --git a/src/irc/serializer.cpp b/src/irc/serializer.cpp index 7a83e55..8819789 100644 --- a/src/irc/serializer.cpp +++ b/src/irc/serializer.cpp @@ -271,7 +271,14 @@ struct Deserializer::Impl { case NodeType::SELECT: { auto expr = read_node(); auto attr = read_node(); - return std::make_shared(SelectNode(expr, attr, line)); + uint8_t has_default = read_u8(); + std::optional> 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(std::move(select_node)); } case NodeType::HAS_ATTR: { auto expr = read_node(); diff --git a/src/irc/types.h b/src/irc/types.h index 50f1cb9..d10acf1 100644 --- a/src/irc/types.h +++ b/src/irc/types.h @@ -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 body; std::optional param_name; + bool strict_pattern = true; uint32_t line = 0; LambdaNode(uint32_t a, std::shared_ptr b, uint32_t l = 0); }; diff --git a/tests/attrset.nix b/tests/attrset.nix new file mode 100644 index 0000000..18425c5 --- /dev/null +++ b/tests/attrset.nix @@ -0,0 +1,8 @@ +# Attrset test +{ + name = "test"; + value = 123; + nested = { + inner = true; + }; +} diff --git a/tests/attrset.nixir b/tests/attrset.nixir new file mode 100644 index 0000000..708f5dd Binary files /dev/null and b/tests/attrset.nixir differ diff --git a/tests/attrset_var.nix b/tests/attrset_var.nix new file mode 100644 index 0000000..11e1da6 --- /dev/null +++ b/tests/attrset_var.nix @@ -0,0 +1,4 @@ +let + x = 10; +in + { a = x; } diff --git a/tests/comparison.nix b/tests/comparison.nix new file mode 100644 index 0000000..be42016 --- /dev/null +++ b/tests/comparison.nix @@ -0,0 +1,6 @@ +# Test comparison operators +let + a = 10; + b = 20; +in + if a < b then true else false diff --git a/tests/comparison.nixir b/tests/comparison.nixir new file mode 100644 index 0000000..fb7b4fd Binary files /dev/null and b/tests/comparison.nixir differ diff --git a/tests/if.nix b/tests/if.nix new file mode 100644 index 0000000..0ef94a5 --- /dev/null +++ b/tests/if.nix @@ -0,0 +1,2 @@ +# Conditional test +if true then 1 else 2 diff --git a/tests/if.nixir b/tests/if.nixir new file mode 100644 index 0000000..4ee0f59 Binary files /dev/null and b/tests/if.nixir differ diff --git a/tests/inherit.nix b/tests/inherit.nix new file mode 100644 index 0000000..470cccc --- /dev/null +++ b/tests/inherit.nix @@ -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; + } diff --git a/tests/inherit_from.nix b/tests/inherit_from.nix new file mode 100644 index 0000000..e2d3797 --- /dev/null +++ b/tests/inherit_from.nix @@ -0,0 +1,4 @@ +let + attrs = { a = 1; }; +in + { inherit (attrs) a; } diff --git a/tests/inherit_simple.nix b/tests/inherit_simple.nix new file mode 100644 index 0000000..6c004ca --- /dev/null +++ b/tests/inherit_simple.nix @@ -0,0 +1,4 @@ +let + x = 10; +in + { inherit x; } diff --git a/tests/lambda_pattern.nix b/tests/lambda_pattern.nix new file mode 100644 index 0000000..dfbef9b --- /dev/null +++ b/tests/lambda_pattern.nix @@ -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; + } diff --git a/tests/let.nix b/tests/let.nix new file mode 100644 index 0000000..d9c706b --- /dev/null +++ b/tests/let.nix @@ -0,0 +1,5 @@ +# Let binding test +let + x = 10; + y = 20; +in x diff --git a/tests/let.nixir b/tests/let.nixir new file mode 100644 index 0000000..cb9dd41 Binary files /dev/null and b/tests/let.nixir differ diff --git a/tests/logical.nix b/tests/logical.nix new file mode 100644 index 0000000..e2e2dac --- /dev/null +++ b/tests/logical.nix @@ -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 diff --git a/tests/logical.nixir b/tests/logical.nixir new file mode 100644 index 0000000..010a5f5 Binary files /dev/null and b/tests/logical.nixir differ diff --git a/tests/operators.nix b/tests/operators.nix new file mode 100644 index 0000000..c9e383b --- /dev/null +++ b/tests/operators.nix @@ -0,0 +1,6 @@ +# Test arithmetic operators +let + x = 10; + y = 5; +in + (x + y) * 2 diff --git a/tests/operators.nixir b/tests/operators.nixir new file mode 100644 index 0000000..f71f899 Binary files /dev/null and b/tests/operators.nixir differ diff --git a/tests/precedence.nix b/tests/precedence.nix new file mode 100644 index 0000000..2949308 --- /dev/null +++ b/tests/precedence.nix @@ -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; } diff --git a/tests/precedence.nixir b/tests/precedence.nixir new file mode 100644 index 0000000..de1b0d4 Binary files /dev/null and b/tests/precedence.nixir differ diff --git a/tests/regression_test.cpp b/tests/regression_test.cpp new file mode 100644 index 0000000..10123a0 --- /dev/null +++ b/tests/regression_test.cpp @@ -0,0 +1,184 @@ +#include "irc/serializer.h" +#include "irc/types.h" +#include +#include + +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(NodeType::WITH) == 0x32) { + std::cout << " PASS: WITH has correct value 0x32" << std::endl; + } else { + std::cerr << " FAIL: WITH should be 0x32, got " + << static_cast(NodeType::WITH) << std::endl; + } + + if (static_cast(NodeType::HAS_ATTR) == 0x34) { + std::cout << " PASS: HAS_ATTR has value 0x34 (new slot after WITH bump)" + << std::endl; + } else if (static_cast(NodeType::HAS_ATTR) == 0x33 && + static_cast(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(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(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(ConstIntNode(42)); + auto attr = std::make_shared(ConstStringNode("key")); + auto default_val = std::make_shared(ConstIntNode(100)); + + SelectNode select_node(expr, attr); + select_node.default_expr = default_val; + auto select = std::make_shared(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(); + if (loaded_select && loaded_select->default_expr && + *loaded_select->default_expr) { + auto *def_val = (*loaded_select->default_expr)->get_if(); + 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(ConstIntNode(42)); + auto attr = std::make_shared(ConstStringNode("key")); + + SelectNode select_node(expr, attr); + auto select = std::make_shared(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(); + 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; +} diff --git a/tests/shortcircuit.nix b/tests/shortcircuit.nix new file mode 100644 index 0000000..063bf70 --- /dev/null +++ b/tests/shortcircuit.nix @@ -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; + } diff --git a/tests/shortcircuit2.nix b/tests/shortcircuit2.nix new file mode 100644 index 0000000..b75cf38 --- /dev/null +++ b/tests/shortcircuit2.nix @@ -0,0 +1,6 @@ +# Test short-circuit evaluation +{ + and_false = false && true; + or_true = true || false; + impl_false = false -> false; +} diff --git a/tests/simple.nix b/tests/simple.nix new file mode 100644 index 0000000..68e636c --- /dev/null +++ b/tests/simple.nix @@ -0,0 +1,2 @@ +# Simple constant test +42 diff --git a/tests/simple.nixir b/tests/simple.nixir new file mode 100644 index 0000000..3e26f83 Binary files /dev/null and b/tests/simple.nixir differ diff --git a/tests/simple_op.nix b/tests/simple_op.nix new file mode 100644 index 0000000..193df0b --- /dev/null +++ b/tests/simple_op.nix @@ -0,0 +1 @@ +1 + 2 \ No newline at end of file diff --git a/tests/simple_op.nixir b/tests/simple_op.nixir new file mode 100644 index 0000000..18ffbd3 Binary files /dev/null and b/tests/simple_op.nixir differ diff --git a/tests/string_interp.nix b/tests/string_interp.nix new file mode 100644 index 0000000..b9ae519 --- /dev/null +++ b/tests/string_interp.nix @@ -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"; + } diff --git a/tests/unary.nix b/tests/unary.nix new file mode 100644 index 0000000..cc7ddab --- /dev/null +++ b/tests/unary.nix @@ -0,0 +1,6 @@ +# Test unary operators +let + x = 10; + y = true; +in + { neg = -x; not = !y; } diff --git a/tests/unary.nixir b/tests/unary.nixir new file mode 100644 index 0000000..652fabc Binary files /dev/null and b/tests/unary.nixir differ